using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using MediaBrowser.Model.Services; namespace Jellyfin.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { static internal string GetParameter(string header, string attr) { int ap = header.IndexOf(attr); if (ap == -1) { return null; } ap += attr.Length; if (ap >= header.Length) { return null; } char ending = header[ap]; if (ending != '"') { ending = ' '; } int end = header.IndexOf(ending, ap + 1); if (end == -1) { return ending == '"' ? null : header.Substring(ap); } return header.Substring(ap + 1, end - ap - 1); } async Task LoadMultiPart(WebROCollection form) { string boundary = GetParameter(ContentType, "; boundary="); if (boundary == null) { return; } using (var requestStream = InputStream) { //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request //Not ending with \r\n? var ms = new MemoryStream(32 * 1024); await requestStream.CopyToAsync(ms).ConfigureAwait(false); var input = ms; ms.WriteByte((byte)'\r'); ms.WriteByte((byte)'\n'); input.Position = 0; // Uncomment to debug //var content = new StreamReader(ms).ReadToEnd(); //Console.WriteLine(boundary + "::" + content); //input.Position = 0; var multi_part = new HttpMultipart(input, boundary, ContentEncoding); HttpMultipart.Element e; while ((e = multi_part.ReadNextElement()) != null) { if (e.Filename == null) { byte[] copy = new byte[e.Length]; input.Position = e.Start; input.Read(copy, 0, (int)e.Length); form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); } else { // // We use a substream, as in 2.x we will support large uploads streamed to disk, // var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); files[e.Name] = sub; } } } } public async Task GetFormData() { var form = new WebROCollection(); files = new Dictionary(); if (IsContentType("multipart/form-data", true)) { await LoadMultiPart(form).ConfigureAwait(false); } else if (IsContentType("application/x-www-form-urlencoded", true)) { await LoadWwwForm(form).ConfigureAwait(false); } #if NET_4_0 if (validateRequestNewMode && !checked_form) { // Setting this before calling the validator prevents // possible endless recursion checked_form = true; ValidateNameValueCollection ("Form", query_string_nvc, RequestValidationSource.Form); } else #endif if (validate_form && !checked_form) { checked_form = true; ValidateNameValueCollection("Form", form); } return form; } public string Accept => string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"]; public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"]; protected bool validate_cookies, validate_query_string, validate_form; protected bool checked_cookies, checked_query_string, checked_form; static void ThrowValidationException(string name, string key, string value) { string v = "\"" + value + "\""; if (v.Length > 20) { v = v.Substring(0, 16) + "...\""; } string msg = string.Format("A potentially dangerous Request.{0} value was " + "detected from the client ({1}={2}).", name, key, v); throw new Exception(msg); } static void ValidateNameValueCollection(string name, QueryParamCollection coll) { if (coll == null) { return; } foreach (var pair in coll) { var key = pair.Name; var val = pair.Value; if (val != null && val.Length > 0 && IsInvalidString(val)) { ThrowValidationException(name, key, val); } } } internal static bool IsInvalidString(string val) => IsInvalidString(val, out var validationFailureIndex); internal static bool IsInvalidString(string val, out int validationFailureIndex) { validationFailureIndex = 0; int len = val.Length; if (len < 2) { return false; } char current = val[0]; for (int idx = 1; idx < len; idx++) { char next = val[idx]; // See http://secunia.com/advisories/14325 if (current == '<' || current == '\xff1c') { if (next == '!' || next < ' ' || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z')) { validationFailureIndex = idx - 1; return true; } } else if (current == '&' && next == '#') { validationFailureIndex = idx - 1; return true; } current = next; } return false; } public void ValidateInput() { validate_cookies = true; validate_query_string = true; validate_form = true; } bool IsContentType(string ct, bool starts_with) { if (ct == null || ContentType == null) { return false; } if (starts_with) { return StrUtils.StartsWith(ContentType, ct, true); } return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); } async Task LoadWwwForm(WebROCollection form) { using (var input = InputStream) { using (var ms = new MemoryStream()) { await input.CopyToAsync(ms).ConfigureAwait(false); ms.Position = 0; using (var s = new StreamReader(ms, ContentEncoding)) { var key = new StringBuilder(); var value = new StringBuilder(); int c; while ((c = s.Read()) != -1) { if (c == '=') { value.Length = 0; while ((c = s.Read()) != -1) { if (c == '&') { AddRawKeyValue(form, key, value); break; } else { value.Append((char)c); } } if (c == -1) { AddRawKeyValue(form, key, value); return; } } else if (c == '&') { AddRawKeyValue(form, key, value); } else { key.Append((char)c); } } if (c == -1) { AddRawKeyValue(form, key, value); } } } } } static void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value) { form.Add(WebUtility.UrlDecode(key.ToString()), WebUtility.UrlDecode(value.ToString())); key.Length = 0; value.Length = 0; } Dictionary files; class WebROCollection : QueryParamCollection { public override string ToString() { var result = new StringBuilder(); foreach (var pair in this) { if (result.Length > 0) { result.Append('&'); } var key = pair.Name; if (key != null && key.Length > 0) { result.Append(key); result.Append('='); } result.Append(pair.Value); } return result.ToString(); } } public sealed class HttpPostedFile { string name; string content_type; Stream stream; class ReadSubStream : Stream { Stream s; long offset; long end; long position; public ReadSubStream(Stream s, long offset, long length) { this.s = s; this.offset = offset; this.end = offset + length; position = offset; } public override void Flush() { } public override int Read(byte[] buffer, int dest_offset, int count) { if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } if (dest_offset < 0) { throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); } if (count < 0) { throw new ArgumentOutOfRangeException(nameof(count), "< 0"); } int len = buffer.Length; if (dest_offset > len) { throw new ArgumentException("destination offset is beyond array size"); } // reordered to avoid possible integer overflow if (dest_offset > len - count) { throw new ArgumentException("Reading would overrun buffer"); } if (count > end - position) { count = (int)(end - position); } if (count <= 0) { return 0; } s.Position = position; int result = s.Read(buffer, dest_offset, count); if (result > 0) { position += result; } else { position = end; } return result; } public override int ReadByte() { if (position >= end) { return -1; } s.Position = position; int result = s.ReadByte(); if (result < 0) { position = end; } else { position++; } return result; } public override long Seek(long d, SeekOrigin origin) { long real; switch (origin) { case SeekOrigin.Begin: real = offset + d; break; case SeekOrigin.End: real = end + d; break; case SeekOrigin.Current: real = position + d; break; default: throw new ArgumentException(); } long virt = real - offset; if (virt < 0 || virt > Length) { throw new ArgumentException(); } position = s.Seek(real, SeekOrigin.Begin); return position; } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; public override long Length => end - offset; public override long Position { get => position - offset; set { if (value > Length) { throw new ArgumentOutOfRangeException(nameof(value)); } position = Seek(value, SeekOrigin.Begin); } } } internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) { this.name = name; this.content_type = content_type; this.stream = new ReadSubStream(base_stream, offset, length); } public string ContentType => content_type; public int ContentLength => (int)stream.Length; public string FileName => name; public Stream InputStream => stream; } class Helpers { public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture; } internal static class StrUtils { public static bool StartsWith(string str1, string str2, bool ignore_case) { if (string.IsNullOrEmpty(str1)) { return false; } var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; return str1.IndexOf(str2, comparison) == 0; } public static bool EndsWith(string str1, string str2, bool ignore_case) { int l2 = str2.Length; if (l2 == 0) { return true; } int l1 = str1.Length; if (l2 > l1) { return false; } var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; return str1.IndexOf(str2, comparison) == str1.Length - str2.Length - 1; } } class HttpMultipart { public class Element { public string ContentType; public string Name; public string Filename; public Encoding Encoding; public long Start; public long Length; public override string ToString() { return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + Start.ToString() + ", Length " + Length.ToString(); } } Stream data; string boundary; byte[] boundary_bytes; byte[] buffer; bool at_eof; Encoding encoding; StringBuilder sb; const byte LF = (byte)'\n', CR = (byte)'\r'; // See RFC 2046 // In the case of multipart entities, in which one or more different // sets of data are combined in a single body, a "multipart" media type // field must appear in the entity's header. The body must then contain // one or more body parts, each preceded by a boundary delimiter line, // and the last one followed by a closing boundary delimiter line. // After its boundary delimiter line, each body part then consists of a // header area, a blank line, and a body area. Thus a body part is // similar to an RFC 822 message in syntax, but different in meaning. public HttpMultipart(Stream data, string b, Encoding encoding) { this.data = data; //DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET //var ms = new MemoryStream(32 * 1024); //data.CopyTo(ms); //this.data = ms; boundary = b; boundary_bytes = encoding.GetBytes(b); buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--' this.encoding = encoding; sb = new StringBuilder(); } private string ReadLine() { // CRLF or LF are ok as line endings. bool got_cr = false; int b = 0; sb.Length = 0; while (true) { b = data.ReadByte(); if (b == -1) { return null; } if (b == LF) { break; } got_cr = b == CR; sb.Append((char)b); } if (got_cr) { sb.Length--; } return sb.ToString(); } private static string GetContentDispositionAttribute(string l, string name) { int idx = l.IndexOf(name + "=\""); if (idx < 0) { return null; } int begin = idx + name.Length + "=\"".Length; int end = l.IndexOf('"', begin); if (end < 0) { return null; } if (begin == end) { return string.Empty; } return l.Substring(begin, end - begin); } private string GetContentDispositionAttributeWithEncoding(string l, string name) { int idx = l.IndexOf(name + "=\""); if (idx < 0) { return null; } int begin = idx + name.Length + "=\"".Length; int end = l.IndexOf('"', begin); if (end < 0) { return null; } if (begin == end) { return string.Empty; } string temp = l.Substring(begin, end - begin); byte[] source = new byte[temp.Length]; for (int i = temp.Length - 1; i >= 0; i--) { source[i] = (byte)temp[i]; } return encoding.GetString(source, 0, source.Length); } private bool ReadBoundary() { try { string line = ReadLine(); while (line == string.Empty) { line = ReadLine(); } if (line[0] != '-' || line[1] != '-') { return false; } if (!StrUtils.EndsWith(line, boundary, false)) { return true; } } catch { } return false; } private string ReadHeaders() { string s = ReadLine(); if (s.Length == 0) { return null; } return s; } private static bool CompareBytes(byte[] orig, byte[] other) { for (int i = orig.Length - 1; i >= 0; i--) { if (orig[i] != other[i]) { return false; } } return true; } private long MoveToNextBoundary() { long retval = 0; bool got_cr = false; int state = 0; int c = data.ReadByte(); while (true) { if (c == -1) { return -1; } if (state == 0 && c == LF) { retval = data.Position - 1; if (got_cr) { retval--; } state = 1; c = data.ReadByte(); } else if (state == 0) { got_cr = c == CR; c = data.ReadByte(); } else if (state == 1 && c == '-') { c = data.ReadByte(); if (c == -1) { return -1; } if (c != '-') { state = 0; got_cr = false; continue; // no ReadByte() here } int nread = data.Read(buffer, 0, buffer.Length); int bl = buffer.Length; if (nread != bl) { return -1; } if (!CompareBytes(boundary_bytes, buffer)) { state = 0; data.Position = retval + 2; if (got_cr) { data.Position++; got_cr = false; } c = data.ReadByte(); continue; } if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') { at_eof = true; } else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) { state = 0; data.Position = retval + 2; if (got_cr) { data.Position++; got_cr = false; } c = data.ReadByte(); continue; } data.Position = retval + 2; if (got_cr) { data.Position++; } break; } else { // state == 1 state = 0; // no ReadByte() here } } return retval; } public Element ReadNextElement() { if (at_eof || ReadBoundary()) { return null; } var elem = new Element(); string header; while ((header = ReadHeaders()) != null) { if (StrUtils.StartsWith(header, "Content-Disposition:", true)) { elem.Name = GetContentDispositionAttribute(header, "name"); elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); } else if (StrUtils.StartsWith(header, "Content-Type:", true)) { elem.ContentType = header.Substring("Content-Type:".Length).Trim(); elem.Encoding = GetEncoding(elem.ContentType); } } long start = 0; start = data.Position; elem.Start = start; long pos = MoveToNextBoundary(); if (pos == -1) { return null; } elem.Length = pos - start; return elem; } private static string StripPath(string path) { if (path == null || path.Length == 0) { return path; } if (path.IndexOf(":\\", StringComparison.Ordinal) != 1 && !path.StartsWith("\\\\", StringComparison.Ordinal)) { return path; } return path.Substring(path.LastIndexOf('\\') + 1); } } } }