diff --git a/README.rst b/README.rst index ee41933..50ad997 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ netty-http ========== A library to develop HTTP services with `Netty `__. Supports the capability to route end-points based on `JAX-RS `__-style annotations. Implements Guava's Service interface to manage the runtime-state of the HTTP service. -Need for this library +Need for this library --------------------- `Netty `__ is a powerful framework to write asynchronous event-driven high-performance applications. While it is relatively easy to write a RESTful HTTP service using netty, the mapping between HTTP routes to handlers is not a straight-forward task. @@ -142,7 +142,7 @@ Example: Sample HTTP service that manages an application lifecycle: // Setup HTTP service and add Handlers - // You can either add varargs of HttpHandler or as a list of HttpHanlders as below to the NettyService Builder + // You can either add varargs of HttpHandler or as a list of HttpHandlers as below to the NettyService Builder List handlers = new ArrayList<>(); handlers.add(new PingHandler()); @@ -175,7 +175,7 @@ Code Sample: .setCertificatePassword("certificatePassword").build()) .build(); -* Set ``String:certificatePassword`` as "null" when not applicable +* Set ``String:certificatePassword`` as "null" when not applicable * ``File:keyStore`` points to the key store that holds your SSL certificate References diff --git a/src/main/java/io/cdap/http/internal/CookieParser.java b/src/main/java/io/cdap/http/internal/CookieParser.java new file mode 100644 index 0000000..00f5754 --- /dev/null +++ b/src/main/java/io/cdap/http/internal/CookieParser.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2017-2019 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.http.internal; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.CookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Parses cookies from a request. + */ +public class CookieParser { + private final boolean strict; + + public CookieParser(boolean strict) { + this.strict = strict; + } + + public Map parseCookies(HttpRequest request) { + List headers = request.headers().getAll(HttpHeaderNames.COOKIE); + if (headers == null || headers.isEmpty()) { + return Collections.emptyMap(); + } + ServerCookieDecoder decoder = getCookieDecoder(); + Map cookies = new LinkedHashMap<>(); + for (String value : headers) { + for (Cookie cookie : decoder.decode(value)) { + cookies.put(cookie.name(), cookie); + } + } + return cookies; + } + + private ServerCookieDecoder getCookieDecoder() { + return strict ? ServerCookieDecoder.STRICT : ServerCookieDecoder.LAX; + } +} diff --git a/src/main/java/io/cdap/http/internal/HttpResourceModel.java b/src/main/java/io/cdap/http/internal/HttpResourceModel.java index 96f0cc3..49d3bef 100644 --- a/src/main/java/io/cdap/http/internal/HttpResourceModel.java +++ b/src/main/java/io/cdap/http/internal/HttpResourceModel.java @@ -23,6 +23,8 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -36,6 +38,7 @@ import java.util.Map; import java.util.Set; import javax.annotation.Nullable; +import javax.ws.rs.CookieParam; import javax.ws.rs.DefaultValue; import javax.ws.rs.HeaderParam; import javax.ws.rs.PathParam; @@ -48,7 +51,8 @@ public final class HttpResourceModel { private static final Set> SUPPORTED_PARAM_ANNOTATIONS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PathParam.class, QueryParam.class, HeaderParam.class))); + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PathParam.class, QueryParam.class, HeaderParam.class, + CookieParam.class))); private final Set httpMethods; private final String path; @@ -58,6 +62,7 @@ public final class HttpResourceModel { private final ExceptionHandler exceptionHandler; private final boolean isSecured; private final String[] requiredRoles; + private final CookieParser cookieParser = new CookieParser(false); /** * Construct a resource model with HttpMethod, method that handles httprequest, Object that contains the method. @@ -137,6 +142,9 @@ public HttpMethodInfo handle(HttpRequest request, if (info.containsKey(HeaderParam.class)) { args[idx] = getHeaderParamValue(info, request); } + if (info.containsKey(CookieParam.class)) { + args[idx] = getCookieParamValue(info, request); + } idx++; } @@ -195,6 +203,16 @@ private Object getHeaderParamValue(Map, ParameterInf return hasHeader ? info.convert(request.headers().getAll(headerName)) : info.convert(defaultValue(annotations)); } + @SuppressWarnings("unchecked") + private Object getCookieParamValue(Map, ParameterInfo> annotations, + HttpRequest request) throws Exception { + Map cookies = cookieParser.parseCookies(request); + ParameterInfo info = (ParameterInfo) annotations.get(CookieParam.class); + CookieParam cookieParam = info.getAnnotation(); + String cookieName = cookieParam.value(); + boolean hasCookie = cookies.containsKey(cookieName); + return hasCookie ? info.convert(cookies.get(cookieName)) : info.convert(defaultCookie(cookieName, annotations)); + } /** * Returns a List of String created based on the {@link DefaultValue} if it is presented in the annotations Map. * @@ -210,6 +228,19 @@ private List defaultValue(Map, ParameterInfo return Collections.singletonList(defaultValue.value()); } + /** + * Returns a Cookie created based on the {@link DefaultValue} if it is presented in the annotations Map. + * + * @return a Cookie or null if {@link DefaultValue} is not presented + */ + private Cookie defaultCookie(String name, Map, ParameterInfo> annotations) { + List strings = defaultValue(annotations); + if (strings == null || strings.isEmpty()) { + return null; + } + return new DefaultCookie(name, strings.get(0)); + } + /** * Gathers all parameters' annotations for the given method, starting from the third parameter. */ @@ -239,6 +270,9 @@ private List, ParameterInfo>> createParameter } else if (HeaderParam.class.isAssignableFrom(annotationType)) { parameterInfo = ParameterInfo.create(annotation, ParamConvertUtils.createHeaderParamConverter(parameterTypes[i])); + } else if (CookieParam.class.isAssignableFrom(annotationType)) { + parameterInfo = ParameterInfo.create(annotation, + ParamConvertUtils.createCookieParamConverter(parameterTypes[i])); } else { parameterInfo = ParameterInfo.create(annotation, null); } diff --git a/src/main/java/io/cdap/http/internal/ParamConvertUtils.java b/src/main/java/io/cdap/http/internal/ParamConvertUtils.java index feab865..03abfe3 100644 --- a/src/main/java/io/cdap/http/internal/ParamConvertUtils.java +++ b/src/main/java/io/cdap/http/internal/ParamConvertUtils.java @@ -16,6 +16,7 @@ package io.cdap.http.internal; +import io.netty.handler.codec.http.cookie.Cookie; import org.apache.commons.beanutils.ConvertUtils; import java.lang.reflect.Array; @@ -89,6 +90,37 @@ public static Converter, Object> createHeaderParamConverter(Type re return createListConverter(resultType); } + /** + * Creates a converter function that converts cookie value into an object of the given result type. + * Currently, only {@link String} result types are supported. + */ + public static Converter createCookieParamConverter(Type resultType) { + Class resultClass = getRawClass(resultType); + + // For string, return the cookie value + if (resultClass == String.class) { + return new Converter() { + @Nullable + @Override + public Object convert(Cookie from) throws Exception { + return from.value(); + } + }; + } + // For cookie objects, convert appropriately. + if (resultClass == Cookie.class) { + return new Converter() { + @Nullable + @Override + public Object convert(Cookie from) throws Exception { + return from; + } + }; + } + + throw new IllegalArgumentException("Unsupported CookieParam type " + resultType); + } + /** * Creates a converter function that converts query parameter into an object of the given result type. * It follows the supported types of {@link QueryParam} with the following exceptions: diff --git a/src/test/java/io/cdap/http/HttpServerTest.java b/src/test/java/io/cdap/http/HttpServerTest.java index 8d33345..2a5b3a7 100644 --- a/src/test/java/io/cdap/http/HttpServerTest.java +++ b/src/test/java/io/cdap/http/HttpServerTest.java @@ -42,6 +42,10 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import io.netty.handler.ssl.SslHandler; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakDetector; @@ -82,6 +86,7 @@ import java.util.zip.DeflaterInputStream; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; +import javax.ws.rs.ext.RuntimeDelegate; /** * Test the HttpServer. @@ -542,7 +547,7 @@ public void testMultiplePathParameters() throws IOException { //Test the end point where the parameter in path and order of declaration in method signature are different @Test - public void testMultiplePathParametersWithParamterInDifferentOrder() throws IOException { + public void testMultiplePathParametersWithParameterInDifferentOrder() throws IOException { HttpURLConnection urlConn = request("/test/v1/message/21/user/sree", HttpMethod.GET); Assert.assertEquals(200, urlConn.getResponseCode()); @@ -697,6 +702,35 @@ public void testSortedSetQueryParam() throws IOException { testContent("/test/v1/sortedSetQueryParam?id=20&id=30&id=20&id=10", expectedContent, HttpMethod.GET); } + @Test + public void testStringCookieParam() throws IOException { + testContent("/test/v1/stringCookieParam", "ck1:cookie value", + new DefaultCookie("ck1", "cookie value")); + } + + @Test + public void testStringCookieParamDefaultValue() throws IOException { + testContent("/test/v1/stringCookieParam", "ck1:def", new Cookie[0]); + } + + @Test + public void testMultipleStringCookieParam() throws IOException { + testContent("/test/v1/multipleStringCookieParam", "ck1:cookie value 1,ck2:cookie value 2", + new DefaultCookie("ck1", "cookie value 1"), + new DefaultCookie("ck2", "cookie value 2")); + } + + @Test + public void testNettyCookieCookieParam() throws IOException { + testContent("/test/v1/nettyCookieParam", "ck1:cookie value", + new DefaultCookie("ck1", "cookie value")); + } + + @Test + public void testNettyCookieParamDefaultValue() throws IOException { + testContent("/test/v1/nettyCookieParam", "ck1:def", new Cookie[0]); + } + @Test public void testListHeaderParam() throws IOException { List names = Arrays.asList("name1", "name3", "name2", "name1"); @@ -972,6 +1006,15 @@ private void testContent(String path, String content, HttpMethod method) throws urlConn.disconnect(); } + private void testContent(String path, String content, Cookie... cookies) throws IOException { + String cookie = ClientCookieEncoder.LAX.encode(cookies); + HttpURLConnection urlConn = request(path, HttpMethod.GET); + urlConn.addRequestProperty(HttpHeaderNames.COOKIE.toString(), cookie); + Assert.assertEquals(200, urlConn.getResponseCode()); + Assert.assertEquals(content, getContent(urlConn)); + urlConn.disconnect(); + } + private HttpURLConnection request(String path, HttpMethod method) throws IOException { return request(path, method, false); } diff --git a/src/test/java/io/cdap/http/TestHandler.java b/src/test/java/io/cdap/http/TestHandler.java index a3ee3b2..645ab0f 100644 --- a/src/test/java/io/cdap/http/TestHandler.java +++ b/src/test/java/io/cdap/http/TestHandler.java @@ -28,6 +28,8 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import org.junit.Assert; import java.io.File; @@ -43,6 +45,7 @@ import java.util.SortedSet; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; +import javax.ws.rs.CookieParam; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; @@ -530,6 +533,27 @@ public void testSortedSetQueryParam(HttpRequest request, HttpResponder responder responder.sendString(HttpResponseStatus.OK, GSON.toJson(ids)); } + @Path("/stringCookieParam") + @GET + public void testStringCookieParam(HttpRequest request, HttpResponder responder, + @CookieParam("ck1") @DefaultValue("def") String ck1) { + responder.sendString(HttpResponseStatus.OK, "ck1:" + ck1); + } + + @Path("/multipleStringCookieParam") + @GET + public void testMultipleStringCookieParam(HttpRequest request, HttpResponder responder, + @CookieParam("ck1") String ck1, @CookieParam("ck2") String ck2) { + responder.sendString(HttpResponseStatus.OK, "ck1:" + ck1 + ",ck2:" + ck2); + } + + @Path("/nettyCookieParam") + @GET + public void testNettyCookieParam(HttpRequest request, HttpResponder responder, + @CookieParam("ck1") @DefaultValue("def") Cookie ck1) { + responder.sendString(HttpResponseStatus.OK, "ck1:" + ck1.value()); + } + @Path("/listHeaderParam") @GET public void testListHeaderParam(HttpRequest request, HttpResponder responder, diff --git a/src/test/java/io/cdap/http/internal/CookieParserTest.java b/src/test/java/io/cdap/http/internal/CookieParserTest.java new file mode 100644 index 0000000..6a6dd97 --- /dev/null +++ b/src/test/java/io/cdap/http/internal/CookieParserTest.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2017-2019 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.http.internal; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + + +public class CookieParserTest { + private CookieParser cookieParser = new CookieParser(false); + + @Test + public void testCookieDecode() { + List cookies = Arrays.asList( + new DefaultCookie("c1", "v1"), + new DefaultCookie("c2", "v2") + ); + String cookieHeader = ClientCookieEncoder.LAX.encode(cookies); + HttpHeaders headers = new DefaultHttpHeaders() + .set(HttpHeaderNames.COOKIE, cookieHeader); + DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "test", headers); + Map parsed = cookieParser.parseCookies(request); + Assert.assertEquals(new HashSet<>(Arrays.asList("c1", "c2")), parsed.keySet()); + Assert.assertEquals(new HashSet<>(cookies), new HashSet<>(parsed.values())); + } +}