Skip to content

Commit

Permalink
CA certificate fix
Browse files Browse the repository at this point in the history
This should work on all webOS versions. It loads certificates from the
Mozilla CA cert bundle. Users may add additional certs by placing
PEM-encoded files in the "certs" directory.
  • Loading branch information
throwaway96 committed Jun 10, 2024
1 parent f19064a commit 10ed3ba
Show file tree
Hide file tree
Showing 6 changed files with 3,792 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"appinfo.json",
"assets/**",
"services/bin/**",
"services/certs/**",
"services/services.json",
"services/package.json",
"services/run-js-service",
Expand Down
3,581 changes: 3,581 additions & 0 deletions services/certs/cacert-2024-03-11.pem

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions services/fetch-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* fetch-wrapper.ts
*
* Makes fetch() trust our CA certs. Certs loaded on first use.
*
* This is part of webOS Homebrew Channel
* https://github.com/webosbrew/webos-homebrew-channel
* Copyright 2024 throwaway96.
*/

import http from 'http';
import https from 'https';

import { loadCertDir } from './load-certs';

import fetch from 'node-fetch';

function createAgent(): https.Agent {
const certsDir = __dirname + '/certs';
const certs = loadCertDir(certsDir, {
logLevel: 0,
});

return new https.Agent({ ca: certs });
}

let defaultHttpsAgent: https.Agent | null = null;

export function fetchWrapper(url: fetch.RequestInfo, init?: fetch.RequestInit): Promise<fetch.Response> {
if (defaultHttpsAgent === null) {
defaultHttpsAgent = createAgent();
}

if (typeof init === 'undefined') {
init = {};
}

init.agent = (parsedURL: URL): http.RequestOptions['agent'] => {
if (parsedURL.protocol == 'http:') {
return http.globalAgent;
} else {
return defaultHttpsAgent as https.Agent;
}
};

return fetch(url, init);
}
13 changes: 13 additions & 0 deletions services/load-certs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Load PEM certificates from a directory.
*
* @param {string} dir Cert directory.
* @param {{ logLevel: number }} options Options.
* @returns {string[]} The certificates.
*/
export function loadCertDir(
dir: string,
options: {
logLevel: number;
},
): string[];
147 changes: 147 additions & 0 deletions services/load-certs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* load-certs.js
*
* Loads PEM certificates from a directory. Not recursive.
*
* This is part of webOS Homebrew Channel
* https://github.com/webosbrew/webos-homebrew-channel
* Copyright 2024 throwaway96
*/

const fs = require('fs'); /* eslint-disable-line @typescript-eslint/no-var-requires */
const path = require('path'); /* eslint-disable-line @typescript-eslint/no-var-requires */

function createLogger(logLevel) {
if (typeof logLevel !== 'number') {
throw new Error('invalid argument type (logLevel): ' + typeof logLevel);
}

function _ignore() {}

return {
debug: logLevel <= 0 ? console.log : _ignore,
verbose: logLevel <= 1 ? console.log : _ignore,
info: logLevel <= 2 ? console.log : _ignore,
warn: logLevel <= 3 ? console.warn : _ignore,
error: logLevel <= 4 ? console.error : _ignore,
};
}

const pemRegex = /-----BEGIN CERTIFICATE-----\n[^]+?\n-----END CERTIFICATE-----/g;

function readPem(file) {
var results, content;

if (typeof file !== 'string') {
throw new Error('invalid argument type: ' + typeof file);
}

const newCAs = [];

try {
content = fs.readFileSync(file, { encoding: 'ascii' }).trim().replace(/\r\n/g, '\n');
} catch (err) {
throw new Error('error in read(' + file + '): ' + err.message, {
cause: err,
});
}

if ((results = content.match(pemRegex)) === null) {
throw new Error('could not parse PEM certificate(s)');
}

results.forEach(function pemIterate(match) {
newCAs.push(match.trim());
});

return newCAs;
}

/**
* Load PEM certificates from a directory.
*
* @param {string} dir Cert directory.
* @param {{ logLevel: number }} options Options.
* @returns {string[]} The certificates.
*/
function loadCertDir(dir, options) {
const rootCerts = [];
var files, dirStat, logLevel;

if (typeof dir !== 'string') {
throw new Error('invalid argument type (dir): ' + typeof dir);
}

if (typeof options === 'undefined') {
options = {};
} else if (typeof options !== 'object') {
throw new Error('invalid argument type (options): ' + typeof options);
}

if (typeof options.logLevel === 'undefined') {
logLevel = 3; /* 3 -> warnings */
} else if (typeof options.logLevel === 'number') {
logLevel = options.logLevel;
} else {
throw new Error('invalid argument type (options.logLevel): ' + typeof options.logLevel);
}

const log = createLogger(logLevel);

try {
dirStat = fs.statSync(dir);
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error('directory does not exist: ' + dir, { cause: err });
} else {
throw new Error('error in stat(' + dir + '): ' + err.message, {
cause: err,
});
}
}

if (!dirStat.isDirectory()) {
throw new Error('not a directory: ' + dir);
}

try {
files = fs.readdirSync(dir);
} catch (err) {
throw new Error('error in readdir(' + dir + '): ' + err.message, {
cause: err,
});
}

files
.map(function resolvePath(filename) {
return path.resolve(dir, filename);
})
.forEach(function processFile(file) {
var stat;

try {
stat = fs.statSync(file);
} catch (err) {
log.verbose('error in stat(' + file + '): ' + err.message);
return;
}

if (stat.isFile()) {
try {
readPem(file).forEach(function certIterate(cert) {
if (rootCerts.indexOf(cert) !== -1) {
log.debug('duplicate cert from ' + file);
} else {
rootCerts.push(cert);
}
});
} catch (err) {
log.verbose('failed to read cert file ' + file + ': ' + err.message);
}
}
});

return rootCerts;
}

module.exports.loadCertDir = loadCertDir;
5 changes: 3 additions & 2 deletions services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import child_process from 'child_process';
import { Promise } from 'bluebird'; // eslint-disable-line @typescript-eslint/no-redeclare
import progress from 'progress-stream';
import Service, { Message } from 'webos-service';
import fetch from 'node-fetch';

import { asyncStat, asyncExecFile, asyncPipeline, asyncUnlink, asyncWriteFile, asyncReadFile, asyncChmod, asyncMkdir } from './adapter';
import { fetchWrapper } from './fetch-wrapper';

import rootAppInfo from '../appinfo.json';
import serviceInfo from './services.json';
Expand Down Expand Up @@ -432,7 +433,7 @@ function runService(): void {

// Download
message.respond({ statusText: 'Downloading…' });
const res = await fetch(payload.ipkUrl);
const res = await fetchWrapper(payload.ipkUrl);
if (!res.ok) {
throw new Error(res.statusText);
}
Expand Down

0 comments on commit 10ed3ba

Please sign in to comment.