/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.server.http; import aphelion.shared.net.HttpWebSocketServerListener; import aphelion.server.http.HttpDownloadThread.UpgradeWebSocketHandler; import aphelion.shared.event.LoopEvent; import aphelion.shared.swissarmyknife.ThreadSafe; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; /** * A simple http 1.1 server with WebSockets. * * Supports: * + Sending static files (GET & HEAD) * + Directory index files * + Resumeable downloads (range header) * + Last-Modified & If-Modified-Since * + WebSockets using the java_websocket lib * * Threading model: HttpServer only has a server socket that it runs accept() on. This can be used in the main loop. All * sockets are then passed on to the HttpDownloadThread thread, which uses a select loop to serve all downloads from a * singe thread. If an Upgrade: WebSocket header is present, the socket is then removed from HttpDownloadThread and * added to one of the HttpWebSocketServer threads that all run their own select loop. The number of HttpWebSocketServer * threads that are spawned, depends on the number of cpu cores (including HyperThreading). select loop and parsing * happen on the same thread. This ensures the anti congestion features of TCP can do their thing properly. * * HttpWebsocketListener callbacks will originate from one of the HttpWebSocketServer threads. * * @author Joris */ public class HttpServer implements UpgradeWebSocketHandler, LoopEvent, HttpWebSocketServerListener { static final int LINEBUFFER_SIZE = 512; // Used to combine a line that spans multiple TCP segments static final int RCVBUFFER_SIZE = 16384; static final int BUFFER_SIZE = RCVBUFFER_SIZE + LINEBUFFER_SIZE; static final int WEBSOCKET_PARSE_THREADS = 2; static final long HTTP_TIMEOUT = 10; private static final Logger log = Logger.getLogger("aphelion.server.http"); public volatile HttpWebSocketServerListener websocketListener; private boolean stop = false; File httpdocs; ServerSocketChannel ssChannel; HttpDownloadThread downloadThread; private List<HttpWebSocketServer> websocketServers; volatile int upgradeWebSocketHandler_counter = 0; private final Set<WebSocket> websockets = new HashSet<>(); public HttpServer(ServerSocketChannel ssChannel, File httpdocs_, HttpWebSocketServerListener websocketListener) throws IOException { this.ssChannel = ssChannel; this.httpdocs = httpdocs_.getCanonicalFile(); this.websocketListener = websocketListener; if (ssChannel == null) { throw new IllegalArgumentException(); } if (this.httpdocs != null) { if (!this.httpdocs.isDirectory()) { this.httpdocs = null; log.log(Level.WARNING, "argument httpdocs is not a directory"); } } downloadThread = new HttpDownloadThread(this.httpdocs == null ? null : new File(this.httpdocs.getPath()), this); downloadThread.setDaemon(true); websocketServers = new ArrayList<>(WEBSOCKET_PARSE_THREADS); for (int a = 0; a < WEBSOCKET_PARSE_THREADS; ++a) { HttpWebSocketServer server = new HttpWebSocketServer(this); server.setDeamon(true); websocketServers.add(server); } } public void addRouteStatic(String path, File file) throws IOException, SecurityException { downloadThread.addRouteStatic(path, file); } public static ServerSocketChannel openServerChannel(InetSocketAddress listenAddr) throws IOException { ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.socket().bind(listenAddr); ssChannel.socket().setReceiveBufferSize(RCVBUFFER_SIZE); log.log(Level.INFO, "Listening on {0}:{1,number,#}", new Object[] { ssChannel.socket().getInetAddress(), getListeningPort(ssChannel) }); return ssChannel; } public void setup() throws IOException { if (stop) { throw new IllegalStateException(); } downloadThread.startWaitReady(); for (HttpWebSocketServer s : websocketServers) { s.startWaitReady(); } log.log(Level.INFO, "Http server setup at {0}:{1,number,#}", new Object[] { ssChannel.socket().getInetAddress(), getListeningPort(ssChannel) }); } public int getListeningPort() { return getListeningPort(ssChannel); } /** Returns the TCP port that we are currently listening on * @param ssChannel * @return -1 if not yet listening, otherwise the listening port. */ public static int getListeningPort(ServerSocketChannel ssChannel) { if (ssChannel == null) { return -1; } ServerSocket sock = ssChannel.socket(); if (sock == null) { return -1; } return sock.getLocalPort(); } public void stop() { downloadThread.interrupt(); for (HttpWebSocketServer s : websocketServers) { s.thread.interrupt(); } try { downloadThread.join(); for (HttpWebSocketServer s : websocketServers) { s.thread.join(); } } catch (InterruptedException ex) { } downloadThread = null; stop = true; log.log(Level.INFO, "HttpServer has stopped"); } @Override public void loop(long systemNanoTime, long sourceNanoTime) { if (stop) { throw new IllegalStateException(); } try { SocketChannel sChannel; while ((sChannel = ssChannel.accept()) != null) { downloadThread.addNewChannel(sChannel); } } catch (ClosedChannelException ex) { // This exception is okay after stop() has been called. // However normally this error should only shown once. // Stop running the loop (or unregister this object from the loop) // when calling close() log.log(Level.INFO, "Channel closed in accept()"); } catch (IOException ex) { log.log(Level.SEVERE, "IOException in accept()", ex); } } /** * Returns a WebSocket[] of currently connected clients. use synchronized() on the list while doing anything * with it * * @return The currently connected clients. */ @ThreadSafe public Set<WebSocket> websockets() { return websockets; } @Override @ThreadSafe // The list is not modified after the constructor public void upgradeWebSocketHandler(SocketChannel sChannel, ByteBuffer prependData) { HttpWebSocketServer s = websocketServers.get(upgradeWebSocketHandler_counter % websocketServers.size()); ++upgradeWebSocketHandler_counter; s.addNewChannel(sChannel, prependData); } @Override @ThreadSafe public boolean wssConnect(SelectionKey key) { HttpWebSocketServerListener listener = this.websocketListener; if (listener == null) { return true; } return listener.wssConnect(key); } @Override @ThreadSafe public void wssOpen(WebSocket conn, ClientHandshake handshake) { HttpWebSocketServerListener listener = this.websocketListener; synchronized(websockets) { websockets.add(conn); } if (listener != null) { listener.wssOpen(conn, handshake); } } @Override @ThreadSafe public void wssClose(WebSocket conn, int code, String reason, boolean remote) { HttpWebSocketServerListener listener = this.websocketListener; try { if (listener != null) { listener.wssClose(conn, code, reason, remote); } } finally { synchronized (websockets) { websockets.remove(conn); } } } @Override @ThreadSafe public void wssMessage(WebSocket conn, String message) { HttpWebSocketServerListener listener = this.websocketListener; if (listener != null) { listener.wssMessage(conn, message); } } @Override @ThreadSafe public void wssMessage(WebSocket conn, ByteBuffer message) { HttpWebSocketServerListener listener = this.websocketListener; if (listener != null) { listener.wssMessage(conn, message); } } @Override @ThreadSafe public void wssError(WebSocket conn, Exception ex) { HttpWebSocketServerListener listener = this.websocketListener; if (listener != null) { listener.wssError(conn, ex); } } }