Skip to content

Commit 1209206

Browse files
author
Yuval Peress
committed
Add the ability to enable logging.
Specifically, if this configuration option is set, the UrlHttpClient will print cURL commands that can be replayed from command line.
1 parent e933343 commit 1209206

File tree

2 files changed

+220
-7
lines changed

2 files changed

+220
-7
lines changed

aws-android-sdk-core/src/main/java/com/amazonaws/ClientConfiguration.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ public class ClientConfiguration {
174174
*/
175175
private TrustManager trustManager = null;
176176

177+
/**
178+
* Enable/disable logging.
179+
*/
180+
private boolean logging = false;
181+
177182
public ClientConfiguration() {
178183
}
179184

@@ -198,6 +203,7 @@ public ClientConfiguration(ClientConfiguration other) {
198203
this.socketSendBufferSizeHint = other.socketSendBufferSizeHint;
199204
this.signerOverride = other.signerOverride;
200205
this.trustManager = other.trustManager;
206+
this.logging = other.logging;
201207
}
202208

203209
/**
@@ -1029,4 +1035,37 @@ public ClientConfiguration withTrustManager(TrustManager trustManager) {
10291035
return this;
10301036
}
10311037

1038+
/**
1039+
* Tells whether or not the client should be logging anything. Currently, logging will print
1040+
* curl commands to replay http requests.
1041+
*
1042+
* @return Whether or not the client will be logging.
1043+
*/
1044+
public boolean isLogging() {
1045+
return logging;
1046+
}
1047+
1048+
/**
1049+
* Sets whether or not the client should be logging any information. This should be used for
1050+
* debug builds only. Defaults to false.
1051+
*
1052+
* @param logging Whether or not the client should be logging operations.
1053+
*/
1054+
public void setLogging(boolean logging) {
1055+
this.logging = logging;
1056+
}
1057+
1058+
/**
1059+
* Sets whether or not the client should be logging any information. This should be used for
1060+
* debug builds only, and returns the updated ClientConfiguration object so that additional
1061+
* calls may be chained together. Defaults to false.
1062+
*
1063+
* @param logging Whether or not the client should be logging operations.
1064+
* @return The updated ClientConfiguration object.
1065+
*/
1066+
public ClientConfiguration withLogging(boolean logging) {
1067+
this.logging = logging;
1068+
return this;
1069+
}
1070+
10321071
}

aws-android-sdk-core/src/main/java/com/amazonaws/http/UrlHttpClient.java

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package com.amazonaws.http;
1717

18+
import android.util.Log;
19+
1820
import com.amazonaws.ClientConfiguration;
1921

2022
import java.io.IOException;
@@ -23,7 +25,9 @@
2325
import java.net.HttpURLConnection;
2426
import java.net.ProtocolException;
2527
import java.net.URL;
28+
import java.nio.ByteBuffer;
2629
import java.security.GeneralSecurityException;
30+
import java.util.HashMap;
2731
import java.util.List;
2832
import java.util.Map;
2933

