/* * HttpAcceptor - Accepts incomming http connections. Copyright (C) 2003 Mark J. * Wielaard * * This file is part of Snark. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2, or (at your option) any later version. * * This program 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 General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.klomp.snark; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; public class HttpAcceptor { private static final String SNARKHTML = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>" + "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"" + "\"http://www.w3.org/TR/html4/loose.dtd\">" + "<html>" + "<head><title>Snark Client</title></head>" + "<body>" + "<h1>Snark Client</h1>" + "<p>Snark is a client for downloading and sharing files distributed with the BitTorrent protocol. It is not a normal webserver.</p>" + "<p><a href=\"announce\">Tracker</a></p>" + "<hr><p>For more info see <a href=\"http://www.klomp.org/snark/\">The Hunting of the Snark Project</a></p>" + "</body>" + "</html>"; private static final byte[] SNARKPAGE; private static final String ASCII = "US-ASCII"; private static final byte[] CRLF = new byte[] { '\r', '\n' }; private static final byte[] HTTP_STATUS; private static final byte[] CONTENT_LENGTH; private static final byte[] CONTENT_TYPE; static { try { SNARKPAGE = SNARKHTML.getBytes(ASCII); String STATUS = "HTTP/1.0 "; HTTP_STATUS = STATUS.getBytes(ASCII); CONTENT_LENGTH = "Content-Length: ".getBytes(ASCII); CONTENT_TYPE = "Content-Type: ".getBytes(ASCII); } catch (UnsupportedEncodingException uee) { // Cannot happen, US-ASCII unknown? throw new InternalError(uee.toString()); } } private final Tracker tracker; /** * Creates a HttpAcceptor that can handle torrent metadata of the given * Tracker. */ public HttpAcceptor (Tracker tracker) { this.tracker = tracker; } public void connection (Socket sock, BufferedInputStream bis, BufferedOutputStream bos) throws IOException { BufferedReader br = new BufferedReader( new InputStreamReader(bis, ASCII)); String resource = readRequest(br); log.log(Level.FINE, "HTTP request for: " + resource); if (resource != null) { Map headers = readHeaders(br); log.log(Level.FINER, headers.toString()); if (resource.equals("/")) { sendData(bos, SNARKPAGE, "text/html"); } else if (resource.startsWith("/announce")) { Map params = parseParams(resource); byte[] response = tracker.handleRequest(sock.getInetAddress(), sock.getPort(), params); sendData(bos, response, "application/octet-stream"); } else if (resource.endsWith(".torrent")) { MetaInfo info = tracker.getMetaInfo( resource.substring(1, resource.length() - 8)); if (info != null) { byte[] torrent = info.getTorrentData(); sendData(bos, torrent, "application/x-bittorrent"); } else { sendError(bos, 404, "Unable to locate that hash."); } } else { sendError(bos, 404, "Snark Client. Not a real webserver."); } } else { sendError(bos, 500, "Snark Client. Not a real webserver."); } sock.close(); } /** * Processes an incoming HTTP request. Only handles the most basic GET * requests. Returns the (URLEncoded) requested resource or null if the * request wasn't a valid GET request. */ private static String readRequest (BufferedReader br) throws IOException { String request = br.readLine(); if (request != null && request.startsWith("GET ")) { String resource; int index = request.indexOf(' ', 4); if (index == -1) { resource = request.substring(4); } else { resource = request.substring(4, index); } return resource; } else { return null; } } /** * Consumes all headers and puts them into a Map mapping header value to * header key Strings. */ private static Map<String, String> readHeaders (BufferedReader br) throws IOException { Map<String, String> m = new HashMap<String, String>(); String header = br.readLine(); while (header != null && header.length() != 0) { header = br.readLine(); if (header != null && header.length() != 0) { int index = header.indexOf(": "); if (index != -1) { String key = header.substring(0, index); String value = header.substring(index + 2); m.put(key, value); } } } return m; } /** * Sends a HTTP OK, the necessary headers and the data. */ private static void sendData (OutputStream out, byte[] data, String content_type) throws IOException { sendData(out, 200, "OK", data, content_type); } private static void sendData (OutputStream out, int responseCode, String reason, byte[] data, String content_type) throws IOException { log.log(Level.FINER, "HTTP/1.0 " + responseCode + " " + reason + " " + content_type + " (" + data.length + " bytes)"); byte[] type = content_type.getBytes(ASCII); // Status line out.write(HTTP_STATUS); out.write(Integer.toString(responseCode).getBytes(ASCII)); out.write(' '); out.write(reason.getBytes(ASCII)); out.write(CRLF); // Entity headers out.write(CONTENT_LENGTH); out.write(Integer.toString(data.length).getBytes(ASCII)); out.write(CRLF); out.write(CONTENT_TYPE); out.write(type); out.write(CRLF); // Start of data out.write(CRLF); out.write(data); out.flush(); } private static void sendError (OutputStream out, int responseCode, String reason) throws IOException { sendData(out, responseCode, reason, reason.getBytes(ASCII), "text/plain"); } /** * Returns a key to value map of the GET request query string parameters. It * expects a '?' and the urlencoded key=value pairs. Note that the key and * value are NOT url decoded before putting in the paramaters map. */ private static Map<String, String> parseParams (String request) { Map<String, String> m = new HashMap<String, String>(); int index = request.indexOf('?'); if (index != -1) { String params = request.substring(index + 1); StringTokenizer st = new StringTokenizer(params, "&"); while (st.hasMoreTokens()) { String token = st.nextToken(); index = token.indexOf('='); if (index != -1) { String key = token.substring(0, index); String value = token.substring(index + 1); m.put(key, value); } } } return m; } /** The Java logger used to process our log events. */ protected static final Logger log = Logger.getLogger("org.klomp.snark.server"); }