├── .gitignore ├── .luacheckrc ├── LICENSE ├── NEWS ├── README.md ├── aws └── v4.lua ├── laws-scm-0.rockspec └── spec └── v4_spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.rock 2 | spec/aws-sig-v4-test-suite/ 3 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec"] = { 3 | std = "+busted"; 4 | } 5 | max_line_length = false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ChatID 4 | 2015-2018 James Callahan 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 0.1 - 2018-08-24 2 | 3 | - Tag of code after long-dormant state 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LAWS 2 | 3 | A [Lua](http://www.lua.org/) library for interacting with [AWS (Amazon Web Services)](https://aws.amazon.com/). 4 | 5 | ## Dependencies 6 | 7 | - [luaossl](http://25thandclement.com/~william/projects/luaossl.html) 8 | 9 | 10 | ## Modules 11 | 12 | ### `aws.v4` 13 | 14 | Implements the [AWS Signature v4](http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) algorithm. 15 | This can be used to sign requests to a variety of Amazon services. 16 | 17 | #### `aws.v4.canonicalise_path` 18 | 19 | ```lua 20 | CanonicalURI = canonicalise_path(path) 21 | ``` 22 | 23 | Canonicalises the given string argument according to the v4 signature rules for paths: 24 | 25 | - Removes redundant path components, e.g. `..` and `.` 26 | - Ensures (only) the correct characters are escaped 27 | 28 | 29 | #### `aws.v4.canonicalise_query_string` 30 | 31 | ```lua 32 | CanonicalQueryString = canonicalise_query_string(query) 33 | ``` 34 | 35 | Canonicalises the given string argument according to the v4 signature rules for query strings: 36 | 37 | - Ensures (only) the correct characters are escaped 38 | - Query arguments are sorted 39 | 40 | 41 | #### `aws.v4.derive_signing_key` 42 | 43 | ```lua 44 | kSigning = derive_signing_key(kSecret, Date, Region, Service) 45 | ``` 46 | 47 | Derives the signing key as specified in http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 48 | Uses a SHA256 HMAC. 49 | 50 | 51 | #### `aws.v4.prepare_request` 52 | 53 | ```lua 54 | request, extra = prepare_request(tbl) 55 | ``` 56 | 57 | Prepares a signed HTTP(S) request. 58 | 59 | Currently does not support Authentication parameters in the query string. 60 | 61 | 62 | The fields of `tbl` are: 63 | 64 | Field | Type | Description 65 | ------------------------|-------------------------------|----------------------- 66 | `.Domain` | `string\|nil` | The domain to hit, defaults to `"amazonaws.com"` 67 | `.Region` | `string` | The Amazon region. e.g. `"us-east-1"` 68 | `.Service` | `string` | The Amazon service. e.g. `"kinesis"` 69 | `.method` | `string` | e.g. `"GET"` 70 | `.CanonicalURI` | `string\|nil` | A pre-canonicalised path. If not provided `.path` will be canonicalised 71 | `.path` | `string\|nil` | The path of your request. Not used if `.CanonicalURI` is specified. If not provided or empty, defaults to `"/"` 72 | `.CanonicalQueryString` | `string\|nil` | A pre-canonicalised query-string. If not provided `.query` will be canonicalised 73 | `.query` | `string\|nil` | The query string for your request. Not used if `.CanonicalQueryString` is specified. Not required. 74 | `.headers` | `{string:string\|false}\|nil` | Table of string key/value pairs to use as signed headers in the request. A value of `false` prevents the header from being automatically filled in. Keys will be normalised to Title-Case; behaviour on duplicates is undefined. 75 | `.body` | `string\|nil` | Request body 76 | `.AccessKey` | `string` | Your Amazon access key. 77 | `.SigningKey` | `string\|nil` | A pre-derived signing key, if one is not provided, it will be derived from `.SecretKey` 78 | `.SecretKey` | `string\|nil` | Your Amazon secret key, only required if you do not specify `.SigningKey` 79 | `.timestamp` | `number\|nil` | When request should be signed for as a unix timestamp. defaults to `os.time()` i.e. now 80 | `.tls` | `boolean\|nil` | A truthy value will use HTTPS, false indicates HTTP. defaults to `true` 81 | `.port` | `number\|nil` | TCP port for request. defaults to `443` for HTTPS or `80` for HTTP 82 | 83 | The returned `request` object has fields: 84 | 85 | Field | Type | Description 86 | ------------|-------------------|----------------------------------------------- 87 | `.url` | `string` | URL representing the Host, 88 | `.host` | `string` | Host to connect to. e.g. `"kinesis.us-east-1.amazonaws.com"` 89 | `.port` | `number` | TCP port to connect to. e.g. `443` 90 | `.tls` | `boolean` | Should SSL/TLS be used to connect? e.g. `true` 91 | `.method` | `string` | e.g. `"GET"` 92 | `.target` | `string` | The target (the part between method and `HTTP/`) ready to be used in a HTTP request. i.e. path and query string 93 | `.headers` | `{string:string}` | Table of headers 94 | `.body` | `string\|nil` | `.body` as passed in 95 | 96 | The returned `extra` object has intermediary values calculated by the AWS v4 signing algorithm. 97 | 98 | Field | Type 99 | ------------------------|---------- 100 | `.CanonicalURI` | `string` 101 | `.CanonicalQueryString` | `string` 102 | `.SignedHeaders` | `string` 103 | `.CanonicalHeaders` | `string` 104 | `.CanonicalRequest` | `string` 105 | `.StringToSign` | `string` 106 | `.SigningKey` | `string` 107 | `.Signature` | `string` 108 | `.Authorization` | `string` 109 | 110 | 111 | If not provided, certain headers will be added. 112 | To prevent a header from getting added, provide it in `.headers` with a boolean value of `false` 113 | 114 | - `Host` 115 | - `X-Amz-Date` 116 | - `Authorization` 117 | 118 | 119 | ##### Example 120 | 121 | This example prepares a HTTP request to Amazon Kinesis to [ListStreams](http://docs.aws.amazon.com/kinesis/latest/APIReference/API_ListStreams.html) 122 | 123 | ```lua 124 | local prepare_request = require "aws.v4".prepare_request 125 | local request, extra = prepare_request { 126 | Region = "us-east-1"; 127 | Service = "kinesis"; 128 | method = "POST"; 129 | headers = { 130 | ["X-Amz-Target"] = "Kinesis_20131202.ListStreams"; 131 | ["Content-Type"] = "application/x-amz-json-1.1"; 132 | }; 133 | body = '{}'; 134 | AccessKey = "AKIDEXAMPLE"; 135 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /aws/v4.lua: -------------------------------------------------------------------------------- 1 | -- Performs AWSv4 Signing 2 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html 3 | 4 | local openssl_hmac_new = require "openssl.hmac".new 5 | local openssl_digest_new = require "openssl.digest".new 6 | 7 | local Algorithm = "AWS4-HMAC-SHA256" 8 | local function HMAC(key, msg) 9 | return openssl_hmac_new(key, "sha256"):final(msg) 10 | end 11 | local function Hash(str) 12 | return openssl_digest_new("sha256"):final(str) 13 | end 14 | local HexEncode do -- From prosody's util.hex 15 | local char_to_hex = {}; 16 | for i = 0, 255 do 17 | local char = string.char(i) 18 | local hex = string.format("%02x", i) 19 | char_to_hex[char] = hex 20 | end 21 | function HexEncode(str) 22 | return (str:gsub(".", char_to_hex)) 23 | end 24 | end 25 | 26 | local function percent_encode(char) 27 | return string.format("%%%02X", string.byte(char)) 28 | end 29 | 30 | local urldecode do 31 | local function urldecode_helper(c) 32 | return string.char(tonumber(c, 16)) 33 | end 34 | function urldecode(str) 35 | return (str:gsub("%%(%x%x)", urldecode_helper)) 36 | end 37 | end 38 | 39 | -- Based on Trim 12 from http://lua-users.org/wiki/StringTrim 40 | local function Trimall(s) 41 | local from = s:match"^%s*()" 42 | return from > #s and "" or s:match(".*%S", from):gsub(" *", " ") 43 | end 44 | 45 | local function canonicalise_path(path) 46 | local segments = {} 47 | for segment in path:gmatch("/([^/]*)") do 48 | if segment == "" or segment == "." then -- luacheck: ignore 542 49 | -- nothing 50 | elseif segment == ".." then 51 | -- intentionally discards components at top level 52 | segments[#segments] = nil 53 | else 54 | segments[#segments+1] = urldecode(segment):gsub("[^%w%-%._~]", percent_encode) 55 | end 56 | end 57 | local len = #segments 58 | if len == 0 then return "/" end 59 | -- If there was a slash on the end, keep it there. 60 | if path:sub(-1, -1) == "/" then 61 | len = len + 1 62 | segments[len] = "" 63 | end 64 | segments[0] = "" 65 | segments = table.concat(segments, "/", 0, len) 66 | return segments 67 | end 68 | 69 | local function canonicalise_query_string(query) 70 | local q = {} 71 | for key, val in query:gmatch("([^&=]+)=?([^&]*)") do 72 | key = urldecode(key):gsub("[^%w%-%._~]", percent_encode) 73 | val = urldecode(val):gsub("[^%w%-%._~]", percent_encode) 74 | q[#q+1] = key .. "=" .. val 75 | end 76 | table.sort(q) 77 | return table.concat(q, "&") 78 | end 79 | 80 | local function derive_signing_key(kSecret, Date, Region, Service) 81 | local kDate = HMAC("AWS4" .. kSecret, Date) 82 | local kRegion = HMAC(kDate, Region) 83 | local kService = HMAC(kRegion, Service) 84 | local kSigning = HMAC(kService, "aws4_request") 85 | return kSigning 86 | end 87 | 88 | local function prepare_awsv4_request(tbl) 89 | local Domain = tbl.Domain or "amazonaws.com" 90 | assert(type(Domain) == "string", "bad field 'Domain' (string or nil expected)") 91 | local Region = tbl.Region 92 | assert(type(Region) == "string", "bad field 'Region' (string expected)") 93 | local Service = tbl.Service 94 | assert(type(Service) == "string", "bad field 'Service' (string expected)") 95 | local HTTPRequestMethod = tbl.method 96 | assert(type(HTTPRequestMethod) == "string", "bad field 'method' (string expected)") 97 | local CanonicalURI = tbl.CanonicalURI 98 | local path = tbl.path 99 | if CanonicalURI == nil and path ~= nil then 100 | assert(type(path) == "string", "bad field 'path' (string or nil expected)") 101 | CanonicalURI = canonicalise_path(path) 102 | elseif CanonicalURI == nil or CanonicalURI == "" then 103 | CanonicalURI = "/" 104 | end 105 | assert(type(CanonicalURI) == "string", "bad field 'CanonicalURI' (string or nil expected)") 106 | local CanonicalQueryString = tbl.CanonicalQueryString 107 | local query = tbl.query 108 | if CanonicalQueryString == nil and query ~= nil then 109 | assert(type(query) == "string", "bad field 'query' (string or nil expected)") 110 | CanonicalQueryString = canonicalise_query_string(query) 111 | end 112 | assert(type(CanonicalQueryString) == "string" or CanonicalQueryString == nil, "bad field 'CanonicalQueryString' (string or nil expected)") 113 | local req_headers = tbl.headers or {} 114 | assert(type(req_headers) == "table", "bad field 'headers' (table or nil expected)") 115 | local RequestPayload = tbl.body 116 | assert(type(RequestPayload) == "string" or RequestPayload == nil, "bad field 'body' (string or nil expected)") 117 | local AccessKey = tbl.AccessKey 118 | assert(type(Region) == "string", "bad field 'AccessKey' (string expected)") 119 | local SigningKey = tbl.SigningKey 120 | assert(type(SigningKey) == "string" or SigningKey == nil, "bad field 'SigningKey' (string or nil expected)") 121 | local SecretKey 122 | if SigningKey == nil then 123 | SecretKey = tbl.SecretKey 124 | if SecretKey == nil then 125 | assert(SecretKey, "either 'SigningKey' or 'SecretKey' must be provided") 126 | end 127 | assert(type(SecretKey) == "string", "bad field 'SecretKey' (string expected)") 128 | end 129 | local timestamp = tbl.timestamp or os.time() 130 | assert(type(timestamp) == "number", "bad field 'timestamp' (number or nil expected)") 131 | local tls = tbl.tls 132 | if tls == nil then tls = true end 133 | local port = tbl.port or (tls and 443 or 80) 134 | assert(type(port) == "number", "bad field 'port' (string or nil expected)") 135 | 136 | local RequestDate = os.date("!%Y%m%dT%H%M%SZ", timestamp) 137 | local Date = os.date("!%Y%m%d", timestamp) 138 | 139 | local host = Service .. "." .. Region .. "." .. Domain 140 | local host_header do -- If the "standard" port is not in use, the port should be added to the Host header 141 | local with_port 142 | if tls then 143 | with_port = port ~= 443 144 | else 145 | with_port = port ~= 80 146 | end 147 | if with_port then 148 | host_header = string.format("%s:%d", host, port) 149 | else 150 | host_header = host 151 | end 152 | end 153 | 154 | local headers = { 155 | ["X-Amz-Date"] = RequestDate; 156 | Host = host_header; 157 | } 158 | local add_auth_header = true 159 | for k, v in pairs(req_headers) do 160 | assert(type(k) == "string", "bad field 'headers' (only string keys allowed)") 161 | k = k:gsub("%f[^%z-]%w", string.upper) -- convert to standard header title case 162 | if k == "Authorization" then 163 | add_auth_header = false 164 | elseif v == false then -- don't allow a default value for this header 165 | v = nil 166 | end 167 | headers[k] = v 168 | end 169 | 170 | -- Task 1: Create a Canonical Request For Signature Version 4 171 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 172 | local CanonicalHeaders, SignedHeaders do 173 | -- We structure this code in a way so that we only have to sort once. 174 | CanonicalHeaders, SignedHeaders = {}, {} 175 | local i = 0 176 | for name, value in pairs(headers) do 177 | if value then -- ignore headers with 'false', they are used to override defaults 178 | i = i + 1 179 | local name_lower = name:lower() 180 | SignedHeaders[i] = name_lower 181 | assert(CanonicalHeaders[name_lower] == nil, "header collision") 182 | CanonicalHeaders[name_lower] = Trimall(value) 183 | end 184 | end 185 | table.sort(SignedHeaders) 186 | for j=1, i do 187 | local name = SignedHeaders[j] 188 | local value = CanonicalHeaders[name] 189 | CanonicalHeaders[j] = name .. ":" .. value .. "\n" 190 | end 191 | SignedHeaders = table.concat(SignedHeaders, ";", 1, i) 192 | CanonicalHeaders = table.concat(CanonicalHeaders, nil, 1, i) 193 | end 194 | local CanonicalRequest = 195 | HTTPRequestMethod .. '\n' .. 196 | CanonicalURI .. '\n' .. 197 | (CanonicalQueryString or "") .. '\n' .. 198 | CanonicalHeaders .. '\n' .. 199 | SignedHeaders .. '\n' .. 200 | HexEncode(Hash(RequestPayload or "")) 201 | local HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest)) 202 | -- Task 2: Create a String to Sign for Signature Version 4 203 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 204 | local CredentialScope = Date .. "/" .. Region .. "/" .. Service .. "/aws4_request" 205 | local StringToSign = 206 | Algorithm .. '\n' .. 207 | RequestDate .. '\n' .. 208 | CredentialScope .. '\n' .. 209 | HashedCanonicalRequest 210 | -- Task 3: Calculate the AWS Signature Version 4 211 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 212 | if SigningKey == nil then 213 | SigningKey = derive_signing_key(SecretKey, Date, Region, Service) 214 | end 215 | local Signature = HexEncode(HMAC(SigningKey, StringToSign)) 216 | -- Task 4: Add the Signing Information to the Request 217 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html 218 | local Authorization = Algorithm 219 | .. " Credential=" .. AccessKey .."/" .. CredentialScope 220 | .. ", SignedHeaders=" .. SignedHeaders 221 | .. ", Signature=" .. Signature 222 | if add_auth_header then 223 | headers.Authorization = Authorization 224 | end 225 | 226 | local target = path or CanonicalURI 227 | if query or CanonicalQueryString then 228 | target = target .. "?" .. (query or CanonicalQueryString) 229 | end 230 | local scheme = tls and "https" or "http" 231 | local url = scheme .. "://" .. host_header .. target 232 | 233 | return { 234 | url = url; 235 | host = host; 236 | port = port; 237 | tls = tls; 238 | method = HTTPRequestMethod; 239 | target = target; 240 | headers = headers; 241 | body = RequestPayload; 242 | }, { 243 | CanonicalURI = CanonicalURI; 244 | CanonicalQueryString = CanonicalQueryString; 245 | SignedHeaders = SignedHeaders; 246 | CanonicalHeaders = CanonicalHeaders; 247 | CanonicalRequest = CanonicalRequest; 248 | StringToSign = StringToSign; 249 | SigningKey = SigningKey; 250 | Signature = Signature; 251 | Authorization = Authorization; 252 | } 253 | end 254 | 255 | return { 256 | canonicalise_path = canonicalise_path; 257 | canonicalise_query_string = canonicalise_query_string; 258 | derive_signing_key = derive_signing_key; 259 | prepare_request = prepare_awsv4_request; 260 | } 261 | -------------------------------------------------------------------------------- /laws-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "laws" 2 | version = "scm-0" 3 | 4 | description = { 5 | summary = "AWS Library for Lua"; 6 | homepage = "https://github.com/james-callahan/laws"; 7 | license = "MIT"; 8 | } 9 | 10 | dependencies = { 11 | "lua"; 12 | "luaossl"; 13 | } 14 | 15 | source = { 16 | url = "git+https://github.com/james-callahan/laws.git"; 17 | } 18 | 19 | build = { 20 | type = "builtin"; 21 | modules = { 22 | ["aws.v4"] = "aws/v4.lua"; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /spec/v4_spec.lua: -------------------------------------------------------------------------------- 1 | local awsv4 = require "aws.v4" 2 | 3 | describe("Pass AWSv4 test suite", function() 4 | -- The test suite was obtained from https://docs.aws.amazon.com/general/latest/gr/samples/aws-sig-v4-test-suite.zip 5 | -- sha1sum of the test suite .zip is c66ea06884dad4348558e93380c8bf91e8551a6f 6 | -- Information about it can be found at http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html 7 | local function read_file(path) 8 | local fd, err, code = io.open(path, "rb") 9 | if fd == nil then 10 | if code == 2 then -- not found 11 | return nil 12 | else 13 | error(err) 14 | end 15 | end 16 | local contents = assert(fd:read"*a") 17 | fd:close() 18 | return contents 19 | end 20 | 21 | local dir = "./spec/aws-sig-v4-test-suite/" 22 | for _, test_name in ipairs { 23 | "get-header-key-duplicate"; 24 | "get-header-value-multiline"; 25 | "get-header-value-order"; 26 | "get-header-value-trim"; 27 | "get-unreserved"; 28 | "get-utf8"; 29 | "get-vanilla"; 30 | "get-vanilla-empty-query-key"; 31 | "get-vanilla-query"; 32 | "get-vanilla-query-order-key"; 33 | "get-vanilla-query-order-key-case"; 34 | "get-vanilla-query-order-value"; 35 | "get-vanilla-query-unreserved"; 36 | "get-vanilla-utf8-query"; 37 | "normalize-path/get-relative"; 38 | "normalize-path/get-relative-relative"; 39 | "normalize-path/get-slash"; 40 | "normalize-path/get-slash-dot-slash"; 41 | "normalize-path/get-slashes"; 42 | "normalize-path/get-slash-pointless-dot"; 43 | "normalize-path/get-space"; 44 | "post-header-key-case"; 45 | "post-header-key-sort"; 46 | "post-header-value-case"; 47 | "post-sts-token/post-sts-header-after"; 48 | "post-sts-token/post-sts-header-before"; 49 | "post-vanilla"; 50 | "post-vanilla-empty-query-value"; 51 | "post-vanilla-query"; 52 | "post-x-www-form-urlencoded"; 53 | "post-x-www-form-urlencoded-parameters"; 54 | } do 55 | local filename = test_name:match("[^/]+$") 56 | local req = read_file(dir..test_name.."/"..filename..".req") 57 | ;(req and it or pending)("passes test #" .. test_name:gsub("%-", "_"), function() 58 | local creq = read_file(dir..test_name.."/"..filename..".creq") 59 | local sts = read_file(dir..test_name.."/"..filename..".sts") 60 | local authz = read_file(dir..test_name.."/"..filename..".authz") 61 | -- local sreq = read_file(dir..test_name..".sreq") 62 | 63 | local method, target, pos = req:match("^(%S+) (.-) HTTP/1.[01]()") 64 | local path, query = target:match("([^%?]*)%??(.*)") 65 | path = path or target 66 | local str_headers, body = req:match("^\n(.-\n)\n(.*)", pos) 67 | str_headers = str_headers or req:sub(pos+1) .. "\n" 68 | local headers = { 69 | ["X-Amz-Date"] = false; -- test suite uses normal date header instead 70 | } 71 | for k, v in str_headers:gmatch("([^%s:]+): *(.-)\n%f[%z%w]") do 72 | v = v:gsub("\n +", ",") 73 | local old_v = headers[k:lower()] 74 | if old_v then 75 | v = old_v .. "," .. v 76 | end 77 | headers[k:lower()] = v 78 | end 79 | if body == "" then body = nil end 80 | local http_req, interim = awsv4.prepare_request { 81 | Region = "us-east-1"; 82 | Service = "service"; 83 | AccessKey = "AKIDEXAMPLE"; 84 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 85 | method = method; 86 | path = path; 87 | query = query; 88 | headers = headers; 89 | body = body; 90 | timestamp = 1440938160; -- Timestamp used by all tests 91 | } 92 | assert.truthy(http_req) 93 | assert.same(creq, interim.CanonicalRequest) 94 | assert.same(sts, interim.StringToSign) 95 | assert.same(authz, interim.Authorization) 96 | end) 97 | end 98 | end) 99 | 100 | describe("Path canonicalisation is correct", function() 101 | it("Handles . and .. correctly", function() 102 | assert.same("/", awsv4.canonicalise_path "/") 103 | assert.same("/", awsv4.canonicalise_path "/.") 104 | assert.same("/", awsv4.canonicalise_path "/./foo/../") 105 | assert.same("/bar", awsv4.canonicalise_path "/foo/../foo/./../bar") 106 | assert.same("/..foo", awsv4.canonicalise_path "/..foo") 107 | assert.same("/bar/.foo", awsv4.canonicalise_path "/./bar/.foo") 108 | end) 109 | it("Can't get above top dir", function() 110 | assert.same("/foo", awsv4.canonicalise_path "/../foo") 111 | end) 112 | it("Escapes correctly", function() 113 | assert.same("/foo", awsv4.canonicalise_path "/%66oo") 114 | -- for aws, space must be %20, not + 115 | assert.same("/%20", awsv4.canonicalise_path "/ ") 116 | end) 117 | end) 118 | 119 | describe("port is handled correctly", function() 120 | it("is appended to Host", function() 121 | assert.same("host.us-east-1.amazonaws.com", (awsv4.prepare_request { 122 | Region = "us-east-1"; 123 | Service = "host"; 124 | AccessKey = "AKIDEXAMPLE"; 125 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 126 | method = "GET"; 127 | }).headers.Host) 128 | assert.same("host.us-east-1.amazonaws.com", (awsv4.prepare_request { 129 | Region = "us-east-1"; 130 | Service = "host"; 131 | AccessKey = "AKIDEXAMPLE"; 132 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 133 | method = "GET"; 134 | port = 443; 135 | }).headers.Host) 136 | assert.same("host.us-east-1.amazonaws.com", (awsv4.prepare_request { 137 | Region = "us-east-1"; 138 | Service = "host"; 139 | AccessKey = "AKIDEXAMPLE"; 140 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 141 | method = "GET"; 142 | port = 80; 143 | tls = false 144 | }).headers.Host) 145 | assert.same("host.us-east-1.amazonaws.com:1234", (awsv4.prepare_request { 146 | Region = "us-east-1"; 147 | Service = "host"; 148 | AccessKey = "AKIDEXAMPLE"; 149 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 150 | method = "GET"; 151 | port = 1234; 152 | }).headers.Host) 153 | assert.same("host.us-east-1.amazonaws.com:8080", (awsv4.prepare_request { 154 | Region = "us-east-1"; 155 | Service = "host"; 156 | AccessKey = "AKIDEXAMPLE"; 157 | SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; 158 | method = "GET"; 159 | port = 8080; 160 | tls = false 161 | }).headers.Host) 162 | end) 163 | end) 164 | --------------------------------------------------------------------------------