feat(pockethost): Add internal app (remote control) support

This commit is contained in:
Ben Allfree 2024-06-29 17:14:42 -07:00
parent b7277cf885
commit 45a8f457da
8 changed files with 82 additions and 7 deletions

View File

@ -0,0 +1,5 @@
---
'pockethost': minor
---
Add internal app (remote control) support

View File

@ -117,7 +117,9 @@ export const [doInstanceLogAction, onInstanceLogAction] =
}>(CoreActions.InstanceLog) }>(CoreActions.InstanceLog)
export const [doAppMountedAction, onAppMountedAction] = export const [doAppMountedAction, onAppMountedAction] =
createCustomActionWithContext<{ app: Express }>(CoreActions.AppMounted) createCustomActionWithContext<{ app: Express; internalApp: Express }>(
CoreActions.AppMounted,
)
export const [doIncomingRequestAction, onIncomingRequestAction] = export const [doIncomingRequestAction, onIncomingRequestAction] =
createCustomActionWithContext<{ req: Request; res: Response; host: string }>( createCustomActionWithContext<{ req: Request; res: Response; host: string }>(

View File

@ -0,0 +1,10 @@
export type RecordId = string
export type UserId = RecordId
export type IsoDate = string
export type BaseFields = {
id: RecordId
created: IsoDate
updated: IsoDate
}
export type AnyField = { [_: string]: string | number }

View File

@ -1,4 +1,6 @@
import { doNewInstanceRecordFilter } from '../plugin' import { doNewInstanceRecordFilter } from '../plugin'
import { IsoDate } from './BaseFields'
import { newId, pocketNow } from './util'
export type VersionId = string export type VersionId = string
@ -21,16 +23,24 @@ export type InstanceSecretCollection = {
} }
export type InstanceFields = { export type InstanceFields = {
id: string
subdomain: string subdomain: string
version: VersionId version: VersionId
secrets: InstanceSecretCollection secrets: InstanceSecretCollection
dev: boolean // Should instance run in --dev mode dev: boolean // Should instance run in --dev mode
created: IsoDate
updated: IsoDate
} }
export const mkInstance = (subdomain: string) => export const mkInstance = (subdomain: string, id = newId()) => {
doNewInstanceRecordFilter({ const now = pocketNow()
return doNewInstanceRecordFilter({
id,
subdomain, subdomain,
version: '*', version: '*',
secrets: {}, secrets: {},
dev: false, dev: false,
created: now,
updated: now,
}) })
}

View File

@ -1 +1,7 @@
export const pocketNow = () => new Date().toISOString() import { customAlphabet } from 'nanoid'
import { IsoDate } from './BaseFields'
export const newId = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10)
export const pocketNow = () => toPocketDate(new Date())
export const toPocketDate = (date: Date) => date.toISOString()
export const fromPocketDate = (iso: IsoDate) => new Date(iso)

View File

