using System; using System.Collections.Generic; using System.IO; using Best.HTTP.Hosts.Connections; using Best.HTTP.Shared.Extensions; using Best.HTTP.Shared.PlatformSupport.Memory; using Best.HTTP.Shared.Streams; using UnityEngine; using static Best.HTTP.Hosts.Connections.HTTP1.Constants; namespace Best.HTTP.Request.Upload.Forms { /// /// An based implementation of the multipart/form-data Content-Type. It's very memory-effective, streams are read into memory in chunks. /// /// /// The return value of is treated specially in the plugin: /// /// /// Less than zero(-1) value /// indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled. /// /// /// Zero (0) /// means that the stream is closed, no more data can be expected. /// /// /// A zero value to signal stream closure can follow a less than zero value. /// public sealed class MultipartFormDataStream : UploadStreamBase { /// /// Gets the length of this multipart/form-data stream. /// public override long Length { get => this._length; } private long _length; /// /// A random boundary generated in the constructor. /// private string boundary; private Queue fields = new Queue(1); private StreamList currentField; /// /// Initializes a new instance of the MultipartFormDataStream class. /// public MultipartFormDataStream() { var hash = new Hash128(); hash.Append(this.GetHashCode()); this.boundary = $"com.Tivadar.Best.HTTP.boundary.{hash}"; } /// /// Initializes a new instance of the MultipartFormDataStream class with a custom boundary. /// public MultipartFormDataStream(string boundary) { this.boundary = boundary; } public override void BeforeSendHeaders(HTTPRequest request) { request.SetHeader("Content-Type", $"multipart/form-data; boundary=\"{this.boundary}\""); var boundaryStream = new BufferPoolMemoryStream(); boundaryStream.WriteLine("--" + this.boundary + "--"); boundaryStream.Position = 0; this.fields.Enqueue(new StreamList(boundaryStream)); if (this._length >= 0) this._length += boundaryStream.Length; } /// /// Adds a textual field to the multipart/form-data stream. /// /// The name of the field. /// The textual value of the field. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddField(string fieldName, string value) => AddField(fieldName, value, System.Text.Encoding.UTF8); /// /// Adds a textual field to the multipart/form-data stream. /// /// The name of the field. /// The textual value of the field. /// The encoding to use for the value. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddField(string fieldName, string value, System.Text.Encoding encoding) { var enc = encoding ?? System.Text.Encoding.UTF8; var byteCount = enc.GetByteCount(value); var buffer = BufferPool.Get(byteCount, true); var stream = new BufferPoolMemoryStream(); enc.GetBytes(value, 0, value.Length, buffer, 0); stream.Write(buffer, 0, byteCount); stream.Position = 0; string mime = encoding != null ? "text/plain; charset=" + encoding.WebName : null; return AddStreamField(fieldName, stream, null, mime); } /// /// Adds a stream field to the multipart/form-data stream. /// /// The name of the field. /// The data containing the field data. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddField(string fieldName, byte[] data) => AddStreamField(fieldName, new MemoryStream(data)); /// /// Adds a stream field to the multipart/form-data stream. /// /// The stream containing the field data. /// The name of the field. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream) => AddStreamField(fieldName, stream, null, null); /// /// Adds a stream field to the multipart/form-data stream. /// /// The stream containing the field data. /// The name of the field. /// The name of the file, if applicable. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName) => AddStreamField(fieldName, stream, fileName, null); /// /// Adds a stream field to the multipart/form-data stream. /// /// The stream containing the field data. /// The name of the field. /// The name of the file, if applicable. /// The MIME type of the content. /// The MultipartFormDataStream instance for method chaining. public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName, string mimeType) { var header = new BufferPoolMemoryStream(); header.WriteLine("--" + this.boundary); header.WriteLine("Content-Disposition: form-data; name=\"" + fieldName + "\"" + (!string.IsNullOrEmpty(fileName) ? "; filename=\"" + fileName + "\"" : string.Empty)); // Set up Content-Type head for the form. if (!string.IsNullOrEmpty(mimeType)) header.WriteLine("Content-Type: " + mimeType); header.WriteLine(); header.Position = 0; var footer = new BufferPoolMemoryStream(); footer.Write(EOL, 0, EOL.Length); footer.Position = 0; // all wrapped streams going to be disposed by the StreamList wrapper. var wrapper = new StreamList(header, stream, footer); try { if (this._length >= 0) this._length += wrapper.Length; } catch { this._length = -1; } this.fields.Enqueue(wrapper); return this; } /// /// Adds the final boundary to the multipart/form-data stream before sending the request body. /// /// The HTTP request. /// The thread signaler for handling asynchronous operations. public override void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler) { base.BeforeSendBody(request, threadSignaler); } /// /// Reads data from the multipart/form-data stream into the provided buffer. /// /// The buffer to read data into. /// The starting offset in the buffer. /// The maximum number of bytes to read. /// The number of bytes read into the buffer. public override int Read(byte[] buffer, int offset, int length) { if (this.currentField == null && this.fields.Count == 0) return -1; if (this.currentField == null && this.fields.Count > 0) this.currentField = this.fields.Dequeue(); int readCount = 0; do { // read from the current stream int count = this.currentField.Read(buffer, offset + readCount, length - readCount); if (count > 0) readCount += count; else { // if the current field's stream is empty, go for the next one. // dispose the current one first try { this.currentField.Dispose(); } catch { } // no more fields/streams? exit if (this.fields.Count == 0) break; // grab the next one this.currentField = this.fields.Dequeue(); } // exit when we reach the length goal, or there's no more streams to read from } while (readCount < length && this.fields.Count > 0); return readCount; } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (fields != null) { foreach (var field in fields) field.Dispose(); fields.Clear(); fields = null; } currentField?.Dispose(); currentField = null; } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return false; } } public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); public override void SetLength(long value) => throw new NotImplementedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); public override void Flush() { } } }