/* * 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.flink.mesos.util; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.router.Handler; import io.netty.handler.codec.http.router.Routed; import io.netty.handler.codec.http.router.Router; import io.netty.handler.stream.ChunkedStream; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; import org.apache.flink.core.fs.FSDataInputStream; import org.apache.flink.core.fs.FileStatus; import org.apache.flink.core.fs.FileSystem; import org.apache.flink.core.fs.Path; import org.apache.flink.configuration.ConfigConstants; import org.apache.flink.configuration.Configuration; import org.apache.flink.runtime.net.SSLUtils; import org.jets3t.service.utils.Mimetypes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.Option; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import static io.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL; import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpMethod.HEAD; import static io.netty.handler.codec.http.HttpResponseStatus.GONE; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * A generic Mesos artifact server, designed specifically for use by the Mesos Fetcher. * * More information: * http://mesos.apache.org/documentation/latest/fetcher/ * http://mesos.apache.org/documentation/latest/fetcher-cache-internals/ */ public class MesosArtifactServer implements MesosArtifactResolver { private static final Logger LOG = LoggerFactory.getLogger(MesosArtifactServer.class); private final Router router; private ServerBootstrap bootstrap; private Channel serverChannel; private final URL baseURL; private final Map<Path,URL> paths = new HashMap<>(); private final SSLContext serverSSLContext; public MesosArtifactServer(String prefix, String serverHostname, int configuredPort, Configuration config) throws Exception { if (configuredPort < 0 || configuredPort > 0xFFFF) { throw new IllegalArgumentException("File server port is invalid: " + configuredPort); } // Config to enable https access to the artifact server boolean enableSSL = config.getBoolean( ConfigConstants.MESOS_ARTIFACT_SERVER_SSL_ENABLED, ConfigConstants.DEFAULT_MESOS_ARTIFACT_SERVER_SSL_ENABLED) && SSLUtils.getSSLEnabled(config); if (enableSSL) { LOG.info("Enabling ssl for the artifact server"); try { serverSSLContext = SSLUtils.createSSLServerContext(config); } catch (Exception e) { throw new IOException("Failed to initialize SSLContext for the artifact server", e); } } else { serverSSLContext = null; } router = new Router(); final Configuration sslConfig = config; ChannelInitializer<SocketChannel> initializer = new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { Handler handler = new Handler(router); // SSL should be the first handler in the pipeline if (serverSSLContext != null) { SSLEngine sslEngine = serverSSLContext.createSSLEngine(); SSLUtils.setSSLVerAndCipherSuites(sslEngine, sslConfig); sslEngine.setUseClientMode(false); ch.pipeline().addLast("ssl", new SslHandler(sslEngine)); } ch.pipeline() .addLast(new HttpServerCodec()) .addLast(new ChunkedWriteHandler()) .addLast(handler.name(), handler) .addLast(new UnknownFileHandler()); } }; NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); this.bootstrap = new ServerBootstrap(); this.bootstrap .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(initializer); Channel ch = this.bootstrap.bind(serverHostname, configuredPort).sync().channel(); this.serverChannel = ch; InetSocketAddress bindAddress = (InetSocketAddress) ch.localAddress(); String address = bindAddress.getAddress().getHostAddress(); int port = bindAddress.getPort(); String httpProtocol = (serverSSLContext != null) ? "https": "http"; baseURL = new URL(httpProtocol, serverHostname, port, "/" + prefix + "/"); LOG.info("Mesos Artifact Server Base URL: {}, listening at {}:{}", baseURL, address, port); } public URL baseURL() { return baseURL; } /** * Get the server port on which the artifact server is listening. */ public synchronized int getServerPort() { Channel server = this.serverChannel; if (server != null) { try { return ((InetSocketAddress) server.localAddress()).getPort(); } catch (Exception e) { LOG.error("Cannot access local server port", e); } } return -1; } /** * Adds a file to the artifact server. * @param localFile the local file to serve. * @param remoteFile the remote path with which to locate the file. * @return the fully-qualified remote path to the file. * @throws MalformedURLException if the remote path is invalid. */ public synchronized URL addFile(File localFile, String remoteFile) throws IOException, MalformedURLException { return addPath(new Path(localFile.toURI()), new Path(remoteFile)); } /** * Adds a path to the artifact server. * @param path the qualified FS path to serve (local, hdfs, etc). * @param remoteFile the remote path with which to locate the file. * @return the fully-qualified remote path to the file. * @throws MalformedURLException if the remote path is invalid. */ public synchronized URL addPath(Path path, Path remoteFile) throws IOException, MalformedURLException { if(paths.containsKey(remoteFile)) { throw new IllegalArgumentException("duplicate path registered"); } if(remoteFile.isAbsolute()) { throw new IllegalArgumentException("not expecting an absolute path"); } URL fileURL = new URL(baseURL, remoteFile.toString()); router.ANY(fileURL.getPath(), new VirtualFileServerHandler(path)); paths.put(remoteFile, fileURL); return fileURL; } public synchronized void removePath(Path remoteFile) { if(paths.containsKey(remoteFile)) { URL fileURL = null; try { fileURL = new URL(baseURL, remoteFile.toString()); } catch (MalformedURLException e) { throw new RuntimeException(e); } router.removePath(fileURL.getPath()); } } @Override public synchronized Option<URL> resolve(Path remoteFile) { Option<URL> resolved = Option.apply(paths.get(remoteFile)); return resolved; } /** * Stops the artifact server. * @throws Exception */ public synchronized void stop() throws Exception { if (this.serverChannel != null) { this.serverChannel.close().awaitUninterruptibly(); this.serverChannel = null; } if (bootstrap != null) { if (bootstrap.group() != null) { bootstrap.group().shutdownGracefully(); } bootstrap = null; } } /** * Handle HEAD and GET requests for a specific file. */ @ChannelHandler.Sharable public static class VirtualFileServerHandler extends SimpleChannelInboundHandler<Routed> { private FileSystem fs; private Path path; public VirtualFileServerHandler(Path path) throws IOException { this.path = path; if(!path.isAbsolute()) { throw new IllegalArgumentException("path must be absolute: " + path.toString()); } this.fs = path.getFileSystem(); if(!fs.exists(path) || fs.getFileStatus(path).isDir()) { throw new IllegalArgumentException("no such file: " + path.toString()); } } @Override protected void channelRead0(ChannelHandlerContext ctx, Routed routed) throws Exception { HttpRequest request = routed.request(); if (LOG.isDebugEnabled()) { LOG.debug("{} request for file '{}'", request.getMethod(), path); } if(!(request.getMethod() == GET || request.getMethod() == HEAD)) { sendMethodNotAllowed(ctx); return; } final FileStatus status; try { status = fs.getFileStatus(path); } catch (IOException e) { LOG.error("unable to stat file", e); sendError(ctx, GONE); return; } // compose the response HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); HttpHeaders.setHeader(response, CONNECTION, HttpHeaders.Values.CLOSE); HttpHeaders.setHeader(response, CACHE_CONTROL, "private"); HttpHeaders.setHeader(response, CONTENT_TYPE, Mimetypes.MIMETYPE_OCTET_STREAM); HttpHeaders.setContentLength(response, status.getLen()); ctx.write(response); if (request.getMethod() == GET) { // write the content. Netty will close the stream. final FSDataInputStream stream = fs.open(path); try { ctx.write(new ChunkedStream(stream)); } catch(Exception e) { stream.close(); throw e; } } ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); lastContentFuture.addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (ctx.channel().isActive()) { LOG.error("Caught exception", cause); sendError(ctx, INTERNAL_SERVER_ERROR); } } /** * Send the "405 Method Not Allowed" response. * * @param ctx The channel context to write the response to. */ private static void sendMethodNotAllowed(ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, METHOD_NOT_ALLOWED); // close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } /** * Writes a simple error response message. * * @param ctx The channel context to write the response to. * @param status The response status. */ private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse( HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8)); HttpHeaders.setHeader(response, CONTENT_TYPE, "text/plain; charset=UTF-8"); // close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } } /** * Handle a request for a non-existent file. */ @ChannelHandler.Sharable public static class UnknownFileHandler extends SimpleChannelInboundHandler<Object> { @Override protected void channelRead0(ChannelHandlerContext ctx, Object message) { sendNotFound(ctx); } private static void sendNotFound(ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); // close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } } }