Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Apr 19, 2024
1 parent 27d387c commit 6f95a6a
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 52 deletions.
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.25",
version: "1.0.0-rc.26",
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
7 changes: 4 additions & 3 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.25",
"version": "1.0.0-rc.26",
"exports": {
".": "./pup.ts",
"./mod.ts": "./mod.ts",
Expand Down Expand Up @@ -34,7 +34,7 @@

"imports": {
"@cross/deepmerge": "jsr:@cross/deepmerge@^1.0.0",
"@cross/env": "jsr:@cross/env@^1.0.0",
"@cross/env": "jsr:@cross/env@^1.0.2",
"@cross/fs": "jsr:@cross/fs@^0.0.9",
"@cross/jwt": "jsr:@cross/jwt@^0.4.1",
"@cross/runtime": "jsr:@cross/runtime@^1.0.0",
Expand All @@ -43,9 +43,10 @@
"@cross/utils": "jsr:@cross/utils@^0.11.0",
"@hexagon/bundlee": "jsr:@hexagon/bundlee@^0.9.6/mod.ts",
"@hexagon/croner": "jsr:@hexagon/croner@^8.0.1",
"@oak/oak": "jsr:@oak/oak@^14.2.0",
"@oak/oak": "jsr:@oak/oak@^15.0.0",
"@std/assert": "jsr:@std/assert@^0.223.0",
"@std/async": "jsr:@std/async@^0.223.0",
"@std/encoding": "jsr:@std/encoding@^0.223.0",
"@std/io": "jsr:@std/io@^0.223.0",
"@std/path": "jsr:@std/path@^0.223.0",
"@std/semver": "jsr:@std/semver@^0.223.0",
Expand Down
3 changes: 2 additions & 1 deletion docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ nav_order: 13

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

## [1.0.0-rc.26] - Unreleased
## [1.0.0-rc.26] - 2023-04-19

- fix(core): Remove stray console.log
- fix(core): Fix working dir different from current dir
- change(plugins): Separate core api from plugin api
- feat(rest): Add rest API with JWT Bearer auth
- fix(core): Update dependency @cross/env to fix a bug in windows caused by legacy environment variable keys such as `=C:`

## [1.0.0-rc.25] - 2023-04-17

Expand Down
6 changes: 6 additions & 0 deletions lib/core/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const KV_SIZE_LIMIT_BYTES = 65_536

interface Configuration {
name?: string
api?: ApiConfiguration
logger?: GlobalLoggerConfiguration
watcher?: GlobalWatcherConfiguration
processes: ProcessConfiguration[]
Expand All @@ -24,6 +25,11 @@ interface Configuration {
terminateGracePeriod?: number
}

interface ApiConfiguration {
hostname?: string
port?: number
}

interface PluginConfiguration {
url: string
options?: unknown
Expand Down
23 changes: 15 additions & 8 deletions lib/core/pup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { RestApi } from "./rest.ts"
import { EventEmitter } from "../common/eventemitter.ts"
import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "../common/utils.ts"
import * as uuid from "@std/uuid"
import { Secret } from "./secret.ts"

interface InstructionResponse {
success: boolean
Expand Down Expand Up @@ -53,6 +54,7 @@ class Pup {
public configFilePath?: string

public cleanupQueue: string[] = []
public secret?: Secret

static async init(unvalidatedConfiguration: unknown, configFilePath?: string): Promise<Pup> {
const temporaryStoragePath: string | undefined = configFilePath ? await toTempPath(configFilePath) : undefined
Expand All @@ -65,6 +67,7 @@ class Pup {
let statusFile
let ipcFile
let logStore
let secretFile
if (configFilePath && temporaryStoragePath && persistentStoragePath) {
this.configFilePath = toResolvedAbsolutePath(configFilePath)

Expand All @@ -75,7 +78,7 @@ class Pup {

ipcFile = `${this.temporaryStoragePath}/.main.ipc` // Plain text file (serialized js object)
statusFile = `${this.persistentStoragePath}/.main.status` // Deno KV store

secretFile = `${this.temporaryStoragePath}/.main.secret` // Plain text file containing the JWT secret for the rest api
logStore = `${this.persistentStoragePath}/.main.log` // Deno KV store
}

Expand All @@ -96,6 +99,9 @@ class Pup {

// Initialize file ipc, if a path were passed
if (ipcFile) this.ipc = new FileIPC(ipcFile)

// Initialize API secret
if (secretFile) this.secret = new Secret(secretFile)
}

/**
Expand Down Expand Up @@ -188,9 +194,9 @@ class Pup {
process.init()
}

this.api()
this.watchdog()
this.maintenance(true)
this.api()
}

public allProcesses(): Process[] {
Expand Down Expand Up @@ -293,18 +299,19 @@ class Pup {
}

/**
* Watchdog function that manages process lifecycle events like
* auto-start, restart, and timeouts.
*
* Starts the api
* @private
*/
private api = () => {
private api = async () => {
const secret = await this.secret?.get()
if (!secret) return

// Initializing rest a
this.logger.info("rest", "Initializing rest api")
// Initialize rest api
try {
this.restApi = new RestApi(this)
this.restApi.start(8002)
this.restApi = new RestApi(this, this.configuration.api?.port, this.configuration.api?.hostname, secret)
this.restApi.start()
} catch (e) {
this.logger.error("rest", `An error occured while inizializing the rest api: ${e.message}`)
}
Expand Down
77 changes: 43 additions & 34 deletions lib/core/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,30 @@ import { PupApi } from "./api.ts"
import { Pup } from "./pup.ts"
import { generateKey, verifyJWT } from "@cross/jwt"

// Dummy secret for now
const jwtSecret = "GawoWOOWOOFSAFFASOFOFOASOAOFASFwoopieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"

const jwtKey = await generateKey(jwtSecret, "HS512")

const authMiddleware = async (ctx: Context, next: () => Promise<unknown>) => {
const headers: Headers = ctx.request.headers
const authorization = headers.get("Authorization")
if (!authorization) {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Authorization header required" }
return
}
const parts = authorization.split(" ")
if (parts.length !== 2 || parts[0] !== "Bearer") {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Invalid authorization format" }
return
}
const token = parts[1]
try {
const payload = await verifyJWT(token, jwtKey)
ctx.state.user = payload.user // Add user info to context state
await next() // Proceed if valid
} catch (_err) {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Invalid token" }
const generateAuthMiddleware = (key: CryptoKey) => {
return async (ctx: Context, next: () => Promise<unknown>) => {
const headers: Headers = ctx.request.headers
const authorization = headers.get("Authorization")
if (!authorization) {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Authorization header required" }
return
}
const parts = authorization.split(" ")
if (parts.length !== 2 || parts[0] !== "Bearer") {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Invalid authorization format" }
return
}
const token = parts[1]
try {
const payload = await verifyJWT(token, key)
ctx.state.user = payload.user // Add user info to context state
await next() // Proceed if valid
} catch (_err) {
ctx.response.status = Status.Unauthorized
ctx.response.body = { message: "Invalid token" }
}
}
}

Expand All @@ -39,16 +36,27 @@ export class RestApi {
private router: Router
private appAbortController: AbortController

constructor(pup: Pup) { // Takes a Pup instance
private port: number
private hostname: string
private secret: string
private key?: CryptoKey

constructor(pup: Pup, port: number | undefined, hostname: string | undefined, jwtSecret: string) { // Takes a Pup instance
this.pupApi = new PupApi(pup)
this.app = new Application()
this.router = new Router()
this.appAbortController = new AbortController()

this.port = port || 16421
this.hostname = hostname || "localhost"
this.secret = jwtSecret
this.setupRoutes() // Setup routes within the constructor
}

private setupRoutes() {
private async setupKey() {
return await generateKey(this.secret, "HS512")
}

private async setupRoutes() {
// Process related routes
this.router
.get("/processes", (ctx) => {
Expand Down Expand Up @@ -189,15 +197,16 @@ export class RestApi {
}
})

this.app.use(authMiddleware)
this.app.use(generateAuthMiddleware(await this.setupKey()))
this.app.use(this.router.routes())
this.app.use(this.router.allowedMethods())
}

public async start(port = 8001) {
public async start() {
this.pupApi.log("info", "rest", `Starting the REST API`)
await this.app.listen({ port, signal: this.appAbortController.signal })
this.pupApi.log("info", "rest", `REST API listening on port ${port}`)
//await this.app.listen({ port, signal: this.appAbortController.signal })
await this.app.listen({ port: this.port, hostname: this.hostname, signal: this.appAbortController.signal })
this.pupApi.log("info", "rest", `REST API listening on port ${this.port}`)
}

public terminate() {
Expand Down
39 changes: 39 additions & 0 deletions lib/core/secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { exists, readFile, writeFile } from "@cross/fs"
import { encodeBase64 } from "@std/encoding/base64"

export class Secret {
path: string // Type annotation for clarity
cache: string | undefined // Cached secret

constructor(secretFilePath: string) { // Type annotation for the parameter
this.path = secretFilePath
}

async generateSecret(): Promise<string> {
const secretArray = new Uint8Array(32)
crypto.getRandomValues(secretArray)
const secret = encodeBase64(secretArray)
await writeFile(this.path, secret)
return secret
}

async loadSecret(): Promise<string> { // Specify return type
return await readFile(this.path, "utf-8")
}

async get(): Promise<string> { // Specify return type
try {
if (!this.cache) {
if (await exists(this.path)) {
this.cache = await this.loadSecret()
} else {
this.cache = await this.generateSecret()
}
}
return this.cache
} catch (err) {
console.error("Error handling secret:", err)
throw err
}
}
}
5 changes: 0 additions & 5 deletions tools/token-generator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { createJWT, generateKey } from "@cross/jwt"

// Dummy secret
const jwtSecret = "GawoWOOWOOFSAFFASOFOFOASOAOFASFwoopieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"

const key = await generateKey(jwtSecret, "HS512")

const payload = {
user: {
name: "Test",
},
exp: Date.now() + 60 * 60 * 1000,
}

console.log(await createJWT(payload, key))
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.26",
"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.25",
"url": "jsr:@pup/[email protected]",
Expand Down

0 comments on commit 6f95a6a

Please sign in to comment.