Home | History | Annotate | Download | only in utils
      1 package com.android.hotspot2.utils;
      2 
      3 import android.util.Base64;
      4 
      5 import java.io.ByteArrayInputStream;
      6 import java.io.IOException;
      7 import java.io.InputStream;
      8 import java.io.OutputStream;
      9 import java.net.URL;
     10 import java.nio.ByteBuffer;
     11 import java.nio.charset.Charset;
     12 import java.nio.charset.StandardCharsets;
     13 import java.security.GeneralSecurityException;
     14 import java.security.MessageDigest;
     15 import java.security.SecureRandom;
     16 import java.util.Collections;
     17 import java.util.HashMap;
     18 import java.util.HashSet;
     19 import java.util.LinkedHashMap;
     20 import java.util.Map;
     21 import java.util.Set;
     22 
     23 public class HTTPRequest implements HTTPMessage {
     24     private static final Charset HeaderCharset = StandardCharsets.US_ASCII;
     25     private static final int HTTPS_PORT = 443;
     26 
     27     private final String mMethodLine;
     28     private final Map<String, String> mHeaderFields;
     29     private final byte[] mBody;
     30 
     31     public HTTPRequest(Method method, URL url) {
     32         this(null, null, method, url, null, false);
     33     }
     34 
     35     public HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType,
     36                        boolean base64) {
     37         mBody = payload != null ? payload.getBytes(charset) : null;
     38 
     39         mHeaderFields = new LinkedHashMap<>();
     40         mHeaderFields.put(AgentHeader, AgentName);
     41         if (url.getPort() != HTTPS_PORT) {
     42             mHeaderFields.put(HostHeader, url.getHost() + ':' + url.getPort());
     43         } else {
     44             mHeaderFields.put(HostHeader, url.getHost());
     45         }
     46         mHeaderFields.put(AcceptHeader, "*/*");
     47         if (payload != null) {
     48             if (base64) {
     49                 mHeaderFields.put(ContentTypeHeader, contentType);
     50                 mHeaderFields.put(ContentEncodingHeader, "base64");
     51             } else {
     52                 mHeaderFields.put(ContentTypeHeader, contentType + "; charset=" +
     53                         charset.displayName().toLowerCase());
     54             }
     55             mHeaderFields.put(ContentLengthHeader, Integer.toString(mBody.length));
     56         }
     57 
     58         mMethodLine = method.name() + ' ' + url.getPath() + ' ' + HTTPVersion + CRLF;
     59     }
     60 
     61     public void doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password,
     62                                URL url, int sequence) throws IOException, GeneralSecurityException {
     63         mHeaderFields.put(HTTPMessage.AuthorizationHeader,
     64                 generateAuthAnswer(httpResponse, userName, password, url, sequence));
     65     }
     66 
     67     private static String generateAuthAnswer(HTTPResponse httpResponse, String userName,
     68                                              byte[] password, URL url, int sequence)
     69             throws IOException, GeneralSecurityException {
     70 
     71         String authRequestLine = httpResponse.getHeader(HTTPMessage.AuthHeader);
     72         if (authRequestLine == null) {
     73             throw new IOException("Missing auth line");
     74         }
     75         String[] tokens = authRequestLine.split("[ ,]+");
     76         //System.out.println("Tokens: " + Arrays.toString(tokens));
     77         if (tokens.length < 3 || !tokens[0].equalsIgnoreCase("digest")) {
     78             throw new IOException("Bad " + HTTPMessage.AuthHeader + ": '" + authRequestLine + "'");
     79         }
     80 
     81         Map<String, String> itemMap = new HashMap<>();
     82         for (int n = 1; n < tokens.length; n++) {
     83             String s = tokens[n];
     84             int split = s.indexOf('=');
     85             if (split < 0) {
     86                 continue;
     87             }
     88             itemMap.put(s.substring(0, split).trim().toLowerCase(),
     89                     unquote(s.substring(split + 1).trim()));
     90         }
     91 
     92         Set<String> qops = splitValue(itemMap.remove("qop"));
     93         if (!qops.contains("auth")) {
     94             throw new IOException("Unsupported quality of protection value(s): '" + qops + "'");
     95         }
     96         String algorithm = itemMap.remove("algorithm");
     97         if (algorithm != null && !algorithm.equalsIgnoreCase("md5")) {
     98             throw new IOException("Unsupported algorithm: '" + algorithm + "'");
     99         }
    100         String realm = itemMap.remove("realm");
    101         String nonceText = itemMap.remove("nonce");
    102         if (realm == null || nonceText == null) {
    103             throw new IOException("realm and/or nonce missing: '" + authRequestLine + "'");
    104         }
    105         //System.out.println("Remaining tokens: " + itemMap);
    106 
    107         byte[] cnonce = new byte[16];
    108         SecureRandom prng = new SecureRandom();
    109         prng.nextBytes(cnonce);
    110 
    111         /*
    112          * H(data) = MD5(data)
    113          * KD(secret, data) = H(concat(secret, ":", data))
    114          *
    115          * A1 = unq(username-value) ":" unq(realm-value) ":" passwd
    116          * A2 = Method ":" digest-uri-value
    117          *
    118          * response = KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":"
    119           * unq(qop-value) ":" H(A2) )
    120          */
    121 
    122         String nc = String.format("%08d", sequence);
    123 
    124         /*
    125          * This bears witness to the ingenuity of the emerging "web generation" and the authors of
    126          * RFC-2617: Strings are treated as a sequence of octets in blind ignorance of character
    127          * encoding, whereas octets strings apparently aren't "good enough" and expanded to
    128          * "hex strings"...
    129          * As a wild guess I apply UTF-8 below.
    130          */
    131         String passwordString = new String(password, StandardCharsets.UTF_8);
    132         String cNonceString = bytesToHex(cnonce);
    133 
    134         byte[] a1 = hash(userName, realm, passwordString);
    135         byte[] a2 = hash("POST", url.getPath());
    136         byte[] response = hash(a1, nonceText, nc, cNonceString, "auth", a2);
    137 
    138         StringBuilder authLine = new StringBuilder();
    139         authLine.append("Digest ")
    140                 .append("username=\"").append(userName).append("\", ")
    141                 .append("realm=\"").append(realm).append("\", ")
    142                 .append("nonce=\"").append(nonceText).append("\", ")
    143                 .append("uri=\"").append(url.getPath()).append("\", ")
    144                 .append("qop=\"auth\", ")
    145                 .append("nc=").append(nc).append(", ")
    146                 .append("cnonce=\"").append(cNonceString).append("\", ")
    147                 .append("response=\"").append(bytesToHex(response)).append('"');
    148         String opaque = itemMap.get("opaque");
    149         if (opaque != null) {
    150             authLine.append(", \"").append(opaque).append('"');
    151         }
    152 
    153         return authLine.toString();
    154     }
    155 
    156     private static Set<String> splitValue(String value) {
    157         Set<String> result = new HashSet<>();
    158         if (value != null) {
    159             for (String s : value.split(",")) {
    160                 result.add(s.trim());
    161             }
    162         }
    163         return result;
    164     }
    165 
    166     private static byte[] hash(Object... objects) throws GeneralSecurityException {
    167         MessageDigest hash = MessageDigest.getInstance("MD5");
    168 
    169         //System.out.println("<Hash>");
    170         boolean first = true;
    171         for (Object object : objects) {
    172             byte[] octets;
    173             if (object.getClass() == String.class) {
    174                 //System.out.println("+= '" + object + "'");
    175                 octets = ((String) object).getBytes(StandardCharsets.UTF_8);
    176             } else {
    177                 octets = bytesToHexBytes((byte[]) object);
    178                 //System.out.println("+= " + new String(octets, StandardCharsets.ISO_8859_1));
    179             }
    180             if (first) {
    181                 first = false;
    182             } else {
    183                 hash.update((byte) ':');
    184             }
    185             hash.update(octets);
    186         }
    187         //System.out.println("</Hash>");
    188         return hash.digest();
    189     }
    190 
    191     private static String unquote(String s) {
    192         return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s;
    193     }
    194 
    195     private static byte[] bytesToHexBytes(byte[] octets) {
    196         return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1);
    197     }
    198 
    199     private static String bytesToHex(byte[] octets) {
    200         StringBuilder sb = new StringBuilder(octets.length * 2);
    201         for (byte b : octets) {
    202             sb.append(String.format("%02x", b & 0xff));
    203         }
    204         return sb.toString();
    205     }
    206 
    207     private byte[] buildHeader() {
    208         StringBuilder header = new StringBuilder();
    209         header.append(mMethodLine);
    210         for (Map.Entry<String, String> entry : mHeaderFields.entrySet()) {
    211             header.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
    212         }
    213         header.append(CRLF);
    214 
    215         //System.out.println("HTTP Request:");
    216         StringBuilder sb2 = new StringBuilder();
    217         sb2.append(header);
    218         if (mBody != null) {
    219             sb2.append(new String(mBody, StandardCharsets.ISO_8859_1));
    220         }
    221         //System.out.println(sb2);
    222         //System.out.println("End HTTP Request.");
    223 
    224         return header.toString().getBytes(HeaderCharset);
    225     }
    226 
    227     public void send(OutputStream out) throws IOException {
    228         out.write(buildHeader());
    229         if (mBody != null) {
    230             out.write(mBody);
    231         }
    232         out.flush();
    233     }
    234 
    235     @Override
    236     public Map<String, String> getHeaders() {
    237         return Collections.unmodifiableMap(mHeaderFields);
    238     }
    239 
    240     @Override
    241     public InputStream getPayloadStream() {
    242         return mBody != null ? new ByteArrayInputStream(mBody) : null;
    243     }
    244 
    245     @Override
    246     public ByteBuffer getPayload() {
    247         return mBody != null ? ByteBuffer.wrap(mBody) : null;
    248     }
    249 
    250     @Override
    251     public ByteBuffer getBinaryPayload() {
    252         byte[] binary = Base64.decode(mBody, Base64.DEFAULT);
    253         return ByteBuffer.wrap(binary);
    254     }
    255 
    256     public static void main(String[] args) throws GeneralSecurityException {
    257         test("Mufasa", "testrealm (at) host.com", "Circle Of Life", "GET", "/dir/index.html",
    258                 "dcd98b7102dd2f0e8b11d0f600bfb0c093", "0a4f113b", "00000001", "auth",
    259                 "6629fae49393a05397450978507c4ef1");
    260 
    261         // WWW-Authenticate: Digest realm="wi-fi.org", qop="auth",
    262         // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
    263         // Authorization: Digest
    264         //  username="1c7e1582-604d-4c00-b411-bb73735cbcb0"
    265         //  realm="wi-fi.org"
    266         //  nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
    267         //  uri="/.well-known/est/simpleenroll"
    268         //  cnonce="NzA3NDk0"
    269         //  nc=00000001
    270         //  qop="auth"
    271         //  response="2c485d24076452e712b77f4e70776463"
    272 
    273         String nonce = "MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==";
    274         String cnonce = "NzA3NDk0";
    275         test("1c7e1582-604d-4c00-b411-bb73735cbcb0", "wi-fi.org", "ruckus1234", "POST",
    276                 "/.well-known/est/simpleenroll",
    277                 /*new String(Base64.getDecoder().decode(nonce), StandardCharsets.ISO_8859_1)*/
    278                 nonce,
    279                 /*new String(Base64.getDecoder().decode(cnonce), StandardCharsets.ISO_8859_1)*/
    280                 cnonce, "00000001", "auth", "2c485d24076452e712b77f4e70776463");
    281     }
    282 
    283     private static void test(String user, String realm, String password, String method, String path,
    284                              String nonce, String cnonce, String nc, String qop, String expect)
    285             throws GeneralSecurityException {
    286         byte[] a1 = hash(user, realm, password);
    287         System.out.println("HA1: " + bytesToHex(a1));
    288         byte[] a2 = hash(method, path);
    289         System.out.println("HA2: " + bytesToHex(a2));
    290         byte[] response = hash(a1, nonce, nc, cnonce, qop, a2);
    291 
    292         StringBuilder authLine = new StringBuilder();
    293         String responseString = bytesToHex(response);
    294         authLine.append("Digest ")
    295                 .append("username=\"").append(user).append("\", ")
    296                 .append("realm=\"").append(realm).append("\", ")
    297                 .append("nonce=\"").append(nonce).append("\", ")
    298                 .append("uri=\"").append(path).append("\", ")
    299                 .append("qop=\"").append(qop).append("\", ")
    300                 .append("nc=").append(nc).append(", ")
    301                 .append("cnonce=\"").append(cnonce).append("\", ")
    302                 .append("response=\"").append(responseString).append('"');
    303 
    304         System.out.println(authLine);
    305         System.out.println("Success: " + responseString.equals(expect));
    306     }
    307 }