/*
 * @(#)HttpClient.java   1.36 95/03/20 Jonathan Payne
 *
 * Copyright (c) 1994 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Permission to use, copy, modify, and distribute this software
 * and its documentation for NON-COMMERCIAL purposes and without
 * fee is hereby granted provided that this copyright notice
 * appears in all copies. Please refer to the file "copyright.html"
 * for further important copyright and licensing information.
 *
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
 * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 */

package net.www.http;

import java.io.*;
import net.*;
import java.util.*;
import net.www.html.URL;
import net.Firewall;
import net.www.html.MessageHeader;

/**
 * @version 1.36 20 Mar 1995
 * @author Jonathan Payne
 */
public final class HttpClient extends NetworkClient {
    static final boolean    debug = false;

    static final String  userAgentString = "User-Agent: HotJava/1.0alpha2\r\n";
    static final String acceptString
     = "Accept: text/html; image/gif; *, q=.2; */*, q=.2\r\n";

    /** Default port number for http daemons - this is not
        a registered service as far as I can tell. */
    static final int          httpPortNumber = 80;

    public static boolean     useProxyForFirewall = false;
    public static String      firewallProxyHost = "wwwproxy";
    public static int         firewallProxyPort = 80;

    public static boolean     useProxyForCaching = false;
    public static String      cachingProxyHost = "wwwproxy";
    public static int         cachingProxyPort = 80;

    /* instance-specific proxy fields override the static fields if */
    /* set. */
    private String            proxy = null;
    private int               proxyPort = -1;

    /* request essentially succeeded */
    public static final int   OK = 200;
    public static final int   CREATED = 201;
    public static final int   ACCEPTED = 202;
    public static final int   PARTIAL = 203;

    /* document redirection */
    public static final int   MOVED = 301;
    public static final int   FOUND = 302;
    public static final int   METHOD = 303;

    /* errors */
    public static final int   BAD = 400;
    public static final int   UNAUTHORIZED = 401;
    public static final int   PAYMENT_REQUIRED = 402;
    public static final int   FORBIDDEN = 403;
    public static final int   NOT_FOUND = 404;
    public static final int   INTERNAL_ERROR = 500;
    public static final int   NOT_IMPLEMENTED = 501;

    int             status;
    MessageHeader   mimeHeader = null;
    boolean         usingProxy = false;
    String          host;
    String          authentication = null;

    /** Url being fetched. */
    URL             url;

    public synchronized void openServer(String host, int port) {
     this.host = host;
     String   s;

     if (debug) {
         System.out.println("openServer " + host + ":" + port);
     }
     if ((s = Firewall.verifyAccess(host, port)) != null) {
         String msg = "Applet at " + s + " attempted illegal URL access: ";

         msg = msg + url.toExternalForm();
         Firewall.securityError(msg);
         return;
     }

     if (url.protocol.equals("http")) {
         if (useProxyForCaching) {
          /* This would only fail if the specified host is
             unknown to the proxy, e.g., a local host was
             specified that isn't exported to the net which
             the proxy is on.  In that case, we keep trying
             just in case the host is known locally. */
          super.openServer(cachingProxyHost, cachingProxyPort);
          usingProxy = true;
          return;
         }
         try {
          super.openServer(host, port);
         } catch (UnknownHostException e) {
          if (useProxyForFirewall) {
              if (proxy == null) {
               super.openServer(firewallProxyHost,firewallProxyPort);
              } else {
               super.openServer(proxy, proxyPort);
              }
              usingProxy = true;
          } else {
              throw e;
          }
         }
     } else {
         // we're opening some other kind of url, most likely an
         // ftp url.
         if (proxy == null) {
          super.openServer(firewallProxyHost,firewallProxyPort);
         } else {
          super.openServer(proxy, proxyPort);
         }
         usingProxy = true;
     }
    }

    /** Parse the first line of the HTTP request.  It usually looks
     something like: "HTTP/1.0 <number> comment\r\n". */

    protected void getRequestStatus(String response, String name) {
     if (debug) {
         System.out.println(response);
     }
     if (response.startsWith("HTTP/1.0 ")) {
         status = Integer.parseInt(response.substring(9, 12));
     } else {
         status = -1;
     }
     switch (status) {
     case UNAUTHORIZED:
         throw new UnauthorizedHttpRequestException(url, this);

     case BAD:
     case PAYMENT_REQUIRED:
     case INTERNAL_ERROR:
     case NOT_IMPLEMENTED:
         throw new Exception("HTTP request failed: " + response);

     case FORBIDDEN:
     case NOT_FOUND:
         //System.out.println("Throwing file not found");
         throw new FileNotFoundException(response + " -- " + name);
     }
    }

    protected String getURLFile(URL u) {
     if (usingProxy) {
         String result = u.protocol + "://" + u.host;
         if (u.port != -1) {
          result += ":" + u.port;
         }
         return result + u.file;
     } else {
         return u.file;
     }
    }

    protected void post(URL u) {
     int port = u.port == -1 ? httpPortNumber : u.port;

     openServer(u.host, port);

     String cmd = "POST " + getURLFile(u) + " HTTP/1.0\r\n";
     cmd += userAgentString;
     cmd += "Referer: " + u.fromUrl.toExternalForm() + "\r\n";
     cmd += acceptString;
     if (authentication != null) {
         cmd += getAuthentication();
     }
     cmd += "Content-type: application/x-www-form-urlencoded\r\n";
     cmd += "Content-length: " + u.postData.length() + "\r\n\r\n";
     serverOutput.print(cmd);
     serverOutput.print(u.postData);
     serverOutput.print("\r\n");
     if (debug) {
         System.out.println(cmd + u.postData);
     }

     processRequest(u.file);
    }

