Skip to content

Commit 2ba7b69

Browse files
committed
HttpResponse parsing
1 parent 653d1ab commit 2ba7b69

5 files changed

Lines changed: 772 additions & 2 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.split.android.client.network;
22

3+
import java.security.cert.Certificate;
4+
35
public interface HttpResponse extends BaseHttpResponse {
46
String getData();
7+
8+
Certificate[] getServerCertificates();
59
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package io.split.android.client.network;
2+
3+
import androidx.annotation.NonNull;
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.ProtocolException;
10+
import java.net.URL;
11+
import java.nio.charset.StandardCharsets;
12+
import java.security.Permission;
13+
import java.security.cert.Certificate;
14+
import java.util.HashMap;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
import javax.net.ssl.HostnameVerifier;
19+
import javax.net.ssl.HttpsURLConnection;
20+
import javax.net.ssl.SSLSocketFactory;
21+
22+
/**
23+
* Adapter that wraps an HttpResponse as an HttpURLConnection.
24+
*
25+
* This class bridges our custom SSL proxy handling (which returns HttpResponse objects)
26+
* with the existing HTTP client architecture (which expects HttpURLConnection objects).
27+
*
28+
* This is not a "mock" in the testing sense - it's a legitimate adapter pattern
29+
* that enables integration between our SSL proxy system and the existing codebase.
30+
*/
31+
class HttpResponseConnectionAdapter extends HttpsURLConnection {
32+
33+
private final HttpResponse mResponse;
34+
private final URL mUrl;
35+
private boolean mConnected = false;
36+
private final Certificate[] mServerCertificates;
37+
38+
/**
39+
* Creates an adapter that wraps an HttpResponse as an HttpURLConnection with server certificates.
40+
*
41+
* @param url The URL of the request
42+
* @param response The HTTP response from the SSL proxy
43+
* @param serverCertificates The server certificates from the SSL connection
44+
*/
45+
public HttpResponseConnectionAdapter(@NonNull URL url,
46+
@NonNull HttpResponse response,
47+
Certificate[] serverCertificates) {
48+
super(url);
49+
mUrl = url;
50+
mResponse = response;
51+
mServerCertificates = serverCertificates;
52+
mConnected = true; // SSL proxy request already executed
53+
}
54+
55+
@Override
56+
public int getResponseCode() throws IOException {
57+
return mResponse.getHttpStatus();
58+
}
59+
60+
@Override
61+
public String getResponseMessage() throws IOException {
62+
// Map common HTTP status codes to messages
63+
switch (mResponse.getHttpStatus()) {
64+
case 200: return "OK";
65+
case 400: return "Bad Request";
66+
case 401: return "Unauthorized";
67+
case 403: return "Forbidden";
68+
case 404: return "Not Found";
69+
case 500: return "Internal Server Error";
70+
default: return "HTTP " + mResponse.getHttpStatus();
71+
}
72+
}
73+
74+
@Override
75+
public InputStream getInputStream() throws IOException {
76+
if (mResponse.getHttpStatus() >= 400) {
77+
throw new IOException("HTTP " + mResponse.getHttpStatus());
78+
}
79+
String data = mResponse.getData();
80+
if (data == null) {
81+
data = "";
82+
}
83+
return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
84+
}
85+
86+
@Override
87+
public InputStream getErrorStream() {
88+
if (mResponse.getHttpStatus() >= 400) {
89+
String data = mResponse.getData();
90+
if (data == null) {
91+
data = "";
92+
}
93+
return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
94+
}
95+
return null;
96+
}
97+
98+
@Override
99+
public void connect() throws IOException {
100+
// Already connected via SSL proxy
101+
mConnected = true;
102+
}
103+
104+
@Override
105+
public boolean usingProxy() {
106+
return true; // Always true for SSL proxy connections
107+
}
108+
109+
@Override
110+
public void disconnect() {
111+
mConnected = false;
112+
}
113+
114+
// Required abstract method implementations for HTTPS connection
115+
@Override
116+
public String getCipherSuite() {
117+
// Return null to indicate cipher suite information is not available
118+
// from the SSL proxy response adapter
119+
return null;
120+
}
121+
122+
@Override
123+
public Certificate[] getLocalCertificates() {
124+
// Local certificates are not available in the response adapter context
125+
return null;
126+
}
127+
128+
@Override
129+
public Certificate[] getServerCertificates() {
130+
// Return the server certificates from the SSL connection
131+
return mServerCertificates;
132+
}
133+
134+
// Minimal implementations for other required methods
135+
@Override public void setRequestMethod(String method) throws ProtocolException {}
136+
@Override public String getRequestMethod() { return "GET"; }
137+
@Override public void setInstanceFollowRedirects(boolean followRedirects) {}
138+
@Override public boolean getInstanceFollowRedirects() { return true; }
139+
@Override public void setDoOutput(boolean dooutput) {}
140+
@Override public boolean getDoOutput() { return false; }
141+
@Override public void setDoInput(boolean doinput) {}
142+
@Override public boolean getDoInput() { return true; }
143+
@Override public void setUseCaches(boolean usecaches) {}
144+
@Override public boolean getUseCaches() { return false; }
145+
@Override public void setIfModifiedSince(long ifmodifiedsince) {}
146+
@Override public long getIfModifiedSince() { return 0; }
147+
@Override public void setDefaultUseCaches(boolean defaultusecaches) {}
148+
@Override public boolean getDefaultUseCaches() { return false; }
149+
@Override public void setRequestProperty(String key, String value) {}
150+
@Override public void addRequestProperty(String key, String value) {}
151+
@Override public String getRequestProperty(String key) { return null; }
152+
@Override public Map<String, List<String>> getRequestProperties() { return null; }
153+
@Override
154+
public String getHeaderField(String name) {
155+
if (name == null) return null;
156+
Map<String, List<String>> headers = getHeaderFields();
157+
List<String> values = headers.get(name.toLowerCase());
158+
return (values != null && !values.isEmpty()) ? values.get(0) : null;
159+
}
160+
161+
@Override
162+
public Map<String, List<String>> getHeaderFields() {
163+
Map<String, List<String>> headers = new HashMap<>();
164+
165+
// Add synthetic headers based on response data
166+
String contentType = getContentType();
167+
if (contentType != null) {
168+
headers.put("content-type", java.util.Arrays.asList(contentType));
169+
}
170+
171+
long contentLength = getContentLengthLong();
172+
if (contentLength >= 0) {
173+
headers.put("content-length", java.util.Arrays.asList(String.valueOf(contentLength)));
174+
}
175+
176+
String contentEncoding = getContentEncoding();
177+
if (contentEncoding != null) {
178+
headers.put("content-encoding", java.util.Arrays.asList(contentEncoding));
179+
}
180+
181+
// Add status line as a synthetic header
182+
try {
183+
headers.put("status", java.util.Arrays.asList(getResponseCode() + " " + getResponseMessage()));
184+
} catch (IOException e) {
185+
// Ignore if we can't get response code
186+
}
187+
188+
return headers;
189+
}
190+
191+
@Override
192+
public int getHeaderFieldInt(String name, int defaultValue) {
193+
String value = getHeaderField(name);
194+
if (value != null) {
195+
try {
196+
return Integer.parseInt(value);
197+
} catch (NumberFormatException e) {
198+
// Fall through to default
199+
}
200+
}
201+
return defaultValue;
202+
}
203+
204+
@Override
205+
public long getHeaderFieldDate(String name, long defaultValue) {
206+
// For SSL proxy responses, we don't have actual date headers
207+
// Return current time for date-related headers
208+
if ("date".equalsIgnoreCase(name)) {
209+
return System.currentTimeMillis();
210+
}
211+
return defaultValue;
212+
}
213+
214+
@Override
215+
public String getHeaderFieldKey(int n) {
216+
Map<String, List<String>> headers = getHeaderFields();
217+
if (n >= 0 && n < headers.size()) {
218+
return (String) headers.keySet().toArray()[n];
219+
}
220+
return null;
221+
}
222+
223+
@Override
224+
public String getHeaderField(int n) {
225+
String key = getHeaderFieldKey(n);
226+
return key != null ? getHeaderField(key) : null;
227+
}
228+
@Override
229+
public long getContentLengthLong() {
230+
String data = mResponse.getData();
231+
if (data == null) {
232+
return 0;
233+
}
234+
return data.getBytes(StandardCharsets.UTF_8).length;
235+
}
236+
237+
@Override
238+
public String getContentType() {
239+
// Try to detect content type from response data, default to JSON for API responses
240+
String data = mResponse.getData();
241+
if (data == null || data.trim().isEmpty()) {
242+
return null;
243+
}
244+
String trimmed = data.trim();
245+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
246+
return "application/json; charset=utf-8";
247+
}
248+
if (trimmed.startsWith("<")) {
249+
return "text/html; charset=utf-8";
250+
}
251+
return "text/plain; charset=utf-8";
252+
}
253+
254+
@Override public String getContentEncoding() { return "utf-8"; }
255+
@Override public long getExpiration() { return 0; }
256+
@Override public long getDate() { return System.currentTimeMillis(); }
257+
@Override public long getLastModified() { return 0; }
258+
@Override public URL getURL() { return mUrl; }
259+
260+
@Override
261+
public int getContentLength() {
262+
long length = getContentLengthLong();
263+
return length > Integer.MAX_VALUE ? -1 : (int) length;
264+
}
265+
@Override public Permission getPermission() throws IOException { return null; }
266+
@Override public OutputStream getOutputStream() throws IOException {
267+
throw new IOException("Output not supported for SSL proxy responses");
268+
}
269+
@Override public void setConnectTimeout(int timeout) {}
270+
@Override public int getConnectTimeout() { return 0; }
271+
@Override public void setReadTimeout(int timeout) {}
272+
@Override public int getReadTimeout() { return 0; }
273+
@Override public void setHostnameVerifier(HostnameVerifier v) {}
274+
@Override public HostnameVerifier getHostnameVerifier() { return null; }
275+
@Override public void setSSLSocketFactory(SSLSocketFactory sf) {}
276+
@Override public SSLSocketFactory getSSLSocketFactory() { return null; }
277+
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
package io.split.android.client.network;
22

3-
public class HttpResponseImpl extends BaseHttpResponseImpl implements HttpResponse {
3+
import java.security.cert.Certificate;
4+
5+
public class HttpResponseImpl extends BaseHttpResponseImpl implements HttpResponse {
46

57
private final String mData;
8+
private Certificate[] mServerCertificates;
69

710
HttpResponseImpl(int httpStatus) {
8-
this(httpStatus, null);
11+
this(httpStatus, (String) null);
12+
}
13+
14+
HttpResponseImpl(int httpStatus, Certificate[] serverCertificates) {
15+
this(httpStatus, null, serverCertificates);
916
}
1017

1118
public HttpResponseImpl(int httpStatus, String data) {
19+
this(httpStatus, data, null);
20+
}
21+
22+
public HttpResponseImpl(int httpStatus, String data, Certificate[] serverCertificates) {
1223
super(httpStatus);
1324
mData = data;
25+
mServerCertificates = serverCertificates;
1426
}
1527

1628
@Override
1729
public String getData() {
1830
return mData;
1931
}
32+
33+
@Override
34+
public Certificate[] getServerCertificates() {
35+
return mServerCertificates;
36+
}
2037
}

0 commit comments

Comments
 (0)