diff options
Diffstat (limited to 'SocketHttpListener/Net/HttpListenerRequest.cs')
| -rw-r--r-- | SocketHttpListener/Net/HttpListenerRequest.cs | 937 |
1 files changed, 409 insertions, 528 deletions
diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs index 5e391424f..1b369dfa8 100644 --- a/SocketHttpListener/Net/HttpListenerRequest.cs +++ b/SocketHttpListener/Net/HttpListenerRequest.cs @@ -10,653 +10,534 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; using MediaBrowser.Model.Text; using SocketHttpListener.Primitives; +using System.Collections.Generic; +using SocketHttpListener.Net.WebSockets; namespace SocketHttpListener.Net { - public sealed class HttpListenerRequest + public sealed unsafe partial class HttpListenerRequest { - string[] accept_types; - Encoding content_encoding; - long content_length; - bool cl_set; - CookieCollection cookies; - WebHeaderCollection headers; - string method; - Stream input_stream; - Version version; - QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness - string raw_url; - Uri url; - Uri referrer; - string[] user_languages; - HttpListenerContext context; - bool is_chunked; - bool ka_set; - bool? _keepAlive; - - private readonly ITextEncoding _textEncoding; - - internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding) - { - this.context = context; - _textEncoding = textEncoding; - headers = new WebHeaderCollection(); - version = HttpVersion.Version10; - } + private CookieCollection _cookies; + private bool? _keepAlive; + private string _rawUrl; + private Uri _requestUri; + private Version _version; - static char[] separators = new char[] { ' ' }; + public string[] AcceptTypes => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.Accept]); - internal void SetRequestLine(string req) - { - string[] parts = req.Split(separators, 3); - if (parts.Length != 3) - { - context.ErrorMessage = "Invalid request line (parts)."; - return; - } + public string[] UserLanguages => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.AcceptLanguage]); - method = parts[0]; - foreach (char c in method) - { - int ic = (int)c; - - if ((ic >= 'A' && ic <= 'Z') || - (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && - c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && - c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && - c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) - continue; - - context.ErrorMessage = "(Invalid verb)"; - return; - } - - raw_url = parts[1]; - if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) - { - context.ErrorMessage = "Invalid request line (version)."; - return; - } - - try - { - version = new Version(parts[2].Substring(5)); - if (version.Major < 1) - throw new Exception(); - } - catch - { - context.ErrorMessage = "Invalid request line (version)."; - return; - } + private CookieCollection ParseCookies(Uri uri, string setCookieHeader) + { + CookieCollection cookies = new CookieCollection(); + return cookies; } - void CreateQueryString(string query) + public CookieCollection Cookies { - if (query == null || query.Length == 0) + get { - query_string = new QueryParamCollection(); - return; + if (_cookies == null) + { + string cookieString = Headers[HttpKnownHeaderNames.Cookie]; + if (!string.IsNullOrEmpty(cookieString)) + { + _cookies = ParseCookies(RequestUri, cookieString); + } + if (_cookies == null) + { + _cookies = new CookieCollection(); + } + } + return _cookies; } + } - query_string = new QueryParamCollection(); - if (query[0] == '?') - query = query.Substring(1); - string[] components = query.Split('&'); - foreach (string kv in components) + public Encoding ContentEncoding + { + get { - int pos = kv.IndexOf('='); - if (pos == -1) + if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) { - query_string.Add(null, WebUtility.UrlDecode(kv)); + string postDataCharset = Headers["x-up-devcap-post-charset"]; + if (postDataCharset != null && postDataCharset.Length > 0) + { + try + { + return Encoding.GetEncoding(postDataCharset); + } + catch (ArgumentException) + { + } + } } - else + if (HasEntityBody) { - string key = WebUtility.UrlDecode(kv.Substring(0, pos)); - string val = WebUtility.UrlDecode(kv.Substring(pos + 1)); - - query_string.Add(key, val); + if (ContentType != null) + { + string charSet = Helpers.GetCharSetValueFromHeader(ContentType); + if (charSet != null) + { + try + { + return Encoding.GetEncoding(charSet); + } + catch (ArgumentException) + { + } + } + } } + return TextEncodingExtensions.GetDefaultEncoding(); } } - internal void FinishInitialization() - { - string host = UserHostName; - if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) - { - context.ErrorMessage = "Invalid host name"; - return; - } - - string path; - Uri raw_uri = null; - if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri)) - path = raw_uri.PathAndQuery; - else - path = raw_url; - - if ((host == null || host.Length == 0)) - host = UserHostAddress; - - if (raw_uri != null) - host = raw_uri.Host; - - int colon = host.LastIndexOf(':'); - if (colon >= 0) - host = host.Substring(0, colon); + public string ContentType => Headers[HttpKnownHeaderNames.ContentType]; - string base_uri = String.Format("{0}://{1}:{2}", - (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"), - host, LocalEndPoint.Port); + public bool IsLocal => LocalEndPoint.Address.Equals(RemoteEndPoint.Address); - if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url)) - { - context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path); - return; return; - } - - CreateQueryString(url.Query); - - if (version >= HttpVersion.Version11) + public bool IsWebSocketRequest + { + get { - string t_encoding = Headers["Transfer-Encoding"]; - is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); - // 'identity' is not valid! - if (t_encoding != null && !is_chunked) + if (!SupportsWebSockets) { - context.Connection.SendError(null, 501); - return; + return false; } - } - if (!is_chunked && !cl_set) - { - if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || - String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) + bool foundConnectionUpgradeHeader = false; + if (string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Connection]) || string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Upgrade])) { - context.Connection.SendError(null, 411); - return; + return false; } - } - - if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) - { - var output = (HttpResponseStream)context.Connection.GetResponseStream(true); - - var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); - - output.InternalWrite(_100continue, 0, _100continue.Length); - } - } - - static bool MaybeUri(string s) - { - int p = s.IndexOf(':'); - if (p == -1) - return false; - if (p >= 10) - return false; - - return IsPredefinedScheme(s.Substring(0, p)); - } + foreach (string connection in Headers.GetValues(HttpKnownHeaderNames.Connection)) + { + if (string.Equals(connection, HttpKnownHeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase)) + { + foundConnectionUpgradeHeader = true; + break; + } + } - // - // Using a simple block of if's is twice as slow as the compiler generated - // switch statement. But using this tuned code is faster than the - // compiler generated code, with a million loops on x86-64: - // - // With "http": .10 vs .51 (first check) - // with "https": .16 vs .51 (second check) - // with "foo": .22 vs .31 (never found) - // with "mailto": .12 vs .51 (last check) - // - // - static bool IsPredefinedScheme(string scheme) - { - if (scheme == null || scheme.Length < 3) - return false; + if (!foundConnectionUpgradeHeader) + { + return false; + } - char c = scheme[0]; - if (c == 'h') - return (scheme == "http" || scheme == "https"); - if (c == 'f') - return (scheme == "file" || scheme == "ftp"); + foreach (string upgrade in Headers.GetValues(HttpKnownHeaderNames.Upgrade)) + { + if (string.Equals(upgrade, HttpWebSocket.WebSocketUpgradeToken, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } - if (c == 'n') - { - c = scheme[1]; - if (c == 'e') - return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); - if (scheme == "nntp") - return true; return false; } - if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) - return true; - - return false; } - internal static string Unquote(String str) - { - int start = str.IndexOf('\"'); - int end = str.LastIndexOf('\"'); - if (start >= 0 && end >= 0) - str = str.Substring(start + 1, end - 1); - return str.Trim(); - } - - internal void AddHeader(string header) + public bool KeepAlive { - int colon = header.IndexOf(':'); - if (colon == -1 || colon == 0) - { - context.ErrorMessage = "Bad Request"; - context.ErrorStatus = 400; - return; - } - - string name = header.Substring(0, colon).Trim(); - string val = header.Substring(colon + 1).Trim(); - string lower = name.ToLowerInvariant(); - headers.SetInternal(name, val); - switch (lower) + get { - case "accept-language": - user_languages = val.Split(','); // yes, only split with a ',' - break; - case "accept": - accept_types = val.Split(','); // yes, only split with a ',' - break; - case "content-length": - try - { - //TODO: max. content_length? - content_length = Int64.Parse(val.Trim()); - if (content_length < 0) - context.ErrorMessage = "Invalid Content-Length."; - cl_set = true; - } - catch - { - context.ErrorMessage = "Invalid Content-Length."; - } - - break; - case "content-type": - { - var contents = val.Split(';'); - foreach (var content in contents) - { - var tmp = content.Trim(); - if (tmp.StartsWith("charset")) - { - var charset = tmp.GetValue("="); - if (charset != null && charset.Length > 0) - { - try - { - - // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n - charset = charset.Trim('"'); - var index = charset.IndexOf('"'); - if (index != -1) charset = charset.Substring(0, index); - - content_encoding = Encoding.GetEncoding(charset); - } - catch - { - context.ErrorMessage = "Invalid Content-Type header: " + charset; - } - } - - break; - } - } - } - break; - case "referer": - try - { - referrer = new Uri(val); - } - catch + if (!_keepAlive.HasValue) + { + string header = Headers[HttpKnownHeaderNames.ProxyConnection]; + if (string.IsNullOrEmpty(header)) { - referrer = new Uri("http://someone.is.screwing.with.the.headers.com/"); + header = Headers[HttpKnownHeaderNames.Connection]; } - break; - case "cookie": - if (cookies == null) - cookies = new CookieCollection(); - - string[] cookieStrings = val.Split(new char[] { ',', ';' }); - Cookie current = null; - int version = 0; - foreach (string cookieString in cookieStrings) + if (string.IsNullOrEmpty(header)) { - string str = cookieString.Trim(); - if (str.Length == 0) - continue; - if (str.StartsWith("$Version")) - { - version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1))); - } - else if (str.StartsWith("$Path")) - { - if (current != null) - current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); - } - else if (str.StartsWith("$Domain")) - { - if (current != null) - current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); - } - else if (str.StartsWith("$Port")) + if (ProtocolVersion >= HttpVersion.Version11) { - if (current != null) - current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + _keepAlive = true; } else { - if (current != null) - { - cookies.Add(current); - } - current = new Cookie(); - int idx = str.IndexOf('='); - if (idx > 0) - { - current.Name = str.Substring(0, idx).Trim(); - current.Value = str.Substring(idx + 1).Trim(); - } - else - { - current.Name = str.Trim(); - current.Value = String.Empty; - } - current.Version = version; + header = Headers[HttpKnownHeaderNames.KeepAlive]; + _keepAlive = !string.IsNullOrEmpty(header); } } - if (current != null) - { - cookies.Add(current); - } - break; - } - } - - // returns true is the stream could be reused. - internal bool FlushInput() - { - if (!HasEntityBody) - return true; - - int length = 2048; - if (content_length > 0) - length = (int)Math.Min(content_length, (long)length); - - byte[] bytes = new byte[length]; - while (true) - { - // TODO: test if MS has a timeout when doing this - try - { - var task = InputStream.ReadAsync(bytes, 0, length); - var result = Task.WaitAll(new [] { task }, 1000); - if (!result) - { - return false; - } - if (task.Result <= 0) + else { - return true; + header = header.ToLower(CultureInfo.InvariantCulture); + _keepAlive = + header.IndexOf("close", StringComparison.OrdinalIgnoreCase) < 0 || + header.IndexOf("keep-alive", StringComparison.OrdinalIgnoreCase) >= 0; } } - catch (ObjectDisposedException e) - { - input_stream = null; - return true; - } - catch - { - return false; - } - } - } - public string[] AcceptTypes - { - get { return accept_types; } + return _keepAlive.Value; + } } - public int ClientCertificateError + public QueryParamCollection QueryString { get { - HttpConnection cnc = context.Connection; - //if (cnc.ClientCertificate == null) - // throw new InvalidOperationException("No client certificate"); - //int[] errors = cnc.ClientCertificateErrors; - //if (errors != null && errors.Length > 0) - // return errors[0]; - return 0; + QueryParamCollection queryString = new QueryParamCollection(); + Helpers.FillFromString(queryString, Url.Query, true, ContentEncoding); + return queryString; } } - public Encoding ContentEncoding + public string RawUrl => _rawUrl; + + private string RequestScheme => IsSecureConnection ? UriScheme.Https : UriScheme.Http; + + public string UserAgent => Headers[HttpKnownHeaderNames.UserAgent]; + + public string UserHostAddress => LocalEndPoint.ToString(); + + public string UserHostName => Headers[HttpKnownHeaderNames.Host]; + + public Uri UrlReferrer { get { - if (content_encoding == null) - content_encoding = _textEncoding.GetDefaultEncoding(); - return content_encoding; + string referrer = Headers[HttpKnownHeaderNames.Referer]; + if (referrer == null) + { + return null; + } + + bool success = Uri.TryCreate(referrer, UriKind.RelativeOrAbsolute, out Uri urlReferrer); + return success ? urlReferrer : null; } } - public long ContentLength64 - { - get { return is_chunked ? -1 : content_length; } - } + public Uri Url => RequestUri; - public string ContentType - { - get { return headers["content-type"]; } - } + public Version ProtocolVersion => _version; - public CookieCollection Cookies + private static class Helpers { - get + // + // Get attribute off header value + // + internal static string GetCharSetValueFromHeader(string headerValue) { - // TODO: check if the collection is read-only - if (cookies == null) - cookies = new CookieCollection(); - return cookies; - } - } + const string AttrName = "charset"; - public bool HasEntityBody - { - get { return (content_length > 0 || is_chunked); } - } + if (headerValue == null) + return null; - public QueryParamCollection Headers - { - get { return headers; } - } + int l = headerValue.Length; + int k = AttrName.Length; - public string HttpMethod - { - get { return method; } - } + // find properly separated attribute name + int i = 1; // start searching from 1 - public Stream InputStream - { - get - { - if (input_stream == null) + while (i < l) { - if (is_chunked || content_length > 0) - input_stream = context.Connection.GetRequestStream(is_chunked, content_length); - else - input_stream = Stream.Null; + i = CultureInfo.InvariantCulture.CompareInfo.IndexOf(headerValue, AttrName, i, CompareOptions.IgnoreCase); + if (i < 0) + break; + if (i + k >= l) + break; + + char chPrev = headerValue[i - 1]; + char chNext = headerValue[i + k]; + if ((chPrev == ';' || chPrev == ',' || char.IsWhiteSpace(chPrev)) && (chNext == '=' || char.IsWhiteSpace(chNext))) + break; + + i += k; } - return input_stream; - } - } + if (i < 0 || i >= l) + return null; - public bool IsAuthenticated - { - get { return false; } - } + // skip to '=' and the following whitespace + i += k; + while (i < l && char.IsWhiteSpace(headerValue[i])) + i++; + if (i >= l || headerValue[i] != '=') + return null; + i++; + while (i < l && char.IsWhiteSpace(headerValue[i])) + i++; + if (i >= l) + return null; - public bool IsLocal - { - get + // parse the value + string attrValue = null; + + int j; + + if (i < l && headerValue[i] == '"') + { + if (i == l - 1) + return null; + j = headerValue.IndexOf('"', i + 1); + if (j < 0 || j == i + 1) + return null; + + attrValue = headerValue.Substring(i + 1, j - i - 1).Trim(); + } + else + { + for (j = i; j < l; j++) + { + if (headerValue[j] == ';') + break; + } + + if (j == i) + return null; + + attrValue = headerValue.Substring(i, j - i).Trim(); + } + + return attrValue; + } + + internal static string[] ParseMultivalueHeader(string s) { - var remoteEndPoint = RemoteEndPoint; + if (s == null) + return null; + + int l = s.Length; + + // collect comma-separated values into list + + List<string> values = new List<string>(); + int i = 0; + + while (i < l) + { + // find next , + int ci = s.IndexOf(',', i); + if (ci < 0) + ci = l; + + // append corresponding server value + values.Add(s.Substring(i, ci - i)); + + // move to next + i = ci + 1; + + // skip leading space + if (i < l && s[i] == ' ') + i++; + } - return remoteEndPoint.Address.Equals(IPAddress.Loopback) || - remoteEndPoint.Address.Equals(IPAddress.IPv6Loopback) || - LocalEndPoint.Address.Equals(remoteEndPoint.Address); + // return list as array of strings + + int n = values.Count; + string[] strings; + + // if n is 0 that means s was empty string + + if (n == 0) + { + strings = new string[1]; + strings[0] = string.Empty; + } + else + { + strings = new string[n]; + values.CopyTo(0, strings, 0, n); + } + return strings; } - } - public bool IsSecureConnection - { - get { return context.Connection.IsSecure; } - } - public bool KeepAlive - { - get + private static string UrlDecodeStringFromStringInternal(string s, Encoding e) { - if (!_keepAlive.HasValue) + int count = s.Length; + UrlDecoder helper = new UrlDecoder(count, e); + + // go through the string's chars collapsing %XX and %uXXXX and + // appending each char as char, with exception of %XX constructs + // that are appended as bytes + + for (int pos = 0; pos < count; pos++) { - string header = Headers["Proxy-Connection"]; - if (string.IsNullOrEmpty(header)) + char ch = s[pos]; + + if (ch == '+') { - header = Headers["Connection"]; + ch = ' '; } - if (string.IsNullOrEmpty(header)) + else if (ch == '%' && pos < count - 2) { - if (ProtocolVersion >= HttpVersion.Version11) + if (s[pos + 1] == 'u' && pos < count - 5) { - _keepAlive = true; + int h1 = HexToInt(s[pos + 2]); + int h2 = HexToInt(s[pos + 3]); + int h3 = HexToInt(s[pos + 4]); + int h4 = HexToInt(s[pos + 5]); + + if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) + { // valid 4 hex chars + ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4); + pos += 5; + + // only add as char + helper.AddChar(ch); + continue; + } } else { - header = Headers["Keep-Alive"]; - _keepAlive = !string.IsNullOrEmpty(header); + int h1 = HexToInt(s[pos + 1]); + int h2 = HexToInt(s[pos + 2]); + + if (h1 >= 0 && h2 >= 0) + { // valid 2 hex chars + byte b = (byte)((h1 << 4) | h2); + pos += 2; + + // don't add as char + helper.AddByte(b); + continue; + } } } + + if ((ch & 0xFF80) == 0) + helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode else - { - header = header.ToLower(CultureInfo.InvariantCulture); - _keepAlive = - header.IndexOf("close", StringComparison.OrdinalIgnoreCase) < 0 || - header.IndexOf("keep-alive", StringComparison.OrdinalIgnoreCase) >= 0; - } + helper.AddChar(ch); } - return _keepAlive.Value; + return helper.GetString(); } - } - public IPEndPoint LocalEndPoint - { - get { return context.Connection.LocalEndPoint; } - } + private static int HexToInt(char h) + { + return (h >= '0' && h <= '9') ? h - '0' : + (h >= 'a' && h <= 'f') ? h - 'a' + 10 : + (h >= 'A' && h <= 'F') ? h - 'A' + 10 : + -1; + } - public Version ProtocolVersion - { - get { return version; } - } + private class UrlDecoder + { + private int _bufferSize; - public QueryParamCollection QueryString - { - get { return query_string; } - } + // Accumulate characters in a special array + private int _numChars; + private char[] _charBuffer; - public string RawUrl - { - get { return raw_url; } - } + // Accumulate bytes for decoding into characters in a special array + private int _numBytes; + private byte[] _byteBuffer; - public IPEndPoint RemoteEndPoint - { - get { return context.Connection.RemoteEndPoint; } - } + // Encoding to convert chars to bytes + private Encoding _encoding; - public Guid RequestTraceIdentifier - { - get { return Guid.Empty; } - } + private void FlushBytes() + { + if (_numBytes > 0) + { + _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); + _numBytes = 0; + } + } - public Uri Url - { - get { return url; } - } + internal UrlDecoder(int bufferSize, Encoding encoding) + { + _bufferSize = bufferSize; + _encoding = encoding; - public Uri UrlReferrer - { - get { return referrer; } - } + _charBuffer = new char[bufferSize]; + // byte buffer created on demand + } - public string UserAgent - { - get { return headers["user-agent"]; } - } + internal void AddChar(char ch) + { + if (_numBytes > 0) + FlushBytes(); - public string UserHostAddress - { - get { return LocalEndPoint.ToString(); } - } + _charBuffer[_numChars++] = ch; + } - public string UserHostName - { - get { return headers["host"]; } - } + internal void AddByte(byte b) + { + { + if (_byteBuffer == null) + _byteBuffer = new byte[_bufferSize]; - public string[] UserLanguages - { - get { return user_languages; } - } + _byteBuffer[_numBytes++] = b; + } + } - public string ServiceName - { - get - { - return null; + internal string GetString() + { + if (_numBytes > 0) + FlushBytes(); + + if (_numChars > 0) + return new string(_charBuffer, 0, _numChars); + else + return string.Empty; + } } - } - private bool _websocketRequestWasSet; - private bool _websocketRequest; - /// <summary> - /// Gets a value indicating whether the request is a WebSocket connection request. - /// </summary> - /// <value> - /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>. - /// </value> - public bool IsWebSocketRequest - { - get + internal static void FillFromString(QueryParamCollection nvc, string s, bool urlencoded, Encoding encoding) { - if (!_websocketRequestWasSet) + int l = (s != null) ? s.Length : 0; + int i = (s.Length > 0 && s[0] == '?') ? 1 : 0; + + while (i < l) { - _websocketRequest = method == "GET" && - version > HttpVersion.Version10 && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); + // find next & while noting first = on the way (and if there are more) - _websocketRequestWasSet = true; - } + int si = i; + int ti = -1; - return _websocketRequest; + while (i < l) + { + char ch = s[i]; + + if (ch == '=') + { + if (ti < 0) + ti = i; + } + else if (ch == '&') + { + break; + } + + i++; + } + + // extract the name / value pair + + string name = null; + string value = null; + + if (ti >= 0) + { + name = s.Substring(si, ti - si); + value = s.Substring(ti + 1, i - ti - 1); + } + else + { + value = s.Substring(si, i - si); + } + + // add name / value pair to the collection + + if (urlencoded) + nvc.Add( + name == null ? null : UrlDecodeStringFromStringInternal(name, encoding), + UrlDecodeStringFromStringInternal(value, encoding)); + else + nvc.Add(name, value); + + // trailing '&' + + if (i == l - 1 && s[i] == '&') + nvc.Add(null, ""); + + i++; + } } } } |
