Skip to content

Commit eef5409

Browse files
authored
Merge pull request #786 from splitio/FME-7176-proxy-config
Output stream in tunnel connection
2 parents e557f16 + 84e967a commit eef5409

File tree

2 files changed

+232
-14
lines changed

2 files changed

+232
-14
lines changed

src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package io.split.android.client.network;
22

33
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
import androidx.annotation.VisibleForTesting;
46

57
import java.io.ByteArrayInputStream;
8+
import java.io.ByteArrayOutputStream;
69
import java.io.IOException;
710
import java.io.InputStream;
811
import java.io.OutputStream;
@@ -22,13 +25,17 @@
2225
/**
2326
* Adapter that wraps an HttpResponse as an HttpURLConnection.
2427
* <p>
25-
* This is only used to adapt the response from the CONNECT method.
28+
* This is only used to adapt the response from request through the TLS tunnel.
2629
*/
2730
class HttpResponseConnectionAdapter extends HttpsURLConnection {
2831

2932
private final HttpResponse mResponse;
3033
private final URL mUrl;
3134
private final Certificate[] mServerCertificates;
35+
private OutputStream mOutputStream;
36+
private InputStream mInputStream;
37+
private InputStream mErrorStream;
38+
private boolean mDoOutput = false;
3239

3340
/**
3441
* Creates an adapter that wraps an HttpResponse as an HttpURLConnection.
@@ -38,12 +45,33 @@ class HttpResponseConnectionAdapter extends HttpsURLConnection {
3845
* @param serverCertificates The server certificates from the SSL connection
3946
*/
4047
HttpResponseConnectionAdapter(@NonNull URL url,
41-
@NonNull HttpResponse response,
42-
Certificate[] serverCertificates) {
48+
@NonNull HttpResponse response,
49+
Certificate[] serverCertificates) {
50+
this(url, response, serverCertificates, new ByteArrayOutputStream());
51+
}
52+
53+
@VisibleForTesting
54+
HttpResponseConnectionAdapter(@NonNull URL url,
55+
@NonNull HttpResponse response,
56+
Certificate[] serverCertificates,
57+
@NonNull OutputStream outputStream) {
58+
this(url, response, serverCertificates, outputStream, null, null);
59+
}
60+
61+
@VisibleForTesting
62+
HttpResponseConnectionAdapter(@NonNull URL url,
63+
@NonNull HttpResponse response,
64+
Certificate[] serverCertificates,
65+
@NonNull OutputStream outputStream,
66+
@Nullable InputStream inputStream,
67+
@Nullable InputStream errorStream) {
4368
super(url);
4469
mUrl = url;
4570
mResponse = response;
4671
mServerCertificates = serverCertificates;
72+
mOutputStream = outputStream;
73+
mInputStream = inputStream;
74+
mErrorStream = errorStream;
4775
}
4876

4977
@Override
@@ -77,21 +105,27 @@ public InputStream getInputStream() throws IOException {
77105
if (mResponse.getHttpStatus() >= 400) {
78106
throw new IOException("HTTP " + mResponse.getHttpStatus());
79107
}
80-
String data = mResponse.getData();
81-
if (data == null) {
82-
data = "";
108+
if (mInputStream == null) {
109+
String data = mResponse.getData();
110+
if (data == null) {
111+
data = "";
112+
}
113+
mInputStream = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
83114
}
84-
return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
115+
return mInputStream;
85116
}
86117

87118
@Override
88119
public InputStream getErrorStream() {
89120
if (mResponse.getHttpStatus() >= 400) {
90-
String data = mResponse.getData();
91-
if (data == null) {
92-
data = "";
121+
if (mErrorStream == null) {
122+
String data = mResponse.getData();
123+
if (data == null) {
124+
data = "";
125+
}
126+
mErrorStream = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
93127
}
94-
return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
128+
return mErrorStream;
95129
}
96130
return null;
97131
}
@@ -108,6 +142,32 @@ public boolean usingProxy() {
108142

109143
@Override
110144
public void disconnect() {
145+
// Close output stream if it exists
146+
try {
147+
if (mOutputStream != null) {
148+
mOutputStream.close();
149+
}
150+
} catch (IOException e) {
151+
// Ignore exception during disconnect
152+
}
153+
154+
// Close input stream if it exists
155+
try {
156+
if (mInputStream != null) {
157+
mInputStream.close();
158+
}
159+
} catch (IOException e) {
160+
// Ignore exception during disconnect
161+
}
162+
163+
// Close error stream if it exists
164+
try {
165+
if (mErrorStream != null) {
166+
mErrorStream.close();
167+
}
168+
} catch (IOException e) {
169+
// Ignore exception during disconnect
170+
}
111171
}
112172

113173
// Required abstract method implementations for HTTPS connection
@@ -148,11 +208,12 @@ public boolean getInstanceFollowRedirects() {
148208

149209
@Override
150210
public void setDoOutput(boolean doOutput) {
211+
mDoOutput = doOutput;
151212
}
152213

153214
@Override
154215
public boolean getDoOutput() {
155-
return false;
216+
return mDoOutput;
156217
}
157218

158219
@Override
@@ -350,7 +411,10 @@ public Permission getPermission() throws IOException {
350411

351412
@Override
352413
public OutputStream getOutputStream() throws IOException {
353-
throw new IOException("Output not supported for SSL proxy responses");
414+
if (!mDoOutput) {
415+
throw new IOException("Output not enabled for this connection. Call setDoOutput(true) first.");
416+
}
417+
return mOutputStream;
354418
}
355419

356420
@Override

src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.split.android.client.network;
22

33
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
45
import static org.junit.Assert.assertNotNull;
56
import static org.junit.Assert.assertNull;
67
import static org.junit.Assert.assertSame;
@@ -12,6 +13,8 @@
1213
import org.junit.Test;
1314
import org.mockito.Mock;
1415

16+
import java.io.ByteArrayInputStream;
17+
import java.io.ByteArrayOutputStream;
1518
import java.io.IOException;
1619
import java.io.InputStream;
1720
import java.net.MalformedURLException;
@@ -367,8 +370,159 @@ public void urlCanBeRetrieved() {
367370
}
368371

369372
@Test(expected = IOException.class)
370-
public void getOutputStreamThrows() throws IOException {
373+
public void getOutputStreamThrowsWhenNotEnabled() throws IOException {
371374
mAdapter = new HttpResponseConnectionAdapter(mTestUrl, mMockResponse, mTestCertificates);
375+
// Should throw exception since doOutput is not enabled
372376
mAdapter.getOutputStream();
373377
}
378+
379+
@Test
380+
public void setDoOutputEnablesOutput() {
381+
mAdapter = new HttpResponseConnectionAdapter(mTestUrl, mMockResponse, mTestCertificates);
382+
383+
// Initially doOutput should be false
384+
assertEquals(false, mAdapter.getDoOutput());
385+
386+
// After setting doOutput to true, getDoOutput should return true
387+
mAdapter.setDoOutput(true);
388+
assertEquals(true, mAdapter.getDoOutput());
389+
}
390+
391+
@Test
392+
public void getOutputStreamAfterEnablingOutput() throws IOException {
393+
mAdapter = new HttpResponseConnectionAdapter(mTestUrl, mMockResponse, mTestCertificates);
394+
mAdapter.setDoOutput(true);
395+
396+
assertNotNull("Output stream should not be null when doOutput is enabled", mAdapter.getOutputStream());
397+
}
398+
399+
@Test
400+
public void writeToOutputStream() throws IOException {
401+
// Create a ByteArrayOutputStream to capture the written data
402+
ByteArrayOutputStream testOutputStream = new ByteArrayOutputStream();
403+
404+
// Use the constructor that accepts a custom OutputStream
405+
mAdapter = new HttpResponseConnectionAdapter(mTestUrl, mMockResponse, mTestCertificates, testOutputStream);
406+
mAdapter.setDoOutput(true);
407+
408+
// Write test data to the output stream
409+
String testData = "Test output data";
410+
mAdapter.getOutputStream().write(testData.getBytes(StandardCharsets.UTF_8));
411+
412+
// Verify that the data was written correctly
413+
assertEquals("Written data should match the input", testData, testOutputStream.toString(StandardCharsets.UTF_8.name()));
414+
}
415+
416+
@Test
417+
public void disconnectClosesOutputStream() throws IOException {
418+
// Create a custom OutputStream that tracks if it's been closed
419+
TestOutputStream testOutputStream = new TestOutputStream();
420+
421+
mAdapter = new HttpResponseConnectionAdapter(mTestUrl, mMockResponse, mTestCertificates, testOutputStream);
422+
mAdapter.setDoOutput(true);
423+
424+
// Get the output stream and write some data
425+
mAdapter.getOutputStream().write("Test".getBytes(StandardCharsets.UTF_8));
426+
427+
// Verify the stream is not closed yet
428+
assertFalse("Output stream should not be closed before disconnect", testOutputStream.isClosed());
429+
430+
// Disconnect should close the output stream
431+
mAdapter.disconnect();
432+
433+
// Verify the stream was closed
434+
assertTrue("Output stream should be closed after disconnect", testOutputStream.isClosed());
435+
}
436+
437+
@Test
438+
public void disconnectClosesInputStream() throws IOException {
439+
// Create a custom InputStream that tracks if it's been closed
440+
TestInputStream testInputStream = new TestInputStream("Test response data".getBytes(StandardCharsets.UTF_8));
441+
TestOutputStream testOutputStream = new TestOutputStream();
442+
443+
// Create adapter with injected test input stream
444+
when(mMockResponse.getHttpStatus()).thenReturn(200);
445+
mAdapter = new HttpResponseConnectionAdapter(
446+
mTestUrl,
447+
mMockResponse,
448+
mTestCertificates,
449+
testOutputStream,
450+
testInputStream,
451+
null);
452+
453+
// Get the input stream and read some data to simulate usage
454+
InputStream stream = mAdapter.getInputStream();
455+
byte[] buffer = new byte[10];
456+
stream.read(buffer);
457+
458+
// Verify the stream is not closed yet
459+
assertFalse("Input stream should not be closed before disconnect", testInputStream.isClosed());
460+
461+
// Disconnect should close the input stream
462+
mAdapter.disconnect();
463+
464+
// Verify the stream was closed
465+
assertTrue("Input stream should be closed after disconnect", testInputStream.isClosed());
466+
}
467+
468+
/**
469+
* Custom OutputStream implementation for testing that tracks if it's been closed.
470+
*/
471+
private static class TestOutputStream extends ByteArrayOutputStream {
472+
private boolean mClosed = false;
473+
474+
@Override
475+
public void close() throws IOException {
476+
super.close();
477+
mClosed = true;
478+
}
479+
480+
public boolean isClosed() {
481+
return mClosed;
482+
}
483+
}
484+
485+
private static class TestInputStream extends ByteArrayInputStream {
486+
private boolean mClosed = false;
487+
488+
public TestInputStream(byte[] data) {
489+
super(data);
490+
}
491+
492+
@Override
493+
public void close() throws IOException {
494+
super.close();
495+
mClosed = true;
496+
}
497+
498+
public boolean isClosed() {
499+
return mClosed;
500+
}
501+
}
502+
503+
@Test
504+
public void disconnectClosesErrorStream() throws IOException {
505+
TestInputStream testErrorStream = new TestInputStream("Error data".getBytes(StandardCharsets.UTF_8));
506+
TestOutputStream testOutputStream = new TestOutputStream();
507+
508+
when(mMockResponse.getHttpStatus()).thenReturn(404); // Error status
509+
mAdapter = new HttpResponseConnectionAdapter(
510+
mTestUrl,
511+
mMockResponse,
512+
mTestCertificates,
513+
testOutputStream,
514+
null,
515+
testErrorStream);
516+
517+
// Get the error stream and read some data to simulate usage
518+
InputStream stream = mAdapter.getErrorStream();
519+
byte[] buffer = new byte[10];
520+
stream.read(buffer);
521+
522+
assertFalse("Error stream should not be closed before disconnect", testErrorStream.isClosed());
523+
524+
mAdapter.disconnect();
525+
526+
assertTrue("Error stream should be closed after disconnect", testErrorStream.isClosed());
527+
}
374528
}

0 commit comments

Comments
 (0)