├── .nvmrc ├── .npmrc ├── packages ├── ui │ ├── .gitignore │ ├── src │ │ ├── services │ │ │ ├── index.ts │ │ │ └── proxy.ts │ │ ├── interface │ │ │ ├── index.ts │ │ │ ├── images.ts │ │ │ └── menu.ts │ │ ├── assets │ │ │ ├── logo.png │ │ │ ├── tray-dark.png │ │ │ ├── tray-light.png │ │ │ ├── logo@124x124.png │ │ │ ├── logo@128x128.ico │ │ │ ├── disabled@64x64.png │ │ │ ├── enabled@64x64.png │ │ │ ├── logo@128x128.icns │ │ │ ├── logo@256x256.icns │ │ │ ├── pending@64x64.png │ │ │ ├── tray-Template.png │ │ │ ├── exit-dark@128x128.png │ │ │ ├── logo-dark@124x124.png │ │ │ ├── play-dark@128x128.png │ │ │ ├── tray-dark-enabled.png │ │ │ ├── exit-light@128x128.png │ │ │ ├── logo-light@124x124.png │ │ │ ├── pause-dark@128x128.png │ │ │ ├── pause-light@128x128.png │ │ │ ├── play-light@128x128.png │ │ │ ├── tray-light-enabled.png │ │ │ ├── tray-enabled-Template.png │ │ │ ├── logo-dark-enabled@124x124.png │ │ │ └── logo-light-enabled@124x124.png │ │ ├── typings.ts │ │ ├── main.ts │ │ ├── commons │ │ │ └── utils.ts │ │ └── proxy-ui.ts │ ├── nodemon.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── build.js │ ├── build │ │ ├── win.ps1 │ │ ├── utils.js │ │ ├── win.js │ │ ├── linux.js │ │ └── mac.js │ ├── README.md │ └── package.json ├── daemon │ ├── .gitignore │ ├── src │ │ ├── platforms │ │ │ ├── index.ts │ │ │ ├── mac │ │ │ │ ├── index.ts │ │ │ │ ├── typings.ts │ │ │ │ ├── utils.ts │ │ │ │ └── mac.ts │ │ │ ├── linux │ │ │ │ ├── index.ts │ │ │ │ ├── typings.ts │ │ │ │ ├── utils.ts │ │ │ │ └── linux.ts │ │ │ ├── windows │ │ │ │ ├── index.ts │ │ │ │ ├── typings.ts │ │ │ │ ├── utils.ts │ │ │ │ └── windows.ts │ │ │ ├── utils.ts │ │ │ ├── typings.ts │ │ │ └── factory.ts │ │ ├── main.ts │ │ ├── utils.ts │ │ ├── typings.ts │ │ ├── start.ts │ │ └── daemon.ts │ ├── nodemon.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── server │ ├── src │ │ ├── commons │ │ │ ├── index.ts │ │ │ ├── typings.ts │ │ │ ├── configs.ts │ │ │ ├── http-interface │ │ │ │ ├── canister_http_interface.did │ │ │ │ ├── canister_http_interface_types.d.ts │ │ │ │ └── canister_http_interface.ts │ │ │ └── streaming.ts │ │ ├── main.ts │ │ ├── servers │ │ │ ├── config │ │ │ │ ├── typings.ts │ │ │ │ └── index.ts │ │ │ ├── daemon │ │ │ │ ├── typings.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── net │ │ │ │ ├── typings.ts │ │ │ │ └── index.ts │ │ │ ├── icp │ │ │ │ ├── static.ts │ │ │ │ ├── typings.ts │ │ │ │ ├── index.ts │ │ │ │ ├── domains.ts │ │ │ │ └── utils.ts │ │ │ ├── typings.ts │ │ │ └── index.ts │ │ ├── errors │ │ │ ├── canister-not-found.ts │ │ │ ├── index.ts │ │ │ ├── missing-connection-host.ts │ │ │ ├── missing-certificate.ts │ │ │ ├── missing-requirements.ts │ │ │ └── not-allowed-redirect-error.ts │ │ └── start.ts │ ├── nodemon.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── README.md │ └── package.json └── core │ ├── src │ ├── ipc │ │ ├── index.ts │ │ ├── typings.ts │ │ ├── client.ts │ │ └── server.ts │ ├── main.ts │ ├── commons │ │ ├── index.ts │ │ ├── typings.ts │ │ ├── logger.ts │ │ ├── configs.ts │ │ └── utils.ts │ ├── tls │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── typings.ts │ │ ├── certificate.ts │ │ ├── store.ts │ │ └── factory.ts │ └── errors │ │ ├── index.ts │ │ ├── unsupported-certificate-type.ts │ │ ├── certificate-create-error.ts │ │ └── unsupported-platform.ts │ ├── nodemon.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── .github └── CODEOWNERS ├── .yarnrc.yml ├── .eslintrc.js ├── .gitignore ├── configs ├── tsconfig.base.json └── eslintrc.base.json ├── package.json ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /packages/daemon/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dfinity/trust 2 | -------------------------------------------------------------------------------- /packages/ui/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './images'; 2 | export * from './menu'; 3 | -------------------------------------------------------------------------------- /packages/server/src/commons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configs'; 2 | export * from './typings'; 3 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './factory'; 2 | export * from './typings'; 3 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/mac/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mac'; 2 | export * from './typings'; 3 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/linux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './linux'; 2 | export * from './typings'; 3 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/windows/index.ts: -------------------------------------------------------------------------------- 1 | export * from './windows'; 2 | export * from './typings'; 3 | -------------------------------------------------------------------------------- /packages/ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo.png -------------------------------------------------------------------------------- /packages/core/src/ipc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './server'; 3 | export * from './typings'; 4 | -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-dark.png -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-light.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo@124x124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo@124x124.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo@128x128.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo@128x128.ico -------------------------------------------------------------------------------- /packages/ui/src/assets/disabled@64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/disabled@64x64.png -------------------------------------------------------------------------------- /packages/ui/src/assets/enabled@64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/enabled@64x64.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo@128x128.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo@128x128.icns -------------------------------------------------------------------------------- /packages/ui/src/assets/logo@256x256.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo@256x256.icns -------------------------------------------------------------------------------- /packages/ui/src/assets/pending@64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/pending@64x64.png -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-Template.png -------------------------------------------------------------------------------- /packages/core/src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './commons'; 2 | export * from './errors'; 3 | export * from './tls'; 4 | export * from './ipc'; 5 | -------------------------------------------------------------------------------- /packages/ui/src/assets/exit-dark@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/exit-dark@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo-dark@124x124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo-dark@124x124.png -------------------------------------------------------------------------------- /packages/ui/src/assets/play-dark@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/play-dark@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-dark-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-dark-enabled.png -------------------------------------------------------------------------------- /packages/ui/src/assets/exit-light@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/exit-light@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo-light@124x124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo-light@124x124.png -------------------------------------------------------------------------------- /packages/ui/src/assets/pause-dark@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/pause-dark@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/pause-light@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/pause-light@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/play-light@128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/play-light@128x128.png -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-light-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-light-enabled.png -------------------------------------------------------------------------------- /packages/daemon/src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './platforms'; 2 | export * from './daemon'; 3 | export * from './typings'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /packages/ui/src/assets/tray-enabled-Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/tray-enabled-Template.png -------------------------------------------------------------------------------- /packages/core/src/commons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configs'; 2 | export * from './logger'; 3 | export * from './typings'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /packages/core/src/tls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './factory'; 2 | export * from './certificate'; 3 | export * from './typings'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /packages/ui/src/assets/logo-dark-enabled@124x124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo-dark-enabled@124x124.png -------------------------------------------------------------------------------- /packages/ui/src/assets/logo-light-enabled@124x124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/http-proxy/HEAD/packages/ui/src/assets/logo-light-enabled@124x124.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | -------------------------------------------------------------------------------- /packages/core/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './certificate-create-error'; 2 | export * from './unsupported-certificate-type'; 3 | export * from './unsupported-platform'; 4 | -------------------------------------------------------------------------------- /packages/server/src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './commons'; 2 | export * from './errors'; 3 | export * from './servers'; 4 | 5 | export const PROXY_ENTRYPOINT_FILENAME = 'start.js'; 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['./configs/eslintrc.base.json'], 4 | ignorePatterns: ['node_modules/*', 'built/*', 'data/*', '.eslintrc.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/daemon/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const CHECK_PROXY_PROCESS_MS = 3000; 2 | export const CHECK_PROXY_RUNNING_RETRIES = 3; 3 | export const DAEMON_ENTRYPOINT_FILENAME = 'start.js'; 4 | -------------------------------------------------------------------------------- /packages/core/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ignore": ["src/**/*.spec.ts"], 4 | "ext": "ts,json", 5 | "exec": "yarn build", 6 | "legacyWatch": true, 7 | "delay": 1000 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/servers/config/typings.ts: -------------------------------------------------------------------------------- 1 | export interface ProxyConfigOpts { 2 | host: string; 3 | port: number; 4 | proxyServer: { 5 | host: string; 6 | port: number; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ignore": ["src/**/*.spec.ts"], 4 | "ext": "ts,json", 5 | "exec": "yarn build && yarn start", 6 | "legacyWatch": true, 7 | "delay": 1000 8 | } 9 | -------------------------------------------------------------------------------- /packages/daemon/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ignore": ["src/**/*.spec.ts"], 4 | "ext": "ts,json", 5 | "exec": "yarn build && yarn start", 6 | "legacyWatch": true, 7 | "delay": 1000 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ignore": ["src/**/*.spec.ts"], 4 | "ext": "ts,json", 5 | "exec": "yarn build && yarn start", 6 | "legacyWatch": true, 7 | "delay": 1000 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/errors/canister-not-found.ts: -------------------------------------------------------------------------------- 1 | export class CanisterNotFoundError extends Error { 2 | constructor(host: string) { 3 | super(`Canister not found for ${host}`); 4 | 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/errors/unsupported-certificate-type.ts: -------------------------------------------------------------------------------- 1 | export class UnsupportedCertificateTypeError extends Error { 2 | constructor() { 3 | super(`Certificate type not supported.`); 4 | 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/typings.ts: -------------------------------------------------------------------------------- 1 | export interface ProxyUIOptions { 2 | darkMode: boolean; 3 | proxy: { 4 | entrypoint: string; 5 | }; 6 | } 7 | 8 | export enum ProxyStatus { 9 | Enabled = 'enbaled', 10 | Disabled = 'disabled', 11 | } 12 | -------------------------------------------------------------------------------- /packages/server/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './canister-not-found'; 2 | export * from './missing-certificate'; 3 | export * from './missing-connection-host'; 4 | export * from './missing-requirements'; 5 | export * from './not-allowed-redirect-error'; 6 | -------------------------------------------------------------------------------- /packages/server/src/errors/missing-connection-host.ts: -------------------------------------------------------------------------------- 1 | export class MissingConnectionHostError extends Error { 2 | constructor() { 3 | super(`The connection is missing the host information`); 4 | 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/errors/certificate-create-error.ts: -------------------------------------------------------------------------------- 1 | export class CertificateCreationFailedError extends Error { 2 | constructor(error: string) { 3 | super(`Certificate creation failed(${error})`); 4 | 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/src/errors/missing-certificate.ts: -------------------------------------------------------------------------------- 1 | export class MissingCertificateError extends Error { 2 | constructor(certName: string) { 3 | super(`The tls certificate for the ${certName} is missing`); 4 | 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["../../configs/eslintrc.base.json"], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: "./tsconfig.json", 6 | }, 7 | ignorePatterns: ["node_modules/*", "built/*", "data/*", ".eslintrc.js"], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/daemon/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["../../configs/eslintrc.base.json"], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: "./tsconfig.json", 6 | }, 7 | ignorePatterns: ["node_modules/*", "built/*", "data/*", ".eslintrc.js"], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["../../configs/eslintrc.base.json"], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: "./tsconfig.json", 6 | }, 7 | ignorePatterns: ["node_modules/*", "built/*", "data/*", ".eslintrc.js"], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["../../configs/eslintrc.base.json"], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: "./tsconfig.json", 6 | }, 7 | ignorePatterns: ["node_modules/*", "built/*", "data/*", ".eslintrc.js", "build/*"], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/server/src/servers/daemon/typings.ts: -------------------------------------------------------------------------------- 1 | export interface EnableProxyOptions { 2 | certificate: { 3 | commonName: string; 4 | path: string; 5 | }; 6 | proxy: { 7 | host: string; 8 | port: number; 9 | }; 10 | pac: { 11 | host: string; 12 | port: number; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/src/errors/missing-requirements.ts: -------------------------------------------------------------------------------- 1 | export class MissingRequirementsError extends Error { 2 | constructor(details?: string) { 3 | super( 4 | `Failed to set gateway requirements (${details ? details : 'unknown'})` 5 | ); 6 | 7 | this.name = this.constructor.name; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/errors/not-allowed-redirect-error.ts: -------------------------------------------------------------------------------- 1 | export class NotAllowedRequestRedirectError extends Error { 2 | constructor() { 3 | super( 4 | 'Due to security reasons redirects are blocked on the IC until further notice!' 5 | ); 6 | 7 | this.name = this.constructor.name; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src", 6 | "outDir": "./built", 7 | "paths": { 8 | "~src/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src", 6 | "outDir": "./built", 7 | "paths": { 8 | "~src/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/daemon/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src", 6 | "outDir": "./built", 7 | "paths": { 8 | "~src/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src", 6 | "outDir": "./built", 7 | "paths": { 8 | "~src/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/servers/net/typings.ts: -------------------------------------------------------------------------------- 1 | export interface NetProxyOpts { 2 | host: string; 3 | port: number; 4 | icpServer: { 5 | host: string; 6 | port: number; 7 | }; 8 | } 9 | 10 | export interface ConnectionInfo { 11 | host: string; 12 | port: number; 13 | secure: boolean; 14 | } 15 | 16 | export interface ServerInfo { 17 | host: string; 18 | port: number; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/ipc/typings.ts: -------------------------------------------------------------------------------- 1 | export interface IPCServerOptions { 2 | path: string; 3 | onMessage?: (event: EventMessage) => Promise; 4 | } 5 | 6 | export interface IPCClientOptions { 7 | path: string; 8 | } 9 | 10 | export type EventMessage = { type: string; skipWait?: boolean }; 11 | 12 | export type ResultMessage = { 13 | processed: boolean; 14 | data?: T; 15 | err?: string; 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | .DS_Store 10 | .pnpm_store 11 | .yarn/* 12 | !.yarn/plugins 13 | 14 | 15 | # Dependency directories 16 | node_modules 17 | 18 | # Cache 19 | *.tsbuildinfo 20 | .npm 21 | .eslintcache 22 | 23 | # Dotenv environment 24 | .env 25 | 26 | # Build folder 27 | built 28 | 29 | # Data storage 30 | data 31 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/utils.ts: -------------------------------------------------------------------------------- 1 | export const PAC_FILE_NAME = 'ic-proxy.pac'; 2 | 3 | export const getProxyAutoConfiguration = ( 4 | proxyHost: string, 5 | proxyPort: number 6 | ): string => { 7 | return `function FindProxyForURL(url, host) { 8 | if (url.startsWith("https:") || url.startsWith("http:")) { 9 | return "PROXY ${proxyHost}:${proxyPort}; DIRECT"; 10 | } 11 | 12 | return "DIRECT"; 13 | }`; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/src/commons/typings.ts: -------------------------------------------------------------------------------- 1 | export enum SupportedPlatforms { 2 | Windows = 'win32', 3 | MacOSX = 'darwin', 4 | Linux = 'linux', 5 | } 6 | 7 | export interface IpcChannels { 8 | daemon: string; 9 | proxy: string; 10 | } 11 | 12 | export interface CoreConfiguration { 13 | dataPath: string; 14 | platform: string; 15 | windows: boolean; 16 | macosx: boolean; 17 | linux: boolean; 18 | ipcChannels: IpcChannels; 19 | encoding: BufferEncoding; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/errors/unsupported-platform.ts: -------------------------------------------------------------------------------- 1 | import { SupportedPlatforms } from '../commons'; 2 | 3 | const supported = Object.values(SupportedPlatforms).join(','); 4 | 5 | export class UnsupportedPlatformError extends Error { 6 | constructor(platform: string) { 7 | super( 8 | `The current platform(${platform}) is not supported. Please use one of the following supported platforms(${supported})` 9 | ); 10 | 11 | this.name = this.constructor.name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /configs/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "allowJs": false, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": false, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/typings.ts: -------------------------------------------------------------------------------- 1 | export interface Platform { 2 | attach(): Promise; 3 | detach(): Promise; 4 | } 5 | 6 | export interface PlatformRootCA { 7 | commonName: string; 8 | path: string; 9 | } 10 | 11 | export interface PlatformProxyInfo { 12 | host: string; 13 | port: number; 14 | } 15 | 16 | export type PlatformPacInfo = PlatformProxyInfo; 17 | 18 | export interface PlatformBuildConfigs { 19 | platform: string; 20 | ca: PlatformRootCA; 21 | proxy: PlatformProxyInfo; 22 | pac: PlatformPacInfo; 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { platform } = require('node:process'); 4 | const macBuild = require('./build/mac'); 5 | const winBuild = require('./build/win'); 6 | const linuxBuild = require('./build/linux'); 7 | 8 | switch (platform) { 9 | case 'win32': { 10 | winBuild(); 11 | break; 12 | } 13 | case 'darwin': { 14 | macBuild(); 15 | break; 16 | } 17 | case 'linux': { 18 | linuxBuild(); 19 | break; 20 | } 21 | default: { 22 | throw new Error("Unsupported platform. Only Mac and Windows are currently supported."); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/build/win.ps1: -------------------------------------------------------------------------------- 1 | $modifyfiles = Get-ChildItem -Path $args[0] -Recurse -Force 2 | foreach($object in $modifyfiles) 3 | { 4 | $object.CreationTime=("11/11/2011 12:00:00") 5 | $object.LastAccessTime=("11/11/2011 12:00:00") 6 | $object.LastWritetime=("11/11/2011 12:00:00") 7 | } 8 | 9 | $modifyfiles = Get-Item -Path $args[0] -Force 10 | foreach($object in $modifyfiles) 11 | { 12 | $object.CreationTime=("11/11/2011 12:00:00") 13 | $object.LastAccessTime=("11/11/2011 12:00:00") 14 | $object.LastWritetime=("11/11/2011 12:00:00") 15 | } 16 | 17 | 18 | echo "All dates modified" 19 | -------------------------------------------------------------------------------- /packages/server/src/servers/icp/static.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | 3 | export const CANISTER_DNS_PREFIX = '_canister-id'; 4 | export const DEFAULT_GATEWAY = new URL('https://icp-api.io'); 5 | 6 | export const hostnameCanisterIdMap: Map = new Map( 7 | Object.entries({ 8 | 'oc.app': Principal.from('6hsbt-vqaaa-aaaaf-aaafq-cai'), 9 | 'identity.ic0.app': Principal.from('rdmx6-jaaaa-aaaaa-aaadq-cai'), 10 | 'nns.ic0.app': Principal.from('qoctq-giaaa-aaaaa-aaaea-cai'), 11 | 'nns.icp': Principal.from('qoctq-giaaa-aaaaa-aaaea-cai'), // this is a crypto domain 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/linux/typings.ts: -------------------------------------------------------------------------------- 1 | import { PlatformPacInfo, PlatformRootCA } from '../typings'; 2 | 3 | export interface PlatformProxyInfo { 4 | host: string; 5 | port: number; 6 | } 7 | 8 | export interface PlatformConfigs { 9 | ca: PlatformRootCA; 10 | proxy: PlatformProxyInfo; 11 | pac: PlatformPacInfo; 12 | } 13 | 14 | export interface SystemWebProxyInfo { 15 | enabled: boolean; 16 | } 17 | 18 | export interface WebProxyConfiguration { 19 | https: SystemWebProxyInfo; 20 | http: SystemWebProxyInfo; 21 | } 22 | 23 | export interface NetworkProxySetup { 24 | [networkPort: string]: WebProxyConfiguration; 25 | } 26 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/mac/typings.ts: -------------------------------------------------------------------------------- 1 | import { PlatformPacInfo, PlatformRootCA } from '../typings'; 2 | 3 | export interface PlatformProxyInfo { 4 | host: string; 5 | port: number; 6 | } 7 | 8 | export interface PlatformConfigs { 9 | ca: PlatformRootCA; 10 | proxy: PlatformProxyInfo; 11 | pac: PlatformPacInfo; 12 | } 13 | 14 | export interface SystemWebProxyInfo { 15 | enabled: boolean; 16 | } 17 | 18 | export interface WebProxyConfiguration { 19 | https: SystemWebProxyInfo; 20 | http: SystemWebProxyInfo; 21 | } 22 | 23 | export interface NetworkProxySetup { 24 | [networkPort: string]: WebProxyConfiguration; 25 | } 26 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/windows/typings.ts: -------------------------------------------------------------------------------- 1 | import { PlatformPacInfo, PlatformRootCA } from '../typings'; 2 | 3 | export interface PlatformProxyInfo { 4 | host: string; 5 | port: number; 6 | } 7 | 8 | export interface PlatformConfigs { 9 | ca: PlatformRootCA; 10 | proxy: PlatformProxyInfo; 11 | pac: PlatformPacInfo; 12 | } 13 | 14 | export interface SystemWebProxyInfo { 15 | enabled: boolean; 16 | } 17 | 18 | export interface WebProxyConfiguration { 19 | https: SystemWebProxyInfo; 20 | http: SystemWebProxyInfo; 21 | } 22 | 23 | export interface NetworkProxySetup { 24 | [networkPort: string]: WebProxyConfiguration; 25 | } 26 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/windows/utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | 3 | export const POWERSHELL_ESCAPED_QUOTES = '\\`"'; 4 | export const FIREFOX_PROFILES_PATH = 'Mozilla\\Firefox\\Profiles'; 5 | 6 | export const escapeString = (argument: string): string => { 7 | return POWERSHELL_ESCAPED_QUOTES + argument + POWERSHELL_ESCAPED_QUOTES; 8 | }; 9 | 10 | export const isTrustedCertificate = async ( 11 | certificateId: string 12 | ): Promise => { 13 | return new Promise((ok) => { 14 | exec(`certutil -verifystore root ${certificateId}`, (error) => { 15 | ok(error ? false : true); 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/server/src/commons/typings.ts: -------------------------------------------------------------------------------- 1 | import { CertificateConfiguration } from '@dfinity/http-proxy-core'; 2 | 3 | export interface ICPServerConfiguration { 4 | host: string; 5 | port: number; 6 | } 7 | 8 | export interface NetServerConfiguration { 9 | host: string; 10 | port: number; 11 | } 12 | 13 | export type ProxyConfigServerConfiguration = NetServerConfiguration; 14 | 15 | export interface EnvironmentConfiguration { 16 | userAgent: string; 17 | platform: string; 18 | certificate: CertificateConfiguration; 19 | proxyConfigServer: ProxyConfigServerConfiguration; 20 | netServer: NetServerConfiguration; 21 | icpServer: ICPServerConfiguration; 22 | } 23 | -------------------------------------------------------------------------------- /packages/daemon/README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](../../LICENSE) 2 | 3 | # @dfinity/http-proxy-daemon 4 | 5 | ## Overview 6 | 7 | Background process responsible for executing tasks received from the main proxy process such as adding/removing a certificate to the device trusted store. The daemon process is also responsbile for cleaning up the system from the proxy configuration once the main process has exited. 8 | 9 | ## Setup 10 | 11 | Build and start the `daemon` process. 12 | ```bash 13 | yarn build 14 | yarn start 15 | ``` 16 | 17 | Starts a `watch` process that will reload on changes. 18 | ```bash 19 | yarn dev 20 | ``` 21 | -------------------------------------------------------------------------------- /configs/eslintrc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:prettier/recommended" 6 | ], 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "prettier" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "rules": { 13 | "no-underscore-dangle": 0, 14 | "no-async-promise-executor": 0, 15 | "prettier/prettier": [ 16 | "error", 17 | { 18 | "trailingComma": "es5", 19 | "singleQuote": true, 20 | "printWidth": 80, 21 | "tabWidth": 2, 22 | "endOfLine": "auto", 23 | "arrowParens": "always" 24 | }, 25 | { 26 | "usePrettierrc": false 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/daemon/src/typings.ts: -------------------------------------------------------------------------------- 1 | import { IpcChannels } from '@dfinity/http-proxy-core'; 2 | 3 | export interface DaemonConfiguration { 4 | ipcChannels: IpcChannels; 5 | platform: string; 6 | } 7 | 8 | export enum MessageType { 9 | EnableProxy = 'enable-proxy', 10 | DisableProxy = 'disable-proxy', 11 | IsProxyEnabled = 'is-proxy-enabled', 12 | } 13 | 14 | export interface EnableProxyMessage { 15 | type: MessageType.EnableProxy; 16 | host: string; 17 | port: number; 18 | pac: { 19 | host: string; 20 | port: number; 21 | }; 22 | certificatePath: string; 23 | commonName: string; 24 | } 25 | 26 | export interface ProxyEnabledResponse { 27 | enabled: boolean; 28 | } 29 | 30 | export type OnMessageResponse = void | ProxyEnabledResponse; 31 | -------------------------------------------------------------------------------- /packages/ui/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const fs = require('fs'); 5 | 6 | const hashReleaseFile = (releaseFiles) => { 7 | const [releaseFile] = releaseFiles.filter( 8 | (file) => !file.endsWith('.blockmap') 9 | ); 10 | 11 | const buffer = fs.readFileSync(releaseFile); 12 | const hashSum = crypto.createHash('sha256'); 13 | hashSum.update(buffer); 14 | 15 | const hash = hashSum.digest('hex'); 16 | 17 | return { file: releaseFile, hash }; 18 | }; 19 | 20 | const createReleaseHashFile = (releaseFiles) => { 21 | const result = hashReleaseFile(releaseFiles); 22 | 23 | fs.writeFileSync(`${result.file}.sha256.txt`, result.hash); 24 | }; 25 | 26 | module.exports = { hashReleaseFile, createReleaseHashFile }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/main.ts: -------------------------------------------------------------------------------- 1 | // Initializes the logger with the correct context 2 | import { initLogger, coreConfigs, logger } from '@dfinity/http-proxy-core'; 3 | initLogger('IC HTTP Proxy UI', 'ui', coreConfigs.dataPath); 4 | 5 | import { app } from 'electron'; 6 | import { ProxyUI } from '~src/proxy-ui'; 7 | 8 | app.whenReady().then(() => { 9 | logger.info('Preparing interface'); 10 | 11 | // this app should only be available in the menubar 12 | app.dock?.hide(); 13 | 14 | ProxyUI.init() 15 | .then(() => { 16 | logger.info('Interface is ready'); 17 | }) 18 | .catch((e) => { 19 | logger.error(`Interface failed to render ${String(e)}`); 20 | }); 21 | }); 22 | 23 | app.on('quit', () => { 24 | logger.info('Exiting interface'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/server/src/commons/configs.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { EnvironmentConfiguration } from './typings'; 3 | 4 | const environment: EnvironmentConfiguration = { 5 | platform: os.platform(), 6 | userAgent: 'ICHttpProxy/0.0.6-alpha', 7 | certificate: { 8 | storage: { 9 | folder: 'certs', 10 | hostPrefix: 'host', 11 | }, 12 | rootca: { 13 | commonName: 'IC Proxy CA', 14 | organizationName: 'IC Proxy', 15 | organizationUnit: 'IC', 16 | }, 17 | }, 18 | proxyConfigServer: { 19 | host: '127.0.0.1', 20 | port: 4049, 21 | }, 22 | netServer: { 23 | host: '127.0.0.1', 24 | port: 4050, 25 | }, 26 | icpServer: { 27 | host: '127.0.0.1', 28 | port: 4051, 29 | }, 30 | }; 31 | 32 | export { environment }; 33 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](../../LICENSE) 2 | 3 | # @dfinity/http-proxy-server 4 | 5 | ## Overview 6 | 7 | Proxy server implementing the IC HTTP Gateway protocol that detects if a domain is serving a dApp from the internet computer and handles the conversion between API calls and HTTP Asset Requests while performing response verification locally. 8 | 9 | The proxy can also resolve crypto custom domains to it's respective canister id if provided [here](src/servers/icp/static.ts). 10 | 11 | ## Setup 12 | 13 | Build and start the `server` process. 14 | ```bash 15 | yarn build 16 | yarn start 17 | ``` 18 | 19 | Starts a `watch` process that will reload on changes. 20 | ```bash 21 | yarn dev 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](../../LICENSE) 2 | 3 | # @dfinity/http-proxy-ui 4 | 5 | ## Overview 6 | 7 | Graphical interface that facilitates the usage of the proxy within the supported operating systems. The interface will create a `menubar` added to the system that shows the current status of the proxy and adds a `start/stop` capability. 8 | 9 | ## Setup 10 | 11 | Build and start the `ui`. 12 | ```bash 13 | yarn build 14 | yarn start 15 | ``` 16 | 17 | Starts a `watch` process that will reload on changes. 18 | ```bash 19 | yarn dev 20 | ``` 21 | 22 | ## Packaging 23 | 24 | Generates the bundle of the application that can be installed natively in the supported operating systems. 25 | 26 | ```bash 27 | yarn pkg 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/core/src/ipc/client.ts: -------------------------------------------------------------------------------- 1 | import net from 'node:net'; 2 | import { 3 | EventMessage, 4 | IPCClientOptions, 5 | ResultMessage, 6 | } from '~src/ipc/typings'; 7 | 8 | export class IPCClient { 9 | public constructor(private readonly options: IPCClientOptions) {} 10 | 11 | public async sendMessage(event: EventMessage): Promise> { 12 | return new Promise>((ok, err) => { 13 | const socket = net.createConnection({ path: this.options.path }, () => { 14 | socket.write(JSON.stringify(event)); 15 | 16 | socket.on('data', (data) => { 17 | const result = JSON.parse(data.toString()) as ResultMessage; 18 | 19 | ok(result); 20 | }); 21 | }); 22 | 23 | socket.on('error', (error) => { 24 | err(error); 25 | }); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/mac/utils.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | export const SHELL_SCRIPT_SEPARATOR = ' ; '; 4 | export const CURL_RC_FILE = '.curlrc'; 5 | export const PROXY_GET_SEPARATOR = ':ic-separator:'; 6 | export const FIREFOX_PROFILES_PATH = `Library/Application Support/Firefox/profiles`; 7 | 8 | export const getActiveNetworkService = (): string | null => { 9 | const networkServices = execSync( 10 | `networksetup -listallnetworkservices | tail -n +2` 11 | ) 12 | .toString() 13 | .split('\n'); 14 | for (const networkService of networkServices) { 15 | const assignedIpAddress = execSync( 16 | `networksetup -getinfo "${networkService}" | awk '/^IP address:/{print $3}'` 17 | ) 18 | .toString() 19 | .trim(); 20 | if (assignedIpAddress.length > 0) { 21 | return networkService; 22 | } 23 | } 24 | 25 | return null; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/linux/utils.ts: -------------------------------------------------------------------------------- 1 | import { execAsync } from '@dfinity/http-proxy-core'; 2 | 3 | export const BASE_SNAP_MOZZILA_PATH = 'snap/firefox/common/.mozilla'; 4 | export const BASE_MOZILLA_PATH = '.mozilla'; 5 | export const MOZILLA_CERTIFICATES_FOLDER = 'certificates'; 6 | export const FIREFOX_PROFILES_FOLDER = `firefox`; 7 | export const ROOT_CA_STORE_PATH = '/usr/local/share/ca-certificates'; 8 | export const ROOT_CA_PATH = `${ROOT_CA_STORE_PATH}/ic-http-proxy-root-ca.crt`; 9 | export const CURL_RC_FILE = '.curlrc'; 10 | 11 | export const findP11KitTrustPath = async (): Promise => { 12 | const path = await execAsync('sudo find /usr -name p11-kit-trust.so'); 13 | 14 | return path.length ? path : null; 15 | }; 16 | 17 | export const findDbusLaunchPath = async (): Promise => { 18 | const path = await execAsync('which dbus-launch'); 19 | 20 | return path.length ? path : null; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/commons/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import { tmpdir } from 'os'; 3 | import { createStream } from 'rotating-file-stream'; 4 | 5 | const defaultLogFileName = `main`; 6 | let logger: pino.Logger; 7 | 8 | export const initLogger = ( 9 | name = 'IC HTTP Proxy', 10 | logName: string = defaultLogFileName, 11 | logFolder = tmpdir() 12 | ): void => { 13 | logger = pino( 14 | { 15 | name, 16 | level: process.env.LOG_LEVEL ?? 'trace', 17 | timestamp: (): string => { 18 | return `, "time": "${new Date().toISOString()}"`; 19 | }, 20 | }, 21 | pino.multistream([ 22 | { 23 | stream: createStream(`ic-http-proxy-${logName}.log`, { 24 | size: '10M', 25 | compress: 'gzip', 26 | maxFiles: 5, 27 | path: logFolder, 28 | }), 29 | }, 30 | { stream: process.stdout }, 31 | ]) 32 | ); 33 | }; 34 | 35 | export { logger }; 36 | 37 | export default initLogger; 38 | -------------------------------------------------------------------------------- /packages/daemon/src/start.ts: -------------------------------------------------------------------------------- 1 | // Initializes the logger with the correct context 2 | import { initLogger, coreConfigs } from '@dfinity/http-proxy-core'; 3 | initLogger('IC HTTP Proxy Daemon', 'daemon', coreConfigs.dataPath); 4 | 5 | import { logger } from '@dfinity/http-proxy-core'; 6 | import { Daemon } from './daemon'; 7 | 8 | process.on('uncaughtException', (err) => { 9 | logger.error(`Uncaught Exception: ${String(err)}`); 10 | process.exit(1); 11 | }); 12 | 13 | process.on('unhandledRejection', (reason) => { 14 | logger.error(`Unhandled rejection at reason: ${reason}`); 15 | process.exit(1); 16 | }); 17 | 18 | (async (): Promise => { 19 | try { 20 | const daemon = await Daemon.create({ 21 | ipcChannels: coreConfigs.ipcChannels, 22 | platform: coreConfigs.platform, 23 | }); 24 | 25 | await daemon.start(); 26 | 27 | logger.info('Waiting for tasks'); 28 | 29 | process.on('SIGINT', async () => await daemon.shutdown()); 30 | } catch (e) { 31 | logger.error(`Failed to start (${String(e)})`); 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /packages/ui/src/commons/utils.ts: -------------------------------------------------------------------------------- 1 | import { wait } from '@dfinity/http-proxy-core'; 2 | import { PROXY_ENTRYPOINT_FILENAME } from '@dfinity/http-proxy-server'; 3 | import { dirname, resolve } from 'node:path'; 4 | 5 | export const proxyNodeEntrypointPath = async (): Promise => { 6 | const proxyPackage = require.resolve('@dfinity/http-proxy-server'); 7 | const entrypointPath = resolve( 8 | dirname(proxyPackage), 9 | PROXY_ENTRYPOINT_FILENAME 10 | ); 11 | 12 | return entrypointPath; 13 | }; 14 | 15 | export const waitProcessing = async ( 16 | stopConditionFn: () => Promise, 17 | timeoutMs = 10000, 18 | checkIntervalMs = 250 19 | ): Promise => { 20 | if (checkIntervalMs > timeoutMs) { 21 | throw new Error(`Check interval must be lower then the timeout`); 22 | } 23 | 24 | let shouldStop = false; 25 | let timeSpent = 0; 26 | do { 27 | await wait(checkIntervalMs); 28 | timeSpent += checkIntervalMs; 29 | 30 | shouldStop = await stopConditionFn(); 31 | } while (!shouldStop && timeSpent < timeoutMs); 32 | 33 | return shouldStop; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/factory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SupportedPlatforms, 3 | UnsupportedPlatformError, 4 | } from '@dfinity/http-proxy-core'; 5 | import { MacPlatform } from './mac'; 6 | import { Platform, PlatformBuildConfigs } from './typings'; 7 | import { WindowsPlatform } from './windows'; 8 | import { LinuxPlatform } from './linux'; 9 | 10 | export class PlatformFactory { 11 | public static async create(configs: PlatformBuildConfigs): Promise { 12 | switch (configs.platform) { 13 | case SupportedPlatforms.MacOSX: 14 | return new MacPlatform({ 15 | ca: configs.ca, 16 | proxy: configs.proxy, 17 | pac: configs.pac, 18 | }); 19 | case SupportedPlatforms.Windows: 20 | return new WindowsPlatform({ 21 | ca: configs.ca, 22 | proxy: configs.proxy, 23 | pac: configs.pac, 24 | }); 25 | case SupportedPlatforms.Linux: 26 | return new LinuxPlatform({ 27 | ca: configs.ca, 28 | proxy: configs.proxy, 29 | pac: configs.pac, 30 | }); 31 | default: 32 | throw new UnsupportedPlatformError('unknown'); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/server/src/servers/typings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CertificateConfiguration, 3 | IpcChannels, 4 | } from '@dfinity/http-proxy-core'; 5 | import { 6 | ICPServerConfiguration, 7 | NetServerConfiguration, 8 | ProxyConfigServerConfiguration, 9 | } from '~src/commons'; 10 | 11 | export interface ProxyServersOptions { 12 | certificate: CertificateConfiguration; 13 | proxyConfigServer: ProxyConfigServerConfiguration; 14 | netServer: NetServerConfiguration; 15 | icpServer: ICPServerConfiguration; 16 | ipcChannels: IpcChannels; 17 | autoEnable?: boolean; 18 | } 19 | 20 | export interface IsRunningMessageResponse { 21 | running: boolean; 22 | } 23 | 24 | export interface IsStartedMessageResponse { 25 | isShuttingDown: boolean; 26 | } 27 | 28 | export type StopMessageResponse = void; 29 | 30 | export type MessageResponse = 31 | | void 32 | | IsRunningMessageResponse 33 | | StopMessageResponse 34 | | IsStartedMessageResponse; 35 | 36 | export enum MessageType { 37 | // Process has started 38 | IsStarted = 'is-started', 39 | // Proxy is attached to the system 40 | IsRunning = 'is-running', 41 | // Shutdown proxy 42 | Stop = 'stop', 43 | // Should enable proxy 44 | Enable = 'enable', 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/commons/http-interface/canister_http_interface.did: -------------------------------------------------------------------------------- 1 | // adapted from https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-gateway-interface 2 | 3 | type HeaderField = record { text; text; }; 4 | 5 | type HttpRequest = record { 6 | method: text; 7 | url: text; 8 | headers: vec HeaderField; 9 | body: blob; 10 | certificate_version: opt nat16; 11 | }; 12 | 13 | type HttpUpdateRequest = record { 14 | method: text; 15 | url: text; 16 | headers: vec HeaderField; 17 | body: blob; 18 | }; 19 | 20 | type HttpResponse = record { 21 | status_code: nat16; 22 | headers: vec HeaderField; 23 | body: blob; 24 | upgrade : opt bool; 25 | streaming_strategy: opt StreamingStrategy; 26 | }; 27 | 28 | type Token = variant { 29 | "type": reserved; 30 | }; 31 | 32 | type StreamingCallbackHttpResponse = record { 33 | body: blob; 34 | token: opt Token; 35 | }; 36 | 37 | type StreamingStrategy = variant { 38 | Callback: record { 39 | callback: func (Token) -> (opt StreamingCallbackHttpResponse) query; 40 | token: Token; 41 | }; 42 | }; 43 | 44 | service : { 45 | http_request: (request: HttpRequest) -> (HttpResponse) query; 46 | http_request_update: (request: HttpUpdateRequest) -> (HttpResponse); 47 | } -------------------------------------------------------------------------------- /packages/server/src/commons/http-interface/canister_http_interface_types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | import type { ActorMethod } from '@dfinity/agent'; 3 | import { IDL } from '@dfinity/candid'; 4 | 5 | export type HeaderField = [string, string]; 6 | export interface HttpRequest { 7 | url: string; 8 | method: string; 9 | body: Uint8Array; 10 | headers: Array; 11 | certificate_version: [] | [number]; 12 | } 13 | export interface HttpResponse { 14 | body: Uint8Array; 15 | headers: Array; 16 | upgrade: [] | [boolean]; 17 | streaming_strategy: [] | [StreamingStrategy]; 18 | status_code: number; 19 | } 20 | export interface HttpUpdateRequest { 21 | url: string; 22 | method: string; 23 | body: Uint8Array; 24 | headers: Array; 25 | } 26 | export interface StreamingCallbackHttpResponse { 27 | token: [] | [Token]; 28 | body: Uint8Array; 29 | } 30 | export type StreamingStrategy = { 31 | Callback: { token: Token; callback: [Principal, string] }; 32 | }; 33 | export type Token = { type: () => IDL.Type }; 34 | export interface _SERVICE { 35 | http_request: ActorMethod<[HttpRequest], HttpResponse>; 36 | http_request_update: ActorMethod<[HttpUpdateRequest], HttpResponse>; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/tls/utils.ts: -------------------------------------------------------------------------------- 1 | import { md, pki } from 'node-forge'; 2 | import { GenerateCertificateOpts, GenerateKeyPairOpts } from './typings'; 3 | 4 | export const DEFAULT_KEY_PAIR_BITS = 2048; 5 | 6 | export const generateKeyPair = async ( 7 | opts?: GenerateKeyPairOpts 8 | ): Promise => { 9 | return pki.rsa.generateKeyPair(opts?.bits ?? DEFAULT_KEY_PAIR_BITS); 10 | }; 11 | 12 | export const createValidityDate = (addDays = 0): Date => { 13 | const current = new Date(); 14 | current.setDate(current.getDate() + addDays); 15 | 16 | return current; 17 | }; 18 | 19 | export const generateCertificate = async ({ 20 | publicKey, 21 | subject, 22 | issuer, 23 | extensions, 24 | signingKey, 25 | serialId, 26 | }: GenerateCertificateOpts): Promise => { 27 | const certificate = pki.createCertificate(); 28 | 29 | certificate.publicKey = publicKey; 30 | certificate.serialNumber = serialId; 31 | certificate.validity.notBefore = createValidityDate(); 32 | certificate.validity.notAfter = createValidityDate(365); 33 | 34 | certificate.setSubject(subject); 35 | certificate.setIssuer(issuer); 36 | certificate.setExtensions(extensions); 37 | 38 | certificate.sign(signingKey, md.sha256.create()); 39 | 40 | return pki.certificateToPem(certificate); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/core/src/tls/typings.ts: -------------------------------------------------------------------------------- 1 | import { pki } from 'node-forge'; 2 | import { Certificate } from './certificate'; 3 | 4 | export interface KeyPair { 5 | private: string; 6 | public: string; 7 | } 8 | 9 | export interface CertificateOpts { 10 | key: pki.PrivateKey; 11 | public: pki.PublicKey; 12 | pem: string; 13 | } 14 | 15 | export interface CertificateConfiguration { 16 | creationRetries?: number; 17 | storage: { 18 | hostPrefix: string; 19 | folder: string; 20 | }; 21 | rootca: { 22 | commonName: string; 23 | organizationName: string; 24 | organizationUnit: string; 25 | }; 26 | } 27 | 28 | export type CreateCertificateOpts = 29 | | { 30 | type: 'ca'; 31 | } 32 | | { 33 | type: 'domain'; 34 | hostname: string; 35 | ca: Certificate; 36 | }; 37 | 38 | export interface GenerateKeyPairOpts { 39 | bits?: number; 40 | } 41 | 42 | export interface GenerateCertificateOpts { 43 | publicKey: pki.PublicKey; 44 | subject: pki.CertificateField[]; 45 | issuer: pki.CertificateField[]; 46 | extensions: object[]; 47 | signingKey: pki.PrivateKey; 48 | serialId: string; 49 | } 50 | 51 | export interface CertificateStoreConfiguration { 52 | folder: string; 53 | } 54 | 55 | export interface CertificateDTO { 56 | id: string; 57 | pem: { 58 | key: string; 59 | publicKey: string; 60 | cert: string; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/src/commons/configs.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'node:fs'; 2 | import os from 'node:os'; 3 | import { join, resolve } from 'node:path'; 4 | import { CoreConfiguration, SupportedPlatforms } from './typings'; 5 | 6 | const platform = os.platform(); 7 | 8 | if (!process.env.HOME && !process.env.APPDATA) { 9 | throw new Error('Missing user data folder'); 10 | } 11 | 12 | const platformDataFolder = 13 | process.env.APPDATA || 14 | (process.platform === 'darwin' 15 | ? resolve(String(process.env.HOME), 'Library', 'Preferences') 16 | : resolve(String(process.env.HOME), '.local', 'share')); 17 | 18 | const dataPath = resolve(platformDataFolder, 'dfinity', 'ichttpproxy'); 19 | 20 | if (!existsSync(dataPath)) { 21 | mkdirSync(dataPath, { recursive: true }); 22 | } 23 | 24 | const isMaxOSX = platform === SupportedPlatforms.MacOSX; 25 | const isWindows = platform === SupportedPlatforms.Windows; 26 | const isLinux = platform === SupportedPlatforms.Linux; 27 | 28 | const coreConfigs: CoreConfiguration = { 29 | dataPath, 30 | platform, 31 | macosx: isMaxOSX, 32 | windows: isWindows, 33 | linux: isLinux, 34 | encoding: isWindows ? 'utf16le' : 'utf8', 35 | ipcChannels: { 36 | daemon: isWindows 37 | ? join('\\\\.\\pipe\\', 'daemon_pipe') 38 | : '/tmp/ic-http-daemon.sock', 39 | proxy: isWindows 40 | ? join('\\\\.\\pipe\\', 'proxy_pipe') 41 | : '/tmp/ic-http-proxy.sock', 42 | }, 43 | }; 44 | 45 | export { coreConfigs }; 46 | -------------------------------------------------------------------------------- /packages/server/src/commons/http-interface/canister_http_interface.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | 3 | const Token = IDL.Unknown; 4 | 5 | export const streamingCallbackHttpResponseType = IDL.Record({ 6 | token: IDL.Opt(Token), 7 | body: IDL.Vec(IDL.Nat8), 8 | }); 9 | 10 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 11 | const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); 12 | const HttpRequest = IDL.Record({ 13 | url: IDL.Text, 14 | method: IDL.Text, 15 | body: IDL.Vec(IDL.Nat8), 16 | headers: IDL.Vec(HeaderField), 17 | certificate_version: IDL.Opt(IDL.Nat16), 18 | }); 19 | const StreamingStrategy = IDL.Variant({ 20 | Callback: IDL.Record({ 21 | token: Token, 22 | callback: IDL.Func( 23 | [Token], 24 | [IDL.Opt(streamingCallbackHttpResponseType)], 25 | ['query'] 26 | ), 27 | }), 28 | }); 29 | const HttpResponse = IDL.Record({ 30 | body: IDL.Vec(IDL.Nat8), 31 | headers: IDL.Vec(HeaderField), 32 | upgrade: IDL.Opt(IDL.Bool), 33 | streaming_strategy: IDL.Opt(StreamingStrategy), 34 | status_code: IDL.Nat16, 35 | }); 36 | const HttpUpdateRequest = IDL.Record({ 37 | url: IDL.Text, 38 | method: IDL.Text, 39 | body: IDL.Vec(IDL.Nat8), 40 | headers: IDL.Vec(HeaderField), 41 | }); 42 | return IDL.Service({ 43 | http_request: IDL.Func([HttpRequest], [HttpResponse], ['query']), 44 | http_request_update: IDL.Func([HttpUpdateRequest], [HttpResponse], []), 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/server/src/start.ts: -------------------------------------------------------------------------------- 1 | // Initializes the logger with the correct context 2 | import { coreConfigs, initLogger } from '@dfinity/http-proxy-core'; 3 | initLogger('IC HTTP Proxy Server', 'proxy', coreConfigs.dataPath); 4 | 5 | import { 6 | UnsupportedPlatformError, 7 | isSupportedPlatform, 8 | logger, 9 | } from '@dfinity/http-proxy-core'; 10 | import { environment } from '~src/commons'; 11 | import { ProxyServers } from './servers'; 12 | 13 | process.on('uncaughtException', (err) => { 14 | logger.error(`Uncaught Exception: ${String(err)}`); 15 | process.exit(1); 16 | }); 17 | 18 | process.on('unhandledRejection', (reason) => { 19 | logger.error(`Unhandled rejection at reason: ${reason}`); 20 | process.exit(1); 21 | }); 22 | 23 | (async (): Promise => { 24 | let servers: ProxyServers | null = null; 25 | try { 26 | if (!isSupportedPlatform(environment.platform)) { 27 | throw new UnsupportedPlatformError(environment.platform); 28 | } 29 | 30 | // setting up proxy servers requirements 31 | logger.info('Preparing system requirements'); 32 | servers = await ProxyServers.create({ 33 | certificate: environment.certificate, 34 | proxyConfigServer: environment.proxyConfigServer, 35 | icpServer: environment.icpServer, 36 | netServer: environment.netServer, 37 | ipcChannels: coreConfigs.ipcChannels, 38 | autoEnable: process.argv.includes('--enable'), 39 | }); 40 | 41 | process.on('SIGINT', async () => await servers?.shutdown()); 42 | 43 | // start proxy servers 44 | await servers.start(); 45 | 46 | logger.info('IC HTTP Proxy servers listening'); 47 | } catch (e) { 48 | logger.error(`Failed to start (${String(e)})`); 49 | 50 | servers?.shutdown(); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/http-proxy", 3 | "version": "0.0.6-alpha", 4 | "description": "HTTP Proxy to enable trustless access to the Internet Computer.", 5 | "author": "Kepler Vital ", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "workspaces": [ 9 | "packages/core", 10 | "packages/daemon", 11 | "packages/server", 12 | "packages/ui" 13 | ], 14 | "homepage": "https://github.com/dfinity/http-proxy#readme", 15 | "repository": { 16 | "url": "https://github.com/dfinity/http-proxy.git", 17 | "type": "git" 18 | }, 19 | "keywords": [ 20 | "Internet Computer", 21 | "IC", 22 | "Proxy", 23 | "HTTP", 24 | "HTTP Proxy", 25 | "IC Response Verification" 26 | ], 27 | "engines": { 28 | "node": ">=18", 29 | "npm": "please-use-yarn", 30 | "pnpm": "please-use-yarn", 31 | "yarn": "~3" 32 | }, 33 | "scripts": { 34 | "clean": "find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \\;", 35 | "build": "yarn workspaces foreach --verbose run build", 36 | "start": "yarn workspace @dfinity/http-proxy-ui run start", 37 | "pkg": "yarn workspaces foreach --verbose run pkg", 38 | "lint": "yarn workspaces foreach --verbose --parallel run lint", 39 | "lint:fix": "yarn workspaces foreach --verbose --parallel run lint:fix" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^18.14.0", 43 | "@typescript-eslint/eslint-plugin": "^5.54.1", 44 | "@typescript-eslint/parser": "^5.54.1", 45 | "eslint": "^8.36.0", 46 | "eslint-config-prettier": "^8.7.0", 47 | "eslint-plugin-prettier": "^4.2.1", 48 | "nodemon": "^2.0.20", 49 | "prettier": "^2.8.4", 50 | "tsc-alias": "^1.8.5", 51 | "typescript": "^4.9.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/http-proxy-core", 3 | "version": "0.0.6-alpha", 4 | "description": "Gateway server to enable trustless access to the Internet Computer.", 5 | "main": "built/main.js", 6 | "types": "built/main.d.ts", 7 | "author": "Kepler Vital ", 8 | "license": "Apache-2.0", 9 | "homepage": "https://github.com/dfinity/http-proxy/tree/main/packages/core#readme", 10 | "repository": { 11 | "url": "https://github.com/dfinity/http-proxy.git", 12 | "type": "git", 13 | "directory": "packages/core" 14 | }, 15 | "scripts": { 16 | "dev": "nodemon", 17 | "build": "tsc && tsc-alias", 18 | "lint": "eslint --ext ts,js src", 19 | "lint:fix": "eslint --ext ts,js --fix src" 20 | }, 21 | "keywords": [ 22 | "Internet Computer", 23 | "IC", 24 | "Proxy", 25 | "HTTP", 26 | "HTTP Proxy", 27 | "IC Response Verification" 28 | ], 29 | "engines": { 30 | "node": ">=18", 31 | "npm": "please-use-yarn", 32 | "pnpm": "please-use-yarn", 33 | "yarn": "~3" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^18.14.0", 37 | "@types/node-forge": "^1.3.1", 38 | "@types/pako": "^2.0.0", 39 | "@typescript-eslint/eslint-plugin": "^5.54.1", 40 | "@typescript-eslint/parser": "^5.54.1", 41 | "eslint": "^8.36.0", 42 | "eslint-config-prettier": "^8.7.0", 43 | "eslint-plugin-prettier": "^4.2.1", 44 | "nodemon": "^2.0.20", 45 | "prettier": "^2.8.4", 46 | "tsc-alias": "^1.8.5", 47 | "typescript": "^4.9.5" 48 | }, 49 | "dependencies": { 50 | "http-proxy": "^1.18.1", 51 | "node-cache": "^5.1.2", 52 | "node-forge": "^1.3.1", 53 | "pako": "^2.1.0", 54 | "pino": "^8.11.0", 55 | "rotating-file-stream": "^3.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/tls/certificate.ts: -------------------------------------------------------------------------------- 1 | import { pki } from 'node-forge'; 2 | import { CertificateDTO, CertificateOpts } from './typings'; 3 | 4 | export class Certificate { 5 | public readonly key: pki.PrivateKey; 6 | public readonly publicKey: pki.PublicKey; 7 | public readonly pem: string; 8 | 9 | constructor( 10 | public readonly id: string, 11 | public readonly opts: CertificateOpts 12 | ) { 13 | this.key = opts.key; 14 | this.publicKey = opts.public; 15 | this.pem = opts.pem; 16 | } 17 | 18 | get keyPem(): string { 19 | return pki.privateKeyToPem(this.key); 20 | } 21 | 22 | get publicKeyPem(): string { 23 | return pki.publicKeyToPem(this.publicKey); 24 | } 25 | 26 | get info(): pki.Certificate { 27 | return pki.certificateFromPem(this.pem); 28 | } 29 | 30 | get shouldRenew(): boolean { 31 | // 10min is added as a buffer to prevent almost expired certificates from being sent back 32 | const expireAt = new Date(Date.now() - 600000); 33 | 34 | return this.info.validity.notAfter.getTime() <= expireAt.getTime(); 35 | } 36 | 37 | public toDTO(): CertificateDTO { 38 | return { 39 | id: this.id, 40 | pem: { 41 | key: this.keyPem, 42 | publicKey: this.publicKeyPem, 43 | cert: this.pem, 44 | }, 45 | }; 46 | } 47 | 48 | public static restore(dto: CertificateDTO): Certificate { 49 | return new Certificate(dto.id, { 50 | key: pki.privateKeyFromPem(dto.pem.key), 51 | public: pki.publicKeyFromPem(dto.pem.publicKey), 52 | pem: dto.pem.cert, 53 | }); 54 | } 55 | 56 | toString(): string { 57 | return JSON.stringify({ 58 | id: this.id, 59 | key: this.key, 60 | publicKey: this.publicKey, 61 | pem: this.pem, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/http-proxy-ui", 3 | "version": "0.0.6-alpha", 4 | "description": "Desktop interface to facilitate user interaction with the HTTP Proxy server.", 5 | "main": "built/main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "dev": "nodemon", 9 | "build": "tsc && tsc-alias && yarn copy-assets", 10 | "copy-assets": "mkdirp ./built/assets && copyfiles -f ./src/assets/* ./built/assets", 11 | "lint": "eslint --ext ts,js src", 12 | "lint:fix": "eslint --ext ts,js --fix src", 13 | "pkg": "USE_SYSTEM_FPM=true node build.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/dfinity/http-proxy.git", 18 | "directory": "packages/ui" 19 | }, 20 | "keywords": [ 21 | "ic", 22 | "icp", 23 | "http", 24 | "proxy", 25 | "dfinity" 26 | ], 27 | "author": "Kepler Vital ", 28 | "license": "Apache-2.0", 29 | "engines": { 30 | "node": ">=18", 31 | "npm": "please-use-yarn", 32 | "pnpm": "please-use-yarn", 33 | "yarn": "~3" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/dfinity/http-proxy/issues" 37 | }, 38 | "homepage": "https://github.com/dfinity/http-proxy/tree/main/packages/ui#readme", 39 | "dependencies": { 40 | "@dfinity/http-proxy-core": "0.0.6-alpha", 41 | "@dfinity/http-proxy-server": "0.0.6-alpha" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^18.14.0", 45 | "@typescript-eslint/eslint-plugin": "^5.54.1", 46 | "@typescript-eslint/parser": "^5.54.1", 47 | "copyfiles": "^2.4.1", 48 | "electron": "23.3.13", 49 | "electron-builder": "^23.6.0", 50 | "eslint": "^8.36.0", 51 | "eslint-config-prettier": "^8.7.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "mkdirp": "^2.1.6", 54 | "nodemon": "^2.0.20", 55 | "prettier": "^2.8.4", 56 | "tsc-alias": "^1.8.5", 57 | "typescript": "^4.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/server/src/servers/icp/typings.ts: -------------------------------------------------------------------------------- 1 | import { ActorSubclass, HttpAgent } from '@dfinity/agent'; 2 | import { Certificate } from '@dfinity/http-proxy-core'; 3 | import { Principal } from '@dfinity/principal'; 4 | import { _SERVICE } from '~src/commons/http-interface/canister_http_interface_types'; 5 | 6 | export interface ICPServerOpts { 7 | host: string; 8 | port: number; 9 | certificate: { 10 | default: Certificate; 11 | create(servername: string): Promise; 12 | }; 13 | } 14 | export interface FetchAssetRequest { 15 | url: string; 16 | method: string; 17 | body: Uint8Array; 18 | headers: [string, string][]; 19 | } 20 | 21 | export interface FetchAssetResponse { 22 | body: Uint8Array; 23 | encoding: string; 24 | headers: [string, string][]; 25 | statusCode: number; 26 | } 27 | 28 | export interface FetchAssetData { 29 | updateCall: boolean; 30 | request: FetchAssetRequest; 31 | response: FetchAssetResponse; 32 | } 33 | 34 | export type FetchAssetResult = 35 | | { 36 | ok: false; 37 | error: unknown; 38 | } 39 | | { 40 | ok: true; 41 | data: FetchAssetData; 42 | }; 43 | 44 | export interface FetchAssetOptions { 45 | request: Request; 46 | canister: Principal; 47 | agent: HttpAgent; 48 | actor: ActorSubclass<_SERVICE>; 49 | certificateVersion: number; 50 | } 51 | 52 | export enum HTTPHeaders { 53 | Vary = 'vary', 54 | CacheControl = 'cache-control', 55 | Range = 'range', 56 | ContentEncoding = 'content-encoding', 57 | ContentLength = 'content-length', 58 | ServiceWorker = 'service-worker', 59 | Referer = 'referer', 60 | ContentType = 'content-type', 61 | UserAgent = 'user-agent', 62 | } 63 | 64 | export enum HTTPMethods { 65 | GET = 'GET', 66 | HEAD = 'HEAD', 67 | POST = 'POST', 68 | PUT = 'PUT', 69 | PATCH = 'PATCH', 70 | DELETE = 'DELETE', 71 | } 72 | 73 | export interface VerifiedResponse { 74 | response: Response; 75 | certifiedHeaders: Headers; 76 | } 77 | 78 | export interface HttpResponse { 79 | status: number; 80 | headers: Headers; 81 | body: Uint8Array; 82 | } 83 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/http-proxy-server", 3 | "version": "0.0.6-alpha", 4 | "description": "Gateway server to enable trustless access to the Internet Computer.", 5 | "main": "built/main.js", 6 | "types": "built/main.d.ts", 7 | "author": "Kepler Vital ", 8 | "license": "Apache-2.0", 9 | "homepage": "https://github.com/dfinity/http-proxy/tree/main/packages/server#readme", 10 | "repository": { 11 | "url": "https://github.com/dfinity/http-proxy.git", 12 | "type": "git", 13 | "directory": "packages/server" 14 | }, 15 | "scripts": { 16 | "start": "node ./built/start.js", 17 | "dev": "nodemon", 18 | "build": "tsc && tsc-alias", 19 | "lint": "eslint --ext ts,js src", 20 | "lint:fix": "eslint --ext ts,js --fix src" 21 | }, 22 | "keywords": [ 23 | "Internet Computer", 24 | "IC", 25 | "Proxy", 26 | "HTTP", 27 | "HTTP Proxy", 28 | "IC Response Verification" 29 | ], 30 | "engines": { 31 | "node": ">=18", 32 | "npm": "please-use-yarn", 33 | "pnpm": "please-use-yarn", 34 | "yarn": "~3" 35 | }, 36 | "devDependencies": { 37 | "@types/isomorphic-fetch": "^0.0.36", 38 | "@types/node": "^18.14.0", 39 | "@types/node-forge": "^1.3.1", 40 | "@types/pako": "^2.0.0", 41 | "@typescript-eslint/eslint-plugin": "^5.54.1", 42 | "@typescript-eslint/parser": "^5.54.1", 43 | "eslint": "^8.36.0", 44 | "eslint-config-prettier": "^8.7.0", 45 | "eslint-plugin-prettier": "^4.2.1", 46 | "nodemon": "^2.0.20", 47 | "prettier": "^2.8.4", 48 | "tsc-alias": "^1.8.5", 49 | "typescript": "^4.9.5" 50 | }, 51 | "dependencies": { 52 | "@dfinity/agent": "^0.19.0", 53 | "@dfinity/candid": "^0.19.0", 54 | "@dfinity/http-proxy-core": "0.0.6-alpha", 55 | "@dfinity/http-proxy-daemon": "0.0.6-alpha", 56 | "@dfinity/principal": "^0.19.0", 57 | "@dfinity/response-verification": "^1.1.0", 58 | "http-proxy": "^1.18.1", 59 | "isomorphic-fetch": "^3.0.0", 60 | "node-cache": "^5.1.2", 61 | "node-forge": "^1.3.1", 62 | "pako": "^2.1.0", 63 | "pino": "^8.11.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/daemon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/http-proxy-daemon", 3 | "version": "0.0.6-alpha", 4 | "description": "Daemon process to enable trustless access to the Internet Computer.", 5 | "main": "built/main.js", 6 | "types": "built/main.d.ts", 7 | "author": "Kepler Vital ", 8 | "license": "Apache-2.0", 9 | "homepage": "https://github.com/dfinity/http-proxy/tree/main/packages/daemon#readme", 10 | "repository": { 11 | "url": "https://github.com/dfinity/http-proxy.git", 12 | "type": "git", 13 | "directory": "packages/daemon" 14 | }, 15 | "bin": "./built/start.js", 16 | "scripts": { 17 | "start": "node ./built/start.js", 18 | "dev": "nodemon", 19 | "build": "tsc && tsc-alias", 20 | "lint": "eslint --ext ts,js src", 21 | "lint:fix": "eslint --ext ts,js --fix src", 22 | "pkg": "pkg ." 23 | }, 24 | "pkg": { 25 | "targets": [ 26 | "node18-macos-arm64", 27 | "node18-macos-x64", 28 | "node18-win-x64", 29 | "node18-linux-arm64", 30 | "node18-linux-x64" 31 | ], 32 | "compress": "GZip", 33 | "outputPath": "bin" 34 | }, 35 | "keywords": [ 36 | "Internet Computer", 37 | "IC", 38 | "Proxy", 39 | "HTTP", 40 | "HTTP Proxy", 41 | "IC Response Verification" 42 | ], 43 | "engines": { 44 | "node": ">=18", 45 | "npm": "please-use-yarn", 46 | "pnpm": "please-use-yarn", 47 | "yarn": "~3" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "^18.14.0", 51 | "@types/node-forge": "^1.3.1", 52 | "@types/pako": "^2.0.0", 53 | "@typescript-eslint/eslint-plugin": "^5.54.1", 54 | "@typescript-eslint/parser": "^5.54.1", 55 | "eslint": "^8.36.0", 56 | "eslint-config-prettier": "^8.7.0", 57 | "eslint-plugin-prettier": "^4.2.1", 58 | "nodemon": "^2.0.20", 59 | "pkg": "^5.8.1", 60 | "prettier": "^2.8.4", 61 | "tsc-alias": "^1.8.5", 62 | "typescript": "^4.9.5" 63 | }, 64 | "dependencies": { 65 | "@dfinity/http-proxy-core": "0.0.6-alpha", 66 | "http-proxy": "^1.18.1", 67 | "node-cache": "^5.1.2", 68 | "node-forge": "^1.3.1", 69 | "pako": "^2.1.0", 70 | "pino": "^8.11.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/ui/src/services/proxy.ts: -------------------------------------------------------------------------------- 1 | import { IPCClient, wait } from '@dfinity/http-proxy-core'; 2 | import { 3 | IsRunningMessageResponse, 4 | IsStartedMessageResponse, 5 | MessageType, 6 | } from '@dfinity/http-proxy-server'; 7 | import { fork } from 'child_process'; 8 | import { waitProcessing } from '~src/commons/utils'; 9 | 10 | export class ProxyService { 11 | public constructor(private readonly ipcClient: IPCClient) {} 12 | 13 | public async isEnabled(): Promise { 14 | return this.ipcClient 15 | .sendMessage({ type: MessageType.IsRunning }) 16 | .then((result) => (result.processed && result.data?.running) ?? false) 17 | .catch(() => false); 18 | } 19 | 20 | public async isStarted(waitIfShuttingDownMs = 3000): Promise { 21 | const response = await this.ipcClient 22 | .sendMessage({ type: MessageType.IsStarted }) 23 | .catch(() => null); 24 | 25 | if (!response) { 26 | return false; 27 | } 28 | 29 | if (response.processed && !response?.data?.isShuttingDown) { 30 | return true; 31 | } 32 | 33 | if (response?.data?.isShuttingDown) { 34 | await wait(waitIfShuttingDownMs); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | public async stopServers(): Promise { 41 | return this.ipcClient 42 | .sendMessage({ type: MessageType.Stop, skipWait: true }) 43 | .then((resp) => resp.processed) 44 | .catch(() => false); 45 | } 46 | 47 | public async enable(): Promise { 48 | return this.ipcClient 49 | .sendMessage({ type: MessageType.Enable }) 50 | .then((resp) => resp.processed) 51 | .catch(() => false); 52 | } 53 | 54 | public async startProxyServers(entrypoint: string): Promise { 55 | const isStarted = await this.isStarted(); 56 | if (isStarted) { 57 | return isStarted; 58 | } 59 | 60 | fork(entrypoint, [], { 61 | stdio: 'ignore', 62 | env: process.env, 63 | detached: true, 64 | }); 65 | 66 | const successfullyProcessed = await waitProcessing( 67 | async () => await this.isStarted() 68 | ); 69 | 70 | return successfullyProcessed ? true : false; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/server/src/servers/config/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@dfinity/http-proxy-core'; 2 | import http from 'http'; 3 | import { ProxyConfigOpts } from './typings'; 4 | 5 | export class ProxyConfigurationServer { 6 | private constructor( 7 | private readonly server: http.Server, 8 | private readonly opts: ProxyConfigOpts 9 | ) {} 10 | 11 | public static create(opts: ProxyConfigOpts): ProxyConfigurationServer { 12 | const server = new ProxyConfigurationServer(http.createServer(), opts); 13 | server.init(); 14 | 15 | return server; 16 | } 17 | 18 | private init(): void { 19 | this.server.on('close', this.onClose.bind(this)); 20 | this.server.on('request', (req, res) => { 21 | res.statusCode = 200; 22 | res.setHeader('Content-Type', 'application/x-ns-proxy-autoconfig'); 23 | // use the proxy for all http and https requests, 24 | // if the proxy server goes down fallback to a direct connection 25 | res.end( 26 | `function FindProxyForURL(url, host) { 27 | if (url.startsWith("https:") || url.startsWith("http:")) { 28 | return "PROXY ${this.opts.proxyServer.host}:${this.opts.proxyServer.port}; DIRECT"; 29 | } 30 | 31 | return "DIRECT"; 32 | }`.trim() 33 | ); 34 | }); 35 | } 36 | 37 | public async shutdown(): Promise { 38 | logger.info('Shutting down proxy configuration server.'); 39 | return new Promise((ok) => this.server.close(() => ok())); 40 | } 41 | 42 | public async start(): Promise { 43 | return new Promise((ok, err) => { 44 | const onListenError = (e: Error) => { 45 | if ('code' in e && e.code === 'EADDRINUSE') { 46 | this.server.close(); 47 | } 48 | 49 | return err(e); 50 | }; 51 | 52 | this.server.addListener('error', onListenError); 53 | 54 | this.server.listen(this.opts.port, this.opts.host, () => { 55 | this.server.removeListener('error', onListenError); 56 | this.server.addListener('error', this.onError.bind(this)); 57 | 58 | ok(); 59 | }); 60 | }); 61 | } 62 | 63 | private async onClose(): Promise { 64 | logger.info('Client disconnected'); 65 | } 66 | 67 | private async onError(err: Error): Promise { 68 | logger.error(`NetProxy error: (${String(err)})`); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/ui/build/win.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const builder = require('electron-builder'); 4 | const { createReleaseHashFile } = require('./utils'); 5 | const { execSync, exec } = require('child_process'); 6 | const { resolve } = require('path'); 7 | const Platform = builder.Platform; 8 | 9 | // Let's get that intellisense working 10 | /** 11 | * @type {import('electron-builder').Configuration} 12 | * @see https://www.electron.build/configuration/configuration 13 | */ 14 | const options = { 15 | // "store” | “normal” | "maximum". - For testing builds, use 'store' to reduce build time significantly. 16 | compression: 'normal', 17 | removePackageScripts: true, 18 | appId: 'com.dfinity.ichttpproxy', 19 | productName: 'IC HTTP Proxy', 20 | executableName: 'ic-http-proxy', 21 | artifactName: 'ic-http-proxy-${os}-${arch}-${version}.${ext}', 22 | nodeVersion: 'current', 23 | nodeGypRebuild: false, 24 | buildDependenciesFromSource: false, 25 | asarUnpack: ['node_modules/@dfinity/http-proxy-daemon/bin/*'], 26 | directories: { 27 | output: 'pkg', 28 | }, 29 | afterSign: async (context) => { 30 | execSync( 31 | `& "${resolve(__dirname, 'win.ps1')}" "${context.appOutDir}"`, { 32 | env: process.env, 33 | shell: 'powershell.exe' 34 | } 35 | ); 36 | }, 37 | win: { 38 | icon: './src/assets/logo@128x128.ico', 39 | files: [ 40 | '!bin/http-proxy-daemon-macos', 41 | '!bin/http-proxy-daemon-macos-x64', 42 | '!bin/http-proxy-daemon-macos-arm64', 43 | '!bin/http-proxy-daemon-linux', 44 | '!bin/http-proxy-daemon-linux-x64', 45 | '!bin/http-proxy-daemon-linux-arm64', 46 | '!.git/*', 47 | '!tsconfig.json', 48 | '!nodemon.json', 49 | '!.eslintrc.js', 50 | ], 51 | }, 52 | }; 53 | 54 | const build = async () => { 55 | // windows zip 56 | await builder 57 | .build({ 58 | targets: Platform.WINDOWS.createTarget('zip', builder.Arch.x64), 59 | config: options, 60 | }) 61 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 62 | // windows installer (non deterministic) 63 | await builder 64 | .build({ 65 | targets: Platform.WINDOWS.createTarget('nsis', builder.Arch.x64), 66 | config: options, 67 | }) 68 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 69 | }; 70 | 71 | module.exports = build; 72 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](../../LICENSE) 2 | 3 | # @dfinity/http-proxy-core 4 | 5 | ## Overview 6 | 7 | Core utilities and shared configuration to be used within the monorepo. 8 | 9 | ## Setup 10 | 11 | Starts a `watch` process that will reload on changes. 12 | ```bash 13 | yarn dev 14 | ``` 15 | 16 | ## Features 17 | 18 | ### Logger 19 | 20 | Exposes a logger helper that helps format and stantardize logs across the different packages. 21 | 22 | ```typescript 23 | // Initializes the logger with the correct context 24 | import { initLogger } from '@dfinity/http-proxy-core'; 25 | initLogger('IC HTTP Proxy Server', 'proxy'); 26 | 27 | // use this logger across all packages 28 | import { logger } from '@dfinity/http-proxy-core'; 29 | 30 | logger.info('logger is ready'); 31 | ``` 32 | 33 | The `initLogger` function should be executed in your entrypoint. 34 | 35 | ### TLS Certificates 36 | 37 | Handles the creation of tls certificates. 38 | 39 | ```typescript 40 | const certificateFactory = await CertificateFactory.build(certificate: { 41 | storage: { 42 | folder: 'certs', 43 | hostPrefix: 'host', 44 | }, 45 | rootca: { 46 | commonName: 'IC HTTP Proxy Root Authority', 47 | organizationName: 'IC HTTP Proxy', 48 | organizationUnit: 'IC', 49 | }, 50 | }); 51 | 52 | // create a new certificate for the certificate authority 53 | const rootCA = await certificateFactory.create({ type: 'ca' }); 54 | 55 | // creates a new certificate for the given hostname that is signed with the given CA 56 | const hostCertificate = await certificateFactory.create({ 57 | type: 'domain', 58 | hostname: 'localhost', 59 | ca: rootCA, 60 | }); 61 | ``` 62 | 63 | ### IPC 64 | 65 | Facilitates communication across different processes. 66 | 67 | ```typescript 68 | // server 69 | import { IPCServer } from '@dfinity/http-proxy-core'; 70 | 71 | const ipcServer = await IPCServer.create({ 72 | path: '/tmp/server.sock', 73 | onMessage: async (event: EventMessage): Promise => { 74 | // process the message and return a response to the client 75 | }, 76 | }); 77 | 78 | // client 79 | import { IPCClient } from '@dfinity/http-proxy-core'; 80 | 81 | const ipcClient = new IPCClient({ path: '/tmp/server.sock' }); 82 | ipcClient.sendMessage({ type: 'your-message' }).then((response) => { 83 | // do something with the response 84 | }); 85 | ``` 86 | -------------------------------------------------------------------------------- /packages/ui/build/linux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { execSync } = require('child_process'); 4 | const builder = require('electron-builder'); 5 | const { createReleaseHashFile } = require('./utils'); 6 | const Platform = builder.Platform; 7 | 8 | /** 9 | * @type {import('electron-builder').Configuration} 10 | * @see https://www.electron.build/configuration/configuration 11 | */ 12 | const options = { 13 | compression: 'normal', 14 | removePackageScripts: true, 15 | appId: 'com.dfinity.ichttpproxy', 16 | productName: 'IC HTTP Proxy', 17 | executableName: 'ic-http-proxy', 18 | artifactName: 'ic-http-proxy-${os}-${arch}-${version}.${ext}', 19 | nodeVersion: 'current', 20 | nodeGypRebuild: false, 21 | buildDependenciesFromSource: false, 22 | asarUnpack: ['node_modules/@dfinity/http-proxy-daemon/bin/*'], 23 | directories: { 24 | output: 'pkg', 25 | }, 26 | afterPack: (context) => { 27 | execSync( 28 | `find "${context.appOutDir}" -exec touch -mht 202201010000.00 {} +` 29 | ); 30 | }, 31 | linux: { 32 | icon: './src/assets/logo@256x256.icns', 33 | category: "System", 34 | files: [ 35 | '!bin/http-proxy-daemon-win.exe', 36 | '!bin/http-proxy-daemon-win-x64.exe', 37 | '!bin/http-proxy-daemon-win-arm64.exe', 38 | '!bin/http-proxy-daemon-macos', 39 | '!bin/http-proxy-daemon-macos-x64', 40 | '!bin/http-proxy-daemon-macos-arm64', 41 | '!.git/*', 42 | '!tsconfig.json', 43 | '!nodemon.json', 44 | '!.eslintrc.js', 45 | ], 46 | }, 47 | }; 48 | 49 | const build = async () => { 50 | // build for linux arm 51 | await builder 52 | .build({ 53 | targets: Platform.LINUX.createTarget('zip', builder.Arch.arm64), 54 | config: options, 55 | }) 56 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 57 | // build for linux x64 58 | await builder 59 | .build({ 60 | targets: Platform.LINUX.createTarget('zip', builder.Arch.x64), 61 | config: options, 62 | }) 63 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 64 | // linux arm installer (non deterministic) 65 | await builder 66 | .build({ 67 | targets: Platform.LINUX.createTarget('deb', builder.Arch.arm64), 68 | config: options, 69 | }) 70 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 71 | // linux x64 installer (non deterministic) 72 | await builder 73 | .build({ 74 | targets: Platform.LINUX.createTarget('deb', builder.Arch.x64), 75 | config: options, 76 | }) 77 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 78 | }; 79 | 80 | module.exports = build; 81 | -------------------------------------------------------------------------------- /packages/ui/build/mac.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { execSync } = require('child_process'); 4 | const builder = require('electron-builder'); 5 | const { createReleaseHashFile } = require('./utils'); 6 | const Platform = builder.Platform; 7 | 8 | /** 9 | * @type {import('electron-builder').Configuration} 10 | * @see https://www.electron.build/configuration/configuration 11 | */ 12 | const options = { 13 | compression: 'normal', 14 | removePackageScripts: true, 15 | appId: 'com.dfinity.ichttpproxy', 16 | productName: 'IC HTTP Proxy', 17 | executableName: 'ic-http-proxy', 18 | artifactName: 'ic-http-proxy-${os}-${arch}-${version}.${ext}', 19 | nodeVersion: 'current', 20 | nodeGypRebuild: false, 21 | buildDependenciesFromSource: false, 22 | asarUnpack: ['node_modules/@dfinity/http-proxy-daemon/bin/*'], 23 | directories: { 24 | output: 'pkg', 25 | }, 26 | afterPack: (context) => { 27 | execSync( 28 | `find "${context.appOutDir}" -exec touch -mht 202201010000.00 {} +` 29 | ); 30 | }, 31 | mac: { 32 | category: 'public.app-category.utilities', 33 | icon: './src/assets/logo@128x128.icns', 34 | identity: null, 35 | files: [ 36 | '!bin/http-proxy-daemon-win.exe', 37 | '!bin/http-proxy-daemon-win-x64.exe', 38 | '!bin/http-proxy-daemon-win-arm64.exe', 39 | '!bin/http-proxy-daemon-linux', 40 | '!bin/http-proxy-daemon-linux-x64', 41 | '!bin/http-proxy-daemon-linux-arm64', 42 | '!.git/*', 43 | '!tsconfig.json', 44 | '!nodemon.json', 45 | '!.eslintrc.js', 46 | ], 47 | }, 48 | }; 49 | 50 | const build = async () => { 51 | // build for mac arm 52 | await builder 53 | .build({ 54 | targets: Platform.MAC.createTarget('zip', builder.Arch.arm64), 55 | config: options, 56 | }) 57 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 58 | 59 | // build for mac intel 60 | await builder 61 | .build({ 62 | targets: Platform.MAC.createTarget('zip', builder.Arch.x64), 63 | config: options, 64 | }) 65 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 66 | // universal mac dmg build (non deterministic) 67 | await builder 68 | .build({ 69 | targets: Platform.MAC.createTarget('dmg', builder.Arch.universal), 70 | config: { 71 | ...options, 72 | mac: { 73 | ...options.mac, 74 | // since the dmg is not deterministic, we want to sign it with the default identity 75 | identity: undefined, 76 | } 77 | }, 78 | }) 79 | .then(async (builtFiles) => createReleaseHashFile(builtFiles)); 80 | }; 81 | 82 | module.exports = build; 83 | -------------------------------------------------------------------------------- /packages/server/src/servers/daemon/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IPCClient, 3 | ResultMessage, 4 | logger, 5 | wait, 6 | } from '@dfinity/http-proxy-core'; 7 | import { EnableProxyMessage, MessageType } from '@dfinity/http-proxy-daemon'; 8 | import { environment } from '~src/commons'; 9 | import { MissingRequirementsError } from '~src/errors'; 10 | import { EnableProxyOptions } from '~src/servers/daemon/typings'; 11 | import { 12 | WAIT_INTERVAL_CHECK_MS, 13 | WAIT_UNTIL_ACTIVE_MS, 14 | spawnDaemonProcess, 15 | } from '~src/servers/daemon/utils'; 16 | 17 | export class DaemonProcess { 18 | private hasStarted = false; 19 | 20 | public constructor(private readonly ipcClient: IPCClient) {} 21 | 22 | public async start(): Promise { 23 | this.hasStarted = true; 24 | const isAlreadyRunning = await this.isRunning(); 25 | if (isAlreadyRunning) { 26 | return; 27 | } 28 | 29 | await spawnDaemonProcess(environment.platform); 30 | 31 | await this.waitUntilActive(); 32 | } 33 | 34 | public async shutdown(): Promise { 35 | if (!this.hasStarted) { 36 | return; 37 | } 38 | 39 | logger.info('Shutting down daemon.'); 40 | await this.ipcClient 41 | .sendMessage({ type: MessageType.DisableProxy, skipWait: true }) 42 | .catch(() => { 43 | // do nothing if the daemon is already shutdown 44 | }); 45 | } 46 | 47 | public async enableProxy(opts: EnableProxyOptions): Promise { 48 | const message: EnableProxyMessage = { 49 | type: MessageType.EnableProxy, 50 | certificatePath: opts.certificate.path, 51 | commonName: opts.certificate.commonName, 52 | host: opts.proxy.host, 53 | port: opts.proxy.port, 54 | pac: { 55 | host: opts.pac.host, 56 | port: opts.pac.port, 57 | }, 58 | }; 59 | 60 | return this.ipcClient.sendMessage(message); 61 | } 62 | 63 | private async isRunning(): Promise { 64 | return this.ipcClient 65 | .sendMessage({ type: 'ping' }) 66 | ?.then((result) => result.processed) 67 | .catch(() => false); 68 | } 69 | 70 | private async waitUntilActive( 71 | timeout: number = WAIT_UNTIL_ACTIVE_MS, 72 | intervalCheckMs = WAIT_INTERVAL_CHECK_MS 73 | ): Promise { 74 | let elapsedTime = 0; 75 | do { 76 | const isRunning = await this.isRunning(); 77 | if (isRunning) { 78 | // daemon is active and ready to receive messages 79 | return; 80 | } 81 | 82 | await wait(intervalCheckMs); 83 | elapsedTime += intervalCheckMs; 84 | } while (elapsedTime < timeout); 85 | 86 | throw new MissingRequirementsError(`Daemon process failed to activate`); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/ui/src/interface/images.ts: -------------------------------------------------------------------------------- 1 | import { nativeImage } from 'electron'; 2 | import { join } from 'node:path'; 3 | 4 | export class Images { 5 | static readonly path = join(__dirname, '..', 'assets'); 6 | 7 | public constructor(private readonly isInDarkMode: boolean = false) {} 8 | 9 | get tray(): string { 10 | if (process.platform === 'darwin') { 11 | return join(Images.path, 'tray-Template.png'); 12 | } 13 | 14 | if (process.platform === 'linux') { 15 | return join(Images.path, 'tray-dark.png'); 16 | } 17 | 18 | const image = this.isInDarkMode ? 'tray-dark.png' : 'tray-light.png'; 19 | 20 | return join(Images.path, image); 21 | } 22 | 23 | get trayEnabled(): string { 24 | if (process.platform === 'darwin') { 25 | return join(Images.path, 'tray-enabled-Template.png'); 26 | } 27 | 28 | if (process.platform === 'linux') { 29 | return join(Images.path, 'tray-dark-enabled.png'); 30 | } 31 | 32 | const image = this.isInDarkMode 33 | ? 'tray-dark-enabled.png' 34 | : 'tray-light-enabled.png'; 35 | 36 | return join(Images.path, image); 37 | } 38 | 39 | get logo(): Electron.NativeImage { 40 | const image = this.isInDarkMode 41 | ? 'logo-dark@124x124.png' 42 | : 'logo-light@124x124.png'; 43 | 44 | return nativeImage.createFromPath(join(Images.path, image)); 45 | } 46 | 47 | get logoEnabled(): Electron.NativeImage { 48 | const image = this.isInDarkMode 49 | ? 'logo-dark-enabled@124x124.png' 50 | : 'logo-light-enabled@124x124.png'; 51 | 52 | return nativeImage.createFromPath(join(Images.path, image)); 53 | } 54 | 55 | get play(): Electron.NativeImage { 56 | const image = this.isInDarkMode 57 | ? 'play-dark@128x128.png' 58 | : 'play-light@128x128.png'; 59 | 60 | return nativeImage.createFromPath(join(Images.path, image)); 61 | } 62 | 63 | get pause(): Electron.NativeImage { 64 | const image = this.isInDarkMode 65 | ? 'pause-dark@128x128.png' 66 | : 'pause-light@128x128.png'; 67 | 68 | return nativeImage.createFromPath(join(Images.path, image)); 69 | } 70 | 71 | get exit(): Electron.NativeImage { 72 | const image = this.isInDarkMode 73 | ? 'exit-dark@128x128.png' 74 | : 'exit-light@128x128.png'; 75 | 76 | return nativeImage.createFromPath(join(Images.path, image)); 77 | } 78 | 79 | get enabled(): Electron.NativeImage { 80 | const image = 'enabled@64x64.png'; 81 | 82 | return nativeImage.createFromPath(join(Images.path, image)); 83 | } 84 | 85 | get disabled(): Electron.NativeImage { 86 | const image = 'disabled@64x64.png'; 87 | 88 | return nativeImage.createFromPath(join(Images.path, image)); 89 | } 90 | 91 | get pending(): Electron.NativeImage { 92 | const image = 'pending@64x64.png'; 93 | 94 | return nativeImage.createFromPath(join(Images.path, image)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/ui/src/interface/menu.ts: -------------------------------------------------------------------------------- 1 | import { Images } from '~src/interface/images'; 2 | import Electron, { BrowserWindow, Menu, MenuItem, WebContents } from 'electron'; 3 | 4 | export enum ProxyMenuItem { 5 | EnabledStatus = 'enabled-status', 6 | DisabledStatus = 'disabled-status', 7 | StartingStatus = 'starting-status', 8 | StoppingStatus = 'stopping-status', 9 | Start = 'start', 10 | Stop = 'stop', 11 | Quit = 'quit', 12 | } 13 | 14 | export type ElectronMenuItem = 15 | | Electron.MenuItemConstructorOptions 16 | | Electron.MenuItem; 17 | 18 | export interface ElectronClickFnOptions { 19 | menuItem: MenuItem; 20 | keyboardEvent: KeyboardEvent; 21 | focusedWindow: BrowserWindow | undefined; 22 | focusedWebContents: WebContents; 23 | } 24 | 25 | export type ElectronClickFn = (opts: ElectronClickFnOptions) => void; 26 | 27 | export class ProxyMenu { 28 | private _menu; 29 | 30 | public constructor(readonly images: Images, items?: ElectronMenuItem[]) { 31 | const template = items ?? ProxyMenu.default(images); 32 | this._menu = Menu.buildFromTemplate(template); 33 | } 34 | 35 | get menu(): Menu { 36 | return this._menu; 37 | } 38 | 39 | onClick(itemId: ProxyMenuItem, callback: ElectronClickFn): void { 40 | const item = this._menu.getMenuItemById(itemId); 41 | if (!item) { 42 | return; 43 | } 44 | 45 | item.click = function ( 46 | keyboardEvent: KeyboardEvent, 47 | focusedWindow: BrowserWindow | undefined, 48 | focusedWebContents: WebContents 49 | ): void { 50 | callback({ 51 | menuItem: item, 52 | keyboardEvent, 53 | focusedWindow, 54 | focusedWebContents, 55 | }); 56 | }; 57 | } 58 | 59 | static default(images: Images): ElectronMenuItem[] { 60 | return [ 61 | { 62 | id: ProxyMenuItem.EnabledStatus, 63 | label: 'Proxy is running', 64 | enabled: false, 65 | visible: false, 66 | icon: images.enabled.resize({ width: 8, height: 8 }), 67 | }, 68 | { 69 | id: ProxyMenuItem.DisabledStatus, 70 | label: 'Proxy is stopped', 71 | enabled: false, 72 | visible: false, 73 | icon: images.disabled.resize({ width: 8, height: 8 }), 74 | }, 75 | { 76 | id: ProxyMenuItem.StartingStatus, 77 | label: 'Proxy is starting', 78 | enabled: false, 79 | visible: false, 80 | icon: images.pending.resize({ width: 8, height: 8 }), 81 | }, 82 | { 83 | id: ProxyMenuItem.StoppingStatus, 84 | label: 'Proxy is stopping', 85 | enabled: false, 86 | visible: false, 87 | icon: images.pending.resize({ width: 8, height: 8 }), 88 | }, 89 | { type: 'separator' }, 90 | { 91 | id: ProxyMenuItem.Start, 92 | label: 'start', 93 | type: 'normal', 94 | icon: images.play.resize({ width: 16, height: 16 }), 95 | enabled: false, 96 | }, 97 | { 98 | id: ProxyMenuItem.Stop, 99 | label: 'stop', 100 | type: 'normal', 101 | icon: images.pause.resize({ width: 16, height: 16 }), 102 | enabled: false, 103 | }, 104 | { type: 'separator' }, 105 | { 106 | id: ProxyMenuItem.Quit, 107 | label: 'quit', 108 | type: 'normal', 109 | icon: images.exit.resize({ width: 16, height: 16 }), 110 | }, 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/server/src/commons/streaming.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpResponse, 3 | StreamingCallbackHttpResponse, 4 | StreamingStrategy, 5 | Token, 6 | } from './http-interface/canister_http_interface_types'; 7 | import { streamingCallbackHttpResponseType } from './http-interface/canister_http_interface'; 8 | import { IDL } from '@dfinity/candid'; 9 | import { 10 | HttpAgent, 11 | QueryResponse, 12 | QueryResponseStatus, 13 | concat, 14 | } from '@dfinity/agent'; 15 | import { Principal } from '@dfinity/principal'; 16 | 17 | const MAX_CALLBACKS = 1000; 18 | 19 | export async function streamContent( 20 | agent: HttpAgent, 21 | canisterId: Principal, 22 | httpResponse: HttpResponse 23 | ): Promise { 24 | // if we do streaming, body contains the first chunk 25 | let buffer = new ArrayBuffer(0); 26 | buffer = concat(buffer, httpResponse.body); 27 | 28 | if (httpResponse.streaming_strategy.length !== 0) { 29 | const remainingChunks = await streamRemainingChunks( 30 | agent, 31 | canisterId, 32 | httpResponse.streaming_strategy[0] 33 | ); 34 | 35 | buffer = concat(buffer, remainingChunks); 36 | } 37 | 38 | return new Uint8Array(buffer); 39 | } 40 | 41 | async function streamRemainingChunks( 42 | agent: HttpAgent, 43 | canisterId: Principal, 44 | streamingStrategy: StreamingStrategy 45 | ): Promise { 46 | let buffer = new ArrayBuffer(0); 47 | let tokenOpt: Token | undefined = streamingStrategy.Callback.token; 48 | const callBackFunc = streamingStrategy.Callback.callback[1]; 49 | 50 | let currentCallback = 1; 51 | while (tokenOpt) { 52 | if (currentCallback > MAX_CALLBACKS) { 53 | throw new Error('Exceeded streaming callback limit'); 54 | } 55 | 56 | const callbackResponse = await queryNextChunk( 57 | tokenOpt, 58 | agent, 59 | canisterId, 60 | callBackFunc 61 | ); 62 | 63 | switch (callbackResponse.status) { 64 | case QueryResponseStatus.Replied: { 65 | const [callbackData] = IDL.decode( 66 | [streamingCallbackHttpResponseType], 67 | callbackResponse.reply.arg 68 | ); 69 | 70 | if (isStreamingCallbackResponse(callbackData)) { 71 | buffer = concat(buffer, callbackData.body); 72 | [tokenOpt] = callbackData.token; 73 | } else { 74 | throw new Error('Unexpected callback response: ' + callbackData); 75 | } 76 | 77 | break; 78 | } 79 | 80 | case QueryResponseStatus.Rejected: { 81 | throw new Error('Streaming callback error: ' + callbackResponse); 82 | } 83 | } 84 | 85 | currentCallback += 1; 86 | } 87 | 88 | return buffer; 89 | } 90 | 91 | function queryNextChunk( 92 | token: Token, 93 | agent: HttpAgent, 94 | canisterId: Principal, 95 | callBackFunc: string 96 | ): Promise { 97 | const tokenType = token.type(); 98 | // unbox primitive values 99 | const tokenValue = 100 | typeof token.valueOf === 'function' ? token.valueOf() : token; 101 | const callbackArg = IDL.encode([tokenType], [tokenValue]); 102 | return agent.query(canisterId, { 103 | methodName: callBackFunc, 104 | arg: callbackArg, 105 | }); 106 | } 107 | 108 | function isStreamingCallbackResponse( 109 | response: unknown 110 | ): response is StreamingCallbackHttpResponse { 111 | return ( 112 | typeof response === 'object' && 113 | response !== null && 114 | 'body' in response && 115 | 'token' in response 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /packages/core/src/ipc/server.ts: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs'; 2 | import net from 'node:net'; 3 | import { logger, pathExists } from '~src/commons'; 4 | import { 5 | EventMessage, 6 | IPCServerOptions, 7 | ResultMessage, 8 | } from '~src/ipc/typings'; 9 | 10 | export class IPCServer { 11 | private constructor( 12 | private readonly server: net.Server, 13 | private readonly options: IPCServerOptions 14 | ) {} 15 | 16 | public static async create(options: IPCServerOptions): Promise { 17 | const server = new IPCServer(net.createServer(), options); 18 | 19 | await server.init(); 20 | 21 | return server; 22 | } 23 | 24 | private async init(): Promise { 25 | this.server.addListener('connection', this.onConnection.bind(this)); 26 | this.server.addListener('close', this.onClose.bind(this)); 27 | } 28 | 29 | private static writeAndCloseSocket( 30 | socket: net.Socket, 31 | result: ResultMessage 32 | ): void { 33 | socket.write(JSON.stringify(result)); 34 | socket.end(); 35 | } 36 | 37 | private async onConnection(socket: net.Socket): Promise { 38 | socket.on('data', async (data) => { 39 | const result: ResultMessage = { processed: true }; 40 | 41 | try { 42 | const event = JSON.parse(data.toString()) as EventMessage; 43 | const skipWait = event?.skipWait ?? false; 44 | 45 | if (skipWait) { 46 | IPCServer.writeAndCloseSocket(socket, result); 47 | 48 | if (event?.type && this.options.onMessage) { 49 | this.options.onMessage(event); 50 | } 51 | return; 52 | } 53 | 54 | if (event?.type && this.options.onMessage) { 55 | result.data = await this.options.onMessage(event); 56 | 57 | IPCServer.writeAndCloseSocket(socket, result); 58 | } 59 | } catch (e) { 60 | result.processed = false; 61 | result.err = String(e); 62 | 63 | IPCServer.writeAndCloseSocket(socket, result); 64 | } 65 | }); 66 | 67 | socket.on('error', (err) => { 68 | logger.error(`Client socket error(${String(err)})`); 69 | }); 70 | } 71 | 72 | private async handleHangingSocket(): Promise { 73 | if (pathExists(this.options.path)) { 74 | rmSync(this.options.path, { force: true }); 75 | } 76 | } 77 | 78 | public async start(): Promise { 79 | await this.handleHangingSocket(); 80 | 81 | return new Promise((ok, err) => { 82 | const onListenError = (e: Error) => { 83 | if ('code' in e && e.code === 'EADDRINUSE') { 84 | this.server.close(); 85 | } 86 | 87 | return err(e); 88 | }; 89 | 90 | this.server.addListener('error', onListenError); 91 | 92 | this.server.listen( 93 | { 94 | path: this.options.path, 95 | readableAll: true, 96 | writableAll: true, 97 | }, 98 | () => { 99 | this.server.removeListener('error', onListenError); 100 | this.server.addListener('error', this.onError.bind(this)); 101 | 102 | ok(); 103 | } 104 | ); 105 | }); 106 | } 107 | 108 | public async shutdown(): Promise { 109 | logger.info('Shutting down ipc server.'); 110 | 111 | return new Promise((ok) => { 112 | this.server.close(() => ok()); 113 | }); 114 | } 115 | 116 | private async onClose(): Promise { 117 | logger.info('IPC server closed'); 118 | } 119 | 120 | private async onError(err: Error): Promise { 121 | logger.error(`IPC server error: (${String(err)})`); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/daemon/src/daemon.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventMessage, 3 | IPCClient, 4 | IPCServer, 5 | logger, 6 | } from '@dfinity/http-proxy-core'; 7 | import { Platform, PlatformFactory } from '~src/platforms'; 8 | import { 9 | CHECK_PROXY_PROCESS_MS, 10 | CHECK_PROXY_RUNNING_RETRIES, 11 | } from '~src/utils'; 12 | import { 13 | DaemonConfiguration, 14 | EnableProxyMessage, 15 | MessageType, 16 | OnMessageResponse, 17 | } from './typings'; 18 | 19 | export class Daemon { 20 | private server?: IPCServer; 21 | private ipcClient?: IPCClient; 22 | private platform?: Platform; 23 | private isProxyRunning = false; 24 | 25 | private constructor(private readonly configs: DaemonConfiguration) {} 26 | 27 | public static async create(configs: DaemonConfiguration): Promise { 28 | const daemon = new Daemon(configs); 29 | await daemon.init(); 30 | 31 | return daemon; 32 | } 33 | 34 | private async init(): Promise { 35 | this.ipcClient = new IPCClient({ path: this.configs.ipcChannels.proxy }); 36 | this.server = await IPCServer.create({ 37 | path: this.configs.ipcChannels.daemon, 38 | onMessage: async (event: EventMessage): Promise => { 39 | switch (event.type) { 40 | case MessageType.EnableProxy: 41 | return await this.enableProxyTask(event as EnableProxyMessage); 42 | case MessageType.DisableProxy: 43 | return await this.disableProxyTask(); 44 | case MessageType.IsProxyEnabled: 45 | return { enabled: this.isProxyRunning }; 46 | } 47 | }, 48 | }); 49 | } 50 | 51 | private async enableProxyTask(message: EnableProxyMessage): Promise { 52 | this.platform = await PlatformFactory.create({ 53 | platform: this.configs.platform, 54 | ca: { 55 | path: message.certificatePath, 56 | commonName: message.commonName, 57 | }, 58 | proxy: { 59 | host: message.host, 60 | port: message.port, 61 | }, 62 | pac: message.pac, 63 | }); 64 | 65 | await this.platform.attach(); 66 | 67 | this.isProxyRunning = true; 68 | } 69 | 70 | private async disableProxyTask(): Promise { 71 | await this.platform?.detach(); 72 | 73 | this.isProxyRunning = false; 74 | 75 | this.shutdown(); 76 | } 77 | 78 | public async start(): Promise { 79 | await this.server?.start(); 80 | 81 | this.registerProxyShutdownListener(); 82 | } 83 | 84 | public async shutdown(): Promise { 85 | logger.info('Shutting down'); 86 | 87 | await this.server?.shutdown(); 88 | 89 | logger.info('Exited.'); 90 | 91 | process.exit(0); 92 | } 93 | 94 | private async registerProxyShutdownListener( 95 | retries = CHECK_PROXY_RUNNING_RETRIES 96 | ): Promise { 97 | const interval = setInterval(async () => { 98 | const isRunning = await this.ipcClient 99 | ?.sendMessage({ type: 'ping' }) 100 | .then((result) => result.processed) 101 | .catch(() => false); 102 | if (isRunning) { 103 | retries = CHECK_PROXY_RUNNING_RETRIES; 104 | return; 105 | } 106 | 107 | // clear current interval when proxy is not running 108 | clearInterval(interval); 109 | 110 | if (retries > 0) { 111 | this.registerProxyShutdownListener(--retries); 112 | return; 113 | } 114 | 115 | logger.info( 116 | 'Proxy server not running, removing configuration from system' 117 | ); 118 | 119 | await this.platform?.detach(); 120 | setTimeout(() => this.shutdown(), 1000); 121 | }, CHECK_PROXY_PROCESS_MS); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/server/src/servers/daemon/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SupportedPlatforms, 3 | UnsupportedPlatformError, 4 | coreConfigs, 5 | execAsync, 6 | } from '@dfinity/http-proxy-core'; 7 | import { spawnSync } from 'child_process'; 8 | 9 | export const WAIT_UNTIL_ACTIVE_MS = 10000; 10 | export const WAIT_INTERVAL_CHECK_MS = 250; 11 | 12 | export const daemonArch = (systemArch = process.arch): string => { 13 | switch (systemArch) { 14 | case 'x64': 15 | return 'x64'; 16 | case 'arm64': 17 | return 'arm64'; 18 | default: 19 | throw new UnsupportedPlatformError(systemArch); 20 | } 21 | }; 22 | 23 | export const daemonBinPath = async (platform: string): Promise => { 24 | switch (platform) { 25 | case SupportedPlatforms.MacOSX: 26 | return require 27 | .resolve( 28 | `@dfinity/http-proxy-daemon/bin/http-proxy-daemon-macos-${daemonArch()}` 29 | ) 30 | .replace('.asar', '.asar.unpacked'); 31 | case SupportedPlatforms.Windows: 32 | return require 33 | .resolve( 34 | `@dfinity/http-proxy-daemon/bin/http-proxy-daemon-win-${daemonArch()}.exe` 35 | ) 36 | .replace('.asar', '.asar.unpacked'); 37 | case SupportedPlatforms.Linux: 38 | return require 39 | .resolve( 40 | `@dfinity/http-proxy-daemon/bin/http-proxy-daemon-linux-${daemonArch()}` 41 | ) 42 | .replace('.asar', '.asar.unpacked'); 43 | default: 44 | throw new UnsupportedPlatformError(platform); 45 | } 46 | }; 47 | 48 | const spawnDaemonProcessMacOSX = async (daemonPath: string): Promise => { 49 | const command = [ 50 | daemonPath.replaceAll(' ', '\\\\ '), 51 | '&>/dev/null', 52 | `&`, 53 | ].join(' '); 54 | const promptMessage = 55 | 'IC HTTP Proxy needs your permission to create a secure environment'; 56 | const runCommand = [ 57 | 'osascript', 58 | '-e', 59 | `'do shell script "${command}" with prompt "${promptMessage}" with administrator privileges'`, 60 | ].join(' '); 61 | 62 | await execAsync(runCommand); 63 | }; 64 | 65 | const spawnDaemonProcessWindows = async (daemonPath: string): Promise => { 66 | const command = [`$env:DAEMON_EXEC_PATH`].join(' '); 67 | const startProcessCommand = `$env:DAEMON_EXEC_PATH='"${daemonPath}"'; start-process -windowstyle hidden cmd -verb runas -argumentlist "/c ${command}"`; 68 | const encodedCommand = Buffer.from( 69 | startProcessCommand, 70 | coreConfigs.encoding 71 | ).toString('base64'); 72 | const spawnCommand = `powershell -EncodedCommand ${encodedCommand}`; 73 | 74 | await execAsync(spawnCommand); 75 | }; 76 | 77 | const spawnDaemonProcessUbuntu = (daemonPath: string) => { 78 | const escapedDaemonPath = daemonPath.replace(/ /g, '\\ '); 79 | const command = 'pkexec'; 80 | const args = [ 81 | 'sh', 82 | '-c', 83 | `HOME="${process.env.HOME}" LOGNAME="${process.env.LOGNAME}" nohup ${escapedDaemonPath} &>/dev/null &`, 84 | ]; 85 | 86 | const result = spawnSync(command, args, { 87 | stdio: 'ignore', 88 | env: process.env, 89 | }); 90 | if (result.status !== 0) { 91 | throw new Error( 92 | `Spawn error (err: ${result.status}): ${result.error ?? 'unknown'}` 93 | ); 94 | } 95 | }; 96 | 97 | export const spawnDaemonProcess = async (platform: string): Promise => { 98 | const daemonPath = await daemonBinPath(platform); 99 | 100 | switch (platform) { 101 | case SupportedPlatforms.MacOSX: 102 | return await spawnDaemonProcessMacOSX(daemonPath); 103 | case SupportedPlatforms.Windows: 104 | return await spawnDaemonProcessWindows(daemonPath); 105 | case SupportedPlatforms.Linux: 106 | return await spawnDaemonProcessUbuntu(daemonPath); 107 | default: 108 | throw new UnsupportedPlatformError(platform); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /packages/server/src/servers/icp/index.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http'; 2 | import https from 'https'; 3 | import { CanisterNotFoundError } from '~src/errors'; 4 | import { SecureContext, createSecureContext } from 'tls'; 5 | import { lookupIcDomain } from './domains'; 6 | import { HTTPHeaders, ICPServerOpts } from './typings'; 7 | import { convertIncomingMessage, processIcRequest } from './utils'; 8 | import { environment } from '~src/commons'; 9 | import { logger } from '@dfinity/http-proxy-core'; 10 | 11 | export class ICPServer { 12 | private httpsServer!: https.Server; 13 | 14 | private constructor(private readonly configuration: ICPServerOpts) {} 15 | 16 | public static create(configuration: ICPServerOpts): ICPServer { 17 | const server = new ICPServer(configuration); 18 | server.init(); 19 | 20 | return server; 21 | } 22 | 23 | public async shutdown(): Promise { 24 | logger.info('Shutting down icp server.'); 25 | return new Promise((ok) => { 26 | this.httpsServer.close(() => ok()); 27 | }); 28 | } 29 | 30 | public async start(): Promise { 31 | this.httpsServer.listen(this.configuration.port, this.configuration.host); 32 | } 33 | 34 | private init(): void { 35 | this.httpsServer = https.createServer( 36 | { 37 | key: this.configuration.certificate.default.keyPem, 38 | cert: this.configuration.certificate.default.pem, 39 | SNICallback: this.SNICallback.bind(this), 40 | }, 41 | this.handleRequest.bind(this) 42 | ); 43 | 44 | this.httpsServer.on('error', (e) => { 45 | logger.error(`ICP HTTPS Server failed with error(${String(e)})`); 46 | }); 47 | } 48 | 49 | private async SNICallback( 50 | servername: string, 51 | cb: (err: Error | null, ctx?: SecureContext | undefined) => void 52 | ): Promise { 53 | try { 54 | const domainCertificate = await this.configuration.certificate.create( 55 | servername 56 | ); 57 | const ctx = createSecureContext({ 58 | key: domainCertificate.keyPem, 59 | cert: domainCertificate.pem, 60 | }); 61 | 62 | cb(null, ctx); 63 | } catch (e) { 64 | logger.error(`SNICallback failed with ${e}`); 65 | 66 | cb(e as Error); 67 | } 68 | } 69 | 70 | private async handleRequest( 71 | incomingMessage: IncomingMessage, 72 | res: ServerResponse & { 73 | req: IncomingMessage; 74 | } 75 | ): Promise { 76 | try { 77 | const request = await convertIncomingMessage( 78 | incomingMessage, 79 | (headers) => { 80 | const userAgent = headers.get(HTTPHeaders.UserAgent); 81 | if (userAgent) { 82 | headers.set( 83 | HTTPHeaders.UserAgent, 84 | `${userAgent} ${environment.userAgent}` 85 | ); 86 | } 87 | 88 | return headers; 89 | } 90 | ); 91 | const url = new URL(request.url); 92 | const canister = await lookupIcDomain(url.hostname); 93 | if (!canister) { 94 | throw new CanisterNotFoundError(url.hostname); 95 | } 96 | 97 | const httpResponse = await processIcRequest(canister, request); 98 | const responseHeaders: { [key: string]: string } = {}; 99 | for (const [headerName, headerValue] of httpResponse.headers.entries()) { 100 | responseHeaders[headerName] = headerValue; 101 | } 102 | responseHeaders[ 103 | HTTPHeaders.ContentLength.toString() 104 | ] = `${httpResponse.body.length}`; 105 | responseHeaders.Server = 'IC HTTP Proxy'; 106 | responseHeaders.Connection = 'close'; 107 | 108 | res.writeHead(httpResponse.status, responseHeaders); 109 | res.end(httpResponse.body); 110 | } catch (e) { 111 | const responseBody = `Proxy failed to handle internet computer request ${e}`; 112 | const bodyLength = Buffer.byteLength(responseBody); 113 | 114 | logger.error(responseBody); 115 | res.writeHead(500, { 116 | 'Content-Type': 'text/plain', 117 | [HTTPHeaders.ContentLength]: bodyLength, 118 | Server: 'IC HTTP Proxy', 119 | Connection: 'close', 120 | }); 121 | res.end(responseBody); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/core/src/commons/utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { 3 | existsSync, 4 | lstatSync, 5 | mkdirSync, 6 | readFile, 7 | readdirSync, 8 | writeFile, 9 | } from 'fs'; 10 | import { dirname } from 'path'; 11 | import { SupportedPlatforms } from '~src/main'; 12 | 13 | export const saveFile = async ( 14 | path: string, 15 | data: string, 16 | { encoding }: { encoding: BufferEncoding } 17 | ): Promise => { 18 | const directory = dirname(path); 19 | createDir(directory); 20 | return new Promise((ok, err) => { 21 | writeFile(path, data, { encoding }, (failed) => { 22 | if (failed) { 23 | return err(failed); 24 | } 25 | 26 | ok(); 27 | }); 28 | }); 29 | }; 30 | 31 | export const getFile = async ( 32 | path: string, 33 | { encoding }: { encoding: BufferEncoding } 34 | ): Promise => { 35 | return new Promise((ok, err) => { 36 | if (!existsSync(path)) { 37 | return ok(null); 38 | } 39 | 40 | readFile(path, encoding, (failed, data) => { 41 | if (failed) { 42 | return err(failed); 43 | } 44 | 45 | ok(data); 46 | }); 47 | }); 48 | }; 49 | 50 | export const pathExists = (path: string): boolean => { 51 | return existsSync(path); 52 | }; 53 | 54 | export const createDir = (path: string): void => { 55 | const exists = pathExists(path); 56 | if (!exists) { 57 | mkdirSync(path, { recursive: true }); 58 | } 59 | }; 60 | 61 | export const isSupportedPlatform = (platform: string): boolean => { 62 | return Object.values(SupportedPlatforms) 63 | .map(String) 64 | .some((supported) => supported.toLowerCase() === platform.toLowerCase()); 65 | }; 66 | 67 | export const wait = (numberMs = 100): Promise => { 68 | return new Promise((ok) => { 69 | setTimeout(() => ok(), numberMs); 70 | }); 71 | }; 72 | 73 | export const execAsync = async (command: string): Promise => { 74 | return new Promise((ok, err) => { 75 | exec(command, { env: process.env }, (error, stdout) => { 76 | if (error) { 77 | return err(error); 78 | } 79 | 80 | ok(stdout); 81 | }); 82 | }); 83 | }; 84 | 85 | export const assertPresent = ( 86 | value: T, 87 | name = 'unknown' 88 | ): NonNullable => { 89 | if (value === null || value === undefined) { 90 | throw new Error(`${name} is not present`); 91 | } 92 | 93 | return value; 94 | }; 95 | 96 | export const retryClosure = async ( 97 | asyncExecFn: () => Promise, 98 | doAfterFailFn?: () => Promise, 99 | retries = 2 100 | ): Promise => { 101 | let result: T; 102 | let tries = retries && retries < 0 ? 0 : retries; 103 | do { 104 | try { 105 | result = await asyncExecFn(); 106 | return result; 107 | } catch (e) { 108 | if (tries === 0) { 109 | throw e; 110 | } 111 | 112 | if (doAfterFailFn) { 113 | await doAfterFailFn(); 114 | } 115 | } 116 | --tries; 117 | } while (tries > 0); 118 | 119 | throw new Error(`Retry closure failed all options`); 120 | }; 121 | 122 | export const getFiles = ( 123 | directoryPath: string, 124 | extensions?: string[] 125 | ): string[] => { 126 | const files: string[] = []; 127 | const isDirectory = 128 | pathExists(directoryPath) && lstatSync(directoryPath).isDirectory(); 129 | if (!isDirectory) { 130 | return []; 131 | } 132 | 133 | readdirSync(directoryPath, { 134 | withFileTypes: true, 135 | }).forEach((file) => { 136 | if (!file.isFile()) { 137 | return; 138 | } 139 | 140 | const shouldFilterExtension = extensions && extensions.length > 0; 141 | if (!shouldFilterExtension) { 142 | files.push(file.name); 143 | return; 144 | } 145 | 146 | const parts = file.name.split('.'); 147 | const extension = parts.length > 1 ? parts[parts.length - 1] : ''; 148 | if (extensions.includes(extension)) { 149 | files.push(file.name); 150 | } 151 | }); 152 | 153 | return files; 154 | }; 155 | 156 | export const getDirectories = (directoryPath: string): string[] => { 157 | const directories: string[] = []; 158 | const isDirectory = 159 | pathExists(directoryPath) && lstatSync(directoryPath).isDirectory(); 160 | if (!isDirectory) { 161 | return []; 162 | } 163 | 164 | readdirSync(directoryPath, { 165 | withFileTypes: true, 166 | }).forEach((entry) => { 167 | if (!entry.isDirectory()) { 168 | return; 169 | } 170 | 171 | directories.push(entry.name); 172 | }); 173 | 174 | return directories; 175 | }; 176 | -------------------------------------------------------------------------------- /packages/core/src/tls/store.ts: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'fs'; 2 | import InMemoryCache from 'node-cache'; 3 | import { resolve } from 'path'; 4 | import { 5 | coreConfigs, 6 | createDir, 7 | getFile, 8 | getFiles, 9 | pathExists, 10 | saveFile, 11 | } from '../commons'; 12 | import { Certificate } from './certificate'; 13 | import { CertificateDTO, CertificateStoreConfiguration } from './typings'; 14 | 15 | export class CertificateStore { 16 | private readonly storePath: string; 17 | private readonly serialIdPath: string; 18 | private static cachedLookups = new InMemoryCache({ 19 | stdTTL: 60 * 5, // 5 minutes 20 | maxKeys: 250, 21 | }); 22 | 23 | private constructor( 24 | private readonly configuration: CertificateStoreConfiguration 25 | ) { 26 | this.storePath = resolve(coreConfigs.dataPath, this.configuration.folder); 27 | this.serialIdPath = resolve(this.storePath, 'serial'); 28 | } 29 | 30 | private static maybeGetFromCache(id: string): Certificate | undefined { 31 | return CertificateStore.cachedLookups.get(id); 32 | } 33 | 34 | private static maybeSetInCache(id: string, certificate: Certificate): void { 35 | try { 36 | CertificateStore.cachedLookups.set(id, certificate); 37 | } catch (_e) { 38 | // cache is full 39 | } 40 | } 41 | 42 | private static deleteFromCache(id: string): void { 43 | CertificateStore.cachedLookups.del(id); 44 | } 45 | 46 | private async init(): Promise { 47 | createDir(this.storePath); 48 | await this.setupSerialDependency(); 49 | } 50 | 51 | private certificateDtoPath(id: string): string { 52 | return resolve(this.storePath, `${id}.json`); 53 | } 54 | 55 | public certificatePath(id: string): string { 56 | return resolve(this.storePath, `${id}.cert`); 57 | } 58 | 59 | public async find(id: string): Promise { 60 | let certificate = CertificateStore.maybeGetFromCache(id); 61 | if (!certificate) { 62 | const dtoPath = this.certificateDtoPath(id); 63 | const fileData = await getFile(dtoPath, { 64 | encoding: coreConfigs.encoding, 65 | }); 66 | 67 | if (!fileData) { 68 | return null; 69 | } 70 | 71 | const certData = JSON.parse(fileData) as CertificateDTO; 72 | certificate = Certificate.restore(certData); 73 | } 74 | 75 | // this prevents expired certificates from being sent to the client 76 | if (certificate.shouldRenew) { 77 | this.delete(id); 78 | return null; 79 | } 80 | 81 | return certificate; 82 | } 83 | 84 | public getIssuedCertificatesIds(): string[] { 85 | const files = getFiles(this.storePath, ['json']); 86 | 87 | return files.map((file) => file.replace(/.json$/, '')); 88 | } 89 | 90 | public delete(id: string): void { 91 | CertificateStore.deleteFromCache(id); 92 | 93 | const dtoPath = this.certificateDtoPath(id); 94 | if (pathExists(dtoPath)) { 95 | rmSync(dtoPath, { force: true }); 96 | } 97 | 98 | const certPath = this.certificatePath(id); 99 | if (pathExists(certPath)) { 100 | rmSync(certPath, { force: true }); 101 | } 102 | } 103 | 104 | public async save(certificate: Certificate): Promise { 105 | const certPath = this.certificatePath(certificate.id); 106 | const dtoPath = this.certificateDtoPath(certificate.id); 107 | const dto = certificate.toDTO(); 108 | 109 | await saveFile(certPath, certificate.pem, { 110 | encoding: coreConfigs.encoding, 111 | }); 112 | await saveFile(dtoPath, JSON.stringify(dto), { 113 | encoding: coreConfigs.encoding, 114 | }); 115 | 116 | CertificateStore.maybeSetInCache(certificate.id, certificate); 117 | } 118 | 119 | public async setupSerialDependency(): Promise { 120 | if (pathExists(this.serialIdPath)) { 121 | return; 122 | } 123 | 124 | const files = getFiles(this.storePath, ['json', 'cert']); 125 | for (const file of files) { 126 | rmSync(resolve(this.storePath, file), { force: true }); 127 | } 128 | } 129 | 130 | public async nextSerialId(): Promise { 131 | const serial = 132 | (await getFile(this.serialIdPath, { encoding: 'utf-8' })) ?? '00'; 133 | const nextSerial = BigInt(serial) + BigInt(1); 134 | const nextSerialStr = nextSerial.toString(); 135 | 136 | await saveFile(this.serialIdPath, nextSerialStr, { 137 | encoding: 'utf-8', 138 | }); 139 | 140 | return nextSerialStr; 141 | } 142 | 143 | public static async create( 144 | configuration: CertificateStoreConfiguration 145 | ): Promise { 146 | const store = new CertificateStore(configuration); 147 | 148 | await store.init(); 149 | 150 | return store; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/windows/windows.ts: -------------------------------------------------------------------------------- 1 | import { 2 | execAsync, 3 | getDirectories, 4 | getFile, 5 | logger, 6 | pathExists, 7 | saveFile, 8 | } from '@dfinity/http-proxy-core'; 9 | import { resolve } from 'path'; 10 | import { Platform, PlatformProxyInfo } from '../typings'; 11 | import { PlatformConfigs } from './typings'; 12 | import { FIREFOX_PROFILES_PATH, isTrustedCertificate } from './utils'; 13 | 14 | export class WindowsPlatform implements Platform { 15 | constructor(private readonly configs: PlatformConfigs) {} 16 | 17 | public async attach(): Promise { 18 | logger.info( 19 | `attaching proxy to system with: ` + 20 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 21 | `capath(${this.configs.ca.path}), ` + 22 | `caname(${this.configs.ca.commonName})` 23 | ); 24 | 25 | await this.trustCertificate( 26 | true, 27 | this.configs.ca.path, 28 | this.configs.ca.commonName 29 | ); 30 | await this.configureWebProxy(true, { 31 | host: this.configs.pac.host, 32 | port: this.configs.pac.port, 33 | }); 34 | } 35 | 36 | public async detach(): Promise { 37 | logger.info( 38 | `detaching proxy from system with: ` + 39 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 40 | `capath(${this.configs.ca.path}), ` + 41 | `caname(${this.configs.ca.commonName})` 42 | ); 43 | 44 | await this.trustCertificate( 45 | false, 46 | this.configs.ca.path, 47 | this.configs.ca.commonName 48 | ); 49 | await this.configureWebProxy(false, { 50 | host: this.configs.pac.host, 51 | port: this.configs.pac.port, 52 | }); 53 | } 54 | 55 | private async deleteCertificateFromStore( 56 | certificateID: string 57 | ): Promise { 58 | const isTrusted = await isTrustedCertificate(certificateID); 59 | if (!isTrusted) { 60 | return; 61 | } 62 | 63 | await execAsync(`certutil -delstore root "${certificateID}"`); 64 | } 65 | 66 | private async trustCertificate( 67 | trust: boolean, 68 | path: string, 69 | certificateID: string 70 | ): Promise { 71 | await this.deleteCertificateFromStore(certificateID); 72 | 73 | if (trust) { 74 | await execAsync(`certutil -addstore root "${path}"`); 75 | 76 | await this.setupFirefoxPolicies(); 77 | } 78 | } 79 | 80 | private async setupFirefoxPolicies(): Promise { 81 | const appData = String(process.env.APPDATA); 82 | const profilesPath = resolve(appData, FIREFOX_PROFILES_PATH); 83 | 84 | if (!pathExists(profilesPath)) { 85 | // Firefox is not installed. 86 | return; 87 | } 88 | 89 | const profiles = getDirectories(profilesPath); 90 | 91 | for (const profileFolder of profiles) { 92 | const userPreferencesPath = resolve( 93 | profilesPath, 94 | profileFolder, 95 | 'user.js' 96 | ); 97 | 98 | const userPreferences = 99 | (await getFile(userPreferencesPath, { encoding: 'utf-8' })) ?? ''; 100 | 101 | const preferences = userPreferences 102 | .split('\r\n') 103 | .filter((line) => !line.includes('security.enterprise_roots.enabled')); 104 | 105 | preferences.push(`user_pref("security.enterprise_roots.enabled", true);`); 106 | 107 | await saveFile( 108 | userPreferencesPath, 109 | preferences.filter((line) => line.length > 0).join('\r\n'), 110 | { 111 | encoding: 'utf-8', 112 | } 113 | ); 114 | } 115 | } 116 | 117 | public async configureWebProxy( 118 | enable: boolean, 119 | { host, port }: PlatformProxyInfo 120 | ): Promise { 121 | return new Promise(async (ok, err) => { 122 | try { 123 | const updateInternetSettingsProxy = enable 124 | ? `powershell -command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' -name AutoConfigURL -Value 'http://${host}:${port}/proxy.pac'"` 125 | : `powershell -command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' -name AutoConfigURL -Value ''"`; 126 | 127 | const updateInternetSettingsEnabled = enable 128 | ? `powershell -command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' ProxyEnable -value 1"` 129 | : `powershell -command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' ProxyEnable -value 0"`; 130 | 131 | await execAsync(`${updateInternetSettingsProxy}`); 132 | await execAsync(`${updateInternetSettingsEnabled}`); 133 | 134 | ok(); 135 | } catch (e) { 136 | // failed to setup web proxy 137 | err(e); 138 | } 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/server/src/servers/icp/domains.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | import InMemoryCache from 'node-cache'; 3 | import { Resolver } from 'node:dns'; 4 | import { CANISTER_DNS_PREFIX, hostnameCanisterIdMap } from './static'; 5 | import { logger } from '@dfinity/http-proxy-core'; 6 | 7 | const cachedLookups = new InMemoryCache({ 8 | stdTTL: 60 * 5, // 5 minutes 9 | maxKeys: 1000, 10 | }); 11 | 12 | const dnsResolverTimeoutMs = 5000; 13 | const dnsResolveReries = 3; 14 | 15 | const inFlightLookups: Map> = new Map(); 16 | 17 | export const lookupIcDomain = async ( 18 | hostname: string 19 | ): Promise => { 20 | const cachedLookup = cachedLookups.get(hostname); 21 | 22 | if (cachedLookup !== undefined) { 23 | return cachedLookup !== null ? Principal.fromText(cachedLookup) : null; 24 | } 25 | 26 | const lookup = await maybeResolveIcDomain(hostname).catch((e) => { 27 | logger.error(`Failed to resolve ic domain(${e})`); 28 | 29 | return null; 30 | }); 31 | try { 32 | cachedLookups.set(hostname, lookup !== null ? lookup.toText() : null); 33 | } catch (e) { 34 | logger.error(`Failed to save lookup in the cache(${e})`); 35 | } 36 | 37 | return lookup; 38 | }; 39 | 40 | export const maybeResolveIcDomain = async ( 41 | hostname: string 42 | ): Promise => { 43 | const existingLookup = inFlightLookups.get(hostname); 44 | if (existingLookup !== undefined) { 45 | return existingLookup; 46 | } 47 | 48 | const inflightLookup = new Promise(async (ok, reject) => { 49 | try { 50 | if (isRawDomain(hostname)) { 51 | return ok(null); 52 | } 53 | 54 | const canisterFromStaticMap = hostnameCanisterIdMap.get(hostname); 55 | if (canisterFromStaticMap) { 56 | return ok(canisterFromStaticMap); 57 | } 58 | 59 | const canisterFromHostname = maybeResolveCanisterFromHostName(hostname); 60 | if (canisterFromHostname) { 61 | return ok(canisterFromHostname); 62 | } 63 | 64 | const canisterFromDns = await maybeResolveCanisterFromDns(hostname); 65 | 66 | ok(canisterFromDns); 67 | } catch (e) { 68 | reject(e); 69 | } 70 | }); 71 | 72 | inFlightLookups.set(hostname, inflightLookup); 73 | const lookupResult = await inflightLookup.finally(() => { 74 | inFlightLookups.delete(hostname); 75 | }); 76 | 77 | return lookupResult; 78 | }; 79 | 80 | export function isRawDomain(hostname: string): boolean { 81 | // For security reasons the match is only made for ic[0-9].app, ic[0-9].dev and icp[0-9].io domains. This makes 82 | // the match less permissive and prevents unwanted matches for domains that could include raw 83 | // but still serve as a normal dapp domain that should go through response verification. 84 | const isIcAppRaw = !!hostname.match(new RegExp(/\.raw\.ic[0-9]+\.app/)); 85 | const isIcDevRaw = !!hostname.match(new RegExp(/\.raw\.ic[0-9]+\.dev/)); 86 | const isIcpIoRaw = !!hostname.match(new RegExp(/\.raw\.icp[0-9]+\.io/)); 87 | const isTestnetRaw = !!hostname.match( 88 | new RegExp(/\.raw\.[\w-]+\.testnet\.[\w-]+\.network/) 89 | ); 90 | 91 | return isIcAppRaw || isIcDevRaw || isIcpIoRaw || isTestnetRaw; 92 | } 93 | 94 | /** 95 | * Split a hostname up-to the first valid canister ID from the right. 96 | * @param hostname The hostname to analyze. 97 | * @returns A canister ID followed by all subdomains that are after it, or null if no canister ID were found. 98 | */ 99 | export function maybeResolveCanisterFromHostName( 100 | hostname: string 101 | ): Principal | null { 102 | const subdomains = hostname.split('.').reverse(); 103 | for (const domain of subdomains) { 104 | try { 105 | return Principal.fromText(domain); 106 | } catch (_) { 107 | // subdomain did not match expected Principal format 108 | // continue checking each subdomain 109 | } 110 | } 111 | 112 | return null; 113 | } 114 | 115 | /** 116 | * Looks for the canister in the dns records of the hostname. 117 | * @param hostname The hostname to analyze. 118 | * @returns A canister ID or null if no canister ID was found. 119 | */ 120 | export const maybeResolveCanisterFromDns = async ( 121 | hostname: string 122 | ): Promise => { 123 | return new Promise((ok, reject) => { 124 | const dnsResolver = new Resolver({ 125 | timeout: dnsResolverTimeoutMs, 126 | tries: dnsResolveReries, 127 | }); 128 | 129 | dnsResolver.resolveTxt( 130 | `${CANISTER_DNS_PREFIX}.${hostname}`, 131 | (err, addresses) => { 132 | // those codes are expected if the subdomain is not set 133 | if (err && ['ENOTFOUND', 'ENODATA'].includes(err.code ?? '')) { 134 | return ok(null); 135 | } else if (err) { 136 | return reject(err); 137 | } 138 | 139 | const [result] = addresses ?? []; 140 | const [canisterId] = result ?? []; 141 | 142 | try { 143 | ok(Principal.fromText(canisterId)); 144 | } catch (e) { 145 | ok(null); 146 | } 147 | } 148 | ); 149 | }); 150 | }; 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICP HTTP Proxy 2 | 3 | > This application is currently only a proof of concept implementation and should be used at your own risk. 4 | 5 | ## Overview 6 | 7 | An implementation of the [ICP HTTP Gateway Protocol](https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-gateway) that enables end-to-end secure connections with dApps being served from the [Internet Computer](https://internetcomputer.org/). 8 | 9 | ### Motivation and Goals 10 | 11 | - Connect to the ICP network without the need for any trusted intermediaries. 12 | 13 | - Verify HTTP responses received from the ICP network for authenticity. 14 | 15 | - Resist censorship by bypassing traditional DNS infrastructure. 16 | 17 | - Enable resolution of crypto domains (not implemented yet). 18 | 19 | ### Key Features 20 | 21 | - Translate between ICP API calls and HTTP Asset Requests. 22 | 23 | - Terminate TLS connections locally with a self-generated root certificate authority. 24 | 25 | - Detect IC domains from principals and custom domain DNS records. 26 | 27 | - Bypass remote HTTP gateway denylists. 28 | 29 | ### Supported Platforms 30 | 31 | - Windows 32 | 33 | - MacOSX 34 | 35 | - Debian 36 | 37 | Other platforms can also be supported by adding the generated root certificate to the device's trusted store and adding the proxy HTTP server to the active network interface configuration. 38 | 39 | ## Installation 40 | 41 | To install the ICP HTTP Proxy, you can follow these steps: 42 | 43 | 1. Choose the appropriate installation package for your operating system: 44 | 45 | 2. Download the installation package for your operating system. 46 | 47 | 3. Run the downloaded package to start the installation process. 48 | 49 | 4. Follow the on-screen instructions to complete the installation. 50 | 51 | 5. Once the installation is complete, you can start using the ICP HTTP Proxy. 52 | 53 | [![Install MacOS](https://img.shields.io/badge/install-MacOSX-blue.svg?style=for-the-badge&logo=apple)](https://github.com/dfinity/http-proxy/releases/download/0.0.6-alpha/ic-http-proxy-mac-universal-0.0.6-alpha.dmg) 54 | [![Install Windows](https://img.shields.io/badge/install-Windows-blue.svg?style=for-the-badge&logo=windows)](https://github.com/dfinity/http-proxy/releases/download/0.0.6-alpha/ic-http-proxy-win-x64-0.0.6-alpha.exe) 55 | [![Install Debian](https://img.shields.io/badge/install-Debian-blue.svg?style=for-the-badge&logo=debian)](https://github.com/dfinity/http-proxy/releases/download/0.0.6-alpha/ic-http-proxy-linux-arm64-0.0.6-alpha.deb) 56 | 57 | ## Contributing 58 | 59 | External code contributions are not currently being accepted to this repository. 60 | 61 | ## Setup 62 | 63 | The package manager of this monorepo is [yarn](https://yarnpkg.com/) and the applications are built for [nodejs](https://nodejs.org/en). The usage of [nvm](https://github.com/nvm-sh/nvm) is recommended to keep the node version in sync. 64 | 65 | ### Setting up dependencies 66 | 67 | The following steps can be used to set up the proxy for local development and to package it to your system architecture. 68 | 69 | This will set up yarn with the latest stable release. 70 | 71 | ```bash 72 | corepack enable 73 | corepack prepare yarn@3.5.0 --activate 74 | ``` 75 | 76 | Yarn can also be enabled through `npm`. 77 | 78 | ``` 79 | npm install --global yarn 80 | yarn set version 3.5.0 81 | ``` 82 | 83 | All dependencies can be installed with a single install command from the root of the monorepo. 84 | 85 | ```bash 86 | yarn install 87 | ``` 88 | 89 | A recursive build is triggered for all of the `monorepo` packages. 90 | 91 | ```bash 92 | yarn build 93 | ``` 94 | 95 | Produces the required binaries and installation bundles for the supported platforms. 96 | 97 | ```bash 98 | yarn pkg 99 | ``` 100 | 101 | The proxy graphical interface is started and added to the operating system menu bar. 102 | 103 | ```bash 104 | yarn start 105 | ``` 106 | 107 | ## Packages 108 | 109 | This monorepo has multiple locally maintained packages in the root [package.json](package.json) configuration. 110 | 111 | | Package | Links | Description | 112 | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | 113 | | `core` | [![README](https://img.shields.io/badge/-README-blue?style=flat-square)](https://github.com/dfinity/http-proxy/tree/main/packages/core) | The `core` package contains a set of core features shared among other packages of this monorepo. | 114 | | `daemon` | [![README](https://img.shields.io/badge/-README-blue?style=flat-square)](https://github.com/dfinity/http-proxy/tree/main/packages/daemon) | A background process that receives tasks to execute against the operating system and monitors the status of the proxy server instance. | 115 | | `server` | [![README](https://img.shields.io/badge/-README-blue?style=flat-square)](https://github.com/dfinity/http-proxy/tree/main/packages/server) | The proxy server implementation the IC HTTP Gateway protocol, terminating TLS and resolving dApp domains. | 116 | | `ui` | [![README](https://img.shields.io/badge/-README-blue?style=flat-square)](https://github.com/dfinity/http-proxy/tree/main/packages/ui) | Electron app responsible for the graphical interface. | 117 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/mac/mac.ts: -------------------------------------------------------------------------------- 1 | import { 2 | execAsync, 3 | getDirectories, 4 | getFile, 5 | logger, 6 | pathExists, 7 | saveFile, 8 | } from '@dfinity/http-proxy-core'; 9 | import { exec } from 'child_process'; 10 | import { resolve } from 'path'; 11 | import { Platform } from '../typings'; 12 | import { PlatformConfigs, PlatformProxyInfo } from './typings'; 13 | import { 14 | CURL_RC_FILE, 15 | FIREFOX_PROFILES_PATH, 16 | SHELL_SCRIPT_SEPARATOR, 17 | getActiveNetworkService, 18 | } from './utils'; 19 | 20 | export class MacPlatform implements Platform { 21 | constructor(private readonly configs: PlatformConfigs) {} 22 | 23 | public async attach(): Promise { 24 | logger.info( 25 | `attaching proxy to system with: ` + 26 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 27 | `capath(${this.configs.ca.path}), ` + 28 | `caname(${this.configs.ca.commonName})` 29 | ); 30 | 31 | await this.trustCertificate( 32 | true, 33 | this.configs.ca.path, 34 | this.configs.ca.commonName 35 | ); 36 | await this.configureWebProxy(true, { 37 | host: this.configs.proxy.host, 38 | port: this.configs.proxy.port, 39 | }); 40 | } 41 | 42 | public async detach(): Promise { 43 | logger.info( 44 | `detaching proxy from system with: ` + 45 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 46 | `capath(${this.configs.ca.path}), ` + 47 | `caname(${this.configs.ca.commonName})` 48 | ); 49 | 50 | await this.trustCertificate( 51 | false, 52 | this.configs.ca.path, 53 | this.configs.ca.commonName 54 | ); 55 | await this.configureWebProxy(false, { 56 | host: this.configs.proxy.host, 57 | port: this.configs.proxy.port, 58 | }); 59 | } 60 | 61 | private async isCertificatedInSystemStore( 62 | commonName: string 63 | ): Promise { 64 | return execAsync(`security find-certificate -c '${commonName}'`) 65 | .then(() => true) 66 | .catch(() => false); 67 | } 68 | 69 | private async deleteCertificateFromStore(commonName: string): Promise { 70 | const isInStore = await this.isCertificatedInSystemStore(commonName); 71 | if (!isInStore) { 72 | return; 73 | } 74 | 75 | await execAsync( 76 | 'security authorizationdb write com.apple.trust-settings.admin allow' + 77 | SHELL_SCRIPT_SEPARATOR + 78 | `security delete-certificate -c '${commonName}'` + 79 | SHELL_SCRIPT_SEPARATOR + 80 | 'security authorizationdb remove com.apple.trust-settings.admin' 81 | ); 82 | } 83 | 84 | private async trustCertificate( 85 | trust: boolean, 86 | path: string, 87 | commonName: string 88 | ): Promise { 89 | await this.deleteCertificateFromStore(commonName); 90 | 91 | if (trust) { 92 | await execAsync( 93 | 'security authorizationdb write com.apple.trust-settings.admin allow' + 94 | SHELL_SCRIPT_SEPARATOR + 95 | `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${path}` + 96 | SHELL_SCRIPT_SEPARATOR + 97 | 'security authorizationdb remove com.apple.trust-settings.admin' 98 | ); 99 | 100 | await this.setupFirefoxPolicies(); 101 | } 102 | } 103 | 104 | private async setupFirefoxPolicies(): Promise { 105 | const homePath = String(process.env.HOME); 106 | const profilesPath = resolve(homePath, FIREFOX_PROFILES_PATH); 107 | 108 | if (!pathExists(profilesPath)) { 109 | // Firefox is not installed. 110 | return; 111 | } 112 | 113 | const profiles = getDirectories(profilesPath); 114 | 115 | for (const profileFolder of profiles) { 116 | const userPreferencesPath = resolve( 117 | profilesPath, 118 | profileFolder, 119 | 'user.js' 120 | ); 121 | 122 | const userPreferences = 123 | (await getFile(userPreferencesPath, { encoding: 'utf8' })) ?? ''; 124 | 125 | const preferences = userPreferences 126 | .split('\n') 127 | .filter((line) => !line.includes('security.enterprise_roots.enabled')); 128 | 129 | preferences.push(`user_pref("security.enterprise_roots.enabled", true);`); 130 | 131 | await saveFile( 132 | userPreferencesPath, 133 | preferences.filter((line) => line.length > 0).join('\n') + '\n', 134 | { 135 | encoding: 'utf-8', 136 | } 137 | ); 138 | } 139 | } 140 | 141 | public async configureWebProxy( 142 | enable: boolean, 143 | { host, port }: PlatformProxyInfo 144 | ): Promise { 145 | return new Promise(async (ok, err) => { 146 | try { 147 | // configure proxy for curl 148 | const curlrcPath = resolve(String(process.env.HOME), CURL_RC_FILE); 149 | const curlrc = (await getFile(curlrcPath, { encoding: 'utf8' })) ?? ''; 150 | const curlrcLines = curlrc 151 | .split('\n') 152 | .filter((line) => !line.startsWith('proxy=')); 153 | if (enable) { 154 | curlrcLines.push(`proxy=http://${host}:${port}`); 155 | } 156 | await saveFile(curlrcPath, curlrcLines.join('\n'), { 157 | encoding: 'utf-8', 158 | }); 159 | 160 | // configure proxy to the active network interface 161 | await this.tooggleNetworkWebProxy(enable); 162 | 163 | ok(); 164 | } catch (e) { 165 | // failed to setup web proxy 166 | err(e); 167 | } 168 | }); 169 | } 170 | 171 | private async tooggleNetworkWebProxy(enable: boolean): Promise { 172 | const networkService = getActiveNetworkService(); 173 | 174 | if (!networkService) { 175 | throw new Error('no active network service found'); 176 | } 177 | 178 | const status = enable ? 'on' : 'off'; 179 | const commands: string[] = []; 180 | // enable admin privileges 181 | commands.push( 182 | `security authorizationdb write com.apple.trust-settings.admin allow` 183 | ); 184 | // toggle web proxy for the active network interface 185 | if (enable) { 186 | commands.push( 187 | `networksetup -setautoproxyurl "${networkService}" "http://${this.configs.pac.host}:${this.configs.pac.port}/proxy.pac"` 188 | ); 189 | } 190 | commands.push( 191 | `networksetup -setautoproxystate "${networkService}" ${status}` 192 | ); 193 | // remove admin privileges 194 | commands.push( 195 | `security authorizationdb remove com.apple.trust-settings.admin` 196 | ); 197 | const shellScript = commands.join(SHELL_SCRIPT_SEPARATOR); 198 | 199 | return new Promise(async (ok, err) => { 200 | exec(`${shellScript}`, (error) => { 201 | if (error) { 202 | return err(error); 203 | } 204 | 205 | ok(); 206 | }); 207 | }); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/core/src/tls/factory.ts: -------------------------------------------------------------------------------- 1 | import { pki } from 'node-forge'; 2 | import { retryClosure } from '~src/commons'; 3 | import { 4 | CertificateCreationFailedError, 5 | UnsupportedCertificateTypeError, 6 | } from '../errors'; 7 | import { Certificate } from './certificate'; 8 | import { CertificateStore } from './store'; 9 | import { CertificateConfiguration, CreateCertificateOpts } from './typings'; 10 | import { generateCertificate, generateKeyPair } from './utils'; 11 | 12 | export class CertificateFactory { 13 | private readonly issuer: pki.CertificateField[]; 14 | public store!: CertificateStore; 15 | 16 | private constructor( 17 | private readonly configuration: CertificateConfiguration 18 | ) { 19 | this.issuer = [ 20 | { name: 'commonName', value: configuration.rootca.commonName }, 21 | { 22 | name: 'organizationName', 23 | value: configuration.rootca.organizationName, 24 | }, 25 | { shortName: 'OU', value: configuration.rootca.organizationUnit }, 26 | ]; 27 | } 28 | 29 | public static async build( 30 | configuration: CertificateConfiguration 31 | ): Promise { 32 | const factory = new CertificateFactory(configuration); 33 | await factory.init(); 34 | 35 | return factory; 36 | } 37 | 38 | private async init(): Promise { 39 | this.store = await CertificateStore.create({ 40 | folder: this.configuration.storage.folder, 41 | }); 42 | } 43 | 44 | async create( 45 | opts: CreateCertificateOpts, 46 | renew?: boolean 47 | ): Promise { 48 | try { 49 | let certificate: Certificate; 50 | switch (opts.type) { 51 | case 'ca': 52 | certificate = await this.createRootCA('ca', renew); 53 | break; 54 | case 'domain': 55 | certificate = await this.createHostCertificate( 56 | opts.hostname, 57 | opts.ca, 58 | renew 59 | ); 60 | break; 61 | default: 62 | throw new UnsupportedCertificateTypeError(); 63 | } 64 | 65 | return certificate; 66 | } catch (e) { 67 | throw new CertificateCreationFailedError(String(e)); 68 | } 69 | } 70 | 71 | private async createRootCA( 72 | caId = 'ca', 73 | renew?: boolean 74 | ): Promise { 75 | const id = `root_${caId}`; 76 | 77 | return retryClosure( 78 | async () => { 79 | const current = await this.store.find(id); 80 | if (current && !renew) { 81 | return current; 82 | } 83 | 84 | const keyPair = await generateKeyPair(); 85 | const pem = await generateCertificate({ 86 | publicKey: keyPair.publicKey, 87 | signingKey: keyPair.privateKey, 88 | issuer: [...this.issuer], 89 | serialId: await this.store.nextSerialId(), 90 | // same as issuer since this is self signed 91 | subject: [...this.issuer], 92 | extensions: [ 93 | { name: 'basicConstraints', cA: true, critical: true }, 94 | { 95 | name: 'keyUsage', 96 | keyCertSign: true, 97 | digitalSignature: true, 98 | keyEncipherment: true, 99 | }, 100 | { 101 | name: 'extKeyUsage', 102 | serverAuth: true, 103 | clientAuth: true, 104 | codeSigning: true, 105 | emailProtection: true, 106 | timeStamping: true, 107 | }, 108 | { name: 'subjectKeyIdentifier' }, 109 | ], 110 | }); 111 | 112 | const certificate = new Certificate(id, { 113 | key: keyPair.privateKey, 114 | public: keyPair.publicKey, 115 | pem, 116 | }); 117 | 118 | await this.store.save(certificate); 119 | await this.renewHostCertificates(certificate); 120 | 121 | return certificate; 122 | }, 123 | async () => this.store.delete(id), 124 | this.configuration.creationRetries 125 | ); 126 | } 127 | 128 | private async renewHostCertificates(ca: Certificate): Promise { 129 | const hostPrefix = `${this.configuration.storage.hostPrefix}_`; 130 | const hostnames = this.store 131 | .getIssuedCertificatesIds() 132 | .filter((file) => file.startsWith(hostPrefix)) 133 | .map((file) => file.replace(hostPrefix, '')); 134 | 135 | for (const hostname of hostnames) { 136 | await this.createHostCertificate(hostname, ca, true); 137 | } 138 | } 139 | 140 | private async createHostCertificate( 141 | hostname: string, 142 | ca: Certificate, 143 | renew?: boolean 144 | ): Promise { 145 | const id = `${this.configuration.storage.hostPrefix}_${hostname}`; 146 | 147 | return retryClosure( 148 | async () => { 149 | const current = await this.store.find(id); 150 | if (current && !renew) { 151 | return current; 152 | } 153 | 154 | const keyPair = await generateKeyPair(); 155 | const caCert = ca.info; 156 | 157 | const pem = await generateCertificate({ 158 | publicKey: keyPair.publicKey, 159 | signingKey: ca.key, 160 | issuer: caCert.subject.attributes, 161 | serialId: await this.store.nextSerialId(), 162 | subject: [ 163 | { name: 'commonName', value: hostname }, 164 | { 165 | name: 'organizationName', 166 | value: `${this.configuration.rootca.organizationName} (${hostname})`, 167 | }, 168 | { 169 | shortName: 'OU', 170 | value: this.configuration.rootca.organizationUnit, 171 | }, 172 | ], 173 | extensions: [ 174 | { name: 'basicConstraints', cA: false, critical: true }, 175 | { 176 | name: 'keyUsage', 177 | digitalSignature: true, 178 | keyEncipherment: true, 179 | }, 180 | { name: 'extKeyUsage', serverAuth: true }, 181 | { name: 'subjectKeyIdentifier' }, 182 | { 183 | name: 'subjectAltName', 184 | altNames: [ 185 | // https://www.rfc-editor.org/rfc/rfc5280#page-38 186 | { 187 | type: 2, // dns name 188 | value: hostname, 189 | }, 190 | { 191 | type: 7, // IP 192 | ip: '127.0.0.1', 193 | }, 194 | ], 195 | }, 196 | ], 197 | }); 198 | 199 | const certificate = new Certificate(id, { 200 | key: keyPair.privateKey, 201 | public: keyPair.publicKey, 202 | pem, 203 | }); 204 | 205 | await this.store.save(certificate); 206 | 207 | return certificate; 208 | }, 209 | async () => this.store.delete(id), 210 | this.configuration.creationRetries 211 | ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /packages/ui/src/proxy-ui.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IPCClient, 3 | assertPresent, 4 | coreConfigs, 5 | logger, 6 | } from '@dfinity/http-proxy-core'; 7 | import { Tray, app, nativeTheme } from 'electron'; 8 | import { proxyNodeEntrypointPath, waitProcessing } from '~src/commons/utils'; 9 | import { 10 | ElectronClickFnOptions, 11 | Images, 12 | ProxyMenu, 13 | ProxyMenuItem, 14 | } from '~src/interface'; 15 | import { ProxyService } from '~src/services'; 16 | import { ProxyStatus, ProxyUIOptions } from '~src/typings'; 17 | 18 | export class ProxyUI { 19 | private tray!: Tray; 20 | private taskbar!: ProxyMenu; 21 | private readonly images: Images; 22 | private isStarting = false; 23 | private isStopping = false; 24 | private updater: null | NodeJS.Timeout = null; 25 | private static readonly maxStatusChangeMs = 20000; 26 | private lastStatus = ProxyStatus.Disabled; 27 | 28 | private constructor( 29 | private readonly configs: ProxyUIOptions, 30 | private readonly proxyService: ProxyService 31 | ) { 32 | this.images = new Images(configs.darkMode); 33 | } 34 | 35 | static async init(): Promise { 36 | const proxyPath = await proxyNodeEntrypointPath(); 37 | const ui = new ProxyUI( 38 | { 39 | proxy: { entrypoint: proxyPath }, 40 | darkMode: nativeTheme.shouldUseDarkColors, 41 | }, 42 | new ProxyService(new IPCClient({ path: coreConfigs.ipcChannels.proxy })) 43 | ); 44 | 45 | await ui.render(); 46 | } 47 | 48 | async render(): Promise { 49 | this.tray = new Tray(this.images.tray); 50 | this.taskbar = new ProxyMenu(this.images); 51 | 52 | this.taskbar.onClick(ProxyMenuItem.Quit, this.onQuit.bind(this)); 53 | this.taskbar.onClick(ProxyMenuItem.Start, this.onStart.bind(this)); 54 | this.taskbar.onClick(ProxyMenuItem.Stop, this.onStop.bind(this)); 55 | 56 | this.tray.setContextMenu(this.taskbar.menu); 57 | this.tray.setToolTip('IC HTTP Proxy'); 58 | 59 | this.registerInterfaceUpdater(); 60 | } 61 | 62 | async registerInterfaceUpdater(statusCheckIntervalMs = 500): Promise { 63 | this.unregisterInterfaceUpdater(); 64 | 65 | this.updater = setTimeout(() => { 66 | this.updateInterface().finally(() => 67 | this.registerInterfaceUpdater(statusCheckIntervalMs) 68 | ); 69 | }, statusCheckIntervalMs); 70 | } 71 | 72 | unregisterInterfaceUpdater(): void { 73 | if (this.updater) { 74 | clearTimeout(this.updater); 75 | } 76 | } 77 | 78 | async updateInterface(isProxyRunning?: boolean): Promise { 79 | try { 80 | const isProxyProcessRunning = 81 | isProxyRunning ?? (await this.proxyService.isEnabled()); 82 | 83 | const startItem = assertPresent( 84 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.Start) 85 | ); 86 | const stopItem = assertPresent( 87 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.Stop) 88 | ); 89 | const enabledStatusItem = assertPresent( 90 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.EnabledStatus) 91 | ); 92 | const disabledStatusItem = assertPresent( 93 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.DisabledStatus) 94 | ); 95 | const stoppingStatusItem = assertPresent( 96 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.StoppingStatus) 97 | ); 98 | const startingStatusItem = assertPresent( 99 | this.taskbar.menu.getMenuItemById(ProxyMenuItem.StartingStatus) 100 | ); 101 | 102 | const shouldBlockActionButtons = this.isStarting || this.isStopping; 103 | 104 | if ( 105 | isProxyProcessRunning && 106 | this.lastStatus !== ProxyStatus.Enabled && 107 | !shouldBlockActionButtons 108 | ) { 109 | this.lastStatus = ProxyStatus.Enabled; 110 | 111 | this.tray.setImage(this.images.trayEnabled); 112 | } else if ( 113 | !isProxyProcessRunning && 114 | this.lastStatus !== ProxyStatus.Disabled && 115 | !shouldBlockActionButtons 116 | ) { 117 | this.lastStatus = ProxyStatus.Disabled; 118 | 119 | this.tray.setImage(this.images.tray); 120 | } 121 | 122 | if (shouldBlockActionButtons) { 123 | startItem.enabled = false; 124 | stopItem.enabled = false; 125 | } else { 126 | startItem.enabled = !isProxyProcessRunning; 127 | stopItem.enabled = isProxyProcessRunning; 128 | } 129 | 130 | startingStatusItem.visible = this.isStarting; 131 | stoppingStatusItem.visible = this.isStopping; 132 | enabledStatusItem.visible = 133 | isProxyProcessRunning && !this.isStarting && !this.isStopping; 134 | disabledStatusItem.visible = 135 | !isProxyProcessRunning && !this.isStarting && !this.isStopping; 136 | 137 | this.refreshUI(); 138 | } catch (e) { 139 | logger.error(`Failed to update statuses(${String(e)})`); 140 | } 141 | } 142 | 143 | async onStart(): Promise { 144 | let isProxyProcessRunning = await this.proxyService.isEnabled(); 145 | if (isProxyProcessRunning) { 146 | return; 147 | } 148 | this.unregisterInterfaceUpdater(); 149 | 150 | try { 151 | this.isStarting = true; 152 | await this.updateInterface(isProxyProcessRunning); 153 | const isStarted = await this.proxyService.startProxyServers( 154 | this.configs.proxy.entrypoint 155 | ); 156 | if (!isStarted) { 157 | throw new Error(`timeout`); 158 | } 159 | 160 | isProxyProcessRunning = await this.proxyService.enable(); 161 | await this.updateInterface(isProxyProcessRunning); 162 | } catch (e) { 163 | logger.error(`Failed to start proxy(${String(e)})`); 164 | } finally { 165 | this.isStarting = false; 166 | } 167 | 168 | this.registerInterfaceUpdater(); 169 | } 170 | 171 | async onStop(): Promise { 172 | let isProxyProcessRunning = await this.proxyService.isEnabled(); 173 | if (!isProxyProcessRunning) { 174 | return; 175 | } 176 | this.unregisterInterfaceUpdater(); 177 | try { 178 | this.isStopping = true; 179 | await this.updateInterface(isProxyProcessRunning); 180 | await this.proxyService.stopServers(); 181 | const executedSuccessfully = await waitProcessing( 182 | async () => !(await this.proxyService.isEnabled()) 183 | ); 184 | isProxyProcessRunning = executedSuccessfully ? false : true; 185 | await this.updateInterface(isProxyProcessRunning); 186 | 187 | if (isProxyProcessRunning) { 188 | logger.error(`Proxy stop event timeout`); 189 | } 190 | } catch (e) { 191 | logger.error(`Failed to stop proxy(${String(e)})`); 192 | } finally { 193 | this.isStopping = false; 194 | } 195 | 196 | this.registerInterfaceUpdater(); 197 | } 198 | 199 | async onQuit(opts: ElectronClickFnOptions): Promise { 200 | this.unregisterInterfaceUpdater(); 201 | opts.menuItem.enabled = false; 202 | 203 | const isProxyStarted = await this.proxyService.isStarted(); 204 | if (isProxyStarted) { 205 | await this.proxyService.stopServers(); 206 | } 207 | 208 | this.refreshUI(); 209 | app.quit(); 210 | } 211 | 212 | refreshUI(): void { 213 | this.tray.setContextMenu(this.taskbar.menu); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /packages/daemon/src/platforms/linux/linux.ts: -------------------------------------------------------------------------------- 1 | import { 2 | execAsync, 3 | getDirectories, 4 | getFile, 5 | logger, 6 | pathExists, 7 | saveFile, 8 | } from '@dfinity/http-proxy-core'; 9 | import { Platform, PlatformProxyInfo } from '../typings'; 10 | import { PlatformConfigs } from './typings'; 11 | import { resolve } from 'path'; 12 | import { 13 | BASE_MOZILLA_PATH, 14 | BASE_SNAP_MOZZILA_PATH, 15 | CURL_RC_FILE, 16 | FIREFOX_PROFILES_FOLDER, 17 | MOZILLA_CERTIFICATES_FOLDER, 18 | ROOT_CA_PATH, 19 | findDbusLaunchPath, 20 | findP11KitTrustPath, 21 | } from './utils'; 22 | 23 | export class LinuxPlatform implements Platform { 24 | constructor( 25 | private readonly configs: PlatformConfigs, 26 | private readonly username = String(process.env.LOGNAME ?? 'root') 27 | ) {} 28 | 29 | public async attach(): Promise { 30 | await this.setupDependencies(); 31 | 32 | logger.info( 33 | `attaching proxy to system with: ` + 34 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 35 | `capath(${this.configs.ca.path}), ` + 36 | `caname(${this.configs.ca.commonName})` 37 | ); 38 | 39 | await this.trustCertificate(true, this.configs.ca.path); 40 | 41 | await this.configureWebProxy(true, { 42 | host: this.configs.proxy.host, 43 | port: this.configs.proxy.port, 44 | }); 45 | } 46 | 47 | public async detach(): Promise { 48 | await this.setupDependencies(); 49 | 50 | logger.info( 51 | `detaching proxy from system with: ` + 52 | `host(${this.configs.proxy.host}:${this.configs.proxy.port}), ` + 53 | `capath(${this.configs.ca.path}), ` + 54 | `caname(${this.configs.ca.commonName})` 55 | ); 56 | 57 | await this.trustCertificate(false, this.configs.ca.path); 58 | 59 | await this.configureWebProxy(false, { 60 | host: this.configs.proxy.host, 61 | port: this.configs.proxy.port, 62 | }); 63 | } 64 | 65 | public async configureWebProxy( 66 | enable: boolean, 67 | { host, port }: PlatformProxyInfo 68 | ): Promise { 69 | const curlrcPath = resolve(String(process.env.HOME), CURL_RC_FILE); 70 | const curlrc = (await getFile(curlrcPath, { encoding: 'utf-8' })) ?? ''; 71 | const curlrcLines = curlrc 72 | .split('\n') 73 | .filter((line) => !line.startsWith('proxy=')); 74 | if (enable) { 75 | curlrcLines.push(`proxy=http://${host}:${port}`); 76 | } 77 | await saveFile(curlrcPath, curlrcLines.join('\n'), { 78 | encoding: 'utf-8', 79 | }); 80 | 81 | await this.tooggleNetworkWebProxy(enable); 82 | } 83 | 84 | private async tooggleNetworkWebProxy(enable: boolean): Promise { 85 | const dconfCachePath = `/home/${this.username}/.cache/dconf`; 86 | const pacUrl = `http://${this.configs.pac.host}:${this.configs.pac.port}/proxy.pac`; 87 | 88 | if (pathExists(dconfCachePath)) { 89 | await execAsync( 90 | `sudo chown -R "${this.username}":"${this.username}" "${dconfCachePath}"` 91 | ); 92 | } 93 | 94 | if (enable) { 95 | await execAsync( 96 | [ 97 | `sudo -u "${this.username}" dbus-launch gsettings set org.gnome.system.proxy mode 'auto'`, 98 | `sudo -u "${this.username}" dbus-launch gsettings set org.gnome.system.proxy autoconfig-url '${pacUrl}'`, 99 | ].join(' && ') 100 | ); 101 | 102 | return; 103 | } 104 | 105 | await execAsync( 106 | [ 107 | `sudo -u "${this.username}" dbus-launch gsettings set org.gnome.system.proxy mode 'none'`, 108 | ].join(' && ') 109 | ); 110 | } 111 | 112 | private async trustCertificate(trust: boolean, path: string): Promise { 113 | if (trust) { 114 | await execAsync( 115 | `sudo cp "${path}" "${ROOT_CA_PATH}" && sudo update-ca-certificates` 116 | ); 117 | 118 | await this.firefoxTrustCertificate(); 119 | return; 120 | } 121 | 122 | await execAsync( 123 | `sudo rm -rf "${ROOT_CA_PATH}" && sudo update-ca-certificates` 124 | ); 125 | } 126 | 127 | private async firefoxTrustCertificate(): Promise { 128 | await this.setupFirefoxCertificateConfigurations(BASE_MOZILLA_PATH); 129 | await this.setupFirefoxCertificateConfigurations(BASE_SNAP_MOZZILA_PATH); 130 | } 131 | 132 | private async setupFirefoxCertificateConfigurations( 133 | basePath: string 134 | ): Promise { 135 | const homePath = String(process.env.HOME); 136 | const mozillaPathPath = resolve(homePath, basePath); 137 | const certificatesPath = resolve( 138 | mozillaPathPath, 139 | MOZILLA_CERTIFICATES_FOLDER 140 | ); 141 | const profilesPath = resolve(mozillaPathPath, FIREFOX_PROFILES_FOLDER); 142 | 143 | if (!pathExists(mozillaPathPath)) { 144 | // Firefox is not installed. 145 | return; 146 | } 147 | 148 | await this.firefoxSetupCertificates(certificatesPath); 149 | await this.firefoxSetupProfiles(profilesPath); 150 | } 151 | 152 | private async setupDependencies(): Promise { 153 | const p11KitPath = await findP11KitTrustPath(); 154 | 155 | if (!p11KitPath) { 156 | await execAsync( 157 | 'sudo apt install p11-kit p11-kit-modules libnss3-tools -y' 158 | ); 159 | const installed = await findP11KitTrustPath(); 160 | 161 | if (!installed) { 162 | throw new Error('Failed to setup p11-kit dependency'); 163 | } 164 | } 165 | 166 | const dbusLaunchPath = await findDbusLaunchPath(); 167 | if (!dbusLaunchPath) { 168 | await execAsync(`sudo apt install dbus-x11 -y`); 169 | 170 | const installed = await findDbusLaunchPath(); 171 | 172 | if (!installed) { 173 | throw new Error('Failed to setup dbus-x11 dependency'); 174 | } 175 | } 176 | } 177 | 178 | private async firefoxSetupCertificates(profilesPath: string): Promise { 179 | if (!pathExists(profilesPath)) { 180 | return; 181 | } 182 | 183 | const p11KitPath = await findP11KitTrustPath(); 184 | if (!p11KitPath) { 185 | throw new Error('Failed to find certificate store path'); 186 | } 187 | 188 | // firefox profile directories end with .default|.default-release 189 | const profiles = getDirectories(profilesPath).filter( 190 | (dir) => dir.endsWith('.default') || dir.endsWith('.default-release') 191 | ); 192 | 193 | for (const profileFolder of profiles) { 194 | const profilePath = resolve(profilesPath, profileFolder); 195 | 196 | await execAsync( 197 | `modutil -dbdir sql:${profilePath} -add "P11 Kit" -libfile ${p11KitPath}` 198 | ); 199 | } 200 | } 201 | 202 | private async firefoxSetupProfiles(profilesPath: string): Promise { 203 | if (!pathExists(profilesPath)) { 204 | return; 205 | } 206 | 207 | // firefox profile directories end with .default|.default-release 208 | const profiles = getDirectories(profilesPath).filter( 209 | (dir) => dir.endsWith('.default') || dir.endsWith('.default-release') 210 | ); 211 | 212 | for (const profileFolder of profiles) { 213 | const userPreferencesPath = resolve( 214 | profilesPath, 215 | profileFolder, 216 | 'user.js' 217 | ); 218 | 219 | const userPreferences = 220 | (await getFile(userPreferencesPath, { encoding: 'utf8' })) ?? ''; 221 | 222 | const preferences = userPreferences 223 | .split('\n') 224 | .filter((line) => !line.includes('security.enterprise_roots.enabled')); 225 | 226 | preferences.push(`user_pref("security.enterprise_roots.enabled", true);`); 227 | 228 | await saveFile( 229 | userPreferencesPath, 230 | preferences.filter((line) => line.length > 0).join('\n') + '\n', 231 | { 232 | encoding: 'utf-8', 233 | } 234 | ); 235 | await execAsync( 236 | `sudo chown ${this.username}:${this.username} "${userPreferencesPath}"` 237 | ); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /packages/server/src/servers/net/index.ts: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import { MissingConnectionHostError } from '~src/errors'; 3 | import { lookupIcDomain } from '../icp/domains'; 4 | import { ConnectionInfo, NetProxyOpts, ServerInfo } from './typings'; 5 | import { logger } from '@dfinity/http-proxy-core'; 6 | 7 | export enum DefaultPorts { 8 | secure = 443, 9 | insecure = 80, 10 | } 11 | 12 | export class NetProxy { 13 | private connections = new Set(); 14 | 15 | private constructor( 16 | private readonly server: net.Server, 17 | private readonly opts: NetProxyOpts 18 | ) {} 19 | 20 | public static create(opts: NetProxyOpts): NetProxy { 21 | const proxy = new NetProxy(net.createServer(), opts); 22 | proxy.init(); 23 | 24 | return proxy; 25 | } 26 | 27 | private init(): void { 28 | this.server.on('connection', this.onConnection.bind(this)); 29 | this.server.on('close', this.onClose.bind(this)); 30 | } 31 | 32 | public async shutdown(): Promise { 33 | logger.info('Shutting down net server.'); 34 | return new Promise((ok) => { 35 | this.server.close(() => ok()); 36 | this.connections.forEach((socket) => { 37 | socket.destroy(); 38 | }); 39 | }); 40 | } 41 | 42 | public async start(): Promise { 43 | return new Promise((ok, err) => { 44 | const onListenError = (e: Error) => { 45 | if ('code' in e && e.code === 'EADDRINUSE') { 46 | this.server.close(); 47 | } 48 | 49 | return err(e); 50 | }; 51 | 52 | this.server.addListener('error', onListenError); 53 | 54 | this.server.listen(this.opts.port, this.opts.host, () => { 55 | this.server.removeListener('error', onListenError); 56 | this.server.addListener('error', this.onError.bind(this)); 57 | 58 | ok(); 59 | }); 60 | }); 61 | } 62 | 63 | private getConnectionInfo(data: string): ConnectionInfo { 64 | const isTLSConnection = data.indexOf('CONNECT') !== -1; 65 | const info = isTLSConnection 66 | ? data.split('CONNECT ')?.[1]?.split(' ')?.[0]?.split(':') 67 | : data.split('Host: ')?.[1]?.split('\r\n')?.[0]?.split(':'); 68 | const [host, port] = info ?? []; 69 | const defaultPort = isTLSConnection 70 | ? DefaultPorts.secure 71 | : DefaultPorts.insecure; 72 | 73 | if (!host) { 74 | throw new MissingConnectionHostError(); 75 | } 76 | 77 | return { 78 | host, 79 | port: port ? Number(port) : defaultPort, 80 | secure: isTLSConnection, 81 | }; 82 | } 83 | 84 | private async onConnection(socket: net.Socket): Promise { 85 | let info: ConnectionInfo | undefined; 86 | 87 | this.connections.add(socket); 88 | 89 | socket.on('close', () => { 90 | socket?.destroy(); 91 | this.connections.delete(socket); 92 | }); 93 | 94 | socket.once('data', async (data) => { 95 | try { 96 | const socketData = data.toString(); 97 | const connectionInfo = this.getConnectionInfo(socketData); 98 | info = connectionInfo; 99 | 100 | // prevent loop connection passthrough to self 101 | if (info.host === this.opts.host && info.port === this.opts.port) { 102 | this.handleProxySameConnection(socket); 103 | return; 104 | } 105 | 106 | const icRequest = await this.shouldHandleAsICPRequest(connectionInfo); 107 | 108 | logger.info( 109 | icRequest 110 | ? `Proxying web3 request for ${connectionInfo.host}:${connectionInfo.port}` 111 | : `Proxying web2 request for ${connectionInfo.host}:${connectionInfo.port}` 112 | ); 113 | if (icRequest) { 114 | this.handleInternetComputerConnection(connectionInfo, socket, data); 115 | return; 116 | } 117 | 118 | this.connectionPassthrough( 119 | { 120 | host: connectionInfo.host, 121 | port: connectionInfo.port, 122 | }, 123 | connectionInfo, 124 | socket, 125 | data 126 | ); 127 | } catch (e) { 128 | logger.error(`Failed to proxy request ${String(e)}`); 129 | 130 | socket.end(); 131 | } 132 | }); 133 | 134 | socket.on('error', (err) => { 135 | logger.error( 136 | !info 137 | ? `Client socket error (${err})` 138 | : `Client socket error ${info.host}:${info.port} (${err})` 139 | ); 140 | 141 | if ('code' in err && err.code === 'ENOTFOUND') { 142 | const body = 143 | 'Error: ENOTFOUND - The requested resource could not be found.'; 144 | socket.write( 145 | 'HTTP/1.1 404 Not Found\r\n' + 146 | 'Server: IC HTTP Proxy\r\n' + 147 | 'Content-Type: text/plain\r\n' + 148 | 'Connection: close\r\n' + 149 | `Content-Length: ${Buffer.byteLength(body)}\r\n` + 150 | `\r\n` + 151 | body 152 | ); 153 | } 154 | socket.end(); 155 | }); 156 | } 157 | 158 | private handleProxySameConnection(socket: net.Socket): void { 159 | const body = 160 | 'The IC HTTP Proxy is running and will proxy your requests to the Internet Computer.'; 161 | 162 | socket.write( 163 | 'HTTP/1.1 200 Ok\r\n' + 164 | 'Server: IC HTTP Proxy\r\n' + 165 | 'Content-Type: text/plain\r\n' + 166 | 'Connection: close\r\n' + 167 | `Content-Length: ${Buffer.byteLength(body)}\r\n` + 168 | `\r\n` + 169 | body 170 | ); 171 | 172 | socket.end(); 173 | } 174 | 175 | private handleInternetComputerConnection( 176 | connection: ConnectionInfo, 177 | clientSocket: net.Socket, 178 | data: Buffer 179 | ): void { 180 | if (!connection.secure) { 181 | const body = 'Page moved permanently'; 182 | 183 | clientSocket.write( 184 | 'HTTP/1.1 301 Moved Permanently\r\n' + 185 | 'Server: IC HTTP Proxy\r\n' + 186 | 'Content-Type: text/plain\r\n' + 187 | 'Connection: keep-alive\r\n' + 188 | `Content-Length: ${Buffer.byteLength(body)}\r\n` + 189 | `Location: https://${connection.host}\r\n\r\n` 190 | ); 191 | clientSocket.write(body); 192 | clientSocket.end(); 193 | return; 194 | } 195 | 196 | this.connectionPassthrough( 197 | { 198 | host: this.opts.icpServer.host, 199 | port: this.opts.icpServer.port, 200 | }, 201 | connection, 202 | clientSocket, 203 | data 204 | ); 205 | } 206 | 207 | private connectionPassthrough( 208 | originServer: ServerInfo, 209 | connection: ConnectionInfo, 210 | clientSocket: net.Socket, 211 | data: Buffer 212 | ): void { 213 | const serverSocket = net.connect( 214 | { 215 | host: originServer.host, 216 | port: originServer.port, 217 | }, 218 | () => { 219 | if (connection.secure) { 220 | clientSocket.write( 221 | 'HTTP/1.1 200 Connection established\r\n' + 222 | 'Proxy-agent: Internet Computer Proxy\r\n' + 223 | '\r\n' 224 | ); 225 | } else { 226 | serverSocket.write(data); 227 | } 228 | 229 | // Piping the sockets 230 | serverSocket.pipe(clientSocket); 231 | clientSocket.pipe(serverSocket); 232 | } 233 | ); 234 | 235 | serverSocket.on('error', (err) => { 236 | clientSocket.emit('error', err); 237 | }); 238 | } 239 | 240 | private async shouldHandleAsICPRequest( 241 | connection: ConnectionInfo 242 | ): Promise { 243 | const canister = await lookupIcDomain(connection.host).catch((err) => { 244 | logger.error( 245 | `Failed to query dns record of ${connection.host} with ${err}` 246 | ); 247 | 248 | return null; 249 | }); 250 | 251 | return canister !== null ? true : false; 252 | } 253 | 254 | private async onClose(): Promise { 255 | logger.info('Client disconnected'); 256 | } 257 | 258 | private async onError(err: Error): Promise { 259 | logger.error(`NetProxy error: (${String(err)})`); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /packages/server/src/servers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Certificate, 3 | CertificateFactory, 4 | EventMessage, 5 | IPCClient, 6 | IPCServer, 7 | logger, 8 | wait, 9 | } from '@dfinity/http-proxy-core'; 10 | import { MissingCertificateError, MissingRequirementsError } from '~src/errors'; 11 | import { DaemonProcess } from '~src/servers/daemon'; 12 | import { 13 | IsRunningMessageResponse, 14 | IsStartedMessageResponse, 15 | MessageResponse, 16 | MessageType, 17 | ProxyServersOptions, 18 | StopMessageResponse, 19 | } from '~src/servers/typings'; 20 | import { ICPServer } from './icp'; 21 | import { NetProxy } from './net'; 22 | import { ProxyConfigurationServer } from '~src/servers/config'; 23 | 24 | export * from './typings'; 25 | 26 | export class ProxyServers { 27 | private icpServer!: ICPServer; 28 | private netServer!: NetProxy; 29 | private ipcServer!: IPCServer; 30 | private proxyConfigServer!: ProxyConfigurationServer; 31 | private isEnabled = false; 32 | private shuttingDown = false; 33 | private inflighMessages = new Map>(); 34 | private static rootCARenewJobIntervalMs = 1000 * 60 * 30; // 30min 35 | 36 | private certificates: { 37 | ca?: Certificate; 38 | proxy?: Certificate; 39 | } = {}; 40 | 41 | private constructor( 42 | private readonly configs: ProxyServersOptions, 43 | private readonly certificateFactory: CertificateFactory, 44 | private readonly daemon: DaemonProcess 45 | ) {} 46 | 47 | public async start(): Promise { 48 | await this.ipcServer.start(); 49 | await this.icpServer.start(); 50 | await this.netServer.start(); 51 | await this.proxyConfigServer.start(); 52 | 53 | if (this.configs?.autoEnable) { 54 | await this.enableSecureEnvironment(); 55 | } 56 | } 57 | 58 | private async registerRenewRootCAJob(): Promise { 59 | await wait(ProxyServers.rootCARenewJobIntervalMs); 60 | 61 | if (!this.certificates.ca?.shouldRenew) { 62 | this.registerRenewRootCAJob(); 63 | return; 64 | } 65 | 66 | this.certificates.ca = await this.certificateFactory.create( 67 | { type: 'ca' }, 68 | true 69 | ); 70 | 71 | if (this.isEnabled) { 72 | await this.enableSecureEnvironment(true).catch((e) => { 73 | logger.error(`failed to enable proxy with renewed root ca with(${e})`); 74 | 75 | this.isEnabled = false; 76 | }); 77 | } 78 | 79 | this.registerRenewRootCAJob(); 80 | } 81 | 82 | private async enableSecureEnvironment(force?: boolean): Promise { 83 | if (this.isEnabled && !force) { 84 | return; 85 | } 86 | 87 | // the daemon process is started with admin privileges to 88 | // update the required system configurations such as adding/removing 89 | // http(s) proxy servers and trusted certificate store 90 | await this.daemon.start(); 91 | 92 | if (!this.certificates.ca) { 93 | throw new MissingCertificateError('ca'); 94 | } 95 | 96 | const rootCAId = this.certificates.ca.id; 97 | const caCertPath = this.certificateFactory.store.certificatePath(rootCAId); 98 | const enabledProxyResult = await this.daemon.enableProxy({ 99 | certificate: { 100 | commonName: this.configs.certificate.rootca.commonName, 101 | path: caCertPath, 102 | }, 103 | proxy: { 104 | host: this.configs.netServer.host, 105 | port: this.configs.netServer.port, 106 | }, 107 | pac: { 108 | host: this.configs.proxyConfigServer.host, 109 | port: this.configs.proxyConfigServer.port, 110 | }, 111 | }); 112 | 113 | if (!enabledProxyResult.processed) { 114 | throw new MissingRequirementsError( 115 | `Failed to enable proxy(${enabledProxyResult.err})` 116 | ); 117 | } 118 | 119 | this.isEnabled = true; 120 | } 121 | 122 | public async shutdown(): Promise { 123 | this.shuttingDown = true; 124 | 125 | logger.info('Proxy is shutting down.'); 126 | 127 | await this.daemon.shutdown(); 128 | await this.proxyConfigServer.shutdown(); 129 | await this.netServer.shutdown(); 130 | await this.icpServer.shutdown(); 131 | await this.ipcServer.shutdown(); 132 | 133 | this.isEnabled = false; 134 | 135 | logger.info('Proxy has exited.'); 136 | 137 | process.exit(0); 138 | } 139 | 140 | private async setupRequirements(): Promise { 141 | this.certificates.ca = await this.certificateFactory.create({ type: 'ca' }); 142 | this.certificates.proxy = await this.certificateFactory.create({ 143 | type: 'domain', 144 | hostname: 'localhost', 145 | ca: this.certificates.ca, 146 | }); 147 | 148 | this.registerRenewRootCAJob(); 149 | 150 | return true; 151 | } 152 | 153 | public static async create( 154 | configs: ProxyServersOptions 155 | ): Promise { 156 | const certificateFactory = await CertificateFactory.build( 157 | configs.certificate 158 | ); 159 | const daemonIpcClient = new IPCClient({ path: configs.ipcChannels.daemon }); 160 | const servers = new ProxyServers( 161 | configs, 162 | certificateFactory, 163 | new DaemonProcess(daemonIpcClient) 164 | ); 165 | 166 | const requirementsReady = await servers.setupRequirements(); 167 | if (!requirementsReady) { 168 | throw new MissingRequirementsError(); 169 | } 170 | 171 | await servers.initServers(); 172 | 173 | return servers; 174 | } 175 | 176 | private async handleIsRunningMessage(): Promise { 177 | return { running: this.isEnabled }; 178 | } 179 | 180 | private async handleStopMessage(): Promise { 181 | await this.shutdown(); 182 | } 183 | 184 | private async handleIsStartedMessage(): Promise { 185 | return { isShuttingDown: this.shuttingDown }; 186 | } 187 | 188 | private async handleEnableMessage(): Promise { 189 | const inflight = this.inflighMessages.get(MessageType.Enable); 190 | if (inflight) { 191 | return inflight as Promise; 192 | } 193 | 194 | const processing = this.enableSecureEnvironment(); 195 | this.inflighMessages.set(MessageType.Enable, processing); 196 | 197 | await processing.finally(() => { 198 | this.inflighMessages.delete(MessageType.Enable); 199 | }); 200 | } 201 | 202 | private async initServers(): Promise { 203 | this.proxyConfigServer = await ProxyConfigurationServer.create({ 204 | host: this.configs.proxyConfigServer.host, 205 | port: this.configs.proxyConfigServer.port, 206 | proxyServer: { 207 | host: this.configs.netServer.host, 208 | port: this.configs.netServer.port, 209 | }, 210 | }); 211 | 212 | this.ipcServer = await IPCServer.create({ 213 | path: this.configs.ipcChannels.proxy, 214 | onMessage: async (event: EventMessage): Promise => { 215 | switch (event.type) { 216 | case MessageType.IsRunning: 217 | return await this.handleIsRunningMessage(); 218 | case MessageType.Stop: 219 | return await this.handleStopMessage(); 220 | case MessageType.IsStarted: 221 | return await this.handleIsStartedMessage(); 222 | case MessageType.Enable: 223 | return await this.handleEnableMessage(); 224 | } 225 | }, 226 | }); 227 | 228 | this.netServer = NetProxy.create({ 229 | host: this.configs.netServer.host, 230 | port: this.configs.netServer.port, 231 | icpServer: { 232 | host: this.configs.icpServer.host, 233 | port: this.configs.icpServer.port, 234 | }, 235 | }); 236 | 237 | if (!this.certificates.proxy) { 238 | throw new MissingCertificateError('proxy'); 239 | } 240 | 241 | this.icpServer = ICPServer.create({ 242 | host: this.configs.icpServer.host, 243 | port: this.configs.icpServer.port, 244 | certificate: { 245 | default: this.certificates.proxy, 246 | create: async (servername): Promise => { 247 | if (!this.certificates.ca) { 248 | throw new MissingCertificateError('ca'); 249 | } 250 | 251 | return await this.certificateFactory.create({ 252 | type: 'domain', 253 | hostname: servername, 254 | ca: this.certificates.ca, 255 | }); 256 | }, 257 | }, 258 | }); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 DFINITY Foundation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /packages/server/src/servers/icp/utils.ts: -------------------------------------------------------------------------------- 1 | import { Actor, ActorSubclass, HttpAgent, concat } from '@dfinity/agent'; 2 | import { logger } from '@dfinity/http-proxy-core'; 3 | import { Principal } from '@dfinity/principal'; 4 | import { 5 | getMaxVerificationVersion, 6 | getMinVerificationVersion, 7 | verifyRequestResponsePair, 8 | } from '@dfinity/response-verification/dist/nodejs/nodejs'; 9 | import { IncomingMessage } from 'http'; 10 | import fetch from 'isomorphic-fetch'; 11 | import { inflate, ungzip } from 'pako'; 12 | import { environment } from '~src/commons'; 13 | import { idlFactory } from '~src/commons/http-interface/canister_http_interface'; 14 | import { 15 | HttpRequest, 16 | _SERVICE, 17 | } from '~src/commons/http-interface/canister_http_interface_types'; 18 | import { streamContent } from '~src/commons/streaming'; 19 | import { NotAllowedRequestRedirectError } from '~src/errors/not-allowed-redirect-error'; 20 | import { DEFAULT_GATEWAY } from './static'; 21 | import { 22 | FetchAssetOptions, 23 | FetchAssetResult, 24 | HTTPHeaders, 25 | HTTPMethods, 26 | HttpResponse, 27 | } from './typings'; 28 | 29 | export const maxCertTimeOffsetNs = BigInt.asUintN(64, BigInt(300_000_000_000)); 30 | export const cacheHeaders = [HTTPHeaders.CacheControl.toString()]; 31 | 32 | export async function createAgentAndActor( 33 | gatewayUrl: URL, 34 | canisterId: Principal, 35 | fetchRootKey: boolean, 36 | userAgent: string 37 | ): Promise<[HttpAgent, ActorSubclass<_SERVICE>]> { 38 | // agent-js currently does not allow changing the user agent of the request with the fetchOptions, 39 | // with this change the initial user agent will be mantained with the addition of the ic http proxy info 40 | const customFetch: typeof fetch = ( 41 | input: RequestInfo | URL, 42 | opts?: RequestInit 43 | ): Promise => { 44 | if (opts) { 45 | opts.headers = { 46 | ...(opts.headers ?? {}), 47 | [HTTPHeaders.UserAgent]: userAgent, 48 | }; 49 | } 50 | 51 | return fetch(input, opts); 52 | }; 53 | const agent = new HttpAgent({ 54 | host: gatewayUrl.toString(), 55 | fetch: customFetch, 56 | }); 57 | if (fetchRootKey) { 58 | await agent.fetchRootKey(); 59 | } 60 | const actor = Actor.createActor<_SERVICE>(idlFactory, { 61 | agent, 62 | canisterId: canisterId, 63 | }); 64 | return [agent, actor]; 65 | } 66 | 67 | /** 68 | * Fetch a requested asset and handles upgrade calls when required. 69 | * 70 | * @param canister Canister holding the asset 71 | * @returns Fetched asset 72 | */ 73 | export const fetchAsset = async ({ 74 | actor, 75 | agent, 76 | canister, 77 | request, 78 | certificateVersion, 79 | }: FetchAssetOptions): Promise => { 80 | try { 81 | const url = new URL(request.url); 82 | 83 | const requestHeaders: [string, string][] = [['Host', url.hostname]]; 84 | request.headers.forEach((value, key) => { 85 | if (key.toLowerCase() === 'if-none-match') { 86 | // Drop the if-none-match header because we do not want a "304 not modified" response back. 87 | // See TT-30. 88 | return; 89 | } 90 | 91 | if ( 92 | key.toLowerCase() === 'accept-encoding' && 93 | !value.includes('identity') 94 | ) { 95 | value = `${value}, identity`; 96 | } 97 | 98 | requestHeaders.push([key, value]); 99 | }); 100 | 101 | // If the accept encoding isn't given, add it because we want to save bandwidth. 102 | if (!request.headers.has('Accept-Encoding')) { 103 | requestHeaders.push(['Accept-Encoding', 'gzip, deflate, identity']); 104 | } 105 | 106 | const httpRequest: HttpRequest = { 107 | method: request.method, 108 | url: url.pathname + url.search, 109 | headers: requestHeaders, 110 | body: new Uint8Array(await request.arrayBuffer()), 111 | certificate_version: [certificateVersion], 112 | }; 113 | 114 | let httpResponse = await actor.http_request(httpRequest); 115 | const upgradeCall = 116 | httpResponse.upgrade.length === 1 && httpResponse.upgrade[0]; 117 | const bodyEncoding = 118 | httpResponse.headers 119 | .filter(([key]) => key.toLowerCase() === HTTPHeaders.ContentEncoding) 120 | ?.map((header) => header[1].trim()) 121 | .pop() ?? ''; 122 | 123 | if (upgradeCall) { 124 | // repeat the request as an update call 125 | httpResponse = await actor.http_request_update(httpRequest); 126 | } 127 | 128 | // if we do streaming, body contains the first chunk 129 | let buffer = new ArrayBuffer(0); 130 | buffer = concat(buffer, httpResponse.body); 131 | if (httpResponse.streaming_strategy.length !== 0) { 132 | buffer = concat( 133 | buffer, 134 | await streamContent(agent, canister, httpResponse) 135 | ); 136 | } 137 | const responseBody = new Uint8Array(buffer); 138 | 139 | return { 140 | ok: true, 141 | data: { 142 | updateCall: upgradeCall, 143 | request: { 144 | body: httpRequest.body, 145 | method: httpRequest.method, 146 | url: httpRequest.url, 147 | headers: httpRequest.headers.map(([key, value]) => [key, value]), 148 | }, 149 | response: { 150 | encoding: bodyEncoding, 151 | body: responseBody, 152 | statusCode: httpResponse.status_code, 153 | headers: httpResponse.headers.map(([key, value]) => [key, value]), 154 | }, 155 | }, 156 | }; 157 | } catch (e) { 158 | return { 159 | ok: false, 160 | error: e, 161 | }; 162 | } 163 | }; 164 | 165 | /** 166 | * Decode a body (ie. deflate or gunzip it) based on its content-encoding. 167 | * @param body The body to decode. 168 | * @param encoding Its content-encoding associated header. 169 | */ 170 | export function decodeBody(body: Uint8Array, encoding: string): Uint8Array { 171 | switch (encoding) { 172 | case 'identity': 173 | case '': 174 | return body; 175 | case 'gzip': 176 | return ungzip(body); 177 | case 'deflate': 178 | return inflate(body); 179 | default: 180 | throw new Error(`Unsupported encoding: "${encoding}"`); 181 | } 182 | } 183 | 184 | const fromResponseVerificationHeaders = ( 185 | headers: [string, string][] 186 | ): Headers => { 187 | const finalHeaders = new Headers(); 188 | headers.forEach(([key, value]) => { 189 | finalHeaders.append(key, value); 190 | }); 191 | 192 | return finalHeaders; 193 | }; 194 | 195 | const SW_UNINSTALL_SCRIPT = ` 196 | // Uninstalling the IC service worker in favor of the proxy. 197 | self.addEventListener('install', () => self.skipWaiting()); 198 | self.addEventListener('activate', () => { 199 | // uninstall itself & reload page 200 | self.registration 201 | .unregister() 202 | .then(function () { 203 | return self.clients.matchAll(); 204 | }) 205 | .then(function (clients) { 206 | clients.forEach((client) => { 207 | client.navigate(client.url); 208 | }); 209 | }); 210 | }); 211 | `; 212 | 213 | export const stringToIntArray = (body: string): Uint8Array => { 214 | const encoder = new TextEncoder(); 215 | 216 | return encoder.encode(body); 217 | }; 218 | 219 | export const processIcRequest = async ( 220 | canister: Principal, 221 | request: Request, 222 | shouldFetchRootKey = false 223 | ): Promise => { 224 | const httpResponse = await fetchFromInternetComputer( 225 | canister, 226 | request, 227 | shouldFetchRootKey 228 | ); 229 | 230 | const maybeUninstallServiceWorker = maybeUninstallHTTPGatewayServiceWorker( 231 | request, 232 | httpResponse 233 | ); 234 | 235 | if (maybeUninstallServiceWorker) { 236 | return maybeUninstallServiceWorker; 237 | } 238 | 239 | return httpResponse; 240 | }; 241 | 242 | export const maybeUninstallHTTPGatewayServiceWorker = ( 243 | request: Request, 244 | icResponse: HttpResponse 245 | ): HttpResponse | null => { 246 | const serviceWorkerUpdateRequest = !!request.headers.get( 247 | HTTPHeaders.ServiceWorker 248 | ); 249 | 250 | if (!serviceWorkerUpdateRequest) { 251 | return null; 252 | } 253 | 254 | const contentType = 255 | icResponse.headers.get(HTTPHeaders.ContentType)?.trim() ?? ''; 256 | const hasOnChainSW = 257 | icResponse.status >= 200 && 258 | icResponse.status <= 299 && 259 | [ 260 | 'text/javascript', 261 | 'application/javascript', 262 | 'application/x-javascript', 263 | ].includes(contentType) && 264 | !new TextDecoder() 265 | .decode(icResponse.body) 266 | .includes('registration.unregister()'); 267 | 268 | // if the ic response contains a service worker we ignore the uninstall script 269 | if (hasOnChainSW) { 270 | return null; 271 | } 272 | 273 | return { 274 | status: 200, 275 | headers: new Headers({ [HTTPHeaders.ContentType]: 'text/javascript' }), 276 | body: stringToIntArray(SW_UNINSTALL_SCRIPT), 277 | }; 278 | }; 279 | 280 | export const fetchFromInternetComputer = async ( 281 | canister: Principal, 282 | request: Request, 283 | shouldFetchRootKey = false 284 | ): Promise => { 285 | try { 286 | const minAllowedVerificationVersion = getMinVerificationVersion(); 287 | const desiredVerificationVersion = getMaxVerificationVersion(); 288 | 289 | const agentUserAgent = 290 | request.headers.get(HTTPHeaders.UserAgent) ?? environment.userAgent; 291 | const [agent, actor] = await createAgentAndActor( 292 | DEFAULT_GATEWAY, 293 | canister, 294 | shouldFetchRootKey, 295 | agentUserAgent 296 | ); 297 | const result = await fetchAsset({ 298 | agent, 299 | actor, 300 | request: request, 301 | canister, 302 | certificateVersion: desiredVerificationVersion, 303 | }); 304 | 305 | if (!result.ok) { 306 | let errMessage = 'Failed to fetch response'; 307 | if (result.error instanceof Error) { 308 | logger.error(`Failed to fetch asset response (${result.error})`); 309 | errMessage = result.error.message; 310 | } 311 | 312 | return { 313 | status: 500, 314 | headers: new Headers(), 315 | body: stringToIntArray(errMessage), 316 | }; 317 | } 318 | 319 | const assetFetchResult = result.data; 320 | const responseHeaders = new Headers(); 321 | for (const [key, value] of assetFetchResult.response.headers) { 322 | const headerKey = key.trim().toLowerCase(); 323 | if (cacheHeaders.includes(headerKey)) { 324 | // cache headers are remove since those are handled by 325 | // cache storage within the service worker. If returned they would 326 | // reach https://www.chromium.org/blink/ in the cache of chromium which 327 | // could cache those entries in memory and those requests can't be 328 | // intercepted by the service worker 329 | continue; 330 | } 331 | 332 | responseHeaders.append(key, value); 333 | } 334 | 335 | // update calls are certified since they've went through consensus 336 | if (assetFetchResult.updateCall) { 337 | responseHeaders.delete(HTTPHeaders.ContentEncoding); 338 | const decodedResponseBody = decodeBody( 339 | assetFetchResult.response.body, 340 | assetFetchResult.response.encoding 341 | ); 342 | 343 | return { 344 | status: assetFetchResult.response.statusCode, 345 | headers: responseHeaders, 346 | body: decodedResponseBody, 347 | }; 348 | } 349 | 350 | const currentTimeNs = BigInt.asUintN(64, BigInt(Date.now() * 1_000_000)); // from ms to nanoseconds 351 | const assetCertification = verifyRequestResponsePair( 352 | { 353 | headers: assetFetchResult.request.headers, 354 | method: assetFetchResult.request.method, 355 | url: assetFetchResult.request.url, 356 | body: assetFetchResult.request.body, 357 | }, 358 | { 359 | statusCode: assetFetchResult.response.statusCode, 360 | body: assetFetchResult.response.body, 361 | headers: assetFetchResult.response.headers, 362 | }, 363 | canister.toUint8Array(), 364 | currentTimeNs, 365 | maxCertTimeOffsetNs, 366 | new Uint8Array(agent.rootKey), 367 | minAllowedVerificationVersion 368 | ); 369 | 370 | if (assetCertification.response) { 371 | const certifiedResponseHeaders = fromResponseVerificationHeaders( 372 | assetCertification.response.headers 373 | ); 374 | 375 | certifiedResponseHeaders.forEach((headerValue, headerKey) => { 376 | responseHeaders.set(headerKey, headerValue); 377 | }); 378 | 379 | responseHeaders.delete(HTTPHeaders.ContentEncoding); 380 | const decodedResponseBody = decodeBody( 381 | assetFetchResult.response.body, 382 | assetFetchResult.response.encoding 383 | ); 384 | 385 | // Redirects are blocked for query calls that are not validating headers 386 | if ( 387 | assetCertification.verificationVersion <= 1 && 388 | assetFetchResult.response.statusCode >= 300 && 389 | assetFetchResult.response.statusCode < 400 390 | ) { 391 | throw new NotAllowedRequestRedirectError(); 392 | } 393 | 394 | return { 395 | status: assetCertification.response.statusCode ?? 200, 396 | headers: responseHeaders, 397 | body: decodedResponseBody, 398 | }; 399 | } 400 | } catch (err) { 401 | logger.error(`ICRequest failed processing verification (${String(err)})`); 402 | } 403 | 404 | return { 405 | status: 500, 406 | headers: new Headers(), 407 | body: stringToIntArray('Body does not pass verification'), 408 | }; 409 | }; 410 | 411 | export const parseIncomingMessageHeaders = ( 412 | rawHeaders: IncomingMessage['headers'] 413 | ): Headers => { 414 | const requestHeaders = new Headers(); 415 | Object.entries(rawHeaders).forEach(([headerName, rawValue]) => { 416 | if (Array.isArray(rawValue)) { 417 | rawValue.forEach((headerValue) => { 418 | requestHeaders.append(headerName, headerValue); 419 | }); 420 | return; 421 | } 422 | 423 | requestHeaders.append(headerName, rawValue ?? ''); 424 | }); 425 | 426 | return requestHeaders; 427 | }; 428 | 429 | export const convertIncomingMessage = ( 430 | incoming: IncomingMessage, 431 | processHeaders?: (headers: Headers) => Headers 432 | ): Promise => { 433 | const rawHeaders = parseIncomingMessageHeaders(incoming.headers); 434 | const urlPath = incoming.url ?? ''; 435 | const hostname = rawHeaders.get('host') ?? ''; 436 | const url = new URL(`https://${hostname}${urlPath}`); 437 | const method = (incoming.method ?? HTTPMethods.GET.toString()).toLowerCase(); 438 | const canHaveBody = ![ 439 | HTTPMethods.GET.toString().toLowerCase(), 440 | HTTPMethods.HEAD.toString().toLowerCase(), 441 | ].includes(method); 442 | const headers = processHeaders ? processHeaders(rawHeaders) : rawHeaders; 443 | 444 | return new Promise((ok) => { 445 | let requestBody = ''; 446 | incoming.on('data', (chunk) => { 447 | requestBody += chunk; 448 | }); 449 | incoming.on('end', () => { 450 | const request = new Request(url.href, { 451 | method, 452 | headers, 453 | body: canHaveBody ? requestBody : undefined, 454 | }); 455 | 456 | ok(request); 457 | }); 458 | }); 459 | }; 460 | --------------------------------------------------------------------------------