Skip to content

Instantly share code, notes, and snippets.

@Varriount
Created September 21, 2017 22:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Varriount/9e84c48baa4fd5ea22558031bfab3554 to your computer and use it in GitHub Desktop.
Save Varriount/9e84c48baa4fd5ea22558031bfab3554 to your computer and use it in GitHub Desktop.
Nim AWS Snippets
# AWS Version 4 signing example
# EC2 API (DescribeRegions)
# See: http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
# This version makes a GET request and passes the signature
# in the Authorization header.
import base64, httpclient, hmac, nimSHA2, os, times, strutils, httpcore
# ************* REQUEST VALUES *************
let
httpMethod = HttpGet
service = "ec2"
host = "ec2.amazonaws.com"
region = "us-east-1"
endpoint = "https://ec2.amazonaws.com"
request_parameters = "Action=DescribeRegions&Version=2013-10-15"
# Key derivation functions. See:
# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python
proc toHex(s: SHA256Digest): string =
hex(s)
proc sign(key: string, msg: string): string =
return $hmac_sha256(key, msg)
proc getSignatureKey(key, date, region, service: string): string =
# Key of the date
result = sign(("AWS4" & key), date)
# Key of the region
result = sign(result, region)
# Key of the service
result = sign(result, service)
# Final signing
result = sign(result, "aws4_request")
# # Read AWS access key from env. variables or configuration file. Best practice is NOT
# # to embed credentials in code.
let
access_key = getEnv("AWS_ACCESS_KEY_ID")
secret_key = getEnv("AWS_SECRET_ACCESS_KEY")
if access_key == "" or secret_key == "":
echo "No access key is available."
quit(1)
# Create a date for headers and the credential string
let
t = getGmTime(getTime())
amzdate = format(t, "yyyymmdd'T'hhmmss'Z'")
datestamp = format(t, "yyyymmdd")
# ************* TASK 1: CREATE A CANONICAL REQUEST *************
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
# Step 1 is to define the verb (GET, POST, etc.)--already done.
# Step 2: Create canonical URI--the part of the URI from domain to query
# string (use '/' if no path)
let canonical_uri = '/'
# Step 3: Create the canonical query string. In this example (a GET request),
# request parameters are in the query string. Query string values must
# be URL-encoded (space=%20). The parameters must be sorted by name.
# For this example, the query string is pre-formatted in the request_parameters variable.
let canonical_querystring = request_parameters
# Step 4: Create the canonical headers and signed headers. Header names
# must be trimmed and lowercase, and sorted in code point order from
# low to high. Note that there is a trailing \n.
let canonical_headers = (
"host:" & host & '\l' &
"x-amz-date:" & amzdate & '\l'
)
# Step 5: Create the list of signed headers. This lists the headers
# in the canonical_headers list, delimited with ";" and in alpha order.
# Note: The request can include any headers; canonical_headers and
# signed_headers lists those that you want to be included in the
# hash of the request. "Host" and "x-amz-date" are always required.
let signed_headers = "host;x-amz-date"
# Step 6: Create payload hash (hash of the request body content). For GET
# requests, the payload is an empty string ("").
let payload_hash = toLower(hex(computeSHA256("")))
# Step 7: Combine elements to create create canonical request
let canonical_request = (
"GET" & '\l' &
canonical_uri & '\l' &
canonical_querystring & '\l' &
canonical_headers & '\l' &
signed_headers & '\l' &
payload_hash
)
# ************* TASK 2: CREATE THE STRING TO SIGN*************
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
# SHA-256 (recommended)
let algorithm = "AWS4-HMAC-SHA256"
let credential_scope = (
datestamp & '/' &
region & '/' &
service & '/' &
"aws4_request"
)
let string_to_sign = (
algorithm & '\l' &
amzdate & '\l' &
credential_scope & '\l' &
toLower(hex(computeSHA256(canonical_request)))
)
# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
let signing_key = getSignatureKey(secret_key, datestamp, region, service)
# # Sign the string_to_sign using the signing_key
let signature = toLower(toHex(hmac_sha256(signing_key, string_to_sign)))
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
# The signing information can be either in a query string value or in
# a header named Authorization. This code shows how to use a header.
# Create authorization header and add to request headers
let authorization_header = (
algorithm & ' ' &
"Credential=" & access_key & '/' & credential_scope & ", " &
"SignedHeaders=" & signed_headers & ", " &
"Signature=" & signature
)
# The request can include any headers, but MUST include "host", "x-amz-date",
# and (for this scenario) "Authorization". "host" and "x-amz-date" must
# be included in the canonical_headers and signed_headers, as noted
# earlier. Order here is not significant.
# Nim note: The 'host' header is added automatically by the Python 'requests' library.
let headers = newHttpHeaders({
"x-amz-date": amzdate,
"Authorization": authorization_header
})
# # ************* SEND THE REQUEST *************
let request_url = endpoint & '?' & canonical_querystring
echo "\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++"
echo "Request URL = " & request_url
let client = newHttpClient()
client.headers = headers
echo repr(client.headers)
let r = client.get(request_url)
echo "\nRESPONSE++++++++++++++++++++++++++++++++++++"
echo "Response code: ", r.code
echo r.body
import json, marshal, os
proc main() =
let
jsonPath = paramStr(1)
jsonData = readFile(jsonPath)
stdout.write($$parseJson(jsonData))
import tables, marshal, json, strutils, macros
proc numFields[T](t: T): int =
when t is ref:
for _ in fields(t[]):
inc(result)
else:
for _ in fields(t):
inc(result)
type
ShapeTable = OrderedTableRef[string, Shape]
OperationTable = OrderedTableRef[string, Operation]
Service = ref object
version: string
metadata: Metadata
documentation: string
operations: OperationTable
shapes: ShapeTable
Metadata = ref object
apiVersion: string
endpointPrefix: string
globalEndpoint: string
protocol: string
serviceAbbreviation: string
serviceFullName: string
signatureVersion: string
uid: string
xmlNamespace: string
Operation = ref object
name: string
documentation: string
http: HttpEndpoint
input: Shape
errors: seq[Shape]
output: Output
HttpEndpoint = ref object
httpMethod: string
requestUri: string
Output = ref object
shape: Shape
resultWrapper: string
ShapeKind = enum
skStructure
skBlob
skBoolean
skInteger
skList
skMap
skString
Shape = ref object
documentation: string not nil
box: bool
case kind: ShapeKind
of skStructure:
members: OrderedTableRef[string, Shape]
required: seq[string]
error: OperationError
exception: bool
# payload: Shape
of skBlob:
sensitive: bool
of skBoolean:
discard
of skInteger:
maxValue: BiggestInt
minValue: BiggestInt
of skList:
member: Shape
of skMap:
key: Shape
value: Shape
of skString:
maxLength: BiggestInt
minLength: BiggestInt
pattern: string
enums: seq[string]
ShapeMember = ref object
shape: Shape
documentation: string
OperationError = ref object
code: string
httpStatusCode: int
senderFault: bool
# JSON helper procedures
proc expect(node: JsonNode, kind: JsonNodeKind, lengths: varargs[int]) =
var
msg = ""
if len(lengths) != 0 and len(node) notin lengths:
msg.addf(
"Expected a node with a length in [$1], got length $2 instead.",
join(lengths, ", "), len(node)
)
if node.kind != kind:
if len(msg) > 0:
msg.add("\n")
msg.addf(
"Expected a node of kind $1, got $2 instead",
kind, node.kind
)
if len(msg) > 0:
msg.add("\n")
msg.addf("Node: $1", pretty(node))
if len(msg) > 0:
raise newException(ValueError, msg)
# "new" procedures
macro loadValue(dest, source: untyped, key: static[string],
required=true): untyped =
let keyIdent = ident(key)
result = quote do:
when `required`:
let node = `source`[`key`]
else:
let node = `source`.getOrDefault(`key`)
if node != nil:
when type(`dest`) is string:
`dest`.`keyIdent` = getStr(node, `dest`.`keyIdent`)
when type(`dest`) is int:
`dest`.`keyIdent` = getNum(node, `dest`.`keyIdent`)
when type(`dest`) is float:
`dest`.`keyIdent` = getFNum(node, `dest`.`keyIdent`)
when type(`dest`) is bool:
`dest`.`keyIdent` = getBVal(node, `dest`.`keyIdent`)
macro loadType(dest, source, callback: untyped, key: static[string],
required=true): untyped =
let keyIdent = ident(key)
result = quote do:
when `required`:
let node = `source`[`key`]
else:
let node = `source`.getOrDefault(`key`)
if node != nil:
`dest`.`keyIdent` = `callback`(node)
proc newShape(result: var Shape, node: JsonNode, shapeTable: ShapeTable)
macro loadShape(dest, source, shapeTable: untyped, key: static[string],
required=true): untyped =
let keyIdent = ident(key)
result = quote do:
when `required`:
let node = `source`[`key`]
else:
let node = `source`.getOrDefault(`key`)
if node != nil:
newShape(`dest`.`keyIdent`, node, shapeTable)
# "new" procedures
proc newOperationError(node: JsonNode): OperationError =
expect(node, JObject, numFields(result))
new(result)
loadValue(result, node, "code")
loadValue(result, node, "httpStatusCode")
loadValue(result, node, "senderFault")
proc newShapeMember(node: JsonNode, shapeTable: ShapeTable): ShapeMember =
expect(node, JObject, numFields(result))
new(result)
loadShape(result, node, shapeTable, "shape")
loadValue(result, node, "documentation")
proc newShape(node: JsonNode, shapeTable: ShapeTable): Shape
proc newShape(result: var Shape, node: JsonNode, shapeTable: ShapeTable) =
template loadOperationError(node): untyped =
newOperationError(node, shapeTable)
# Handle shape references
if len(node) == 1 and "shape" in node:
# Try retrieving the shape from the table. If it's not in the table,
# initialize and add it.
let name = getStr(node["shape"])
result = shapeTable.getOrDefault(name)
if result == nil:
new(result)
shapeTable[name] = result
return
loadValue(result, node, "documentation")
loadValue(result, node, "box")
# Determine the shape kind
let kindName = getStr(node["type"])
if kindName == "structure":
result.kind = skStructure
loadType(result, node, newOperationError, "error")
loadValue(result, node, "exception")
if "required" in node:
result.required = newSeq[string]()
for value in node["required"]:
result.required.add(getStr(value))
if "members" in node:
result.members = newOrderedTable[string, Shape]()
for key, shapeNode in node["members"]:
result.members[key] = newShape(shapeNode, shapeTable)
# result.members: OrderedTableRef[string, Shape]
# payload: Shape
if kindName == "list":
result.kind = skList
loadShape(result, node, shapeTable, "member")
if kindName in ["string", "timestamp"]:
result.kind = skString
loadValue(result, node, "pattern", false)
if "min" in node:
result.maxLength = getNum(node["min"])
if "max" in node:
result.minLength = getNum(node["max"])
if "enum" in node:
result.enums = newSeq[string]()
for value in node:
result.enums.add(getStr(value))
if kindName == "blob":
result.kind = skBlob
loadValue(result, node, "sensitive")
elif kindName == "integer":
result.kind = skInteger
if "min" in node:
result.maxValue = getNum(node["min"])
if "max" in node:
result.minValue = getNum(node["max"])
elif kindName == "map":
result.kind = skMap
loadShape(result, node, shapeTable, "key")
loadShape(result, node, shapeTable, "value")
elif kindName == "boolean":
result.kind = skBoolean
else:
raise newException(Exception, "No valid shape type")
proc newShape(node: JsonNode, shapeTable: ShapeTable): Shape =
newShape(result, node, shapeTable)
proc newOutput(node: JsonNode, shapeTable: ShapeTable): Output =
expect(node, JObject, numFields(result))
new(result)
loadShape(result, node, shapeTable, "shape")
loadValue(result, node, "resultWrapper")
proc newHttpEndpoint(node: JsonNode): HttpEndpoint =
expect(node, JObject, numFields(result))
new(result)
result.httpMethod = getStr(node["method"])
loadValue(result, node, "requestUri")
proc newOperation(node: JsonNode, shapeTable: ShapeTable): Operation =
template loadOutput(node): untyped =
newOutput(node, shapeTable)
expect(node, JObject, 4, 5, 6)
new(result)
loadValue(result, node, "name")
loadType(result, node, newHttpEndpoint, "http")
loadValue(result, node, "documentation")
loadShape(result, node, shapeTable, "input", false)
loadType(result, node, loadOutput, "output", false)
let errorsNode = node.getOrDefault("errors")
if errorsNode != nil:
result.errors = newSeq[Shape]()
for shapeNode in node["errors"]:
var s: Shape
newShape(s, shapeNode, shapeTable)
result.errors.add(s)
proc newMetadata(node: JsonNode): Metadata =
expect(node, JObject, numFields(result))
new(result)
loadValue(result, node, "apiVersion")
loadValue(result, node, "endpointPrefix")
loadValue(result, node, "globalEndpoint")
loadValue(result, node, "protocol")
loadValue(result, node, "serviceAbbreviation")
loadValue(result, node, "serviceFullName")
loadValue(result, node, "signatureVersion")
loadValue(result, node, "uid")
loadValue(result, node, "xmlNamespace")
proc newService(data: JsonNode): Service =
expect(data, JObject, numFields(result))
new(result)
result.metadata = newMetadata(data["metadata"])
result.documentation = data["documentation"].getStr()
result.operations = newOrderedTable[string, Operation]()
result.shapes = newOrderedTable[string, Shape]()
# Load operations and shapes
for name, node in data["operations"]:
result.operations[name] = newOperation(node, result.shapes)
for name, node in data["shapes"]:
var s: Shape
newShape(s, node, result.shapes)
macro staticMain(): untyped =
discard staticExec("nim c convert_schema.nim")
let
rawJsonData = staticExec("./convert_schema service-2.json")
jsonData = marshal.to[JsonNode](rawJsonData)
let s = newService(jsonData)
echo(repr(s))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment