12345678module remote910import crypto.hmac11import crypto.sha25612import time1314pub struct Signer {15pub:16 access_key string17 secret_key string18 region string19 service string20 21 22 23 sign_payload bool = true24}252627282930pub fn (signer Signer) sign(method string, url string, query string, headers map[string]string, body []u8, now time.Time) map[string]string {31 parts := split_url(url)32 payload_hash := sha256_hex(body)33 amz_date := amz_timestamp(now)3435 mut signed := map[string]string{}36 for name, value in headers {37 signed[name.to_lower().trim_space()] = value.trim_space()38 }39 signed['host'] = parts.host40 signed['x-amz-date'] = amz_date41 if signer.sign_payload {42 signed['x-amz-content-sha256'] = payload_hash43 }4445 mut names := signed.keys()46 names.sort()47 mut canonical_headers := ''48 for name in names {49 canonical_headers += '${name}:${signed[name]}\n'50 }51 signed_names := names.join(';')52 canonical_request := '${method}\n${parts.path}\n${canonical_query(query)}\n${canonical_headers}\n${signed_names}\n${payload_hash}'5354 date := amz_date[..8]55 scope := '${date}/${signer.region}/${signer.service}/aws4_request'56 string_to_sign := 'AWS4-HMAC-SHA256\n${amz_date}\n${scope}\n${sha256_hex(canonical_request.bytes())}'57 signature :=58 hmac.new(signing_key(signer, date), string_to_sign.bytes(), sha256.sum256, 64).hex()5960 mut out := signed.clone()61 out['authorization'] = 'AWS4-HMAC-SHA256 Credential=${signer.access_key}/${scope}, SignedHeaders=${signed_names}, Signature=${signature}'62 return out63}64656667fn signing_key(signer Signer, date string) []u8 {68 k_date := hmac.new('AWS4${signer.secret_key}'.bytes(), date.bytes(), sha256.sum256, 64)69 k_region := hmac.new(k_date, signer.region.bytes(), sha256.sum256, 64)70 k_service := hmac.new(k_region, signer.service.bytes(), sha256.sum256, 64)71 return hmac.new(k_service, 'aws4_request'.bytes(), sha256.sum256, 64)72}737475fn sha256_hex(data []u8) string {76 return sha256.sum256(data).hex()77}787980pub fn amz_timestamp(moment time.Time) string {81 return '${moment.year}${pad2(moment.month)}${pad2(moment.day)}T${pad2(moment.hour)}${pad2(moment.minute)}${pad2(moment.second)}Z'82}8384fn pad2(value int) string {85 return if value < 10 { '0${value}' } else { '${value}' }86}8788pub struct UrlParts {89pub:90 host string 91 path string 92}9394959697pub fn split_url(url string) UrlParts {98 mut rest := url99 if at := rest.index('://') {100 rest = rest[at + 3..]101 }102 mut host := rest103 mut path := '/'104 if slash := rest.index('/') {105 host = rest[..slash]106 path = rest[slash..]107 }108 if question := path.index('?') {109 path = path[..question]110 }111 return UrlParts{112 host: host113 path: path114 }115}116117118119pub fn canonical_query(query string) string {120 if query == '' {121 return ''122 }123 mut pairs := []string{}124 for part in query.split('&') {125 if part == '' {126 continue127 }128 if at := part.index('=') {129 pairs << '${encode(part[..at], false)}=${encode(part[at + 1..], false)}'130 } else {131 pairs << '${encode(part, false)}='132 }133 }134 pairs.sort()135 return pairs.join('&')136}137138139140pub fn encode(text string, keep_slash bool) string {141 mut out := ''142 for byte in text.bytes() {143 if (byte >= `A` && byte <= `Z`) || (byte >= `a` && byte <= `z`)144 || (byte >= `0` && byte <= `9`) || byte in [`-`, `_`, `.`, `~`]145 || (keep_slash && byte == `/`) {146 out += byte.ascii_str()147 } else {148 out += '%${byte.hex().to_upper()}'149 }150 }151 return out152}