    protected void get(URL url) {
     openServer(url.host, url.port == -1 ? httpPortNumber : url.port);

     String    cmd = "GET " + getURLFile(url)
         + " HTTP/1.0\r\n" + userAgentString
         + getAuthentication()
         + acceptString
         + "\r\n";
     serverOutput.print(cmd);
     serverOutput.flush();
     if (debug) {
         System.out.println(cmd);
     }
     processRequest(url.file);
    }

    protected void processRequest(String name) {
     /*
      * Read until 200 bytes or EOF, or until we hit a blank
      * line, whichever occurs first, looking for the string
      * "HTTP/1.0" at the beginning of the line.  This is not
      * strictly speaking necessary from the spec., but it is
      * necessary since Netscape does it and therefore people
      * have written their pages this way, broken as it is.  An
      * example is mumble:80 which sometimes spews the line
      * "Trying..." before returning a valid HTTP reply.
      *
      * In any case we always retain the first line of the
      * response because it is useful if the server turns out to
      * be a pre-HTTP1.0 server.
      */

     BufferedInputStream bis = (BufferedInputStream) serverInput;
     String status_response = null;
     String firstLine = null;

     bis.mark(200);
     DataInputStream dis = new DataInputStream(new TelnetInputStream(serverInput, false));

     while (true) {
         String  line = dis.readLine();

         if (debug) {
          System.err.println(line);
         }

         if (firstLine == null)
          firstLine = new String(line);
         if (line == null || line.length() == 0) /* end of header */
          break;
         if (line.startsWith("HTTP/1.0")) {
          status_response = new String(line);
          if (!firstLine.startsWith("HTTP/1.0")) {
              System.err.println("Warning: The first line from "+url.toExternalForm()+" was not HTTP/1.0 compliant.");
              System.err.println("         \""+firstLine+"\"");
          }
          break;
         }
     }

     bis.reset();

     try {
         if (status_response != null) {
          byte buf[] = new byte[10];
          int count = 0;
          int c;

          /* Even if the request status indicates error, we parse
             the mime header in case there is something in the mime
             header we need in order to recover from the error. E.g.,
             authentication. */
          try {
              getRequestStatus(status_response, name);
          } finally {
              mimeHeader = new MessageHeader(serverInput);
          }
         } else {
          String    line = firstLine;

          if (line == null || line.length() == 0) {
              throw new SocketException("Unexpected EOF");
          } else if (line.indexOf("hostname unknown") != -1) {
              throw new UnknownHostException(host);
          } else if (line.indexOf("refused") != -1
                  || line.startsWith("</BODY>")) {
              /* REMIND: </BODY> seems to be what the proxy on
                 sunweb.ebay spits back for connection refused,
                 but I am just hoping that *some* proxy server
                 indicates "connection refused" with the string
                 "refused" some place in the error message. */
              throw new SocketException("Connection refused");
          }
         }
     } catch (UnauthorizedHttpRequestException e) {
         /* don't close server, because we might let it
            fall through to get whatever message is behind
            the authentication, assuming the authentication
            fails */
         throw e;
     } catch (Exception e) {
         String cl = getHeaderField("content-length");
         int len = 0;
         if (cl == null || (len = Integer.parseInt(cl)) == 0) {
          closeServer();
          throw e;
         }
     }
    }

    public String getHeaderField(String fieldName) {
     return mimeHeader != null
         ? mimeHeader.findValue(fieldName)
         : null;
    }

    public int getStatus() {
     return status;
    }

    public InputStream getInputStream() {
     return serverInput;
    }

    public void setAuthentication(String au) {
     authentication = au;
    }

    public String getAuthentication() {
     if (authentication  == null) {
         return "";
     } else {
         String  auth = "Authorization: " + authentication + "\r\n";
         return auth;
     }
    }

    public String getAuthenticationScheme() {
     return getAuthenticationField(1);
    }

    public String getAuthenticationRealm() {
     return getAuthenticationField(3);
    }

    public String getAuthenticationField(int which) {
     String    field = getHeaderField("www-authenticate");
     if (field == null) {
         return null;
     }
     StringTokenizer t = new StringTokenizer(field, " =\"");
     while (--which > 0) {
         t.nextToken();
     }
     if (t.hasMoreTokens()) {
         return t.nextToken();
     }
     return null;
    }

    protected void handleUrl(URL url) {
     this.url = url;

     AuthenticationInfo  info = null;

     while (true) {
         try {
          if (url.isPostURL()) {
              post(url);
          } else {
              get(url);
          }
          break;
         } catch (UnauthorizedHttpRequestException e) {
          if (info != null) {
              AuthenticationInfo.uncacheInfo(info);
              throw e;
          }
          info = AuthenticationInfo.getAuth(url,
                                getAuthenticationRealm());
          if (info == null) {
              throw e;
          }
          setAuthentication(info.auth);
         }
     }
    }

    public HttpClient(URL url, String auth, String proxy, int  proxyPort) {
     this.proxy = proxy;
     this.proxyPort = proxyPort;
     setAuthentication(auth);
     handleUrl(url);
    }

    public HttpClient(URL url, String proxy, int  proxyPort) {
     this.proxy = proxy;
     this.proxyPort = proxyPort;
     handleUrl(url);
    }

    public HttpClient(URL url, String auth) {
     this(url, auth, null, -1);
    }

    public HttpClient(URL url) {
     this(url, null, -1);
    }
}
