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) { return 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); } } } } void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value) { string decodedKey = WebUtility.UrlDecode(key.ToString()); form.Add(decodedKey, 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(); 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 sealed 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 HYPHEN = (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(); } 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(); } 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 ""; return l.Substring(begin, end - begin); } 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 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); } bool ReadBoundary() { try { string line = ReadLine(); while (line == "") line = ReadLine(); if (line[0] != '-' || line[1] != '-') return false; if (!StrUtils.EndsWith(line, boundary, false)) return true; } catch { } return false; } string ReadHeaders() { string s = ReadLine(); if (s == "") return null; return s; } bool CompareBytes(byte[] orig, byte[] other) { for (int i = orig.Length - 1; i >= 0; i--) if (orig[i] != other[i]) return false; return true; } 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; } static string StripPath(string path) { if (path == null || path.Length == 0) return path; if (path.IndexOf(":\\") != 1 && !path.StartsWith("\\\\")) return path; return path.Substring(path.LastIndexOf('\\') + 1); } } } }