updater.js 14 KB

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