/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.frostwire.android.httpserver;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executor;
import android.util.Log;
/**
* Provides implementation for HTTP
*/
public class HttpServer {
private static final String TAG = "FW.HttpServer";
private static final int CLOCK_TICK = ServerConfig.getClockTick();
private static final long IDLE_INTERVAL = ServerConfig.getIdleInterval();
private static final int MAX_IDLE_CONNECTIONS = ServerConfig.getMaxIdleConnections();
private String _protocol;
private Executor _executor;
private ContextList _contexts;
private ServerSocketChannel _schan;
private Selector _selector;
private SelectionKey _listenerKey;
private Set<HttpConnection> _idleConnections;
private Set<HttpConnection> _allConnections;
private List<Event> _events;
private Object _lolock = new Object();
private volatile boolean _finished = false;
private volatile boolean _terminating = false;
private boolean _bound = false;
private boolean _started = false;
private volatile long _time; /* current time */
//private volatile long _ticks; /* number of clock ticks since server started */
private Timer _timer;
public HttpServer(String protocol, InetSocketAddress addr, int backlog) throws IOException {
_protocol = protocol;
_contexts = new ContextList();
_schan = ServerSocketChannel.open();
if (addr != null) {
ServerSocket socket = _schan.socket();
socket.bind(addr, backlog);
_bound = true;
}
_selector = Selector.open();
_schan.configureBlocking(false);
_listenerKey = _schan.register(_selector, SelectionKey.OP_ACCEPT);
dispatcher = new Dispatcher();
_idleConnections = Collections.synchronizedSet(new HashSet<HttpConnection>());
_allConnections = Collections.synchronizedSet(new HashSet<HttpConnection>());
_time = System.currentTimeMillis();
_timer = new Timer("server-timer", true);
_timer.schedule(new ServerTimerTask(), CLOCK_TICK, CLOCK_TICK);
_events = new LinkedList<Event>();
//Log.d(TAG, "HttpServer created " + protocol + " " + addr);
}
public void bind(InetSocketAddress addr, int backlog) throws IOException {
if (_bound) {
throw new BindException("HttpServer already bound");
}
if (addr == null) {
throw new NullPointerException("null address");
}
ServerSocket socket = _schan.socket();
socket.bind(addr, backlog);
_bound = true;
}
public void start() {
if (!_bound || _started || _finished) {
throw new IllegalStateException("server in wrong state");
}
if (_executor == null) {
_executor = new DefaultExecutor();
}
Thread t = new Thread(dispatcher);
_started = true;
t.start();
}
public void setExecutor(Executor executor) {
if (_started) {
throw new IllegalStateException("server already started");
}
this._executor = executor;
}
private static class DefaultExecutor implements Executor {
public void execute(Runnable task) {
task.run();
}
}
public Executor getExecutor() {
return _executor;
}
public void stop(int delay) {
if (delay < 0) {
throw new IllegalArgumentException("negative delay parameter");
}
_terminating = true;
try {
_schan.close();
} catch (IOException e) {
}
try {
_selector.wakeup();
} catch (Exception e) {
}
long latest = System.currentTimeMillis() + delay * 1000;
while (System.currentTimeMillis() < latest) {
delay();
if (_finished) {
break;
}
}
_finished = true;
try {
_selector.wakeup();
} catch (Exception e) {
}
synchronized (_allConnections) {
for (HttpConnection c : _allConnections) {
c.close();
}
}
_allConnections.clear();
_idleConnections.clear();
_timer.cancel();
}
Dispatcher dispatcher;
public synchronized HttpContext createContext(String path, HttpHandler handler) {
if (handler == null || path == null) {
throw new NullPointerException("null handler, or path parameter");
}
HttpContext context = new HttpContext(_protocol, path, handler, this);
_contexts.add(context);
//Log.d(TAG, "context created: " + path);
return context;
}
public synchronized HttpContext createContext(String path) {
if (path == null) {
throw new NullPointerException("null path parameter");
}
HttpContext context = new HttpContext(_protocol, path, null, this);
_contexts.add(context);
//Log.d(TAG, "context created: " + path);
return context;
}
public synchronized void removeContext(String path) throws IllegalArgumentException {
if (path == null) {
throw new NullPointerException("null path parameter");
}
_contexts.remove(_protocol, path);
//Log.d(TAG, "context removed: " + path);
}
public synchronized void removeContext(HttpContext context) throws IllegalArgumentException {
_contexts.remove(context);
//Log.d(TAG, "context removed: " + context.getPath());
}
public InetSocketAddress getAddress() {
return (InetSocketAddress) _schan.socket().getLocalSocketAddress();
}
public void addEvent(Event r) {
synchronized (_lolock) {
_events.add(r);
_selector.wakeup();
}
}
private int resultSize() {
synchronized (_lolock) {
return _events.size();
}
}
/* main server listener task */
private final class Dispatcher implements Runnable {
private void handleEvent(Event r) {
HttpExchange t = r.exchange;
HttpConnection c = t.getConnection();
try {
if (r instanceof WriteFinishedEvent) {
int exchanges = endExchange();
if (_terminating && exchanges == 0) {
_finished = true;
}
LeftOverInputStream is = t.getOriginalInputStream();
if (!is.isEOF()) {
t.close = true;
}
if (t.close || _idleConnections.size() >= MAX_IDLE_CONNECTIONS) {
c.close();
_allConnections.remove(c);
} else {
if (is.isDataBuffered()) {
/* don't re-enable the interestops, just handle it */
handle(c.getChannel(), c);
} else {
/* re-enable interestops */
SelectionKey key = c.getSelectionKey();
if (key.isValid()) {
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
}
c.time = getTime() + IDLE_INTERVAL;
_idleConnections.add(c);
}
}
}
} catch (IOException e) {
Log.e(TAG, "Dispatcher (1)", e);
c.close();
}
}
public void run() {
while (!_finished) {
try {
/* process the events list first */
while (resultSize() > 0) {
Event r;
synchronized (_lolock) {
r = _events.remove(0);
handleEvent(r);
}
}
_selector.select(1000);
/* process the selected list now */
Set<SelectionKey> selected = _selector.selectedKeys();
Iterator<SelectionKey> iter = selected.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.equals(_listenerKey)) {
if (_terminating) {
continue;
}
SocketChannel chan = _schan.accept();
if (chan == null) {
continue; /* cancel something ? */
}
chan.configureBlocking(false);
SelectionKey newkey = chan.register(_selector, SelectionKey.OP_READ);
HttpConnection c = new HttpConnection();
c.selectionKey = newkey;
c.setChannel(chan);
newkey.attach(c);
_allConnections.add(c);
} else {
try {
if (key.isReadable()) {
SocketChannel chan = (SocketChannel) key.channel();
HttpConnection conn = (HttpConnection) key.attachment();
// interestOps will be restored at end of read
key.interestOps(0);
handle(chan, conn);
} else {
assert false;
}
} catch (IOException e) {
HttpConnection conn = (HttpConnection) key.attachment();
Log.e(TAG, "Dispatcher (2)", e);
conn.close();
}
}
}
} catch (CancelledKeyException e) {
Log.e(TAG, "Dispatcher (3)", e);
} catch (IOException e) {
Log.e(TAG, "Dispatcher (4)", e);
} catch (Exception e) {
Log.e(TAG, "Dispatcher (7)", e);
}
}
}
public void handle(SocketChannel chan, HttpConnection conn) throws IOException {
try {
Exchange t = new Exchange(chan, _protocol, conn);
_executor.execute(t);
} catch (HttpError e1) {
Log.e(TAG, "Dispatcher (5)", e1);
conn.close();
} catch (IOException e) {
Log.e(TAG, "Dispatcher (6)", e);
conn.close();
}
}
}
static boolean debug = ServerConfig.debugEnabled();
static synchronized void dprint(String s) {
if (debug) {
Log.d(TAG, s);
}
}
static synchronized void dprint(Exception e) {
if (debug) {
Log.d(TAG, e.getMessage(), e);
}
}
/* per exchange task */
private class Exchange implements Runnable {
private SocketChannel _channel;
private HttpConnection _connection;
private HttpContext _context;
private InputStream _rawin;
private OutputStream _rawout;
private String protocol;
private HttpExchange _tx;
private HttpContext _ctx;
public Exchange(SocketChannel chan, String protocol, HttpConnection conn) throws IOException {
_channel = chan;
_connection = conn;
this.protocol = protocol;
}
public void run() {
/* context will be null for new connections */
_context = _connection.getHttpContext();
boolean newconnection;
String requestLine = null;
try {
if (_context != null) {
_rawin = _connection.getInputStream();
_rawout = _connection.getRawOutputStream();
newconnection = false;
} else {
/* figure out what kind of connection this is */
newconnection = true;
_rawin = new BufferedInputStream(new Request.ReadStream(HttpServer.this, _channel));
_rawout = new Request.WriteStream(HttpServer.this, _channel);
_connection.raw = _rawin;
_connection.rawout = _rawout;
}
Request req = new Request(_rawin, _rawout);
requestLine = req.requestLine();
if (requestLine == null) {
/* connection closed */
_connection.close();
return;
}
int space = requestLine.indexOf(' ');
if (space == -1) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "Bad request line");
return;
}
String method = requestLine.substring(0, space);
int start = space + 1;
space = requestLine.indexOf(' ', start);
if (space == -1) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "Bad request line");
return;
}
String uriStr = requestLine.substring(start, space);
URI uri = new URI(uriStr);
start = space + 1;
String version = requestLine.substring(start);
Headers headers = req.headers();
String s = headers.getFirst("Transfer-encoding");
int clen = 0;
if (s != null && s.equalsIgnoreCase("chunked")) {
clen = -1;
} else {
s = headers.getFirst("Content-Length");
if (s != null) {
clen = Integer.parseInt(s);
}
}
_ctx = _contexts.findContext(protocol, uri.getPath());
if (_ctx == null) {
reject(Code.HTTP_NOT_FOUND, requestLine, "No context found for request");
return;
}
_connection.setContext(_ctx);
if (_ctx.getHandler() == null) {
reject(Code.HTTP_INTERNAL_ERROR, requestLine, "No handler for context");
return;
}
_tx = new HttpExchange(method, uri, req, clen, _connection);
String chdr = headers.getFirst("Connection");
Headers rheaders = _tx.getResponseHeaders();
if (chdr != null && chdr.equalsIgnoreCase("close")) {
_tx.close = true;
}
if (version.equalsIgnoreCase("http/1.0")) {
_tx.http10 = true;
if (chdr == null) {
_tx.close = true;
rheaders.set("Connection", "close");
} else if (chdr.equalsIgnoreCase("keep-alive")) {
rheaders.set("Connection", "keep-alive");
int idle = (int) ServerConfig.getIdleInterval() / 1000;
int max = ServerConfig.getMaxIdleConnections();
String val = "timeout=" + idle + ", max=" + max;
rheaders.set("Keep-Alive", val);
}
}
if (newconnection) {
_connection.setParameters(_rawin, _rawout, _channel, protocol, _ctx, _rawin);
}
/* check if client sent an Expect 100 Continue.
* In that case, need to send an interim response.
* In future API may be modified to allow app to
* be involved in this process.
*/
String exp = headers.getFirst("Expect");
if (exp != null && exp.equalsIgnoreCase("100-continue")) {
logReply(100, requestLine, null);
sendReply(Code.HTTP_CONTINUE, false, null);
}
/* uf is the list of filters seen/set by the user.
* sf is the list of filters established internally
* and which are not visible to the user. uc and sc
* are the corresponding Filter.Chains.
* They are linked together by a LinkHandler
* so that they can both be invoked in one call.
*/
List<Filter> sf = _ctx.getSystemFilters();
List<Filter> uf = _ctx.getFilters();
Filter.Chain sc = new Filter.Chain(sf, _ctx.getHandler());
Filter.Chain uc = new Filter.Chain(uf, new LinkHandler(sc));
/* set up the two stream references */
_tx.getRequestBody();
_tx.getResponseBody();
uc.doFilter(_tx);
} catch (IOException e1) {
Log.e(TAG, "ServerImpl.Exchange (1), e: " + e1.getMessage());
_connection.close();
} catch (NumberFormatException e3) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "NumberFormatException thrown");
} catch (URISyntaxException e) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "URISyntaxException thrown");
} catch (Throwable e4) {
Log.e(TAG, "ServerImpl.Exchange (2)", e4);
_connection.close();
}
}
/* used to link to 2 or more Filter.Chains together */
private class LinkHandler implements HttpHandler {
Filter.Chain nextChain;
LinkHandler(Filter.Chain nextChain) {
this.nextChain = nextChain;
}
public void handle(HttpExchange exchange) throws IOException {
nextChain.doFilter(exchange);
}
}
void reject(int code, String requestStr, String message) {
logReply(code, requestStr, message);
sendReply(code, true, "<h1>" + code + Code.msg(code) + "</h1>" + message);
}
void sendReply(int code, boolean closeNow, String text) {
try {
String s = "HTTP/1.1 " + code + Code.msg(code) + "\r\n";
if (text != null && text.length() != 0) {
s = s + "Content-Length: " + text.length() + "\r\n";
s = s + "Content-Type: text/html\r\n";
} else {
s = s + "Content-Length: 0\r\n";
text = "";
}
if (closeNow) {
s = s + "Connection: close\r\n";
}
s = s + "\r\n" + text;
byte[] b = s.getBytes("ISO8859_1");
_rawout.write(b);
_rawout.flush();
if (closeNow) {
_connection.close();
}
} catch (IOException e) {
Log.e(TAG, "ServerImpl.sendReply", e);
_connection.close();
}
}
}
public void logReply(int code, String requestStr, String text) {
if (text == null) {
text = "";
}
//String message = requestStr + " [" + code + " " + Code.msg(code) + "] (" + text + ")";
//Log.i(TAG, message);
}
public long getTime() {
return _time;
}
private void delay() {
Thread.yield();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
private int exchangeCount = 0;
public synchronized void startExchange() {
exchangeCount++;
}
private synchronized int endExchange() {
exchangeCount--;
assert exchangeCount >= 0;
return exchangeCount;
}
/**
* TimerTask run every CLOCK_TICK ms
*/
private class ServerTimerTask extends TimerTask {
public void run() {
LinkedList<HttpConnection> toClose = new LinkedList<HttpConnection>();
_time = System.currentTimeMillis();
//_ticks++;
synchronized (_idleConnections) {
for (HttpConnection c : _idleConnections) {
if (c.time <= _time) {
toClose.add(c);
}
}
for (HttpConnection c : toClose) {
_idleConnections.remove(c);
_allConnections.remove(c);
c.close();
}
}
}
}
}