Skip to content

Commit

Permalink
Automatically refresh plugin api tokens prior to expiry
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Apr 28, 2024
1 parent ebb5cae commit 3b787d2
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ _For detailed documentation, visit [pup.56k.guru](https://pup.56k.guru)._
To install Pup, open your terminal and execute the following command:

```bash
deno run -Ar jsr:@pup/[email protected].36 setup --channel prerelease
deno run -Ar jsr:@pup/[email protected].37 setup --channel prerelease
```

This command downloads the latest version of Pup and installs it on your system. The `--channel prerelease` option is included as there is no stable version of Pup yet. Read more abour release
Expand Down
2 changes: 1 addition & 1 deletion application.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

const Application = {
name: "pup",
version: "1.0.0-rc.36",
version: "1.0.0-rc.37",
url: "jsr:@pup/pup@$VERSION",
canary_url: "https://raw.githubusercontent.com/Hexagon/pup/main/pup.ts",
deno: null, /* Minimum stable version of Deno required to run Pup (without --unstable-* flags) */
Expand Down
8 changes: 4 additions & 4 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pup/pup",
"version": "1.0.0-rc.36",
"version": "1.0.0-rc.37",

"exports": {
".": "./pup.ts",
Expand Down Expand Up @@ -42,17 +42,17 @@
"@cross/deepmerge": "jsr:@cross/deepmerge@^1.0.0",
"@cross/env": "jsr:@cross/env@^1.0.2",
"@cross/fs": "jsr:@cross/fs@^0.0.10",
"@cross/jwt": "jsr:@cross/jwt@^0.4.2",
"@cross/jwt": "jsr:@cross/jwt@^0.4.6",
"@cross/runtime": "jsr:@cross/runtime@^1.0.0",
"@cross/service": "jsr:@cross/service@^1.0.3",
"@cross/test": "jsr:@cross/test@^0.0.9",
"@cross/utils": "jsr:@cross/utils@^0.12.0",
"@hexagon/croner": "jsr:@hexagon/croner@^8.0.2",
"@oak/oak": "jsr:@oak/oak@^15.0.0",
"@pup/api-client": "jsr:@pup/api-client@^1.0.1",
"@pup/api-client": "jsr:@pup/api-client@^1.0.2",
"@pup/api-definitions": "jsr:@pup/api-definitions@^1.0.1",
"@pup/common": "jsr:@pup/common@^1.0.2",
"@pup/plugin": "jsr:@pup/plugin@^1.0.0",
"@pup/plugin": "jsr:@pup/plugin@^1.0.1",
"@std/assert": "jsr:@std/assert@^0.223.0",
"@std/async": "jsr:@std/async@^0.223.0",
"@std/encoding": "jsr:@std/encoding@^0.223.0",
Expand Down
2 changes: 1 addition & 1 deletion docs/src/_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Universal Process Manager"
},
"substitute": {
"$PUP_VERSION": "1.0.0-rc.36"
"$PUP_VERSION": "1.0.0-rc.37"
},
"top_links": [
{
Expand Down
4 changes: 4 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ nav_order: 13

All notable changes to this project will be documented in this section.

## [1.0.0-rc.37] - 2024-04-26

- fix(plugins): Automatically refresh plugin API tokens prior to expiry

## [1.0.0-rc.36] - 2024-04-26

## Added
Expand Down
5 changes: 5 additions & 0 deletions docs/src/examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export class PupPlugin extends PluginImplementation {
true,
)
}

// Forward api token refreshes to the api client
public async refreshApiToken(apiToken: string): Promise<void> {
this.client.refreshApiToken(apiToken)
}
}
```

Expand Down
5 changes: 5 additions & 0 deletions docs/src/examples/plugins/example-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export class PupPlugin extends PluginImplementation {
})
}

// deno-lint-ignore require-await
public async refreshApiToken(apiToken: string): Promise<void> {
this.client.refreshApiToken(apiToken)
}

// Helper function to send logs via the Rest API
public async sendLog(severity: string, message: string) {
// Wrap all API calls in try/catch
Expand Down
2 changes: 1 addition & 1 deletion docs/src/examples/plugins/pup.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
// Normally, you would use full uri to plugin, e.g. jsr:@pup/plugin-web-interface
// But when developing locally, you'll have to use the full absolute local path
"url": "file:///path/to/pup/docs/src/examples/plugins/example-plugin.ts",
"url": "file:///config/workspace/pup/docs/src/examples/plugins/example-plugin.ts",

// It is possible to pass options to a plugin, like this:
"options": {
Expand Down
15 changes: 14 additions & 1 deletion lib/common/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @license MIT
*/

import { createJWT, generateKey, JWTPayload, validateJWT } from "@cross/jwt"
import { createJWT, generateKey, JWTPayload, unsafeParseJWT, validateJWT } from "@cross/jwt"
import { DEFAULT_SECRET_KEY_ALGORITHM } from "../core/configuration.ts"

export interface PupTokenPayload {
Expand Down Expand Up @@ -37,3 +37,16 @@ export function ValidateToken(token: string, key: CryptoKey): unknown | null {
return null // Token invalid
}
}

export function SecondsToExpiry(token: string): number | undefined {
try {
const decoded = unsafeParseJWT(token)
if (decoded && decoded.exp) {
return decoded.exp - Math.round(Date.now() / 1000)
} else {
return undefined
}
} catch (_err) {
return undefined
}
}
4 changes: 4 additions & 0 deletions lib/core/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const DEFAULT_REST_API_HOSTNAME = "127.0.0.1"
// Prop file constants
export const DEFAULT_PROP_FILE_PERMISSIONS = 0o600

// Plugin constants
export const PLUGIN_TOKEN_EXPIRE_S = 300 // Expire API tokens after 5 minutes
export const PLUGIN_TOKEN_REFRESH_ADVANCE_S = 10 // Renew Plugin API token automatically 10 seconds before expiry

interface Configuration {
name?: string
api?: ApiConfiguration
Expand Down
7 changes: 7 additions & 0 deletions lib/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export class Plugin {
throw new Error("Plugin missing meta.api")
}
}
async refreshToken(token: string): Promise<void> {
this.apiToken = token
await this.impl?.refreshApiToken(token)
}
getToken(): string {
return this.apiToken
}
async terminate() {
if (this.impl?.cleanup) await this.impl?.cleanup()
}
Expand Down
31 changes: 25 additions & 6 deletions lib/core/pup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
DEFAULT_SECRET_FILE_PERMISSIONS,
type GlobalLoggerConfiguration,
MAINTENANCE_INTERVAL_MS,
PLUGIN_TOKEN_EXPIRE_S,
PLUGIN_TOKEN_REFRESH_ADVANCE_S,
type ProcessConfiguration,
validateConfiguration,
WATCHDOG_INTERVAL_MS,
Expand All @@ -28,8 +30,7 @@ import type { ApiTelemetryData } from "@pup/api-definitions"
import { rm } from "@cross/fs"
import { findFreePort } from "./port.ts"
import { Plugin } from "./plugin.ts"
import { GenerateToken } from "../common/token.ts"

import { GenerateToken, SecondsToExpiry } from "../common/token.ts"
interface InstructionResponse {
success: boolean
action?: string
Expand Down Expand Up @@ -134,7 +135,7 @@ class Pup {
// Initialize plugins
if (this.configuration.plugins) {
const secret = await this.secret?.load()
const pluginToken = await GenerateToken(secret!, { consumer: "plugin" }, Date.now() + 720 * 24 * 60 * 60 * 1000)
const pluginToken = await GenerateToken(secret!, { consumer: "plugin" }, Date.now() + PLUGIN_TOKEN_EXPIRE_S * 1000)
for (const plugin of this.configuration.plugins) {
const newPlugin = new Plugin(plugin, `${this.restApi?.hostname}:${this.restApi?.port}`, pluginToken)
let success = true
Expand All @@ -152,7 +153,6 @@ class Pup {
this.logger.error("plugins", `Failed to verify plugin '${plugin.url}': ${e.message}`)
success = false
}

if (success) {
this.plugins.push(newPlugin)
this.logger.log("plugins", `Plugin '${newPlugin.impl?.meta.name}@${newPlugin.impl?.meta.version}' loaded from '${plugin.url}'`)
Expand Down Expand Up @@ -247,7 +247,7 @@ class Pup {
*
* @private
*/
private watchdog = () => {
private watchdog = async () => {
this.events.emit("watchdog")
// Wrap watchdog operation in a catch to prevent it from ever stopping
try {
Expand Down Expand Up @@ -315,6 +315,25 @@ class Pup {
this.logger.error("watchdog", `Heartbeat update failed: ${e}`)
}

// Refresh plugin tokens if needed
try {
for (const plugin of this.plugins) {
// Parse token and check seconds left to expiry
const secondsLeft = await SecondsToExpiry(plugin.getToken())
if (secondsLeft !== undefined && secondsLeft < PLUGIN_TOKEN_REFRESH_ADVANCE_S) {
this.logger.log("plugins", `API token for plugin '${plugin.impl?.meta.name} is about to expire in ${secondsLeft}s. Refreshing token.`)
const secret = this.secret?.fromCache()
if (secret) {
// Send a fresh token to the plugin
const newPluginToken = await GenerateToken(secret!, { consumer: "plugin" }, Date.now() + PLUGIN_TOKEN_EXPIRE_S * 1000)
plugin.refreshToken(newPluginToken)
}
}
}
} catch (e) {
this.logger.error("watchdog", `API Token refresh failed: ${e}`)
}

// Reschedule watchdog
if (!this.requestTerminate) {
this.watchdogTimer = setTimeout(() => {
Expand All @@ -337,7 +356,7 @@ class Pup {
return resultingPort.toString()
})

// Initializing rest a
// Initializing rest api
this.logger.info("rest", "Initializing rest api")

// Initialize rest api
Expand Down
14 changes: 14 additions & 0 deletions versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
"canary_url": "https://raw.githubusercontent.com/Hexagon/pup/main/pup.ts",
"stable": [],
"prerelease": [
{
"version": "1.0.0-rc.37",
"url": "jsr:@pup/[email protected]",
"deno": null,
"deno_unstable": "1.42.0",
"default_permissions": [
"--allow-env",
"--allow-read",
"--allow-write",
"--allow-sys=loadavg,systemMemoryInfo,osUptime,osRelease,uid,gid",
"--allow-net",
"--allow-run"
]
},
{
"version": "1.0.0-rc.36",
"url": "jsr:@pup/[email protected]",
Expand Down

0 comments on commit 3b787d2

Please sign in to comment.