Last active
October 31, 2023 19:30
-
-
Save libetl/c9f93ae9d455bdd548a4517317ee843d to your computer and use it in GitHub Desktop.
Vanilla http-client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.tools.aws; | |
import javax.crypto.Mac; | |
import javax.crypto.spec.SecretKeySpec; | |
import java.io.ByteArrayInputStream; | |
import java.io.DataInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.net.URISyntaxException; | |
import java.nio.charset.StandardCharsets; | |
import java.security.InvalidKeyException; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.text.DateFormat; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Date; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.TimeZone; | |
class AWS4 { | |
public static class SignerRequestParams { | |
String serviceName; | |
String regionName; | |
Date date; | |
String signingAlgorithm = "AWS4-HMAC-SHA256"; | |
public SignerRequestParams(String serviceName, | |
String regionName, | |
Date date) { | |
this.serviceName = serviceName; | |
this.regionName = regionName; | |
this.date = date; | |
} | |
public String getCredentialsScope() { | |
return dateFormat.format(date) + "/" + | |
regionName + "/" + serviceName + | |
"/aws4_request"; | |
} | |
} | |
private static final List<String> | |
listOfHeadersToIgnoreInLowerCase = | |
Arrays.asList("connection", "x-amzn-trace-id", | |
"user-agent"); | |
private static final int[] base16 = | |
new int[]{'0', '1', '2' | |
, '3', '4', '5', '6', '7', '8', '9', | |
'a', 'b' | |
, 'c', 'd', 'e', 'f'}; | |
private static final int MASK_4BITS = (1 << 4) - 1; | |
private static final DateFormat dateFormat = | |
new SimpleDateFormat("yyyyMMdd"); | |
static final DateFormat dateTimeFormat = | |
new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); | |
static { | |
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); | |
dateTimeFormat.setTimeZone(TimeZone.getTimeZone( | |
"UTC")); | |
} | |
public static Map<String, List<String>> sign( | |
Request request, Credentials credentials, | |
SignerRequestParams signerRequestParams) | |
throws IOException, NoSuchAlgorithmException, | |
URISyntaxException, InvalidKeyException { | |
Map<String, List<String>> headers = | |
new HashMap<String, List<String>>( | |
request.headers); | |
headers.put("X-Amz-Security-Token", | |
Collections.singletonList( | |
credentials.securityToken)); | |
headers.put("host", | |
Collections.singletonList( | |
request.url.getHost())); | |
headers.put("x-amz-date", | |
Collections.singletonList( | |
dateTimeFormat.format( | |
signerRequestParams.date))); | |
InputStream payloadStream = | |
new ByteArrayInputStream(request.body); | |
byte[] contentBytes = | |
new byte[payloadStream.available()]; | |
DataInputStream dataInputStream = | |
new DataInputStream(payloadStream); | |
dataInputStream.readFully(contentBytes); | |
String contentSha256 = sha256(contentBytes); | |
payloadStream.reset(); | |
final Map<String, List<String>> headersToConsider = | |
new HashMap<>(request.headers); | |
headersToConsider.put("x-amz-security-token", | |
Collections.singletonList( | |
credentials.securityToken)); | |
headersToConsider.put("x-amz-date", | |
Collections.singletonList( | |
dateTimeFormat.format( | |
signerRequestParams.date))); | |
headersToConsider.remove("user-agent"); | |
final List<String> sortedHeaders = | |
new ArrayList<>(headersToConsider.keySet()); | |
Collections.sort(sortedHeaders, | |
String.CASE_INSENSITIVE_ORDER); | |
StringBuilder headerKeysAsList = | |
new StringBuilder(); | |
StringBuilder headersAsList = new StringBuilder(); | |
for (String header : sortedHeaders) { | |
if (listOfHeadersToIgnoreInLowerCase.contains( | |
header.toLowerCase())) { | |
continue; | |
} | |
if (headerKeysAsList.length() > 0) | |
headerKeysAsList.append(";"); | |
String key = header.toLowerCase().trim(); | |
String value = | |
headersToConsider.get(header).get(0); | |
headersAsList.append(key.replaceAll("\\s+", | |
" ")).append(":"); | |
if (value != null && | |
value.trim().length() > 0) { | |
headersAsList.append(value.replaceAll( | |
"\\s+", " ")); | |
} | |
headersAsList.append("\n"); | |
headerKeysAsList.append(key); | |
} | |
String canonicalRequest = | |
request.method.name() + "\n" + | |
(request.url.getPath().trim() | |
.length() == 0 ? "/" : | |
request.url.getPath()) + | |
"\n" + | |
(request.url.toURI().getQuery() == | |
null ? "" : | |
request.url.toURI() | |
.getQuery()) + | |
"\n" + headersAsList + | |
"\n" + headerKeysAsList + | |
"\n" + contentSha256; | |
final String stringToSign = | |
signerRequestParams.signingAlgorithm + | |
"\n" + | |
dateTimeFormat.format( | |
signerRequestParams.date) + | |
"\n" + | |
signerRequestParams.getCredentialsScope() + | |
"\n" + | |
sha256(canonicalRequest.getBytes()); | |
byte[] kSecret = | |
("AWS4" + credentials.secretKey).getBytes( | |
StandardCharsets.UTF_8); | |
byte[] kDate = | |
sign(dateFormat.format( | |
signerRequestParams.date) | |
.getBytes(), kSecret, "HmacSHA256"); | |
byte[] kRegion = | |
sign(signerRequestParams.regionName.getBytes(), | |
kDate, "HmacSHA256"); | |
byte[] kService = | |
sign(signerRequestParams.serviceName.getBytes(), | |
kRegion, "HmacSHA256"); | |
byte[] signingKey = | |
sign("aws4_request".getBytes(), kService, | |
"HmacSHA256"); | |
final byte[] signature = | |
sign(stringToSign.getBytes( | |
StandardCharsets.UTF_8), | |
signingKey, "HmacSHA256"); | |
final String signingCredentials = | |
credentials.accessKeyId + "/" + | |
signerRequestParams.getCredentialsScope(); | |
final String credential = | |
"Credential=" + signingCredentials; | |
final String signerHeaders = | |
"SignedHeaders=" + headerKeysAsList; | |
final String signatureHeader = | |
"Signature=" + hex(signature); | |
headers.put("Authorization", | |
Collections.singletonList("AWS4-HMAC" + | |
"-SHA256 " + credential + ", " + | |
signerHeaders + ", " + | |
signatureHeader)); | |
return headers; | |
} | |
private static String sha256(byte[] input) | |
throws NoSuchAlgorithmException { | |
MessageDigest md = MessageDigest.getInstance("SHA" + | |
"-256"); | |
md.update(input); | |
byte[] hashedPayloadBytes = md.digest(); | |
return hex(hashedPayloadBytes); | |
} | |
private static String hex(byte[] input) { | |
byte[] contentSha256Bytes = | |
new byte[input.length * 2]; | |
for (int i = 0; i < input.length; i++) { | |
contentSha256Bytes[2 * i] = | |
(byte) base16[input[i] >>> 4 & | |
MASK_4BITS]; | |
contentSha256Bytes[2 * i + 1] = | |
(byte) base16[input[i] & MASK_4BITS]; | |
} | |
return new String(contentSha256Bytes); | |
} | |
private static byte[] sign(byte[] data, byte[] key, | |
String algorithm) | |
throws NoSuchAlgorithmException, | |
InvalidKeyException { | |
Mac mac = Mac.getInstance(algorithm); | |
mac.init(new SecretKeySpec(key, algorithm)); | |
return mac.doFinal(data); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mycompany.tools.httpclient; | |
import java.io.BufferedReader; | |
import java.io.ByteArrayInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InputStreamReader; | |
import java.net.HttpURLConnection; | |
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
class Request { | |
URL url; | |
HttpMethod method = HttpMethod.GET; | |
Map<String, List<String>> headers = | |
new HashMap<String, List<String>>(); | |
byte[] body; | |
enum HttpMethod { | |
POST, GET, PUT, DELETE, PATCH, OPTIONS, HEAD, | |
CONNECT | |
} | |
public Request(URL url, HttpMethod method, byte[] body, | |
Map.Entry<String, String>... keyValue) { | |
this(url, method, body, convert(keyValue)); | |
} | |
public static Request parse(String requestSpec) | |
throws MalformedURLException { | |
List<String> requestLines = | |
Arrays.asList(requestSpec.split("\n")); | |
String[] requestLineWords = | |
requestLines.get(0).trim().split(" "); | |
HttpMethod httpMethod = | |
HttpMethod.valueOf(requestLineWords[0]); | |
String uri = (requestLineWords.length < 2 || | |
requestLineWords[1].startsWith("HTTP/")) ? | |
"/" : requestLineWords[1]; | |
List<String> afterRequestLine = | |
requestLines.subList(2, | |
requestLines.size()); | |
List<String> onlyHeaders = | |
afterRequestLine.subList(0, | |
afterRequestLine.indexOf("")); | |
List<String> onlyBody = afterRequestLine.subList( | |
afterRequestLine.indexOf("") + 1, | |
afterRequestLine.size()); | |
Map<String, List<String>> headers = new HashMap<>(); | |
for (int i = 0; i < onlyHeaders.size(); i++) { | |
String keyValue = onlyHeaders.get(i); | |
String headerName = keyValue.substring(0, | |
keyValue.indexOf(":")).toLowerCase() | |
.trim(); | |
String headerValue = keyValue.substring( | |
keyValue.indexOf(":") + 1).trim(); | |
if (headers.get(headerName) == null) { | |
headers.put(headerName, new ArrayList<>()); | |
} | |
headers.get(headerName).add(headerValue); | |
} | |
String bodyAsString = ""; | |
for (int i = 0; i < onlyBody.size(); i++) { | |
bodyAsString = bodyAsString + onlyBody.get(i); | |
} | |
URL url = new URL("https://" + | |
headers.get("host").get(0) + uri); | |
return new Request(url, httpMethod, | |
bodyAsString.getBytes(), headers); | |
} | |
private static Map<String, List<String>> convert( | |
Map.Entry<String, String>[] keyValue) { | |
Map<String, List<String>> headers1 = | |
new HashMap<String, List<String>>(); | |
List<Map.Entry<String, String>> keyValues = | |
Arrays.asList(keyValue); | |
for (int i = 0; i < keyValues.size(); i++) { | |
headers1.put(keyValues.get(i).getKey(), | |
Collections.singletonList( | |
keyValues.get(i).getValue())); | |
} | |
return headers1; | |
} | |
public Request(URL url, HttpMethod method, byte[] body, | |
Map<String, List<String>> headers) { | |
this.url = url; | |
this.method = method; | |
this.headers = headers; | |
this.body = body; | |
} | |
Request withHeaders( | |
Map<String, List<String>> additionalHeaders) { | |
Map<String, List<String>> newHeaders = | |
new HashMap<String, List<String>>(headers); | |
newHeaders.putAll(additionalHeaders); | |
return new Request(url, method, body, newHeaders); | |
} | |
String run() throws IOException { | |
HttpURLConnection connection = | |
((HttpURLConnection) url.openConnection()); | |
connection.setRequestMethod(method.name()); | |
Iterator<Map.Entry<String, List<String>>> | |
headersIterator = | |
headers.entrySet().iterator(); | |
while (headersIterator.hasNext()) { | |
Map.Entry<String, List<String>> header = | |
headersIterator.next(); | |
connection.setRequestProperty(header.getKey(), | |
header.getValue().get(0)); | |
} | |
if (body != null && body.length > 0) { | |
connection.setDoOutput(true); | |
connection.getOutputStream().write(body); | |
connection.getOutputStream().close(); | |
} | |
InputStream result; | |
try { | |
result = connection.getInputStream(); | |
} catch (IOException e) { | |
result = connection.getErrorStream(); | |
} | |
if (result == null) | |
result = new ByteArrayInputStream( | |
new byte[0]); | |
StringBuilder response = new StringBuilder(); | |
try (BufferedReader br = new BufferedReader( | |
new InputStreamReader(result, "utf-8"))) { | |
String responseLine = null; | |
while ((responseLine = br.readLine()) != null) { | |
response.append(responseLine.trim()); | |
} | |
} | |
return response.toString(); | |
} | |
public String toString() { | |
StringBuilder headersBuilder = new StringBuilder(); | |
Iterator<Map.Entry<String, List<String>>> | |
headersIterator = | |
headers.entrySet().iterator(); | |
while (headersIterator.hasNext()) { | |
Map.Entry<String, List<String>> header = | |
headersIterator.next(); | |
headersBuilder.append(header.getKey()); | |
headersBuilder.append(": "); | |
headersBuilder.append(header.getValue().get(0)); | |
headersBuilder.append("\n"); | |
} | |
return method.name() + " " + | |
url.getPath() + | |
(url.getQuery() == null ? "" : | |
url.getQuery()) + "\n" + | |
headersBuilder + "\n\n" + | |
(body == null ? "<EOF>" : new String(body)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment