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