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 日:初始版本