@blamejs/core 0.7.18 → 0.7.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +427 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +350 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +399 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/fetch-metadata.js +129 -0
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +85 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +121 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +7 -5
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,488 +1,488 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Azure Blob Storage protocol adapter — Shared Key auth (account-level).
|
|
4
|
-
*
|
|
5
|
-
* Auth: HMAC-SHA256 over a canonicalized string-to-sign (different format
|
|
6
|
-
* from AWS SigV4 — Azure has its own signing scheme). Signature is
|
|
7
|
-
* base64-encoded; Authorization header: "SharedKey <account>:<sig>".
|
|
8
|
-
*
|
|
9
|
-
* Endpoint: https://<account>.blob.core.windows.net (override via
|
|
10
|
-
* config.endpoint for Azurite emulator / Azure Stack / private endpoints).
|
|
11
|
-
*
|
|
12
|
-
* Config:
|
|
13
|
-
* {
|
|
14
|
-
* accountName: 'mystorage' // required
|
|
15
|
-
* accountKey: '<base64 storage key>' // required (REST shared key)
|
|
16
|
-
* container: 'my-container' // required
|
|
17
|
-
* endpoint: 'https://...' // optional override
|
|
18
|
-
* apiVersion: '2024-08-04' // x-ms-version header
|
|
19
|
-
* timeoutMs: C.TIME.seconds(30)
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* Reference:
|
|
23
|
-
* https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
|
|
24
|
-
* https://learn.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api
|
|
25
|
-
*
|
|
26
|
-
* Scope notes:
|
|
27
|
-
* - Auth: Shared Key only. SAS tokens (Shared Access Signatures) and
|
|
28
|
-
* Azure AD OAuth2 are not implemented; this covers the common
|
|
29
|
-
* server-to-storage case with rotated keys.
|
|
30
|
-
* - PutBlock + PutBlockList (multipart for >256MB blobs) is not
|
|
31
|
-
* implemented; uploads above that ceiling will fail at the API.
|
|
32
|
-
*/
|
|
33
|
-
var nodeCrypto = require("crypto");
|
|
34
|
-
var { URL } = require("url");
|
|
35
|
-
var { Readable } = require("stream");
|
|
36
|
-
var safeXml = require("../parsers/safe-xml");
|
|
37
|
-
var sharedRequest = require("./http-request");
|
|
38
|
-
var C = require("../constants");
|
|
39
|
-
var requestHelpers = require("../request-helpers");
|
|
40
|
-
var { ObjectStoreError } = require("../framework-error");
|
|
41
|
-
var time = require("../time");
|
|
42
|
-
var safeUrl = require("../safe-url");
|
|
43
|
-
|
|
44
|
-
// Azure Blob list responses are commonly multi-thousand-key paginated
|
|
45
|
-
// payloads — well above the parser's default 1 MiB / 10K element ceilings.
|
|
46
|
-
var LIST_PARSE_OPTS = {
|
|
47
|
-
maxBytes: C.BYTES.mib(8),
|
|
48
|
-
maxElements: C.BYTES.bytes(50000),
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Internal URL builder — endpoint + path string from validated config.
|
|
52
|
-
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
53
|
-
// apply uniformly. Cap is generous since SAS-presigned URLs carry many
|
|
54
|
-
// query params.
|
|
55
|
-
function _internalUrl(input, allowedProtocols) {
|
|
56
|
-
return safeUrl.parse(input, {
|
|
57
|
-
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
58
|
-
errorClass: ObjectStoreError,
|
|
59
|
-
maxUrlLength: C.BYTES.kib(32),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function _arrayify(value) {
|
|
64
|
-
if (value == null) return [];
|
|
65
|
-
return Array.isArray(value) ? value : [value];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
var DEFAULT_API_VERSION = "2024-08-04";
|
|
69
|
-
|
|
70
|
-
// Service SAS expiry bounds. Azure doesn't enforce a hard max, but
|
|
71
|
-
// matching the SigV4 / V4 7-day cap keeps semantics uniform across
|
|
72
|
-
// backends and reflects best practice (long-lived SAS tokens are
|
|
73
|
-
// effectively credentials).
|
|
74
|
-
var SAS_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
|
|
75
|
-
var SAS_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
|
|
76
|
-
var SAS_MIN_EXPIRES_SECONDS = 1;
|
|
77
|
-
|
|
78
|
-
var _err = ObjectStoreError.factory;
|
|
79
|
-
|
|
80
|
-
var _httpRequest = sharedRequest;
|
|
81
|
-
|
|
82
|
-
// ---- Shared Key signing ----
|
|
83
|
-
//
|
|
84
|
-
// StringToSign for Blob (Shared Key):
|
|
85
|
-
// VERB + "\n" +
|
|
86
|
-
// Content-Encoding + "\n" +
|
|
87
|
-
// Content-Language + "\n" +
|
|
88
|
-
// Content-Length + "\n" +
|
|
89
|
-
// Content-MD5 + "\n" +
|
|
90
|
-
// Content-Type + "\n" +
|
|
91
|
-
// Date + "\n" +
|
|
92
|
-
// If-Modified-Since + "\n" +
|
|
93
|
-
// If-Match + "\n" +
|
|
94
|
-
// If-None-Match + "\n" +
|
|
95
|
-
// If-Unmodified-Since + "\n" +
|
|
96
|
-
// Range + "\n" +
|
|
97
|
-
// CanonicalizedHeaders +
|
|
98
|
-
// CanonicalizedResource
|
|
99
|
-
//
|
|
100
|
-
// CanonicalizedHeaders: x-ms-* headers, lowercased, sorted, "key:value\n"
|
|
101
|
-
// CanonicalizedResource: "/<account>/<container>/<blob>" + sorted query
|
|
102
|
-
// params each on their own line as "param:value\n"
|
|
103
|
-
|
|
104
|
-
function buildStringToSign(opts) {
|
|
105
|
-
var headers = opts.headers || {};
|
|
106
|
-
var url = opts.url instanceof URL ? opts.url : _internalUrl(opts.url);
|
|
107
|
-
|
|
108
|
-
var canonicalHeaders = (function () {
|
|
109
|
-
var pairs = [];
|
|
110
|
-
Object.keys(headers).forEach(function (k) {
|
|
111
|
-
var lk = k.toLowerCase();
|
|
112
|
-
if (lk.indexOf("x-ms-") === 0) {
|
|
113
|
-
pairs.push([lk, String(headers[k]).trim().replace(/\s+/g, " ")]);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
pairs.sort(function (a, b) { return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; });
|
|
117
|
-
return pairs.map(function (p) { return p[0] + ":" + p[1]; }).join("\n");
|
|
118
|
-
})();
|
|
119
|
-
|
|
120
|
-
var canonicalResource = (function () {
|
|
121
|
-
// /<account>/<rest of path>
|
|
122
|
-
// Plus sorted query params, each "name:value\n"
|
|
123
|
-
var resourcePath = "/" + opts.accountName + url.pathname;
|
|
124
|
-
var paramPairs = [];
|
|
125
|
-
url.searchParams.forEach(function (v, k) {
|
|
126
|
-
paramPairs.push([k.toLowerCase(), v]);
|
|
127
|
-
});
|
|
128
|
-
paramPairs.sort(function (a, b) {
|
|
129
|
-
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
130
|
-
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
|
|
131
|
-
});
|
|
132
|
-
// Group by name; multiple values comma-separated per Azure rules
|
|
133
|
-
var grouped = {};
|
|
134
|
-
paramPairs.forEach(function (p) {
|
|
135
|
-
if (!grouped[p[0]]) grouped[p[0]] = [];
|
|
136
|
-
grouped[p[0]].push(p[1]);
|
|
137
|
-
});
|
|
138
|
-
// Azure Shared Key canonicalized resource: sorted query param names,
|
|
139
|
-
// each on its own line as `name:value` — Azure-spec, not JSON
|
|
140
|
-
// canonicalization.
|
|
141
|
-
var groupedKeys = [];
|
|
142
|
-
for (var gk in grouped) groupedKeys.push(gk);
|
|
143
|
-
groupedKeys.sort();
|
|
144
|
-
var queryLines = groupedKeys.map(function (name) {
|
|
145
|
-
return "\n" + name + ":" + grouped[name].join(",");
|
|
146
|
-
}).join("");
|
|
147
|
-
return resourcePath + queryLines;
|
|
148
|
-
})();
|
|
149
|
-
|
|
150
|
-
return [
|
|
151
|
-
opts.method.toUpperCase(),
|
|
152
|
-
headers["Content-Encoding"] || "",
|
|
153
|
-
headers["Content-Language"] || "",
|
|
154
|
-
headers["Content-Length"] && headers["Content-Length"] !== "0"
|
|
155
|
-
? headers["Content-Length"] : "", // "0" → empty (Azure quirk)
|
|
156
|
-
headers["Content-MD5"] || "",
|
|
157
|
-
headers["Content-Type"] || "",
|
|
158
|
-
"", // Date line — empty when x-ms-date is set
|
|
159
|
-
headers["If-Modified-Since"] || "",
|
|
160
|
-
headers["If-Match"] || "",
|
|
161
|
-
headers["If-None-Match"] || "",
|
|
162
|
-
headers["If-Unmodified-Since"] || "",
|
|
163
|
-
headers["Range"] || "",
|
|
164
|
-
canonicalHeaders,
|
|
165
|
-
canonicalResource,
|
|
166
|
-
].join("\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function signRequest(opts) {
|
|
170
|
-
var headers = Object.assign({}, opts.headers || {});
|
|
171
|
-
if (!headers["x-ms-version"]) headers["x-ms-version"] = opts.apiVersion || DEFAULT_API_VERSION;
|
|
172
|
-
if (!headers["x-ms-date"]) headers["x-ms-date"] = new Date().toUTCString();
|
|
173
|
-
|
|
174
|
-
var url = opts.url instanceof URL ? opts.url : _internalUrl(opts.url);
|
|
175
|
-
if (!headers["host"]) headers["host"] = url.host;
|
|
176
|
-
|
|
177
|
-
var sts = buildStringToSign({
|
|
178
|
-
method: opts.method,
|
|
179
|
-
url: url,
|
|
180
|
-
headers: headers,
|
|
181
|
-
accountName: opts.accountName,
|
|
182
|
-
});
|
|
183
|
-
var keyBytes = Buffer.from(opts.accountKey, "base64");
|
|
184
|
-
var signature = nodeCrypto.createHmac("sha256", keyBytes).update(sts, "utf8").digest("base64");
|
|
185
|
-
headers["Authorization"] = "SharedKey " + opts.accountName + ":" + signature;
|
|
186
|
-
|
|
187
|
-
return { headers: headers, stringToSign: sts, signature: signature };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ---- Public adapter factory ----
|
|
191
|
-
|
|
192
|
-
function create(config) {
|
|
193
|
-
if (!config) throw new Error("azure-blob protocol requires config");
|
|
194
|
-
if (!config.accountName) throw new Error("azure-blob: accountName is required");
|
|
195
|
-
if (!config.accountKey) throw new Error("azure-blob: accountKey is required");
|
|
196
|
-
if (!config.container) throw new Error("azure-blob: container is required");
|
|
197
|
-
|
|
198
|
-
var endpoint = config.endpoint || ("https://" + config.accountName + ".blob.core.windows.net");
|
|
199
|
-
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
200
|
-
var apiVersion = config.apiVersion || DEFAULT_API_VERSION;
|
|
201
|
-
var timeoutMs = config.timeoutMs;
|
|
202
|
-
// HTTPS-only by default — real Azure is always HTTPS. Operators with
|
|
203
|
-
// an Azurite emulator endpoint opt in via config.allowedProtocols.
|
|
204
|
-
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
205
|
-
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
206
|
-
safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
|
|
207
|
-
var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
|
|
208
|
-
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
209
|
-
|
|
210
|
-
function _blobUrl(key, params) {
|
|
211
|
-
var u = _internalUrl(endpoint + "/" + config.container + "/" + key, allowedProtocols);
|
|
212
|
-
if (params) {
|
|
213
|
-
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
214
|
-
}
|
|
215
|
-
return u;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function _containerUrl(params) {
|
|
219
|
-
var u = _internalUrl(endpoint + "/" + config.container, allowedProtocols);
|
|
220
|
-
if (params) {
|
|
221
|
-
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
222
|
-
}
|
|
223
|
-
return u;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function _signed(method, url, headers) {
|
|
227
|
-
var s = signRequest({
|
|
228
|
-
method: method,
|
|
229
|
-
url: url,
|
|
230
|
-
headers: headers || {},
|
|
231
|
-
accountName: config.accountName,
|
|
232
|
-
accountKey: config.accountKey,
|
|
233
|
-
apiVersion: apiVersion,
|
|
234
|
-
});
|
|
235
|
-
return s.headers;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function put(key, body, opts) {
|
|
239
|
-
var url = _blobUrl(key);
|
|
240
|
-
var buf = Buffer.isBuffer(body) ? body : Buffer.from(typeof body === "string" ? body : "", "utf8");
|
|
241
|
-
var contentType = (opts && opts.contentType) || "application/octet-stream";
|
|
242
|
-
var headers = _signed("PUT", url, {
|
|
243
|
-
"Content-Type": contentType,
|
|
244
|
-
"Content-Length": String(buf.length),
|
|
245
|
-
"x-ms-blob-type": "BlockBlob",
|
|
246
|
-
});
|
|
247
|
-
return _httpRequest("PUT", url, headers, buf, reqOpts).then(function (res) {
|
|
248
|
-
return { size: buf.length, etag: res.headers.etag };
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// get(key, opts?) — opts forwarded to Azure as conditional / range
|
|
253
|
-
// headers (Range / x-ms-range, If-None-Match, If-Match,
|
|
254
|
-
// If-Modified-Since, If-Unmodified-Since). Returns the body buffer;
|
|
255
|
-
// operators wanting status + response headers call getResponse().
|
|
256
|
-
function get(key, opts) {
|
|
257
|
-
return getResponse(key, opts).then(function (r) { return r.body; });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function getStream(key, opts) { return Readable.from(get(key, opts)); }
|
|
261
|
-
|
|
262
|
-
function getResponse(key, opts) {
|
|
263
|
-
opts = opts || {};
|
|
264
|
-
var url = _blobUrl(key);
|
|
265
|
-
var extraHeaders = {};
|
|
266
|
-
if (opts.range) {
|
|
267
|
-
extraHeaders["x-ms-range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
|
|
268
|
-
}
|
|
269
|
-
if (opts.ifNoneMatch) extraHeaders["If-None-Match"] = opts.ifNoneMatch;
|
|
270
|
-
if (opts.ifMatch) extraHeaders["If-Match"] = opts.ifMatch;
|
|
271
|
-
if (opts.ifModifiedSince) extraHeaders["If-Modified-Since"] = opts.ifModifiedSince;
|
|
272
|
-
if (opts.ifUnmodifiedSince) extraHeaders["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
|
|
273
|
-
var headers = _signed("GET", url, extraHeaders);
|
|
274
|
-
return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
|
|
275
|
-
return {
|
|
276
|
-
statusCode: res.statusCode,
|
|
277
|
-
body: res.body,
|
|
278
|
-
etag: res.headers && res.headers.etag,
|
|
279
|
-
lastModified: res.headers && res.headers["last-modified"]
|
|
280
|
-
? Date.parse(res.headers["last-modified"]) : null,
|
|
281
|
-
contentRange: res.headers && res.headers["content-range"] || null,
|
|
282
|
-
size: res.headers && res.headers["content-length"]
|
|
283
|
-
? parseInt(res.headers["content-length"], 10) : null,
|
|
284
|
-
contentType: res.headers && res.headers["content-type"] || null,
|
|
285
|
-
};
|
|
286
|
-
}, function (err) {
|
|
287
|
-
if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
|
|
288
|
-
return {
|
|
289
|
-
statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
|
|
290
|
-
body: null, etag: null, lastModified: null,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
throw err;
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function head(key) {
|
|
298
|
-
var url = _blobUrl(key);
|
|
299
|
-
var headers = _signed("HEAD", url, {});
|
|
300
|
-
return _httpRequest("HEAD", url, headers, null, reqOpts).then(function (res) {
|
|
301
|
-
return {
|
|
302
|
-
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
303
|
-
etag: res.headers.etag,
|
|
304
|
-
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function deleteKey(key) {
|
|
310
|
-
var url = _blobUrl(key);
|
|
311
|
-
var headers = _signed("DELETE", url, {});
|
|
312
|
-
return _httpRequest("DELETE", url, headers, null, reqOpts).then(
|
|
313
|
-
function () { return true; },
|
|
314
|
-
function (e) { if (e.statusCode === 404) return false; throw e; }
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function list(prefix, opts) {
|
|
319
|
-
opts = opts || {};
|
|
320
|
-
var params = { restype: "container", comp: "list" };
|
|
321
|
-
if (prefix) params.prefix = prefix;
|
|
322
|
-
if (opts.maxResults) params.maxresults = String(opts.maxResults);
|
|
323
|
-
if (opts.continuationToken) params.marker = opts.continuationToken;
|
|
324
|
-
var url = _containerUrl(params);
|
|
325
|
-
var headers = _signed("GET", url, {});
|
|
326
|
-
return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
|
|
327
|
-
var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
|
|
328
|
-
var result = doc.EnumerationResults || {};
|
|
329
|
-
var blobsContainer = result.Blobs || {};
|
|
330
|
-
var blobs = _arrayify(blobsContainer.Blob);
|
|
331
|
-
var items = blobs.map(function (b) {
|
|
332
|
-
var props = b.Properties || {};
|
|
333
|
-
var size = props["Content-Length"];
|
|
334
|
-
var lm = props["Last-Modified"];
|
|
335
|
-
return {
|
|
336
|
-
key: b.Name,
|
|
337
|
-
size: size != null ? parseInt(size, 10) : null,
|
|
338
|
-
lastModified: lm ? Date.parse(lm) : null,
|
|
339
|
-
};
|
|
340
|
-
}).filter(function (it) { return it.key; });
|
|
341
|
-
// <NextMarker/> (self-closing) parses to "" — falsy. A real token
|
|
342
|
-
// is a non-empty string, which is truthy.
|
|
343
|
-
var marker = (typeof result.NextMarker === "string") ? result.NextMarker : "";
|
|
344
|
-
return {
|
|
345
|
-
items: items,
|
|
346
|
-
truncated: marker.length > 0,
|
|
347
|
-
continuationToken: marker.length > 0 ? marker : null,
|
|
348
|
-
};
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Service SAS (Shared Access Signature) generator for blob endpoints.
|
|
353
|
-
// The string-to-sign layout is fixed by API version — see Azure docs
|
|
354
|
-
// "Create a service SAS". We sign with the account key (HMAC-SHA256
|
|
355
|
-
// over the canonical string) and emit the token as URL query params.
|
|
356
|
-
function _buildSasToken(permissions, opts) {
|
|
357
|
-
var expiresIn = opts.expiresIn != null ? opts.expiresIn : SAS_DEFAULT_EXPIRES_SECONDS;
|
|
358
|
-
if (typeof expiresIn !== "number" ||
|
|
359
|
-
expiresIn < SAS_MIN_EXPIRES_SECONDS ||
|
|
360
|
-
expiresIn > SAS_MAX_EXPIRES_SECONDS) {
|
|
361
|
-
throw _err("INVALID_EXPIRES",
|
|
362
|
-
"presigned URL: expiresIn must be a number of seconds between " +
|
|
363
|
-
SAS_MIN_EXPIRES_SECONDS + " and " + SAS_MAX_EXPIRES_SECONDS +
|
|
364
|
-
" (7 days)", true);
|
|
365
|
-
}
|
|
366
|
-
var nowDate = opts.date || new Date();
|
|
367
|
-
var expiry = new Date(nowDate.getTime() + C.TIME.seconds(expiresIn));
|
|
368
|
-
// Azure accepts ISO 8601 with second precision; strip ms.
|
|
369
|
-
var signedExpiry = time.toIso8601NoMs(expiry);
|
|
370
|
-
var signedStart = ""; // omitted = SAS valid immediately
|
|
371
|
-
var signedVersion = apiVersion;
|
|
372
|
-
var signedResource = "b"; // blob
|
|
373
|
-
var signedProtocol = "https";
|
|
374
|
-
var canonicalizedResource = "/blob/" + config.accountName + "/" +
|
|
375
|
-
config.container + "/" + opts.key;
|
|
376
|
-
var signedContentType = opts.contentType || "";
|
|
377
|
-
|
|
378
|
-
// String-to-sign layout for Service SAS (blob), API version 2018-11-09+:
|
|
379
|
-
// signedPermissions \n signedStart \n signedExpiry \n
|
|
380
|
-
// canonicalizedResource \n signedIdentifier \n signedIP \n
|
|
381
|
-
// signedProtocol \n signedVersion \n signedResource \n
|
|
382
|
-
// signedSnapshotTime \n signedEncryptionScope \n
|
|
383
|
-
// rscc \n rscd \n rsce \n rscl \n rsct
|
|
384
|
-
var stringToSign = [
|
|
385
|
-
permissions,
|
|
386
|
-
signedStart,
|
|
387
|
-
signedExpiry,
|
|
388
|
-
canonicalizedResource,
|
|
389
|
-
"", // signedIdentifier
|
|
390
|
-
"", // signedIP
|
|
391
|
-
signedProtocol,
|
|
392
|
-
signedVersion,
|
|
393
|
-
signedResource,
|
|
394
|
-
"", // signedSnapshotTime
|
|
395
|
-
"", // signedEncryptionScope
|
|
396
|
-
"", // rscc — Cache-Control
|
|
397
|
-
"", // rscd — Content-Disposition
|
|
398
|
-
"", // rsce — Content-Encoding
|
|
399
|
-
"", // rscl — Content-Language
|
|
400
|
-
signedContentType, // rsct — Content-Type (signed when supplied)
|
|
401
|
-
].join("\n");
|
|
402
|
-
|
|
403
|
-
var keyBuf = Buffer.from(config.accountKey, "base64");
|
|
404
|
-
var signature = nodeCrypto.createHmac("sha256", keyBuf)
|
|
405
|
-
.update(stringToSign, "utf8").digest("base64");
|
|
406
|
-
|
|
407
|
-
var sas = new URLSearchParams();
|
|
408
|
-
sas.set("sv", signedVersion);
|
|
409
|
-
sas.set("sr", signedResource);
|
|
410
|
-
sas.set("sp", permissions);
|
|
411
|
-
sas.set("se", signedExpiry);
|
|
412
|
-
sas.set("spr", signedProtocol);
|
|
413
|
-
if (signedContentType) sas.set("rsct", signedContentType);
|
|
414
|
-
sas.set("sig", signature);
|
|
415
|
-
|
|
416
|
-
return { sas: sas.toString(), expiresAt: expiry.getTime() };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function _presign(method, permissions, opts) {
|
|
420
|
-
opts = opts || {};
|
|
421
|
-
if (!opts.key || typeof opts.key !== "string") {
|
|
422
|
-
throw _err("INVALID_KEY", "presigned URL: key is required", true);
|
|
423
|
-
}
|
|
424
|
-
if (opts.key.indexOf("\0") !== -1) {
|
|
425
|
-
throw _err("INVALID_KEY", "null byte in key", true);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
var token = _buildSasToken(permissions, opts);
|
|
429
|
-
var url = _internalUrl(endpoint + "/" + config.container + "/" + opts.key + "?" + token.sas, allowedProtocols);
|
|
430
|
-
|
|
431
|
-
var clientHeaders = {};
|
|
432
|
-
if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
|
|
433
|
-
if (method === "PUT") clientHeaders["x-ms-blob-type"] = "BlockBlob";
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
url: url.toString(),
|
|
437
|
-
method: method,
|
|
438
|
-
headers: clientHeaders,
|
|
439
|
-
expiresAt: token.expiresAt,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function presignedUploadUrl(opts) { return _presign("PUT", "cw", opts); }
|
|
444
|
-
function presignedDownloadUrl(opts) { return _presign("GET", "r", opts); }
|
|
445
|
-
|
|
446
|
-
// Azure SAS has no equivalent of S3 / GCS POST policy with a
|
|
447
|
-
// content-length-range constraint — the SAS spec carries permissions
|
|
448
|
-
// / start / expiry / IP / protocol / resource-content-headers but
|
|
449
|
-
// no body-size cap. Returning a PUT URL under the POST-policy name
|
|
450
|
-
// would be a silent shape mismatch (operators wiring an HTML form
|
|
451
|
-
// expecting multipart fields get a PUT URL with no fields), so the
|
|
452
|
-
// azure-blob backend refuses cleanly.
|
|
453
|
-
//
|
|
454
|
-
// For strict server-side body-size enforcement on Azure: use
|
|
455
|
-
// presignedUploadUrl + a server-side post-upload HEAD that deletes
|
|
456
|
-
// and 4xxs the requester if Content-Length > limit.
|
|
457
|
-
function presignedUploadPolicy(_opts) {
|
|
458
|
-
throw _err("PRESIGN_NOT_SUPPORTED",
|
|
459
|
-
"azure-blob backend does not support presigned upload policies — " +
|
|
460
|
-
"Azure SAS has no body-size cap. Use presignedUploadUrl + a server-side " +
|
|
461
|
-
"HEAD-and-delete check, or switch to an S3 / GCS-compatible backend.",
|
|
462
|
-
true);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return {
|
|
466
|
-
protocol: "azure-blob",
|
|
467
|
-
endpoint: endpoint,
|
|
468
|
-
container: config.container,
|
|
469
|
-
accountName: config.accountName,
|
|
470
|
-
put: put,
|
|
471
|
-
get: get,
|
|
472
|
-
getStream: getStream,
|
|
473
|
-
getResponse: getResponse,
|
|
474
|
-
head: head,
|
|
475
|
-
delete: deleteKey,
|
|
476
|
-
list: list,
|
|
477
|
-
presignedUploadUrl: presignedUploadUrl,
|
|
478
|
-
presignedDownloadUrl: presignedDownloadUrl,
|
|
479
|
-
presignedUploadPolicy: presignedUploadPolicy,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
module.exports = {
|
|
484
|
-
create: create,
|
|
485
|
-
signRequest: signRequest,
|
|
486
|
-
buildStringToSign: buildStringToSign,
|
|
487
|
-
DEFAULT_API_VERSION: DEFAULT_API_VERSION,
|
|
488
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Azure Blob Storage protocol adapter — Shared Key auth (account-level).
|
|
4
|
+
*
|
|
5
|
+
* Auth: HMAC-SHA256 over a canonicalized string-to-sign (different format
|
|
6
|
+
* from AWS SigV4 — Azure has its own signing scheme). Signature is
|
|
7
|
+
* base64-encoded; Authorization header: "SharedKey <account>:<sig>".
|
|
8
|
+
*
|
|
9
|
+
* Endpoint: https://<account>.blob.core.windows.net (override via
|
|
10
|
+
* config.endpoint for Azurite emulator / Azure Stack / private endpoints).
|
|
11
|
+
*
|
|
12
|
+
* Config:
|
|
13
|
+
* {
|
|
14
|
+
* accountName: 'mystorage' // required
|
|
15
|
+
* accountKey: '<base64 storage key>' // required (REST shared key)
|
|
16
|
+
* container: 'my-container' // required
|
|
17
|
+
* endpoint: 'https://...' // optional override
|
|
18
|
+
* apiVersion: '2024-08-04' // x-ms-version header
|
|
19
|
+
* timeoutMs: C.TIME.seconds(30)
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Reference:
|
|
23
|
+
* https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
|
|
24
|
+
* https://learn.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api
|
|
25
|
+
*
|
|
26
|
+
* Scope notes:
|
|
27
|
+
* - Auth: Shared Key only. SAS tokens (Shared Access Signatures) and
|
|
28
|
+
* Azure AD OAuth2 are not implemented; this covers the common
|
|
29
|
+
* server-to-storage case with rotated keys.
|
|
30
|
+
* - PutBlock + PutBlockList (multipart for >256MB blobs) is not
|
|
31
|
+
* implemented; uploads above that ceiling will fail at the API.
|
|
32
|
+
*/
|
|
33
|
+
var nodeCrypto = require("crypto");
|
|
34
|
+
var { URL } = require("url");
|
|
35
|
+
var { Readable } = require("stream");
|
|
36
|
+
var safeXml = require("../parsers/safe-xml");
|
|
37
|
+
var sharedRequest = require("./http-request");
|
|
38
|
+
var C = require("../constants");
|
|
39
|
+
var requestHelpers = require("../request-helpers");
|
|
40
|
+
var { ObjectStoreError } = require("../framework-error");
|
|
41
|
+
var time = require("../time");
|
|
42
|
+
var safeUrl = require("../safe-url");
|
|
43
|
+
|
|
44
|
+
// Azure Blob list responses are commonly multi-thousand-key paginated
|
|
45
|
+
// payloads — well above the parser's default 1 MiB / 10K element ceilings.
|
|
46
|
+
var LIST_PARSE_OPTS = {
|
|
47
|
+
maxBytes: C.BYTES.mib(8),
|
|
48
|
+
maxElements: C.BYTES.bytes(50000),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Internal URL builder — endpoint + path string from validated config.
|
|
52
|
+
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
53
|
+
// apply uniformly. Cap is generous since SAS-presigned URLs carry many
|
|
54
|
+
// query params.
|
|
55
|
+
function _internalUrl(input, allowedProtocols) {
|
|
56
|
+
return safeUrl.parse(input, {
|
|
57
|
+
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
58
|
+
errorClass: ObjectStoreError,
|
|
59
|
+
maxUrlLength: C.BYTES.kib(32),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _arrayify(value) {
|
|
64
|
+
if (value == null) return [];
|
|
65
|
+
return Array.isArray(value) ? value : [value];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var DEFAULT_API_VERSION = "2024-08-04";
|
|
69
|
+
|
|
70
|
+
// Service SAS expiry bounds. Azure doesn't enforce a hard max, but
|
|
71
|
+
// matching the SigV4 / V4 7-day cap keeps semantics uniform across
|
|
72
|
+
// backends and reflects best practice (long-lived SAS tokens are
|
|
73
|
+
// effectively credentials).
|
|
74
|
+
var SAS_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
|
|
75
|
+
var SAS_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
|
|
76
|
+
var SAS_MIN_EXPIRES_SECONDS = 1;
|
|
77
|
+
|
|
78
|
+
var _err = ObjectStoreError.factory;
|
|
79
|
+
|
|
80
|
+
var _httpRequest = sharedRequest;
|
|
81
|
+
|
|
82
|
+
// ---- Shared Key signing ----
|
|
83
|
+
//
|
|
84
|
+
// StringToSign for Blob (Shared Key):
|
|
85
|
+
// VERB + "\n" +
|
|
86
|
+
// Content-Encoding + "\n" +
|
|
87
|
+
// Content-Language + "\n" +
|
|
88
|
+
// Content-Length + "\n" +
|
|
89
|
+
// Content-MD5 + "\n" +
|
|
90
|
+
// Content-Type + "\n" +
|
|
91
|
+
// Date + "\n" +
|
|
92
|
+
// If-Modified-Since + "\n" +
|
|
93
|
+
// If-Match + "\n" +
|
|
94
|
+
// If-None-Match + "\n" +
|
|
95
|
+
// If-Unmodified-Since + "\n" +
|
|
96
|
+
// Range + "\n" +
|
|
97
|
+
// CanonicalizedHeaders +
|
|
98
|
+
// CanonicalizedResource
|
|
99
|
+
//
|
|
100
|
+
// CanonicalizedHeaders: x-ms-* headers, lowercased, sorted, "key:value\n"
|
|
101
|
+
// CanonicalizedResource: "/<account>/<container>/<blob>" + sorted query
|
|
102
|
+
// params each on their own line as "param:value\n"
|
|
103
|
+
|
|
104
|
+
function buildStringToSign(opts) {
|
|
105
|
+
var headers = opts.headers || {};
|
|
106
|
+
var url = opts.url instanceof URL ? opts.url : _internalUrl(opts.url);
|
|
107
|
+
|
|
108
|
+
var canonicalHeaders = (function () {
|
|
109
|
+
var pairs = [];
|
|
110
|
+
Object.keys(headers).forEach(function (k) {
|
|
111
|
+
var lk = k.toLowerCase();
|
|
112
|
+
if (lk.indexOf("x-ms-") === 0) {
|
|
113
|
+
pairs.push([lk, String(headers[k]).trim().replace(/\s+/g, " ")]);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
pairs.sort(function (a, b) { return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; });
|
|
117
|
+
return pairs.map(function (p) { return p[0] + ":" + p[1]; }).join("\n");
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
var canonicalResource = (function () {
|
|
121
|
+
// /<account>/<rest of path>
|
|
122
|
+
// Plus sorted query params, each "name:value\n"
|
|
123
|
+
var resourcePath = "/" + opts.accountName + url.pathname;
|
|
124
|
+
var paramPairs = [];
|
|
125
|
+
url.searchParams.forEach(function (v, k) {
|
|
126
|
+
paramPairs.push([k.toLowerCase(), v]);
|
|
127
|
+
});
|
|
128
|
+
paramPairs.sort(function (a, b) {
|
|
129
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
130
|
+
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
|
|
131
|
+
});
|
|
132
|
+
// Group by name; multiple values comma-separated per Azure rules
|
|
133
|
+
var grouped = {};
|
|
134
|
+
paramPairs.forEach(function (p) {
|
|
135
|
+
if (!grouped[p[0]]) grouped[p[0]] = [];
|
|
136
|
+
grouped[p[0]].push(p[1]);
|
|
137
|
+
});
|
|
138
|
+
// Azure Shared Key canonicalized resource: sorted query param names,
|
|
139
|
+
// each on its own line as `name:value` — Azure-spec, not JSON
|
|
140
|
+
// canonicalization.
|
|
141
|
+
var groupedKeys = [];
|
|
142
|
+
for (var gk in grouped) groupedKeys.push(gk);
|
|
143
|
+
groupedKeys.sort();
|
|
144
|
+
var queryLines = groupedKeys.map(function (name) {
|
|
145
|
+
return "\n" + name + ":" + grouped[name].join(",");
|
|
146
|
+
}).join("");
|
|
147
|
+
return resourcePath + queryLines;
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
opts.method.toUpperCase(),
|
|
152
|
+
headers["Content-Encoding"] || "",
|
|
153
|
+
headers["Content-Language"] || "",
|
|
154
|
+
headers["Content-Length"] && headers["Content-Length"] !== "0"
|
|
155
|
+
? headers["Content-Length"] : "", // "0" → empty (Azure quirk)
|
|
156
|
+
headers["Content-MD5"] || "",
|
|
157
|
+
headers["Content-Type"] || "",
|
|
158
|
+
"", // Date line — empty when x-ms-date is set
|
|
159
|
+
headers["If-Modified-Since"] || "",
|
|
160
|
+
headers["If-Match"] || "",
|
|
161
|
+
headers["If-None-Match"] || "",
|
|
162
|
+
headers["If-Unmodified-Since"] || "",
|
|
163
|
+
headers["Range"] || "",
|
|
164
|
+
canonicalHeaders,
|
|
165
|
+
canonicalResource,
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function signRequest(opts) {
|
|
170
|
+
var headers = Object.assign({}, opts.headers || {});
|
|
171
|
+
if (!headers["x-ms-version"]) headers["x-ms-version"] = opts.apiVersion || DEFAULT_API_VERSION;
|
|
172
|
+
if (!headers["x-ms-date"]) headers["x-ms-date"] = new Date().toUTCString();
|
|
173
|
+
|
|
174
|
+
var url = opts.url instanceof URL ? opts.url : _internalUrl(opts.url);
|
|
175
|
+
if (!headers["host"]) headers["host"] = url.host;
|
|
176
|
+
|
|
177
|
+
var sts = buildStringToSign({
|
|
178
|
+
method: opts.method,
|
|
179
|
+
url: url,
|
|
180
|
+
headers: headers,
|
|
181
|
+
accountName: opts.accountName,
|
|
182
|
+
});
|
|
183
|
+
var keyBytes = Buffer.from(opts.accountKey, "base64");
|
|
184
|
+
var signature = nodeCrypto.createHmac("sha256", keyBytes).update(sts, "utf8").digest("base64");
|
|
185
|
+
headers["Authorization"] = "SharedKey " + opts.accountName + ":" + signature;
|
|
186
|
+
|
|
187
|
+
return { headers: headers, stringToSign: sts, signature: signature };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- Public adapter factory ----
|
|
191
|
+
|
|
192
|
+
function create(config) {
|
|
193
|
+
if (!config) throw new Error("azure-blob protocol requires config");
|
|
194
|
+
if (!config.accountName) throw new Error("azure-blob: accountName is required");
|
|
195
|
+
if (!config.accountKey) throw new Error("azure-blob: accountKey is required");
|
|
196
|
+
if (!config.container) throw new Error("azure-blob: container is required");
|
|
197
|
+
|
|
198
|
+
var endpoint = config.endpoint || ("https://" + config.accountName + ".blob.core.windows.net");
|
|
199
|
+
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
200
|
+
var apiVersion = config.apiVersion || DEFAULT_API_VERSION;
|
|
201
|
+
var timeoutMs = config.timeoutMs;
|
|
202
|
+
// HTTPS-only by default — real Azure is always HTTPS. Operators with
|
|
203
|
+
// an Azurite emulator endpoint opt in via config.allowedProtocols.
|
|
204
|
+
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
205
|
+
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
206
|
+
safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
|
|
207
|
+
var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
|
|
208
|
+
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
209
|
+
|
|
210
|
+
function _blobUrl(key, params) {
|
|
211
|
+
var u = _internalUrl(endpoint + "/" + config.container + "/" + key, allowedProtocols);
|
|
212
|
+
if (params) {
|
|
213
|
+
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
214
|
+
}
|
|
215
|
+
return u;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _containerUrl(params) {
|
|
219
|
+
var u = _internalUrl(endpoint + "/" + config.container, allowedProtocols);
|
|
220
|
+
if (params) {
|
|
221
|
+
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
222
|
+
}
|
|
223
|
+
return u;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _signed(method, url, headers) {
|
|
227
|
+
var s = signRequest({
|
|
228
|
+
method: method,
|
|
229
|
+
url: url,
|
|
230
|
+
headers: headers || {},
|
|
231
|
+
accountName: config.accountName,
|
|
232
|
+
accountKey: config.accountKey,
|
|
233
|
+
apiVersion: apiVersion,
|
|
234
|
+
});
|
|
235
|
+
return s.headers;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function put(key, body, opts) {
|
|
239
|
+
var url = _blobUrl(key);
|
|
240
|
+
var buf = Buffer.isBuffer(body) ? body : Buffer.from(typeof body === "string" ? body : "", "utf8");
|
|
241
|
+
var contentType = (opts && opts.contentType) || "application/octet-stream";
|
|
242
|
+
var headers = _signed("PUT", url, {
|
|
243
|
+
"Content-Type": contentType,
|
|
244
|
+
"Content-Length": String(buf.length),
|
|
245
|
+
"x-ms-blob-type": "BlockBlob",
|
|
246
|
+
});
|
|
247
|
+
return _httpRequest("PUT", url, headers, buf, reqOpts).then(function (res) {
|
|
248
|
+
return { size: buf.length, etag: res.headers.etag };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// get(key, opts?) — opts forwarded to Azure as conditional / range
|
|
253
|
+
// headers (Range / x-ms-range, If-None-Match, If-Match,
|
|
254
|
+
// If-Modified-Since, If-Unmodified-Since). Returns the body buffer;
|
|
255
|
+
// operators wanting status + response headers call getResponse().
|
|
256
|
+
function get(key, opts) {
|
|
257
|
+
return getResponse(key, opts).then(function (r) { return r.body; });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getStream(key, opts) { return Readable.from(get(key, opts)); }
|
|
261
|
+
|
|
262
|
+
function getResponse(key, opts) {
|
|
263
|
+
opts = opts || {};
|
|
264
|
+
var url = _blobUrl(key);
|
|
265
|
+
var extraHeaders = {};
|
|
266
|
+
if (opts.range) {
|
|
267
|
+
extraHeaders["x-ms-range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
|
|
268
|
+
}
|
|
269
|
+
if (opts.ifNoneMatch) extraHeaders["If-None-Match"] = opts.ifNoneMatch;
|
|
270
|
+
if (opts.ifMatch) extraHeaders["If-Match"] = opts.ifMatch;
|
|
271
|
+
if (opts.ifModifiedSince) extraHeaders["If-Modified-Since"] = opts.ifModifiedSince;
|
|
272
|
+
if (opts.ifUnmodifiedSince) extraHeaders["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
|
|
273
|
+
var headers = _signed("GET", url, extraHeaders);
|
|
274
|
+
return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
|
|
275
|
+
return {
|
|
276
|
+
statusCode: res.statusCode,
|
|
277
|
+
body: res.body,
|
|
278
|
+
etag: res.headers && res.headers.etag,
|
|
279
|
+
lastModified: res.headers && res.headers["last-modified"]
|
|
280
|
+
? Date.parse(res.headers["last-modified"]) : null,
|
|
281
|
+
contentRange: res.headers && res.headers["content-range"] || null,
|
|
282
|
+
size: res.headers && res.headers["content-length"]
|
|
283
|
+
? parseInt(res.headers["content-length"], 10) : null,
|
|
284
|
+
contentType: res.headers && res.headers["content-type"] || null,
|
|
285
|
+
};
|
|
286
|
+
}, function (err) {
|
|
287
|
+
if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
|
|
288
|
+
return {
|
|
289
|
+
statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
|
|
290
|
+
body: null, etag: null, lastModified: null,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
throw err;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function head(key) {
|
|
298
|
+
var url = _blobUrl(key);
|
|
299
|
+
var headers = _signed("HEAD", url, {});
|
|
300
|
+
return _httpRequest("HEAD", url, headers, null, reqOpts).then(function (res) {
|
|
301
|
+
return {
|
|
302
|
+
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
303
|
+
etag: res.headers.etag,
|
|
304
|
+
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function deleteKey(key) {
|
|
310
|
+
var url = _blobUrl(key);
|
|
311
|
+
var headers = _signed("DELETE", url, {});
|
|
312
|
+
return _httpRequest("DELETE", url, headers, null, reqOpts).then(
|
|
313
|
+
function () { return true; },
|
|
314
|
+
function (e) { if (e.statusCode === 404) return false; throw e; }
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function list(prefix, opts) {
|
|
319
|
+
opts = opts || {};
|
|
320
|
+
var params = { restype: "container", comp: "list" };
|
|
321
|
+
if (prefix) params.prefix = prefix;
|
|
322
|
+
if (opts.maxResults) params.maxresults = String(opts.maxResults);
|
|
323
|
+
if (opts.continuationToken) params.marker = opts.continuationToken;
|
|
324
|
+
var url = _containerUrl(params);
|
|
325
|
+
var headers = _signed("GET", url, {});
|
|
326
|
+
return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
|
|
327
|
+
var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
|
|
328
|
+
var result = doc.EnumerationResults || {};
|
|
329
|
+
var blobsContainer = result.Blobs || {};
|
|
330
|
+
var blobs = _arrayify(blobsContainer.Blob);
|
|
331
|
+
var items = blobs.map(function (b) {
|
|
332
|
+
var props = b.Properties || {};
|
|
333
|
+
var size = props["Content-Length"];
|
|
334
|
+
var lm = props["Last-Modified"];
|
|
335
|
+
return {
|
|
336
|
+
key: b.Name,
|
|
337
|
+
size: size != null ? parseInt(size, 10) : null,
|
|
338
|
+
lastModified: lm ? Date.parse(lm) : null,
|
|
339
|
+
};
|
|
340
|
+
}).filter(function (it) { return it.key; });
|
|
341
|
+
// <NextMarker/> (self-closing) parses to "" — falsy. A real token
|
|
342
|
+
// is a non-empty string, which is truthy.
|
|
343
|
+
var marker = (typeof result.NextMarker === "string") ? result.NextMarker : "";
|
|
344
|
+
return {
|
|
345
|
+
items: items,
|
|
346
|
+
truncated: marker.length > 0,
|
|
347
|
+
continuationToken: marker.length > 0 ? marker : null,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Service SAS (Shared Access Signature) generator for blob endpoints.
|
|
353
|
+
// The string-to-sign layout is fixed by API version — see Azure docs
|
|
354
|
+
// "Create a service SAS". We sign with the account key (HMAC-SHA256
|
|
355
|
+
// over the canonical string) and emit the token as URL query params.
|
|
356
|
+
function _buildSasToken(permissions, opts) {
|
|
357
|
+
var expiresIn = opts.expiresIn != null ? opts.expiresIn : SAS_DEFAULT_EXPIRES_SECONDS;
|
|
358
|
+
if (typeof expiresIn !== "number" ||
|
|
359
|
+
expiresIn < SAS_MIN_EXPIRES_SECONDS ||
|
|
360
|
+
expiresIn > SAS_MAX_EXPIRES_SECONDS) {
|
|
361
|
+
throw _err("INVALID_EXPIRES",
|
|
362
|
+
"presigned URL: expiresIn must be a number of seconds between " +
|
|
363
|
+
SAS_MIN_EXPIRES_SECONDS + " and " + SAS_MAX_EXPIRES_SECONDS +
|
|
364
|
+
" (7 days)", true);
|
|
365
|
+
}
|
|
366
|
+
var nowDate = opts.date || new Date();
|
|
367
|
+
var expiry = new Date(nowDate.getTime() + C.TIME.seconds(expiresIn));
|
|
368
|
+
// Azure accepts ISO 8601 with second precision; strip ms.
|
|
369
|
+
var signedExpiry = time.toIso8601NoMs(expiry);
|
|
370
|
+
var signedStart = ""; // omitted = SAS valid immediately
|
|
371
|
+
var signedVersion = apiVersion;
|
|
372
|
+
var signedResource = "b"; // blob
|
|
373
|
+
var signedProtocol = "https";
|
|
374
|
+
var canonicalizedResource = "/blob/" + config.accountName + "/" +
|
|
375
|
+
config.container + "/" + opts.key;
|
|
376
|
+
var signedContentType = opts.contentType || "";
|
|
377
|
+
|
|
378
|
+
// String-to-sign layout for Service SAS (blob), API version 2018-11-09+:
|
|
379
|
+
// signedPermissions \n signedStart \n signedExpiry \n
|
|
380
|
+
// canonicalizedResource \n signedIdentifier \n signedIP \n
|
|
381
|
+
// signedProtocol \n signedVersion \n signedResource \n
|
|
382
|
+
// signedSnapshotTime \n signedEncryptionScope \n
|
|
383
|
+
// rscc \n rscd \n rsce \n rscl \n rsct
|
|
384
|
+
var stringToSign = [
|
|
385
|
+
permissions,
|
|
386
|
+
signedStart,
|
|
387
|
+
signedExpiry,
|
|
388
|
+
canonicalizedResource,
|
|
389
|
+
"", // signedIdentifier
|
|
390
|
+
"", // signedIP
|
|
391
|
+
signedProtocol,
|
|
392
|
+
signedVersion,
|
|
393
|
+
signedResource,
|
|
394
|
+
"", // signedSnapshotTime
|
|
395
|
+
"", // signedEncryptionScope
|
|
396
|
+
"", // rscc — Cache-Control
|
|
397
|
+
"", // rscd — Content-Disposition
|
|
398
|
+
"", // rsce — Content-Encoding
|
|
399
|
+
"", // rscl — Content-Language
|
|
400
|
+
signedContentType, // rsct — Content-Type (signed when supplied)
|
|
401
|
+
].join("\n");
|
|
402
|
+
|
|
403
|
+
var keyBuf = Buffer.from(config.accountKey, "base64");
|
|
404
|
+
var signature = nodeCrypto.createHmac("sha256", keyBuf)
|
|
405
|
+
.update(stringToSign, "utf8").digest("base64");
|
|
406
|
+
|
|
407
|
+
var sas = new URLSearchParams();
|
|
408
|
+
sas.set("sv", signedVersion);
|
|
409
|
+
sas.set("sr", signedResource);
|
|
410
|
+
sas.set("sp", permissions);
|
|
411
|
+
sas.set("se", signedExpiry);
|
|
412
|
+
sas.set("spr", signedProtocol);
|
|
413
|
+
if (signedContentType) sas.set("rsct", signedContentType);
|
|
414
|
+
sas.set("sig", signature);
|
|
415
|
+
|
|
416
|
+
return { sas: sas.toString(), expiresAt: expiry.getTime() };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _presign(method, permissions, opts) {
|
|
420
|
+
opts = opts || {};
|
|
421
|
+
if (!opts.key || typeof opts.key !== "string") {
|
|
422
|
+
throw _err("INVALID_KEY", "presigned URL: key is required", true);
|
|
423
|
+
}
|
|
424
|
+
if (opts.key.indexOf("\0") !== -1) {
|
|
425
|
+
throw _err("INVALID_KEY", "null byte in key", true);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
var token = _buildSasToken(permissions, opts);
|
|
429
|
+
var url = _internalUrl(endpoint + "/" + config.container + "/" + opts.key + "?" + token.sas, allowedProtocols);
|
|
430
|
+
|
|
431
|
+
var clientHeaders = {};
|
|
432
|
+
if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
|
|
433
|
+
if (method === "PUT") clientHeaders["x-ms-blob-type"] = "BlockBlob";
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
url: url.toString(),
|
|
437
|
+
method: method,
|
|
438
|
+
headers: clientHeaders,
|
|
439
|
+
expiresAt: token.expiresAt,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function presignedUploadUrl(opts) { return _presign("PUT", "cw", opts); }
|
|
444
|
+
function presignedDownloadUrl(opts) { return _presign("GET", "r", opts); }
|
|
445
|
+
|
|
446
|
+
// Azure SAS has no equivalent of S3 / GCS POST policy with a
|
|
447
|
+
// content-length-range constraint — the SAS spec carries permissions
|
|
448
|
+
// / start / expiry / IP / protocol / resource-content-headers but
|
|
449
|
+
// no body-size cap. Returning a PUT URL under the POST-policy name
|
|
450
|
+
// would be a silent shape mismatch (operators wiring an HTML form
|
|
451
|
+
// expecting multipart fields get a PUT URL with no fields), so the
|
|
452
|
+
// azure-blob backend refuses cleanly.
|
|
453
|
+
//
|
|
454
|
+
// For strict server-side body-size enforcement on Azure: use
|
|
455
|
+
// presignedUploadUrl + a server-side post-upload HEAD that deletes
|
|
456
|
+
// and 4xxs the requester if Content-Length > limit.
|
|
457
|
+
function presignedUploadPolicy(_opts) {
|
|
458
|
+
throw _err("PRESIGN_NOT_SUPPORTED",
|
|
459
|
+
"azure-blob backend does not support presigned upload policies — " +
|
|
460
|
+
"Azure SAS has no body-size cap. Use presignedUploadUrl + a server-side " +
|
|
461
|
+
"HEAD-and-delete check, or switch to an S3 / GCS-compatible backend.",
|
|
462
|
+
true);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
protocol: "azure-blob",
|
|
467
|
+
endpoint: endpoint,
|
|
468
|
+
container: config.container,
|
|
469
|
+
accountName: config.accountName,
|
|
470
|
+
put: put,
|
|
471
|
+
get: get,
|
|
472
|
+
getStream: getStream,
|
|
473
|
+
getResponse: getResponse,
|
|
474
|
+
head: head,
|
|
475
|
+
delete: deleteKey,
|
|
476
|
+
list: list,
|
|
477
|
+
presignedUploadUrl: presignedUploadUrl,
|
|
478
|
+
presignedDownloadUrl: presignedDownloadUrl,
|
|
479
|
+
presignedUploadPolicy: presignedUploadPolicy,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = {
|
|
484
|
+
create: create,
|
|
485
|
+
signRequest: signRequest,
|
|
486
|
+
buildStringToSign: buildStringToSign,
|
|
487
|
+
DEFAULT_API_VERSION: DEFAULT_API_VERSION,
|
|
488
|
+
};
|