/* * Copyright 2013-2014, ApiFest project * * 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 com.apifest; import java.net.URISyntaxException; import java.util.List; import java.util.Map; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMessage; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; import org.jboss.netty.handler.codec.http.QueryStringEncoder; import org.jboss.netty.util.CharsetUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.apifest.api.BasicAction; import com.apifest.api.BasicFilter; import com.apifest.api.MappingEndpoint; import com.apifest.api.MappingException; import com.apifest.api.UpstreamException; import com.google.gson.Gson; /** * Handler for requests received on the server. * * @author Rossitsa Borissova */ public class HttpRequestHandler extends SimpleChannelUpstreamHandler { protected static final String RELOAD_URI = "/apifest-reload"; protected static final String MAPPINGS_URI = "/apifest-mappings"; protected static final String GLOBAL_ERRORS_URI = "/apifest-global-errors"; protected static final String ACCESS_TOKEN_REQUIRED = "{\"error\":\"access token required\"}"; protected static final String INVALID_ACCESS_TOKEN_SCOPE = "{\"error\":\"access token scope not valid\"}"; protected static final String INVALID_ACCESS_TOKEN = "{\"error\":\"access token not valid\"}"; protected static final String INVALID_ACCESS_TOKEN_TYPE = "{\"error\":\"access token type not valid\"}"; protected static final String OAUTH_TOKEN_VALIDATE_URI = "/oauth20/tokens/validate"; protected static Logger log = LoggerFactory.getLogger(HttpRequestHandler.class); private MappingClient client = MappingClient.getClient(); @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { final Channel channel = ctx.getChannel(); setConnectTimeout(channel); Object message = e.getMessage(); if (message instanceof HttpRequest) { HttpRequest req = (HttpRequest) message; LifecycleEventHandlers.invokeRequestEventHandlers(req, null); String uri = req.getUri(); HttpMethod method = req.getMethod(); if (RELOAD_URI.equals(uri) && method.equals(HttpMethod.GET)) { reloadMappingConfig(channel); return; } if (MAPPINGS_URI.equals(uri) && method.equals(HttpMethod.GET)) { getLoadedMappings(channel); return; } if (GLOBAL_ERRORS_URI.equals(uri) && method.equals(HttpMethod.GET)) { getLoadedGlobalErrors(channel); return; } List<MappingConfig> configList = ConfigLoader.getConfig(); MappingEndpoint mapping = null; MappingConfig config = null; for (MappingConfig mconfig : configList) { mapping = mconfig.getMappingEndpoint(uri, method.toString()); if (mapping != null) { config = mconfig; break; } } if (mapping != null) { if (mapping.getAuthType() != null) { String accessToken = null; List<String> authorizationHeaders = req.headers().getAll(HttpHeaders.Names.AUTHORIZATION); for (String header : authorizationHeaders) { accessToken = AccessTokenValidator.extractAccessToken(header); if (accessToken != null) { break; } } if (accessToken == null) { writeResponseToChannel(channel, req, HttpResponseFactory.createUnauthorizedResponse(ACCESS_TOKEN_REQUIRED)); return; } BasicFilter filter; try { filter = getMappingFilter(mapping, config, channel); } catch (MappingException e2) { log.error("cannot map request", e2); LifecycleEventHandlers.invokeExceptionHandler(e2, req); writeResponseToChannel(channel, req, HttpResponseFactory.createISEResponse()); return; } final ResponseListener responseListener = createResponseListener(filter, config.getErrors(), channel, req); final HttpRequest request = req; final MappingEndpoint endpoint = mapping; final MappingConfig conf = config; // validates access token TokenValidationListener validatorListener = new TokenValidationListener() { @Override public void responseReceived(HttpMessage response) { HttpMessage tokenResponse = response; if (response instanceof HttpResponse) { HttpResponse tokenValidationResponse = (HttpResponse) response; if (!HttpResponseStatus.OK.equals(tokenValidationResponse.getStatus())) { writeResponseToChannel(channel, request, HttpResponseFactory.createUnauthorizedResponse(INVALID_ACCESS_TOKEN)); return; } String tokenContent = tokenValidationResponse.getContent().toString(CharsetUtil.UTF_8); boolean scopeOk = AccessTokenValidator.validateTokenScope(tokenContent, endpoint.getScope()); if (!scopeOk) { log.debug("access token scope not valid"); writeResponseToChannel(channel, request, HttpResponseFactory.createUnauthorizedResponse(INVALID_ACCESS_TOKEN_SCOPE)); return; } String userId = BasicAction.getUserId(tokenValidationResponse); if ((MappingEndpoint.AUTH_TYPE_USER.equals(endpoint.getAuthType()) && (userId != null && userId.length() > 0)) || MappingEndpoint.AUTH_TYPE_CLIENT_APP.equals(endpoint.getAuthType())) { try { HttpRequest mappedReq = mapRequest(request, endpoint, conf, tokenValidationResponse); if (mappedReq == null) { throw new UpstreamException(HttpResponseFactory.createISEResponse()); } channel.getPipeline().getContext("handler").setAttachment(responseListener); client.send(mappedReq, endpoint.getBackendHost(), Integer.valueOf(endpoint.getBackendPort()), responseListener); } catch (MappingException e) { log.error("cannot map request", e); LifecycleEventHandlers.invokeExceptionHandler(e, request); writeResponseToChannel(channel, request, HttpResponseFactory.createISEResponse()); return; } catch (UpstreamException ue) { writeResponseToChannel(channel, request, ue.getResponse()); return; } catch (Exception e) { // Not nice but ensures we ALWAYS respond to the client writeResponseToChannel(channel, request, HttpResponseFactory.createISEResponse()); return; } } else { writeResponseToChannel(channel, request, HttpResponseFactory.createUnauthorizedResponse(INVALID_ACCESS_TOKEN_TYPE)); return; } } else { ChannelFuture future = channel.write(tokenResponse); setConnectTimeout(channel); future.addListener(ChannelFutureListener.CLOSE); } } }; channel.getPipeline().getContext("handler").setAttachment(validatorListener); if (ServerConfig.tokenValidateHost == null || ServerConfig.tokenValidateHost.isEmpty() || ServerConfig.tokenValidatePort == null) { log.error("token.validation.host and token.validation.port properties are not set. Cannot validate access token."); writeResponseToChannel(channel, request, HttpResponseFactory.createUnauthorizedResponse(INVALID_ACCESS_TOKEN)); } else { HttpRequest validateReq = createTokenValidateRequest(accessToken); client.sendValidation(validateReq, ServerConfig.tokenValidateHost, ServerConfig.tokenValidatePort, validatorListener); } } else { try { BasicFilter filter = getMappingFilter(mapping, config, channel); ResponseListener responseListener = createResponseListener(filter, config.getErrors(), channel, req); channel.getPipeline().getContext("handler").setAttachment(responseListener); HttpRequest mappedReq = mapRequest(req, mapping, config, null); client.send(mappedReq, mapping.getBackendHost(), Integer.valueOf(mapping.getBackendPort()), responseListener); } catch (MappingException e2) { log.error("cannot map request", e2); LifecycleEventHandlers.invokeExceptionHandler(e2, req); writeResponseToChannel(channel, req, HttpResponseFactory.createISEResponse()); return; } catch (UpstreamException ue) { LifecycleEventHandlers.invokeResponseEventHandlers(req, ue.getResponse()); writeResponseToChannel(channel, req, ue.getResponse()); return; } } } else { // if no mapping found HttpResponse response = HttpResponseFactory.createNotFoundResponse(); writeResponseToChannel(channel, req, response); return; } } else { log.debug("write response here from the BE"); } } protected ResponseListener createResponseListener(BasicFilter filter, Map<String, String> errors, final Channel channel, final HttpRequest request) { ResponseListener responseListener = new ResponseListener(filter, errors) { @Override public void responseReceived(HttpMessage response) { HttpMessage newResponse = response; if (response instanceof HttpResponse) { if (getFilter() != null) { newResponse = getFilter().execute((HttpResponse) response); } } LifecycleEventHandlers.invokeResponseEventHandlers(request, (HttpResponse) newResponse); ChannelFuture future = channel.write(newResponse); if (!HttpHeaders.isKeepAlive(request)) { future.addListener(ChannelFutureListener.CLOSE); } } }; return responseListener; } protected HttpRequest mapRequest(HttpRequest request, MappingEndpoint mapping, MappingConfig config, HttpResponse tokenValidationResponse) throws MappingException, UpstreamException { BaseMapper mapper = new BaseMapper(); request.headers().set(HttpHeaders.Names.HOST, mapping.getBackendHost()); HttpRequest req = mapper.map(request, mapping.getInternalEndpoint()); if (mapping.getAction() != null) { BasicAction action = config.getAction(mapping.getAction()); req = action.execute(req, tokenValidationResponse, mapping); } return req; } protected BasicFilter getMappingFilter(MappingEndpoint mapping, MappingConfig config, final Channel channel) throws MappingException { BasicFilter filter = null; if (mapping.getFilter() != null) { filter = config.getFilter(mapping.getFilter()); } return filter; } protected void writeResponseToChannel(Channel channel, HttpRequest request, HttpResponse response) { LifecycleEventHandlers.invokeResponseEventHandlers(request, response); ChannelFuture future = channel.write(response); future.addListener(ChannelFutureListener.CLOSE); } protected void setConnectTimeout(final Channel channel) { channel.getConfig().setConnectTimeoutMillis(ServerConfig.getConnectTimeout()); channel.getConfig().setOption("soLinger", -1); } protected void reloadMappingConfig(final Channel channel) { HttpResponse response = null; try { ConfigLoader.reloadConfigs(); response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); } catch (MappingException e) { response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); ChannelBuffer content = ChannelBuffers.copiedBuffer(e.getMessage().getBytes(CharsetUtil.UTF_8)); response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json"); response.setContent(content); } ChannelFuture future = channel.write(response); future.addListener(ChannelFutureListener.CLOSE); } protected HttpRequest createTokenValidateRequest(String accessToken) { QueryStringEncoder enc = new QueryStringEncoder(OAUTH_TOKEN_VALIDATE_URI); enc.addParam("token", accessToken); String uri = OAUTH_TOKEN_VALIDATE_URI; try { uri = enc.toUri().toString(); } catch (URISyntaxException e) { log.error("cannot build token validation URI", e); } HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); request.headers().add(HttpHeaders.Names.HOST, ServerConfig.tokenValidateHost); // REVISIT: propagate all custom headers? return request; } protected void getLoadedMappings(Channel channel) { HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json"); Map<String, MappingConfig> mappings = ConfigLoader.getLoadedMappings(); Gson gson = new Gson(); String jsonObj = gson.toJson(mappings); response.setContent(ChannelBuffers.copiedBuffer(jsonObj.getBytes(CharsetUtil.UTF_8))); ChannelFuture future = channel.write(response); future.addListener(ChannelFutureListener.CLOSE); } protected void getLoadedGlobalErrors(Channel channel) { HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json"); Map<Integer, String> mappings = ConfigLoader.getLoadedGlobalErrors(); Gson gson = new Gson(); String jsonObj = gson.toJson(mappings); response.setContent(ChannelBuffers.copiedBuffer(jsonObj.getBytes(CharsetUtil.UTF_8))); ChannelFuture future = channel.write(response); future.addListener(ChannelFutureListener.CLOSE); } }