/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
 * (c) 2003 - 2004 mihi
 */
package net.i2p.i2ptunnel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import java.util.HashMap;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;

/**
 * Act as a mini HTTP proxy, handling various different types of requests,
 * forwarding them through I2P appropriately, and displaying the reply.  Supported
 * request formats are: <pre>
 *   $method http://$site[$port]/$path $protocolVersion
 * or
 *   $method $path $protocolVersion\nHost: $site
 * or
 *   $method http://i2p/$site/$path $protocolVersion
 * or 
 *   $method /$site/$path $protocolVersion
 * </pre>
 *
 * If the $site resolves with the I2P naming service, then it is directed towards
 * that eepsite, otherwise it is directed towards this client's outproxy (typically
 * "squid.i2p").  Only HTTP is supported (no HTTPS, ftp, mailto, etc).  Both GET
 * and POST have been tested, though other $methods should work.
 *
 */
public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable {
    private static final Log _log = new Log(I2PTunnelHTTPClient.class);

    private List proxyList;

    private HashMap addressHelpers = new HashMap();

    private final static byte[] ERR_REQUEST_DENIED =
        ("HTTP/1.1 403 Access Denied\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n"+
         "\r\n"+
         "<html><body><H1>I2P ERROR: REQUEST DENIED</H1>"+
         "You attempted to connect to a non-I2P website or location.<BR>")
        .getBytes();
    
    private final static byte[] ERR_DESTINATION_UNKNOWN =
        ("HTTP/1.1 503 Service Unavailable\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n"+
         "\r\n"+
         "<html><body><H1>I2P ERROR: DESTINATION NOT FOUND</H1>"+
         "That I2P Destination was not found. Perhaps you pasted in the "+
         "wrong BASE64 I2P Destination or the link you are following is "+
         "bad. The host (or the WWW proxy, if you're using one) could also "+
	 "be temporarily offline.  You may want to <b>retry</b>.  "+
         "Could not find the following Destination:<BR><BR><div>")
        .getBytes();
    
    private final static byte[] ERR_TIMEOUT =
        ("HTTP/1.1 504 Gateway Timeout\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n\r\n"+
         "<html><body><H1>I2P ERROR: TIMEOUT</H1>"+
         "That Destination was reachable, but timed out getting a "+
         "response.  This is likely a temporary error, so you should simply "+
         "try to refresh, though if the problem persists, the remote "+
         "destination may have issues.  Could not get a response from "+
         "the following Destination:<BR><BR>")
        .getBytes();

    private final static byte[] ERR_NO_OUTPROXY =
        ("HTTP/1.1 503 Service Unavailable\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n"+
         "\r\n"+
         "<html><body><H1>I2P ERROR: No outproxy found</H1>"+
         "Your request was for a site outside of I2P, but you have no "+
         "HTTP outproxy configured.  Please configure an outproxy in I2PTunnel")
         .getBytes();
    
    private final static byte[] ERR_AHELPER_CONFLICT =
        ("HTTP/1.1 409 Conflict\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n"+
         "\r\n"+
         "<html><body><H1>I2P ERROR: Destination key conflict</H1>"+
         "The addresshelper link you followed specifies a different destination key "+
         "than a host entry in your host database. "+
         "Someone could be trying to impersonate another eepsite, "+
         "or people have given two eepsites identical names.<P/>"+
         "You can resolve the conflict by considering which key you trust, "+
         "and either discarding the addresshelper link, "+
         "discarding the host entry from your host database, "+
         "or naming one of them differently.<P/>")
         .getBytes();
    
    private final static byte[] ERR_BAD_PROTOCOL =
        ("HTTP/1.1 403 Bad Protocol\r\n"+
         "Content-Type: text/html; charset=iso-8859-1\r\n"+
         "Cache-control: no-cache\r\n"+
         "\r\n"+
         "<html><body><H1>I2P ERROR: NON-HTTP PROTOCOL</H1>"+
         "The request uses a bad protocol. "+
         "The I2P HTTP Proxy supports http:// requests ONLY. Other protocols such as https:// and ftp:// are not allowed.<BR>")
        .getBytes();
    
    /** used to assign unique IDs to the threads / clients.  no logic or functionality */
    private static volatile long __clientId = 0;

    /**
     * @throws IllegalArgumentException if the I2PTunnel does not contain
     *                                  valid config to contact the router
     */
    public I2PTunnelHTTPClient(int localPort, Logging l, boolean ownDest, 
                               String wwwProxy, EventDispatcher notifyThis, 
                               I2PTunnel tunnel) throws IllegalArgumentException {
        super(localPort, ownDest, l, notifyThis, "HTTPHandler " + (++__clientId), tunnel);

        if (waitEventValue("openBaseClientResult").equals("error")) {
            notifyEvent("openHTTPClientResult", "error");
            return;
        }

        proxyList = new ArrayList();
        if (wwwProxy != null) {
            StringTokenizer tok = new StringTokenizer(wwwProxy, ",");
            while (tok.hasMoreTokens())
                proxyList.add(tok.nextToken().trim());
        }

        setName(getLocalPort() + " -> HTTPClient [WWW outproxy list: " + wwwProxy + "]");

        startRunning();

        notifyEvent("openHTTPClientResult", "ok");
    }

    private String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; }
    
    private String selectProxy() {
        synchronized (proxyList) {
            int size = proxyList.size();
            if (size <= 0) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Proxy list is empty - no outproxy available");
                l.log("Proxy list is emtpy - no outproxy available");
                return null;
            }
            int index = I2PAppContext.getGlobalContext().random().nextInt(size);
            String proxy = (String)proxyList.get(index);
            return proxy;
        }
    }

    private static final int DEFAULT_READ_TIMEOUT = 60*1000;
    
    /** 
     * create the default options (using the default timeout, etc)
     *
     */
    protected I2PSocketOptions getDefaultOptions() {
        Properties defaultOpts = getTunnel().getClientOptions();
        if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT))
            defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT);
        //if (!defaultOpts.contains("i2p.streaming.inactivityTimeout"))
        //    defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT);
        I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
        if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
            opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
        return opts;
    }
    
    /** 
     * create the default options (using the default timeout, etc)
     *
     */
    protected I2PSocketOptions getDefaultOptions(Properties overrides) {
        Properties defaultOpts = getTunnel().getClientOptions();
        defaultOpts.putAll(overrides);
        if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT))
            defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT);
        if (!defaultOpts.contains("i2p.streaming.inactivityTimeout"))
            defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT);
        I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
        if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
            opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
        return opts;
    }

    private static final boolean DEFAULT_GZIP = true;
    
    private static long __requestId = 0;
    protected void clientConnectionRun(Socket s) {
        OutputStream out = null;
        String targetRequest = null;
        boolean usingWWWProxy = false;
        String currentProxy = null;
        long requestId = ++__requestId;
        try {
            out = s.getOutputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(), "ISO-8859-1"));
            String line, method = null, protocol = null, host = null, destination = null;
            StringBuffer newRequest = new StringBuffer();
            int ahelper = 0;
            while ((line = br.readLine()) != null) {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug(getPrefix(requestId) + "Line=[" + line + "]");
                
                String lowercaseLine = line.toLowerCase();
                if (lowercaseLine.startsWith("connection: ") || 
                    lowercaseLine.startsWith("keep-alive: ") || 
                    lowercaseLine.startsWith("proxy-connection: "))
                    continue;
                
                if (method == null) { // first line (GET /base64/realaddr)
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug(getPrefix(requestId) + "Method is null for [" + line + "]");
                    
                    line = line.trim();
                    int pos = line.indexOf(" ");
                    if (pos == -1) break;
                    method = line.substring(0, pos);
                    String request = line.substring(pos + 1);
                    if (request.startsWith("/") && getTunnel().getClientOptions().getProperty("i2ptunnel.noproxy") != null) {
                        request = "http://i2p" + request;
                    } else if (request.startsWith("/eepproxy/")) {
                        // /eepproxy/foo.i2p/bar/baz.html HTTP/1.0
                        String subRequest = request.substring("/eepproxy/".length());
                        int protopos = subRequest.indexOf(" ");
                        String uri = subRequest.substring(0, protopos);
                        if (uri.indexOf("/") == -1) {
                                uri = uri + "/";
                        }
                        // "http://" + "foo.i2p/bar/baz.html" + " HTTP/1.0"
                        request = "http://" + uri + subRequest.substring(protopos);
                    }

                    pos = request.indexOf("//");
                    if (pos == -1) {
                        method = null;
                        break;
                    }
                    protocol = request.substring(0, pos + 2);
                    request = request.substring(pos + 2);

                    targetRequest = request;

                    pos = request.indexOf("/");
                    if (pos == -1) {
                        method = null;
                        break;
                    }
                    host = request.substring(0, pos);

                    // Quick hack for foo.bar.i2p
                    if (host.toLowerCase().endsWith(".i2p")) {
                        // Destination gets the host name
                        destination = host;
                        // Host becomes the destination key
                        host = getHostName(destination);

                        int pos2;
                        if ((pos2 = request.indexOf("?")) != -1) {
                            // Try to find an address helper in the fragments
                            // and split the request into it's component parts for rebuilding later
                            String ahelperKey = null;
                            boolean ahelperConflict = false;
                            
                            String fragments = request.substring(pos2 + 1);
                            String uriPath = request.substring(0, pos2);
                            pos2 = fragments.indexOf(" ");
                            String protocolVersion = fragments.substring(pos2 + 1);
                            String urlEncoding = "";
                            fragments = fragments.substring(0, pos2);
                            String initialFragments = fragments;
                            fragments = fragments + "&";
                            String fragment;
                            while(fragments.length() > 0) {
                                pos2 = fragments.indexOf("&");
                                fragment = fragments.substring(0, pos2);
                                fragments = fragments.substring(pos2 + 1);
                                
                                // Fragment looks like addresshelper key
                                if (fragment.startsWith("i2paddresshelper=")) {
                                    pos2 = fragment.indexOf("=");
                                    ahelperKey = fragment.substring(pos2 + 1);
                                    
                                    // Key contains data, lets not ignore it
                                    if (ahelperKey != null) {
                                        
                                        // Host resolvable only with addresshelper
                                        if ( (host == null) || ("i2p".equals(host)) )
                                        {
                                            // Cannot check, use addresshelper key
                                            addressHelpers.put(destination,ahelperKey);
                                        } else {
                                            // Host resolvable from database, verify addresshelper key
                                            // Silently bypass correct keys, otherwise alert
                                            if (!host.equals(ahelperKey))
                                            {
                                                // Conflict: handle when URL reconstruction done
                                                ahelperConflict = true;
                                                if (_log.shouldLog(Log.WARN))
                                                    _log.warn(getPrefix(requestId) + "Addresshelper key conflict for site [" + destination + "], trusted key [" + host + "], specified key [" + ahelperKey + "].");
                                            }
                                        }
                                    }
                                } else {
                                    // Other fragments, just pass along
                                    // Append each fragment to urlEncoding
                                    if ("".equals(urlEncoding)) {
                                        urlEncoding = "?" + fragment;
                                    } else {
                                        urlEncoding = urlEncoding + "&" + fragment; 
                                    }
                                }
                            }
                            // Reconstruct the request minus the i2paddresshelper GET var
                            request = uriPath + urlEncoding + " " + protocolVersion;
                            
                            // Did addresshelper key conflict?
                            if (ahelperConflict)
                            {
                                String str;
                                byte[] header;
                                str = FileUtil.readTextFile("docs/ahelper-conflict-header.ht", 100, true);
                                if (str != null) header = str.getBytes();
                                  else header = ERR_AHELPER_CONFLICT;

                                if (out != null) {
                                    long alias = I2PAppContext.getGlobalContext().random().nextLong();
                                    String trustedURL = protocol + uriPath + urlEncoding;
                                    String conflictURL = protocol + alias + ".i2p/?" + initialFragments;
                                    out.write(header);
                                    out.write(("To visit the destination in your host database, click <a href=\"" + trustedURL + "\">here</a>. To visit the conflicting addresshelper link by temporarily giving it a random alias, click <a href=\"" + conflictURL + "\">here</a>.<P/>").getBytes());
                                    out.write("</div><p><i>I2P HTTP Proxy Server<br>Generated on: ".getBytes());
                                    out.write(new Date().toString().getBytes());
                                    out.write("</i></body></html>\n".getBytes());
                                    out.flush();
                                }
                                s.close();
                                return;
                            }
                        }

                        String addressHelper = (String) addressHelpers.get(destination);
                        if (addressHelper != null) {
                            destination = addressHelper;
                            host = getHostName(destination);
                            ahelper = 1;
                        }
                        
                        line = method + " " + request.substring(pos);
                    } else if (host.indexOf(".") != -1) {
                        // The request must be forwarded to a WWW proxy
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Before selecting outproxy for " + host);
                        currentProxy = selectProxy();
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("After selecting outproxy for " + host + ": " + currentProxy);
                        if (currentProxy == null) {
                            if (_log.shouldLog(Log.WARN))
                                _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!");
                            l.log("No HTTP outproxy found for the request.");
                            if (out != null) {
                                out.write(ERR_NO_OUTPROXY);
                                out.write("<p /><i>Generated on: ".getBytes());
                                out.write(new Date().toString().getBytes());
                                out.write("</i></body></html>\n".getBytes());
                                out.flush();
                            }
                            s.close();
                            return;
                        }
                        destination = currentProxy;
                        usingWWWProxy = true;
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug(getPrefix(requestId) + "Host doesnt end with .i2p and it contains a period [" + host + "]: wwwProxy!");
                    } else {
                        request = request.substring(pos + 1);
                        pos = request.indexOf("/");
                        if (pos < 0) {
                            l.log("Invalid request url [" + request + "]");
                            if (out != null) {
                                out.write(ERR_REQUEST_DENIED);
                                out.write("<p /><i>Generated on: ".getBytes());
                                out.write(new Date().toString().getBytes());
                                out.write("</i></body></html>\n".getBytes());
                                out.flush();
                            }
                            s.close();
                            return;
                        }
                        destination = request.substring(0, pos);
                        line = method + " " + request.substring(pos);
                    }

                    boolean isValid = usingWWWProxy || isSupportedAddress(host, protocol);
                    if (!isValid) {
                        if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "notValid(" + host + ")");
                        method = null;
                        destination = null;
                        break;
                    } else if (!usingWWWProxy) {
                        if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "host=getHostName(" + destination + ")");
                        host = getHostName(destination); // hide original host
                    }

                    if (_log.shouldLog(Log.DEBUG)) {
                        _log.debug(getPrefix(requestId) + "METHOD:" + method + ":");
                        _log.debug(getPrefix(requestId) + "PROTOC:" + protocol + ":");
                        _log.debug(getPrefix(requestId) + "HOST  :" + host + ":");
                        _log.debug(getPrefix(requestId) + "DEST  :" + destination + ":");
                    }
                    
                } else {
                    if (lowercaseLine.startsWith("host: ") && !usingWWWProxy) {
                        line = "Host: " + host;
                        if (_log.shouldLog(Log.INFO)) 
                            _log.info(getPrefix(requestId) + "Setting host = " + host);
                    } else if (lowercaseLine.startsWith("user-agent: ")) {
                        // always stripped, added back at the end
                        line = null;
                        continue;
                    } else if (lowercaseLine.startsWith("accept")) {
                        // strip the accept-blah headers, as they vary dramatically from
                        // browser to browser
                        line = null;
                        continue;
                    } else if (lowercaseLine.startsWith("referer: ")) {
                        // Shouldn't we be more specific, like accepting in-site referers ?
                        //line = "Referer: i2p";
                        line = null;
                        continue; // completely strip the line
                    } else if (lowercaseLine.startsWith("via: ")) {
                        //line = "Via: i2p";
                        line = null;
                        continue; // completely strip the line
                    } else if (lowercaseLine.startsWith("from: ")) {
                        //line = "From: i2p";
                        line = null;
                        continue; // completely strip the line
                    }
                }
                
                if (line.length() == 0) {
                    
                    String ok = getTunnel().getContext().getProperty("i2ptunnel.gzip");
                    boolean gzip = DEFAULT_GZIP;
                    if (ok != null)
                        gzip = Boolean.valueOf(ok).booleanValue();
                    if (gzip) {
                        // according to rfc2616 s14.3, this *should* force identity, even if
                        // an explicit q=0 for gzip doesn't.  tested against orion.i2p, and it
                        // seems to work.
                        newRequest.append("Accept-Encoding: \r\n");
                        newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n");
                    }
                    newRequest.append("User-Agent: MYOB/6.66 (AN/ON)\r\n");
                    newRequest.append("Connection: close\r\n\r\n");
                    break;
                } else {
                    newRequest.append(line.trim()).append("\r\n"); // HTTP spec
                }
            }
            if (_log.shouldLog(Log.DEBUG))
                _log.debug(getPrefix(requestId) + "NewRequest header: [" + newRequest.toString() + "]");
            
            while (br.ready()) { // empty the buffer (POST requests)
                int i = br.read();
                if (i != -1) {
                    newRequest.append((char) i);
                }
            }
            if (method == null || destination == null) {
                l.log("No HTTP method found in the request.");
                if (out != null) {
                    if ("http://".equalsIgnoreCase(protocol))
                        out.write(ERR_REQUEST_DENIED);
                    else
                        out.write(ERR_BAD_PROTOCOL);
                    out.write("<p /><i>Generated on: ".getBytes());
                    out.write(new Date().toString().getBytes());
                    out.write("</i></body></html>\n".getBytes());
                    out.flush();
                }
                s.close();
                return;
            }
            
            if (_log.shouldLog(Log.DEBUG))
                _log.debug(getPrefix(requestId) + "Destination: " + destination);
            
            Destination dest = I2PTunnel.destFromName(destination);
            if (dest == null) {
                l.log("Could not resolve " + destination + ".");
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Unable to resolve " + destination + " (proxy? " + usingWWWProxy + ", request: " + targetRequest);
                String str;
                byte[] header;
                boolean showAddrHelper = false;
                if (usingWWWProxy)
                    str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
                else if(ahelper != 0)
                    str = FileUtil.readTextFile("docs/dnfb-header.ht", 100, true);
                else {
                    str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true);
                    showAddrHelper = true;
                }
                if (str != null)
                    header = str.getBytes();
                else
                    header = ERR_DESTINATION_UNKNOWN;
                writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination, showAddrHelper);
                s.close();
                return;
            }
            String remoteID;
            
            Properties opts = new Properties();
            //opts.setProperty("i2p.streaming.inactivityTimeout", ""+120*1000);
            // 1 == disconnect.  see ConnectionOptions in the new streaming lib, which i
            // dont want to hard link to here
            //opts.setProperty("i2p.streaming.inactivityTimeoutAction", ""+1);
            I2PSocket i2ps = createI2PSocket(dest, getDefaultOptions(opts));
            byte[] data = newRequest.toString().getBytes("ISO-8859-1");
            Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
            I2PTunnelRunner runner = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
        } catch (SocketException ex) {
            _log.info(getPrefix(requestId) + "Error trying to connect", ex);
            l.log(ex.getMessage());
            handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
            closeSocket(s);
        } catch (IOException ex) {
            _log.info(getPrefix(requestId) + "Error trying to connect", ex);
            l.log(ex.getMessage());
            handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
            closeSocket(s);
        } catch (I2PException ex) {
            _log.info("getPrefix(requestId) + Error trying to connect", ex);
            l.log(ex.getMessage());
            handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
            closeSocket(s);
        }
    }

    private final static String getHostName(String host) {
        if (host == null) return null;
        try {
            Destination dest = I2PTunnel.destFromName(host);
            if (dest == null) return "i2p";
            return dest.toBase64();
        } catch (DataFormatException dfe) {
            return "i2p";
        }
    }

    private class OnTimeout implements Runnable {
        private Socket _socket;
        private OutputStream _out;
        private String _target;
        private boolean _usingProxy;
        private String _wwwProxy;
        private long _requestId;
        public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
            _socket = s;
            _out = out;
            _target = target;
            _usingProxy = usingProxy;
            _wwwProxy = wwwProxy;
            _requestId = id;
        }
        public void run() {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Timeout occured requesting " + _target);
            handleHTTPClientException(new RuntimeException("Timeout"), _out, 
                                      _target, _usingProxy, _wwwProxy, _requestId);
            closeSocket(_socket);
        }
    }
    
    private static void writeErrorMessage(byte[] errMessage, OutputStream out, String targetRequest,
                                          boolean usingWWWProxy, String wwwProxy, boolean showAddrHelper) throws IOException {
        if (out != null) {
            out.write(errMessage);
            if (targetRequest != null) {
                int protopos = targetRequest.indexOf(" ");
                String uri = null;
                if (protopos >= 0)
                    uri = targetRequest.substring(0, protopos);
                else
                    uri = targetRequest;
                out.write("<a href=\"http://".getBytes());
                out.write(uri.getBytes());
                out.write("\">http://".getBytes());
                out.write(uri.getBytes());
                out.write("</a>".getBytes());
                if (usingWWWProxy) out.write(("<br>WWW proxy: " + wwwProxy).getBytes());
                if (showAddrHelper) {
                    out.write("<br><br>Click below to try an address helper link:<br><br><a href=\"http://orion.i2p/jump/".getBytes());
                    out.write(uri.getBytes());
                    out.write("\">http://orion.i2p/jump/".getBytes());
                    out.write(uri.getBytes());
                    out.write("</a>".getBytes());
                }
            }
            out.write("</div><p><i>I2P HTTP Proxy Server<br>Generated on: ".getBytes());
            out.write(new Date().toString().getBytes());
            out.write("</i></body></html>\n".getBytes());
            out.flush();
        }
    }

    private void handleHTTPClientException(Exception ex, OutputStream out, String targetRequest,
                                                  boolean usingWWWProxy, String wwwProxy, long requestId) {
                                                      
        if (_log.shouldLog(Log.WARN))
            _log.warn(getPrefix(requestId) + "Error sending to " + wwwProxy + " (proxy? " + usingWWWProxy + ", request: " + targetRequest, ex);
        if (out != null) {
            try {
                String str;
                byte[] header;
                if (usingWWWProxy)
                    str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
                else
                    str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true);
                if (str != null)
                    header = str.getBytes();
                else
                    header = ERR_DESTINATION_UNKNOWN;
                writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy, false);
            } catch (IOException ioe) {
                _log.warn(getPrefix(requestId) + "Error writing out the 'destination was unknown' " + "message", ioe);
            }
        } else {
            _log.warn(getPrefix(requestId) + "Client disconnected before we could say that destination " + "was unknown", ex);
        }
    }

    private final static String SUPPORTED_HOSTS[] = { "i2p", "www.i2p.com", "i2p."};

    private boolean isSupportedAddress(String host, String protocol) {
        if ((host == null) || (protocol == null)) return false;
        boolean found = false;
        String lcHost = host.toLowerCase();
        for (int i = 0; i < SUPPORTED_HOSTS.length; i++) {
            if (SUPPORTED_HOSTS[i].equals(lcHost)) {
                found = true;
                break;
            }
        }

        if (!found) {
            try {
                Destination d = I2PTunnel.destFromName(host);
                if (d == null) return false;
            } catch (DataFormatException dfe) {
            }
        }

        return protocol.equalsIgnoreCase("http://");
    }
}
