Skip to content

Commit

Permalink
Allowed the back-end to select a port as needed, marked the kind of d…
Browse files Browse the repository at this point in the history
…rivers, and added some security to the tRPC ch

annel.
  • Loading branch information
6XGate committed Nov 5, 2024
1 parent 1251105 commit 4e5eb52
Show file tree
Hide file tree
Showing 29 changed files with 182 additions and 160 deletions.
2 changes: 0 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
- Milestones
- v2.1
- (#86) Add a means to select a not-in-use port for the tRPC channel.
- v2.2
- Move more modules to core.
- tRPC over Electron IPC.
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/extron/sis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useExtronSisProtocol } from '../../services/protocols/extronSis'
const extronSisDriver = defineDriver({
enabled: true,
experimental: false,
kind: 'switch',
guid: '4C8F2838-C91D-431E-84DD-3666D14A6E2C',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/shinybow/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useShinybowV2Protocol } from '../../services/protocols/shinybow'
const shinybowV2 = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '75FB7ED2-EE3A-46D5-B11F-7D8C3C208E7C',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/shinybow/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useShinybowV3Protocol } from '../../services/protocols/shinybow'
const shinybowV3 = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: 'BBED08A1-C749-4733-8F2E-96C9B56C0C41',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/sony/rs485.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSonyBvmProtocol } from '../../services/protocols/sonyBvm'
const sonyRs485Driver = defineDriver({
enabled: true,
experimental: false,
kind: 'monitor',
guid: '8626D6D3-C211-4D21-B5CC-F5E3B50D9FF0',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesla-smart/kvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecKvmProtocol } from '../../services/protocols/teslaElec'
const teslaSmartKvmDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '91D5BC95-A8E2-4F58-BCAC-A77BA1054D61',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesla-smart/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecMatrixProtocol } from '../../services/protocols/teslaElec'
const teslaSmartMatrixDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '671824ED-0BC4-43A6-85CC-4877890A7722',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesla-smart/sdi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecSdiProtocol } from '../../services/protocols/teslaElec'
const teslaSmartSdiDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: 'DDB13CBC-ABFC-405E-9EA6-4A999F9A16BD',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesmart/kvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecKvmProtocol } from '../../services/protocols/teslaElec'
const tesmartKvmDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '2B4EDB8E-D2D6-4809-BA18-D5B1785DA028',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesmart/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecMatrixProtocol } from '../../services/protocols/teslaElec'
const tesmartMatrixDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '01B8884C-1D7D-4451-883D-3C8F18E17B14',
localized: {
en: {
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/tesmart/sdi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTeslaElecSdiProtocol } from '../../services/protocols/teslaElec'
const tesmartSdiDriver = defineDriver({
enabled: true,
experimental: true,
kind: 'switch',
guid: '8C524E65-83EF-4AEF-B0DA-29C4582AA4A0',
localized: {
en: {
Expand Down
15 changes: 0 additions & 15 deletions src/main/info/config.ts

This file was deleted.

49 changes: 24 additions & 25 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { app, shell, BrowserWindow, nativeTheme } from 'electron'
import Logger from 'electron-log'
import { sleep } from 'radash'
import appIcon from '../../resources/icon.png?asset&asarUnpack'
import useAppConfig from './info/config'
import useApiServer from './server'
import { getAuthToken } from './services/trpc'
import { logError } from './utilities'
import { toError } from '@/error-handling'

Expand All @@ -18,30 +18,27 @@ Logger.transports.console.format = '{h}:{i}:{s}.{ms} [{level}] › {text}'
Logger.transports.file.level = 'debug'
Logger.errorHandler.startCatching()

async function createWindow() {
async function createWindow(port: number) {
const willStartWithDark = nativeTheme.shouldUseDarkColors || nativeTheme.shouldUseInvertedColorScheme

const main = new BrowserWindow({
const window = new BrowserWindow({
width: 800,
height: 480,
backgroundColor: willStartWithDark ? '#121212' : 'white',
icon: appIcon,
show: true,
useContentSize: true,
webPreferences: {
preload: joinPath(__dirname, '../preload/index.mjs'),
sandbox: false
}
useContentSize: true
})

main.removeMenu()
window.removeMenu()
if (import.meta.env.PROD) {
main.setFullScreen(true)
window.setFullScreen(true)
} else {
main.webContents.openDevTools({ mode: 'undocked' })
window.webContents.openDevTools({ mode: 'undocked' })
}

main.webContents.setWindowOpenHandler(function windowOpenHandler(details) {
// Open all new window links in the system browser.
window.webContents.setWindowOpenHandler(function windowOpenHandler(details) {
shell.openExternal(details.url).catch((e: unknown) => {
Logger.error(e)
})
Expand All @@ -52,28 +49,32 @@ async function createWindow() {
const kWait = 2000
let lastError: unknown

/* eslint-disable no-await-in-loop -- Retry loop must be serial. */
for (let tries = 3; tries > 0; --tries) {
try {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env.ELECTRON_RENDERER_URL != null) {
// eslint-disable-next-line no-await-in-loop -- Retry loop must be serial.
await main.loadURL(process.env.ELECTRON_RENDERER_URL)
const url = new URL(process.env.ELECTRON_RENDERER_URL)
url.searchParams.set('port', String(port))
url.searchParams.set('auth', getAuthToken())
await window.loadURL(url.toString())
} else {
// eslint-disable-next-line no-await-in-loop -- Retry loop must be serial.
await main.loadFile(joinPath(__dirname, '../renderer/index.html'))
await window.loadFile(joinPath(__dirname, '../renderer/index.html'), {
query: { port: String(port), auth: getAuthToken() }
})
}

return main
return window
} catch (e) {
lastError = e
Logger.warn(e)

// eslint-disable-next-line no-await-in-loop -- Retry loop must be serial.
await sleep(kWait)
}
}

/* eslint-enable no-await-in-loop */
throw logError(toError(lastError))
}

Expand Down Expand Up @@ -101,15 +102,13 @@ process.on('SIGTERM', () => {
})

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
// initialization and is ready to create browser
// windows. Some APIs can only be used after
// this event occurs.
await app.whenReady()

// Set app user model id for windows
electronApp.setAppUserModelId('org.sleepingcats.BridgeCmdr')

useAppConfig()

useApiServer()

await createWindow()
const port = useApiServer()
await createWindow(port)
68 changes: 38 additions & 30 deletions src/main/server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createHTTPServer } from '@trpc/server/adapters/standalone'
import { applyWSSHandler } from '@trpc/server/adapters/ws'
import Logger from 'electron-log'
import { range } from 'radash'
import { WebSocketServer } from 'ws'
import useAppConfig from './info/config'
import { useAppRouter } from './routes/router'
import { getServerUrl } from '@/url'
import { createStandaloneContext } from './services/trpc'

function startWebSocketServer(url: URL, host: string, port: number) {
function startWebSocketServer(url: string, host: string, port: number) {
// TODO: Authentication via the IPC, later we'll implement a proper authentication model.

process.env['WS_NO_UTF_8_VALIDATE'] = '1'
Expand All @@ -17,7 +16,7 @@ function startWebSocketServer(url: URL, host: string, port: number) {
Logger.info(`RPC server at ${url}`)
})

const handler = applyWSSHandler({ wss, router: useAppRouter() })
const handler = applyWSSHandler({ wss, router: useAppRouter(), createContext: createStandaloneContext })

process.on('exit', () => {
handler.broadcastReconnectNotification()
Expand All @@ -30,32 +29,41 @@ function startWebSocketServer(url: URL, host: string, port: number) {
})
}

function startHttpServer(url: URL, host: string, port: number) {
// TODO: Authentication via the IPC, later we'll implement a proper authentication model.

const server = createHTTPServer({
router: useAppRouter()
})

server.server.on('listening', () => {
Logger.info(`RPC server at ${url}`)
})

server.listen(port, host)
process.on('exit', () => {
server.server.close()
})

process.on('SIGTERM', () => {
server.server.close()
})
}
// TODO: Maybe usable for the remote server one day.
// function startHttpServer(url: URL, host: string, port: number) {
// // TODO: Authentication via the IPC, later we'll implement a proper authentication model.
//
// const server = createHTTPServer({
// router: useAppRouter()
// })
//
// server.server.on('listening', () => {
// Logger.info(`RPC server at ${url}`)
// })
//
// server.listen(port, host)
// process.on('exit', () => {
// server.server.close()
// })
//
// process.on('SIGTERM', () => {
// server.server.close()
// })
// }

export default function useApiServer() {
const config = useAppConfig()
const url = new URL(config.rpcUrl)
const [host, port, protocol] = getServerUrl(url, 7180)
let cause
const host = '127.0.0.1'
for (const port of range(7000, 8000)) {
const url = `ws://${host}:${port}`
try {
startWebSocketServer(url, host, port)
return port
} catch (err) {
cause = err
console.warn(`Unable to bind server to ${url}`, cause)
}
}

if (protocol === 'http:') startHttpServer(url, host, port)
if (protocol === 'ws:') startWebSocketServer(url, host, port)
throw new Error('No port available for the server within 7000-8000', { cause })
}
6 changes: 6 additions & 0 deletions src/main/services/drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const kDeviceSupportsMultipleOutputs = 1
export type kDeviceCanDecoupleAudioOutput = typeof kDeviceCanDecoupleAudioOutput
export const kDeviceCanDecoupleAudioOutput = 2

export type DriverKind = 'monitor' | 'switch'

//
// Driver definition
//
Expand All @@ -30,6 +32,8 @@ export interface DriverBasicInformation {
readonly enabled: boolean
/** Indicates whether the driver is experimental, usually due to lack of testing. */
readonly experimental: boolean
/** Identifies the kind of device driven by the driver. */
readonly kind: DriverKind
/** A unique identifier for the driver. */
readonly guid: string
/** Defines the capabilities of the device driven by the driver. */
Expand Down Expand Up @@ -114,6 +118,7 @@ export function defineDriver(options: DefineDriverOptions) {
return {
enabled: info.enabled,
experimental: info.experimental,
kind: info.kind,
guid: info.guid,
...localizedInfo,
capabilities: info.capabilities
Expand All @@ -129,6 +134,7 @@ export function defineDriver(options: DefineDriverOptions) {
// Information and informational functionality.
enabled: info.enabled,
experimental: info.experimental,
kind: info.kind,
guid: info.guid,
capabilities: info.capabilities,
metadata: info,
Expand Down
46 changes: 43 additions & 3 deletions src/main/services/trpc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
import { initTRPC } from '@trpc/server'
import { randomBytes } from 'crypto'
import { initTRPC, TRPCError } from '@trpc/server'
import { memo } from 'radash'
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'
import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'
import type { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'
import useSuperJson from '@/rpc'

const t = initTRPC.create({
export type Context = Awaited<ReturnType<typeof createContext>>

function createContext(path: string | null) {
if (path == null) return { auth: undefined }
const url = new URL(`ws://127.0.0.1${path}`)
const auth = url.searchParams.get('auth') ?? undefined

return { auth }
}

export function createStandaloneContext(opts: CreateHTTPContextOptions | CreateWSSContextFnOptions) {
return createContext(opts.req.url ?? null)
}

const t = initTRPC.context<Context>().create({
transformer: useSuperJson()
})

export const { router, procedure, createCallerFactory } = t
export const getAuthToken = memo(function getAuthToken() {
return randomBytes(16).toString('base64url')
})

function error(code: TRPC_ERROR_CODE_KEY, message?: string, cause?: unknown): never {
throw new TRPCError({
code,
...(message ? { message } : {}),
...(cause != null ? { cause } : {})
})
}

export const { router, createCallerFactory } = t
export const procedure = t.procedure.use(async function checkAuth(opts) {
const { ctx } = opts
const { auth } = ctx

if (auth == null) error('UNAUTHORIZED')
if (auth !== getAuthToken()) error('UNAUTHORIZED')

return await opts.next()
})
Loading

0 comments on commit 4e5eb52

Please sign in to comment.