/* * 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.server.http.HttpConnection.ConnectionStateChangeListener; import aphelion.server.http.HttpConnection.STATE; import aphelion.shared.swissarmyknife.ThreadSafe; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedSelectorException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Joris */ class HttpDownloadThread extends Thread implements ConnectionStateChangeListener { private static final Logger log = Logger.getLogger("aphelion.server.http"); private volatile boolean running = false; private volatile boolean ready = false; private Selector selector; private final File defaultRoute; private final Map<String, File> routes = new HashMap<>(); private final UpgradeWebSocketHandler upgradeWebSocketHandler; private final ByteBuffer buf = ByteBuffer.allocateDirect(HttpServer.BUFFER_SIZE); private final ConcurrentLinkedQueue <SocketChannel> newChannels = new ConcurrentLinkedQueue<>(); private long lastTimeoutCheck = System.nanoTime(); HttpDownloadThread(File httpdocs, UpgradeWebSocketHandler upgradeWebSocketHandler) { this.defaultRoute = httpdocs; this.upgradeWebSocketHandler = upgradeWebSocketHandler; } /** Register a route (url path) to be served by the specified file or directory. * @param path The path part of the URL that this route applies to. * Must not begin or end with a slash * For example "assets" or "abc/def" * @param file File or directory */ public void addRouteStatic(String path, File file) throws IOException, SecurityException { if (running) { throw new IllegalStateException(); // If adding routes while the server runs is desirable, // this.routes should become thread safe. } routes.put(path, file.getCanonicalFile()); } @Override public void connectionStateChange(HttpConnection conn, STATE oldState, STATE newState) { if (newState == HttpConnection.STATE.CLOSED) { conn.key.attach(null); conn.key.cancel(); try { conn.channel.close(); } catch (IOException ex) { log.log(Level.SEVERE, null, ex); } } else if (newState == HttpConnection.STATE.UPGRADE) { conn.key.attach(null); conn.key.cancel(); if (conn.websocket && upgradeWebSocketHandler != null) { ByteBuffer rawHead = conn.rawHead; conn.rawHead = null; // make sure nothing is able to interfere rawHead.flip(); upgradeWebSocketHandler.upgradeWebSocketHandler(conn.channel, rawHead); } else { try { conn.channel.close(); } catch (IOException ex) { log.log(Level.SEVERE, null, ex); } } } } public void startWaitReady() { this.start(); while (!ready) { try { Thread.sleep(1); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return; } } } static interface UpgradeWebSocketHandler { void upgradeWebSocketHandler(SocketChannel sChannel, ByteBuffer prependData); } @Override public void run() { running = true; setName("HttpDownload-"+getId()); try { selector = Selector.open(); ready = true; while (!this.isInterrupted()) { try { selector.select(500); } catch (ClosedSelectorException ex) { break; } { SocketChannel sChannel = newChannels.poll(); if (sChannel != null) { sChannel.configureBlocking(false); sChannel.socket().setTcpNoDelay(false); SelectionKey key = sChannel.register(selector, SelectionKey.OP_READ); key.attach(new HttpConnection(this, key, sChannel, defaultRoute, routes)); } } Iterator<SelectionKey> it; long now = System.nanoTime(); if (now - lastTimeoutCheck > 1_000_000_000l) { lastTimeoutCheck = now; it = selector.keys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); HttpConnection conn = (HttpConnection) key.attachment(); if (now - conn.nanoLastReceived > HttpServer.HTTP_TIMEOUT * 1_000_000_000l) { log.log(Level.INFO, "Dropping connection {0} because of timeout", conn.channel.getRemoteAddress()); key.attach(null); key.cancel(); conn.channel.close(); conn.closed(); } } } it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); HttpConnection conn = (HttpConnection) key.attachment(); it.remove(); if (conn == null) { // was just removed by a timeout continue; } try { if (key.isReadable()) { if (!readable(conn)) { key.attach(null); key.cancel(); conn.channel.close(); conn.closed(); continue; } } if (key.isValid() && key.isWritable()) { conn.writeable(); } } catch (IOException ex) { log.log(Level.WARNING, null, ex); key.attach(null); key.cancel(); conn.channel.close(); conn.closed(); } } } selector.close(); selector = null; } catch (IOException ex) { log.log(Level.SEVERE, null, ex); } } /** @return false if this connection should be removed */ private boolean readable(HttpConnection conn) throws IOException { buf.clear(); // start reading at LINEBUFFER_SIZE so that the previous line can be prepended buf.limit(buf.capacity()); buf.position(HttpServer.LINEBUFFER_SIZE); buf.mark(); int read; try { read = conn.channel.read(buf); } catch (ClosedChannelException ex) { return false; } if (read < 0) { return false; } if (read > 0) { buf.limit(buf.position()); buf.reset(); conn.read(buf); } return true; } /** * Add a new socket channel to be handled by this thread. */ @ThreadSafe void addNewChannel(SocketChannel sChannel) throws IOException { newChannels.add(sChannel); try { selector.wakeup(); } catch (IllegalStateException | NullPointerException ex) { // Thread has not started yet, or it just stopped assert false; } } }