Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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"));
Expand Down
16 changes: 14 additions & 2 deletions src/services/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -16,7 +16,7 @@ import User from "../models/User.ts";
export default class Server {
public static async serve(): Promise<string> {
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) => {
Expand Down Expand Up @@ -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,
Expand All @@ -108,6 +119,7 @@ export default class Server {
date: date ? new Date(date) : undefined,
status: status,
stage: stage,
payload: payload,
};

let code = 0;
Expand Down
1 change: 1 addition & 0 deletions src/types/CommandArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export default interface CommandArguments {
date?: Date;
status?: PostStatus;
stage?: SourceStage;
payload?: Buffer | string | object;
}
42 changes: 42 additions & 0 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer | string | object> {
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
Expand Down