Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
HttpFileUploadComponent.cs
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Text;
5using System.Threading.Tasks;
6using System.Web;
8using Waher.Events;
12
14{
20 {
21 private static readonly char[] invalidCharacters = Path.GetInvalidFileNameChars();
22
23 private readonly HttpFileUploadSettings settings;
26
30 public const string Namespace = "urn:xmpp:http:upload:0";
31
35 public const string BackupNamespace = "http://waher.se/Schema/Backups.xsd";
36
40 public const string EncryptedStorageNamespace = "http://waher.se/Schema/EncryptedStorage.xsd";
41
45 public const string PubSubNamespace = "http://waher.se/Schema/PubSub.xsd";
46
55 : base(Server, Subdomain, "HTTP File Upload")
56 {
57 this.settings = Settings;
58
59 this.DeleteUploadedFiles();
60
61 if (!Directory.Exists(Settings.FileFolder))
62 Directory.CreateDirectory(Settings.FileFolder);
63
64 this.statusByClient = new Cache<CaseInsensitiveString, UploadInformation>(int.MaxValue, TimeSpan.MaxValue, this.settings.FileLifetime, true);
65 this.files = new Cache<CaseInsensitiveString, FileInfo>(int.MaxValue, TimeSpan.MaxValue, this.settings.FileLifetime, true);
66
67 this.files.Removed += this.Files_Removed;
68
69 this.RegisterIqGetHandler("request", Namespace, this.RequestHandler, true);
70 this.RegisterIqSetHandler("prepare", BackupNamespace, this.PrepareBackupHandler, true);
71 this.RegisterIqSetHandler("prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler, true);
72 this.RegisterIqSetHandler("prepare", PubSubNamespace, this.PreparePubSubStorageHandler, true);
73 }
74
75 private void DeleteUploadedFiles()
76 {
77 if (Directory.Exists(this.settings.FileFolder))
78 {
79 try
80 {
81 Directory.Delete(this.settings.FileFolder, true);
82 }
83 catch (Exception ex)
84 {
85 Log.Exception(ex);
86 }
87 }
88 }
89
93 public override void Dispose()
94 {
95 base.Dispose();
96
97 this.UnregisterIqGetHandler("request", Namespace, this.RequestHandler, true);
98 this.UnregisterIqSetHandler("prepare", BackupNamespace, this.PrepareBackupHandler, true);
99 this.UnregisterIqSetHandler("prepare", EncryptedStorageNamespace, this.PrepareEncryptedStorageHandler, true);
100 this.UnregisterIqSetHandler("prepare", PubSubNamespace, this.PreparePubSubStorageHandler, true);
101
102 this.statusByClient?.Dispose();
103 this.statusByClient = null;
104
105 this.files?.Dispose();
106 this.files = null;
107
108 this.DeleteUploadedFiles();
109 }
110
115 public override bool SupportsAccounts => false;
116
120 protected override Task AppendServiceDiscoveryIdentities(StringBuilder Xml, IqEventArgs e, string Node)
121 {
122 Xml.Append("<identity category='store' type='file' name='HTTP File Upload' />");
123
124 return Task.CompletedTask;
125 }
126
130 protected override Task AppendServiceDiscoveryFeatures(StringBuilder Xml, IqEventArgs e, string Node)
131 {
132 Xml.Append("<x type='result' xmlns='jabber:x:data'>");
133 Xml.Append("<field var='FORM_TYPE' type='hidden'>");
134 Xml.Append("<value>");
135 Xml.Append(Namespace);
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>");
143 Xml.Append("</x>");
144
145 return Task.CompletedTask;
146 }
147
148 private async Task RequestHandler(object Sender, IqEventArgs e)
149 {
150 string FileName = XML.Attribute(e.Query, "filename");
151 string ContentType = XML.Attribute(e.Query, "content-type");
152 long Size = XML.Attribute(e.Query, "size", 0L);
153
154 if (!await this.IsFileOk(FileName, ContentType, Size, e))
155 return;
156
157 if (!e.To.IsDomain)
158 {
159 await e.IqErrorBadRequest(e.To, "Invalid destination address.", "en");
160 return;
161 }
162
164 FilePurpose Purpose;
165
166 if (this.statusByClient.TryGetValue(BareJid, out UploadInformation Status) &&
167 Status.TryGetSpecialFile(FileName, ContentType, Size, out SpecialFile SpecialFile))
168 {
169 Purpose = SpecialFile.Purpose;
170 }
171 else
172 Purpose = FilePurpose.Temporary;
173
174 if (Purpose == FilePurpose.Temporary)
175 {
176 int i = BareJid.IndexOf('@');
177 if (i < 0)
178 {
179 await e.IqErrorForbidden(e.To, "Only local clients are allowed to upload content.", "en");
180 return;
181 }
182
183 CaseInsensitiveString Domain = BareJid.Substring(i + 1);
184 if (Domain != this.Server.Domain)
185 {
186 await e.IqErrorForbidden(e.To, "Only local clients are allowed to upload content.", "en");
187 return;
188 }
189
190 CaseInsensitiveString UserName = BareJid.Substring(0, i);
191 IAccount Account = await this.Server.GetAccount(UserName);
192 if (Account is null)
193 {
194 await e.IqErrorForbidden(e.To, "Only local clients are allowed to upload content.", "en");
195 return;
196 }
197
198 if (Size > this.settings.MaxFileSize)
199 {
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() +
204 " bytes.", "en");
205 return;
206 }
207 }
208
209 if (!await this.Server.PersistenceLayer.IsPermitted(BareJid, "HTTP.Upload"))
210 {
211 await e.IqErrorNotAllowed(e.To, "You are not allowed to upload files.", "en");
212 return;
213 }
214
215 if (Status is null)
216 {
217 Status = new UploadInformation(BareJid);
218 this.statusByClient[BareJid] = Status;
219 }
220
221 switch (Status.CanUploadFile(Size, Purpose, this.settings))
222 {
223 case CanUploadResult.FileQuotaReached:
224 await e.IqErrorResourceConstraint(e.To, "Quota reached. You can only upload " +
225 this.settings.MaxFilesPerMinute.ToString() + " files per minute.", "en");
226 return;
227
228 case CanUploadResult.ByteQuotaReached:
229 await e.IqErrorResourceConstraint(e.To, "Quota reached. You can only upload " +
230 this.settings.MaxBytesPerMinute.ToString() + " bytes per minute.", "en");
231 return;
232
233 case CanUploadResult.Permitted:
234 StringBuilder Xml = new StringBuilder();
235 string Id = this.Server.NewId(32);
236 string Resource = "/" + Id + "/" + HttpUtility.UrlEncode(FileName);
237 string FilePath;
238 string PutUrl = this.settings.HttpFolder + Resource;
239 string GetUrl = PutUrl;
240 string Key = this.Server.NewId(32);
241
242 switch (Purpose)
243 {
244 case FilePurpose.Temporary:
245 default:
246 FilePath = Path.Combine(this.settings.FileFolder, BareJid, Id + ".bin");
247 break;
248
249 case FilePurpose.Backup:
250 if (FileName.EndsWith(".key", StringComparison.InvariantCultureIgnoreCase))
251 FilePath = Path.Combine(this.settings.KeyFolder, BareJid, FileName);
252 else
253 FilePath = Path.Combine(this.settings.BackupFolder, BareJid, FileName);
254 break;
255
256 case FilePurpose.Encrypted:
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, '/');
262 break;
263
264 case FilePurpose.PubSub:
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))
270 {
271 await e.IqErrorConflict(e.To, "File already exists.", "en");
272 return;
273 }
274
275 GetUrl = this.settings.PubSubStorageRoot + "/" + PubSubResource.Replace(Path.DirectorySeparatorChar, '/');
276 break;
277 }
278
279 this.files.Add(Resource, new FileInfo()
280 {
281 Jid = e.From.Address,
282 Resource = Resource,
283 Key = Key,
285 FilePath = FilePath,
286 Size = Size,
287 HasBeenPut = false,
288 Purpose = Purpose
289 });
290
291 Xml.Append("<slot xmlns='urn:xmpp:http:upload:0'>");
292 Xml.Append("<put url='");
293 Xml.Append(XML.Encode(PutUrl));
294 Xml.Append("'><header name='X-Key'>");
295 Xml.Append(Key);
296 Xml.Append("</header></put>");
297 Xml.Append("<get url='");
298 Xml.Append(XML.Encode(GetUrl));
299 Xml.Append("'/></slot>");
300
301 await e.IqResult(Xml.ToString(), e.To);
302 break;
303 }
304 }
305
306 private async Task<bool> IsFileOk(string FileName, string ContentType, long Size, IqEventArgs e)
307 {
308 if (Size < 0)
309 {
310 await e.IqErrorBadRequest(e.To, "Invalid size.", "en");
311 return false;
312 }
313
314 if (string.IsNullOrEmpty(FileName) ||
315 FileName.Contains("\\") ||
316 FileName.Contains("/") ||
317 FileName.Contains("..") ||
318 FileName.IndexOfAny(invalidCharacters) >= 0)
319 {
320 await e.IqErrorBadRequest(e.To, "Invalid file name.", "en");
321 return false;
322 }
323
324 if (string.IsNullOrEmpty(ContentType))
325 {
326 await e.IqErrorBadRequest(e.To, "Invalid content type.", "en");
327 return false;
328 }
329
330 return true;
331 }
332
333 private Task PrepareBackupHandler(object Sender, IqEventArgs e)
334 {
335 return this.Prepare(e, FilePurpose.Backup);
336 }
337
338 private Task PrepareEncryptedStorageHandler(object Sender, IqEventArgs e)
339 {
340 return this.Prepare(e, FilePurpose.Encrypted);
341 }
342
343 private Task PreparePubSubStorageHandler(object Sender, IqEventArgs e)
344 {
345 return this.Prepare(e, FilePurpose.PubSub);
346 }
347
348 private async Task Prepare(IqEventArgs e, FilePurpose Purpose)
349 {
350 CaseInsensitiveString BareJid = e.From.BareJid;
351 string FileName = XML.Attribute(e.Query, "filename");
352 string ContentType = XML.Attribute(e.Query, "content-type");
353 long Size = XML.Attribute(e.Query, "size", 0L);
354
355 if (!await this.IsFileOk(FileName, ContentType, Size, e))
356 return;
357
358 if (!this.statusByClient.TryGetValue(BareJid, out UploadInformation Status))
359 {
360 Status = new UploadInformation(BareJid);
361 this.statusByClient[BareJid] = Status;
362 }
363
364 Status.AddSpecialFile(FileName, ContentType, Size, Purpose);
365
366 await e.IqResult(string.Empty, e.To);
367 }
368
369 private Task Files_Removed(object Sender, CacheItemEventArgs<CaseInsensitiveString, FileInfo> e)
370 {
371 try
372 {
373 if (e.Value.Purpose == FilePurpose.Temporary && File.Exists(e.Value.FilePath))
374 {
375 File.Delete(e.Value.FilePath);
376
377 string Folder = Path.GetDirectoryName(e.Value.FilePath);
378
379 if (Directory.GetFiles(Folder, "*.*", SearchOption.AllDirectories).Length == 0)
380 Directory.Delete(Folder);
381 }
382 }
383 catch (Exception ex)
384 {
385 Log.Exception(ex);
386 }
387
388 return Task.CompletedTask;
389 }
390
399 public async Task<bool> DataUploaded(string Resource, string Key, Stream Data, ContentByteRangeInterval Interval)
400 {
401 if (!this.files.TryGetValue(Resource, out FileInfo FileInfo))
402 {
403 Log.Error("PUT/PATCH request rejected. Resource slot not found.",
404 new KeyValuePair<string, object>("Resource", Resource));
405
406 return false;
407 }
408
409 if (Key != FileInfo.Key)
410 {
411 Log.Error("PUT/PATCH request rejected. Invalid key.",
412 new KeyValuePair<string, object>("Resource", Resource),
413 new KeyValuePair<string, object>("Key", Key));
414
415 return false;
416 }
417
419 {
420 Log.Error("PUT/PATCH request rejected. File has already been put.",
421 new KeyValuePair<string, object>("Resource", Resource));
422
423 return false;
424 }
425
426 if (Interval.Last - Interval.First + 1 != Data.Length)
427 {
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));
432
433 return false;
434 }
435
436 if (Interval.Last >= FileInfo.Size)
437 {
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));
442
443 return false;
444 }
445
446 Data.Position = 0;
447
448 string Folder = Path.GetDirectoryName(FileInfo.FilePath);
449 if (!Directory.Exists(Folder))
450 Directory.CreateDirectory(Folder);
451
452 using (FileStream Dest = File.Exists(FileInfo.FilePath) ? File.OpenWrite(FileInfo.FilePath) : File.Create(FileInfo.FilePath))
453 {
454 if (Interval.First > 0)
455 {
456 if (Dest.Length < Interval.First)
457 {
458 Dest.Position = Dest.Length;
459
460 long Rest = Interval.First - Dest.Length;
461 byte[] Buffer = new byte[Math.Min(Rest, 65536)];
462
463 while (Rest > 0)
464 {
465 int c = (int)Math.Min(Rest, 65536);
466 await Dest.WriteAsync(Buffer, 0, c);
467 Rest -= c;
468 }
469 }
470 else
471 Dest.Position = Interval.First;
472 }
473
474 await Data.CopyToAsync(Dest);
475
476 if (Data.Position >= FileInfo.Size)
477 {
478 FileInfo.HasBeenPut = true;
479
480 Log.Informational("File uploaded.",
481 new KeyValuePair<string, object>("JID", FileInfo.Jid.Value),
482 new KeyValuePair<string, object>("ContentType", FileInfo.ContentType),
483 new KeyValuePair<string, object>("Path", FileInfo.FilePath),
484 new KeyValuePair<string, object>("Purpose", FileInfo.Purpose),
485 new KeyValuePair<string, object>("Size", Data.Length));
486 }
487 }
488
489 return true;
490 }
491
499 public bool TryGetFile(string Resource, out string FilePath, out string ContentType)
500 {
501 if (this.files.TryGetValue(Resource, out FileInfo FileInfo))
502 {
503 FilePath = FileInfo.FilePath;
504 ContentType = FileInfo.ContentType;
505 return true;
506 }
507 else
508 {
509 FilePath = null;
510 ContentType = null;
511 return false;
512 }
513 }
514
515 }
516}
Helps with common XML-related tasks.
Definition: XML.cs:19
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Definition: XML.cs:914
static string Encode(string s)
Encodes a string for use in XML.
Definition: XML.cs:27
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:13
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.
Definition: Log.cs:1647
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.
Definition: Log.cs:682
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.
Definition: Log.cs:334
Represents a content range in a ranged HTTP request or response.
long Last
Last byte of interval, inclusive.
Base class for components.
Definition: Component.cs:16
void RegisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Set handler.
Definition: Component.cs:161
CaseInsensitiveString Subdomain
Subdomain name.
Definition: Component.cs:76
void RegisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Get handler.
Definition: Component.cs:149
XmppServer Server
XMPP Server.
Definition: Component.cs:96
bool UnregisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Get handler.
Definition: Component.cs:249
bool UnregisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Set handler.
Definition: Component.cs:262
Information about a file upload.
Definition: FileInfo.cs:35
string Key
Key authorizing access to file upload.
Definition: FileInfo.cs:49
string ContentType
Content-Type of uploaded content.
Definition: FileInfo.cs:54
CaseInsensitiveString Jid
JID uploading the file
Definition: FileInfo.cs:39
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.
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.
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)
Event arguments for IQ queries.
Definition: IqEventArgs.cs:12
XmppAddress From
From address attribute
Definition: IqEventArgs.cs:93
Task IqResult(string Xml, string From)
Returns a response to the current request.
Definition: IqEventArgs.cs:113
Task IqErrorResourceConstraint(XmppAddress From, string ErrorText, string Language)
Returns a resource-constraint error.
Definition: IqEventArgs.cs:173
XmlElement Query
Query element, if found, null otherwise.
Definition: IqEventArgs.cs:70
Task IqErrorNotAllowed(XmppAddress From, string ErrorText, string Language)
Returns a not-allowed error.
Definition: IqEventArgs.cs:187
XmppAddress To
To address attribute
Definition: IqEventArgs.cs:88
async Task IqError(string ErrorType, string Xml, XmppAddress From, string ErrorText, string Language)
Returns an error response to the current request.
Definition: IqEventArgs.cs:137
Task IqErrorConflict(XmppAddress From, string ErrorText, string Language)
Returns a conflict error.
Definition: IqEventArgs.cs:257
Task IqErrorBadRequest(XmppAddress From, string ErrorText, string Language)
Returns a bad-request error.
Definition: IqEventArgs.cs:159
Task IqErrorForbidden(XmppAddress From, string ErrorText, string Language)
Returns a forbidden error.
Definition: IqEventArgs.cs:229
override string ToString()
object.ToString()
Definition: XmppAddress.cs:190
CaseInsensitiveString Address
XMPP Address
Definition: XmppAddress.cs:37
bool IsDomain
If the Address is a domain.
Definition: XmppAddress.cs:175
CaseInsensitiveString BareJid
Bare JID
Definition: XmppAddress.cs:45
CaseInsensitiveString Domain
Domain name.
Definition: XmppServer.cs:882
string NewId(int NrBytes)
Generates a new ID.
Definition: XmppServer.cs:662
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.
Definition: Cache.cs:15
void Dispose()
IDisposable.Dispose
Definition: Cache.cs:74
bool TryGetValue(KeyType Key, out ValueType Value)
Tries to get a value from the cache.
Definition: Cache.cs:203
void Add(KeyType Key, ValueType Value)
Adds an item to the cache.
Definition: Cache.cs:338
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
Definition: FileInfo.cs:9
ContentType
DTLS Record content type.
Definition: Enumerations.cs:11