|
| 1 | +// Script to update certdata.txt from NSS. |
| 2 | +import{execFileSync}from'node:child_process'; |
| 3 | +import{randomUUID}from'node:crypto'; |
| 4 | +import{createWriteStream}from'node:fs'; |
| 5 | +import{basename,join,relative}from'node:path'; |
| 6 | +import{Readable}from'node:stream'; |
| 7 | +import{pipeline}from'node:stream/promises'; |
| 8 | +import{fileURLToPath}from'node:url'; |
| 9 | +import{parseArgs}from'node:util'; |
| 10 | + |
| 11 | +// Constants for NSS release metadata. |
| 12 | +constkNSSVersion='version'; |
| 13 | +constkNSSDate='date'; |
| 14 | +constkFirefoxVersion='firefoxVersion'; |
| 15 | +constkFirefoxDate='firefoxDate'; |
| 16 | + |
| 17 | +const__filename=fileURLToPath(import.meta.url); |
| 18 | +constnow=newDate(); |
| 19 | + |
| 20 | +constformatDate=(d)=>{ |
| 21 | +constiso=d.toISOString(); |
| 22 | +returniso.substring(0,iso.indexOf('T')); |
| 23 | +}; |
| 24 | + |
| 25 | +constnormalizeTD=(text)=>{ |
| 26 | +// Remove whitespace and any HTML tags. |
| 27 | +returntext?.trim().replace(/<.*?>/g,''); |
| 28 | +}; |
| 29 | +constgetReleases=(text)=>{ |
| 30 | +constreleases=[]; |
| 31 | +consttableRE=/<table[^>]+>([\S\s]*?)<\/table>/g; |
| 32 | +consttableRowRE=/<tr?[^>]*>([\S\s]*?)<\/tr>/g; |
| 33 | +consttableHeaderRE=/<th?[^>]*>([\S\s]*?)<\/th>/g; |
| 34 | +consttableDataRE=/<td?[^>]*>([\S\s]*?)<\/td>/g; |
| 35 | +for(consttableoftext.matchAll(tableRE)){ |
| 36 | +constcolumns={}; |
| 37 | +constmatches=table[1].matchAll(tableRowRE); |
| 38 | +// First row has the table header. |
| 39 | +letrow=matches.next(); |
| 40 | +if(row.done){ |
| 41 | +continue; |
| 42 | +} |
| 43 | +constheaders=Array.from(row.value[1].matchAll(tableHeaderRE),(m)=>m[1]); |
| 44 | +if(headers.length>0){ |
| 45 | +for(leti=0;i<headers.length;i++){ |
| 46 | +if(/NSSversion/i.test(headers[i])){ |
| 47 | +columns[kNSSVersion]=i; |
| 48 | +}elseif(/Release.*frombranch/i.test(headers[i])){ |
| 49 | +columns[kNSSDate]=i; |
| 50 | +}elseif(/Firefoxversion/i.test(headers[i])){ |
| 51 | +columns[kFirefoxVersion]=i; |
| 52 | +}elseif(/Firefoxreleasedate/i.test(headers[i])){ |
| 53 | +columns[kFirefoxDate]=i; |
| 54 | +} |
| 55 | +} |
| 56 | +} |
| 57 | +// Filter out "NSS Certificate bugs" table. |
| 58 | +if(columns[kNSSDate]===undefined){ |
| 59 | +continue; |
| 60 | +} |
| 61 | +// Scrape releases. |
| 62 | +row=matches.next(); |
| 63 | +while(!row.done){ |
| 64 | +constcells=Array.from(row.value[1].matchAll(tableDataRE),(m)=>m[1]); |
| 65 | +constrelease={}; |
| 66 | +release[kNSSVersion]=normalizeTD(cells[columns[kNSSVersion]]); |
| 67 | +release[kNSSDate]=newDate(normalizeTD(cells[columns[kNSSDate]])); |
| 68 | +release[kFirefoxVersion]=normalizeTD(cells[columns[kFirefoxVersion]]); |
| 69 | +release[kFirefoxDate]=newDate(normalizeTD(cells[columns[kFirefoxDate]])); |
| 70 | +releases.push(release); |
| 71 | +row=matches.next(); |
| 72 | +} |
| 73 | +} |
| 74 | +returnreleases; |
| 75 | +}; |
| 76 | + |
| 77 | +constgetLatestVersion=(releases)=>{ |
| 78 | +constarrayNumberSort=(x,y,i)=>{ |
| 79 | +if(x[i]===undefined&&y[i]===undefined){ |
| 80 | +return0; |
| 81 | +}elseif(x[i]===y[i]){ |
| 82 | +returnarrayNumberSort(x,y,i+1); |
| 83 | +} |
| 84 | +return(x[i]??0)-(y[i]??0); |
| 85 | +}; |
| 86 | +constextractVersion=(t)=>{ |
| 87 | +returnt[kNSSVersion].split('.').map((n)=>parseInt(n)); |
| 88 | +}; |
| 89 | +constreleaseSorter=(x,y)=>{ |
| 90 | +returnarrayNumberSort(extractVersion(x),extractVersion(y),0); |
| 91 | +}; |
| 92 | +returnreleases.sort(releaseSorter).filter(pastRelease).at(-1)[kNSSVersion]; |
| 93 | +}; |
| 94 | + |
| 95 | +constpastRelease=(r)=>{ |
| 96 | +returnr[kNSSDate]<now; |
| 97 | +}; |
| 98 | + |
| 99 | +constoptions={ |
| 100 | +help: { |
| 101 | +type: 'boolean', |
| 102 | +}, |
| 103 | +file: { |
| 104 | +short: 'f', |
| 105 | +type: 'string', |
| 106 | +}, |
| 107 | +verbose: { |
| 108 | +short: 'v', |
| 109 | +type: 'boolean', |
| 110 | +}, |
| 111 | +}; |
| 112 | +const{ |
| 113 | + positionals, |
| 114 | + values, |
| 115 | +}=parseArgs({ |
| 116 | +allowPositionals: true, |
| 117 | + options, |
| 118 | +}); |
| 119 | + |
| 120 | +if(values.help){ |
| 121 | +console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION]...`); |
| 122 | +console.log(); |
| 123 | +console.log('Updates certdata.txt to NSS VERSION (most recent release by default).'); |
| 124 | +console.log(''); |
| 125 | +console.log(' -f, --file=FILE writes a commit message reflecting the change to the'); |
| 126 | +console.log(' specified FILE'); |
| 127 | +console.log(' -v, --verbose writes progress to stdout'); |
| 128 | +console.log(' --help display this help and exit'); |
| 129 | +process.exit(0); |
| 130 | +} |
| 131 | + |
| 132 | +if(values.verbose){ |
| 133 | +console.log('Fetching NSS release schedule'); |
| 134 | +} |
| 135 | +constscheduleURL='https://wiki.mozilla.org/NSS:Release_Versions'; |
| 136 | +constschedule=awaitfetch(scheduleURL); |
| 137 | +if(!schedule.ok){ |
| 138 | +console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`); |
| 139 | +process.exit(-1); |
| 140 | +} |
| 141 | +constscheduleText=awaitschedule.text(); |
| 142 | +constnssReleases=getReleases(scheduleText); |
| 143 | + |
| 144 | +// Retrieve metadata for the NSS release being updated to. |
| 145 | +constversion=positionals[0]??getLatestVersion(nssReleases); |
| 146 | +constrelease=nssReleases.find((r)=>{ |
| 147 | +returnnewRegExp(`^${version.replace('.','\\.')}\\b`).test(r[kNSSVersion]); |
| 148 | +}); |
| 149 | +if(!pastRelease(release)){ |
| 150 | +console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`); |
| 151 | +} |
| 152 | +if(values.verbose){ |
| 153 | +console.log('Found NSS version:'); |
| 154 | +console.log(release); |
| 155 | +} |
| 156 | + |
| 157 | +// Fetch certdata.txt and overwrite the local copy. |
| 158 | +consttag=`NSS_${version.replaceAll('.','_')}_RTM`; |
| 159 | +constcertdataURL=`https://hg.mozilla.org/projects/nss/raw-file/${tag}/lib/ckfw/builtins/certdata.txt`; |
| 160 | +if(values.verbose){ |
| 161 | +console.log(`Fetching ${certdataURL}`); |
| 162 | +} |
| 163 | +constcheckoutDir=join(__filename,'..','..','..'); |
| 164 | +constcertdata=awaitfetch(certdataURL); |
| 165 | +constcertdataFile=join(checkoutDir,'tools','certdata.txt'); |
| 166 | +if(!certdata.ok){ |
| 167 | +console.error(`Failed to fetch ${certdataURL}: ${certdata.status}: ${certdata.statusText}`); |
| 168 | +process.exit(-1); |
| 169 | +} |
| 170 | +if(values.verbose){ |
| 171 | +console.log(`Writing ${certdataFile}`); |
| 172 | +} |
| 173 | +awaitpipeline(certdata.body,createWriteStream(certdataFile)); |
| 174 | + |
| 175 | +// Run tools/mk-ca-bundle.pl to generate src/node_root_certs.h. |
| 176 | +if(values.verbose){ |
| 177 | +console.log('Running tools/mk-ca-bundle.pl'); |
| 178 | +} |
| 179 | +constopts={encoding: 'utf8'}; |
| 180 | +constmkCABundleTool=join(checkoutDir,'tools','mk-ca-bundle.pl'); |
| 181 | +constmkCABundleOut=execFileSync(mkCABundleTool, |
| 182 | +values.verbose ? ['-v'] : [], |
| 183 | +opts); |
| 184 | +if(values.verbose){ |
| 185 | +console.log(mkCABundleOut); |
| 186 | +} |
| 187 | + |
| 188 | +// Determine certificates added and/or removed. |
| 189 | +constcertHeaderFile=relative(process.cwd(),join(checkoutDir,'src','node_root_certs.h')); |
| 190 | +constdiff=execFileSync('git',['diff-files','-u','--',certHeaderFile],opts); |
| 191 | +if(values.verbose){ |
| 192 | +console.log(diff); |
| 193 | +} |
| 194 | +constcertsAddedRE=/^\+\/\*(.*)\*\//gm; |
| 195 | +constcertsRemovedRE=/^-\/\*(.*)\*\//gm; |
| 196 | +constadded=[ ...diff.matchAll(certsAddedRE)].map((m)=>m[1]); |
| 197 | +constremoved=[ ...diff.matchAll(certsRemovedRE)].map((m)=>m[1]); |
| 198 | + |
| 199 | +constcommitMsg=[ |
| 200 | +`crypto: update root certificates to NSS ${release[kNSSVersion]}`, |
| 201 | +'', |
| 202 | +`This is the certdata.txt[0] from NSS ${release[kNSSVersion]}, released on ${formatDate(release[kNSSDate])}.`, |
| 203 | +'', |
| 204 | +`This is the version of NSS that ${release[kFirefoxDate]<now ? 'shipped' : 'will ship'} in Firefox ${release[kFirefoxVersion]} on`, |
| 205 | +`${formatDate(release[kFirefoxDate])}.`, |
| 206 | +'', |
| 207 | +]; |
| 208 | +if(added.length>0){ |
| 209 | +commitMsg.push('Certificates added:'); |
| 210 | +commitMsg.push(...added.map((cert)=>`- ${cert}`)); |
| 211 | +commitMsg.push(''); |
| 212 | +} |
| 213 | +if(removed.length>0){ |
| 214 | +commitMsg.push('Certificates removed:'); |
| 215 | +commitMsg.push(...removed.map((cert)=>`- ${cert}`)); |
| 216 | +commitMsg.push(''); |
| 217 | +} |
| 218 | +commitMsg.push(`[0] ${certdataURL}`); |
| 219 | +constdelimiter=randomUUID(); |
| 220 | +constproperties=[ |
| 221 | +`NEW_VERSION=${release[kNSSVersion]}`, |
| 222 | +`COMMIT_MSG<<${delimiter}`, |
| 223 | + ...commitMsg, |
| 224 | +delimiter, |
| 225 | +'', |
| 226 | +].join('\n'); |
| 227 | +if(values.verbose){ |
| 228 | +console.log(properties); |
| 229 | +} |
| 230 | +constpropertyFile=values.file; |
| 231 | +if(propertyFile!==undefined){ |
| 232 | +console.log(`Writing to ${propertyFile}`); |
| 233 | +awaitpipeline(Readable.from(properties),createWriteStream(propertyFile)); |
| 234 | +} |
0 commit comments