diff --git a/packages/teleterm/src/mainProcess/resolveNetworkAddress.ts b/packages/teleterm/src/mainProcess/resolveNetworkAddress.ts index 1af909eab..589707664 100644 --- a/packages/teleterm/src/mainProcess/resolveNetworkAddress.ts +++ b/packages/teleterm/src/mainProcess/resolveNetworkAddress.ts @@ -5,10 +5,15 @@ const TCP_PORT_MATCH = /\{CONNECT_GRPC_PORT:\s(\d+)}/; // transport method. const UDS_MATCH = /\{CONNECT_GRPC_PORT:/; +/** + * Waits for the process to start a gRPC server and log the address used by the gRPC server. + * + * @return {Promise} The address used by the gRPC server started from the process. + */ export async function resolveNetworkAddress( requestedAddress: string, process: ChildProcess, - timeoutMs = 10_000 // 10s + timeoutMs = 15_000 // 15s; needs to be larger than other timeouts in the processes. ): Promise { const protocol = new URL(requestedAddress).protocol; diff --git a/packages/teleterm/src/preload.ts b/packages/teleterm/src/preload.ts index 730f562dc..836c488b8 100644 --- a/packages/teleterm/src/preload.ts +++ b/packages/teleterm/src/preload.ts @@ -1,14 +1,20 @@ import { contextBridge } from 'electron'; +import { ChannelCredentials } from '@grpc/grpc-js'; import createTshClient from 'teleterm/services/tshd/createClient'; import createMainProcessClient from 'teleterm/mainProcess/mainProcessClient'; import createLoggerService from 'teleterm/services/logger'; -import PreloadLogger from 'teleterm/logger'; - +import Logger from 'teleterm/logger'; import { createPtyService } from 'teleterm/services/pty/ptyService'; -import { getClientCredentials } from 'teleterm/services/grpcCredentials'; - -import { ElectronGlobals } from './types'; +import { + GrpcCertName, + createClientCredentials, + createInsecureClientCredentials, + generateAndSaveGrpcCert, + readGrpcCert, + shouldEncryptConnection, +} from 'teleterm/services/grpcCredentials'; +import { ElectronGlobals, RuntimeSettings } from 'teleterm/types'; const mainProcessClient = createMainProcessClient(); const runtimeSettings = mainProcessClient.getRuntimeSettings(); @@ -18,16 +24,18 @@ const loggerService = createLoggerService({ name: 'renderer', }); -PreloadLogger.init(loggerService); +Logger.init(loggerService); + +contextBridge.exposeInMainWorld('loggerService', loggerService); contextBridge.exposeInMainWorld('electron', getElectronGlobals()); async function getElectronGlobals(): Promise { const [addresses, credentials] = await Promise.all([ mainProcessClient.getResolvedChildProcessAddresses(), - getClientCredentials(runtimeSettings), + createGrpcCredentials(runtimeSettings), ]); - const tshClient = createTshClient(addresses.tsh, credentials.tsh); + const tshClient = createTshClient(addresses.tsh, credentials.tshd); const ptyServiceClient = createPtyService( addresses.shared, credentials.shared, @@ -38,6 +46,38 @@ async function getElectronGlobals(): Promise { mainProcessClient, tshClient, ptyServiceClient, - loggerService, + }; +} + +/** + * For TCP transport, createGrpcCredentials generates the renderer key pair and reads the public key + * for tshd and the shared process from disk. This lets us set up gRPC clients in the renderer + * process that connect to the gRPC servers of tshd and the shared process. + */ +async function createGrpcCredentials( + runtimeSettings: RuntimeSettings +): Promise<{ + // Credentials for talking to the tshd process. + tshd: ChannelCredentials; + // Credentials for talking to the shared process. + shared: ChannelCredentials; +}> { + if (!shouldEncryptConnection(runtimeSettings)) { + return { + tshd: createInsecureClientCredentials(), + shared: createInsecureClientCredentials(), + }; + } + + const { certsDir } = runtimeSettings; + const [rendererKeyPair, tshdCert, sharedCert] = await Promise.all([ + generateAndSaveGrpcCert(certsDir, GrpcCertName.Renderer), + readGrpcCert(certsDir, GrpcCertName.Tshd), + readGrpcCert(certsDir, GrpcCertName.Shared), + ]); + + return { + tshd: createClientCredentials(rendererKeyPair, tshdCert), + shared: createClientCredentials(rendererKeyPair, sharedCert), }; } diff --git a/packages/teleterm/src/services/grpcCredentials/clientCredentials.ts b/packages/teleterm/src/services/grpcCredentials/clientCredentials.ts deleted file mode 100644 index 8d0d764dd..000000000 --- a/packages/teleterm/src/services/grpcCredentials/clientCredentials.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ChannelCredentials, credentials } from '@grpc/grpc-js'; - -import { RuntimeSettings } from 'teleterm/mainProcess/types'; - -import { - readGrpcCert, - generateAndSaveGrpcCert, - shouldEncryptConnection, -} from './helpers'; -import { GrpcCertName } from './types'; - -export async function getClientCredentials( - runtimeSettings: RuntimeSettings -): Promise<{ - tsh: ChannelCredentials; - shared: ChannelCredentials; -}> { - if (shouldEncryptConnection(runtimeSettings)) { - const certs = await getCerts(runtimeSettings.certsDir); - return { - tsh: createSecureCredentials(certs.clientKeyPair, certs.tshServerCert), - shared: createSecureCredentials( - certs.clientKeyPair, - certs.sharedServerCert - ), - }; - } - - return { - tsh: createInsecureCredentials(), - shared: createInsecureCredentials(), - }; -} - -async function getCerts(certsDir: string): Promise<{ - clientKeyPair: { cert: Buffer; key: Buffer }; - tshServerCert: Buffer; - sharedServerCert: Buffer; -}> { - const [clientKeyPair, tshServerCert, sharedServerCert] = await Promise.all([ - generateAndSaveGrpcCert(certsDir, GrpcCertName.Client), - readGrpcCert(certsDir, GrpcCertName.TshServer), - readGrpcCert(certsDir, GrpcCertName.SharedServer), - ]); - return { clientKeyPair, tshServerCert, sharedServerCert }; -} - -function createSecureCredentials( - clientKeyPair: { cert: Buffer; key: Buffer }, - serverCert: Buffer -): ChannelCredentials { - return credentials.createSsl( - serverCert, - clientKeyPair.key, - clientKeyPair.cert - ); -} - -function createInsecureCredentials(): ChannelCredentials { - return credentials.createInsecure(); -} diff --git a/packages/teleterm/src/services/grpcCredentials/credentials.ts b/packages/teleterm/src/services/grpcCredentials/credentials.ts new file mode 100644 index 000000000..f7e905c4d --- /dev/null +++ b/packages/teleterm/src/services/grpcCredentials/credentials.ts @@ -0,0 +1,56 @@ +import { + ChannelCredentials, + credentials, + ServerCredentials, +} from '@grpc/grpc-js'; + +import { RuntimeSettings } from 'teleterm/mainProcess/types'; + +export function createClientCredentials( + clientKeyPair: { cert: Buffer; key: Buffer }, + serverCert: Buffer +): ChannelCredentials { + return credentials.createSsl( + serverCert, + clientKeyPair.key, + clientKeyPair.cert + ); +} + +export function createServerCredentials( + serverKeyPair: { cert: Buffer; key: Buffer }, + clientCert: Buffer +): ServerCredentials { + return ServerCredentials.createSsl( + clientCert, + [ + { + cert_chain: serverKeyPair.cert, + private_key: serverKeyPair.key, + }, + ], + true + ); +} + +export function createInsecureClientCredentials(): ChannelCredentials { + return credentials.createInsecure(); +} + +export function createInsecureServerCredentials(): ServerCredentials { + return ServerCredentials.createInsecure(); +} + +/** + * Checks if the gRPC connection should be encrypted. + * The only source of truth is the type of tshd protocol. + * Any protocol other than `unix` should be encrypted. + * The same check is performed on the tshd side. + */ +export function shouldEncryptConnection( + runtimeSettings: RuntimeSettings +): boolean { + return ( + new URL(runtimeSettings.tshd.requestedNetworkAddress).protocol !== 'unix:' + ); +} diff --git a/packages/teleterm/src/services/grpcCredentials/helpers.ts b/packages/teleterm/src/services/grpcCredentials/files.ts similarity index 83% rename from packages/teleterm/src/services/grpcCredentials/helpers.ts rename to packages/teleterm/src/services/grpcCredentials/files.ts index 72a84e136..73937be2e 100644 --- a/packages/teleterm/src/services/grpcCredentials/helpers.ts +++ b/packages/teleterm/src/services/grpcCredentials/files.ts @@ -1,10 +1,7 @@ import path from 'path'; - import { watch } from 'fs'; import { readFile, writeFile, stat, rename } from 'fs/promises'; -import { RuntimeSettings } from 'teleterm/mainProcess/types'; - import { makeCert } from './makeCert'; /** @@ -83,17 +80,3 @@ export async function readGrpcCert( abortController.abort(); } } - -/** - * Checks if the gRPC connection should be encrypted. - * The only source of truth is the type of tshd protocol. - * Any other protocol than `unix` should be encrypted. - * The same check is performed on the tshd side. - */ -export function shouldEncryptConnection( - runtimeSettings: RuntimeSettings -): boolean { - return ( - new URL(runtimeSettings.tshd.requestedNetworkAddress).protocol !== 'unix:' - ); -} diff --git a/packages/teleterm/src/services/grpcCredentials/index.ts b/packages/teleterm/src/services/grpcCredentials/index.ts index 68e779061..a7efb882d 100644 --- a/packages/teleterm/src/services/grpcCredentials/index.ts +++ b/packages/teleterm/src/services/grpcCredentials/index.ts @@ -1,2 +1,3 @@ -export * from './clientCredentials'; -export * from './serverCredentials'; +export * from './types'; +export * from './credentials'; +export * from './files'; diff --git a/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.test.ts b/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.test.ts index c65321835..033efe0ac 100644 --- a/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.test.ts +++ b/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.test.ts @@ -43,7 +43,7 @@ import { makeCert } from './makeCert'; test('create CA cert', async () => { const ca = await makeCert({ - commonName: 'Test CA', + commonName: 'test-ca', validityDays: 365, }); diff --git a/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.ts b/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.ts index 2e852104a..e82d7595b 100644 --- a/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.ts +++ b/packages/teleterm/src/services/grpcCredentials/makeCert/makeCert.ts @@ -49,6 +49,9 @@ interface GeneratedCert { cert: string; } +/** + * Creates a self-signed cert. commonName should be a valid domain name. + */ export async function makeCert({ commonName, validityDays, @@ -69,6 +72,15 @@ export async function makeCert({ digitalSignature: true, keyEncipherment: true, }, + { + name: 'subjectAltName', + altNames: [ + { + type: 2, // DNS type + value: commonName, + }, + ], + }, ]; return await generateRawCert({ diff --git a/packages/teleterm/src/services/grpcCredentials/serverCredentials.ts b/packages/teleterm/src/services/grpcCredentials/serverCredentials.ts deleted file mode 100644 index 1ce51ae71..000000000 --- a/packages/teleterm/src/services/grpcCredentials/serverCredentials.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ServerCredentials } from '@grpc/grpc-js'; - -import { RuntimeSettings } from 'teleterm/mainProcess/types'; - -import { - readGrpcCert, - generateAndSaveGrpcCert, - shouldEncryptConnection, -} from './helpers'; -import { GrpcCertName } from './types'; - -export async function getServerCredentials( - runtimeSettings: RuntimeSettings -): Promise<{ shared: ServerCredentials }> { - if (shouldEncryptConnection(runtimeSettings)) { - return { shared: await createSecureCredentials(runtimeSettings.certsDir) }; - } - return { shared: createInsecureCredentials() }; -} - -async function createSecureCredentials( - certsDir: string -): Promise { - const [serverKeyPair, clientCert] = await Promise.all([ - generateAndSaveGrpcCert(certsDir, GrpcCertName.SharedServer), - readGrpcCert(certsDir, GrpcCertName.Client), - ]); - - return ServerCredentials.createSsl( - clientCert, - [ - { - cert_chain: serverKeyPair.cert, - private_key: serverKeyPair.key, - }, - ], - true - ); -} - -function createInsecureCredentials(): ServerCredentials { - return ServerCredentials.createInsecure(); -} diff --git a/packages/teleterm/src/services/grpcCredentials/types.ts b/packages/teleterm/src/services/grpcCredentials/types.ts index 59c67aaee..fac97dbbd 100644 --- a/packages/teleterm/src/services/grpcCredentials/types.ts +++ b/packages/teleterm/src/services/grpcCredentials/types.ts @@ -1,6 +1,9 @@ -// `Client` and `TshServer` file names are also used on the tshd side +// Each process creates its own key pair. The public key is saved to disk under the specified +// filename, the private key stays in the memory. +// +// `Renderer` and `Tshd` file names are also used on the tshd side. export enum GrpcCertName { - Client = 'client.crt', - TshServer = 'tsh_server.crt', - SharedServer = 'shared_server.crt', + Renderer = 'renderer.crt', + Tshd = 'tshd.crt', + Shared = 'shared.crt', } diff --git a/packages/teleterm/src/sharedProcess/sharedProcess.ts b/packages/teleterm/src/sharedProcess/sharedProcess.ts index fc0bf9028..abc5b40a3 100644 --- a/packages/teleterm/src/sharedProcess/sharedProcess.ts +++ b/packages/teleterm/src/sharedProcess/sharedProcess.ts @@ -1,13 +1,24 @@ -import { Server } from '@grpc/grpc-js'; +import { Server, ServerCredentials } from '@grpc/grpc-js'; import createLoggerService from 'teleterm/services/logger'; -import { getServerCredentials } from 'teleterm/services/grpcCredentials'; +import { + createInsecureServerCredentials, + createServerCredentials, + generateAndSaveGrpcCert, + GrpcCertName, + readGrpcCert, + shouldEncryptConnection, +} from 'teleterm/services/grpcCredentials'; import { RuntimeSettings } from 'teleterm/mainProcess/types'; import Logger from 'teleterm/logger'; import { PtyHostService } from './api/protogen/ptyHostService_grpc_pb'; import { createPtyHostService } from './ptyHost/ptyHostService'; +const runtimeSettings = getRuntimeSettings(); +initializeLogger(runtimeSettings); +initializeServer(runtimeSettings); + function getRuntimeSettings(): RuntimeSettings { const args = process.argv.slice(2); const argName = '--runtimeSettingsJson='; @@ -52,19 +63,17 @@ async function initializeServer( const grpcServerAddress = address.replace('tcp://', ''); try { - server.bindAsync( - grpcServerAddress, - (await getServerCredentials(runtimeSettings)).shared, - (error, port) => { - sendBoundNetworkPortToStdout(port); + const credentials = await createGrpcCredentials(runtimeSettings); - if (error) { - return logger.error(error.message); - } + server.bindAsync(grpcServerAddress, credentials, (error, port) => { + sendBoundNetworkPortToStdout(port); - server.start(); + if (error) { + return logger.error(error.message); } - ); + + server.start(); + }); } catch (e) { logger.error('Could not start shared server', e); } @@ -78,6 +87,21 @@ function sendBoundNetworkPortToStdout(port: number) { console.log(`{CONNECT_GRPC_PORT: ${port}}`); } -const runtimeSettings = getRuntimeSettings(); -initializeLogger(runtimeSettings); -initializeServer(runtimeSettings); +/** + * Creates credentials for the gRPC server running in the shared process. + */ +async function createGrpcCredentials( + runtimeSettings: RuntimeSettings +): Promise { + if (!shouldEncryptConnection(runtimeSettings)) { + return createInsecureServerCredentials(); + } + + const { certsDir } = runtimeSettings; + const [sharedKeyPair, rendererCert] = await Promise.all([ + generateAndSaveGrpcCert(certsDir, GrpcCertName.Shared), + readGrpcCert(certsDir, GrpcCertName.Renderer), + ]); + + return createServerCredentials(sharedKeyPair, rendererCert); +} diff --git a/packages/teleterm/src/types.ts b/packages/teleterm/src/types.ts index 431f1b24d..925d34e37 100644 --- a/packages/teleterm/src/types.ts +++ b/packages/teleterm/src/types.ts @@ -20,5 +20,4 @@ export type ElectronGlobals = { readonly mainProcessClient: MainProcessClient; readonly tshClient: TshClient; readonly ptyServiceClient: PtyServiceClient; - readonly loggerService: LoggerService; }; diff --git a/packages/teleterm/src/ui/boot.tsx b/packages/teleterm/src/ui/boot.tsx index b9485a398..ab3aee5cf 100644 --- a/packages/teleterm/src/ui/boot.tsx +++ b/packages/teleterm/src/ui/boot.tsx @@ -7,11 +7,12 @@ import AppContext from 'teleterm/ui/appContext'; import Logger from 'teleterm/logger'; async function boot(): Promise { + Logger.init(window['loggerService']); + const logger = new Logger('UI'); + try { const globals = await getElectronGlobals(); - Logger.init(globals.loggerService); - const logger = new Logger('UI'); const appContext = new AppContext(globals); window.addEventListener('error', event => { @@ -25,6 +26,7 @@ async function boot(): Promise { renderApp(); } catch (e) { + logger.error('Failed to boot the React app', e); renderApp(); } } diff --git a/packages/teleterm/src/ui/fixtures/mocks.ts b/packages/teleterm/src/ui/fixtures/mocks.ts index 5abd94982..6ca4c659f 100644 --- a/packages/teleterm/src/ui/fixtures/mocks.ts +++ b/packages/teleterm/src/ui/fixtures/mocks.ts @@ -8,26 +8,11 @@ export class MockAppContext extends AppContext { const mainProcessClient = new MockMainProcessClient(); const tshdClient = new MockTshClient(); const ptyServiceClient = new MockPtyServiceClient(); - const loggerService = createLoggerService(); super({ - loggerService, mainProcessClient, tshClient: tshdClient, ptyServiceClient, }); } } - -function createLoggerService() { - return { - pipeProcessOutputIntoLogger() {}, - createLogger() { - return { - error: () => {}, - warn: () => {}, - info: () => {}, - }; - }, - }; -}