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)
export const [doAppMountedAction, onAppMountedAction] =
createCustomActionWithContext<{ app: Express }>(CoreActions.AppMounted)
createCustomActionWithContext<{ app: Express; internalApp: Express }>(
CoreActions.AppMounted,
)
export const [doIncomingRequestAction, onIncomingRequestAction] =
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 { IsoDate } from './BaseFields'
import { newId, pocketNow } from './util'
export type VersionId = string
@ -21,16 +23,24 @@ export type InstanceSecretCollection = {
}
export type InstanceFields = {
id: string
subdomain: string
version: VersionId
secrets: InstanceSecretCollection
dev: boolean // Should instance run in --dev mode
created: IsoDate
updated: IsoDate
}
export const mkInstance = (subdomain: string) =>
doNewInstanceRecordFilter({
export const mkInstance = (subdomain: string, id = newId()) => {
const now = pocketNow()
return doNewInstanceRecordFilter({
id,
subdomain,
version: '*',
secrets: {},
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 { fileURLToPath } from 'url'
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 { Settings } from './core/Settings'
import { logSettings } from './core/logSettings'
@ -32,6 +32,7 @@ export const settings = Settings({
PH_HOME: mkPath(_PH_HOME),
PH_PROJECT_DIR: mkPath(_PH_PROJECT_DIR),
PH_INTERNAL_HOST: mkString('localhost'),
PH_APEX_DOMAIN: mkString(_APEX_DOMAIN),
PH_PORT: mkNumber(3000),
@ -39,6 +40,7 @@ export const settings = Settings({
PH_DATA_DIR: mkPath(join(_PH_HOME, 'data'), { create: true }),
PH_DEV: mkBoolean(IS_DEV()),
PH_DEBUG: mkBoolean(DEBUG()),
PH_INTERNAL_APP_SECRET: mkString(''),
})
/** Accessors */
@ -57,6 +59,14 @@ export const DATA_DIR = (...paths: string[]) =>
export const NODE_ENV = () => process.env.NODE_ENV
export const INSTANCE_DATA_DIR = (id: InstanceId, ...paths: string[]) =>
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 PUBLIC_INSTANCE_URL = ({ subdomain }: Partial<InstanceFields>) => {
const url = new URL(`${HTTP_PROTOCOL()}//${subdomain}.${APEX_DOMAIN()}`)

View File

@ -4,6 +4,7 @@ import {
PocketHostPlugin,
doSettingsFilter,
namespace,
newId,
onAfterPluginsLoadedAction,
onInstanceConfigFilter,
onInstanceLogAction,
@ -12,6 +13,7 @@ import {
} from '../common'
import { dbg, info } from './cli'
import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants'
import { setConfig } from './core/config'
import { serve } from './server'
export const pockethost: PocketHostPlugin = async ({}) => {
@ -44,4 +46,9 @@ export const pockethost: PocketHostPlugin = async ({}) => {
})
}, 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,
doRequestErrorMessageFilter,
} 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 () => {
const _proxyLogger = LoggerService().create('ProxyService')
@ -45,7 +52,25 @@ export const serve = async () => {
}
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) => {
const method = req.method