@ -4,7 +4,7 @@ import { dirname, join } from 'path'
import { cwd } from 'process' import { cwd } from 'process'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { mkBoolean, mkCsvString, mkNumber, mkPath, mkString } from '../core' import { mkBoolean, mkCsvString, mkNumber, mkPath, mkString } from '../core'
import { InstanceId } from './common' import { InstanceFields, InstanceId } from './common'
import { DEBUG, IS_DEV } from './common/debug' import { DEBUG, IS_DEV } from './common/debug'
import { Settings } from './core/Settings' import { Settings } from './core/Settings'
import { logSettings } from './core/logSettings' import { logSettings } from './core/logSettings'
@ -32,6 +32,7 @@ export const settings = Settings({
PH_HOME: mkPath(_PH_HOME), PH_HOME: mkPath(_PH_HOME),
PH_PROJECT_DIR: mkPath(_PH_PROJECT_DIR), PH_PROJECT_DIR: mkPath(_PH_PROJECT_DIR),
PH_INTERNAL_HOST: mkString('localhost'),
PH_APEX_DOMAIN: mkString(_APEX_DOMAIN), PH_APEX_DOMAIN: mkString(_APEX_DOMAIN),
PH_PORT: mkNumber(3000), PH_PORT: mkNumber(3000),
@ -39,6 +40,7 @@ export const settings = Settings({
PH_DATA_DIR: mkPath(join(_PH_HOME, 'data'), { create: true }), PH_DATA_DIR: mkPath(join(_PH_HOME, 'data'), { create: true }),
PH_DEV: mkBoolean(IS_DEV()), PH_DEV: mkBoolean(IS_DEV()),
PH_DEBUG: mkBoolean(DEBUG()), PH_DEBUG: mkBoolean(DEBUG()),
PH_INTERNAL_APP_SECRET: mkString(''),
}) })
/** Accessors */ /** Accessors */
@ -57,6 +59,14 @@ export const DATA_DIR = (...paths: string[]) =>
export const NODE_ENV = () => process.env.NODE_ENV export const NODE_ENV = () => process.env.NODE_ENV
export const INSTANCE_DATA_DIR = (id: InstanceId, ...paths: string[]) => export const INSTANCE_DATA_DIR = (id: InstanceId, ...paths: string[]) =>
join(DATA_DIR(), id, ...paths) join(DATA_DIR(), id, ...paths)
export const INTERNAL_HOST = () => settings.PH_INTERNAL_HOST
export const INTERNAL_APP_SECRET = () => settings.PH_INTERNAL_APP_SECRET
export const INTERNAL_APP_URL = (...paths: string[]) =>
`${HTTP_PROTOCOL()}//${INTERNAL_HOST()}:${PORT()}/${INTERNAL_APP_PREFIX}/${join(
...paths,
)}`
export const INTERNAL_APP_PREFIX = `_internal`
export const INTERNAL_APP_AUTH_HEADER = `x-pockethost-authorization`
export const HTTP_PROTOCOL = () => (PORT() === 443 ? 'https:' : 'http:') export const HTTP_PROTOCOL = () => (PORT() === 443 ? 'https:' : 'http:')
export const PUBLIC_INSTANCE_URL = ({ subdomain }: Partial<InstanceFields>) => { export const PUBLIC_INSTANCE_URL = ({ subdomain }: Partial<InstanceFields>) => {
const url = new URL(`${HTTP_PROTOCOL()}//${subdomain}.${APEX_DOMAIN()}`) const url = new URL(`${HTTP_PROTOCOL()}//${subdomain}.${APEX_DOMAIN()}`)

View File

@ -4,6 +4,7 @@ import {
PocketHostPlugin, PocketHostPlugin,
doSettingsFilter, doSettingsFilter,
namespace, namespace,
newId,
onAfterPluginsLoadedAction, onAfterPluginsLoadedAction,
onInstanceConfigFilter, onInstanceConfigFilter,
onInstanceLogAction, onInstanceLogAction,
@ -12,6 +13,7 @@ import {
} from '../common' } from '../common'
import { dbg, info } from './cli' import { dbg, info } from './cli'
import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants' import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants'
import { setConfig } from './core/config'
import { serve } from './server' import { serve } from './server'
export const pockethost: PocketHostPlugin = async ({}) => { export const pockethost: PocketHostPlugin = async ({}) => {
@ -44,4 +46,9 @@ export const pockethost: PocketHostPlugin = async ({}) => {
}) })
}, 99) }, 99)
onAfterPluginsLoadedAction(async () => {
if (INTERNAL_APP_SECRET()) return
info(`Generating internal app secret...`)
setConfig(`PH_INTERNAL_APP_SECRET`, newId(30))
})
} }

View File

@ -12,7 +12,14 @@ import {
doRequestErrorAction, doRequestErrorAction,
doRequestErrorMessageFilter, doRequestErrorMessageFilter,
} from '../common' } from '../common'
import { APEX_DOMAIN, PORT, asyncExitHook } from '../core' import {
APEX_DOMAIN,
INTERNAL_APP_AUTH_HEADER,
INTERNAL_APP_PREFIX,
INTERNAL_APP_SECRET,
PORT,
asyncExitHook,
} from '../core'
export const serve = async () => { export const serve = async () => {
const _proxyLogger = LoggerService().create('ProxyService') const _proxyLogger = LoggerService().create('ProxyService')
@ -45,7 +52,25 @@ export const serve = async () => {
} }
app.use(errorHandler) app.use(errorHandler)
doAppMountedAction({ app }) const internalApp = express()
internalApp.use((req, res, next) => {
dbg(`Got a request to internal app ${req.url}`)
next()
})
internalApp.use((req, res, next) => {
const authHeader = req.headers[INTERNAL_APP_AUTH_HEADER]
const internalAppSecret = INTERNAL_APP_SECRET()
if (authHeader !== internalAppSecret) {
res.status(403).send('Forbidden')
return
}
next()
})
app.use(`/${INTERNAL_APP_PREFIX}`, internalApp)
doAppMountedAction({ app, internalApp })
app.all(`*`, async (req, res, next) => { app.all(`*`, async (req, res, next) => {
const method = req.method const method = req.method