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