codekingpro/portable-devtools
114k
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 var desc = Object.getOwnPropertyDescriptor(m, k);
5 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6 desc = { enumerable: true, get: function() { return m[k]; } };
7 }
8 Object.defineProperty(o, k2, desc);
9}) : (function(o, m, k, k2) {
10 if (k2 === undefined) k2 = k;
11 o[k2] = m[k];
12}));
13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14 Object.defineProperty(o, "default", { enumerable: true, value: v });
15}) : function(o, v) {
16 o["default"] = v;
17});
18var __importStar = (this && this.__importStar) || (function () {
19 var ownKeys = function(o) {
20 ownKeys = Object.getOwnPropertyNames || function (o) {
21 var ar = [];
22 for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23 return ar;
24 };
25 return ownKeys(o);
26 };
27 return function (mod) {
28 if (mod && mod.__esModule) return mod;
29 var result = {};
30 if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31 __setModuleDefault(result, mod);
32 return result;
33 };
34})();
35var __importDefault = (this && this.__importDefault) || function (mod) {
36 return (mod && mod.__esModule) ? mod : { "default": mod };
37};
38Object.defineProperty(exports, "__esModule", { value: true });
39exports.Updater = void 0;
40const models_1 = require("@tufjs/models");
41const debug_1 = __importDefault(require("debug"));
42const fs = __importStar(require("fs"));
43const path = __importStar(require("path"));
44const package_json_1 = require("../package.json");
45const config_1 = require("./config");
46const error_1 = require("./error");
47const fetcher_1 = require("./fetcher");
48const store_1 = require("./store");
49const url = __importStar(require("./utils/url"));
50const log = (0, debug_1.default)('tuf:cache');
51class Updater {
52 dir;
53 metadataBaseUrl;
54 targetDir;
55 targetBaseUrl;
56 forceCache;
57 trustedSet;
58 config;
59 fetcher;
60 constructor(options) {
61 const { metadataDir, metadataBaseUrl, targetDir, targetBaseUrl, fetcher, config, } = options;
62 this.dir = metadataDir;
63 this.metadataBaseUrl = metadataBaseUrl;
64 this.targetDir = targetDir;
65 this.targetBaseUrl = targetBaseUrl;
66 this.forceCache = options.forceCache ?? false;
67 const data = this.loadLocalMetadata(models_1.MetadataKind.Root);
68 this.trustedSet = new store_1.TrustedMetadataStore(data);
69 this.config = { ...config_1.defaultConfig, ...config };
70 const userAgent = config?.userAgent
71 ? `${config.userAgent} tuf-js/${package_json_1.version}`
72 : `tuf-js/${package_json_1.version}`;
73 this.fetcher =
74 fetcher ||
75 new fetcher_1.DefaultFetcher({
76 userAgent,
77 timeout: this.config.fetchTimeout,
78 retry: this.config.fetchRetries ?? this.config.fetchRetry,
79 });
80 }
81 // refresh and load the metadata before downloading the target
82 // refresh should be called once after the client is initialized
83 async refresh() {
84 // If forceCache is true, try to load the timestamp from local storage
85 // without fetching it from the remote. Otherwise, load the root and
86 // timestamp from the remote per the TUF spec.
87 if (this.forceCache) {
88 // If anything fails, load the root and timestamp from the remote. This
89 // should cover any situation where the local metadata is corrupted or
90 // expired.
91 try {
92 await this.loadTimestamp({ checkRemote: false });
93 }
94 catch (error) {
95 await this.loadRoot();
96 await this.loadTimestamp();
97 }
98 }
99 else {
100 await this.loadRoot();
101 await this.loadTimestamp();
102 }
103 await this.loadSnapshot();
104 await this.loadTargets(models_1.MetadataKind.Targets, models_1.MetadataKind.Root);
105 }
106 // Returns the TargetFile instance with information for the given target path.
107 //
108 // Implicitly calls refresh if it hasn't already been called.
109 async getTargetInfo(targetPath) {
110 if (!this.trustedSet.targets) {
111 await this.refresh();
112 }
113 return this.preorderDepthFirstWalk(targetPath);
114 }
115 async downloadTarget(targetInfo, filePath, targetBaseUrl) {
116 const targetPath = filePath || this.generateTargetPath(targetInfo);
117 if (!targetBaseUrl) {
118 if (!this.targetBaseUrl) {
119 throw new error_1.ValueError('Target base URL not set');
120 }
121 targetBaseUrl = this.targetBaseUrl;
122 }
123 let targetFilePath = targetInfo.path;
124 const consistentSnapshot = this.trustedSet.root.signed.consistentSnapshot;
125 if (consistentSnapshot && this.config.prefixTargetsWithHash) {
126 const hashes = Object.values(targetInfo.hashes);
127 const { dir, base } = path.parse(targetFilePath);
128 const filename = `${hashes[0]}.${base}`;
129 targetFilePath = dir ? `${dir}/${filename}` : filename;
130 }
131 const targetUrl = url.join(targetBaseUrl, targetFilePath);
132 // Client workflow 5.7.3: download target file
133 await this.fetcher.downloadFile(targetUrl, targetInfo.length, async (fileName) => {
134 // Verify hashes and length of downloaded file
135 await targetInfo.verify(fs.createReadStream(fileName));
136 // Copy file to target path
137 log('WRITE %s', targetPath);
138 fs.copyFileSync(fileName, targetPath);
139 });
140 return targetPath;
141 }
142 async findCachedTarget(targetInfo, filePath) {
143 if (!filePath) {
144 filePath = this.generateTargetPath(targetInfo);
145 }
146 try {
147 if (fs.existsSync(filePath)) {
148 await targetInfo.verify(fs.createReadStream(filePath));
149 return filePath;
150 }
151 }
152 catch (error) {
153 return; // File not found
154 }
155 return; // File not found
156 }
157 loadLocalMetadata(fileName) {
158 const filePath = path.join(this.dir, `${fileName}.json`);
159 log('READ %s', filePath);
160 return fs.readFileSync(filePath);
161 }
162 // Sequentially load and persist on local disk every newer root metadata
163 // version available on the remote.
164 // Client workflow 5.3: update root role
165 async loadRoot() {
166 // Client workflow 5.3.2: version of trusted root metadata file
167 const rootVersion = this.trustedSet.root.signed.version;
168 const lowerBound = rootVersion + 1;
169 const upperBound = lowerBound + this.config.maxRootRotations;
170 for (let version = lowerBound; version < upperBound; version++) {
171 const rootUrl = url.join(this.metadataBaseUrl, `${version}.root.json`);
172 try {
173 // Client workflow 5.3.3: download new root metadata file
174 const bytesData = await this.fetcher.downloadBytes(rootUrl, this.config.rootMaxLength);
175 // Client workflow 5.3.4 - 5.4.7
176 this.trustedSet.updateRoot(bytesData);
177 // Client workflow 5.3.8: persist root metadata file
178 this.persistMetadata(models_1.MetadataKind.Root, bytesData);
179 }
180 catch (error) {
181 if (error instanceof error_1.DownloadHTTPError) {
182 // 404/403 means current root is newest available
183 if ([403, 404].includes(error.statusCode)) {
184 break;
185 }
186 }
187 throw error;
188 }
189 }
190 }
191 // Load local and remote timestamp metadata.
192 // Client workflow 5.4: update timestamp role
193 async loadTimestamp({ checkRemote } = { checkRemote: true }) {
194 // Load local and remote timestamp metadata
195 try {
196 const data = this.loadLocalMetadata(models_1.MetadataKind.Timestamp);
197 this.trustedSet.updateTimestamp(data);
198 // If checkRemote is disabled, return here to avoid fetching the remote
199 // timestamp metadata.
200 if (!checkRemote) {
201 return;
202 }
203 }
204 catch (error) {
205 // continue
206 }
207 //Load from remote (whether local load succeeded or not)
208 const timestampUrl = url.join(this.metadataBaseUrl, 'timestamp.json');
209 // Client workflow 5.4.1: download timestamp metadata file
210 const bytesData = await this.fetcher.downloadBytes(timestampUrl, this.config.timestampMaxLength);
211 try {
212 // Client workflow 5.4.2 - 5.4.4
213 this.trustedSet.updateTimestamp(bytesData);
214 }
215 catch (error) {
216 // If new timestamp version is same as current, discardd the new one.
217 // This is normal and should NOT raise an error.
218 if (error instanceof error_1.EqualVersionError) {
219 return;
220 }
221 // Re-raise any other error
222 throw error;
223 }
224 // Client workflow 5.4.5: persist timestamp metadata
225 this.persistMetadata(models_1.MetadataKind.Timestamp, bytesData);
226 }
227 // Load local and remote snapshot metadata.
228 // Client workflow 5.5: update snapshot role
229 async loadSnapshot() {
230 //Load local (and if needed remote) snapshot metadata
231 try {
232 const data = this.loadLocalMetadata(models_1.MetadataKind.Snapshot);
233 this.trustedSet.updateSnapshot(data, true);
234 }
235 catch (error) {
236 if (!this.trustedSet.timestamp) {
237 throw new ReferenceError('No timestamp metadata');
238 }
239 const snapshotMeta = this.trustedSet.timestamp.signed.snapshotMeta;
240 const maxLength = snapshotMeta.length || this.config.snapshotMaxLength;
241 const version = this.trustedSet.root.signed.consistentSnapshot
242 ? snapshotMeta.version
243 : undefined;
244 const snapshotUrl = url.join(this.metadataBaseUrl, version ? `${version}.snapshot.json` : 'snapshot.json');
245 try {
246 // Client workflow 5.5.1: download snapshot metadata file
247 const bytesData = await this.fetcher.downloadBytes(snapshotUrl, maxLength);
248 // Client workflow 5.5.2 - 5.5.6
249 this.trustedSet.updateSnapshot(bytesData);
250 // Client workflow 5.5.7: persist snapshot metadata file
251 this.persistMetadata(models_1.MetadataKind.Snapshot, bytesData);
252 }
253 catch (error) {
254 throw new error_1.RuntimeError(`Unable to load snapshot metadata error ${error}`);
255 }
256 }
257 }
258 // Load local and remote targets metadata.
259 // Client workflow 5.6: update targets role
260 async loadTargets(role, parentRole) {
261 if (this.trustedSet.getRole(role)) {
262 return this.trustedSet.getRole(role);
263 }
264 try {
265 const buffer = this.loadLocalMetadata(role);
266 this.trustedSet.updateDelegatedTargets(buffer, role, parentRole);
267 }
268 catch (error) {
269 // Local 'role' does not exist or is invalid: update from remote
270 if (!this.trustedSet.snapshot) {
271 throw new ReferenceError('No snapshot metadata');
272 }
273 const metaInfo = this.trustedSet.snapshot.signed.meta[`${role}.json`];
274 // TODO: use length for fetching
275 const maxLength = metaInfo.length || this.config.targetsMaxLength;
276 const version = this.trustedSet.root.signed.consistentSnapshot
277 ? metaInfo.version
278 : undefined;
279 const encodedRole = encodeURIComponent(role);
280 const metadataUrl = url.join(this.metadataBaseUrl, version ? `${version}.${encodedRole}.json` : `${encodedRole}.json`);
281 try {
282 // Client workflow 5.6.1: download targets metadata file
283 const bytesData = await this.fetcher.downloadBytes(metadataUrl, maxLength);
284 // Client workflow 5.6.2 - 5.6.6
285 this.trustedSet.updateDelegatedTargets(bytesData, role, parentRole);
286 // Client workflow 5.6.7: persist targets metadata file
287 this.persistMetadata(role, bytesData);
288 }
289 catch (error) {
290 throw new error_1.RuntimeError(`Unable to load targets error ${error}`);
291 }
292 }
293 return this.trustedSet.getRole(role);
294 }
295 async preorderDepthFirstWalk(targetPath) {
296 // Interrogates the tree of target delegations in order of appearance
297 // (which implicitly order trustworthiness), and returns the matching
298 // target found in the most trusted role.
299 // List of delegations to be interrogated. A (role, parent role) pair
300 // is needed to load and verify the delegated targets metadata.
301 const delegationsToVisit = [
302 {
303 roleName: models_1.MetadataKind.Targets,
304 parentRoleName: models_1.MetadataKind.Root,
305 },
306 ];
307 const visitedRoleNames = new Set();
308 // Client workflow 5.6.7: preorder depth-first traversal of the graph of
309 // target delegations
310 while (visitedRoleNames.size <= this.config.maxDelegations &&
311 delegationsToVisit.length > 0) {
312 // Pop the role name from the top of the stack.
313 const { roleName, parentRoleName } = delegationsToVisit.pop();
314 // Skip any visited current role to prevent cycles.
315 // Client workflow 5.6.7.1: skip already-visited roles
316 if (visitedRoleNames.has(roleName)) {
317 continue;
318 }
319 // The metadata for 'role_name' must be downloaded/updated before
320 // its targets, delegations, and child roles can be inspected.
321 const targets = (await this.loadTargets(roleName, parentRoleName))
322 ?.signed;
323 if (!targets) {
324 continue;
325 }
326 const target = targets.targets?.[targetPath];
327 if (target) {
328 return target;
329 }
330 // After preorder check, add current role to set of visited roles.
331 visitedRoleNames.add(roleName);
332 if (targets.delegations) {
333 const childRolesToVisit = [];
334 // NOTE: This may be a slow operation if there are many delegated roles.
335 const rolesForTarget = targets.delegations.rolesForTarget(targetPath);
336 for (const { role: childName, terminating } of rolesForTarget) {
337 childRolesToVisit.push({
338 roleName: childName,
339 parentRoleName: roleName,
340 });
341 // Client workflow 5.6.7.2.1
342 if (terminating) {
343 delegationsToVisit.splice(0); // empty the array
344 break;
345 }
346 }
347 childRolesToVisit.reverse();
348 delegationsToVisit.push(...childRolesToVisit);
349 }
350 }
351 return; // no matching target found
352 }
353 generateTargetPath(targetInfo) {
354 if (!this.targetDir) {
355 throw new error_1.ValueError('Target directory not set');
356 }
357 // URL encode target path
358 const filePath = encodeURIComponent(targetInfo.path);
359 return path.join(this.targetDir, filePath);
360 }
361 persistMetadata(metaDataName, bytesData) {
362 const encodedName = encodeURIComponent(metaDataName);
363 try {
364 const filePath = path.join(this.dir, `${encodedName}.json`);
365 log('WRITE %s', filePath);
366 fs.writeFileSync(filePath, bytesData.toString('utf8'));
367 }
368 catch (error) {
369 throw new error_1.PersistError(`Failed to persist metadata ${encodedName} error: ${error}`);
370 }
371 }
372}
373exports.Updater = Updater;
374 