/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.tajo.worker.dataserver; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.*; import org.jboss.netty.handler.codec.frame.TooLongFrameException; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; 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.ssl.SslHandler; import org.jboss.netty.handler.stream.ChunkedFile; import org.jboss.netty.util.CharsetUtil; import org.apache.tajo.worker.dataserver.retriever.DataRetriever; import org.apache.tajo.worker.dataserver.retriever.FileChunk; import java.io.*; import java.net.URLDecoder; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive; import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; import static org.jboss.netty.handler.codec.http.HttpMethod.GET; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; public class HttpDataServerHandler extends SimpleChannelUpstreamHandler { private final static Log LOG = LogFactory.getLog(HttpDataServer.class); private final DataRetriever retriever; public HttpDataServerHandler(DataRetriever retriever) { this.retriever = retriever; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { HttpRequest request = (HttpRequest) e.getMessage(); if (request.getMethod() != GET) { sendError(ctx, METHOD_NOT_ALLOWED); return; } FileChunk [] file; try { file = retriever.handle(ctx, request); } catch (FileNotFoundException fnf) { LOG.error(fnf); sendError(ctx, NOT_FOUND); return; } catch (IllegalArgumentException iae) { LOG.error(iae); sendError(ctx, BAD_REQUEST); return; } catch (FileAccessForbiddenException fafe) { LOG.error(fafe); sendError(ctx, FORBIDDEN); return; } catch (IOException ioe) { LOG.error(ioe); sendError(ctx, INTERNAL_SERVER_ERROR); return; } // Write the content. Channel ch = e.getChannel(); if (file == null) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, NO_CONTENT); ch.write(response); if (!isKeepAlive(request)) { ch.close(); } } else { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); long totalSize = 0; for (FileChunk chunk : file) { totalSize += chunk.length(); } setContentLength(response, totalSize); // Write the initial line and the header. ch.write(response); ChannelFuture writeFuture = null; for (FileChunk chunk : file) { writeFuture = sendFile(ctx, ch, chunk); if (writeFuture == null) { sendError(ctx, NOT_FOUND); return; } } // Decide whether to close the connection or not. if (!isKeepAlive(request)) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } } private ChannelFuture sendFile(ChannelHandlerContext ctx, Channel ch, FileChunk file) throws IOException { RandomAccessFile raf; try { raf = new RandomAccessFile(file.getFile(), "r"); } catch (FileNotFoundException fnfe) { return null; } ChannelFuture writeFuture; if (ch.getPipeline().get(SslHandler.class) != null) { // Cannot use zero-copy with HTTPS. writeFuture = ch.write(new ChunkedFile(raf, file.startOffset(), file.length(), 8192)); } else { // No encryption - use zero-copy. final FileRegion region = new DefaultFileRegion(raf.getChannel(), file.startOffset(), file.length()); writeFuture = ch.write(region); writeFuture.addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) { region.releaseExternalResources(); } }); } return writeFuture; } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Channel ch = e.getChannel(); Throwable cause = e.getCause(); if (cause instanceof TooLongFrameException) { sendError(ctx, BAD_REQUEST); return; } cause.printStackTrace(); if (ch.isConnected()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } public static String sanitizeUri(String uri) { // Decode the path. try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { try { uri = URLDecoder.decode(uri, "ISO-8859-1"); } catch (UnsupportedEncodingException e1) { throw new Error(); } } // Convert file separators. uri = uri.replace('/', File.separatorChar); // Simplistic dumb security check. // You will have to do something serious in the production environment. if (uri.contains(File.separator + ".") || uri.contains("." + File.separator) || uri.startsWith(".") || uri.endsWith(".")) { return null; } // Convert to absolute path. return uri; } private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); response.setContent(ChannelBuffers.copiedBuffer( "Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); // Close the connection as soon as the error message is sent. ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); } }