Skip to content

Commit

Permalink
Close #334 - support fastify
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed May 5, 2023
1 parent eb090f8 commit f3840de
Show file tree
Hide file tree
Showing 49 changed files with 2,892 additions and 126 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"@nestjs/common": ">= 7.0.1",
"@nestjs/core": ">= 7.0.1",
"@nestjs/platform-express": ">= 7.0.1",
"@nestjs/platform-fastify": ">= 7.0.1",
"detect-ts-node": "^1.0.5",
"glob": "^7.2.0",
"raw-body": ">= 2.0.0",
"reflect-metadata": ">= 0.1.12",
"rxjs": ">= 6.0.0",
"typia": "^3.8.4"
Expand Down
33 changes: 17 additions & 16 deletions packages/core/src/decorators/EncryptedBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
createParamDecorator,
} from "@nestjs/common";
import type express from "express";
import raw from "raw-body";
import type { FastifyRequest } from "fastify";

import { assert, is, validate } from "typia";

Expand Down Expand Up @@ -44,10 +44,12 @@ export function EncryptedBody<T>(
_unknown: any,
ctx: ExecutionContext,
) {
const request: express.Request = ctx.switchToHttp().getRequest();
if (request.readable === false)
const request: express.Request | FastifyRequest = ctx
.switchToHttp()
.getRequest();
if (isTextPlain(request.headers["content-type"]) === false)
throw new BadRequestException(
"Request body is not the text/plain.",
`Request body type is not "text/plain".`,
);

const param:
Expand All @@ -66,22 +68,14 @@ export function EncryptedBody<T>(
const headers: Singleton<Record<string, string>> = new Singleton(() =>
headers_to_object(request.headers),
);
const body: string = (await raw(request, "utf8")).trim();
const body: string = request.body;
const password: IEncryptionPassword =
typeof param === "function"
? param({ headers: headers.get(), body }, false)
: param;
const disabled: boolean =
password.disabled === undefined
? false
: typeof password.disabled === "function"
? password.disabled({ headers: headers.get(), body }, true)
: password.disabled;

// PARSE AND VALIDATE DATA
const data: any = JSON.parse(
disabled ? body : decrypt(body, password.key, password.iv),
);
const data: any = JSON.parse(decrypt(body, password.key, password.iv));
checker(data);
return data;
})();
Expand All @@ -93,7 +87,7 @@ Object.assign(EncryptedBody, validate);
/**
* @internal
*/
function decrypt(body: string, key: string, iv: string): string {
const decrypt = (body: string, key: string, iv: string): string => {
try {
return AesPkcs5.decrypt(body, key, iv);
} catch (exp) {
Expand All @@ -103,4 +97,11 @@ function decrypt(body: string, key: string, iv: string): string {
);
else throw exp;
}
}
};

const isTextPlain = (text?: string): boolean =>
text !== undefined &&
text
.split(";")
.map((str) => str.trim())
.some((str) => str === "text/plain");
5 changes: 0 additions & 5 deletions packages/core/src/decorators/EncryptedController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ import { ENCRYPTION_METADATA_KEY } from "./internal/EncryptedConstant";
* encryption algorithm and password would be used by {@link EncryptedRoute} and
* {@link EncryptedBody} to encrypt the request and response body of the HTTP protocol.
*
* > However, if you've configure the {@link IEncryptionPassword.disabled} to be `true`,
* > you can disable the encryption and decryption algorithm. Therefore, when the
* > {@link IEncryptionPassword.disable} becomes the `true`, content like request and
* > response body would be considered as a plain text instead.
*
* By the way, you can configure the encryption password in the global level by using
* {@link EncryptedModule} instead of the {@link nest.Module} in the module level. In
* that case, you don't need to use this `EncryptedController` more. Just use the
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/decorators/EncryptedRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,24 +165,11 @@ class EncryptedRouteInterceptor implements NestInterceptor {
typeof param === "function"
? param({ headers: headers.get(), body }, false)
: param;
const disabled: boolean =
password.disabled === undefined
? false
: typeof password.disabled === "function"
? password.disabled(
{ headers: headers.get(), body },
false,
)
: password.disabled;

const response: express.Response = http.getResponse();
response.header(
"Content-Type",
disabled ? "application/json" : "text/plain",
);
response.header("Content-Type", "text/plain");

if (disabled === true) return body;
else if (body === undefined) return body;
if (body === undefined) return body;
return AesPkcs5.encrypt(body, password.key, password.iv);
}),
catchError((err) => route_error(http.getRequest(), err)),
Expand Down
24 changes: 17 additions & 7 deletions packages/core/src/decorators/PlainBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
createParamDecorator,
} from "@nestjs/common";
import type express from "express";
import raw from "raw-body";
import type { FastifyRequest } from "fastify";

/**
* Plain body decorator.
Expand All @@ -28,11 +28,21 @@ import raw from "raw-body";
* @author Jeongho Nam - https://github.com/samchon
*/
export const PlainBody: () => ParameterDecorator = createParamDecorator(
async function PlainBody(_data: any, context: ExecutionContext) {
const request: express.Request = context.switchToHttp().getRequest();
if (!request.readable) throw new BadRequestException("Invalid body");

const body: string = (await raw(request)).toString("utf8").trim();
return body;
async function PlainBody(_data: any, ctx: ExecutionContext) {
const request: express.Request | FastifyRequest = ctx
.switchToHttp()
.getRequest();
if (isTextPlain(request.headers["content-type"]) === false)
throw new BadRequestException(
`Request body type is not "text/plain".`,
);
return request.body;
},
);

const isTextPlain = (text?: string): boolean =>
text !== undefined &&
text
.split(";")
.map((str) => str.trim())
.some((str) => str === "text/plain");
29 changes: 20 additions & 9 deletions packages/core/src/decorators/TypedBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
createParamDecorator,
} from "@nestjs/common";
import type express from "express";
import raw from "raw-body";
import type { FastifyRequest } from "fastify";

