5using System.Threading.Tasks;
23 private static readonly
char[] invalidCharacters = Path.GetInvalidFileNameChars();
32 public const string Namespace =
"urn:xmpp:http:upload:0";
64 this.settings = Settings;
66 this.DeleteUploadedFiles();
69 Directory.CreateDirectory(Settings.
FileFolder);
74 this.files.Removed += this.Files_Removed;
78 this.
RegisterIqSetHandler(
"prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler,
true);
80 this.
RegisterIqSetHandler(
"prepare", InternalTransferNamespace, this.PrepareInternalTransferHandler,
true);
83 private void DeleteUploadedFiles()
85 if (Directory.Exists(
this.settings.FileFolder))
89 Directory.Delete(this.settings.FileFolder,
true);
107 this.
UnregisterIqSetHandler(
"prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler,
true);
109 this.
UnregisterIqSetHandler(
"prepare", InternalTransferNamespace, this.PrepareInternalTransferHandler,
true);
111 this.statusByClient?.
Dispose();
112 this.statusByClient =
null;
117 this.DeleteUploadedFiles();
131 Xml.Append(
"<identity category='store' type='file' name='HTTP File Upload' />");
133 return Task.CompletedTask;
141 Xml.Append(
"<x type='result' xmlns='jabber:x:data'>");
142 Xml.Append(
"<field var='FORM_TYPE' type='hidden'>");
143 Xml.Append(
"<value>");
145 Xml.Append(
"</value>");
146 Xml.Append(
"</field>");
147 Xml.Append(
"<field var='max-file-size'>");
148 Xml.Append(
"<value>");
149 Xml.Append(this.settings.MaxFileSize.ToString());
150 Xml.Append(
"</value>");
151 Xml.Append(
"</field>");
154 return Task.CompletedTask;
157 private async Task RequestHandler(
object Sender,
IqEventArgs e)
163 if (!await this.IsFileOk(FileName, ContentType, Size, e))
172 string Extension = Path.GetExtension(FileName).ToLower();
173 if (Extension.StartsWith(
'.'))
174 Extension = Extension[1..];
176 string BaseContentType = ContentType;
184 ExtensionDecoder.GetType() != ContentDecoder.GetType())
191 Array.IndexOf(ContentDecoder.FileExtensions, Extension) < 0)
200 if (this.statusByClient.
TryGetValue(BareJid, out UploadInformation Status) &&
201 Status.TryGetSpecialFile(FileName, ContentType, Size, out SpecialFile SpecialFile))
203 Purpose = SpecialFile.Purpose;
213 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
220 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
225 IAccount Account = await this.
Server.GetAccount(UserName);
228 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
232 if (Size > this.settings.MaxFileSize)
234 await e.
IqError(
"modify",
"<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />" +
235 "<file-too-large xmlns='urn:xmpp:http:upload:0'>" +
236 "<max-file-size>" + this.settings.MaxFileSize.ToString() +
"</max-file-size>" +
237 "</file-too-large>", e.
To,
"File too large. The maximum file size is " +
this.settings.MaxFileSize.
ToString() +
251 Status =
new UploadInformation(BareJid);
252 this.statusByClient[BareJid] = Status;
255 switch (Status.CanUploadFile(Size, Purpose,
this.settings))
257 case CanUploadResult.FileQuotaReached:
259 this.settings.MaxFilesPerMinute.
ToString() +
" files per minute.",
"en");
262 case CanUploadResult.ByteQuotaReached:
264 this.settings.MaxBytesPerMinute.
ToString() +
" bytes per minute.",
"en");
267 case CanUploadResult.Permitted:
268 StringBuilder Xml =
new StringBuilder();
270 string Resource =
"/" + Id +
"/" + HttpUtility.UrlEncode(FileName);
272 string PutUrl = this.settings.HttpFolder + Resource;
273 string GetUrl = PutUrl;
280 FilePath = Path.Combine(this.settings.FileFolder, BareJid, Id +
".bin");
284 if (FileName.EndsWith(
".key", StringComparison.InvariantCultureIgnoreCase))
285 FilePath = Path.Combine(this.settings.KeyFolder, BareJid, FileName);
287 FilePath = Path.Combine(this.settings.BackupFolder, BareJid, FileName);
291 DateTime Today = DateTime.Today;
292 string EncryptedResource = Path.Combine(Today.Year.ToString(
"D4"),
293 Today.Month.ToString(
"D2"), Today.Day.ToString(
"D2"), Id +
".bin");
294 FilePath = Path.Combine(this.settings.EncryptedStorageFolder, EncryptedResource);
295 GetUrl = this.settings.EncryptedStorageRoot +
"/" + EncryptedResource.Replace(Path.DirectorySeparatorChar,
'/');
299 Today = DateTime.Today;
300 string PubSubResource = Path.Combine(Today.Year.ToString(
"D4"),
301 Today.Month.ToString(
"D2"), Today.Day.ToString(
"D2"), FileName);
302 FilePath = Path.Combine(this.settings.PubSubStorageFolder, PubSubResource);
303 if (File.Exists(FilePath))
309 GetUrl = this.settings.PubSubStorageRoot +
"/" + PubSubResource.Replace(Path.DirectorySeparatorChar,
'/');
313 FilePath = Path.Combine(this.settings.InternalTransferFolder, Id +
".bin");
317 this.files.
Add(Resource,
new FileInfo()
330 Xml.Append(
"<slot xmlns='urn:xmpp:http:upload:0'>");
331 Xml.Append(
"<put url='");
333 Xml.Append(
"'><header name='X-Key'>");
335 Xml.Append(
"</header></put>");
336 Xml.Append(
"<get url='");
338 Xml.Append(
"'/></slot>");
345 private async Task<bool> IsFileOk(
string FileName,
string ContentType,
long Size, IqEventArgs e)
349 await e.IqErrorBadRequest(e.To,
"Invalid size.",
"en");
353 if (
string.IsNullOrEmpty(FileName) ||
354 FileName.Contains(
"\\") ||
355 FileName.Contains(
"/") ||
356 FileName.Contains(
"..") ||
357 FileName.IndexOfAny(invalidCharacters) >= 0)
359 await e.IqErrorBadRequest(e.To,
"Invalid file name.",
"en");
363 if (
string.IsNullOrEmpty(ContentType))
365 await e.IqErrorBadRequest(e.To,
"Invalid content type.",
"en");
372 private Task PrepareBackupHandler(
object Sender, IqEventArgs e)
377 private Task PrepareEncryptedStorageHandler(
object Sender, IqEventArgs e)
382 private Task PreparePubSubStorageHandler(
object Sender, IqEventArgs e)
387 private Task PrepareInternalTransferHandler(
object Sender, IqEventArgs e)
389 return this.Prepare(e,
FilePurpose.InternalTransfer);
392 private async Task Prepare(IqEventArgs e,
FilePurpose Purpose)
399 if (!await this.IsFileOk(FileName, ContentType, Size, e))
402 if (!this.statusByClient.
TryGetValue(BareJid, out UploadInformation Status))
404 Status =
new UploadInformation(BareJid);
405 this.statusByClient[BareJid] = Status;
408 Status.AddSpecialFile(FileName, ContentType, Size, Purpose);
410 await e.IqResult(
string.Empty, e.To);
419 File.Exists(e.
Value.FilePath))
421 File.Delete(e.
Value.FilePath);
423 string Folder = Path.GetDirectoryName(e.
Value.FilePath);
425 if (Directory.GetFiles(Folder,
"*.*", SearchOption.AllDirectories).Length == 0)
426 Directory.Delete(Folder);
434 return Task.CompletedTask;
447 public async Task<bool>
DataUploaded(
string Resource,
string Key, Stream Data,
452 Log.
Error(
"PUT/PATCH request rejected. Resource slot not found.",
453 new KeyValuePair<string, object>(
"Resource", Resource),
454 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
461 Log.
Error(
"PUT/PATCH request rejected. Invalid key.",
462 new KeyValuePair<string, object>(
"Resource", Resource),
463 new KeyValuePair<string, object>(
"Key", Key),
464 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
471 Log.
Error(
"PUT/PATCH request rejected. Invalid Content-Type.",
472 new KeyValuePair<string, object>(
"Resource", Resource),
473 new KeyValuePair<string, object>(
"Key", Key),
474 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
481 Log.
Error(
"PUT/PATCH request rejected. File has already been put.",
482 new KeyValuePair<string, object>(
"Resource", Resource),
483 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
488 if (Interval.
Last - Interval.
First + 1 != Data.Length)
490 Log.
Error(
"PUT/PATCH request rejected. Range error.",
491 new KeyValuePair<string, object>(
"Resource", Resource),
492 new KeyValuePair<string, object>(
"Actual", Data.Length),
493 new KeyValuePair<string, object>(
"Expected",
FileInfo.
Size),
494 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
501 Log.
Error(
"PUT/PATCH request rejected. File size mismatch.",
502 new KeyValuePair<string, object>(
"Resource", Resource),
503 new KeyValuePair<string, object>(
"Actual", Data.Length),
504 new KeyValuePair<string, object>(
"Expected",
FileInfo.
Size),
505 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
513 if (!Directory.Exists(Folder))
514 Directory.CreateDirectory(Folder);
520 if (Interval.
First > 0)
522 if (Dest.Length < Interval.
First)
524 Dest.Position = Dest.Length;
526 long Rest = Interval.First - Dest.Length;
527 byte[] Buffer =
new byte[Math.Min(Rest, 65536)];
531 int c = (int)Math.Min(Rest, 65536);
532 await Dest.WriteAsync(Buffer, 0, c);
537 Dest.Position = Interval.
First;
540 await Data.CopyToAsync(Dest);
544 FileInfo.HasBeenPut =
true;
553 byte[] Data2 = await Dest.ReadAllAsync();
559 Error = Decoded.
Error.Message;
566 if (!
string.IsNullOrEmpty(Error))
568 Log.
Error(
"PUT/PATCH request rejected. Uploaded content could not be " +
569 "decoded properly: " + Error,
570 new KeyValuePair<string, object>(
"Resource", Resource),
571 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
575 this.files.
Remove(Resource);
586 new KeyValuePair<string, object>(
"Size", Data.Length),
587 new KeyValuePair<string, object>(
"RemoteEndPoint", RemoteEndPoint));
601 public bool TryGetFile(
string Resource, out
string FilePath, out
string ContentType,
Contains information about a response to a content request.
bool HasError
If an error occurred.
Exception Error
Error response.
Static class managing encoding and decoding of internet content.
static bool Decodes(string ContentType, out Grade Grade, out IContentDecoder Decoder)
If an object with a given content type can be decoded.
static bool ParseContentType(ref string ContentType, out Encoding Encoding, out KeyValuePair< string, string >[] Fields)
Parses a Content-Type, providing the base Content-Type, character encoding, if any,...
static Task< ContentResponse > DecodeAsync(string ContentType, byte[] Data, Encoding Encoding, KeyValuePair< string, string >[] Fields, Uri BaseUri)
Decodes an object.
static bool TryGetContentType(string FileExtension, out string ContentType)
Tries to get the content type of an item, given its file extension.
Helps with common XML-related tasks.
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
static string Encode(string s)
Encodes a string for use in XML.
Static class managing the application event log. Applications and services log events on this static ...
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
static void Error(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an error event.
static void Informational(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an informational event.
Represents a content range in a ranged HTTP request or response.
long First
First byte of interval.
long Last
Last byte of interval, inclusive.
Base class for components.
void RegisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Set handler.
CaseInsensitiveString Subdomain
Subdomain name.
void RegisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Get handler.
XmppServer Server
XMPP Server.
bool UnregisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Get handler.
bool UnregisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Set handler.
Information about a file upload.
string Url
GET URL for uploaded file.
string Key
Key authorizing access to file upload.
string ContentType
Content-Type of uploaded content.
bool HasBeenPut
If file has been put.
string FilePath
Path to uploaded file.
CaseInsensitiveString Jid
JID uploading the file
FilePurpose Purpose
Purpose of file.
Implements HTTP File Upload support as an XMPP component: https://xmpp.org/extensions/xep-0363....
const string EncryptedStorageNamespace
http://waher.se/Schema/EncryptedStorage.xsd
override bool SupportsAccounts
If the component supports accounts (true), or if the subdomain name is the only valid address.
const string Namespace
urn:xmpp:http:upload:0
const string BackupNamespace
http://waher.se/Schema/Backups.xsd
const string InternalTransferNamespace
http://waher.se/Schema/InternalTransfer.xsd
HttpFileUploadComponent(XmppServer Server, CaseInsensitiveString Subdomain, HttpFileUploadSettings Settings)
Implements HTTP File Upload support as an XMPP component: https://xmpp.org/extensions/xep-0363....
const string PubSubNamespace
http://waher.se/Schema/PubSub.xsd
override void Dispose()
IDisposable.Dispose
bool TryGetFile(string Resource, out string FilePath, out string ContentType, out FilePurpose Purpose)
Tries to get a file.
override Task AppendServiceDiscoveryFeatures(StringBuilder Xml, IqEventArgs e, string Node)
Component.AppendServiceDiscoveryFeatures(StringBuilder, IqEventArgs, string)
async Task< bool > DataUploaded(string Resource, string Key, Stream Data, ContentByteRangeInterval Interval, string ContentType, string RemoteEndPoint)
Attempts to put a file in the file folder.
override Task AppendServiceDiscoveryIdentities(StringBuilder Xml, IqEventArgs e, string Node)
Component.AppendServiceDiscoveryIdentities(StringBuilder, IqEventArgs, string)
HTTP File Upload settings.
string FileFolder
Folder where uploaded files reside;
Event arguments for IQ queries.
XmppAddress From
From address attribute
Task IqResult(string Xml, string From)
Returns a response to the current request.
Task IqErrorResourceConstraint(XmppAddress From, string ErrorText, string Language)
Returns a resource-constraint error.
XmlElement Query
Query element, if found, null otherwise.
Task IqErrorNotAllowed(XmppAddress From, string ErrorText, string Language)
Returns a not-allowed error.
XmppAddress To
To address attribute
async Task IqError(string ErrorType, string Xml, XmppAddress From, string ErrorText, string Language)
Returns an error response to the current request.
Task IqErrorConflict(XmppAddress From, string ErrorText, string Language)
Returns a conflict error.
Task IqErrorBadRequest(XmppAddress From, string ErrorText, string Language)
Returns a bad-request error.
Task IqErrorForbidden(XmppAddress From, string ErrorText, string Language)
Returns a forbidden error.
override string ToString()
object.ToString()
CaseInsensitiveString Address
XMPP Address
bool IsDomain
If the Address is a domain.
CaseInsensitiveString BareJid
Bare JID
IXmppServerPersistenceLayer PersistenceLayer
Reference to persistence layer
bool IsServerDomain(CaseInsensitiveString Domain, bool IncludeAlternativeDomains)
Checks if a domain is the server domain, or optionally, an alternative domain.
string NewId(int NrBytes)
Generates a new ID.
Represents a case-insensitive string.
string Value
String-representation of the case-insensitive string. (Representation is case sensitive....
int IndexOf(CaseInsensitiveString value, StringComparison comparisonType)
Reports the zero-based index of the first occurrence of the specified string in the current System....
CaseInsensitiveString Substring(int startIndex, int length)
Retrieves a substring from this instance. The substring starts at a specified character position and ...
Implements an in-memory cache.
void Dispose()
IDisposable.Dispose
bool Remove(KeyType Key)
Removes an item from the cache.
bool TryGetValue(KeyType Key, out ValueType Value)
Tries to get a value from the cache.
void Add(KeyType Key, ValueType Value)
Adds an item to the cache.
Event arguments for cache item removal events.
ValueType Value
Value of item that was removed.
Basic interface for Internet Content decoders. A class implementing this interface and having a defau...
Task< bool > IsPermitted(CaseInsensitiveString BareJid, string Setting)
Checks if a feature is permitted for a Bare JID.
FilePurpose
Purpose of file uploaded
ContentType
DTLS Record content type.