ftp2? msgfiles 演进的文件传输





5.00/5 (5投票s)
一个用于通过网络发送文件的 .NET 客户端-服务器应用程序
引言
早在 2000 年初,当云朵只是天空中的那些蓬松的白色物体时,我在一家初创公司工作,这家公司的目标是改变文件的存储方式。我们称之为在线文件存储。您可以上传文件,然后可以在任何地方访问它们……并将它们发送给您的朋友。这不像 Napster 那样是文件共享;您不会与全世界共享您的 CD 收藏。但是,您可以轻松地将您的 CD 收藏发送给您的姐妹。
从那时起,技术已经发展和改进,您可以争辩说,文件共享或发送没有正当理由,因为网上有一个蓬勃发展的市场,用于购买和租赁对文件的访问权。每月支付费用即可访问文件城堡,然后将您想要的任何内容发送给支付相同城堡月费的其他人,问题就解决了。一切合法,一切合规。像 Napster 这样的东西永远不会被允许再次存在。
那么 msgfiles 到底是怎么回事呢?msgfiles 关注的是本地化和简单性。在您想与之传输文件的人都能访问的地方运行服务器软件。然后让他们运行客户端,您就可以轻松地相互发送文件。将其视为改进的消息式 FTP 系统,重点是该首字母缩略词的“传输”部分。它恰好介于电子邮件和 FTP 之间。电子邮件不适合处理大型文件或大量文件。FTP 不是消息式的。我很想听听您的看法。
ftp2 是一个煽动性的、宏伟的文章标题,但深入挖掘,您可能会看到为什么这并非不切实际的提议。好吧,也许有点疯狂……
顺便说一下……
我很久没写 C# 了。
有过几个 JavaScript 网页游戏:tapglasses.io 和 tiletaps.com。
我对 mscript 进行了一些迭代。
我将 C# 的一个 NoSQL 数据库移植到了 C++,4db。
很少有 .NET。
我回来的第一感觉是 C# 已经通过 .NET 库和 LINQ 具有很高的生产力。我认为新的 AI 功能太过头了,无法使用……它只会碍事。我写代码是因为我想写代码,而不是一直点击代码建议。自动完成是理所当然的,就像切好的面包一样,但 AI 必须消失,我无法足够快地禁用它。
从积极的方面来看,空值可空性一开始很烦人,但它做得很好,并且有其目的。它给我的感觉有点像 Rust,编译器站在你这边帮助你编写正确的程序。我喜欢这一点。
所以有好有坏。
我仍然认为 C# 不适合大型项目。世界正在远离这些项目,所以也许这没关系。
msgfiles 客户端应用程序
您可以在 msgfiles.io 上安装客户端并查看截图指南。我在这里不再赘述, suffice to say...
您启动客户端……
- 输入一个显示名称和您的电子邮件地址。
- 输入服务器地址,就像 FTP 服务器一样。
- 服务器会将登录代码发送到您的电子邮件地址。
- 您输入该代码,然后就可以发送和接收消息了。
发送文件……
- 按“发送文件”按钮。
- 选择要发送文件的人。
- 输入您的小消息。
- 选择要发送的文件。
- 然后客户端会将文件 ZIP 压缩……
- ……并将 ZIP 文件和消息发送到服务器。
- 服务器存储 ZIP 文件和消息……
- ……并将带有访问令牌的电子邮件发送给您的收件人。
当您收到一封说明您有文件的电子邮件时……
- 启动客户端。
- 连接到服务器。
- 按“接收文件”按钮。
- 从电子邮件中复制访问令牌并将其粘贴到客户端中。
- 客户端会显示消息来自谁以及小消息。
- 您对此进行审阅,然后决定放弃或继续。
- 客户端会下载 ZIP 文件并向您显示内容的清单。
- 您对此进行审阅,然后决定放弃或继续。
- 然后您选择将文件放在哪里,ZIP 文件将被解压到该位置。
- 任务完成!
这就是整个应用程序。两个大按钮和一些简单的对话框。非常容易!
它可以更漂亮。有人要很快去毛伊岛吗?
msgfiles 服务器应用程序
您可以在 msgfiles.io 上安装服务器并获取安装步骤和维护技巧。
代码
msgfiles 是开源的,在 GitHub 上,采用 Apache 2.0 许可证。它全部是 .NET 6 C#,在一个解决方案中包含单元测试。
这是解决方案中项目的概述。
有两个应用程序项目,client 和 server。这些项目中的代码很少,只有顶层的编排。
securenet
这个低级库封装了第三方依赖项,包括 ZIP、AES 和 JSON。它还包括核心 TLS 代码,包括自签名证书生成和 SslStream 包装函数。许多核心构建块,如 SMTP 包装类 EmailClient 和会话管理类 SessionStore 也在这里。
msglib
这个库实现了客户端 MsgClient 和服务器端 MsgRequestHandler.cs 类中的消息处理。核心 MessageStore 类也在这里。
client(客户端)
一个基本的概念验证 WinForms 应用程序,它响应 MsgClient 事件来显示进度并提示用户输入令牌和确认。我设想更漂亮的应用程序将取代这个程序;希望它们会像这个卑微的开始一样简单易用。
服务器
用于在服务器端运行程序的命令行应用程序。服务器依赖于一个设置 INI 文件以及允许和阻止列表文件。命令行提示提供了对这些文件的方便访问,服务器会拾取一些更改并立即生效。
理论上,您可以只使用 securenet 项目来开发自己的客户端-服务器应用程序。这有点像 http2,它可以带您走很远。
这就是 CodeProject……
说了这么多民间传说和项目,让我们看看代码吧!
安全网络
msgfiles 的核心是通过 SslStream 进行基本的自签名安全网络。
/// Create a self-signed cert...in six lines of code
public static X509Certificate GenCert()
{
    using (RSA rsa = RSA.Create(4096))
    {
        var distinguishedName = new X500DistinguishedName($"CN=msgfiles.io");
        var request = new CertificateRequest(distinguishedName, rsa, 
                      HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), 
                          DateTimeOffset.UtcNow.AddDays(3650));
        return new X509Certificate2
               (certificate.Export(X509ContentType.Pfx, "password"), 
               "password", X509KeyStorageFlags.MachineKeySet);
    }
}
/// Given a client TCP connection, secure communications with the server
public static Stream SecureConnectionToServer(TcpClient client)
{
    var client_stream = client.GetStream();
    var ssl_stream = 
        new SslStream
        (
            client_stream,
            false,
            (object obj, X509Certificate? cert, X509Chain? chain, 
                         SslPolicyErrors errors) => true
        );
    ssl_stream.AuthenticateAsClient("msgfiles.io");
    if (!ssl_stream.IsAuthenticated)
        throw new NetworkException("Connection to server not authenticated");
    return ssl_stream;
}
/// Given a server TCP connection, secure communications with the client
public async static Task<Stream> SecureConnectionFromClient
             (TcpClient client, X509Certificate cert)
{
    var client_stream = client.GetStream();
    var ssl_stream = new SslStream(client_stream, false, 
    (object obj, X509Certificate? cert2, X509Chain? chain, 
                 SslPolicyErrors errors) => true);
    await ssl_stream.AuthenticateAsServerAsync(cert).ConfigureAwait(false);
    if (!ssl_stream.IsAuthenticated)
        throw new NetworkException("Connection from client not authenticated");
    return ssl_stream;
}
网络载荷序列化
一旦有了安全网络,您就需要一个机制来在网络上传输载荷。我选择让载荷类似于 HTTP,并压缩头部,以便将诸如消息文本和收件人等重要内容放在其中。如果您的短消息和收件人列表加起来(压缩后)超过 64 KB……您可能更喜欢电子邮件!
public static int MaxObjectByteCount = 64 * 1024;
/// Given pretty much anything, 
/// turn it into JSON,
/// get the UTF-8 bytes,
/// compress the bytes,
/// make sure it isn't too big,
/// then send it over the stream, length-prefixed
public static void SendObject<T>(Stream stream, T headers)
{
    string json = JsonConvert.SerializeObject(headers);
    byte[] json_bytes = Utils.Compress(Encoding.UTF8.GetBytes(json));
    if (json_bytes.Length > MaxObjectByteCount)
        throw new InputException("Too much to send");
    byte[] num_bytes = BitConverter.GetBytes
           (IPAddress.HostToNetworkOrder(json_bytes.Length));
    using (var buffer = Utils.CombineArrays(num_bytes, json_bytes))
        stream.Write(buffer.GetBuffer(), 0, (int)buffer.Length);
}
public static async Task SendObjectAsync<T>(Stream stream, T headers)
...
/// Receive pretty much anything from a stream
/// Read the length, ensure it's not too much
/// Read the bytes, decompress, turn into a string, JSON parse,
/// and out comes an object
public static T ReadObject<T>(Stream stream)
{
    byte[] num_bytes = new byte[4];
    if (stream.Read(num_bytes, 0, num_bytes.Length) != num_bytes.Length)
        throw new SocketException();
    int bytes_length = 
        IPAddress.NetworkToHostOrder(BitConverter.ToInt32(num_bytes, 0));
    if (bytes_length > MaxObjectByteCount)
        throw new InputException("Too much to read");
    byte[] header_bytes = new byte[bytes_length];
    int read_yet = 0;
    while (read_yet < bytes_length)
    {
        int to_read = bytes_length - read_yet;
        int new_read = stream.Read(header_bytes, read_yet, to_read);
        if (new_read <= 0)
            throw new NetworkException("Connection closed");
        else
            read_yet += new_read;
    }
    string json = Encoding.UTF8.GetString
                  (Utils.Decompress(header_bytes, bytes_length));
    var obj = JsonConvert.DeserializeObject<T>(json);
    if (obj == null)
        throw new InputException("Input did not parse");
    else
        return obj;
}
public static async Task<T> ReadObjectAsync<T>(Stream stream)
...
访问控制
运行任何类型的服务器的一个重要主题是访问控制。我不认为将此服务器放在互联网上;这是一个内部网的用例。也许您在一个部门设置了一个服务器,而不希望其他部门对其进行干预。
因此,服务器有两个用于访问控制的文件,allow.txt 和 block.txt。您可以放入完整的电子邮件地址或带有 @ 前缀的域名。如果您的电子邮件地址不在允许列表中,或者您被阻止,您就无法连接,也无法发送任何内容。服务器已被锁定。
这是强制执行访问控制的代码
/// Manage allow and block lists of email addresses
/// to validate that a given email address is allowed access
public class AllowBlock
{
    /// Swap in new lists
    public void SetLists(HashSet<string> allow, HashSet<string> block)
    {
        try
        {
            m_rwLock.EnterWriteLock();
            m_allowList = allow;
            m_blockList = block;
        }
        finally
        {
            m_rwLock.ExitWriteLock();
        }
    }
    /// Ensure that an email address or its domain is allowed,
    /// or at least not blocked
    public void EnsureEmailAllowed(string email)
    {
        try
        {
            m_rwLock.EnterReadLock();
            // Normalize the email address
            email = Utils.GetValidEmail(email).ToLower();
            if (email.Length == 0)
                throw new InputException($"Invalid email: {email}");
            // Include the leading @, list files use this to allow/block entire domains
            string domain = email.Substring(email.IndexOf('@')).ToLower();
            // Look for specific email address blocks first, that trumps all
            if (m_blockList.Contains(email))
                throw new InputException($"Blocked email: {email}");
            // Check for specific email address being allowed, this trumps domains
            if (m_allowList.Contains(email))
                return;
            // Check for a whole blocked domain
            if (m_blockList.Contains(domain))
                throw new InputException($"Blocked domain: {email}");
            // Allow a whole domain
            if (m_allowList.Contains(domain))
                return;
            // Failing all of that, if there is an allow list,
            // the email is not on any of them, so they're blocked by default
            if (m_allowList.Count > 0)
                throw new InputException($"Not allowed: {email}");
            // no allow list, not blocked -> allowed
        }
        finally
        {
            m_rwLock.ExitReadLock();
        }
    }
    private HashSet<string> m_allowList = new HashSet<string>();
    private HashSet<string> m_blockList = new HashSet<string>();
    private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim();
}
发送电子邮件
msgfiles 的另一个主题是发送电子邮件。System.Net.SmtpClient 很有意思。它不是线程安全的。它有一个非 async/await 的 SendAsync 函数。文档建议底层有网络连接池,所以我们为每条消息创建一个 SmtpClient,并使用“发送并祈祷”的 SendAsync 函数。
/// SmtpClient wrapper class
public class EmailClient
{
    public EmailClient(string server, int port, string username, string password)
    {
        m_server = server;
        m_port = port;
        m_credential = new NetworkCredential(username, password);
    }
    public void SendEmail
    (
        string from, // display <email> or just email
        Dictionary<string, string> toAddrs, // email -> display
        string subject,
        string body
    )
    {
        var fromKvp = Utils.ParseEmail(from);
        var mail_message = new MailMessage();
        mail_message.From = new MailAddress(fromKvp.Key, fromKvp.Value);
        foreach (var toKvp in toAddrs)
            mail_message.To.Add(new MailAddress(toKvp.Key, toKvp.Value));
        mail_message.Subject = subject;
        mail_message.Body = body;
        SmtpClient client = new SmtpClient(m_server, m_port);
        client.Credentials = m_credential;
        client.DeliveryMethod = SmtpDeliveryMethod.Network;
        client.EnableSsl = true;
        client.SendAsync(mail_message, null);
    }
    private string m_server;
    private int m_port;
    private NetworkCredential m_credential;
}
ZIP 文件处理
ZIP 文件是此应用程序的核心。我创建了一些围绕 DotNetZip NuGet 包的包装函数。
/// Create a ZIP file from files and folders to include
public static void CreateZip(IClientApp app, string zipFilePath, IEnumerable<string> paths)
{
    using (var zip = new Ionic.Zip.ZipFile(zipFilePath))
    {
        zip.CompressionLevel = Ionic.Zlib.CompressionLevel.BestSpeed;
        string lastZipCurrentFilename = "";
        zip.SaveProgress +=
            (object? sender, Ionic.Zip.SaveProgressEventArgs e) =>
            {
                if (e.CurrentEntry != null && 
                    e.CurrentEntry.FileName != lastZipCurrentFilename)
                {
                    lastZipCurrentFilename = e.CurrentEntry.FileName;
                    app.Log(lastZipCurrentFilename);
                }
                if (e.TotalBytesToTransfer > 0)
                    app.Progress((double)e.BytesTransferred / e.TotalBytesToTransfer);
            };
        foreach (var path in paths)
        {
            if (File.Exists(path))
                zip.AddFile(path, "");
            else if (Directory.Exists(path))
                zip.AddDirectory(path, Path.GetFileName(path));
            else
                throw new InputException($"Item to send not found: {path}");
        }
        zip.Save();
    }
}
/// Summarize the contents of a ZIP file for the benefit of having an idea
/// whether they are what is expected, and safe
public static string ManifestZip(string zipFilePath)
{
    int file_count = 0;
    long total_byte_count = 0;
    StringBuilder entry_lines = new StringBuilder();
            
    Dictionary<string, int> ext_counts = new Dictionary<string, int>();
            
    using (var zip_file = new Ionic.Zip.ZipFile(zipFilePath))
    {
        foreach (var zip_entry in zip_file.Entries)
        {
            if (zip_entry.IsDirectory)
                continue;
            string size_str = 
                Utils.ByteCountToStr(zip_entry.UncompressedSize);
            entry_lines.AppendLine($"{zip_entry.FileName} ({size_str})");
            string ext = Path.GetExtension(zip_entry.FileName).ToUpper();
            if (ext_counts.ContainsKey(ext))
                ++ext_counts[ext];
            else
                ext_counts[ext] = 1;
            ++file_count;
            total_byte_count += zip_entry.UncompressedSize;
        }
    }
    string ext_summary =
        "File Types:\r\n" +
        string.Join
        (
            "\r\n",
            ext_counts
                .Select(kvp => $"{kvp.Key.Trim('.')}: {kvp.Value}")
                .OrderBy(str => str)
        );
    return
        $"Files: {file_count}" +
        $" - " +
        $"Total: {Utils.ByteCountToStr(total_byte_count)}" +
        $"\r\n\r\n" +
        $"{ext_summary}" +
        $"\r\n\r\n" +
        $"{entry_lines}";
}
/// Extract a ZIP file's contents into an output directory
public static void ExtractZip
       (IClientApp app, string zipFilePath, string extractionDirPath)
{
    using (var zip = new Ionic.Zip.ZipFile(zipFilePath))
    {
        string lastZipCurrentFilename = "";
        zip.ExtractProgress +=
            (object? sender, Ionic.Zip.ExtractProgressEventArgs e) =>
            {
                if (e.CurrentEntry != null && 
                    e.CurrentEntry.FileName != lastZipCurrentFilename)
                {
                    lastZipCurrentFilename = e.CurrentEntry.FileName;
                    app.Log(lastZipCurrentFilename);
                }
                if (e.TotalBytesToTransfer > 0)
                    app.Progress((double)e.BytesTransferred / e.TotalBytesToTransfer);
            };
        zip.ExtractAll(extractionDirPath);
    }
}
客户端消息发送
再往上层是发送消息的客户端代码。
/// Send a message with files to recipients
public bool SendMsg
(
    IEnumerable<string> to, 
    string message, 
    IEnumerable<string> paths
)
{
    using (var temp_file_use = new TempFileUse(".zip"))
    {
        string zip_file_path = temp_file_use.FilePath;
        App.Log("Adding files to package...");
        Utils.CreateZip(App, zip_file_path, paths);
        App.Log("Scanning package...");
        string zip_hash;
        using (var fs = File.OpenRead(zip_file_path))
            zip_hash = Utils.HashStream(fs);
        App.Log("Sending message...");
        long zip_file_size_bytes = new FileInfo(zip_file_path).Length;
        var send_request =
            new ClientRequest()
            {
                version = 1,
                verb = "POST",
                contentLength = zip_file_size_bytes,
                headers = new Dictionary<string, string>()
                {
                    { "to", string.Join("; ", to) },
                    { "message", message },
                    { "hash", zip_hash }
                }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, send_request);
        App.Log("Sending package...");
        using (var zip_file_stream = File.OpenRead(zip_file_path))
        {
            long sent_yet = 0;
            byte[] buffer = new byte[64 * 1024];
            while (sent_yet < zip_file_size_bytes)
            {
                int to_read = 
                (int)Math.Min(zip_file_size_bytes - sent_yet, buffer.Length);
                int read = zip_file_stream.Read(buffer, 0, to_read);
                if (App.Cancelled)
                    return false;
                if (ServerStream == null)
                    return false;
                ServerStream.Write(buffer, 0, read);
                sent_yet += read;
                App.Progress((double)sent_yet / zip_file_size_bytes);
                if (App.Cancelled)
                    return false;
            }
        }
        if (App.Cancelled)
            return false;
        App.Log("Receiving response...");
        using (var send_response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {send_response.ResponseSummary}");
            if (send_response.statusCode / 100 != 2)
                throw send_response.CreateException();
        }
        return true;
    }
}
客户端消息接收
这是客户端接收消息的代码。
/// Given a message token, get a message for the current user
/// Returns true if getting the message succeeded
/// Sets shouldDelete to true if the user canceled the operation
public bool GetMessage(string msgToken, out bool shouldDelete)
{
    shouldDelete = false;
    {
        App.Log("Sending GET msg request...");
        var request =
            new ClientRequest()
            {
                version = 1,
                verb = "GET",
                headers =
                    new Dictionary<string, string>()
                    { { "token", msgToken }, { "part", "msg"} }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, request);
        if (App.Cancelled)
            return false;
        App.Log("Receiving GET msg response...");
        if (ServerStream == null)
            return false;
        using (var response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {response.ResponseSummary}");
            if (response.statusCode / 100 != 2)
                throw response.CreateException();
            msg? m = JsonConvert.DeserializeObject<msg>(response.headers["msg"]);
            string status = m == null ? "(null)" : m.from;
            App.Log($"Message: {status}");
            if (m == null)
                return false;
            else
                msgToken = m.token;
            if (!App.ConfirmDownload(m.from, m.message, out shouldDelete))
                return false;
        }
    }
    {
        App.Log("Sending GET file request...");
        var request =
            new ClientRequest()
            {
                version = 1,
                verb = "GET",
                headers =
                    new Dictionary<string, string>()
                    { { "token", msgToken }, { "part", "file"} }
            };
        if (ServerStream == null)
            return false;
        SecureNet.SendObject(ServerStream, request);
        if (App.Cancelled)
            return false;
        App.Log("Receiving GET file response...");
        if (ServerStream == null)
            return false;
        using (var response = SecureNet.ReadObject<ServerResponse>(ServerStream))
        {
            App.Log($"Server Response: {response.ResponseSummary}");
            if (response.statusCode / 100 != 2)
                throw response.CreateException();
            using (var temp_file_use = new TempFileUse(".zip"))
            {
                string temp_file_path = temp_file_use.FilePath;
                App.Log($"Downloading files...");
                if (App.Cancelled)
                    return false;
                using (var fs = File.OpenWrite(temp_file_path))
                {
                    long total_to_read = response.contentLength;
                    long read_yet = 0;
                    byte[] buffer = new byte[64 * 1024];
                    while (read_yet < total_to_read)
                    {
                        int to_read = (int)Math.Min(total_to_read - read_yet, 
                                       buffer.Length);
                        if (ServerStream == null)
                            return false;
                        int read = ServerStream.Read(buffer, 0, to_read);
                        if (App.Cancelled)
                            return false;
                        if (read == 0)
                            throw new NetworkException("Connection lost");
                        fs.Write(buffer, 0, read);
                        if (App.Cancelled)
                            return false;
                        read_yet += read;
                        App.Progress((double)read_yet / total_to_read);
                    }
                }
                App.Log($"Scanning downloaded files...");
                if (App.Cancelled)
                    return false;
                string local_hash;
                using (var fs = File.OpenRead(temp_file_path))
                    local_hash = Utils.HashStream(fs);
                if (App.Cancelled)
                    return false;
                if (local_hash != response.headers["hash"])
                    throw new NetworkException("File transmission error");
                App.Log($"Examining downloaded files...");
                string manifest = Utils.ManifestZip(temp_file_path);
                if (App.Cancelled)
                    return false;
                string extraction_dir_path = "";
                if (!App.ConfirmExtraction(manifest, out shouldDelete, 
                                           out extraction_dir_path))
                    return false;
                App.Log($"Saving downloaded files...");
                Utils.ExtractZip(App, temp_file_path, extraction_dir_path);
                App.Log($"All done.");
                return true;
            }
        }
    }
}
服务器消息发送
在服务器端,这是处理用户发送消息的代码。
private async Task<ServerResponse> HandleSendRequestAsync
        (ClientRequest request, HandlerContext ctxt)
{
    // Unpack the message
    Utils.NormalizeDict
    (
        request.headers,
        new[]
        { "to", "message", "packageHash" }
    );
    string to = request.headers["to"];
    if (to == "")
        throw new InputException("Header missing: to");
    string message = request.headers["message"];
    if (message == "")
        throw new InputException("Header missing: message");
    long package_size_bytes = request.contentLength;
    if
    (
        MaxSendPayloadMB > 0
        &&
        package_size_bytes / 1024 / 1024 > MaxSendPayloadMB
    )
    {
        throw new InputException("Header invalid: package too big");
    }
    string sent_zip_hash = request.headers["hash"];
    if (sent_zip_hash == "")
        throw new InputException("Header missing: hash");
    Log(ctxt, $"Sending: To: {to}");
    using (var temp_file_use = new TempFileUse(".zip"))
    {
        string stored_file_path = "";
        string temp_zip_file_path = temp_file_use.FilePath;
        try
        {
            Log(ctxt, $"Saving ZIP: {temp_zip_file_path}");
            using (var zip_file_stream = File.OpenWrite(temp_zip_file_path))
            {
                long written_yet = 0;
                byte[] buffer = new byte[64 * 1024];
                while (written_yet < package_size_bytes)
                {
                    int to_read = (int)Math.Min
                                  (package_size_bytes - written_yet, buffer.Length);
                    int read = await ctxt.ConnectionStream.ReadAsync
                               (buffer, 0, to_read).ConfigureAwait(false);
                    if (read == 0)
                        throw new NetworkException("Connection lost");
                    await zip_file_stream.WriteAsync
                          (buffer, 0, read).ConfigureAwait(false);
                    written_yet += read;
                }
            }
            Log(ctxt, $"Hashing ZIP");
            string local_zip_hash;
            using (var zip_file_stream = File.OpenRead(temp_zip_file_path))
                local_zip_hash = await Utils.HashStreamAsync
                                 (zip_file_stream).ConfigureAwait(false);
            if (local_zip_hash != sent_zip_hash)
                throw new InputException("Received file contents do not match 
                                          what was sent");
            Log(ctxt, $"Storing ZIP");
            stored_file_path = m_fileStore.StoreFile(temp_zip_file_path);
            File.Delete(temp_zip_file_path);
            temp_zip_file_path = "";
            temp_file_use.Clear();
            Log(ctxt, $"Storing messages");
            string email_from = $"{ctxt.Auth["display"]} <{ctxt.Auth["email"]}>";
            var toos = to.Split(';').Select(t => t.Trim()).Where(t => t.Length > 0);
            foreach (var too in toos)
            {
                string token = 
                    m_msgStore.StoreMessage
                    (
                        new msg()
                        {
                            from = email_from,
                            to = too,
                            message = message
                        },
                        stored_file_path,
                        local_zip_hash
                    );
                    Log(ctxt, $"Sending email");
                    ctxt.App.SendDeliveryMessage
                    (
                        email_from,
                        too,
                        message,
                        token
                    );
            }
            stored_file_path = "";
            return
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 200,
                    statusMessage = "OK"
                };
        }
        finally
        {
            if (stored_file_path != "" && File.Exists(stored_file_path))
                File.Delete(stored_file_path);
        }
    }
}
服务器消息接收
这是服务器处理用户接收消息的代码。
private async Task<ServerResponse> HandleGetRequestAsync
              (ClientRequest request, HandlerContext ctxt)
{
    string to = ctxt.Auth["email"];
    m_allowBlock.EnsureEmailAllowed(to);
    Utils.NormalizeDict(request.headers, new[] { "token", "part" });
    string token = request.headers["token"];
    if (token.Length == 0)
        throw new InputException("Header missing: token");
    string part_to_get = request.headers["part"];
    if (part_to_get.Length == 0)
        throw new InputException("Header missing: part");
    bool get_msg = false, get_file = false;
    if (part_to_get == "msg")
        get_msg = true;
    else if (part_to_get == "file")
        get_file = true;
    else
        throw new InputException("Invalid header: part");
    Log(ctxt, $"Get Message: {to} - {token} - {part_to_get}");
    string package_file_path, package_file_hash;
    var msg = 
        m_msgStore.GetMessage
        (to, token, out package_file_path, out package_file_hash);
    if (get_msg)
    {
        if (msg == null)
        {
            var response_404 =
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 404,
                    statusMessage = "Message Not Found"
                };
            return response_404;
        }
        var response =
            new ServerResponse()
            {
                version = 1,
                statusCode = 200,
                statusMessage = "OK",
                headers =
                    new Dictionary<string, string>()
                    { { "msg", JsonConvert.SerializeObject(msg) } },
            };
        await Task.FromResult(0);
        return response;
    }
    else if (get_file)
    {
        if (!File.Exists(package_file_path))
        {
            var response_404 =
                new ServerResponse()
                {
                    version = 1,
                    statusCode = 404,
                    statusMessage = "File Not Found"
                };
            return response_404;
        }
        var response =
            new ServerResponse()
            {
                version = 1,
                statusCode = 200,
                statusMessage = "OK",
                contentLength = new FileInfo(package_file_path).Length,
                headers =
                    new Dictionary<string, string>()
                    { { "hash", package_file_hash } },
                streamToSend = File.OpenRead(package_file_path)
            };
        await Task.FromResult(0);
        return response;
    }
    else
        throw new InputException("Invalid header: part");
}
服务器应用程序启动
仍然在服务器端,这是服务器的启动代码,您可以看到所有内容是如何协同工作的。
public ServerApp()
{
    string settings_file_path = Path.Combine(AppDocsDirPath, "settings.ini");
    if (!File.Exists(settings_file_path))
        throw new InputException
              ($"settings.ini file does not exist in {AppDocsDirPath}");
            
    m_settings = new Settings(settings_file_path);
    if (!int.TryParse
    (
        m_settings.Get("application", "MaxSendPayloadMB"),
        out MsgRequestHandler.MaxSendPayloadMB
    ))
    {
        throw new InputException("Invalid setting: MaxSendPayloadMB");
    }
    if (!int.TryParse
    (
        m_settings.Get("application", "ReceiveTimeoutSeconds"),
        out Server.ReceiveTimeoutSeconds
    ))
    {
        throw new InputException("Invalid setting: ReceiveTimeoutSeconds");
    }
    if (!int.TryParse
    (
        m_settings.Get("application", "ServerPort"),
        out ServerPort
    ))
    {
        throw new InputException("Invalid setting: ServerPort");
    }
    m_settingsWatcher = new FileSystemWatcher(AppDocsDirPath, "*.ini");
    m_settingsWatcher.Changed += SettingsWatcher_Changed;
    m_settingsWatcher.Created += SettingsWatcher_Changed;
    m_settingsWatcher.Deleted += SettingsWatcher_Changed;
    SettingsWatcher_Changed(new object(), 
            new FileSystemEventArgs(WatcherChangeTypes.All, "", null));
    m_txtFilesWatcher = new FileSystemWatcher(AppDocsDirPath, "*.txt");
    m_txtFilesWatcher.Changed += TextWatcher_Changed;
    m_txtFilesWatcher.Created += TextWatcher_Changed;
    m_txtFilesWatcher.Deleted += TextWatcher_Changed;
    TextWatcher_Changed(new object(), 
                new FileSystemEventArgs(WatcherChangeTypes.All, "", null));
    m_sessions = new SessionStore(Path.Combine(AppDocsDirPath, "sessions.db"));
    m_messageStore = new MessageStore(Path.Combine(AppDocsDirPath, "messages.db"));
    m_fileStore = new FileStore(m_settings.Get("application", "FileStoreDir"));
    m_logStore = new LogStore(Path.Combine(AppDocsDirPath, "logs"), "raw");
    m_accessStore = new LogStore(Path.Combine(AppDocsDirPath, "logs"), "access");
    string mail_server = m_settings.Get("application", "MailServer");
    if (string.IsNullOrWhiteSpace(mail_server))
        throw new InputException("Invalid setting: MailServer");
    int mail_port;
    if (!int.TryParse
    (
        m_settings.Get("application", "MailPort"),
        out mail_port
    ))
    {
        throw new InputException("Invalid setting: MailPort");
    }
    m_emailClient =
        new EmailClient
        (
            mail_server,
            mail_port,
            m_settings.Get("application", "MailUsername"),
            m_settings.Get("application", "MailPassword")
        );
    m_maintenanceTimer = new Timer(MaintenanceTimer, null, 0, 60 * 1000);
    var to_kvp = Utils.ParseEmail(m_settings.Get("application", "MailAdminAddress"));
    m_emailClient.SendEmail
    (
        m_settings.Get("application", "MailFromAddress"),
        new Dictionary<string, string>() { { to_kvp.Key, to_kvp.Value } },
        "Server Started Up",
        "So far so good..."
    );
}
允许阻止列表文件加载器
最后,这是用于加载允许和阻止列表文本文件的函数。LINQ 可能很慢,但它确实很漂亮,所以在速度不是首要考虑的情况下,尽管使用它吧!
private HashSet<string> LoadFileList(string fileName)
{
    string file_path = Path.Combine(AppDocsDirPath, fileName);
    if (File.Exists(file_path))
    {
        return
            new HashSet<string>
            (
                File.ReadAllLines(file_path)
                .Select(e => e.Trim().ToLower())
                .Where(e => e.Length > 0 && e[0] != '#')
            );
    }
    else
        return new HashSet<string>();
}
结论
好了,这是一次 .NET 客户端-服务器内部网应用程序的快速浏览,希望您喜欢。
我最近没怎么写 C# / .NET,所以请用新颖的方法来启发我以及我这些恐龙般的同僚。
如果您在阅读完这一切后,认为这是您想尝试的东西,请发送一两个您希望访问演示服务器的电子邮件地址,并附上一句简短的“我不是机器人”消息到 contact@msgfiles.io,我将为您设置。只发送一个地址也可以,您可以像电子邮件一样将文件发送给自己。您发送的地址应来自与您要访问的地址相同域的地址。
历史
- 2022 年 9 月 11 日:初始版本


