static void AssertQueryString(string expected, string actual) { var ed = new QueryStringDecoder(expected); var ad = new QueryStringDecoder(actual); Assert.Equal(ed.Path, ad.Path); IDictionary <string, List <string> > edParams = ed.Parameters; IDictionary <string, List <string> > adParams = ad.Parameters; Assert.Equal(edParams.Count, adParams.Count); foreach (string name in edParams.Keys) { List <string> expectedValues = edParams[name]; Assert.True(adParams.ContainsKey(name)); List <string> values = adParams[name]; Assert.Equal(expectedValues.Count, values.Count); foreach (string value in expectedValues) { Assert.Contains(value, values); } } }
public void UriSlashPath() { var uri = new Uri("http://localhost:8080/?param1=value1¶m2=value2¶m3=value3"); var d = new QueryStringDecoder(uri); Assert.Equal("/", d.Path); IDictionary <string, List <string> > parameters = d.Parameters; Assert.Equal(3, parameters.Count); KeyValuePair <string, List <string> > entry = parameters.ElementAt(0); Assert.Equal("param1", entry.Key); Assert.Single(entry.Value); Assert.Equal("value1", entry.Value[0]); entry = parameters.ElementAt(1); Assert.Equal("param2", entry.Key); Assert.Single(entry.Value); Assert.Equal("value2", entry.Value[0]); entry = parameters.ElementAt(2); Assert.Equal("param3", entry.Key); Assert.Single(entry.Value); Assert.Equal("value3", entry.Value[0]); }
internal ParameterParser(QueryStringDecoder decoder, Configuration conf) { this.path = DecodeComponent(Sharpen.Runtime.Substring(decoder.Path(), WebHdfsHandler .WebhdfsPrefixLength), Charsets.Utf8); this.@params = decoder.Parameters(); this.conf = conf; }
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpRequest msg) { StringBuilder stringBuilder = new StringBuilder("请求参数:\r\n"); { if (msg.Method == HttpMethod.Get) { var fer = new QueryStringDecoder(msg.Uri); foreach (var item in fer.Parameters) { stringBuilder.AppendFormat("{0}:{1}\r\n", item.Key, item.Value[0]); } } if (msg.Method == HttpMethod.Post) { var postRequestDecoder = new HttpPostRequestDecoder(msg).Offer(msg); foreach (var item in postRequestDecoder.GetBodyHttpDatas()) { var mixedAttribute = postRequestDecoder.Next() as MixedAttribute; stringBuilder.AppendFormat("{0}:{1}\r\n", mixedAttribute?.Name, mixedAttribute?.Value); mixedAttribute.Release(); } } byte[] text = Encoding.UTF8.GetBytes(stringBuilder.ToString()); WriteResponse(ctx, Unpooled.WrappedBuffer(text), AsciiString.Cached("text/plain"), AsciiString.Cached(text.Length.ToString())); } }
private static string GetOp(QueryStringDecoder decoder) { IDictionary <string, IList <string> > parameters = decoder.Parameters(); return(parameters.Contains("op") ? StringUtils.ToUpperCase(parameters["op"][0]) : null); }
public void UriNoPath() { var uri = new Uri("http://localhost:8080?param1=value1¶m2=value2¶m3=value3"); var d = new QueryStringDecoder(uri); // The path component cannot be empty string, // if there are no path component, it shoudl be '/' as above UriSlashPath test Assert.Equal("/", d.Path); IDictionary <string, List <string> > parameters = d.Parameters; Assert.Equal(3, parameters.Count); KeyValuePair <string, List <string> > entry = parameters.ElementAt(0); Assert.Equal("param1", entry.Key); Assert.Single(entry.Value); Assert.Equal("value1", entry.Value[0]); entry = parameters.ElementAt(1); Assert.Equal("param2", entry.Key); Assert.Single(entry.Value); Assert.Equal("value2", entry.Value[0]); entry = parameters.ElementAt(2); Assert.Equal("param3", entry.Key); Assert.Single(entry.Value); Assert.Equal("value3", entry.Value[0]); }
public virtual void TestDecodePath() { string EscapedPath = "/test%25+1%26%3Dtest?op=OPEN&foo=bar"; string ExpectedPath = "/test%+1&=test"; Configuration conf = new Configuration(); QueryStringDecoder decoder = new QueryStringDecoder(WebHdfsHandler.WebhdfsPrefix + EscapedPath); ParameterParser testParser = new ParameterParser(decoder, conf); NUnit.Framework.Assert.AreEqual(ExpectedPath, testParser.Path()); }
/// <exception cref="System.Exception"/> protected override void ChannelRead0(ChannelHandlerContext ctx, HttpRequest request ) { if (request.GetMethod() != HttpMethod.Get) { DefaultHttpResponse resp = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus .MethodNotAllowed); resp.Headers().Set(HttpHeaders.Names.Connection, HttpHeaders.Values.Close); ctx.Write(resp).AddListener(ChannelFutureListener.Close); return; } QueryStringDecoder decoder = new QueryStringDecoder(request.GetUri()); string op = GetOp(decoder); string content; string path = GetPath(decoder); switch (op) { case "GETFILESTATUS": { content = image.GetFileStatus(path); break; } case "LISTSTATUS": { content = image.ListStatus(path); break; } case "GETACLSTATUS": { content = image.GetAclStatus(path); break; } default: { throw new ArgumentException("Invalid value for webhdfs parameter" + " \"op\""); } } Log.Info("op=" + op + " target=" + path); DefaultFullHttpResponse resp_1 = new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.Ok, Unpooled.WrappedBuffer(Sharpen.Runtime.GetBytesForString( content, Charsets.Utf8))); resp_1.Headers().Set(HttpHeaders.Names.ContentType, WebHdfsHandler.ApplicationJsonUtf8 ); resp_1.Headers().Set(HttpHeaders.Names.ContentLength, resp_1.Content().ReadableBytes ()); resp_1.Headers().Set(HttpHeaders.Names.Connection, HttpHeaders.Values.Close); ctx.Write(resp_1).AddListener(ChannelFutureListener.Close); }
public static string FirstValue(QueryStringDecoder query, string key) { if (null == query) { return(null); } if (!query.Parameters.TryGetValue(key, out var values)) { return(null); } return(values[0]); }
/// <exception cref="System.Exception"/> protected override void ChannelRead0(ChannelHandlerContext ctx, HttpRequest req) { Preconditions.CheckArgument(req.GetUri().StartsWith(WebhdfsPrefix)); QueryStringDecoder queryString = new QueryStringDecoder(req.GetUri()); @params = new ParameterParser(queryString, conf); DataNodeUGIProvider ugiProvider = new DataNodeUGIProvider(@params); ugi = ugiProvider.Ugi(); path = @params.Path(); InjectToken(); ugi.DoAs(new _PrivilegedExceptionAction_110(this, ctx, req)); }
/// <exception cref="System.IO.FileNotFoundException"/> private static string GetPath(QueryStringDecoder decoder) { string path = decoder.Path(); if (path.StartsWith(WebHdfsHandler.WebhdfsPrefix)) { return(Sharpen.Runtime.Substring(path, WebHdfsHandler.WebhdfsPrefixLength)); } else { throw new FileNotFoundException("Path: " + path + " should " + "start with " + WebHdfsHandler .WebhdfsPrefix); } }
public void EmptyStrings() { var pathSlash = new QueryStringDecoder("path/"); Assert.Equal("path/", pathSlash.RawPath()); Assert.Equal("", pathSlash.RawQuery()); var pathQuestion = new QueryStringDecoder("path?"); Assert.Equal("path", pathQuestion.RawPath()); Assert.Equal("", pathQuestion.RawQuery()); var empty = new QueryStringDecoder(""); Assert.Equal("", empty.RawPath()); Assert.Equal("", empty.RawQuery()); }
public void HasPath() { var d = new QueryStringDecoder("1=2", false); Assert.Equal("", d.Path); IDictionary <string, List <string> > parameters = d.Parameters; Assert.Equal(1, parameters.Count); Assert.True(parameters.ContainsKey("1")); List <string> param = parameters["1"]; Assert.NotNull(param); Assert.Single(param); Assert.Equal("2", param[0]); }
public virtual void TestDeserializeHAToken() { Configuration conf = DFSTestUtil.NewHAConfiguration(LogicalName); Org.Apache.Hadoop.Security.Token.Token <DelegationTokenIdentifier> token = new Org.Apache.Hadoop.Security.Token.Token <DelegationTokenIdentifier>(); QueryStringDecoder decoder = new QueryStringDecoder(WebHdfsHandler.WebhdfsPrefix + "/?" + NamenodeAddressParam.Name + "=" + LogicalName + "&" + DelegationParam.Name + "=" + token.EncodeToUrlString()); ParameterParser testParser = new ParameterParser(decoder, conf); Org.Apache.Hadoop.Security.Token.Token <DelegationTokenIdentifier> tok2 = testParser .DelegationToken(); NUnit.Framework.Assert.IsTrue(HAUtil.IsTokenForLogicalUri(tok2)); }
public void HashDos() { var buf = new StringBuilder(); buf.Append('?'); for (int i = 0; i < 65536; i++) { buf.Append('k'); buf.Append(i); buf.Append("=v"); buf.Append(i); buf.Append('&'); } var d = new QueryStringDecoder(buf.ToString()); IDictionary <string, List <string> > parameters = d.Parameters; Assert.Equal(1024, parameters.Count); }
public void UrlDecoding() { string caffe = new string( // "Caffé" but instead of putting the literal E-acute in the // source file, we directly use the UTF-8 encoding so as to // not rely on the platform's default encoding (not portable). new [] { 'C', 'a', 'f', 'f', '\u00E9' /* C3 A9 */ }); string[] tests = { // Encoded -> Decoded or error message substring "", "", "foo", "foo", "f+o", "f o", "f++", "f ", "fo%", "unterminated escape sequence at index 2 of: fo%", "%42", "B", "%5f", "_", "f%4", "unterminated escape sequence at index 1 of: f%4", "%x2", "invalid hex byte 'x2' at index 1 of '%x2'", "%4x", "invalid hex byte '4x' at index 1 of '%4x'", "Caff%C3%A9", caffe, "случайный праздник", "случайный праздник", "случайный%20праздник", "случайный праздник", "случайный%20праздник%20%E2%98%BA", "случайный праздник ☺", }; for (int i = 0; i < tests.Length; i += 2) { string encoded = tests[i]; string expected = tests[i + 1]; try { string decoded = QueryStringDecoder.DecodeComponent(encoded); Assert.Equal(expected, decoded); } catch (ArgumentException e) { Assert.Equal(expected, e.Message); } } }
public void Uri2() { var uri = new Uri("http://foo.com/images;num=10?query=name;value=123"); var d = new QueryStringDecoder(uri); Assert.Equal("/images;num=10", d.Path); IDictionary <string, List <string> > parameters = d.Parameters; Assert.Equal(2, parameters.Count); KeyValuePair <string, List <string> > entry = parameters.ElementAt(0); Assert.Equal("query", entry.Key); Assert.Single(entry.Value); Assert.Equal("name", entry.Value[0]); entry = parameters.ElementAt(1); Assert.Equal("value", entry.Key); Assert.Single(entry.Value); Assert.Equal("123", entry.Value[0]); }
protected override void ChannelRead0(IChannelHandlerContext ctx, IFullHttpRequest request) { QueryStringDecoder queryString = new QueryStringDecoder(request.Uri); string streamId = StreamId(request); int latency = Http2ExampleUtil.ToInt(Http2ExampleUtil.FirstValue(queryString, LATENCY_FIELD_NAME), 0); if (latency < MIN_LATENCY || latency > MAX_LATENCY) { SendBadRequest(ctx, streamId); return; } string x = Http2ExampleUtil.FirstValue(queryString, IMAGE_COORDINATE_X); string y = Http2ExampleUtil.FirstValue(queryString, IMAGE_COORDINATE_Y); if (x == null || y == null) { HandlePage(ctx, streamId, latency, request); } else { HandleImage(x, y, ctx, streamId, latency, request); } }
protected override void ChannelRead0(IChannelHandlerContext ctx, IHttpObject msg) { if (msg is IHttpRequest request) { s_logger.LogTrace("=========The Request Header========"); s_logger.LogDebug(request.ToString()); s_logger.LogTrace("==================================="); _request = request; var uriPath = GetPath(request.Uri); if (!uriPath.StartsWith("/form")) { // Write Menu WriteMenu(ctx); return; } _responseContent.Clear(); _responseContent.Append("WELCOME TO THE WILD WILD WEB SERVER\r\n"); _responseContent.Append("===================================\r\n"); _responseContent.Append("VERSION: " + request.ProtocolVersion.Text + "\r\n"); _responseContent.Append("REQUEST_URI: " + request.Uri + "\r\n\r\n"); _responseContent.Append("\r\n\r\n"); // new getMethod foreach (var entry in request.Headers) { _responseContent.Append("HEADER: " + entry.Key + '=' + entry.Value + "\r\n"); } _responseContent.Append("\r\n\r\n"); // new getMethod ISet <ICookie> cookies; string value = request.Headers.GetAsString(HttpHeaderNames.Cookie); if (value == null) { cookies = new HashSet <ICookie>(); } else { cookies = ServerCookieDecoder.StrictDecoder.Decode(value); } foreach (var cookie in cookies) { _responseContent.Append("COOKIE: " + cookie + "\r\n"); } _responseContent.Append("\r\n\r\n"); QueryStringDecoder decoderQuery = new QueryStringDecoder(request.Uri); var uriAttributes = decoderQuery.Parameters; foreach (var attr in uriAttributes) { foreach (var attrVal in attr.Value) { _responseContent.Append("URI: " + attr.Key + '=' + attrVal + "\r\n"); } } _responseContent.Append("\r\n\r\n"); // if GET Method: should not try to create an HttpPostRequestDecoder if (HttpMethod.Get.Equals(request.Method)) { // GET Method: should not try to create an HttpPostRequestDecoder // So stop here _responseContent.Append("\r\n\r\nEND OF GET CONTENT\r\n"); // Not now: LastHttpContent will be sent writeResponse(ctx.channel()); return; } try { _decoder = new HttpPostRequestDecoder(s_factory, request); } catch (ErrorDataDecoderException e1) { s_logger.LogError(e1.ToString()); _responseContent.Append(e1.Message); WriteResponseAsync(ctx.Channel, true); return; } var readingChunks = HttpUtil.IsTransferEncodingChunked(request); _responseContent.Append("Is Chunked: " + readingChunks + "\r\n"); _responseContent.Append("IsMultipart: " + _decoder.IsMultipart + "\r\n"); if (readingChunks) { // Chunk version _responseContent.Append("Chunks: "); } } // check if the decoder was constructed before // if not it handles the form get if (_decoder != null) { if (msg is IHttpContent chunk) // New chunk is received { try { _decoder.Offer(chunk); } catch (ErrorDataDecoderException e1) { s_logger.LogError(e1.ToString()); _responseContent.Append(e1.Message); WriteResponseAsync(ctx.Channel, true); return; } _responseContent.Append('o'); // example of reading chunk by chunk (minimize memory usage due to // Factory) ReadHttpDataChunkByChunk(); // example of reading only if at the end if (chunk is ILastHttpContent) { WriteResponseAsync(ctx.Channel); Reset(); } } } else { WriteResponseAsync(ctx.Channel); } }
protected override async void ChannelRead0(IChannelHandlerContext ctx, IFullHttpRequest msg) { try { IFullHttpRequest request = msg; string uri = request.Uri; QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri); string path = queryStringDecoder.Path; if (!path.Equals(Settings.Ins.HttpUrl)) { await ctx.CloseAsync(); return; } // chrome等浏览器会请求一次.ico if (uri.EndsWith(".ico")) { await ctx.WriteAndFlushAsync(response("")); return; } Dictionary <string, string> paramMap = new Dictionary <string, string>(); HttpMethod method = request.Method; QueryStringDecoder decoder = new QueryStringDecoder(request.Uri); foreach (var keyValuePair in decoder.Parameters) { paramMap.Add(keyValuePair.Key, keyValuePair.Value[0]); } if (Equals(HttpMethod.Post, method)) { var headCType = request.Headers.Get(HttpHeaderNames.ContentType, null); if (headCType == null) { await ctx.WriteAndFlushAsync(response(HttpHeaderNames.ContentType + " is null")); await ctx.CloseAsync(); return; } string content_type = headCType.ToString().ToLower(); if (content_type != null && content_type.Equals("application/json")) { // json 格式 string str = request.Content.ToString(Encoding.UTF8); var jsonNode = JSON.Parse(str) as JSONClass; if (jsonNode == null) { return; } var enumerator = jsonNode.GetEnumerator(); while (enumerator.MoveNext()) { var keyValuePair = (KeyValuePair <string, JSONNode>)enumerator.Current; if (paramMap.ContainsKey(keyValuePair.Key)) { await ctx.WriteAndFlushAsync(response("参数重复了:" + keyValuePair.Key)); await ctx.CloseAsync(); return; } if (!string.IsNullOrEmpty(keyValuePair.Value.Value)) { paramMap.Add(keyValuePair.Key, keyValuePair.Value.Value); } else { paramMap.Add(keyValuePair.Key, keyValuePair.Value.ToString()); } } } else { // key value 形式 HttpPostRequestDecoder decoder1 = new HttpPostRequestDecoder(request); decoder1.Offer(request); List <IInterfaceHttpData> parmList = decoder1.GetBodyHttpDatas(); foreach (var httpData in parmList) { if (httpData is IAttribute data) { paramMap.Add(data.Name, data.Value); } } decoder1.Destroy(); } } string res = await handleHttpRequest(ctx.Channel.RemoteAddress.ToString(), uri, paramMap); await ctx.WriteAndFlushAsync(response(res)); } catch (Exception e) { LOGGER.Error("http response error {} \n {}", e.Message, e.StackTrace); try { await ctx.WriteAndFlushAsync(response(e.Message)); await ctx.CloseAsync(); } catch (Exception) { LOGGER.Error("HTTP连接关闭异常"); } } }
/// <exception cref="System.Exception"/> public override void MessageReceived(ChannelHandlerContext ctx, MessageEvent evt) { HttpRequest request = (HttpRequest)evt.GetMessage(); if (request.GetMethod() != HttpMethod.Get) { this.SendError(ctx, HttpResponseStatus.MethodNotAllowed); return; } // Check whether the shuffle version is compatible if (!ShuffleHeader.DefaultHttpHeaderName.Equals(request.GetHeader(ShuffleHeader.HttpHeaderName )) || !ShuffleHeader.DefaultHttpHeaderVersion.Equals(request.GetHeader(ShuffleHeader .HttpHeaderVersion))) { this.SendError(ctx, "Incompatible shuffle request version", HttpResponseStatus.BadRequest ); } IDictionary<string, IList<string>> q = new QueryStringDecoder(request.GetUri()).GetParameters (); IList<string> keepAliveList = q["keepAlive"]; bool keepAliveParam = false; if (keepAliveList != null && keepAliveList.Count == 1) { keepAliveParam = Sharpen.Extensions.ValueOf(keepAliveList[0]); if (ShuffleHandler.Log.IsDebugEnabled()) { ShuffleHandler.Log.Debug("KeepAliveParam : " + keepAliveList + " : " + keepAliveParam ); } } IList<string> mapIds = this.SplitMaps(q["map"]); IList<string> reduceQ = q["reduce"]; IList<string> jobQ = q["job"]; if (ShuffleHandler.Log.IsDebugEnabled()) { ShuffleHandler.Log.Debug("RECV: " + request.GetUri() + "\n mapId: " + mapIds + "\n reduceId: " + reduceQ + "\n jobId: " + jobQ + "\n keepAlive: " + keepAliveParam); } if (mapIds == null || reduceQ == null || jobQ == null) { this.SendError(ctx, "Required param job, map and reduce", HttpResponseStatus.BadRequest ); return; } if (reduceQ.Count != 1 || jobQ.Count != 1) { this.SendError(ctx, "Too many job/reduce parameters", HttpResponseStatus.BadRequest ); return; } int reduceId; string jobId; try { reduceId = System.Convert.ToInt32(reduceQ[0]); jobId = jobQ[0]; } catch (FormatException) { this.SendError(ctx, "Bad reduce parameter", HttpResponseStatus.BadRequest); return; } catch (ArgumentException) { this.SendError(ctx, "Bad job parameter", HttpResponseStatus.BadRequest); return; } string reqUri = request.GetUri(); if (null == reqUri) { // TODO? add upstream? this.SendError(ctx, HttpResponseStatus.Forbidden); return; } HttpResponse response = new DefaultHttpResponse(HttpVersion.Http11, HttpResponseStatus .Ok); try { this.VerifyRequest(jobId, ctx, request, response, new Uri("http", string.Empty, this .port, reqUri)); } catch (IOException e) { ShuffleHandler.Log.Warn("Shuffle failure ", e); this.SendError(ctx, e.Message, HttpResponseStatus.Unauthorized); return; } IDictionary<string, ShuffleHandler.Shuffle.MapOutputInfo> mapOutputInfoMap = new Dictionary<string, ShuffleHandler.Shuffle.MapOutputInfo>(); Org.Jboss.Netty.Channel.Channel ch = evt.GetChannel(); string user = this._enclosing.userRsrc[jobId]; // $x/$user/appcache/$appId/output/$mapId // TODO: Once Shuffle is out of NM, this can use MR APIs to convert // between App and Job string outputBasePathStr = this.GetBaseLocation(jobId, user); try { this.PopulateHeaders(mapIds, outputBasePathStr, user, reduceId, request, response , keepAliveParam, mapOutputInfoMap); } catch (IOException e) { ch.Write(response); ShuffleHandler.Log.Error("Shuffle error in populating headers :", e); string errorMessage = this.GetErrorMessage(e); this.SendError(ctx, errorMessage, HttpResponseStatus.InternalServerError); return; } ch.Write(response); //Initialize one ReduceContext object per messageReceived call ShuffleHandler.ReduceContext reduceContext = new ShuffleHandler.ReduceContext(mapIds , reduceId, ctx, user, mapOutputInfoMap, outputBasePathStr); for (int i = 0; i < Math.Min(this._enclosing.maxSessionOpenFiles, mapIds.Count); i++) { ChannelFuture nextMap = this.SendMap(reduceContext); if (nextMap == null) { return; } } }
public void BasicUris() { var d = new QueryStringDecoder("http://localhost/path"); Assert.Equal(0, d.Parameters.Count); }
public void Basic() { var d = new QueryStringDecoder("/foo"); Assert.Equal("/foo", d.Path); Assert.Equal(0, d.Parameters.Count); d = new QueryStringDecoder("/foo%20bar"); Assert.Equal("/foo bar", d.Path); Assert.Equal(0, d.Parameters.Count); d = new QueryStringDecoder("/foo?a=b=c"); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Single(d.Parameters["a"]); Assert.Equal("b=c", d.Parameters["a"][0]); d = new QueryStringDecoder("/foo?a=1&a=2"); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["a"].Count); Assert.Equal("1", d.Parameters["a"][0]); Assert.Equal("2", d.Parameters["a"][1]); d = new QueryStringDecoder("/foo%20bar?a=1&a=2"); Assert.Equal("/foo bar", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["a"].Count); Assert.Equal("1", d.Parameters["a"][0]); Assert.Equal("2", d.Parameters["a"][1]); d = new QueryStringDecoder("/foo?a=&a=2"); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["a"].Count); Assert.Equal("", d.Parameters["a"][0]); Assert.Equal("2", d.Parameters["a"][1]); d = new QueryStringDecoder("/foo?a=1&a="); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["a"].Count); Assert.Equal("1", d.Parameters["a"][0]); Assert.Equal("", d.Parameters["a"][1]); d = new QueryStringDecoder("/foo?a=1&a=&a="); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(3, d.Parameters["a"].Count); Assert.Equal("1", d.Parameters["a"][0]); Assert.Equal("", d.Parameters["a"][1]); Assert.Equal("", d.Parameters["a"][2]); d = new QueryStringDecoder("/foo?a=1=&a==2"); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["a"].Count); Assert.Equal("1=", d.Parameters["a"][0]); Assert.Equal("=2", d.Parameters["a"][1]); d = new QueryStringDecoder("/foo?abc=1%2023&abc=124%20"); Assert.Equal("/foo", d.Path); Assert.Equal(1, d.Parameters.Count); Assert.Equal(2, d.Parameters["abc"].Count); Assert.Equal("1 23", d.Parameters["abc"][0]); Assert.Equal("124 ", d.Parameters["abc"][1]); }