diff --git a/.env.dist b/.env.dist index 88e6197..6a5e075 100644 --- a/.env.dist +++ b/.env.dist @@ -22,7 +22,7 @@ FAIRPOST_OAUTH_HOSTNAME=localhost FAIRPOST_OAUTH_PORT=8000 # rest api server settings -FAIRPOST_SERVER_HOSTNAME=localhost +FAIRPOST_SERVER_BIND=localhost FAIRPOST_SERVER_PORT=8000 FAIRPOST_SERVER_CORS=* FAIRPOST_SESSION_SECURE=true diff --git a/src/cli.ts b/src/cli.ts index c5df98b..cde4c70 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,7 +6,7 @@ import "./bootstrap.ts"; import Fairpost from "./services/Fairpost.ts"; -import { JSONReplacer } from "./utilities.ts"; +import { JSONReplacer, parsePayload } from "./utilities.ts"; import { PlatformId } from "./platforms/index.ts"; import { SourceStage, PostStatus } from "./types/index.ts"; import Operator from "./models/Operator.ts"; @@ -38,6 +38,17 @@ if (POST) { [SOURCE, PLATFORM] = POST.split(":") as [string, PlatformId]; } +// payload +const chunks: Buffer[] = []; +if (!process.stdin.isTTY) { + for await (const chunk of process.stdin) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } +} +const PAYLOAD = chunks.length + ? await parsePayload(Buffer.concat(chunks)) + : undefined; + // utilities function getOption(key: string): boolean | string | null { if (process.argv.includes(`--${key}`)) return true; @@ -64,6 +75,7 @@ async function main() { date: DATE ? new Date(DATE) : undefined, status: STATUS, stage: STAGE, + payload: PAYLOAD, }); console.info(JSON.stringify(output, JSONReplacer, "\t")); diff --git a/src/services/Server.ts b/src/services/Server.ts index eee4801..ffdc95d 100644 --- a/src/services/Server.ts +++ b/src/services/Server.ts @@ -4,7 +4,7 @@ import { createServer, IncomingMessage, ServerResponse } from "http"; import Fairpost from "./Fairpost.ts"; import AuthService from "./AuthService.ts"; -import { JSONReplacer } from "../utilities.ts"; +import { JSONReplacer, parsePayload } from "../utilities.ts"; import { PlatformId } from "../platforms/index.ts"; import { SourceStage, PostStatus } from "../types/index.ts"; import Operator from "../models/Operator.ts"; @@ -16,7 +16,7 @@ import User from "../models/User.ts"; export default class Server { public static async serve(): Promise { process.env.FAIRPOST_UI = "api"; - const host = process.env.FAIRPOST_SERVER_HOSTNAME; + const host = process.env.FAIRPOST_SERVER_BIND; const port = Number(process.env.FAIRPOST_SERVER_PORT); return await new Promise((resolve) => { const server = createServer((req, res) => { @@ -98,6 +98,17 @@ export default class Server { const stage = (parsed.searchParams.get("stage") as SourceStage) || undefined; + // read payload from PUT or POST + let payload = undefined as undefined | Buffer | string | object; + if (request.method === "POST" || request.method === "PUT") { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(chunk as Buffer); + } + const buffer = Buffer.concat(chunks); + payload = await parsePayload(buffer, request.headers["content-type"]); + } + const args = { password: password, dryrun: dryrun || undefined, @@ -108,6 +119,7 @@ export default class Server { date: date ? new Date(date) : undefined, status: status, stage: stage, + payload: payload, }; let code = 0; diff --git a/src/types/CommandArguments.ts b/src/types/CommandArguments.ts index 7ea7a34..5a0d85a 100644 --- a/src/types/CommandArguments.ts +++ b/src/types/CommandArguments.ts @@ -16,4 +16,5 @@ export default interface CommandArguments { date?: Date; status?: PostStatus; stage?: SourceStage; + payload?: Buffer | string | object; } diff --git a/src/utilities.ts b/src/utilities.ts index 8ae9663..88837bf 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -16,6 +16,48 @@ export function isSimilarArray(a: any, b: any) { return a.length === b.length && a.every((el: any) => b.includes(el)); } +export async function parsePayload( + buffer: Buffer, + type?: string, +): Promise { + const str = buffer.toString("utf8"); + + const contentType = type?.split(";")[0].trim().toLowerCase(); + if (contentType === "application/json") { + try { + return JSON.parse(str); + } catch { + return buffer; // invalid JSON, keep raw + } + } + if (contentType && contentType.startsWith("text/")) { + return str; + } + + // Heuristic fallback: Try JSON first + try { + const trimmed = str.trim(); + if ( + (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + return JSON.parse(trimmed); + } + } catch { + // Ignore, not valid JSON + } + + // Check for binary (NUL bytes or lots of control chars) + const isBinary = buffer.some((b) => b === 0 || b < 7 || (b > 13 && b < 32)); + + if (!isBinary) { + return str; + } + + // Otherwise, return raw binary + return buffer; +} + export class ApiResponseError extends Error { response: Response; // eslint-disable-next-line @typescript-eslint/no-explicit-any