View Javadoc
1   /*
2    * Copyright 2016 The Lannister Project
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package net.anyflow.lannister.http;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.net.URI;
22  import java.net.URISyntaxException;
23  import java.util.Date;
24  
25  import org.apache.commons.io.IOUtils;
26  
27  import com.google.common.io.Files;
28  
29  import io.netty.channel.ChannelFutureListener;
30  import io.netty.channel.ChannelHandlerContext;
31  import io.netty.channel.SimpleChannelInboundHandler;
32  import io.netty.handler.codec.http.DefaultFullHttpResponse;
33  import io.netty.handler.codec.http.FullHttpRequest;
34  import io.netty.handler.codec.http.HttpHeaderNames;
35  import io.netty.handler.codec.http.HttpHeaderValues;
36  import io.netty.handler.codec.http.HttpResponseStatus;
37  import io.netty.handler.codec.http.HttpUtil;
38  import io.netty.handler.codec.http.HttpVersion;
39  import net.anyflow.lannister.Application;
40  import net.anyflow.lannister.Settings;
41  
42  public class HttpRequestRouter extends SimpleChannelInboundHandler<FullHttpRequest> {
43  
44  	private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(HttpRequestRouter.class);
45  
46  	private boolean isStaticResourcePath(String path) {
47  		String ext = Files.getFileExtension(path);
48  		if (ext == null) { return false; }
49  
50  		return Settings.INSTANCE.webResourceExtensionToMimes().keySet().stream().anyMatch(s -> s.equalsIgnoreCase(ext));
51  	}
52  
53  	@Override
54  	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest rawRequest) throws Exception {
55  		Date now = new Date();
56  		HttpRequest request = new HttpRequest(rawRequest);
57  
58  		if (HttpHeaderValues.WEBSOCKET.toString().equalsIgnoreCase(rawRequest.headers().get(HttpHeaderNames.UPGRADE))
59  				&& HttpHeaderValues.UPGRADE.toString()
60  						.equalsIgnoreCase(rawRequest.headers().get(HttpHeaderNames.CONNECTION))) {
61  
62  			if (ctx.pipeline().get(WebsocketFrameHandler.class) == null) {
63  				logger.error("No WebSocket Handler available");
64  
65  				ctx.channel()
66  						.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN))
67  						.addListener(ChannelFutureListener.CLOSE);
68  				return;
69  			}
70  
71  			ctx.fireChannelRead(rawRequest.retain());
72  			return;
73  		}
74  
75  		if (HttpUtil.is100ContinueExpected(rawRequest)) {
76  			ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE));
77  			return;
78  		}
79  
80  		HttpResponse response = HttpResponse.createServerDefault(request.headers().get(HttpHeaderNames.COOKIE));
81  
82  		String requestPath = new URI(request.uri()).getPath();
83  
84  		if (isStaticResourcePath(requestPath)) {
85  			handleStaticResource(response, requestPath);
86  		}
87  		else {
88  			try {
89  				handleDynamicResource(request, response);
90  			}
91  			catch (URISyntaxException e) {
92  				set404Response(response);
93  			}
94  			catch (Exception e) {
95  				response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
96  				logger.error("Unknown exception was thrown in business logic handler.\r\n{}", e.getMessage(), e);
97  			}
98  		}
99  
100 		setDefaultHeaders(rawRequest, response);
101 
102 		if ("true".equalsIgnoreCase(Settings.INSTANCE.getProperty("webserver.logging.writeHttpRequest"))) {
103 			logger.info(request.toString(now));
104 		}
105 		if ("true".equalsIgnoreCase(Settings.INSTANCE.getProperty("webserver.logging.writeHttpResponse"))) {
106 			logger.info(response.toString());
107 		}
108 
109 		ctx.write(response);
110 	}
111 
112 	private void set404Response(HttpResponse response) {
113 		response.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
114 		response.setStatus(HttpResponseStatus.NOT_FOUND);
115 		response.setContent(HttpResponseStatus.NOT_FOUND.toString());
116 	}
117 
118 	private void handleStaticResource(HttpResponse response, String webResourceRequestPath) throws IOException {
119 		String requestPath = webResourceRequestPath;
120 		if (requestPath.startsWith("/")) {
121 			requestPath = requestPath.substring(1, requestPath.length());
122 		}
123 
124 		InputStream is = Application.class.getClassLoader().getResourceAsStream(requestPath);
125 
126 		try {
127 			if (is == null) {
128 				set404Response(response);
129 			}
130 			else {
131 				response.content().writeBytes(IOUtils.toByteArray(is));
132 
133 				String ext = Files.getFileExtension(requestPath);
134 				response.headers().set(HttpHeaderNames.CONTENT_TYPE,
135 						Settings.INSTANCE.webResourceExtensionToMimes().get(ext));
136 			}
137 		}
138 		finally {
139 			if (is != null) {
140 				is.close();
141 			}
142 		}
143 	}
144 
145 	private void handleDynamicResource(HttpRequest request, HttpResponse response)
146 			throws InstantiationException, IllegalAccessException, IOException, URISyntaxException {
147 		HttpRequestHandler.MatchedCriterion mc = HttpRequestHandler.findRequestHandler(new URI(request.uri()).getPath(),
148 				request.method().toString());
149 
150 		if (mc.requestHandlerClass() == null) {
151 			set404Response(response);
152 		}
153 		else {
154 			HttpRequestHandler handler = mc.requestHandlerClass().newInstance();
155 
156 			String webResourcePath = handler.getClass().getAnnotation(HttpRequestHandler.Handles.class)
157 					.webResourcePath();
158 
159 			if (HttpRequestHandler.NO_WEB_RESOURCE_PATH.equals(webResourcePath)) {
160 				response.setContent(
161 						handler.initialize(request.pathParameters(mc.pathParameters()), response).service());
162 			}
163 			else {
164 				handleStaticResource(response, webResourcePath);
165 			}
166 		}
167 	}
168 
169 	@Override
170 	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
171 		ctx.flush();
172 	}
173 
174 	@Override
175 	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
176 		logger.error(cause.getMessage(), cause);
177 		ctx.close();
178 	}
179 
180 	protected static void setDefaultHeaders(FullHttpRequest request, HttpResponse response) {
181 		response.headers().add(HttpHeaderNames.SERVER,
182 				"lannister " + net.anyflow.lannister.Settings.INSTANCE.version());
183 
184 		boolean keepAlive = HttpHeaderValues.KEEP_ALIVE.toString()
185 				.equals(request.headers().get(HttpHeaderNames.CONNECTION));
186 		if (keepAlive) {
187 			response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
188 		}
189 
190 		if (Settings.INSTANCE.getProperty("webserver.allowCrossDomain", "false").equalsIgnoreCase("true")) {
191 			response.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
192 			response.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "POST, GET, PUT, DELETE");
193 			response.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "X-PINGARUNER");
194 			response.headers().add(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "1728000");
195 		}
196 
197 		response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
198 	}
199 }