UNPKG

5.47 kBPlain TextView Raw
1import * as crypto from 'crypto';
2
3export interface SignUrlOptions {
4 type: 's3' | 'gs',
5 expires: number,
6 keyName: string,
7 key: string,
8}
9
10export function signUrl(url: string, opts: SignUrlOptions) {
11 if (opts.type === 's3') {
12 return s3_sign_url(url, opts);
13 } else if (opts.type === 'gs') {
14 return gs_sign_url(url, opts);
15 } else {
16 throw new Error(`cdnSign does not support type ${opts.type} for now`);
17 }
18}
19
20/**
21 * Return a path signer based on a baseUrl and the sign option. This is the optimal way to sign many urls and should be typically used over the signUrl method.
22 *
23 * ```ts
24 * const signer = urlSigner('https://.../some/dir/');
25 * const signedUrls = ['my/image-001.jpeg', 'my/file.json'].map(signer);
26 * ```
27 *
28 * Performance benefits:
29 *
30 * - s3 - This takes full advantage of the aws 'directory signing' like urlSigner('https://.../some/dir/*', opts) will create one signature for the folder and apply it to each sub path.
31 * - gs - While google storage does not have the same capability, there are small benefits as well on some base64 object creation (not much though). However, because of GCP small key, the signature is much faster than s3 (about 10x)
32 */
33export function urlSigner(baseUrl: string, opts: SignUrlOptions): (pathFromBaseUrl: string) => string {
34 if (opts.type === 's3') {
35 return s3_urlSigner(baseUrl, opts);
36 } if (opts.type === 'gs') {
37 return gs_urlSigner(baseUrl, opts);
38 } else {
39 throw new Error('urlSigner only supported for s3');
40 }
41}
42
43
44//#region ---------- S3 Signer ----------
45function s3_urlSigner(baseUrl: string, opts: SignUrlOptions): (pathFromBaseUrl: string) => string {
46
47 const isWildPolicy = baseUrl.endsWith('*');
48
49 const [base_policyStringified, base_policyB64Norm] = isWildPolicy ? s3_makePolicy(baseUrl, opts.expires) : [undefined, undefined];
50 const base_signature = (base_policyStringified) ? s3_sign(base_policyStringified, opts.key) : undefined;
51 const base_url = isWildPolicy ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
52
53 return function (pathFromBaseUrl: string) {
54 let signature: string | undefined;
55 let policyStringified: string | undefined;
56 let policyB64Norm: string | undefined;
57
58 // If we have a base_signature and all, it means we had an a pattern signature (with *) s we can reuse
59 if (base_signature && base_policyB64Norm && base_policyStringified) {
60 policyStringified = base_policyStringified;
61 policyB64Norm = base_policyB64Norm;
62 signature = base_signature;
63 }
64 // otherwise needs to compute the signature
65 else {
66 [policyStringified, policyB64Norm] = s3_makePolicy(baseUrl + pathFromBaseUrl, opts.expires);
67 signature = s3_sign(policyStringified, opts.key);
68 }
69 return `${base_url}${pathFromBaseUrl}?Expires=${opts.expires}&Policy=${policyB64Norm}&Signature=${signature}&Key-Pair-Id=${opts.keyName}`;;
70 }
71}
72
73function s3_sign_url(url: string, opts: SignUrlOptions) {
74
75 const [policyStringified, policyB64Norm] = s3_makePolicy(url, opts.expires);
76 const signature = s3_sign(policyStringified, opts.key);
77 return `${url}?Expires=${opts.expires}&Policy=${policyB64Norm}&Signature=${signature}&Key-Pair-Id=${opts.keyName}`;;
78}
79
80function s3_makePolicy(url: string, expires: number) {
81 const policyObject = {
82 "Statement": [
83 {
84 "Resource": url,
85 "Condition": {
86 "DateLessThan": { "AWS:EpochTime": expires }
87 }
88 }
89 ]
90 };
91 const policyStringified = JSON.stringify(policyObject); //.replace(' ', '').replace('\n', '');
92 const policyB64Norm = s3_normalize_b64(Buffer.from(policyStringified).toString('base64'));
93
94 return [policyStringified, policyB64Norm];
95}
96
97function s3_sign(policyStringified: string, key: string) {
98 const signer = crypto.createSign('RSA-SHA1');
99 const signatureB64 = signer.update(policyStringified).sign(key, 'base64');
100 return s3_normalize_b64(signatureB64);
101}
102
103const S3_BASE64_REPLACE = { '+': '-', '/': '~', '=': '_' };
104function s3_normalize_b64(val: string) {
105 return val.replace(/[+/=]/g, c => (<any>S3_BASE64_REPLACE)[c]);
106}
107
108//#endregion ---------- /S3 Signer ----------
109
110
111//#region ---------- GCP Signer ----------
112
113const GCP_BASE64_REPLACE = { '+': '-', '/': '_', '=': '' };
114
115function gs_urlSigner(baseUrl: string, opts: SignUrlOptions): (pathFromBaseUrl: string) => string {
116 // just for API symetry, as gcp does not support wild policy signature
117 const isWildPolicy = baseUrl.endsWith('*');
118
119 const su_key_buff = Buffer.from(opts.key, 'base64');
120 const base_url = isWildPolicy ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
121
122 return function (pathFromBaseUrl: string) {
123 const url = base_url + pathFromBaseUrl;
124 // URL to sign
125 const urlToSign = `${url}?Expires=${opts.expires}&KeyName=${opts.keyName}`;
126 let signature = crypto.createHmac('sha1', su_key_buff).update(urlToSign).digest('base64');
127 signature = signature.replace(/[+/=]/g, c => (<any>GCP_BASE64_REPLACE)[c]);
128 // Add signature to urlToSign
129 return `${urlToSign}&Signature=${signature}`;
130 }
131}
132
133
134function gs_sign_url(url: string, opts: SignUrlOptions) {
135 // URL to sign
136 const urlToSign = `${url}?Expires=${opts.expires}&KeyName=${opts.keyName}`;
137
138 // Compute signature
139 let su_key_buff = Buffer.from(opts.key, 'base64');
140 let signature = crypto.createHmac('sha1', su_key_buff).update(urlToSign).digest('base64');
141 signature = signature.replace(/[+/=]/g, c => (<any>GCP_BASE64_REPLACE)[c]);
142
143 // Add signature to urlToSign
144 return urlToSign + `&Signature=${signature}`;
145}
146
147//#endregion ---------- /GCP Signer ----------
\No newline at end of file