Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
XmppFileUploadResource.cs
1using System;
3using System.IO;
4using System.Threading.Tasks;
5using Waher.Content;
6using Waher.Events;
10using Waher.Security;
11
13{
19 {
20 private const int BufferSize = 32768;
21
22 private readonly HttpFileUploadComponent fileUploadComponent;
23 private readonly bool requireEncryption;
24
31 public XmppFileUploadResource(string ResourceName, HttpFileUploadComponent FileUploadComponent, bool RequireEncryption)
32 : base(ResourceName)
33 {
34 this.fileUploadComponent = FileUploadComponent;
35 this.requireEncryption = RequireEncryption;
36 }
37
41 public override bool HandlesSubPaths => true;
42
46 public override bool UserSessions => false;
47
51 public bool AllowsPUT => true;
52
56 public bool AllowsPATCH => true;
57
61 public bool AllowsGET => true;
62
69 public override void Validate(HttpRequest Request)
70 {
71 base.Validate(Request);
72
73 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath, out _, out _))
74 {
75 string Method = Request.Header.Method.ToUpper();
76
77 if (Method == "PUT" || Method == "PATCH")
78 throw new NotFoundException("No PUT/PATCH slot found for: " + Request.SubPath);
79 else
80 throw new NotFoundException("Item not found (it might have been deleted): " + Request.SubPath);
81 }
82
83 if (this.requireEncryption && string.Compare(Request.Header.UriScheme, "http", true) == 0)
84 throw new ForbiddenException(Request, "Encryption required.");
85
86 HttpRequestHeader Header = Request.Header;
87 DateTimeOffset? Limit;
88
89 if (Header.IfMatch is null && !(Header.IfUnmodifiedSince is null) && (Limit = Header.IfUnmodifiedSince.Timestamp).HasValue)
90 {
91 if (File.Exists(FullPath))
92 {
93 DateTime LastModified = File.GetLastWriteTimeUtc(FullPath);
94
95 if (HttpFolderResource.GreaterOrEqual(LastModified, Limit.Value.ToUniversalTime()))
96 throw new NotModifiedException();
97 }
98 }
99 }
100
101 private class ReadProgress : IDisposableAsync
102 {
103 public ByteRangeInterval Next;
104 public HttpResponse Response;
105 public Stream f;
106 public string Boundary;
107 public string ContentType;
108 public string FullPath;
109 public long BytesLeft;
110 public long TotalLength;
111 public int BlockSize;
112 public byte[] Buffer;
113 public bool DeleteFileOnCompletion;
114
115 public void Done()
116 {
117 if (this.DeleteFileOnCompletion)
118 {
119 try
120 {
121 if (File.Exists(this.FullPath))
122 File.Delete(this.FullPath);
123 }
124 catch (Exception ex)
125 {
126 Log.Error("Unable to delete temporary file for internal transfer: " +
127 ex.Message, this.FullPath);
128 }
129 }
130 }
131
132 public async Task BeginRead()
133 {
134 int NrRead;
135
136 try
137 {
138 do
139 {
140 while (this.BytesLeft > 0)
141 {
142 NrRead = await this.f.TryReadAllAsync(this.Buffer, 0, (int)Math.Min(this.BlockSize, this.BytesLeft));
143
144 if (NrRead <= 0)
145 {
146 await this.DisposeAsync();
147 return;
148 }
149 else
150 {
151 await this.Response.Write(false, this.Buffer, 0, NrRead);
152 this.BytesLeft -= NrRead;
153 }
154 }
155
156 if (!(this.Next is null))
157 {
158 long First;
159
160 if (this.Next.First.HasValue)
161 First = this.Next.First.Value;
162 else
163 First = this.TotalLength - this.Next.Last.Value;
164
165 this.f.Position = First;
166 this.BytesLeft = this.Next.GetIntervalLength(this.TotalLength);
167
168 await this.Response.WriteLine();
169 await this.Response.WriteLine("--" + this.Boundary);
170 await this.Response.WriteLine("Content-Type: " + this.ContentType);
171 await this.Response.WriteLine("Content-Range: " + ContentByteRangeInterval.ContentRangeToString(First, First + this.BytesLeft - 1, this.TotalLength));
172 await this.Response.WriteLine();
173
174 this.Next = this.Next.Next;
175 }
176 }
177 while (this.BytesLeft > 0);
178
179 if (!string.IsNullOrEmpty(this.Boundary))
180 {
181 await this.Response.WriteLine();
182 await this.Response.WriteLine("--" + this.Boundary + "--");
183 }
184
185 await this.DisposeAsync();
186
187 this.Done(); // Do not call if transfer is not complete, or an error occurs.
188 }
189 catch (Exception ex)
190 {
191 try
192 {
193 if (!this.Response.HeaderSent)
194 await this.Response.SendResponse(ex);
195 else
196 await this.Response.Flush(true);
197
198 await this.Response.DisposeAsync();
199 this.Response = null;
200
201 await this.DisposeAsync();
202 }
203 catch (Exception)
204 {
205 // Ignore
206 }
207 }
208 }
209
210 public async Task DisposeAsync()
211 {
212 if (!(this.Response is null))
213 {
214 await this.Response.SendResponse();
215 this.Response = null;
216 }
217
218 if (!(this.f is null))
219 {
220 await this.f.FlushAsync();
221 this.f.Dispose();
222 this.f = null;
223 }
224 }
225
226 public void Dispose()
227 {
228 Task _ = this.DisposeAsync();
229 }
230 }
231
232 private readonly Dictionary<string, CacheRec> cacheInfo = new Dictionary<string, CacheRec>();
233
234 private class CacheRec
235 {
236 public DateTime LastModified;
237 public string ETag;
238 }
239
246 public async Task GET(HttpRequest Request, HttpResponse Response)
247 {
248 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath,
249 out string ContentType, out FilePurpose Purpose))
250 {
251 await Response.SendResponse(new NotFoundException());
252 return;
253 }
254
255 if (Purpose == FilePurpose.InternalTransfer &&
257 {
258 await Response.SendResponse(new ForbiddenException("Internal transfer files not accessible by remote parties."));
259 return;
260 }
261
262 if (!File.Exists(FullPath))
263 {
264 await Response.SendResponse(new NotFoundException());
265 return;
266 }
267
268 DateTime LastModified = File.GetLastWriteTimeUtc(FullPath);
269 CacheRec Rec;
270
271 Rec = this.CheckCacheHeaders(FullPath, LastModified, Request);
272 if (Rec is null)
273 {
274 await Response.SendResponse(new NotModifiedException());
275 return;
276 }
277
278 if (!(Request.Header.Accept is null))
279 {
280 if (!Request.Header.Accept.IsAcceptable(ContentType))
281 {
282 await Response.SendResponse(new NotAcceptableException());
283 return;
284 }
285 }
286
287 Stream f = File.OpenRead(FullPath);
288
289 await SendResponse(f, FullPath, ContentType, Rec.ETag, LastModified, Response, Purpose);
290 }
291
292 private static async Task SendResponse(Stream f, string FullPath, string ContentType,
293 string ETag, DateTime LastModified, HttpResponse Response, FilePurpose Purpose)
294 {
295 ReadProgress Progress = new ReadProgress()
296 {
297 Response = Response,
298 f = f ?? File.OpenRead(FullPath),
299 FullPath = FullPath,
300 Next = null,
301 Boundary = null,
302 ContentType = null,
303 DeleteFileOnCompletion = Purpose == FilePurpose.InternalTransfer
304 };
305 Progress.BytesLeft = Progress.TotalLength = Progress.f.Length;
306 Progress.BlockSize = (int)Math.Min(BufferSize, Progress.BytesLeft);
307 Progress.Buffer = new byte[Progress.BlockSize];
308
309 Response.ContentType = ContentType;
310 Response.ContentLength = Progress.TotalLength;
311
312 Response.SetHeader("ETag", ETag);
313 Response.SetHeader("Last-Modified", CommonTypes.EncodeRfc822(LastModified));
314
315 if (Response.OnlyHeader || Progress.TotalLength == 0)
316 {
317 await Response.SendResponse();
318 await Progress.DisposeAsync();
319
320 if (Progress.TotalLength == 0)
321 Progress.Done();
322 }
323 else
324 {
325 Task _ = Progress.BeginRead();
326 }
327 }
328
329 private CacheRec CheckCacheHeaders(string FullPath, DateTime LastModified, HttpRequest Request)
330 {
331 string CacheKey = FullPath.ToLower();
332 HttpRequestHeader Header = Request.Header;
333 CacheRec Rec;
334 DateTimeOffset? Limit;
335
336 lock (this.cacheInfo)
337 {
338 if (this.cacheInfo.TryGetValue(CacheKey, out Rec))
339 {
340 if (Rec.LastModified != LastModified)
341 {
342 this.cacheInfo.Remove(CacheKey);
343 Rec = null;
344 }
345 }
346 }
347
348 if (Rec is null)
349 {
350 Rec = new CacheRec()
351 {
352 LastModified = LastModified,
353 };
354
355 using (FileStream fs = File.OpenRead(FullPath))
356 {
357 Rec.ETag = Hashes.ComputeSHA1HashString(fs);
358 }
359
360 lock (this.cacheInfo)
361 {
362 this.cacheInfo[CacheKey] = Rec;
363 }
364 }
365
366 if (!(Header.IfNoneMatch is null))
367 {
368 if (Header.IfNoneMatch.Value == Rec.ETag)
369 return null;
370 }
371 else if (!(Header.IfModifiedSince is null))
372 {
373 if ((Limit = Header.IfModifiedSince.Timestamp).HasValue &&
374 HttpFolderResource.LessOrEqual(LastModified, Limit.Value.ToUniversalTime()))
375 {
376 return null;
377 }
378 }
379
380 return Rec;
381 }
382
390 public async Task GET(HttpRequest Request, HttpResponse Response, ByteRangeInterval FirstInterval)
391 {
392 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath,
393 out string ContentType, out FilePurpose Purpose))
394 {
395 await Response.SendResponse(new NotFoundException());
396 return;
397 }
398
399 if (Purpose == FilePurpose.InternalTransfer &&
401 {
402 await Response.SendResponse(new ForbiddenException("Internal transfer files not accessible by remote parties."));
403 return;
404 }
405
406 if (!File.Exists(FullPath))
407 {
408 await Response.SendResponse(new NotFoundException());
409 return;
410 }
411
412 HttpRequestHeader Header = Request.Header;
413 DateTime LastModified = File.GetLastWriteTimeUtc(FullPath);
414 DateTimeOffset? Limit;
415 CacheRec Rec;
416
417 if (!(Header.IfRange is null) && (Limit = Header.IfRange.Timestamp).HasValue &&
418 !HttpFolderResource.LessOrEqual(LastModified, Limit.Value.ToUniversalTime()))
419 {
420 Response.StatusCode = 200;
421 Response.StatusMessage = "OK";
422 await this.GET(Request, Response); // No ranged request.
423 return;
424 }
425
426 Rec = this.CheckCacheHeaders(FullPath, LastModified, Request);
427 if (Rec is null)
428 {
429 await Response.SendResponse(new NotModifiedException());
430 return;
431 }
432
433 if (!(Request.Header.Accept is null))
434 {
435 if (!Request.Header.Accept.IsAcceptable(ContentType))
436 {
437 await Response.SendResponse(new NotAcceptableException());
438 return;
439 }
440 }
441
442 Stream f = File.OpenRead(FullPath);
443
444 ReadProgress Progress = new ReadProgress()
445 {
446 Response = Response,
447 DeleteFileOnCompletion = false,
448 f = f ?? File.OpenRead(FullPath)
449 };
450
451 ByteRangeInterval Interval = FirstInterval;
452 Progress.TotalLength = Progress.f.Length;
453
454 long i = 0;
455 long j;
456 long First;
457
458 if (FirstInterval.First.HasValue)
459 First = FirstInterval.First.Value;
460 else
461 First = Progress.TotalLength - FirstInterval.Last.Value;
462
463 Progress.f.Position = First;
464 Progress.BytesLeft = Interval.GetIntervalLength(Progress.TotalLength);
465 Progress.Next = Interval.Next;
466
467 while (!(Interval is null))
468 {
469 j = Interval.GetIntervalLength(Progress.TotalLength);
470 if (j > i)
471 i = j;
472
473 Interval = Interval.Next;
474 }
475
476 Progress.BlockSize = (int)Math.Min(BufferSize, i);
477 Progress.Buffer = new byte[Progress.BlockSize];
478
479 if (FirstInterval.Next is null)
480 {
481 Progress.Boundary = null;
482 Progress.ContentType = null;
483
484 Response.ContentType = ContentType;
485 Response.ContentLength = FirstInterval.GetIntervalLength(Progress.f.Length);
486 Response.SetHeader("Content-Range", ContentByteRangeInterval.ContentRangeToString(First, First + Progress.BytesLeft - 1, Progress.TotalLength));
487 }
488 else
489 {
490 Progress.Boundary = Guid.NewGuid().ToString().Replace("-", string.Empty);
491 Progress.ContentType = ContentType;
492
493 Response.ContentType = "multipart/byteranges; boundary=" + Progress.Boundary;
494 // chunked transfer encoding will be used
495 }
496
497 Response.SetHeader("ETag", Rec.ETag);
498 Response.SetHeader("Last-Modified", CommonTypes.EncodeRfc822(LastModified));
499
500 if (Response.OnlyHeader || Progress.BytesLeft == 0)
501 {
502 await Response.SendResponse();
503 await Progress.DisposeAsync();
504 }
505 else
506 {
507 if (!(FirstInterval.Next is null))
508 {
509 await Response.WriteLine();
510 await Response.WriteLine("--" + Progress.Boundary);
511 await Response.WriteLine("Content-Type: " + Progress.ContentType);
512 await Response.WriteLine("Content-Range: " + ContentByteRangeInterval.ContentRangeToString(First, First + Progress.BytesLeft - 1, Progress.TotalLength));
513 await Response.WriteLine();
514 }
515
516 Task _ = Progress.BeginRead();
517 }
518 }
519
527 public Task PATCH(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
528 {
529 return this.PUT(Request, Response, Interval);
530 }
531
539 public async Task PUT(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
540 {
541 try
542 {
543 if (!Request.HasData)
544 throw new BadRequestException();
545
546 string Key = Request.Header["X-Key"];
547
548 if (!await this.fileUploadComponent.DataUploaded(Request.SubPath, Key,
549 Request.DataStream, Interval, Request.Header.ContentType?.Value,
550 Request.RemoteEndPoint))
551 {
552 throw new ForbiddenException(Request, "Upload rejected.");
553 }
554
555 Response.StatusCode = 201;
556 Response.StatusMessage = "Created";
557 await Response.SendResponse();
558 }
559 catch (Exception ex)
560 {
561 await Response.SendResponse(ex);
562 }
563 }
564
573 public bool TryGetFileName(string SubPath, string Host, bool MustExist,
574 out string FileName)
575 {
576 if (this.fileUploadComponent.TryGetFile(SubPath, out string FullPath, out _, out _))
577 {
578 if (!MustExist || File.Exists(FullPath))
579 {
580 FileName = FullPath;
581 return true;
582 }
583 }
584
585 FileName = null;
586 return false;
587 }
588
589 }
590}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:15
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Definition: CommonTypes.cs:669
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
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:692
The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repe...
Represents a range in a ranged HTTP request or response.
long? Last
Last byte of interval, inclusive, if provided. If not provided, the interval ends at the end of the r...
ByteRangeInterval Next
Next segment.
long GetIntervalLength(long TotalLength)
Calculates the number of bytes spanned by the interval.
long? First
First byte of interval, if provided. If not provided, the interval represents the last Last number of...
Represents a content range in a ranged HTTP request or response.
static string ContentRangeToString(long First, long Last, long Total)
Converts the content range to an HTTP header field value string.
The server understood the request, but is refusing to fulfill it. Authorization will not help and the...
Base class for all asynchronous HTTP resources. An asynchronous resource responds outside of the meth...
Publishes a folder with all its files and subfolders through HTTP GET, with optional support for PUT,...
static bool GreaterOrEqual(DateTime LastModified, DateTimeOffset Limit)
Computes LastModified >=Limit . The normal >= operator behaved strangely, and did not get the equalit...
static bool LessOrEqual(DateTime LastModified, DateTimeOffset Limit)
Computes LastModified <=Limit . The normal <= operator behaved strangely, and did not get the equalit...
bool Remove(HttpField item)
Removes a field having the same Key and Value properties as item .
Definition: HttpHeader.cs:211
HttpFieldContentType ContentType
Content-Type HTTP Field header. (RFC 2616, §14.17)
Definition: HttpHeader.cs:285
Contains information about all fields in an HTTP request header.
HttpFieldAccept Accept
Accept HTTP Field header. (RFC 2616, §14.1)
Represents an HTTP request.
Definition: HttpRequest.cs:22
Stream DataStream
Data stream, if data is available, or null if data is not available.
Definition: HttpRequest.cs:187
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:182
string RemoteEndPoint
Remote end-point.
Definition: HttpRequest.cs:243
bool HasData
If the request has data.
Definition: HttpRequest.cs:113
string SubPath
Sub-path. If a resource is found handling the request, this property contains the trailing sub-path o...
Definition: HttpRequest.cs:194
IUser User
Authenticated user, if available, or null if not available.
Definition: HttpRequest.cs:203
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:23
Task Flush(bool EndOfData)
Clears all buffers for the current writer and causes any buffered data to be written to the underlyin...
async Task DisposeAsync()
Closes the connection and disposes of all resources.
async Task SendResponse()
Sends the response back to the client. If the resource is synchronous, there's no need to call this m...
Task Write(byte[] Data)
Returns binary data in the response.
void SetHeader(string FieldName, string Value)
Sets a custom header field value.
bool HeaderSent
If the header has been sent.
bool OnlyHeader
If only the header is of interest.
Task WriteLine()
Writes a new line character sequence (CRLF).
The resource identified by the request is only capable of generating response entities which have con...
The server has not found anything matching the Request-URI. No indication is given of whether the con...
If the client has performed a conditional GET request and access is allowed, but the document has not...
Implements HTTP File Upload support as an XMPP component: https://xmpp.org/extensions/xep-0363....
Access scheme used for internal transfer of files between client and server.
static bool IsInternal(HttpRequest Request)
Checks if a request is an internal transfer request, meaning that both the local and remote end point...
HTTP Resource managing HTTP Uploads, and access to uploaded files.
async Task PUT(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
Executes the ranged PUT method on the resource.
XmppFileUploadResource(string ResourceName, HttpFileUploadComponent FileUploadComponent, bool RequireEncryption)
HTTP Resource managing HTTP Uploads, and access to uploaded files.
override bool UserSessions
If the resource uses user sessions.
bool TryGetFileName(string SubPath, string Host, bool MustExist, out string FileName)
Tries to get the full path of a file-based resource.
override void Validate(HttpRequest Request)
Validates the request itself. This method is called prior to processing the request,...
override bool HandlesSubPaths
If the resource handles sub-paths.
Task PATCH(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
Executes the ranged PATCH method on the resource.
async Task GET(HttpRequest Request, HttpResponse Response, ByteRangeInterval FirstInterval)
Executes the ranged GET method on the resource.
async Task GET(HttpRequest Request, HttpResponse Response)
Executes the GET method on the resource.
Contains methods for simple hash calculations.
Definition: Hashes.cs:57
static string ComputeSHA1HashString(byte[] Data)
Computes the SHA-1 hash of a block of binary data.
Definition: Hashes.cs:395
Interface for asynchronously disposable objects.
GET Interface for HTTP resources.
Ranged GET Interface for HTTP resources.
Ranged PATCH Interface for HTTP resources.
Ranged PUT Interface for HTTP resources.
Interface for resources hosting files in a folder.
Definition: ImplTypes.g.cs:58
class Header(ISimulationNode Parent, Model Model)
Represents an identity property.
Definition: Header.cs:18
FilePurpose
Purpose of file uploaded
Definition: FileInfo.cs:9