• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package software.amazon.awssdk.crt.test;
2 
3 import java.io.BufferedOutputStream;
4 import java.io.FileOutputStream;
5 import java.net.URI;
6 import java.nio.ByteBuffer;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.Paths;
10 import java.util.Arrays;
11 import java.util.concurrent.CompletableFuture;
12 import java.util.concurrent.TimeUnit;
13 
14 import org.apache.commons.cli.CommandLine;
15 import org.apache.commons.cli.CommandLineParser;
16 import org.apache.commons.cli.DefaultParser;
17 import org.apache.commons.cli.HelpFormatter;
18 import org.apache.commons.cli.Option;
19 import org.apache.commons.cli.Options;
20 import org.apache.commons.cli.ParseException;
21 
22 import software.amazon.awssdk.crt.CrtRuntimeException;
23 import software.amazon.awssdk.crt.Log;
24 import software.amazon.awssdk.crt.Log.LogLevel;
25 import software.amazon.awssdk.crt.http.HttpVersion;
26 import software.amazon.awssdk.crt.http.Http2Request;
27 import software.amazon.awssdk.crt.http.HttpClientConnection;
28 import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
29 import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions;
30 import software.amazon.awssdk.crt.http.HttpHeader;
31 import software.amazon.awssdk.crt.http.HttpRequest;
32 import software.amazon.awssdk.crt.http.HttpRequestBase;
33 import software.amazon.awssdk.crt.http.HttpRequestBodyStream;
34 import software.amazon.awssdk.crt.http.HttpStreamBase;
35 import software.amazon.awssdk.crt.http.HttpStreamBaseResponseHandler;
36 import software.amazon.awssdk.crt.io.ClientBootstrap;
37 import software.amazon.awssdk.crt.io.EventLoopGroup;
38 import software.amazon.awssdk.crt.io.HostResolver;
39 import software.amazon.awssdk.crt.io.SocketOptions;
40 import software.amazon.awssdk.crt.io.TlsContext;
41 import software.amazon.awssdk.crt.io.TlsContextOptions;
42 import software.amazon.awssdk.crt.utils.ByteBufferUtils;
43 
44 public class Elasticurl {
45 
exit()46     static void exit() {
47         System.exit(1);
48     }
49 
exit(String msg)50     static void exit(String msg) {
51         System.out.println(msg);
52         exit();
53     }
54 
parseArgs(String args[])55     static CommandLine parseArgs(String args[]) {
56         Options cliOpts = new Options();
57 
58         cliOpts.addOption("h", "help", false, "show this help message and exit");
59         cliOpts.addOption(Option.builder().longOpt("cacert").hasArg().argName("file")
60                 .desc("path to a CA certificate file.").build());
61         cliOpts.addOption(Option.builder().longOpt("capath").hasArg().argName("dir")
62                 .desc("path to a directory containing CA files.").build());
63         cliOpts.addOption(Option.builder().longOpt("cert").hasArg().argName("file")
64                 .desc("path to a PEM encoded certificate to use with mTLS.").build());
65         cliOpts.addOption(Option.builder().longOpt("key").hasArg().argName("file")
66                 .desc("path to a PEM encoded private key that matches cert.").build());
67         cliOpts.addOption(Option.builder().longOpt("connect_timeout").hasArg().argName("int")
68                 .desc("time in milliseconds to wait for a connection.").build());
69         cliOpts.addOption(Option.builder("H").longOpt("header").hasArgs().argName("str")
70                 .desc("line to send as a header in format 'name:value'. May be specified multiple times.").build());
71         cliOpts.addOption(Option.builder("d").longOpt("data").hasArg().argName("str")
72                 .desc("data to send in POST or PUT.").build());
73         cliOpts.addOption(Option.builder().longOpt("data_file").hasArg().argName("file")
74                 .desc("file to send in POST or PUT.").build());
75         cliOpts.addOption(Option.builder("M").longOpt("method").hasArg().argName("str")
76                 .desc("request method. Default is GET)").build());
77         cliOpts.addOption("G", "get", false, "uses GET for request method.");
78         cliOpts.addOption("P", "post", false, "uses POST for request method.");
79         cliOpts.addOption("I", "head", false, "uses HEAD for request method.");
80         cliOpts.addOption("i", "include", false, "includes headers in output.");
81         cliOpts.addOption("k", "insecure", false, "turns off x.509 validation.");
82         cliOpts.addOption(Option.builder("o").longOpt("output").hasArg().argName("file")
83                 .desc("dumps content-body to FILE instead of stdout.").build());
84         cliOpts.addOption(Option.builder("t").longOpt("trace").hasArg().argName("file")
85                 .desc("dumps logs to FILE instead of stderr.").build());
86         cliOpts.addOption(Option.builder("p").longOpt("alpn").hasArgs().argName("str")
87                 .desc("protocol for ALPN. May be specified multiple times.").build());
88         cliOpts.addOption(null, "http1_1", false, "HTTP/1.1 connection required.");
89         cliOpts.addOption(null, "http2", false, "HTTP/2 connection required.");
90         cliOpts.addOption(Option.builder("v").longOpt("verbose").hasArg().argName("str")
91                 .desc("logging level (ERROR|WARN|INFO|DEBUG|TRACE) default is none.").build());
92 
93         CommandLineParser cliParser = new DefaultParser();
94         CommandLine cli = null;
95         try {
96             cli = cliParser.parse(cliOpts, args);
97 
98             if (cli.hasOption("help") || cli.getArgs().length == 0) {
99                 HelpFormatter formatter = new HelpFormatter();
100                 formatter.printHelp("elasticurl [OPTIONS]... URL", cliOpts);
101                 exit();
102             }
103 
104         } catch (ParseException e) {
105             exit(e.getMessage());
106         }
107 
108         return cli;
109     }
110 
buildHttpRequest(CommandLine cli, HttpVersion requiredVersion, URI uri)111     private static HttpRequestBase buildHttpRequest(CommandLine cli, HttpVersion requiredVersion, URI uri)
112             throws Exception {
113         String method = cli.getOptionValue("method");
114         if (cli.hasOption("get")) {
115             method = "GET";
116         } else if (cli.hasOption("post")) {
117             method = "POST";
118         } else if (cli.hasOption("head")) {
119             method = "HEAD";
120         }
121         if (method == null) {
122             method = "GET";
123         }
124         String pathAndQuery = uri.getQuery() == null ? uri.getPath() : uri.getPath() + "?" + uri.getQuery();
125         if (pathAndQuery.length() == 0) {
126             pathAndQuery = "/";
127         }
128 
129         /* body */
130         ByteBuffer tmpPayload = null;
131         if (cli.getOptionValue("data") != null) {
132             tmpPayload = ByteBuffer.wrap(cli.getOptionValue("data").getBytes());
133         } else if (cli.getOptionValue("data_file") != null) {
134             Path path = Paths.get(cli.getOptionValue("data_file"));
135             tmpPayload = ByteBuffer.wrap(Files.readAllBytes(path));
136         }
137         HttpRequestBodyStream payloadStream = null;
138         if (tmpPayload != null) {
139             final ByteBuffer payload = tmpPayload;
140             payloadStream = new HttpRequestBodyStream() {
141                 @Override
142                 public boolean sendRequestBody(ByteBuffer outBuffer) {
143                     ByteBufferUtils.transferData(payload, outBuffer);
144                     return payload.remaining() == 0;
145                 }
146 
147                 @Override
148                 public boolean resetPosition() {
149                     return true;
150                 }
151 
152                 @Override
153                 public long getLength() {
154                     return payload.capacity();
155                 }
156             };
157         }
158         /* initial headers */
159         HttpHeader[] headers = new HttpHeader[] {};
160         HttpRequestBase request = requiredVersion == HttpVersion.HTTP_2 ? new Http2Request(headers, payloadStream)
161                 : new HttpRequest(method, pathAndQuery, headers, payloadStream);
162 
163         /* Version specific headers */
164         if (requiredVersion == HttpVersion.HTTP_2) {
165             request.addHeader(new HttpHeader(":method", method));
166             request.addHeader(new HttpHeader(":scheme", uri.getScheme()));
167             request.addHeader(new HttpHeader(":authority", uri.getAuthority()));
168             request.addHeader(new HttpHeader(":path", pathAndQuery));
169         } else {
170             request.addHeader(new HttpHeader("Host", uri.getHost()));
171         }
172 
173         /* General headers */
174         request.addHeader(new HttpHeader("accept", "*/*"));
175         request.addHeader(new HttpHeader("user-agent", "elasticurl 1.0, Powered by the AWS Common Runtime."));
176 
177         /* Customized headers */
178         String[] customizedHeaders = cli.getOptionValues("header");
179         if (customizedHeaders != null) {
180             for (String header : customizedHeaders) {
181                 String[] pair = header.split(":");
182                 request.addHeader(new HttpHeader(pair[0].trim(), pair[1].trim()));
183             }
184         }
185         return request;
186     }
187 
main(String args[])188     public static void main(String args[]) throws Exception {
189         CommandLine cli = parseArgs(args);
190 
191         // enable logging
192         String verbose = cli.getOptionValue("verbose");
193         if (verbose != null) {
194             LogLevel logLevel = LogLevel.None;
195             if (verbose.equals("ERROR")) {
196                 logLevel = LogLevel.Error;
197             } else if (verbose.equals("WARN")) {
198                 logLevel = LogLevel.Warn;
199             } else if (verbose.equals("INFO")) {
200                 logLevel = LogLevel.Info;
201             } else if (verbose.equals("DEBUG")) {
202                 logLevel = LogLevel.Debug;
203             } else if (verbose.equals("TRACE")) {
204                 logLevel = LogLevel.Trace;
205             } else {
206                 exit(logLevel + " unsupported value for verbose option");
207             }
208 
209             String trace = cli.getOptionValue("trace");
210             if (trace != null) {
211                 Log.initLoggingToFile(logLevel, trace);
212             } else {
213                 Log.initLoggingToStderr(logLevel);
214             }
215         }
216 
217         if (cli.getArgs().length == 0) {
218             exit("missing URL");
219         }
220 
221         URI uri = new URI(cli.getArgs()[0]);
222         boolean useTls = true;
223         int port = 443;
224         if (uri.getScheme().equals("http")) {
225             useTls = false;
226             port = 80;
227         }
228         if (uri.getPort() != -1) {
229             port = uri.getPort();
230         }
231 
232         HttpVersion requiredVersion = HttpVersion.UNKNOWN;
233         if (cli.hasOption("http1_1")) {
234             requiredVersion = HttpVersion.HTTP_1_1;
235         } else if (cli.hasOption("http2")) {
236             requiredVersion = HttpVersion.HTTP_2;
237         }
238 
239         TlsContextOptions tlsOpts = null;
240         TlsContext tlsCtx = null;
241         try {
242             // set up TLS (if https)
243             if (useTls) {
244                 String cert = cli.getOptionValue("cert");
245                 String key = cli.getOptionValue("key");
246                 if (cert != null && key != null) {
247                     tlsOpts = TlsContextOptions.createWithMtlsFromPath(cert, key);
248                 } else {
249                     tlsOpts = TlsContextOptions.createDefaultClient();
250                 }
251 
252                 String caPath = cli.getOptionValue("capath");
253                 String caCert = cli.getOptionValue("cacert");
254                 if (caPath != null || caCert != null) {
255                     tlsOpts.overrideDefaultTrustStoreFromPath(caPath, caCert);
256                 }
257 
258                 if (cli.hasOption("insecure")) {
259                     tlsOpts.verifyPeer = false;
260                 }
261 
262                 String[] alpn = cli.getOptionValues("alpn");
263                 if (alpn == null) {
264                     if (requiredVersion == HttpVersion.HTTP_1_1) {
265                         alpn = new String[] { "http/1.1" };
266                     } else if (requiredVersion == HttpVersion.HTTP_2) {
267                         alpn = new String[] { "h2" };
268                     } else {
269                         alpn = new String[] { "h2", "http/1.1" };
270                     }
271                 }
272                 tlsOpts.alpnList = Arrays.asList(alpn);
273 
274                 tlsCtx = new TlsContext(tlsOpts);
275             }
276 
277             CompletableFuture<Void> connMgrShutdownComplete = null;
278             final BufferedOutputStream out = cli.getOptionValue("output") == null ? new BufferedOutputStream(System.out)
279                     : new BufferedOutputStream(new FileOutputStream(cli.getOptionValue("output")));
280             try (EventLoopGroup eventLoopGroup = new EventLoopGroup(1);
281                     HostResolver resolver = new HostResolver(eventLoopGroup);
282                     ClientBootstrap bootstrap = new ClientBootstrap(eventLoopGroup, resolver);
283                     SocketOptions socketOpts = new SocketOptions()) {
284                 if (cli.getOptionValue("connect_timeout") != null) {
285                     int timeout = Integer.parseInt(cli.getOptionValue("connect_timeout"));
286                     socketOpts.connectTimeoutMs = timeout;
287                 }
288                 HttpClientConnectionManagerOptions connMgrOpts = new HttpClientConnectionManagerOptions()
289                         .withClientBootstrap(bootstrap).withSocketOptions(socketOpts).withTlsContext(tlsCtx)
290                         .withUri(uri).withPort(port);
291 
292                 try (HttpClientConnectionManager connMgr = HttpClientConnectionManager.create(connMgrOpts)) {
293                     connMgrShutdownComplete = connMgr.getShutdownCompleteFuture();
294                     try (HttpClientConnection conn = connMgr.acquireConnection().get(60, TimeUnit.SECONDS)) {
295 
296                         final CompletableFuture<Void> reqCompleted = new CompletableFuture<>();
297                         HttpStreamBaseResponseHandler streamHandler = new HttpStreamBaseResponseHandler() {
298                             boolean statusWritten = false;
299 
300                             @Override
301                             public void onResponseHeaders(HttpStreamBase stream, int responseStatusCode, int blockType,
302                                     HttpHeader[] nextHeaders) {
303                                 if (blockType == 1) {
304                                     /* Ignore informational headers */
305                                     return;
306                                 }
307                                 if (cli.hasOption("include")) {
308                                     if (!statusWritten) {
309                                         System.out.println(String.format("Response Status: %d", responseStatusCode));
310                                         statusWritten = true;
311                                     }
312                                     for (HttpHeader header : nextHeaders) {
313                                         System.out.println(header.getName() + ": " + header.getValue());
314                                     }
315                                 }
316                             }
317 
318                             @Override
319                             public int onResponseBody(HttpStreamBase stream, byte[] bodyBytesIn) {
320                                 try {
321                                     out.write(bodyBytesIn, 0, bodyBytesIn.length);
322                                     out.flush();
323                                 } catch (Exception e) {
324                                     exit("Failed to write the body");
325                                 }
326                                 return bodyBytesIn.length;
327                             }
328 
329                             @Override
330                             public void onResponseComplete(HttpStreamBase stream, int errorCode) {
331                                 if (errorCode != 0) {
332                                     reqCompleted.completeExceptionally(new CrtRuntimeException(errorCode));
333                                 } else {
334                                     reqCompleted.complete(null);
335                                 }
336                                 stream.close();
337                             }
338                         };
339                         HttpRequestBase request = buildHttpRequest(cli, conn.getVersion(), uri);
340                         try (HttpStreamBase stream = conn.makeRequest(request, streamHandler)) {
341                             stream.activate();
342 
343                             // Give the request up to 60 seconds to complete, otherwise throw a
344                             // TimeoutException
345                             reqCompleted.get(60, TimeUnit.SECONDS);
346                         }
347                     }
348                 } catch (Exception e) {
349                     throw new RuntimeException(e);
350                 }
351             } finally {
352                 out.close();
353                 if (connMgrShutdownComplete != null) {
354                     connMgrShutdownComplete.get();
355                 }
356             }
357 
358         } finally {
359             if (tlsCtx != null) {
360                 tlsCtx.close();
361             }
362 
363             if (tlsOpts != null) {
364                 tlsOpts.close();
365             }
366         }
367     }
368 }
369