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;
2using System.Collections.Generic;
3using System.IO;
4using System.Threading.Tasks;
5using Waher.Content;
9
11{
18 {
19 private const int BufferSize = 32768;
20
21 private readonly HttpFileUploadComponent fileUploadComponent;
22 private readonly bool requireEncryption;
23
30 public XmppFileUploadResource(string ResourceName, HttpFileUploadComponent FileUploadComponent, bool RequireEncryption)
31 : base(ResourceName)
32 {
33 this.fileUploadComponent = FileUploadComponent;
34 this.requireEncryption = RequireEncryption;
35 }
36
40 public override bool HandlesSubPaths => true;
41
45 public override bool UserSessions => false;
46
50 public bool AllowsPUT => true;
51
55 public bool AllowsPATCH => true;
56
60 public bool AllowsGET => true;
61
68 public override void Validate(HttpRequest Request)
69 {
70 base.Validate(Request);
71
72 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath, out string _))
73 {
74 string Method = Request.Header.Method.ToUpper();
75
76 if (Method == "PUT" || Method == "PATCH")
77 throw new NotFoundException("No PUT/PATCH slot found for: " + Request.SubPath);
78 else
79 throw new NotFoundException("Item not found (it might have been deleted): " + Request.SubPath);
80 }
81
82 if (this.requireEncryption && string.Compare(Request.Header.UriScheme, "http", true) == 0)
83 throw new ForbiddenException("Encryption required.");
84
85 HttpRequestHeader Header = Request.Header;
86 DateTimeOffset? Limit;
87
88 if (Header.IfMatch is null && !(Header.IfUnmodifiedSince is null) && (Limit = Header.IfUnmodifiedSince.Timestamp).HasValue)
89 {
90 if (File.Exists(FullPath))
91 {
92 DateTime LastModified = File.GetLastWriteTime(FullPath);
93 LastModified = LastModified.ToUniversalTime();
94
95 if (HttpFolderResource.GreaterOrEqual(LastModified, Limit.Value.ToUniversalTime()))
96 throw new NotModifiedException();
97 }
98 }
99 }
100
101 private class ReadProgress : IDisposable
102 {
103 public ByteRangeInterval Next;
104 public HttpResponse Response;
105 public Stream f;
106 public string Boundary;
107 public string ContentType;
108 public long BytesLeft;
109 public long TotalLength;
110 public int BlockSize;
111 public byte[] Buffer;
112
113 public async Task BeginRead()
114 {
115 int NrRead;
116
117 try
118 {
119 do
120 {
121 while (this.BytesLeft > 0)
122 {
123 NrRead = await this.f.TryReadAllAsync(this.Buffer, 0, (int)Math.Min(this.BlockSize, this.BytesLeft));
124
125 if (NrRead <= 0)
126 {
127 await this.DisposeAsync();
128 return;
129 }
130 else
131 {
132 await this.Response.Write(this.Buffer, 0, NrRead);
133 this.BytesLeft -= NrRead;
134 }
135 }
136
137 if (!(this.Next is null))
138 {
139 long First;
140
141 if (this.Next.First.HasValue)
142 First = this.Next.First.Value;
143 else
144 First = this.TotalLength - this.Next.Last.Value;
145
146 this.f.Position = First;
147 this.BytesLeft = this.Next.GetIntervalLength(this.TotalLength);
148
149 await this.Response.WriteLine();
150 await this.Response.WriteLine("--" + this.Boundary);
151 await this.Response.WriteLine("Content-Type: " + this.ContentType);
152 await this.Response.WriteLine("Content-Range: " + ContentByteRangeInterval.ContentRangeToString(First, First + this.BytesLeft - 1, this.TotalLength));
153 await this.Response.WriteLine();
154
155 this.Next = this.Next.Next;
156 }
157 }
158 while (this.BytesLeft > 0);
159
160 if (!string.IsNullOrEmpty(this.Boundary))
161 {
162 await this.Response.WriteLine();
163 await this.Response.WriteLine("--" + this.Boundary + "--");
164 }
165
166 await this.DisposeAsync();
167 }
168 catch (Exception ex)
169 {
170 try
171 {
172 if (!this.Response.HeaderSent)
173 await this.Response.SendResponse(ex);
174 else
175 await this.Response.Flush();
176
177 await this.Response.DisposeAsync();
178 this.Response = null;
179
180 await this.DisposeAsync();
181 }
182 catch (Exception)
183 {
184 // Ignore
185 }
186 }
187 }
188
189 public async Task DisposeAsync()
190 {
191 if (!(this.Response is null))
192 {
193 await this.Response.SendResponse();
194 this.Response = null;
195 }
196
197 if (!(this.f is null))
198 {
199 await this.f.FlushAsync();
200 this.f.Dispose();
201 this.f = null;
202 }
203 }
204
205 public void Dispose()
206 {
207 Task _ = this.DisposeAsync();
208 }
209 }
210
211 private readonly Dictionary<string, CacheRec> cacheInfo = new Dictionary<string, CacheRec>();
212
213 private class CacheRec
214 {
215 public DateTime LastModified;
216 public string ETag;
217 }
218
225 public async Task GET(HttpRequest Request, HttpResponse Response)
226 {
227 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath, out string ContentType))
228 throw new NotFoundException();
229
230 if (!File.Exists(FullPath))
231 throw new NotFoundException();
232
233 DateTime LastModified = File.GetLastWriteTime(FullPath).ToUniversalTime();
234 CacheRec Rec;
235
236 Rec = this.CheckCacheHeaders(FullPath, LastModified, Request);
237
238 if (!(Request.Header.Accept is null))
239 {
240 if (!Request.Header.Accept.IsAcceptable(ContentType))
241 throw new NotAcceptableException();
242 }
243
244 Stream f = File.OpenRead(FullPath);
245
246 await SendResponse(f, FullPath, ContentType, Rec.ETag, LastModified, Response);
247 }
248
249 private static async Task SendResponse(Stream f, string FullPath, string ContentType, string ETag, DateTime LastModified,
250 HttpResponse Response)
251 {
252 ReadProgress Progress = new ReadProgress()
253 {
254 Response = Response,
255 f = f ?? File.OpenRead(FullPath),
256 Next = null,
257 Boundary = null,
258 ContentType = null
259 };
260 Progress.BytesLeft = Progress.TotalLength = Progress.f.Length;
261 Progress.BlockSize = (int)Math.Min(BufferSize, Progress.BytesLeft);
262 Progress.Buffer = new byte[Progress.BlockSize];
263
264 Response.ContentType = ContentType;
265 Response.ContentLength = Progress.TotalLength;
266
267 Response.SetHeader("ETag", ETag);
268 Response.SetHeader("Last-Modified", CommonTypes.EncodeRfc822(LastModified));
269
270 if (Response.OnlyHeader || Progress.TotalLength == 0)
271 {
272 await Response.SendResponse();
273 await Progress.DisposeAsync();
274 }
275 else
276 {
277 Task _ = Progress.BeginRead();
278 }
279 }
280
281 private CacheRec CheckCacheHeaders(string FullPath, DateTime LastModified, HttpRequest Request)
282 {
283 string CacheKey = FullPath.ToLower();
284 HttpRequestHeader Header = Request.Header;
285 CacheRec Rec;
286 DateTimeOffset? Limit;
287
288 lock (this.cacheInfo)
289 {
290 if (this.cacheInfo.TryGetValue(CacheKey, out Rec))
291 {
292 if (Rec.LastModified != LastModified)
293 {
294 this.cacheInfo.Remove(CacheKey);
295 Rec = null;
296 }
297 }
298 }
299
300 if (Rec is null)
301 {
302 Rec = new CacheRec()
303 {
304 LastModified = LastModified,
305 };
306
307 using (FileStream fs = File.OpenRead(FullPath))
308 {
309 Rec.ETag = Hashes.ComputeSHA1HashString(fs);
310 }
311
312 lock (this.cacheInfo)
313 {
314 this.cacheInfo[CacheKey] = Rec;
315 }
316 }
317
318 if (!(Header.IfNoneMatch is null))
319 {
320 if (Header.IfNoneMatch.Value == Rec.ETag)
321 throw new NotModifiedException();
322 }
323 else if (!(Header.IfModifiedSince is null))
324 {
325 if ((Limit = Header.IfModifiedSince.Timestamp).HasValue &&
326 HttpFolderResource.LessOrEqual(LastModified, Limit.Value.ToUniversalTime()))
327 {
328 throw new NotModifiedException();
329 }
330 }
331
332 return Rec;
333 }
334
342 public async Task GET(HttpRequest Request, HttpResponse Response, ByteRangeInterval FirstInterval)
343 {
344 if (!this.fileUploadComponent.TryGetFile(Request.SubPath, out string FullPath, out string ContentType))
345 throw new NotFoundException();
346
347 if (!File.Exists(FullPath))
348 throw new NotFoundException();
349
350 HttpRequestHeader Header = Request.Header;
351 DateTime LastModified = File.GetLastWriteTime(FullPath).ToUniversalTime();
352 DateTimeOffset? Limit;
353 CacheRec Rec;
354
355 if (!(Header.IfRange is null) && (Limit = Header.IfRange.Timestamp).HasValue &&
356 !HttpFolderResource.LessOrEqual(LastModified, Limit.Value.ToUniversalTime()))
357 {
358 Response.StatusCode = 200;
359 await this.GET(Request, Response); // No ranged request.
360 return;
361 }
362
363 Rec = this.CheckCacheHeaders(FullPath, LastModified, Request);
364
365 if (!(Request.Header.Accept is null))
366 {
367 if (!Request.Header.Accept.IsAcceptable(ContentType))
368 throw new NotAcceptableException();
369 }
370
371 Stream f = File.OpenRead(FullPath);
372
373 ReadProgress Progress = new ReadProgress()
374 {
375 Response = Response,
376 f = f ?? File.OpenRead(FullPath)
377 };
378
379 ByteRangeInterval Interval = FirstInterval;
380 Progress.TotalLength = Progress.f.Length;
381
382 long i = 0;
383 long j;
384 long First;
385
386 if (FirstInterval.First.HasValue)
387 First = FirstInterval.First.Value;
388 else
389 First = Progress.TotalLength - FirstInterval.Last.Value;
390
391 Progress.f.Position = First;
392 Progress.BytesLeft = Interval.GetIntervalLength(Progress.TotalLength);
393 Progress.Next = Interval.Next;
394
395 while (!(Interval is null))
396 {
397 j = Interval.GetIntervalLength(Progress.TotalLength);
398 if (j > i)
399 i = j;
400
401 Interval = Interval.Next;
402 }
403
404 Progress.BlockSize = (int)Math.Min(BufferSize, i);
405 Progress.Buffer = new byte[Progress.BlockSize];
406
407 if (FirstInterval.Next is null)
408 {
409 Progress.Boundary = null;
410 Progress.ContentType = null;
411
412 Response.ContentType = ContentType;
413 Response.ContentLength = FirstInterval.GetIntervalLength(Progress.f.Length);
414 Response.SetHeader("Content-Range", ContentByteRangeInterval.ContentRangeToString(First, First + Progress.BytesLeft - 1, Progress.TotalLength));
415 }
416 else
417 {
418 Progress.Boundary = Guid.NewGuid().ToString().Replace("-", string.Empty);
419 Progress.ContentType = ContentType;
420
421 Response.ContentType = "multipart/byteranges; boundary=" + Progress.Boundary;
422 // chunked transfer encoding will be used
423 }
424
425 Response.SetHeader("ETag", Rec.ETag);
426 Response.SetHeader("Last-Modified", CommonTypes.EncodeRfc822(LastModified));
427
428 if (Response.OnlyHeader || Progress.BytesLeft == 0)
429 {
430 await Response.SendResponse();
431 await Progress.DisposeAsync();
432 }
433 else
434 {
435 if (!(FirstInterval.Next is null))
436 {
437 await Response.WriteLine();
438 await Response.WriteLine("--" + Progress.Boundary);
439 await Response.WriteLine("Content-Type: " + Progress.ContentType);
440 await Response.WriteLine("Content-Range: " + ContentByteRangeInterval.ContentRangeToString(First, First + Progress.BytesLeft - 1, Progress.TotalLength));
441 await Response.WriteLine();
442 }
443
444 Task _ = Progress.BeginRead();
445 }
446 }
447
455 public Task PATCH(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
456 {
457 return this.PUT(Request, Response, Interval);
458 }
459
467 public async Task PUT(HttpRequest Request, HttpResponse Response, ContentByteRangeInterval Interval)
468 {
469 try
470 {
471 if (!Request.HasData)
472 throw new BadRequestException();
473
474 string Key = Request.Header["X-Key"];
475
476 if (!await this.fileUploadComponent.DataUploaded(Request.SubPath, Key, Request.DataStream, Interval))
477 throw new ForbiddenException("Upload rejected.");
478
479 Response.StatusCode = 201;
480 Response.StatusMessage = "Created";
481 await Response.SendResponse();
482 }
483 catch (Exception ex)
484 {
485 await Response.SendResponse(ex);
486 }
487 }
488
489 }
490}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:13
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Definition: CommonTypes.cs:667
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:191
Contains information about all fields in an HTTP request header.
HttpFieldIfRange IfRange
If-Range HTTP Field header. (RFC 2616, §14.27)
HttpFieldIfMatch IfMatch
If-Match HTTP Field header. (RFC 2616, §14.24)
HttpFieldAccept Accept
Accept HTTP Field header. (RFC 2616, §14.1)
HttpFieldIfUnmodifiedSince IfUnmodifiedSince
If-Unmodified-Since HTTP Field header. (RFC 2616, §14.28)
HttpFieldIfModifiedSince IfModifiedSince
If-Modified-Since HTTP Field header. (RFC 2616, §14.25)
HttpFieldIfNoneMatch IfNoneMatch
If-None-Match HTTP Field header. (RFC 2616, §14.26)
Represents an HTTP request.
Definition: HttpRequest.cs:18
Stream DataStream
Data stream, if data is available, or null if data is not available.
Definition: HttpRequest.cs:139
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:134
bool HasData
If the request has data.
Definition: HttpRequest.cs:74
string SubPath
Sub-path. If a resource is found handling the request, this property contains the trailing sub-path o...
Definition: HttpRequest.cs:146
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:21
async Task DisposeAsync()
Closes the connection and disposes of all resources.
Task Flush()
Clears all buffers for the current writer and causes any buffered data to be written to the underlyin...
async Task SendResponse()
Sends the response back to the client. If the resource is synchronous, there's no need to call this m...
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).
async Task Write(byte[] Data)
Returns binary data in the response.
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....
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.
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:59
static string ComputeSHA1HashString(byte[] Data)
Computes the SHA-1 hash of a block of binary data.
Definition: Hashes.cs:274
GET Interface for HTTP resources.
Ranged GET Interface for HTTP resources.
Ranged PATCH Interface for HTTP resources.
Ranged PUT Interface for HTTP resources.