Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/pdftk/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"executor": "@riwi/bun:test",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"dependsOn": ["docker-build-test"],
"options": {}
"options": {
"maxConcurrency": 1
}
},
"local-test": {
"executor": "@riwi/bun:test",
Expand Down
41 changes: 9 additions & 32 deletions apps/pdftk/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
formFillStream,
uncompressStream,
} from '@riwi/binary/pdftk';
import { handleRouteError } from '@riwi/http/error';
import { connect, healthEndpoints, post, processEndpoints } from '@riwi/http/route';
import { httpServer } from '@riwi/http/server';
import { middlewareQuery } from '@riwi/http/validate';
Expand All @@ -30,20 +31,14 @@ httpServer(
try {
await compressStream({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
post({ path: '/uncompress' }, async ({ req, res }) => {
try {
await uncompressStream({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
post(
Expand All @@ -53,41 +48,29 @@ httpServer(
try {
await encryptStream({ input: req, output: res, password, userPassword, allow });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
},
),
post({ path: '/decrypt' }, middlewareQuery(decryptSchema), async ({ req, res, query: { password } }) => {
try {
await decryptStream({ input: req, output: res, password });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
post({ path: '/data/fields' }, async ({ req, res }) => {
try {
await dataFieldsStream({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
post({ path: '/data/dump' }, async ({ req, res }) => {
try {
await dataDumpStream({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
// post({ path: '/data/annots' }, async ({ req, res }) => {
Expand All @@ -109,21 +92,15 @@ httpServer(
try {
await formFillStream({ input: req, output: res, flag, fontName, data });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
},
),
post({ path: '/data/fdf' }, async ({ req, res }) => {
try {
await dataFdfStream({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'pdftk failed');
}
handleRouteError(res, error, 'pdftk failed');
}
}),
...healthEndpoints,
Expand Down
113 changes: 101 additions & 12 deletions apps/pdftk/src/test/pdftk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync, statSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { currentArch } from '@riwi/docker';
import { wait } from '@riwi/helper';
import { streamLength, streamToBuffer } from '@riwi/stream';
import { createProcessEndpointsDisabledTest, createProcessEndpointTests, useTestContainer } from '@riwi/test/bun';
import { beautifyJson, streamRequest, testRequest } from '@riwi/test/request';
Expand All @@ -18,13 +19,20 @@ const assertSnapshot = (actual: string, relativePath: string): void => {
expect(actual).toBe(expected);
};

const expectStatusOk = (statusCode: number | undefined): void => {
expect(statusCode).toBe(200);
};

const requestSettleDelayMs = 75;

describe('pdftk', () => {
[currentArch()].forEach((arch) => {
describe(`arch: ${arch}`, () => {
const setup = useTestContainer({
image: `philiplehmann/pdftk:test-${arch}`,
containerPort,
env: { PDFTK_PROCESS_ENABLED: 'true' },
type: 'each',
});

describe('compress', async () => {
Expand All @@ -39,12 +47,38 @@ describe('pdftk', () => {
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
const size = await streamLength(response);
await wait(requestSettleDelayMs);

expect(stats.size).toBeGreaterThan(size);
});
});

describe.skip('compress parallel', async () => {
it('pdf file reduces in size', async () => {
const file = resolve(__dirname, 'assets/uncompressed.pdf');
const stats = statSync(file);
const responses = await Promise.all(
Array.from({ length: 20 }).map(() =>
streamRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/compress',
headers: { 'Content-Type': 'application/pdf' },
file,
}),
),
);
for (const response of responses) {
expectStatusOk(response.statusCode);
const size1 = await streamLength(response);
expect(stats.size > size1).toBeTruthy();
}
});
});

describe('uncompress', () => {
it('pdf file increases in size', async () => {
const file = resolve(__dirname, 'assets/compressed.pdf');
Expand All @@ -57,7 +91,9 @@ describe('pdftk', () => {
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
const size = await streamLength(response);
await wait(requestSettleDelayMs);

expect(stats.size).toBeLessThan(size);
});
Expand All @@ -66,58 +102,102 @@ describe('pdftk', () => {
describe('encrypt', () => {
it('pdf file is encrypted', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const encryptResponse = await streamRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/encrypt?password=1234',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(encryptResponse.statusCode);

expect(text).toInclude('/Encrypt');
const encryptedPdf = await streamToBuffer(encryptResponse);
await wait(requestSettleDelayMs);
const [decryptResponse, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/decrypt?password=1234',
headers: { 'Content-Type': 'application/pdf' },
body: encryptedPdf,
});
expectStatusOk(decryptResponse.statusCode);
await wait(requestSettleDelayMs);

expect(text.includes('/Encrypt')).toBeFalsy();
});
Comment on lines +115 to 129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Encryption tests no longer prove /encrypt actually encrypted the payload.

Right now, these can still pass if /encrypt returns plaintext and /decrypt behaves like a pass-through. Add an assertion on the encrypted buffer before decrypting.

Suggested test hardening
           const encryptedPdf = await streamToBuffer(encryptResponse);
+          expect(encryptedPdf.includes(Buffer.from('/Encrypt'))).toBeTruthy();
           const [decryptResponse, text] = await testRequest({
             method: 'POST',
             host: 'localhost',
             port: setup.port,
             path: '/decrypt?password=1234',

Apply the same assertion in all three encrypt scenarios.

Also applies to: 131-143, 157-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/pdftk/src/test/pdftk.spec.ts` around lines 105 - 117, The test currently
decrypts whatever /encrypt returned without verifying the encrypted payload was
actually transformed; before calling /decrypt assert that the encrypted buffer
(encryptedPdf from streamToBuffer(encryptResponse)) is different from the
original plaintext buffer and does not contain the original PDF plaintext marker
(e.g., assert Buffer.compare(encryptedPdf, originalPdfBuffer) !== 0 and
encryptedPdf does not include the original plaintext string), then proceed to
call testRequest for /decrypt; apply the same added assertion in the other two
encrypt scenarios that create encryptResponse/encryptedPdf so each encrypt test
proves the payload was altered by /encrypt.


it('pdf file is encrypted and has password', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const encryptResponse = await streamRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/encrypt?password=1234&userPassword=5678',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(encryptResponse.statusCode);

const encryptedPdf = await streamToBuffer(encryptResponse);
await wait(requestSettleDelayMs);
const [decryptResponse, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/decrypt?password=5678',
headers: { 'Content-Type': 'application/pdf' },
body: encryptedPdf,
});
expectStatusOk(decryptResponse.statusCode);
await wait(requestSettleDelayMs);

expect(text).toInclude('/Encrypt');
expect(text.includes('/Encrypt')).toBeFalsy();
});

it('pdf file is encrypted, has password and allow is defined', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const encryptResponse = await streamRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/encrypt?password=1234&userPassword=5678&allow=AllFeatures',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(encryptResponse.statusCode);

const encryptedPdf = await streamToBuffer(encryptResponse);
await wait(requestSettleDelayMs);
const [decryptResponse, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/decrypt?password=1234',
headers: { 'Content-Type': 'application/pdf' },
body: encryptedPdf,
});
expectStatusOk(decryptResponse.statusCode);
await wait(requestSettleDelayMs);

expect(text).toInclude('/Encrypt');
expect(text.includes('/Encrypt')).toBeFalsy();
});
});

describe('decrypt', () => {
it('pdf file is decrypted', async () => {
const file = resolve(__dirname, 'assets/encrypted.pdf');
const [, text] = await testRequest({
const [response, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/decrypt?password=1234',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
await wait(requestSettleDelayMs);

expect(text).not.toInclude('/Encrypt');
});
Expand All @@ -126,14 +206,16 @@ describe('pdftk', () => {
describe('dataFields', () => {
it('return pdf data fields', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const [response, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/data/fields',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
await wait(requestSettleDelayMs);

assertSnapshot(beautifyJson(text), './snapshots/dataFields.json');
});
Expand All @@ -142,14 +224,16 @@ describe('pdftk', () => {
describe('dataDump', () => {
it('return pdf data dump', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const [response, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/data/dump',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
await wait(requestSettleDelayMs);

assertSnapshot(beautifyJson(text), './snapshots/dataDump.json');
});
Expand All @@ -158,14 +242,16 @@ describe('pdftk', () => {
describe('dataFDF', () => {
it('return pdf generated fdf', async () => {
const file = resolve(__dirname, 'assets/form.pdf');
const [, text] = await testRequest({
const [response, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/data/fdf',
headers: { 'Content-Type': 'application/pdf' },
file,
});
expectStatusOk(response.statusCode);
await wait(requestSettleDelayMs);

assertSnapshot(text, './snapshots/dataFdf.fdf');
});
Expand Down Expand Up @@ -196,18 +282,21 @@ describe('pdftk', () => {
headers: { 'Content-Type': 'application/pdf' },
file,
});
expect(response.statusCode).toBe(200);
expectStatusOk(response.statusCode);

const pdf = await streamToBuffer(response);
await wait(requestSettleDelayMs);

const [, text] = await testRequest({
const [dataFieldsResponse, text] = await testRequest({
method: 'POST',
host: 'localhost',
port: setup.port,
path: '/data/fields',
headers: { 'Content-Type': 'application/pdf' },
body: pdf,
});
expectStatusOk(dataFieldsResponse.statusCode);
await wait(requestSettleDelayMs);
assertSnapshot(beautifyJson(text), './snapshots/formFill.json');
});
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 2 additions & 4 deletions apps/tesseract/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { imageToText } from '@riwi/binary/tesseract';
import { handleRouteError } from '@riwi/http/error';
import { connect, healthEndpoints, post, processEndpoints } from '@riwi/http/route';
import { httpServer } from '@riwi/http/server';

Expand All @@ -18,10 +19,7 @@ httpServer(
try {
await imageToText({ input: req, output: res });
} catch (error) {
if (!res.headersSent) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : 'tesseract failed');
}
handleRouteError(res, error, 'tesseract failed');
}
}),
...healthEndpoints,
Expand Down
Loading
Loading