Skip to content

Instantly share code, notes, and snippets.

@Varriount
Last active May 21, 2018 03:23
Show Gist options
  • Save Varriount/edfaf064fa17658f2d88c1d740a7070b to your computer and use it in GitHub Desktop.
Save Varriount/edfaf064fa17658f2d88c1d740a7070b to your computer and use it in GitHub Desktop.
import v4
import strformat
import strutils
import httpcore
import times
import httpclient
proc main =
let
region = "ap-southeast-1"
service = "ec2"
secretKey = "SECRET_KEY"
accessKey = "ACCESS_KEY"
httpMethod = "GET"
uri = "/"
queryString = "Version=2016-11-15&Action=DescribeInstances"
# you can also try
# queryString = "Version=2016-11-15&RegionName.1=ap-southeast-1&Action=DescribeRegions"
var
endpoint = fmt"{service}.{region}.amazonaws.com"
payload = ""
var
headers = newHttpHeaders()
headerList = {
"Host": endpoint,
}
for data in headerList:
let (header, value) = data
headers.add(header, value)
addAwsQueryAuth(
queryString = queryString,
httpMethod = httpMethod,
uri = uri,
payload = payload,
headers = headers,
region = region,
service = service,
secretKey = secretKey,
accessKey = accessKey,
requestDateTime = getTime().utc,
expires = 60
)
# addAwsHeaderAuth(
# headers = headers,
# httpMethod = httpMethod,
# uri = uri,
# queryString = queryString,
# payload = payload,
# region = region,
# service = service,
# secretKey = secretKey,
# accessKey = accessKey,
# requestDateTime = getTime()
# )
var url = "https://" & endpoint
if queryString != "":
add(url, "/?")
add(url, queryString)
echo "Request:"
echo fmt("{httpMethod} {uri} HTTP/1.1")
for header, value in headers:
stdout.write(header)
stdout.write(":")
echo value
echo ""
del headers, "host"
let client = newHttpClient()
echo fmt"Requesting response from {url}"
var response = client.request(url, headers=headers)
echo response.body
main()
from strutils import toHex
import nimcrypto
# ## Crypto Procs ## #
type Sha256Digest* = MDigest[sha256.bits]
proc `$`*(digest: MDigest, lowercase: bool): string =
result = ""
for d in digest.data:
result &= hexChar(d, lowercase)
proc update*[T](ctx: var T; data: char) =
update(ctx, cast[ptr byte](unsafeAddr data), 1)
proc updateWithUriEncoded*[T](
result: var T,
decodedChars: set[char],
data: openarray[char]) =
# stdout.write data
const hexChars = "0123456789ABCDEF"
var hexContainer = ['%', '\0', '\0']
for index, character in data:
if character in decodedChars:
update(result, character)
else:
let number = ord(character)
hexContainer[1] = hexChars[number shr 4]
hexContainer[2] = hexChars[number and 0xF]
update(result, hexContainer)
# ## String Procs
proc add*(s: var string, a: openarray[char]) =
let oldLen = len(s)
setLen(s, len(s) + len(a))
for index, character in a:
s[oldLen + index] = character
proc addUriEncoded*(
s: var string,
decodedChars: set[char],
a: openarray[char]) =
const hexChars = "0123456789ABCDEF"
for index, character in a:
if character in decodedChars:
add(s, character)
else:
let number = ord(character)
add(s, '%')
add(s, hexChars[number shr 4])
add(s, hexChars[number and 0xF])
proc c_memcmp(a, b: ptr char, size: csize): cint {.
importc: "memcmp", header: "<string.h>", noSideEffect
.}
proc `$`*(s: openarray[char]): string =
result = newString(len(s))
for index, character in s:
result[index] = character
proc `cmp`*(x, y: openarray[char]): int =
let minlen = min(x.len, y.len)
result = int(c_memcmp(unsafeAddr x[0], unsafeAddr y[0], minlen.csize))
if result == 0:
result = x.len - y.len
import strutils
import tables
import algorithm
import httpcore
import unicode
import strformat
import times
import nimcrypto
import httpclient
import utils
const
decodedChars = {'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~'}
lineFeed = '\l'
signingAlgorithm = "AWS4-HMAC-SHA256"
# StringSegment Implementation
type StringSegment = tuple[start, mid, stop: int]
proc len(segment: StringSegment): int =
result = segment.stop - segment.start + 1
template updates(a, b: untyped) =
# write stdout, b
update(a, b)
proc sortSegmentsIn(segments: var seq[StringSegment], source: string) =
## Sorts a list of string segments using their corresponding source string.
##
## Each segment is sorted using lexicographic comparison of it's first
## segment (characters between segment.start and segment.stop)
segments.sort do (first, second: StringSegment) -> int:
result = cmp(
toOpenArray(source, first.start, first.stop),
toOpenArray(source, second.start, second.stop)
)
# ## Canonical URI Procs ## #
## The canonical URI is the URI-encoded version of the absolute path component
## of the URI, which is everything in the URI from the HTTP host to the
## question mark character ("?") that begins the query string parameters
## (if any)
proc updateWithCanonicalUri(result: var sha256, uri: string) =
## Update the given sha256 object with the canonical version of the given URI.
updateWithUriEncoded(
result = result,
decodedChars = decodedChars + {'/'},
data = uri
)
# ## Canonical Query String Procs ## #
## To construct the canonical query string, complete the following steps:
## - Sort the parameter names by character code point in ascending order.
## For example, a parameter name that begins with the uppercase letter F
## precedes a parameter name that begins with a lowercase letter b.
## - URI-encode each parameter name and value according to the following
## rules:
## - Do not URI-encode any of the unreserved characters that RFC 3986
## defines: A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ),
## and tilde ( ~ ).
## - Percent-encode all other characters with %XY, where X and Y are
## hexadecimal characters (0-9 and uppercase A-F). For example, the space
## character must be encoded as %20 (not using '+', as some encoding
## schemes do) and extended UTF-8 characters must be in the form
## %XY%ZA%BC.
## - Build the canonical query string by starting with the first parameter
## name in the sorted list.
## - For each parameter, append the URI-encoded parameter name, followed by
## the equals sign character (=), followed by the URI-encoded parameter
## value. Use an empty string for parameters that have no value.
## - Append the ampersand character (&) after each parameter value, except
## for the last value in the list.
proc findQueryStringSegments(result: var seq[StringSegment], qs: string) =
## Add the query parameter segments found in a query string to a given
## sequence.
##
## This allows the query canonicalization code to do sorting without copying
## the query string into seperate segments - only the segment markers must be
## moved.
template addSegment(segmentStart, equalsIndex, segmentEnd) =
# Attempt to protect against invalid query strings by performing
# character position checks.
let valid = (
segmentStart < segmentEnd and
segmentStart < equalsIndex and
equalsIndex < segmentEnd
)
if valid:
add(result, (segmentStart, equalsIndex, segmentEnd))
var
lastAmpersand = -1
lastEquals = 0
for index, character in qs:
case character:
of '&':
addSegment(lastAmpersand+1, lastEquals, index-1)
lastAmpersand = index
of '=':
lastEquals = index
else:
discard
addSegment(lastAmpersand+1, lastEquals, high(qs))
proc updateWithCanonicalQueryString(result: var sha256, qs: string) =
## Update the given sha256 object with the canonicalized version of the given
## query string.
if len(qs) == 0:
return
var segments = newSeq[StringSegment]()
# Find and order all the query segments
findQueryStringSegments(segments, qs)
sortSegmentsIn(segments, qs)
# Add url-encoded versions of the query segments to the sha256 object.
for index, segment in segments:
updateWithUriEncoded(
result = result,
decodedChars = decodedChars,
data = toOpenArray(qs, segment.start, segment.mid - 1)
)
updates(result, '=')
updateWithUriEncoded(
result = result,
decodedChars = decodedChars,
data = toOpenArray(qs, segment.mid+1, segment.stop)
)
if index != high(segments):
updates(result, '&')
# ## Header Data Procs ## #
## The header canonicalization procedures both need similar transformations
## performed on header data:
## - Both the header list and the signed header name list need lowered
## header names.
## - The header list needs values with leading and trailing whitespace
## removed, and multiple runs of whitespace condensed.
##
## To optimize the above process, the below procedures are used to turn an
## HttpHeaders object into a HeaderData object, which has had these
## transformations performed on it.
type HeaderData = tuple[
data: string,
segments: seq[StringSegment]
]
proc addLoweredString(result: var string, name: string) =
for r in runes(name):
let
loweredRune = toLower(r)
position = len(result)
fastToUTF8Copy(loweredRune, result, position, false)
proc addCondensedString(result: var string, source: string) =
var
startPos = 0
endPos = 0
# Find the start index
for index in countUp(0, high(source)):
let character = source[index]
if character != ' ':
startPos = index
break
# Find the stop index
for index in countDown(high(source), 0):
let character = source[index]
if character != ' ':
endPos = index
break
# Handle cases where the string is entirely whitespace
if startPos >= endPos:
return
# Condense spaces
var seenSpace = false
template addSpace =
if seenSpace:
seenSpace = false
add(result, ' ')
for character in source:
case character
of ' ':
if seenSpace == false:
seenSpace = true
else:
addSpace()
add(result, character)
addSpace()
proc createHeaderData(headers: HttpHeaders): HeaderData =
result.data = ""
result.segments = @[]
# Add the canonical headers
for key, values in headers.table:
var segment: StringSegment
segment.start = len(result.data)
# Add the lowered version of the header
addLoweredString(result.data, key)
add(result.data, ':')
segment.mid = high(result.data)
# Add a trimmed, comma-separated string
for value in values:
addCondensedString(result.data, value)
add(result.data, ',')
# Add a newline
setLen(result.data, high(result.data))
segment.stop = high(result.data)
add(result.data, '\l')
# Add the segment
add(result.segments, segment)
# Order the segments
sortSegmentsIn(result.segments, result.data)
# ## Canonical Header Procs ## #
## The canonical headers consist of a list of all the HTTP headers that you are
## including with the signed request.
##
## To create the canonical headers list, convert all header names to lowercase
## and remove leading spaces and trailing spaces. Convert sequential spaces in
## the header value to a single space.
##
## Build the canonical headers list by sorting the (lowercase) headers by
## character code and then iterating through the header names.
## Construct each header according to the following rules:
## - Append the lowercase header name followed by a colon.
## - Append a comma-separated list of values for that header.
## Do not sort the values in headers that have multiple values.
## - Append a new line ('\n').
##
## In the canonical form, the following changes were made:
## - The header names were converted to lowercase characters.
## - The headers were sorted by character code.
## - Leading and trailing spaces were removed from the header values.
## - Sequential spaces in a b c were converted to a single space for the
## header values.
proc updateWithCanonicalHeaders(result: var sha256, headerData: HeaderData) =
for segment in headerData.segments:
updates(
result,
toOpenArray(headerData.data, segment.start, segment.stop)
)
updates(result, lineFeed)
# ## Signed Header Procs ## #
## This signed header list is the list of headers that you included in the
## canonical headers. By adding this list of headers, you tell AWS which
## headers in the request are part of the signing process and which ones AWS
## can ignore (for example, any additional headers added by a proxy) for
## purposes of validating the request.
##
## To create the signed headers list, convert all header names to lowercase,
## sort them by character code, and use a semicolon to separate the header
## names.
##
## For each header name except the last, append a semicolon (';') to the header
## name to separate it from the following header name.
proc updateWithHeaderList(result: var sha256, headerData: HeaderData) =
for index, segment in headerData.segments:
updates(
result,
toOpenArray(headerData.data, segment.start, segment.mid-1)
)
if index != high(headerData.segments):
updates(result, ';')
proc addHeaderList(result: var string, headerData: HeaderData) =
for index, segment in headerData.segments:
add(
result,
toOpenArray(headerData.data, segment.start, segment.mid-1)
)
if index != high(headerData.segments):
add(result, ';')
# ## Hash Payload procs ## #
## Use a hash (digest) function like SHA256 to create a hashed value from the
## payload in the body of the HTTP or HTTPS request.
##
## The hashed payload must be represented as a lowercase hexadecimal string.
## If the payload is empty, use an empty string as the input to the hash
## function.
proc updateWithPayloadHash(result: var sha256, payload: string) =
let payloadDigest = digest(sha256, payload)
for dataByte in payloadDigest.data:
updates(result, hexChar(dataByte, lowercase=true))
# ## Primary Signing Procs ## #
proc createCredentialScope(region, service, date: string): string =
result = fmt("{date}/{region}/{service}/aws4_request")
proc createCanonicalRequestHash*(
httpMethod: string,
uri: string,
queryString: string,
payload: string,
headerData: HeaderData): Sha256Digest =
# echo "Creating Canonical Request Hash:"
# echo fmt"httpMethod = {httpMethod}"
# echo fmt"uri = {uri}"
# echo fmt"queryString = {queryString}"
# echo fmt"payload = {payload}"
# echo fmt"headerData = {headerData}"
# echo ""
var shaContext: sha256
init(shaContext)
updates(shaContext, httpMethod)
updates(shaContext, lineFeed)
updateWithCanonicalUri(shaContext, uri)
updates(shaContext, lineFeed)
updateWithCanonicalQueryString(shaContext, queryString)
updates(shaContext, lineFeed)
updateWithCanonicalHeaders(shaContext, headerData)
updates(shaContext, lineFeed)
updateWithHeaderList(shaContext, headerData)
updates(shaContext, lineFeed)
updateWithPayloadHash(shaContext, payload)
result = finish(shaContext)
proc addSignature(
result: var string,
requestDateTimeString: string,
credentialScope: string,
canonicalRequestHash: Sha256Digest,
signingKey: Sha256Digest) =
## To create the string to sign, concatenate the algorithm, date and time,
## credential scope, and digest of the canonical request, as shown in the
## following pseudocode:
## StringToSign =
## Algorithm + \n +
## RequestDateTime + \n +
## CredentialScope + \n +
## HashedCanonicalRequest
var signingHmac: HMAC[sha256]
init(signingHmac, signingKey.data)
updates(signingHmac, signingAlgorithm)
updates(signingHmac, lineFeed)
updates(signingHmac, requestDateTimeString)
updates(signingHmac, lineFeed)
updates(signingHmac, credentialScope)
updates(signingHmac, lineFeed)
for dataByte in canonicalRequestHash.data:
updates(signingHmac, hexChar(dataByte, lowercase=true))
let digest = finish(signingHmac)
for d in digest.data:
result &= hexChar(d, true)
proc createSigningKey(
secretKey: string,
requestDateString: string,
region: string,
service: string): Sha256Digest =
## To create the signing key, use the secret access key to create a series of
## hash-based message authentication codes (HMACs). This is shown in the
## following pseudocode, where HMAC(key, data) represents an HMAC-SHA256
## function that returns output in binary format. The result of each hash
## function becomes input for the next one.
##
## Pseudocode for deriving a signing key:
## kSecret = your secret access key
## kDate = HMAC("AWS4" + kSecret, Date)
## kRegion = HMAC(kDate, Region)
## kService = HMAC(kRegion, Service)
## kSigning = HMAC(kService, "aws4_request")
let
dateKey = hmac(sha256, "AWS4" & secretKey, requestDateString)
regionKey = hmac(sha256, dateKey.data, region)
serviceKey = hmac(sha256, regionKey.data, service)
result = hmac(sha256, serviceKey.data, "aws4_request")
proc addAwsAuthSignature(
result: var string,
httpMethod: string,
uri: string,
queryString: string,
payload: string,
headerData: HeaderData,
region: string,
service: string,
secretKey: string,
requestDateString: string,
requestDateTimeString: string,
credentialScope: string) =
# echo "Creating AWS Auth Signature:"
# echo fmt"result = {result}"
# echo fmt"httpMethod = {httpMethod}"
# echo fmt"uri = {uri}"
# echo fmt"queryString = {queryString}"
# echo fmt"payload = {payload}"
# echo fmt"headerData = {headerData}"
# echo fmt"region = {region}"
# echo fmt"service = {service}"
# echo fmt"secretKey = {secretKey}"
# echo fmt"requestDateString = {requestDateString}"
# echo fmt"requestDateTimeString = {requestDateTimeString}"
# echo fmt"credentialScope = {credentialScope}"
# echo ""
let
canonicalRequestHash = createCanonicalRequestHash(
httpMethod = httpMethod,
uri = uri,
queryString = queryString,
payload = payload,
headerData = headerData
)
signingKey = createSigningKey(
secretKey = secretKey,
requestDateString = requestDateString,
region = region,
service = service
)
addSignature(
result = result,
requestDateTimeString = requestDateTimeString,
credentialScope = credentialScope,
canonicalRequestHash = canonicalRequestHash,
signingKey = signingKey
)
proc addAwsQueryAuth*(
queryString: var string,
httpMethod: string,
uri: string,
payload: string,
headers: HttpHeaders,
region: string,
service: string,
secretKey: string,
accessKey: string,
requestDateTime: DateTime,
expires: int) =
var
requestDateTimeString = format(requestDateTime, "yyyyMMdd'T'HHmmss'Z'")
requestDateString = requestDateTimeString[0..7]
headerData = createHeaderData(headers)
credentialScope = createCredentialScope(region, service, requestDateString)
template addParam(name, value) =
add(queryString, '&')
add(queryString, name)
add(queryString, '=')
add(queryString, value)
addParam("X-Amz-Algorithm", signingAlgorithm)
addParam("X-Amz-Date", requestDateTimeString)
addParam("X-Amz-Expires", expires)
add(queryString, "&X-Amz-Credential=")
add(queryString, accessKey)
add(queryString, "/")
add(queryString, credentialScope)
add(queryString, "&X-Amz-SignedHeaders=")
addHeaderList(queryString, headerData)
var signatureValue = ""
addAwsAuthSignature(
result = signatureValue,
httpMethod = httpMethod,
uri = uri,
queryString = queryString,
payload = payload,
headerData = headerData,
region = region,
service = service,
secretKey = secretKey,
requestDateString = requestDateString,
requestDateTimeString = requestDateTimeString,
credentialScope = credentialScope
)
addParam("X-Amz-Signature", signatureValue)
proc addAwsHeaderAuth*(
headers: var HttpHeaders,
httpMethod: string,
uri: string,
queryString: string,
payload: string,
region: string,
service: string,
secretKey: string,
accessKey: string,
requestDateTime: Time) =
## Creates the authorization string required by requests to the AWS API.
## The produced string must be used in the "Authorization" header.
## If you need to use pre-signed authorization query parameters, use
## `awsQueryAuth` instead.
##
## NOTE:
## This procedure makes the following assumptions:
## - The queryString parameter does *not* start with an '?'.
## - The URI has been formatted for use by the desired service.
## This usually means normalization.
let
requestDateTimeString = format(requestDateTime.utc, "yyyyMMdd'T'HHmmss'Z'")
requestDateString = requestDateTimeString[0..7]
headers["X-Amz-Date"] = requestDateTimeString
let
headerData = createHeaderData(headers)
credentialScope = createCredentialScope(region, service, requestDateString)
var headerValue = fmt(
" {signingAlgorithm} " &
"Credential={accessKey}/{credentialScope}, " &
"SignedHeaders="
)
addHeaderList(headerValue, headerData)
add(headerValue, ", Signature=")
addAwsAuthSignature(
result = headerValue,
httpMethod = httpMethod,
uri = uri,
queryString = queryString,
payload = payload,
headerData = headerData,
region = region,
service = service,
secretKey = secretKey,
requestDateString = requestDateString,
requestDateTimeString = requestDateTimeString,
credentialScope = credentialScope
)
headers["Authorization"] = headerValue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment