1package com.android.hotspot2.osu;
2
3import android.util.Log;
4
5import com.android.hotspot2.utils.HTTPMessage;
6import com.android.hotspot2.utils.HTTPRequest;
7import com.android.hotspot2.utils.HTTPResponse;
8
9import com.android.org.conscrypt.OpenSSLSocketImpl;
10
11import org.xml.sax.SAXException;
12
13import java.io.BufferedInputStream;
14import java.io.BufferedOutputStream;
15import java.io.IOException;
16import java.io.InputStream;
17import java.net.Socket;
18import java.net.URL;
19import java.nio.ByteBuffer;
20import java.nio.charset.Charset;
21import java.nio.charset.StandardCharsets;
22import java.security.GeneralSecurityException;
23import java.security.PrivateKey;
24import java.security.cert.X509Certificate;
25import java.util.List;
26import java.util.Map;
27import java.util.concurrent.atomic.AtomicInteger;
28
29import javax.net.ssl.SSLException;
30import javax.net.ssl.SSLSocket;
31import javax.xml.parsers.ParserConfigurationException;
32
33public class HTTPHandler implements AutoCloseable {
34    private final Charset mCharset;
35    private final OSUSocketFactory mSocketFactory;
36    private Socket mSocket;
37    private BufferedOutputStream mOut;
38    private BufferedInputStream mIn;
39    private final String mUser;
40    private final byte[] mPassword;
41    private boolean mHTTPAuthPerformed;
42    private static final AtomicInteger sSequence = new AtomicInteger();
43
44    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory) throws IOException {
45        this(charset, socketFactory, null, null);
46    }
47
48    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory,
49                       String user, byte[] password) throws IOException {
50        mCharset = charset;
51        mSocketFactory = socketFactory;
52        mSocket = mSocketFactory.createSocket();
53        mOut = new BufferedOutputStream(mSocket.getOutputStream());
54        mIn = new BufferedInputStream(mSocket.getInputStream());
55        mUser = user;
56        mPassword = password;
57    }
58
59    public boolean isHTTPAuthPerformed() {
60        return mHTTPAuthPerformed;
61    }
62
63    public X509Certificate getOSUCertificate(URL osu) throws GeneralSecurityException {
64        return mSocketFactory.getOSUCertificate(osu);
65    }
66
67    public void renegotiate(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
68            throws IOException {
69        if (!(mSocket instanceof SSLSocket)) {
70            throw new IOException("Not a TLS connection");
71        }
72        if (certs != null) {
73            mSocketFactory.reloadKeys(certs, key);
74        }
75        ((SSLSocket) mSocket).startHandshake();
76    }
77
78    public byte[] getTLSUnique() throws SSLException {
79        if (mSocket instanceof OpenSSLSocketImpl) {
80            return ((OpenSSLSocketImpl) mSocket).getChannelId();
81        }
82        return null;
83    }
84
85    public OSUResponse exchangeSOAP(URL url, String message) throws IOException {
86        HTTPResponse response = exchangeWithRetry(url, message, HTTPMessage.Method.POST,
87                HTTPMessage.ContentTypeSOAP);
88        if (response.getStatusCode() >= 300) {
89            throw new IOException("Bad HTTP status code " + response.getStatusCode());
90        }
91        try {
92            SOAPParser parser = new SOAPParser(response.getPayloadStream());
93            return parser.getResponse();
94        } catch (ParserConfigurationException | SAXException e) {
95            ByteBuffer x = response.getPayload();
96            byte[] b = new byte[x.remaining()];
97            x.get(b);
98            Log.w("XML", "Bad: '" + new String(b, StandardCharsets.ISO_8859_1));
99            throw new IOException(e);
100        }
101    }
102
103    public ByteBuffer exchangeBinary(URL url, String message, String contentType)
104            throws IOException {
105        HTTPResponse response =
106                exchangeWithRetry(url, message, HTTPMessage.Method.POST, contentType);
107        return response.getBinaryPayload();
108    }
109
110    public InputStream doGet(URL url) throws IOException {
111        HTTPResponse response = exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
112        return response.getPayloadStream();
113    }
114
115    public HTTPResponse doGetHTTP(URL url) throws IOException {
116        return exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
117    }
118
119    private HTTPResponse exchangeWithRetry(URL url, String message, HTTPMessage.Method method,
120                                           String contentType) throws IOException {
121        HTTPResponse response = null;
122        int retry = 0;
123        for (; ; ) {
124            try {
125                response = httpExchange(url, message, method, contentType);
126                break;
127            } catch (IOException ioe) {
128                close();
129                retry++;
130                if (retry > 3) {
131                    break;
132                }
133                Log.d(OSUManager.TAG, "Failed HTTP exchange, retry " + retry);
134                mSocket = mSocketFactory.createSocket();
135                mOut = new BufferedOutputStream(mSocket.getOutputStream());
136                mIn = new BufferedInputStream(mSocket.getInputStream());
137            }
138        }
139        if (response == null) {
140            throw new IOException("Failed to establish connection to peer");
141        }
142        return response;
143    }
144
145    private HTTPResponse httpExchange(URL url, String message, HTTPMessage.Method method,
146                                      String contentType)
147            throws IOException {
148        HTTPRequest request = new HTTPRequest(message, mCharset, method, url, contentType, false);
149        request.send(mOut);
150        HTTPResponse response = new HTTPResponse(mIn);
151        Log.d(OSUManager.TAG, "HTTP code " + response.getStatusCode() + ", user " + mUser +
152                ", pw " + (mPassword != null ? '\'' + new String(mPassword) + '\'' : "-"));
153        if (response.getStatusCode() == 401) {
154            if (mUser == null) {
155                throw new IOException("Missing user name for HTTP authentication");
156            }
157            try {
158                request = new HTTPRequest(message, StandardCharsets.ISO_8859_1, method, url,
159                        contentType, true);
160                request.doAuthenticate(response, mUser, mPassword, url,
161                        sSequence.incrementAndGet());
162                request.send(mOut);
163                mHTTPAuthPerformed = true;
164            } catch (GeneralSecurityException gse) {
165                throw new IOException(gse);
166            }
167
168            response = new HTTPResponse(mIn);
169        }
170        return response;
171    }
172
173    public void close() throws IOException {
174        mSocket.shutdownInput();
175        mSocket.shutdownOutput();
176        mSocket.close();
177        mIn.close();
178        mOut.close();
179    }
180}
181