@@ -43,6 +47,8 @@
4347
*/
4448
public class UrlHttpClient implements HttpClient {
4549

50+
private static final String TAG = "amazonaws";
51+
4652
private final ClientConfiguration config;
4753

4854
public UrlHttpClient(ClientConfiguration config) {
@@ -53,10 +59,21 @@ public UrlHttpClient(ClientConfiguration config) {
5359
public HttpResponse execute(final HttpRequest request) throws IOException {
5460
final URL url = request.getUri().toURL();
5561
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
62+
final CurlBuilder curlBuilder = config.isLogging()
63+
? null : new CurlBuilder(request.getUri().toURL());
5664

5765
configureConnection(request, connection);
58-
applyHeadersAndMethod(request, connection);
59-
writeContentToConnection(request, connection);
66+
applyHeadersAndMethod(request, connection, curlBuilder);
67+
writeContentToConnection(request, connection, curlBuilder);
68+
69+
if (curlBuilder != null) {
70+
if (curlBuilder.isValid()) {
71+
Log.v(TAG, curlBuilder.build());
72+
} else {
73+
Log.v(TAG, "Failed to create curl, content too long");
74+
}
75+
}
76+
6077
return createHttpResponse(request, connection);
6178
}
6279

@@ -103,15 +120,25 @@ public void shutdown() {
103120
// No op
104121
}
105122

123+
/**
124+
* Needed to pass UrlHttpClientTest.
125+
* @see #writeContentToConnection(HttpRequest, HttpURLConnection, CurlBuilder)
126+
*/
127+
void writeContentToConnection(HttpRequest request, HttpURLConnection connection)
128+
throws IOException {
129+
writeContentToConnection(request, connection, null /* curlBuilder */);
130+
}
131+
106132
/**
107133
* Writes the content (if any) of the request to the passed connection
108134
*
109135
* @param request
110136
* @param connection
137+
* @param curlBuilder
111138
* @throws IOException
112139
*/
113-
void writeContentToConnection(final HttpRequest request, final HttpURLConnection connection)
114-
throws IOException {
140+
void writeContentToConnection(final HttpRequest request, final HttpURLConnection connection,
141+
final CurlBuilder curlBuilder) throws IOException {
115142
// Note: if DoOutput is set to true and method is GET, HttpUrlConnection
116143
// will silently change the method to POST.
117144
if (request.getContent() != null && request.getContentLength() >= 0) {
@@ -122,17 +149,41 @@ void writeContentToConnection(final HttpRequest request, final HttpURLConnection
122149
connection.setFixedLengthStreamingMode((int) request.getContentLength());
123150
}
124151
final OutputStream os = connection.getOutputStream();
125-
write(request.getContent(), os);
152+
ByteBuffer curlBuffer = null;
153+
if (curlBuilder != null) {
154+
if (request.getContentLength() < Integer.MAX_VALUE) {
155+
curlBuffer = ByteBuffer.allocate((int) request.getContentLength());
156+
} else {
157+
curlBuilder.setContentOverflow(true);
158+
}
159+
}
160+
write(request.getContent(), os, curlBuffer);
161+
if (curlBuilder != null && curlBuffer != null && curlBuffer.position() != 0) {
162+
// has content
163+
curlBuilder.setContent(new String(curlBuffer.array(), "UTF-8"));
164+
}
126165
os.flush();
127166
os.close();
128167
}
129168
}
130169

170+
/**
171+
* Needed to pass UrlHttpClientTest.
172+
* @see #applyHeadersAndMethod(HttpRequest, HttpURLConnection, CurlBuilder)
173+
*/
174+
HttpURLConnection applyHeadersAndMethod(final HttpRequest request,
175+
final HttpURLConnection connection) throws ProtocolException {
176+
return applyHeadersAndMethod(request, connection, null /* curlBuilder */);
177+
}
178+
131179
HttpURLConnection applyHeadersAndMethod(final HttpRequest request,
132-
final HttpURLConnection connection)
180+
final HttpURLConnection connection, final CurlBuilder curlBuilder)
133181
throws ProtocolException {
134182
// add headers
135183
if (request.getHeaders() != null && !request.getHeaders().isEmpty()) {
184+
if (curlBuilder != null) {
185+
curlBuilder.setHeaders(request.getHeaders());
186+
}
136187
for (final Map.Entry<String, String> header : request.getHeaders().entrySet()) {
137188
final String key = header.getKey();
138189
// Skip reserved headers for HttpURLConnection
@@ -159,13 +210,19 @@ HttpURLConnection applyHeadersAndMethod(final HttpRequest request,
159210

160211
final String method = request.getMethod();
161212
connection.setRequestMethod(method);
213+
if (curlBuilder != null) {
214+
curlBuilder.setMethod(method);
215+
}
162216
return connection;
163217
}
164218

165-
private void write(InputStream is, OutputStream os) throws IOException {
219+
private void write(InputStream is, OutputStream os, ByteBuffer curlBuffer) throws IOException {
166220
final byte[] buf = new byte[1024 * 8];
167221
int len;
168222
while ((len = is.read(buf)) != -1) {
223+
if (curlBuffer != null) {
224+
curlBuffer.put(buf, 0 /* offset */, len);
225+
}
169226
os.write(buf, 0, len);
170227
}
171228
}
@@ -270,4 +327,121 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) {
270327
}
271328
}
272329
*/
330+
331+
/**
332+
* Helper class to build a curl message.
333+
*/
334+
private final class CurlBuilder {
335+
336+
/** The {@link URL} of the operation. */
337+
private final URL url;
338+
/** The method to execute on the given url. */
339+
private String method = null;
340+
/** A map of headers and their values to be sent with the curl request. */
341+
private HashMap<String,String> headers = new HashMap<String,String>();
342+
/** The content to send with the curl request. */
343+
private String content = null;
344+
/** Whether or not the content cannot be written to the curl command. */
345+
private boolean contentOverflow = false;
346+
347+
/**
348+
* Builds a new curl command for the given {@link URL}.
349+
* @param url The {@link URL} for the operation, must not be {@code null}.
350+
*/
351+
public CurlBuilder(URL url) {
352+
if (url == null) {
353+
throw new IllegalArgumentException("Must have a valid url");
354+
}
355+
this.url = url;
356+
}
357+
358+
/**
359+
* Set the method to call for the given curl command. This method will override the previous
360+
* value.
361+
*
362+
* @param method The method to use for the request.
363+
* @return This object for chaining.
364+
*/
365+
public CurlBuilder setMethod(String method) {
366+
this.method = method;
367+
return this;
368+
}
369+
370+
/**
371+
* Set the headers used for the given curl command. This method will override the previous
372+
* values.
373+
*
374+
* @param headers The headers to use for the request.
375+
* @return This object for chaining.
376+
*/
377+
public CurlBuilder setHeaders(Map<String,String> headers) {
378+
this.headers.clear();
379+
this.headers.putAll(headers);
380+
return this;
381+
}
382+
383+
/**
384+
* Set the content used for the given curl command. This method will override the previous
385+
* value.
386+
*
387+
* @param content The content to use for the request.
388+
* @return This object for chaining.
389+
*/
390+
public CurlBuilder setContent(String content) {
391+
this.content = content;
392+
return this;
393+
}
394+
395+
/**
396+
* Sets whether or not the content is too large for the curl command. Content of length
397+
* greater than {@link Integer#MAX_VALUE} are considered too long. If set, the curl should
398+
* not be logged as it will be invalid.
399+
*
400+
* @param contentOverflow Whether or not the content is too long to print.
401+
* @return This object for chaining.
402+
*/
403+
public CurlBuilder setContentOverflow(boolean contentOverflow) {
404+
this.contentOverflow = contentOverflow;
405+
return this;
406+
}
407+
408+
/**
409+
* @return Whether or not this object is valid for printing.
410+
*/
411+
public boolean isValid() {
412+
return !contentOverflow;
413+
}
414+
415+
/**
416+
* Creates a curl command that can be replayed from command line.
417+
*
418+
* @return The curl command.
419+
* @throws IllegalStateException If {@link #isValid()} returns false.
420+
*/
421+
public String build() {
422+
if (!isValid()) {
423+
throw new IllegalStateException("Invalid state, cannot create curl command");
424+
}
425+
StringBuilder stringBuilder = new StringBuilder("curl");
426+
if (method != null) {
427+
stringBuilder.append(" -X ")
428+
.append(method);
429+
}
430+
for (Map.Entry<String,String> entry : headers.entrySet()) {
431+
stringBuilder.append(" -H \"")
432+
.append(entry.getKey())
433+
.append(":")
434+
.append(entry.getValue())
435+
.append("\"");
436+
}
437+
if (content != null) {
438+
stringBuilder.append(" -d '")
439+
.append(content)
440+
.append("'");
441+
}
442+
return stringBuilder.append(" ")
443+
.append(url.toString())
444+
.toString();
445+
}
446+
}
273447
}

0 commit comments

Comments
 (0)