Skip to content

Commit

Permalink
feat: add ubuntu support (#21)
Browse files Browse the repository at this point in the history
* chore: initial linux support experiment
* feat: add ubuntu arm64 support
* chore: run linter fix
* chore: add icon to deb package
* feat: support x64 arch for daemon
* feat: add support for deb for x64 arch
  • Loading branch information
keplervital authored Sep 5, 2023
1 parent f8dc05b commit 4228b97
Show file tree
Hide file tree
Showing 24 changed files with 461 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/core/nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"watch": ["src"],
"ignore": ["src/**/*.spec.ts"],
"ext": "ts,json",
"exec": "pnpm build && pnpm start",
"exec": "yarn build",
"legacyWatch": true,
"delay": 1000
}
2 changes: 2 additions & 0 deletions packages/core/src/commons/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ if (!existsSync(dataPath)) {

const isMaxOSX = platform === SupportedPlatforms.MacOSX;
const isWindows = platform === SupportedPlatforms.Windows;
const isLinux = platform === SupportedPlatforms.Linux;

const coreConfigs: CoreConfiguration = {
dataPath,
platform,
macosx: isMaxOSX,
windows: isWindows,
linux: isLinux,
encoding: isWindows ? 'utf16le' : 'utf8',
ipcChannels: {
daemon: isWindows
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/commons/typings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum SupportedPlatforms {
Windows = 'win32',
MacOSX = 'darwin',
Linux = 'linux',
}

export interface IpcChannels {
Expand All @@ -13,6 +14,7 @@ export interface CoreConfiguration {
platform: string;
windows: boolean;
macosx: boolean;
linux: boolean;
ipcChannels: IpcChannels;
encoding: BufferEncoding;
}
2 changes: 2 additions & 0 deletions packages/core/src/tls/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class CertificateFactory {
publicKey: keyPair.publicKey,
signingKey: keyPair.privateKey,
issuer: [...this.issuer],
serialId: await this.store.nextSerialId(),
// same as issuer since this is self signed
subject: [...this.issuer],
extensions: [
Expand Down Expand Up @@ -157,6 +158,7 @@ export class CertificateFactory {
publicKey: keyPair.publicKey,
signingKey: ca.key,
issuer: caCert.subject.attributes,
serialId: await this.store.nextSerialId(),
subject: [
{ name: 'commonName', value: hostname },
{
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/tls/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CertificateDTO, CertificateStoreConfiguration } from './typings';

export class CertificateStore {
private readonly storePath: string;
private readonly serialIdPath: string;
private static cachedLookups = new InMemoryCache({
stdTTL: 60 * 5, // 5 minutes
maxKeys: 250,
Expand All @@ -23,6 +24,7 @@ export class CertificateStore {
private readonly configuration: CertificateStoreConfiguration
) {
this.storePath = resolve(coreConfigs.dataPath, this.configuration.folder);
this.serialIdPath = resolve(this.storePath, 'serial');
}

private static maybeGetFromCache(id: string): Certificate | undefined {
Expand All @@ -43,6 +45,7 @@ export class CertificateStore {

private async init(): Promise<void> {
createDir(this.storePath);
await this.setupSerialDependency();
}

private certificateDtoPath(id: string): string {
Expand Down Expand Up @@ -113,6 +116,30 @@ export class CertificateStore {
CertificateStore.maybeSetInCache(certificate.id, certificate);
}

public async setupSerialDependency(): Promise<void> {
if (pathExists(this.serialIdPath)) {
return;
}

const files = getFiles(this.storePath, ['json', 'cert']);
for (const file of files) {
rmSync(resolve(this.storePath, file), { force: true });
}
}

public async nextSerialId(): Promise<string> {
const serial =
(await getFile(this.serialIdPath, { encoding: 'utf-8' })) ?? '00';
const nextSerial = BigInt(serial) + BigInt(1);
const nextSerialStr = nextSerial.toString();

await saveFile(this.serialIdPath, nextSerialStr, {
encoding: 'utf-8',
});

return nextSerialStr;
}

public static async create(
configuration: CertificateStoreConfiguration
): Promise<CertificateStore> {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tls/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface GenerateCertificateOpts {
issuer: pki.CertificateField[];
extensions: object[];
signingKey: pki.PrivateKey;
serialId: string;
}

export interface CertificateStoreConfiguration {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tls/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export const generateCertificate = async ({
issuer,
extensions,
signingKey,
serialId,
}: GenerateCertificateOpts): Promise<string> => {
const certificate = pki.createCertificate();

certificate.publicKey = publicKey;
certificate.serialNumber = '01';
certificate.serialNumber = serialId;
certificate.validity.notBefore = createValidityDate();
certificate.validity.notAfter = createValidityDate(365);

Expand Down
5 changes: 4 additions & 1 deletion packages/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
},
"pkg": {
"targets": [
"node18-macos-arm64",
"node18-macos-x64",
"node18-win-x64"
"node18-win-x64",
"node18-linux-arm64",
"node18-linux-x64"
],
"compress": "GZip",
"outputPath": "bin"
Expand Down
7 changes: 7 additions & 0 deletions packages/daemon/src/platforms/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { MacPlatform } from './mac';
import { Platform, PlatformBuildConfigs } from './typings';
import { WindowsPlatform } from './windows';
import { LinuxPlatform } from './linux';

export class PlatformFactory {
public static async create(configs: PlatformBuildConfigs): Promise<Platform> {
Expand All @@ -21,6 +22,12 @@ export class PlatformFactory {
proxy: configs.proxy,
pac: configs.pac,
});
case SupportedPlatforms.Linux:
return new LinuxPlatform({
ca: configs.ca,
proxy: configs.proxy,
pac: configs.pac,
});
default:
throw new UnsupportedPlatformError('unknown');
}
Expand Down
2 changes: 2 additions & 0 deletions packages/daemon/src/platforms/linux/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './linux';
export * from './typings';
220 changes: 220 additions & 0 deletions packages/daemon/src/platforms/linux/linux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
execAsync,
getDirectories,
getFile,
logger,
pathExists,
saveFile,
} from '@dfinity/http-proxy-core';
import { Platform, PlatformProxyInfo } from '../typings';
import { PlatformConfigs } from './typings';
import { resolve } from 'path';
import {
BASE_MOZILLA_PATH,
BASE_SNAP_MOZZILA_PATH,
CURL_RC_FILE,
FIREFOX_PROFILES_FOLDER,
MOZILLA_CERTIFICATES_FOLDER,
ROOT_CA_PATH,
findP11KitTrustPath,
} from './utils';

export class LinuxPlatform implements Platform {
constructor(
private readonly configs: PlatformConfigs,
private readonly username = String(process.env.LOGNAME ?? 'root')
) {}

public async attach(): Promise<void> {
await this.setupDependencies();

logger.info(
`attaching proxy to system with: ` +
`host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` +
`capath(${this.configs.ca.path}), ` +
`caname(${this.configs.ca.commonName})`
);

await this.trustCertificate(true, this.configs.ca.path);

await this.configureWebProxy(true, {
host: this.configs.proxy.host,
port: this.configs.proxy.port,
});
}

public async detach(): Promise<void> {
await this.setupDependencies();

logger.info(
`detaching proxy from system with: ` +
`host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` +
`capath(${this.configs.ca.path}), ` +
`caname(${this.configs.ca.commonName})`
);

await this.trustCertificate(false, this.configs.ca.path);

await this.configureWebProxy(false, {
host: this.configs.proxy.host,
port: this.configs.proxy.port,
});
}

public async configureWebProxy(
enable: boolean,
{ host, port }: PlatformProxyInfo
): Promise<void> {
const curlrcPath = resolve(String(process.env.HOME), CURL_RC_FILE);
const curlrc = (await getFile(curlrcPath, { encoding: 'utf-8' })) ?? '';
const curlrcLines = curlrc
.split('\n')
.filter((line) => !line.startsWith('proxy='));
if (enable) {
curlrcLines.push(`proxy=http://${host}:${port}`);
}
await saveFile(curlrcPath, curlrcLines.join('\n'), {
encoding: 'utf-8',
});

await this.tooggleNetworkWebProxy(enable);
}

private async tooggleNetworkWebProxy(enable: boolean): Promise<void> {
const pacUrl = `http://${this.configs.pac.host}:${this.configs.pac.port}/proxy.pac`;

if (enable) {
await execAsync(
[
`su -l ${this.username} -c "gsettings set org.gnome.system.proxy mode 'auto' && gsettings set org.gnome.system.proxy autoconfig-url '${pacUrl}'"`,
].join(' && ')
);

return;
}

await execAsync(
[
`su -l ${this.username} -c "gsettings set org.gnome.system.proxy mode 'none'"`,
].join(' && ')
);
}

private async trustCertificate(trust: boolean, path: string): Promise<void> {
if (trust) {
await execAsync(
`sudo cp "${path}" "${ROOT_CA_PATH}" && sudo update-ca-certificates`
);

await this.firefoxTrustCertificate();
return;
}

await execAsync(
`sudo rm -rf "${ROOT_CA_PATH}" && sudo update-ca-certificates`
);
}

private async firefoxTrustCertificate(): Promise<void> {
await this.setupFirefoxCertificateConfigurations(BASE_MOZILLA_PATH);
await this.setupFirefoxCertificateConfigurations(BASE_SNAP_MOZZILA_PATH);
}

private async setupFirefoxCertificateConfigurations(
basePath: string
): Promise<void> {
const homePath = String(process.env.HOME);
const mozillaPathPath = resolve(homePath, basePath);
const certificatesPath = resolve(
mozillaPathPath,
MOZILLA_CERTIFICATES_FOLDER
);
const profilesPath = resolve(mozillaPathPath, FIREFOX_PROFILES_FOLDER);

if (!pathExists(mozillaPathPath)) {
// Firefox is not installed.
return;
}

await this.firefoxSetupCertificates(certificatesPath);
await this.firefoxSetupProfiles(profilesPath);
}

private async setupDependencies(): Promise<void> {
const p11KitPath = await findP11KitTrustPath();

if (!p11KitPath) {
await execAsync(
'sudo apt install p11-kit p11-kit-modules libnss3-tools -y'
);
const installed = await findP11KitTrustPath();

if (!installed) {
throw new Error('Failed to setup p11-kit dependency');
}
}
}

private async firefoxSetupCertificates(profilesPath: string): Promise<void> {
if (!pathExists(profilesPath)) {
return;
}

const p11KitPath = await findP11KitTrustPath();
if (!p11KitPath) {
throw new Error('Failed to find certificate store path');
}

// firefox profile directories end with .default|.default-release
const profiles = getDirectories(profilesPath).filter(
(dir) => dir.endsWith('.default') || dir.endsWith('.default-release')
);

for (const profileFolder of profiles) {
const profilePath = resolve(profilesPath, profileFolder);

await execAsync(
`modutil -dbdir sql:${profilePath} -add "P11 Kit" -libfile ${p11KitPath}`
);
}
}

private async firefoxSetupProfiles(profilesPath: string): Promise<void> {
if (!pathExists(profilesPath)) {
return;
}

// firefox profile directories end with .default|.default-release
const profiles = getDirectories(profilesPath).filter(
(dir) => dir.endsWith('.default') || dir.endsWith('.default-release')
);

for (const profileFolder of profiles) {
const userPreferencesPath = resolve(
profilesPath,
profileFolder,
'user.js'
);

const userPreferences =
(await getFile(userPreferencesPath, { encoding: 'utf8' })) ?? '';

const preferences = userPreferences
.split('\n')
.filter((line) => !line.includes('security.enterprise_roots.enabled'));

preferences.push(`user_pref("security.enterprise_roots.enabled", true);`);

await saveFile(
userPreferencesPath,
preferences.filter((line) => line.length > 0).join('\n') + '\n',
{
encoding: 'utf-8',
}
);
await execAsync(
`sudo chown ${this.username}:${this.username} "${userPreferencesPath}"`
);
}
}
}
Loading

0 comments on commit 4228b97

Please sign in to comment.