1package com.android.hotspot2.utils;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.android.hotspot2.osu.OSUManager;
7
8import java.io.ByteArrayInputStream;
9import java.io.EOFException;
10import java.io.IOException;
11import java.io.InputStream;
12import java.nio.ByteBuffer;
13import java.nio.charset.Charset;
14import java.nio.charset.StandardCharsets;
15import java.util.Arrays;
16import java.util.Collections;
17import java.util.Iterator;
18import java.util.LinkedHashMap;
19import java.util.Map;
20
21public class HTTPResponse implements HTTPMessage {
22    private final int mStatusCode;
23    private final Map<String, String> mHeaders = new LinkedHashMap<>();
24    private final ByteBuffer mBody;
25
26    private static final String csIndicator = "charset=";
27
28    public HTTPResponse(InputStream in) throws IOException {
29        int expected = Integer.MAX_VALUE;
30        int offset = 0;
31        int body = -1;
32        byte[] input = new byte[RX_BUFFER];
33
34        int statusCode = -1;
35        int bodyPattern = 0;
36
37        while (offset < expected) {
38            int amount = in.read(input, offset, input.length - offset);
39            Log.d(OSUManager.TAG, String.format("Reading into %d from %d, amount %d -> %d",
40                    input.length, offset, input.length - offset, amount));
41            if (amount < 0) {
42                throw new EOFException();
43            }
44            //Log.d("ZXZ", "HTTP response: '"
45            // + new String(input, 0, offset + amount, StandardCharsets.ISO_8859_1));
46
47            if (body < 0) {
48                for (int n = offset; n < offset + amount; n++) {
49                    bodyPattern = (bodyPattern << 8) | (input[n] & 0xff);
50                    if (bodyPattern == 0x0d0a0d0a) {
51                        body = n + 1;
52                        statusCode = parseHeader(input, body, mHeaders);
53                        expected = calculateLength(body, mHeaders);
54                        if (expected > input.length) {
55                            input = Arrays.copyOf(input, expected);
56                        }
57                        break;
58                    }
59                }
60            }
61            offset += amount;
62            if (offset < expected && offset == input.length) {
63                input = Arrays.copyOf(input, input.length * 2);
64            }
65        }
66        mStatusCode = statusCode;
67        mBody = ByteBuffer.wrap(input, body, expected - body);
68    }
69
70    private static int parseHeader(byte[] input, int body, Map<String, String> headers)
71            throws IOException {
72        String headerText = new String(input, 0, body - BODY_SEPARATOR_LENGTH,
73                StandardCharsets.ISO_8859_1);
74        //System.out.println("Received header: " + headerText);
75        Iterator<String> headerLines = Arrays.asList(headerText.split(CRLF)).iterator();
76        if (!headerLines.hasNext()) {
77            throw new IOException("Bad HTTP Request");
78        }
79
80        int statusCode;
81        String line0 = headerLines.next();
82        String[] status = line0.split(" ");
83        if (status.length != 3 || !"HTTP/1.1".equals(status[0])) {
84            throw new IOException("Bad HTTP Result: " + line0);
85        }
86        try {
87            statusCode = Integer.parseInt(status[1].trim());
88        } catch (NumberFormatException nfe) {
89            throw new IOException("Bad HTTP header line: '" + line0 + "'");
90        }
91
92        while (headerLines.hasNext()) {
93            String line = headerLines.next();
94            int keyEnd = line.indexOf(':');
95            if (keyEnd < 0) {
96                throw new IOException("Bad header line: '" + line + "'");
97            }
98            String key = line.substring(0, keyEnd).trim();
99            String value = line.substring(keyEnd + 1).trim();
100            headers.put(key, value);
101        }
102        return statusCode;
103    }
104
105    private static int calculateLength(int body, Map<String, String> headers) throws IOException {
106        String contentLength = headers.get(LengthHeader);
107        if (contentLength == null) {
108            throw new IOException("No " + LengthHeader);
109        }
110        try {
111            return body + Integer.parseInt(contentLength);
112        } catch (NumberFormatException nfe) {
113            throw new IOException("Bad " + LengthHeader + ": " + contentLength);
114        }
115    }
116
117    public int getStatusCode() {
118        return mStatusCode;
119    }
120
121    @Override
122    public Map<String, String> getHeaders() {
123        return Collections.unmodifiableMap(mHeaders);
124    }
125
126    public String getHeader(String key) {
127        return mHeaders.get(key);
128    }
129
130    @Override
131    public InputStream getPayloadStream() {
132        return new ByteArrayInputStream(mBody.array(), mBody.position(),
133                mBody.limit() - mBody.position());
134    }
135
136    @Override
137    public ByteBuffer getPayload() {
138        return mBody.duplicate();
139    }
140
141    @Override
142    public ByteBuffer getBinaryPayload() {
143        byte[] data = new byte[mBody.remaining()];
144        mBody.duplicate().get(data);
145        byte[] binary = Base64.decode(data, Base64.DEFAULT);
146        return ByteBuffer.wrap(binary);
147    }
148
149    @Override
150    public String toString() {
151        StringBuilder sb = new StringBuilder();
152        sb.append("Status: ").append(mStatusCode).append(CRLF);
153        for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
154            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
155        }
156        sb.append(CRLF);
157        Charset charset;
158        try {
159            charset = Charset.forName(getCharset());
160        } catch (IllegalArgumentException iae) {
161            charset = StandardCharsets.ISO_8859_1;
162        }
163        sb.append(new String(mBody.array(), mBody.position(),
164                mBody.limit() - mBody.position(), charset));
165        return sb.toString();
166    }
167
168    public String getCharset() {
169        String contentType = mHeaders.get(ContentTypeHeader);
170        if (contentType == null) {
171            return null;
172        }
173        int csPos = contentType.indexOf(csIndicator);
174        return csPos < 0 ? null : contentType.substring(csPos + csIndicator.length()).trim();
175    }
176
177    private static boolean equals(byte[] b1, int offset, byte[] pattern) {
178        for (int n = 0; n < pattern.length; n++) {
179            if (b1[n + offset] != pattern[n]) {
180                return false;
181            }
182        }
183        return true;
184    }
185}
186