Changeset d01aae78


Ignore:
Timestamp:
Oct 15, 2012 3:37:13 PM (9 years ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
9b6d5da
Parents:
50cb4273
Message:

HTTP Proxy:

  • Move error page methods to base
  • Preliminary code for digest auth
Location:
apps/i2ptunnel/java/src/net/i2p/i2ptunnel
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java

    r50cb4273 rd01aae78  
    1414import java.util.StringTokenizer;
    1515
     16import net.i2p.I2PAppContext;
    1617import net.i2p.I2PException;
    1718import net.i2p.client.streaming.I2PSocket;
     
    2122import net.i2p.data.Destination;
    2223import net.i2p.util.EventDispatcher;
    23 import net.i2p.util.FileUtil;
    2424import net.i2p.util.Log;
    2525import net.i2p.util.PortMapper;
     
    5959public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
    6060
     61    private static final String AUTH_REALM = "I2P SSL Proxy";
     62
    6163    private final static byte[] ERR_DESTINATION_UNKNOWN =
    6264        ("HTTP/1.1 503 Service Unavailable\r\n"+
     
    9597         "Cache-control: no-cache\r\n"+
    9698         "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n" +         // try to get a UTF-8-encoded response back for the password
    97          "Proxy-Authenticate: Basic realm=\"I2P SSL Proxy\"\r\n" +
     99         "Proxy-Authenticate: Basic realm=\"" + AUTH_REALM + "\"\r\n" +
    98100         "\r\n"+
    99101         "<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>"+
     
    164166            _context.portMapper().unregister(PortMapper.SVC_HTTPS_PROXY);
    165167        return super.close(forced);
     168    }
     169
     170    /** @since 0.9.4 */
     171    protected String getRealm() {
     172        return AUTH_REALM;
    166173    }
    167174
     
    238245                        _log.debug(getPrefix(requestId) + "DEST  :" + destination + ":");
    239246                    }
    240                 } else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: basic ")) {
     247                } else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: ")) {
    241248                    // strip Proxy-Authenticate from the response in HTTPResponseOutputStream
    242249                    // save for auth check below
    243                     authorization = line.substring(27);  // "proxy-authorization: basic ".length()
     250                    authorization = line.substring(21);  // "proxy-authorization: ".length()
    244251                    line = null;
    245252                } else if (line.length() > 0) {
     
    296303            Destination clientDest = _context.namingService().lookup(destination);
    297304            if (clientDest == null) {
    298                 String str;
    299305                byte[] header;
    300306                if (usingWWWProxy)
    301                     str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
     307                    header = getErrorPage("dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
    302308                else
    303                     str = FileUtil.readTextFile((new File(_errorDir, "dnfh-header.ht")).getAbsolutePath(), 100, true);
    304                 if (str != null)
    305                     header = str.getBytes();
    306                 else
    307                     header = ERR_DESTINATION_UNKNOWN;
     309                    header = getErrorPage("dnfh-header.ht", ERR_DESTINATION_UNKNOWN);
    308310                writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination);
    309311                s.close();
     
    342344
    343345    private static class OnTimeout implements Runnable {
    344         private Socket _socket;
    345         private OutputStream _out;
    346         private String _target;
    347         private boolean _usingProxy;
    348         private String _wwwProxy;
    349         private long _requestId;
     346        private final Socket _socket;
     347        private final OutputStream _out;
     348        private final String _target;
     349        private final boolean _usingProxy;
     350        private final String _wwwProxy;
     351        private final long _requestId;
     352
    350353        public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
    351354            _socket = s;
     
    356359            _requestId = id;
    357360        }
     361
    358362        public void run() {
    359363            //if (_log.shouldLog(Log.DEBUG))
     
    392396        if (out == null)
    393397            return;
     398        byte[] header;
     399        if (usingWWWProxy)
     400            header = getErrorPage(I2PAppContext.getGlobalContext(), "dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
     401        else
     402            header = getErrorPage(I2PAppContext.getGlobalContext(), "dnf-header.ht", ERR_DESTINATION_UNKNOWN);
    394403        try {
    395             String str;
    396             byte[] header;
    397             if (usingWWWProxy)
    398                 str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
    399             else
    400                 str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
    401             if (str != null)
    402                 header = str.getBytes();
    403             else
    404                 header = ERR_DESTINATION_UNKNOWN;
    405404            writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy);
    406405        } catch (IOException ioe) {}
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java

    r50cb4273 rd01aae78  
    44package net.i2p.i2ptunnel;
    55
    6 import java.io.ByteArrayOutputStream;
    76import java.io.File;
    8 import java.io.FileInputStream;
    97import java.io.IOException;
    108import java.io.InputStream;
     
    7472     */
    7573    private final ConcurrentHashMap<String, String> addressHelpers = new ConcurrentHashMap(8);
     74
    7675    /**
    7776     *  Used to protect actions via http://proxy.i2p/
    7877     */
    7978    private final String _proxyNonce;
     79
     80    private static final String AUTH_REALM = "I2P HTTP Proxy";
     81
    8082    /**
    8183     *  These are backups if the xxx.ht error page is missing.
     
    168170            "<html><body><H1>I2P ERROR: REQUEST DENIED</H1>" +
    169171            "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>").getBytes();
     172
    170173    private final static byte[] ERR_AUTH =
    171174                                ("HTTP/1.1 407 Proxy Authentication Required\r\n" +
     
    173176            "Cache-control: no-cache\r\n" +
    174177            "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n" + // try to get a UTF-8-encoded response back for the password
    175             "Proxy-Authenticate: Basic realm=\"I2P HTTP Proxy\"\r\n" +
     178            "Proxy-Authenticate: Basic realm=\"" + AUTH_REALM + "\"\r\n" +
    176179            "\r\n" +
    177180            "<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>" +
     
    301304        return rv;
    302305    }
     306
     307    /** @since 0.9.4 */
     308    protected String getRealm() {
     309        return AUTH_REALM;
     310    }
     311
    303312    private static final String HELPER_PARAM = "i2paddresshelper";
    304313    public static final String LOCAL_SERVER = "proxy.i2p";
     
    770779                        // Response to far-end shouldn't happen, as we
    771780                        // strip Proxy-Authenticate from the response in HTTPResponseOutputStream
    772                         if(lowercaseLine.startsWith("proxy-authorization: basic ")) // save for auth check below
    773                         {
    774                             authorization = line.substring(27);  // "proxy-authorization: basic ".length()
    775                         }
     781                        authorization = line.substring(21);  // "proxy-authorization: ".length()
    776782                        line = null;
    777783                        continue;
     
    859865                    }
    860866                }
    861                 out.write(getErrorPage("auth", ERR_AUTH));
     867                if (isDigestAuthRequired()) {
     868                    // weep
     869                } else {
     870                    out.write(getErrorPage("auth", ERR_AUTH));
     871                }
    862872                writeFooter(out);
    863873                s.close();
     
    10971107
    10981108    /**
    1099      *  foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
    1100      *  or the backup byte array on fail.
    1101      *
    1102      *  .ht files must be UTF-8 encoded and use \r\n terminators so the
    1103      *  HTTP headers are conformant.
    1104      *  We can't use FileUtil.readFile() because it strips \r
    1105      *
    1106      *  @return non-null
    1107      */
    1108     private byte[] getErrorPage(String base, byte[] backup) {
    1109         return getErrorPage(_context, base, backup);
    1110     }
    1111 
    1112     private static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
    1113         File errorDir = new File(ctx.getBaseDir(), "docs");
    1114         String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
    1115         if(lang != null && lang.length() > 0 && !lang.equals("en")) {
    1116             File file = new File(errorDir, base + "-header_" + lang + ".ht");
    1117             try {
    1118                 return readFile(file);
    1119             } catch(IOException ioe) {
    1120                 // try the english version now
    1121             }
    1122         }
    1123         File file = new File(errorDir, base + "-header.ht");
    1124         try {
    1125             return readFile(file);
    1126         } catch(IOException ioe) {
    1127             return backup;
    1128         }
    1129     }
    1130 
    1131     private static byte[] readFile(File file) throws IOException {
    1132         FileInputStream fis = null;
    1133         byte[] buf = new byte[512];
    1134         ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
    1135         try {
    1136             int len = 0;
    1137             fis = new FileInputStream(file);
    1138             while((len = fis.read(buf)) > 0) {
    1139                 baos.write(buf, 0, len);
    1140             }
    1141             return baos.toByteArray();
    1142         } finally {
    1143             try {
    1144                 if(fis != null) {
    1145                     fis.close();
    1146                 }
    1147             } catch(IOException foo) {
    1148             }
    1149         }
    1150     // we won't ever get here
    1151     }
    1152 
    1153     /**
    11541109     *  Public only for LocalHTTPServer, not for general use
    11551110     */
     
    11641119    private static class OnTimeout implements Runnable {
    11651120
    1166         private Socket _socket;
    1167         private OutputStream _out;
    1168         private String _target;
    1169         private boolean _usingProxy;
    1170         private String _wwwProxy;
    1171         private long _requestId;
     1121        private final Socket _socket;
     1122        private final OutputStream _out;
     1123        private final String _target;
     1124        private final boolean _usingProxy;
     1125        private final String _wwwProxy;
     1126        private final long _requestId;
    11721127
    11731128        public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java

    r50cb4273 rd01aae78  
    44package net.i2p.i2ptunnel;
    55
     6import java.io.ByteArrayOutputStream;
     7import java.io.FileInputStream;
     8import java.io.IOException;
    69import java.io.UnsupportedEncodingException;
    710import java.net.Socket;
     
    1417import net.i2p.client.streaming.I2PSocketManager;
    1518import net.i2p.data.Base64;
     19import net.i2p.data.DataHelper;
    1620import net.i2p.util.EventDispatcher;
    1721import net.i2p.util.InternalSocket;
    1822import net.i2p.util.Log;
     23import net.i2p.util.PasswordManager;
    1924
    2025/**
     
    2530 */
    2631public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implements Runnable {
     32
     33    private static final int PROXYNONCE_BYTES = 8;
     34    private static final int MD5_BYTES = 16;
     35    /** 24 */
     36    private static final int NONCE_BYTES = DataHelper.DATE_LENGTH + MD5_BYTES;
     37    private static final long MAX_NONCE_AGE = 30*24*60*60*1000L;
    2738
    2839    protected final List<String> _proxyList;
     
    4152    protected static volatile long __clientId = 0;
    4253
    43     protected static final File _errorDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs");
     54    private final byte[] _proxyNonce;
    4455
    4556    protected String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; }
     
    6475        super(localPort, ownDest, l, notifyThis, handlerName, tunnel);
    6576        _proxyList = new ArrayList(4);
     77        _proxyNonce = new byte[PROXYNONCE_BYTES];
     78        _context.random().nextBytes(_proxyNonce);
    6679    }
    6780
     
    7790        super(localPort, l, sktMgr, tunnel, notifyThis, clientId);
    7891        _proxyList = new ArrayList(4);
     92        _proxyNonce = new byte[PROXYNONCE_BYTES];
     93        _context.random().nextBytes(_proxyNonce);
    7994    }
    8095
     
    92107    public static final String PROP_OUTPROXY_PW_PREFIX = PROP_OUTPROXY_PW + '.';
    93108
    94     /**
    95      *  @param authorization may be null
     109    protected abstract String getRealm();
     110
     111    /**
     112     *  @since 0.9.4
     113     */
     114    protected boolean isDigestAuthRequired() {
     115        String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
     116        if (authRequired == null)
     117            return true;
     118        return authRequired.toLowerCase(Locale.US).equals("digest");
     119    }
     120
     121    /**
     122     *  Authorization
     123     *  Ref: RFC 2617
     124     *  If the socket is an InternalSocket, no auth required.
     125     *
     126     *  @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
    96127     *  @return success
    97128     */
    98129    protected boolean authorize(Socket s, long requestId, String authorization) {
    99         // Authorization
    100         // Ref: RFC 2617
    101         // If the socket is an InternalSocket, no auth required.
    102130        String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
    103         if (Boolean.parseBoolean(authRequired) ||
    104             (authRequired != null && "basic".equals(authRequired.toLowerCase(Locale.US)))) {
    105             if (s instanceof InternalSocket) {
    106                 if (_log.shouldLog(Log.INFO))
    107                     _log.info(getPrefix(requestId) + "Internal access, no auth required");
    108                 return true;
    109             } else if (authorization != null) {
     131        if (authRequired == null)
     132            return true;
     133        authRequired = authRequired.toLowerCase(Locale.US);
     134        if (authRequired.equals("false"))
     135            return true;
     136        if (s instanceof InternalSocket) {
     137            if (_log.shouldLog(Log.INFO))
     138                _log.info(getPrefix(requestId) + "Internal access, no auth required");
     139            return true;
     140        }
     141        if (authorization == null)
     142            return false;
     143        if (_log.shouldLog(Log.INFO))
     144            _log.info(getPrefix(requestId) + "Auth: " + authorization);
     145        String authLC = authorization.toLowerCase(Locale.US);
     146        if (authRequired.equals("true") || authRequired.equals("basic")) {
     147            if (!authLC.startsWith("basic "))
     148                return false;
     149            authorization = authorization.substring(6);
     150
    110151                // hmm safeDecode(foo, true) to use standard alphabet is private in Base64
    111152                byte[] decoded = Base64.decode(authorization.replace("/", "~").replace("+", "="));
     
    149190                        _log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization);
    150191                }
     192
     193            return false;
     194        } else if (authRequired.equals("digest")) {
     195            if (!authLC.startsWith("digest "))
     196                return false;
     197            authorization = authorization.substring(7);
     198            _log.error("Digest unimplemented");
     199            return true;
     200        } else {
     201            _log.error("Unknown proxy authorization type configured: " + authRequired);
     202            return true;
     203        }
     204    }
     205
     206    /**
     207     *  The Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
     208     *  @since 0.9.4
     209     */
     210    private String getNonce() {
     211        byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
     212        byte[] n = new byte[NONCE_BYTES];
     213        long now = _context.clock().now();
     214        DataHelper.toLong(b, 0, DataHelper.DATE_LENGTH, now);
     215        System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
     216        System.arraycopy(b, 0, n, 0, DataHelper.DATE_LENGTH);
     217        byte[] md5 = PasswordManager.md5Sum(b);
     218        System.arraycopy(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES);
     219        return Base64.encode(n);
     220    }
     221
     222    enum AuthResult {AUTH_BAD, AUTH_STALE, AUTH_GOOD}
     223
     224    /**
     225     *  Verify the Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
     226     *  @since 0.9.4
     227     */
     228    private AuthResult verifyNonce(String b64) {
     229        byte[] n = Base64.decode(b64);
     230        if (n == null || n.length != NONCE_BYTES)
     231            return AuthResult.AUTH_BAD;
     232        long now = _context.clock().now();
     233        long stamp = DataHelper.fromLong(n, 0, DataHelper.DATE_LENGTH);
     234        if (now - stamp > MAX_NONCE_AGE)
     235            return AuthResult.AUTH_STALE;
     236        byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
     237        System.arraycopy(n, 0, b, 0, DataHelper.DATE_LENGTH);
     238        System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
     239        byte[] md5 = PasswordManager.md5Sum(b);
     240        if (!DataHelper.eq(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES))
     241            return AuthResult.AUTH_BAD;
     242        return AuthResult.AUTH_GOOD;
     243    }
     244
     245    protected String getDigestHeader(boolean isStale) {
     246        return
     247            "Proxy-Authenticate: Digest realm=\"" + getRealm() + "\"" +
     248            " nonce=\"" + getNonce() + "\"" +
     249            " algorithm=MD5" +
     250            " qop=\"auth\"" +
     251            (isStale ? " stale=true" : "") +
     252            "\r\n";
     253    }
     254
     255    /**
     256     *  foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
     257     *  or the backup byte array on fail.
     258     *
     259     *  .ht files must be UTF-8 encoded and use \r\n terminators so the
     260     *  HTTP headers are conformant.
     261     *  We can't use FileUtil.readFile() because it strips \r
     262     *
     263     *  @return non-null
     264     *  @since 0.9.4 moved from I2PTunnelHTTPClient
     265     */
     266    protected byte[] getErrorPage(String base, byte[] backup) {
     267        return getErrorPage(_context, base, backup);
     268    }
     269
     270    /**
     271     *  foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
     272     *  or the backup byte array on fail.
     273     *
     274     *  .ht files must be UTF-8 encoded and use \r\n terminators so the
     275     *  HTTP headers are conformant.
     276     *  We can't use FileUtil.readFile() because it strips \r
     277     *
     278     *  @return non-null
     279     *  @since 0.9.4 moved from I2PTunnelHTTPClient
     280     */
     281    protected static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
     282        File errorDir = new File(ctx.getBaseDir(), "docs");
     283        String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
     284        if(lang != null && lang.length() > 0 && !lang.equals("en")) {
     285            File file = new File(errorDir, base + "-header_" + lang + ".ht");
     286            try {
     287                return readFile(file);
     288            } catch(IOException ioe) {
     289                // try the english version now
    151290            }
    152             return false;
    153         } else {
    154             return true;
    155         }
     291        }
     292        File file = new File(errorDir, base + "-header.ht");
     293        try {
     294            return readFile(file);
     295        } catch(IOException ioe) {
     296            return backup;
     297        }
     298    }
     299
     300    /**
     301     *  @since 0.9.4 moved from I2PTunnelHTTPClient
     302     */
     303    private static byte[] readFile(File file) throws IOException {
     304        FileInputStream fis = null;
     305        byte[] buf = new byte[2048];
     306        ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
     307        try {
     308            int len = 0;
     309            fis = new FileInputStream(file);
     310            while((len = fis.read(buf)) > 0) {
     311                baos.write(buf, 0, len);
     312            }
     313            return baos.toByteArray();
     314        } finally {
     315            try {
     316                if(fis != null) {
     317                    fis.close();
     318                }
     319            } catch(IOException foo) {
     320            }
     321        }
     322        // we won't ever get here
    156323    }
    157324}
Note: See TracChangeset for help on using the changeset viewer.