import { assert, is, validate } from "typia";

Expand Down Expand Up @@ -33,20 +33,31 @@ export function TypedBody<T>(
_unknown: any,
context: ExecutionContext,
) {
const request: express.Request = context.switchToHttp().getRequest();
if (!request.is("application/json")) {
const request: express.Request | FastifyRequest = context
.switchToHttp()
.getRequest();
if (isApplicationJson(request.headers["content-type"]) === false)
throw new BadRequestException(
"Request body is not the application/json.",
`Request body type is not "application/json".`,
);

try {
checker(request.body);
} catch (exp) {
if (exp instanceof BadRequestException) console.log(exp);
throw exp;
}
const data: any = request.body
? request.body
: JSON.parse((await raw(request, "utf8")).trim());
checker(data);
return data;
return request.body;
})();
}

Object.assign(TypedBody, is);
Object.assign(TypedBody, assert);
Object.assign(TypedBody, validate);

const isApplicationJson = (text?: string): boolean =>
text !== undefined &&
text
.split(";")
.map((str) => str.trim())
.some((str) => str === "application/json");
2 changes: 1 addition & 1 deletion packages/fetcher/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/fetcher",
"version": "1.2.0",
"version": "1.2.1",
"description": "Fetcher library of Nestia SDK",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down
92 changes: 42 additions & 50 deletions packages/fetcher/src/Fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,35 @@ export class Fetcher {
...connection.headers,
"Content-Type": "application/json",
}
: connection.headers,
: connection.headers ?? {},
};
if (encrypted.request)
(init.headers as Record<string, string>)["Content-Type"] =
"text/plain";

// REQUEST BODY (WITH ENCRYPTION)
if (input !== undefined) {
let body: string = (stringify || JSON.stringify)(input);
if (encrypted.request === true) {
const headers: Singleton<Record<string, string>> =
new Singleton(() => init.headers as Record<string, string>);
if (input !== undefined)
init.body = (() => {
const json: string = (stringify ?? JSON.stringify)(input);
if (!encrypted.request) return json;

const password:
| IEncryptionPassword
| IEncryptionPassword.Closure =
connection.encryption instanceof Function
? connection.encryption!(
{ headers: headers.get(), body },
{
headers: init.headers as Record<
string,
string
>,
body: json,
},
true,
)
: connection.encryption!;
if (is_disabled(password, headers, body, true) === false)
body = AesPkcs5.encrypt(body, password.key, password.iv);
}
init.body = body;
}
return AesPkcs5.encrypt(json, password.key, password.iv);
})();

//----
// RESPONSE MESSAGE
Expand All @@ -125,8 +131,8 @@ export class Fetcher {

// DO FETCH
const response: Response = await (await polyfill.get())(url.href, init);
let body: string = await response.text();
if (!body) return undefined!;
const text: string = await response.text();
if (!text) return undefined!;

// CHECK THE STATUS CODE
if (
Expand All @@ -136,29 +142,33 @@ export class Fetcher {
response.status !== 200 &&
response.status !== 201)
)
throw new HttpError(method, path, response.status, body);

if (encrypted.response === true) {
// FINALIZATION (WITH DECODING)
const headers: Singleton<Record<string, string>> = new Singleton(
() => headers_to_object(response.headers),
);
const password: IEncryptionPassword | IEncryptionPassword.Closure =
connection.encryption instanceof Function
? connection.encryption!(
{ headers: headers.get(), body },
false,
)
: connection.encryption!;
if (is_disabled(password, headers, body, false) === false)
body = AesPkcs5.decrypt(body, password.key, password.iv);
}
throw new HttpError(method, path, response.status, text);

const content: string = !encrypted.response
? text
: (() => {
const password:
| IEncryptionPassword
| IEncryptionPassword.Closure =
connection.encryption instanceof Function
? connection.encryption!(
{
headers: headers_to_object(
response.headers,
),
body: text,
},
false,
)
: connection.encryption!;
return AesPkcs5.decrypt(text, password.key, password.iv);
})();

//----
// OUTPUT
//----
let ret: { __set_headers__: Record<string, any> } & Primitive<Output> =
body as any;
content as any;
try {
// PARSE RESPONSE BODY
ret = JSON.parse(ret as any);
Expand Down Expand Up @@ -227,24 +237,6 @@ const polyfill = new Singleton(async (): Promise<typeof fetch> => {
return window.fetch;
});

function is_disabled(
password: IEncryptionPassword,
headers: Singleton<Record<string, string>>,
body: string,
encoded: boolean,
): boolean {
if (password.disabled === undefined) return false;
if (typeof password.disabled === "function")
return password.disabled(
{
headers: headers.get(),
body,
},
encoded,
);
return password.disabled;
}

function headers_to_object(headers: Headers): Record<string, string> {
const output: Record<string, string> = {};
headers.forEach((value, key) => (output[key] = value));
Expand Down
16 changes: 0 additions & 16 deletions packages/fetcher/src/IEncryptionPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,6 @@ export interface IEncryptionPassword {
* Initialization vector.
*/
iv: string;

/**
* Disable encryption to let content as plain.
*
* When you configure this `disabled` variable to be `false`, encryption and decryption
* algorithm would be disabled. Therefore, content like request or response body
* would be considered as a plain text instead.
*
* Default is `false`.
*/
disabled?:
| boolean
| ((
param: IEncryptionPassword.IParameter,
encoded: boolean,
) => boolean);
}
export namespace IEncryptionPassword {
/**
Expand Down
Loading

0 comments on commit f3840de

Please sign in to comment.