2using System.Collections.Generic;
5using System.Threading.Tasks;
21 private static readonly
char[] invalidCharacters = Path.GetInvalidFileNameChars();
30 public const string Namespace =
"urn:xmpp:http:upload:0";
57 this.settings = Settings;
59 this.DeleteUploadedFiles();
62 Directory.CreateDirectory(Settings.
FileFolder);
67 this.files.Removed += this.Files_Removed;
71 this.
RegisterIqSetHandler(
"prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler,
true);
75 private void DeleteUploadedFiles()
77 if (Directory.Exists(
this.settings.FileFolder))
81 Directory.Delete(this.settings.FileFolder,
true);
99 this.
UnregisterIqSetHandler(
"prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler,
true);
102 this.statusByClient?.
Dispose();
103 this.statusByClient =
null;
108 this.DeleteUploadedFiles();
122 Xml.Append(
"<identity category='store' type='file' name='HTTP File Upload' />");
124 return Task.CompletedTask;
132 Xml.Append(
"<x type='result' xmlns='jabber:x:data'>");
133 Xml.Append(
"<field var='FORM_TYPE' type='hidden'>");
134 Xml.Append(
"<value>");
136 Xml.Append(
"</value>");
137 Xml.Append(
"</field>");
138 Xml.Append(
"<field var='max-file-size'>");
139 Xml.Append(
"<value>");
140 Xml.Append(this.settings.MaxFileSize.ToString());
141 Xml.Append(
"</value>");
142 Xml.Append(
"</field>");
145 return Task.CompletedTask;
148 private async Task RequestHandler(
object Sender,
IqEventArgs e)
154 if (!await this.IsFileOk(FileName, ContentType, Size, e))
166 if (this.statusByClient.
TryGetValue(BareJid, out UploadInformation Status) &&
167 Status.TryGetSpecialFile(FileName, ContentType, Size, out SpecialFile SpecialFile))
169 Purpose = SpecialFile.Purpose;
179 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
186 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
191 IAccount Account = await this.
Server.GetAccount(UserName);
194 await e.
IqErrorForbidden(e.
To,
"Only local clients are allowed to upload content.",
"en");
198 if (Size > this.settings.MaxFileSize)
200 await e.
IqError(
"modify",
"<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />" +
201 "<file-too-large xmlns='urn:xmpp:http:upload:0'>" +
202 "<max-file-size>" + this.settings.MaxFileSize.ToString() +
"</max-file-size>" +
203 "</file-too-large>", e.
To,
"File too large. The maximum file size is " +
this.settings.MaxFileSize.
ToString() +
217 Status =
new UploadInformation(BareJid);
218 this.statusByClient[BareJid] = Status;
221 switch (Status.CanUploadFile(Size, Purpose,
this.settings))
223 case CanUploadResult.FileQuotaReached:
225 this.settings.MaxFilesPerMinute.
ToString() +
" files per minute.",
"en");
228 case CanUploadResult.ByteQuotaReached:
230 this.settings.MaxBytesPerMinute.
ToString() +
" bytes per minute.",
"en");
233 case CanUploadResult.Permitted:
234 StringBuilder Xml =
new StringBuilder();
236 string Resource =
"/" + Id +
"/" + HttpUtility.UrlEncode(FileName);
238 string PutUrl = this.settings.HttpFolder + Resource;
239 string GetUrl = PutUrl;
246 FilePath = Path.Combine(this.settings.FileFolder, BareJid, Id +
".bin");
250 if (FileName.EndsWith(
".key", StringComparison.InvariantCultureIgnoreCase))
251 FilePath = Path.Combine(this.settings.KeyFolder, BareJid, FileName);
253 FilePath = Path.Combine(this.settings.BackupFolder, BareJid, FileName);
257 DateTime Today = DateTime.Today;
258 string EncryptedResource = Path.Combine(Today.Year.ToString(
"D4"),
259 Today.Month.ToString(
"D2"), Today.Day.ToString(
"D2"), Id +
".bin");
260 FilePath = Path.Combine(this.settings.EncryptedStorageFolder, EncryptedResource);
261 GetUrl = this.settings.EncryptedStorageRoot +
"/" + EncryptedResource.Replace(Path.DirectorySeparatorChar,
'/');
265 Today = DateTime.Today;
266 string PubSubResource = Path.Combine(Today.Year.ToString(
"D4"),
267 Today.Month.ToString(
"D2"), Today.Day.ToString(
"D2"), FileName);
268 FilePath = Path.Combine(this.settings.PubSubStorageFolder, PubSubResource);
269 if (File.Exists(FilePath))
275 GetUrl = this.settings.PubSubStorageRoot +
"/" + PubSubResource.Replace(Path.DirectorySeparatorChar,
'/');
279 this.files.
Add(Resource,
new FileInfo()
291 Xml.Append(
"<slot xmlns='urn:xmpp:http:upload:0'>");
292 Xml.Append(
"<put url='");
294 Xml.Append(
"'><header name='X-Key'>");
296 Xml.Append(
"</header></put>");
297 Xml.Append(
"<get url='");
299 Xml.Append(
"'/></slot>");
306 private async Task<bool> IsFileOk(
string FileName,
string ContentType,
long Size, IqEventArgs e)
310 await e.IqErrorBadRequest(e.To,
"Invalid size.",
"en");
314 if (
string.IsNullOrEmpty(FileName) ||
315 FileName.Contains(
"\\") ||
316 FileName.Contains(
"/") ||
317 FileName.Contains(
"..") ||
318 FileName.IndexOfAny(invalidCharacters) >= 0)
320 await e.IqErrorBadRequest(e.To,
"Invalid file name.",
"en");
324 if (
string.IsNullOrEmpty(ContentType))
326 await e.IqErrorBadRequest(e.To,
"Invalid content type.",
"en");
333 private Task PrepareBackupHandler(
object Sender, IqEventArgs e)
338 private Task PrepareEncryptedStorageHandler(
object Sender, IqEventArgs e)
343 private Task PreparePubSubStorageHandler(
object Sender, IqEventArgs e)
348 private async Task Prepare(IqEventArgs e,
FilePurpose Purpose)
355 if (!await this.IsFileOk(FileName, ContentType, Size, e))
358 if (!this.statusByClient.
TryGetValue(BareJid, out UploadInformation Status))
360 Status =
new UploadInformation(BareJid);
361 this.statusByClient[BareJid] = Status;
364 Status.AddSpecialFile(FileName, ContentType, Size, Purpose);
366 await e.IqResult(
string.Empty, e.To);
375 File.Delete(e.
Value.FilePath);
377 string Folder = Path.GetDirectoryName(e.
Value.FilePath);
379 if (Directory.GetFiles(Folder,
"*.*", SearchOption.AllDirectories).Length == 0)
380 Directory.Delete(Folder);
388 return Task.CompletedTask;
403 Log.
Error(
"PUT/PATCH request rejected. Resource slot not found.",
404 new KeyValuePair<string, object>(
"Resource", Resource));
411 Log.
Error(
"PUT/PATCH request rejected. Invalid key.",
412 new KeyValuePair<string, object>(
"Resource", Resource),
413 new KeyValuePair<string, object>(
"Key", Key));
420 Log.
Error(
"PUT/PATCH request rejected. File has already been put.",
421 new KeyValuePair<string, object>(
"Resource", Resource));
426 if (Interval.
Last - Interval.
First + 1 != Data.Length)
428 Log.
Error(
"PUT/PATCH request rejected. Range error.",
429 new KeyValuePair<string, object>(
"Resource", Resource),
430 new KeyValuePair<string, object>(
"Actual", Data.Length),
431 new KeyValuePair<string, object>(
"Expected",
FileInfo.
Size));
438 Log.
Error(
"PUT/PATCH request rejected. File size mismatch.",
439 new KeyValuePair<string, object>(
"Resource", Resource),
440 new KeyValuePair<string, object>(
"Actual", Data.Length),
441 new KeyValuePair<string, object>(
"Expected",
FileInfo.
Size));
449 if (!Directory.Exists(Folder))
450 Directory.CreateDirectory(Folder);
454 if (Interval.
First > 0)
456 if (Dest.Length < Interval.
First)
458 Dest.Position = Dest.Length;
460 long Rest = Interval.First - Dest.Length;
461 byte[] Buffer =
new byte[Math.Min(Rest, 65536)];
465 int c = (int)Math.Min(Rest, 65536);
466 await Dest.WriteAsync(Buffer, 0, c);
471 Dest.Position = Interval.
First;
474 await Data.CopyToAsync(Dest);
478 FileInfo.HasBeenPut =
true;
485 new KeyValuePair<string, object>(
"Size", Data.Length));
499 public bool TryGetFile(
string Resource, out
string FilePath, out
string ContentType)
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 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
bool TryGetFile(string Resource, out string FilePath, out string ContentType)
Tries to get a file.
HttpFileUploadComponent(XmppServer Server, CaseInsensitiveString Subdomain, HttpFileUploadSettings Settings)
Implements HTTP File Upload support as an XMPP component: https://xmpp.org/extensions/xep-0363....
async Task< bool > DataUploaded(string Resource, string Key, Stream Data, ContentByteRangeInterval Interval)
Attempts to put a file in the file folder.
const string PubSubNamespace
http://waher.se/Schema/PubSub.xsd
override void Dispose()
IDisposable.Dispose
override Task AppendServiceDiscoveryFeatures(StringBuilder Xml, IqEventArgs e, string Node)
Component.AppendServiceDiscoveryFeatures(StringBuilder, IqEventArgs, string)
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
CaseInsensitiveString Domain
Domain name.
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 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.
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.