/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.katari.tools;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Enumeration;
import java.util.Locale;
import java.util.TimeZone;
import java.net.ServerSocket;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple, tiny, nicely embeddable HTTP 1.0 server in Java
*
* <p> NanoHTTPD version 1.1,
* Copyright © 2001,2005-2007 Jarno Elonen (elonen@iki.fi,
* http://iki.fi/elonen/)
*
* <p><b>Features + limitations: </b><ul>
*
* <li> Only one Java file </li>
* <li> Java 1.1 compatible </li>
* <li> Released as open source, Modified BSD licence </li>
* <li> No fixed config files, logging, authorization etc. (Implement
* yourself if you need them.) </li>
* <li> Supports parameter parsing of GET and POST methods </li>
* <li> Supports both dynamic content and file serving </li>
* <li> Never caches anything </li>
* <li> Doesn't limit bandwidth, request time or simultaneous connections
* </li>
* <li> Default code serves files and shows all HTTP parameters and
* headers</li>
* <li> File server supports directory listing, index.html and index.htm
* </li>
* <li> File server does the 301 redirection trick for directories without
* '/'</li>
* <li> File server supports simple skipping for files (continue download)
* </li>
* <li> File server uses current directory as a web root </li>
* <li> File server serves also very long files without memory overhead
* </li>
* <li> Contains a built-in list of most common mime types </li>
* <li> All header names are converted lowercase so they don't vary between
* browsers/clients </li>
*
* </ul>
*
* <p><b>Ways to use: </b><ul>
*
* <li> Subclass serve() and embed to your own program </li>
*
* </ul>
*
* See the end of the source file for distribution license
* (Modified BSD licence)
*/
public abstract class NanoHTTPD {
/** The class logger.
*/
private static Logger log = LoggerFactory.getLogger(NanoHTTPD.class);
/** The buffer size for reading and writing to the client.
*/
private static final int BUFFER_SIZE = 2048;
/** The end of line sequence.
*/
private static final String EOL = "\r\n";
// ==================================================
// API parts
// ==================================================
/** Override this to customize the server.
*
* @param uri Percent-decoded URI without parameters, for example
* "/index.cgi"
*
* @param method "GET", "POST" etc.
*
* @param parms Parsed, percent decoded parameters from URI and, in case of
* POST, data.
*
* @param header Header entries, percent decoded
*
* @return HTTP response, see class Response for details
*/
protected abstract Response serve(final String uri, final String method,
final Properties header, final Properties parms);
/** HTTP response.
*
* Return one of these from serve().
*/
public static class Response {
/** HTTP status code after processing, for example "200 OK", HTTP_OK.
*/
private String status;
/** MIME type of content, e.g. "text/html".
*/
private String mimeType;
/** Data of the response, may be null.
*/
private InputStream data;
/** Headers for the HTTP response.
*
* Use addHeader() to add lines.
*/
private Properties header = new Properties();
/** Default constructor: response = HTTP_OK, data = mime = 'null'.
*/
public Response() {
status = HTTP_OK;
}
/** Basic constructor.
*
* @param theStatus the response status code (for example, HTTP_OK).
*
* @param theMimeType the mime type of the response.
*
* @param theData the Data to send to the client.
*/
public Response(final String theStatus, final String theMimeType, final
InputStream theData) {
status = theStatus;
mimeType = theMimeType;
data = theData;
}
/** Convenience method that makes an InputStream out of given text.
*
* @param theMimeType the mime type of the response.
*
* @param txt the Data to send to the client.
*/
public Response(final String theMimeType, final String txt) {
status = HTTP_OK;
mimeType = theMimeType;
data = new ByteArrayInputStream(txt.getBytes());
}
/**
* Adds given line to the header.
*
* @param theName The header name.
*
* @param theValue The header value.
*/
public void addHeader(final String theName, final String theValue) {
header.put(theName, theValue);
}
/** HTTP status code after processing, for example "200 OK", HTTP_OK.
*
* @return The http status code.
*/
public String getStatus() {
return status;
}
/** MIME type of content, e.g. "text/html".
*
* @return The mime content type.
*/
public String getMimeType() {
return mimeType;
}
/** Data of the response, may be null.
*
* @return The data to send to the client.
*/
public InputStream getData() {
return data;
}
/** Headers for the HTTP response.
*
* @return the http headers.
*/
public Properties getHeader() {
return header;
}
}
/** Some HTTP response status codes.
*/
public static final String
HTTP_OK = "200 OK",
HTTP_REDIRECT = "301 Moved Permanently",
HTTP_FORBIDDEN = "403 Forbidden",
HTTP_NOTFOUND = "404 Not Found",
HTTP_BADREQUEST = "400 Bad Request",
HTTP_INTERNALERROR = "500 Internal Server Error",
HTTP_NOTIMPLEMENTED = "501 Not Implemented";
/** Common mime types for dynamic content.
*/
public static final String
MIME_PLAINTEXT = "text/plain",
MIME_HTML = "text/html",
MIME_DEFAULT_BINARY = "application/octet-stream";
// ==================================================
// Socket & server code
// ==================================================
/** The thread that waits for client requests.
*/
private Thread serverThread = null;
/** The connection established by a client.
*/
private ServerSocket serverSocket = null;
/** Starts a HTTP server to given port.
*
* @param port The port number to listen on.
*/
public NanoHTTPD(final int port) {
try {
serverSocket = new ServerSocket(port);
myTcpPort = serverSocket.getLocalPort();
} catch (IOException e) {
throw new RuntimeException("Error creating server socket", e);
}
serverThread = new Thread(new Runnable() {
public void run() {
log.trace("Entering run");
try {
while (true) {
Socket socket = serverSocket.accept();
if (serverThread != null) {
new HTTPSession(socket);
} else {
break;
}
}
} catch (IOException e) {
if (!serverSocket.isClosed()) {
throw new RuntimeException("Error accepting connections", e);
} else {
serverSocket = null;
}
}
log.trace("Leaving run");
}
});
serverThread.setDaemon(true);
serverThread.start();
}
/** Stops the server.
*
* This ends the serving thread and closes the listening socket.
*/
public void stop() {
if (serverThread != null) {
Thread threadToStop = serverThread;
serverThread = null;
threadToStop.interrupt();
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException("Unable to close server socket", e);
}
try {
threadToStop.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/** Handles one session, parsing the HTTP request and returning the response.
*/
private class HTTPSession implements Runnable {
/** Builds a session.
*
* @param s The socket that this session processes.
*/
public HTTPSession(final Socket s) {
mySocket = s;
Thread t = new Thread(this);
t.setDaemon(true);
t.start();
}
/** Waits for client data and processes the request.
*/
public void run() {
try {
InputStream is = mySocket.getInputStream();
if (is == null) {
return;
}
BufferedReader in = new BufferedReader(new InputStreamReader(is));
// Read the request line
StringTokenizer st = new StringTokenizer(in.readLine());
if (!st.hasMoreTokens()) {
sendError(HTTP_BADREQUEST,
"BAD REQUEST: Syntax error. Usage: GET /example/file.html");
}
String method = st.nextToken();
if (!st.hasMoreTokens()) {
sendError(HTTP_BADREQUEST,
"BAD REQUEST: Missing URI. Usage: GET /example/file.html");
}
String uri = decodePercent(st.nextToken());
// Decode parameters from the URI
Properties parms = new Properties();
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
}
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names uppercase since they are
// case insensitive and vary by client.
Properties header = new Properties();
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
header.put(line.substring(0, p).trim().toLowerCase(),
line.substring(p + 1).trim());
line = in.readLine();
}
}
// If the method is POST, there may be parameters
// in data section, too, read it:
if (method.equalsIgnoreCase("POST")) {
long contentLength = Long.MAX_VALUE;
String contentLengthHeader = header.getProperty("content-length");
if (contentLengthHeader != null) {
try {
contentLength = Integer.parseInt(contentLengthHeader);
} catch (NumberFormatException ex) {
log.error("Ignoring content-length header.", ex);
}
}
StringBuilder postLine = new StringBuilder();
char[] buf = new char[BUFFER_SIZE];
int read = 0;
if (contentLength != 0) {
read = in.read(buf);
}
while (read >= 0 && contentLength > 0
&& !postLine.toString().endsWith(EOL)) {
contentLength -= read;
postLine.append(buf, 0, read);
if (contentLength > 0) {
read = in.read(buf);
}
}
decodeParms(postLine.toString().trim(), parms);
}
// Ok, now do the serve()
Response r = serve(uri, method, header, parms);
if (r == null) {
sendError(HTTP_INTERNALERROR,
"SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
sendResponse(r.getStatus(), r.getMimeType(), r.getHeader(),
r.getData());
}
in.close();
} catch (IOException ioe) {
try {
sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: "
+ ioe.getMessage());
} catch (Exception t) {
log.error("Error processing a request.", t);
}
} catch (InterruptedException ie) {
// Thrown by sendError, ignore and exit the thread.
log.error("Ignoring a thred interruption", ie);
}
}
/** The radix for hexadecimal numbers.
*/
private static final int HEX_RADIX = 16;
/** Decodes the percent encoding scheme.
*
* @param str the string to decode, for example: "an+example%20string" gets
* decoded to "an example string"
*
* @return returns the decoded string.
*
* @throws InterruptedException when another thread calls interrupt on this
* thread.
*/
private String decodePercent(final String str)
throws InterruptedException {
try {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
switch (c) {
case '+':
sb.append(' ');
break;
case '%':
// Skip the %.
++i;
sb.append((char) Integer.parseInt(
str.substring(i, i + 2), HEX_RADIX));
++i;
break;
default:
sb.append(c);
break;
}
}
return new String(sb.toString().getBytes());
} catch (Exception e) {
sendError(HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding.");
return null;
}
}
/** Decodes parameters in percent-encoded URI-format.
*
* (e.g. "name=Jack%20Daniels&pass=Single%20Malt") and adds them to given
* Properties.
*
* @param parms The parameters. If null, this operation does nothing.
*
* @param p The property to decode.
*
* @throws InterruptedException when another thread calls interrupt on this
* thread.
*/
private void decodeParms(final String parms, final Properties p)
throws InterruptedException {
if (parms == null) {
return;
}
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0) {
p.put(decodePercent(e.substring(0, sep)).trim(),
decodePercent(e.substring(sep + 1)));
}
}
}
/**
* Returns an error message as a HTTP response and
* throws InterruptedException to stop furhter request processing.
*
* @param status The status to send to the client.
*
* @param msg The error message.
*
* @throws InterruptedException when another thread calls interrupt on this
* thread.
*/
private void sendError(final String status, final String msg) throws
InterruptedException {
sendResponse(status, MIME_PLAINTEXT, null,
new ByteArrayInputStream(msg.getBytes()));
throw new InterruptedException();
}
/** Sends given response to the socket.
*
* @param status The response status.
*
* @param mime The mime content type.
*
* @param header The headers to send.
*
* @param data The data to send to the client.
*/
private void sendResponse(final String status, final String mime, final
Properties header, final InputStream data) {
try {
if (status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
OutputStream out = mySocket.getOutputStream();
PrintWriter pw = new PrintWriter(out);
pw.print("HTTP/1.0 " + status + " \r\n");
if (mime != null) {
pw.print("Content-Type: " + mime + EOL);
}
if (header == null || header.getProperty("Date") == null) {
pw.print("Date: " + gmtFrmt.format(new Date()) + EOL);
}
if (header != null) {
Enumeration<?> e = header.keys();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String value = header.getProperty(key);
pw.print(key + ": " + value + EOL);
}
}
pw.print(EOL);
pw.flush();
if (data != null) {
byte[] buff = new byte[BUFFER_SIZE];
while (true) {
int read = data.read(buff, 0, BUFFER_SIZE);
if (read <= 0) {
break;
}
out.write(buff, 0, read);
}
}
out.flush();
out.close();
if (data != null) {
data.close();
}
} catch (IOException ioe) {
// Couldn't write? No can do.
try {
mySocket.close();
} catch (Exception t) {
// We ignore, but log, the exception.
log.error("Error writing to client.", t);
}
}
}
/** The socket for this session.
*/
private Socket mySocket;
};
/** The tcp port that the server is listening on.
*/
private int myTcpPort;
/** Returns the port the server is listening on.
*
* @return a port number.
*/
public int getPort() {
return myTcpPort;
}
/** GMT date formatter.
*/
private static java.text.SimpleDateFormat gmtFrmt;
static {
gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'",
Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
}
/** The distribution licence.
*/
public static final String LICENCE =
"Copyright (C) 2001,2005 by Jarno Elonen <elonen@iki.fi>\n"
+ "\n"
+ "Redistribution and use in source and binary forms, with or without\n"
+ "modification, are permitted provided that the following conditions\n"
+ "are met:\n"
+ "\n"
+ "Redistributions of source code must retain the above copyright"
+ " notice,\n"
+ "this list of conditions and the following disclaimer. Redistributions"
+ " in\n"
+ "binary form must reproduce the above copyright notice, this list of\n"
+ "conditions and the following disclaimer in the documentation and/or"
+ " other\n"
+ "materials provided with the distribution. The name of the author may"
+ " not\n"
+ "be used to endorse or promote products derived from this software"
+ " without\n"
+ "specific prior written permission. \n"
+ " \n"
+ "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
+ "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED"
+ " WARRANTIES\n"
+ "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE"
+ " DISCLAIMED.\n"
+ "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"
+ "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,"
+ " BUT\n"
+ "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF"
+ " USE,\n"
+ "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"
+ "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"
+ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
}