import { toHex } from "@smithy/util-hex-encoding"; import { normalizeProvider } from "@smithy/util-middleware"; import { escapeUri } from "@smithy/util-uri-escape"; import { toUint8Array } from "@smithy/util-utf8"; import { getCanonicalQuery } from "./getCanonicalQuery"; import { iso8601 } from "./utilDate"; export class SignatureV4Base { service; regionProvider; credentialProvider; sha256; uriEscapePath; applyChecksum; constructor({ applyChecksum, credentials, region, service, sha256, uriEscapePath = true, }) { this.service = service; this.sha256 = sha256; this.uriEscapePath = uriEscapePath; this.applyChecksum = typeof applyChecksum === "boolean" ? applyChecksum : true; this.regionProvider = normalizeProvider(region); this.credentialProvider = normalizeProvider(credentials); } createCanonicalRequest(request, canonicalHeaders, payloadHash) { const sortedHeaders = Object.keys(canonicalHeaders).sort(); return `${request.method} ${this.getCanonicalPath(request)} ${getCanonicalQuery(request)} ${sortedHeaders.map((name) => `${name}:${canonicalHeaders[name]}`).join("\n")} ${sortedHeaders.join(";")} ${payloadHash}`; } async createStringToSign(longDate, credentialScope, canonicalRequest, algorithmIdentifier) { const hash = new this.sha256(); hash.update(toUint8Array(canonicalRequest)); const hashedRequest = await hash.digest(); return `${algorithmIdentifier} ${longDate} ${credentialScope} ${toHex(hashedRequest)}`; } getCanonicalPath({ path }) { if (this.uriEscapePath) { const normalizedPathSegments = []; for (const pathSegment of path.split("/")) { if (pathSegment?.length === 0) continue; if (pathSegment === ".") continue; if (pathSegment === "..") { normalizedPathSegments.pop(); } else { normalizedPathSegments.push(pathSegment); } } const normalizedPath = `${path?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${normalizedPathSegments.length > 0 && path?.endsWith("/") ? "/" : ""}`; const doubleEncoded = escapeUri(normalizedPath); return doubleEncoded.replace(/%2F/g, "/"); } return path; } validateResolvedCredentials(credentials) { if (typeof credentials !== "object" || typeof credentials.accessKeyId !== "string" || typeof credentials.secretAccessKey !== "string") { throw new Error("Resolved credential object is not valid"); } } formatDate(now) { const longDate = iso8601(now).replace(/[\-:]/g, ""); return { longDate, shortDate: longDate.slice(0, 8), }; } getCanonicalHeaderList(headers) { return Object.keys(headers).sort().join(";"); } }