fix(core): update

This commit is contained in:
Philipp Kunz 2023-03-30 15:15:48 +02:00
parent 2372e36367
commit 87a5337db3
26 changed files with 1080 additions and 71 deletions

View File

@ -40,17 +40,29 @@
"@apiglobal/typedrequest": "^2.0.12",
"@apiglobal/typedrequest-interfaces": "^2.0.1",
"@apiglobal/typedsocket": "^2.0.23",
"@pushrocks/lik": "^6.0.2",
"@pushrocks/smartchok": "^1.0.23",
"@pushrocks/smartdelay": "^2.0.13",
"@pushrocks/smartexpress": "^4.0.34",
"@pushrocks/smartenv": "^5.0.5",
"@pushrocks/smartfeed": "^1.0.11",
"@pushrocks/smartfile": "^10.0.7",
"@pushrocks/smartlog": "^3.0.1",
"@pushrocks/smartlog-destination-devtools": "^1.0.10",
"@pushrocks/smartmanifest": "^1.0.8",
"@pushrocks/smartmime": "^1.0.5",
"@pushrocks/smartopen": "^2.0.0",
"@pushrocks/smartpath": "^5.0.5",
"@pushrocks/smartpromise": "^3.1.7",
"@pushrocks/smartrequest": "^2.0.11",
"@pushrocks/smartrx": "^3.0.0",
"@pushrocks/smartsitemap": "^2.0.1",
"@pushrocks/smarttime": "^4.0.1",
"@pushrocks/webstore": "^2.0.5",
"@tsclass/tsclass": "^4.0.34",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-force-ssl": "^0.3.2",
"lit": "^2.7.0"
},
"devDependencies": {

83
pnpm-lock.yaml generated
View File

@ -10,15 +10,21 @@ dependencies:
'@apiglobal/typedsocket':
specifier: ^2.0.23
version: 2.0.23
'@pushrocks/lik':
specifier: ^6.0.2
version: 6.0.2
'@pushrocks/smartchok':
specifier: ^1.0.23
version: 1.0.23
'@pushrocks/smartdelay':
specifier: ^2.0.13
version: 2.0.13
'@pushrocks/smartexpress':
specifier: ^4.0.34
version: 4.0.34
'@pushrocks/smartenv':
specifier: ^5.0.5
version: 5.0.5
'@pushrocks/smartfeed':
specifier: ^1.0.11
version: 1.0.11
'@pushrocks/smartfile':
specifier: ^10.0.7
version: 10.0.7
@ -28,6 +34,12 @@ dependencies:
'@pushrocks/smartlog-destination-devtools':
specifier: ^1.0.10
version: 1.0.10
'@pushrocks/smartmanifest':
specifier: ^1.0.8
version: 1.0.8
'@pushrocks/smartmime':
specifier: ^1.0.5
version: 1.0.5
'@pushrocks/smartopen':
specifier: ^2.0.0
version: 2.0.0
@ -37,12 +49,36 @@ dependencies:
'@pushrocks/smartpromise':
specifier: ^3.1.7
version: 3.1.7
'@pushrocks/smartrequest':
specifier: ^2.0.11
version: 2.0.11
'@pushrocks/smartrx':
specifier: ^3.0.0
version: 3.0.0
'@pushrocks/smartsitemap':
specifier: ^2.0.1
version: 2.0.1
'@pushrocks/smarttime':
specifier: ^4.0.1
version: 4.0.1
'@pushrocks/webstore':
specifier: ^2.0.5
version: 2.0.5
'@tsclass/tsclass':
specifier: ^4.0.34
version: 4.0.34
body-parser:
specifier: ^1.20.2
version: 1.20.2
cors:
specifier: ^2.8.5
version: 2.8.5
express:
specifier: ^4.18.2
version: 4.18.2
express-force-ssl:
specifier: ^0.3.2
version: 0.3.2
lit:
specifier: ^2.7.0
version: 2.7.0
@ -402,18 +438,6 @@ packages:
'@types/minimatch': 3.0.5
symbol-tree: 3.2.4
/@pushrocks/lik@6.0.0:
resolution: {integrity: sha512-li2kLNVdhNxSP7N9Opun2iPdZZkVLaVZFRNek/G//r6qOuleFIK+TqjTTS6YYvUrFxAgJ4/vB5uPXbza1i8iBQ==}
dependencies:
'@pushrocks/smartdelay': 2.0.13
'@pushrocks/smartmatch': 1.0.7
'@pushrocks/smartpromise': 3.1.7
'@pushrocks/smartrx': 2.0.27
'@pushrocks/smarttime': 3.0.50
'@types/minimatch': 3.0.5
'@types/symbol-tree': 3.2.2
symbol-tree: 3.2.4
/@pushrocks/lik@6.0.2:
resolution: {integrity: sha512-jO85PCb4gULfZbLoVpXb9HIR9Wgoigq6Zjcp1JqHOgM4KB38IZrU+HPWPWWMErAOOQmmYvVCdl4gkrkO/Rzn4w==}
dependencies:
@ -462,7 +486,7 @@ packages:
/@pushrocks/smartcli@4.0.6:
resolution: {integrity: sha512-nv2Ldy+jTRsVpGpOz+9o0F8FMELoWYk/sy5ecyh9AsP97Kdj3CtqwRwHhcl7mLepdrcRw1qHK3DAloln1XP4Vg==}
dependencies:
'@pushrocks/lik': 6.0.0
'@pushrocks/lik': 6.0.2
'@pushrocks/smartlog': 3.0.2
'@pushrocks/smartparam': 1.1.6
'@pushrocks/smartpromise': 3.1.7
@ -528,7 +552,9 @@ packages:
express: 4.18.2
express-force-ssl: 0.3.2
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
/@pushrocks/smartfeed@1.0.11:
resolution: {integrity: sha512-PcsiQ4tkwTpGxOdLiEpAR5vfFpn8Utnlind4mmX+FLIZVuuONaApefWMvaYv5ysmfnWQuCE2qkFq1J5ulDcBbQ==}
@ -543,7 +569,7 @@ packages:
/@pushrocks/smartfile@10.0.7:
resolution: {integrity: sha512-ZjMkHLjiKaHFy5bz2k+0bLNr3S0Ef6EU65vuZuq8MbhJQW/xhBUWZWT/sKNSkPiXVCWI+vpHOA6j1G3qCnLspg==}
dependencies:
'@pushrocks/lik': 6.0.0
'@pushrocks/lik': 6.0.2
'@pushrocks/smartdelay': 2.0.13
'@pushrocks/smartfile-interfaces': 1.0.7
'@pushrocks/smarthash': 3.0.2
@ -686,7 +712,7 @@ packages:
'@pushrocks/smartpromise': 3.1.7
'@pushrocks/smartpuppeteer': 2.0.2
'@pushrocks/smartunique': 3.0.3
'@tsclass/tsclass': 4.0.28
'@tsclass/tsclass': 4.0.34
'@types/express': 4.17.15
express: 4.18.2
pdf-merger-js: 3.4.0
@ -944,12 +970,6 @@ packages:
dependencies:
type-fest: 2.19.0
/@tsclass/tsclass@4.0.28:
resolution: {integrity: sha512-69OUcb9hR1PYTCHwxsyJhlE7jUJKYZMl4tQWagN7Kmv8gIWRvjfPecNQTUSmSYx6N99z4l+0E/WJ6h18EiDJWg==}
dependencies:
type-fest: 3.7.1
dev: true
/@tsclass/tsclass@4.0.34:
resolution: {integrity: sha512-Fk4y/cKfzAjq+9HcsR/CRvWDn7ERrKxd75oPVASrfjECyA/Mf7zDKbPfLwZyAq4zk4abkg1RydfNjQWRLXHdTA==}
dependencies:
@ -1629,7 +1649,7 @@ packages:
engines: {node: '>=14.16'}
dependencies:
get-stream: 6.0.1
http-cache-semantics: 4.1.0
http-cache-semantics: 4.1.1
keyv: 4.5.2
mimic-response: 4.0.0
normalize-url: 8.0.0
@ -1722,7 +1742,7 @@ packages:
dependencies:
inflation: 2.0.0
qs: 6.11.0
raw-body: 2.5.1
raw-body: 2.5.2
type-is: 1.6.18
dev: true
@ -1771,11 +1791,6 @@ packages:
dependencies:
safe-buffer: 5.2.1
/content-type@1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: true
/content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
@ -2637,10 +2652,6 @@ packages:
http-errors: 1.8.1
dev: true
/http-cache-semantics@4.1.0:
resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
dev: true
/http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
@ -2968,7 +2979,7 @@ packages:
accepts: 1.3.8
cache-content-type: 1.0.1
content-disposition: 0.5.4
content-type: 1.0.4
content-type: 1.0.5
cookies: 0.8.0
debug: 4.3.4
delegates: 1.0.0

View File

@ -10,6 +10,7 @@ tap.test('should create a valid instance of TypedServer', async () => {
port: 3000,
serveDir: smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
watch: true,
cors: true,
});
expect(testTypedServer).toBeInstanceOf(TypedServer);
});

127
test/test.server.ts Normal file
View File

@ -0,0 +1,127 @@
// tslint:disable-next-line:no-implicit-dependencies
import { expect, tap } from '@pushrocks/tapbundle';
// helper dependencies
// tslint:disable-next-line:no-implicit-dependencies
import * as smartpath from '@pushrocks/smartpath';
import * as smartrequest from '@pushrocks/smartrequest';
import * as typedserver from '../ts/index.js';
let testServer: typedserver.servertools.Server;
let testRoute: typedserver.servertools.Route;
let testRoute2: typedserver.servertools.Route;
let testHandler: typedserver.servertools.Handler;
// =================
// Test class Server
// =================
tap.test('should create a valid Server', async () => {
testServer = new typedserver.servertools.Server({
cors: true,
domain: 'testing.git.zone',
forceSsl: false,
appVersion: 'v3.2.1',
manifest: {
name: 'Test App',
short_name: 'testapp',
},
feed: true,
sitemap: true,
robots: true,
});
expect(testServer).toBeInstanceOf(typedserver.servertools.Server);
});
// ================
// Test class Route
// ================
tap.test('should create a valid Route', async () => {
testRoute = testServer.addRoute('/someroute');
testRoute2 = testServer.addRoute('/someroute/*');
expect(testRoute).toBeInstanceOf(typedserver.servertools.Route);
});
// ==================
// Test class Handler
// ==================
tap.test('should produce a valid handler', async () => {
testHandler = new typedserver.servertools.Handler('POST', (request, response) => {
console.log('request body is:');
console.log(request.body);
response.send('hi');
});
expect(testHandler).toBeInstanceOf(typedserver.servertools.Handler);
});
tap.test('should add handler to route', async () => {
testRoute.addHandler(testHandler);
});
tap.test('should create a valid StaticHandler', async () => {
testRoute2.addHandler(
new typedserver.servertools.HandlerStatic(smartpath.get.dirnameFromImportMetaUrl(import.meta.url))
);
});
tap.test('should add typedrequest and typedsocket', async () => {
const typedrequest = await import('@apiglobal/typedrequest');
const typedrouter = new typedrequest.TypedRouter();
testServer.addTypedRequest(typedrouter);
testServer.addTypedSocket(typedrouter);
});
// =====================
// start the server and test the configuration
// =====================
tap.test('should start the server allright', async () => {
await testServer.start(3000);
console.log('Yay Test Start successfull!');
});
// see if a demo request holds up
tap.test('should issue a request', async (tools) => {
const response = await smartrequest.postJson('http://localhost:3000/someroute', {
headers: {
'X-Forwarded-Proto': 'https',
},
requestBody: {
someprop: 'hi',
},
});
console.log(response.body);
});
tap.test('should get a file from disk', async () => {
const response = await fetch('http://localhost:3000/someroute/testresponse.js');
console.log(response.status);
console.log(response.headers);
});
tap.test('should answer a preflight request', async () => {
const response = await fetch('http://localhost:3000/some/randompath/', {
method: 'OPTIONS',
});
console.log(response.headers);
});
tap.test('should exposer a sitemap', async () => {
const response = await fetch('http://localhost:3000/sitemap');
console.log(await response.text());
});
// ========
// clean up
// ========
tap.test('should stop the server', async () => {
await testServer.stop();
});
tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiglobal/typedserver',
version: '2.0.37',
version: '2.0.38',
description: 'easy serving of static files'
}

View File

@ -1 +1,12 @@
import * as plugins from './typedserver.plugins.js';
import * as servertools from './servertools/index.js';
export {
servertools
}
export * from './typedserver.classes.typedserver.js';
// Type helpers
export type Request = plugins.express.Request;
export type Response = plugins.express.Response;

View File

@ -1,12 +1,3 @@
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
export interface IReq_PushLatestServerChangeTime extends typedrequestInterfaces.implementsTR<
typedrequestInterfaces.ITypedRequest,
IReq_PushLatestServerChangeTime
> {
method: 'pushLatestServerChangeTime',
request: {
time: number;
};
response: {}
}
export * from './requestmodifier.js';
export * from './responsemodifier.js';
export * from './typedrequests.js';

View File

@ -0,0 +1,11 @@
export type TRequestModifier = <T>(responseArg: {
headers: { [header: string]: string | string[] | undefined };
path: string;
body: string;
travelData?: T;
}) => Promise<{
headers: { [header: string]: string | string[] | undefined };
path: string;
body: string;
travelData?: T;
}>;

View File

@ -0,0 +1,11 @@
export type TResponseModifier = <T>(responseArg: {
headers: { [header: string]: number | string | string[] | undefined };
path: string;
responseContent: string;
travelData?: T;
}) => Promise<{
headers: { [header: string]: number | string | string[] | undefined };
path: string;
responseContent: string;
travelData?: T;
}>;

View File

@ -0,0 +1,12 @@
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
export interface IReq_PushLatestServerChangeTime extends typedrequestInterfaces.implementsTR<
typedrequestInterfaces.ITypedRequest,
IReq_PushLatestServerChangeTime
> {
method: 'pushLatestServerChangeTime',
request: {
time: number;
};
response: {}
}

View File

@ -0,0 +1,35 @@
import { Handler } from './classes.handler.js';
import { Server } from './classes.server.js';
import * as plugins from '../typedserver.plugins.js';
export class Feed {
public smartexpressRef: Server;
public smartfeedInstance = new plugins.smartfeed.Smartfeed();
public feedHandler = new Handler('GET', async (req, res) => {
if (!this.smartexpressRef.options.feedMetadata) {
res.status(500);
res.write('feed metadata is missing');
res.end();
return;
}
if (!this.smartexpressRef.options.articleGetterFunction) {
res.status(500);
res.write('no article getter function defined.');
res.end();
return;
}
const xmlString = await this.smartfeedInstance.createFeedFromArticleArray(
this.smartexpressRef.options.feedMetadata,
await this.smartexpressRef.options.articleGetterFunction()
);
res.type('.xml');
res.write(xmlString);
res.end();
});
constructor(smartexpressRefArg: Server) {
this.smartexpressRef = smartexpressRefArg;
this.smartexpressRef.addRouteBefore('/feed', this.feedHandler);
}
}

View File

@ -0,0 +1,17 @@
import * as plugins from '../typedserver.plugins.js';
import { Request, Response } from 'express';
export interface IHandlerFunction {
(requestArg: Request, responseArg: Response): void;
}
export type THttpMethods = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE';
export class Handler {
httpMethod: THttpMethods;
handlerFunction: IHandlerFunction;
constructor(httpMethodArg: THttpMethods, handlerArg: IHandlerFunction) {
this.httpMethod = httpMethodArg;
this.handlerFunction = handlerArg;
}
}

View File

@ -0,0 +1,77 @@
import * as plugins from '../typedserver.plugins.js';
import { Handler } from './classes.handler.js';
import * as interfaces from '../interfaces/index.js';
export class HandlerProxy extends Handler {
/**
* The constuctor of HandlerProxy
* @param remoteMountPointArg
*/
constructor(
remoteMountPointArg: string,
optionsArg?: {
responseModifier?: interfaces.TResponseModifier;
headers?: { [key: string]: string };
}
) {
super('ALL', async (req, res) => {
const relativeRequestPath = req.path.slice(req.route.path.length - 1);
const proxyRequestUrl = remoteMountPointArg + relativeRequestPath;
console.log(`proxy ${req.path} to ${proxyRequestUrl}`);
let proxiedResponse: plugins.smartrequest.IExtendedIncomingMessage;
try {
proxiedResponse = await plugins.smartrequest.request(proxyRequestUrl, {
method: req.method,
autoJsonParse: false,
});
} catch {
res.end('failed to fullfill request');
return;
}
for (const header of Object.keys(proxiedResponse.headers)) {
res.set(header, proxiedResponse.headers[header] as string);
}
// set additional headers
if (optionsArg && optionsArg.headers) {
for (const key of Object.keys(optionsArg.headers)) {
res.set(key, optionsArg.headers[key]);
}
}
let responseToSend: string = proxiedResponse.body;
if (typeof responseToSend !== 'string') {
console.log(proxyRequestUrl);
console.log(responseToSend);
throw new Error(`Proxied response is not a string, but ${typeof responseToSend}`);
}
if (optionsArg && optionsArg.responseModifier) {
const modifiedResponse = await optionsArg.responseModifier({
headers: res.getHeaders(),
path: req.path,
responseContent: responseToSend,
});
// headers
for (const key of Object.keys(res.getHeaders())) {
if (!modifiedResponse.headers[key]) {
res.removeHeader(key);
}
}
for (const key of Object.keys(modifiedResponse.headers)) {
res.setHeader(key, modifiedResponse.headers[key]);
}
// responseContent
responseToSend = modifiedResponse.responseContent;
}
res.status(200);
res.write(responseToSend);
res.end();
});
}
}

View File

@ -0,0 +1,126 @@
import * as plugins from '../typedserver.plugins.js';
import * as interfaces from '../interfaces/index.js';
import { Handler } from './classes.handler.js';
export class HandlerStatic extends Handler {
constructor(
pathArg: string,
optionsArg?: {
requestModifier?: interfaces.TRequestModifier;
responseModifier?: interfaces.TResponseModifier;
headers?: { [key: string]: string };
serveIndexHtmlDefault?: boolean;
}
) {
super('GET', async (req, res) => {
let requestPath = req.path;
let requestHeaders = req.headers;
let requestBody = req.body;
let travelData: unknown;
if (optionsArg && optionsArg.requestModifier) {
const modifiedRequest = await optionsArg.requestModifier({
headers: requestHeaders,
path: requestPath,
body: requestBody,
});
requestHeaders = modifiedRequest.headers;
requestPath = modifiedRequest.path;
requestBody = modifiedRequest.body;
travelData = modifiedRequest.travelData;
}
// lets compute some paths
let filePath: string = requestPath.slice(req.route.path.length - 1); // lets slice of the root
if (requestPath === '') {
console.log('replaced root with index.html');
filePath = 'index.html';
}
console.log(filePath);
const joinedPath = plugins.path.join(pathArg, filePath);
const defaultPath = plugins.path.join(pathArg, 'index.html');
let parsedPath = plugins.path.parse(joinedPath);
let usedPath: string;
// important security checks
if (
requestPath.includes('..') || // don't allow going up the filePath
requestPath.includes('~') || // don't allow referencing of home directory
!joinedPath.startsWith(pathArg) // make sure the joined path is within the directory
) {
res.writeHead(500);
res.end();
return;
}
// set additional headers
if (optionsArg && optionsArg.headers) {
for (const key of Object.keys(optionsArg.headers)) {
res.set(key, optionsArg.headers[key]);
}
}
// lets actually care about serving, if security checks pass
let fileString: string;
let fileEncoding: 'binary' | 'utf8';
try {
fileString = plugins.smartfile.fs.toStringSync(joinedPath);
fileEncoding = plugins.smartmime.getEncoding(joinedPath);
usedPath = joinedPath;
} catch (err) {
// try serving index.html instead
console.log(`could not resolve ${joinedPath}`);
if (optionsArg && optionsArg.serveIndexHtmlDefault) {
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
try {
parsedPath = plugins.path.parse(defaultPath);
fileString = plugins.smartfile.fs.toStringSync(defaultPath);
fileEncoding = plugins.smartmime.getEncoding(defaultPath);
usedPath = defaultPath;
} catch (err) {
res.writeHead(500);
res.end('File not found!');
return;
}
} else {
res.writeHead(500);
res.end('File not found!');
return;
}
}
res.type(parsedPath.ext);
const headers = res.getHeaders();
// lets modify the response at last
if (optionsArg && optionsArg.responseModifier) {
const modifiedResponse = await optionsArg.responseModifier({
headers: res.getHeaders(),
path: usedPath,
responseContent: fileString,
travelData,
});
// headers
for (const key of Object.keys(res.getHeaders())) {
if (!modifiedResponse.headers[key]) {
res.removeHeader(key);
}
}
for (const key of Object.keys(modifiedResponse.headers)) {
res.setHeader(key, modifiedResponse.headers[key]);
}
// responseContent
fileString = modifiedResponse.responseContent;
}
res.status(200);
res.write(Buffer.from(fileString, fileEncoding));
res.end();
});
}
}

View File

@ -0,0 +1,17 @@
import * as plugins from '../typedserver.plugins.js';
import { Handler } from './classes.handler.js';
import * as interfaces from '../interfaces/index.js';
export class HandlerTypedRouter extends Handler {
/**
* The constuctor of HandlerProxy
* @param remoteMountPointArg
*/
constructor(typedrouter: plugins.typedrequest.TypedRouter) {
super('POST', async (req, res) => {
const response = await typedrouter.routeAndAddResponse(req.body);
res.json(response);
});
}
}

View File

@ -0,0 +1,32 @@
import * as plugins from '../typedserver.plugins.js';
import { Handler } from './classes.handler.js';
import { Server } from './classes.server.js';
import { ObjectMap } from '@pushrocks/lik';
import { IRoute as IExpressRoute } from 'express';
export class Route {
public routeString: string;
public handlerObjectMap = new ObjectMap<Handler>();
public expressMiddlewareObjectMap = new ObjectMap<any>();
public expressRoute: IExpressRoute; // will be set to server route on server start
constructor(ServerArg: Server, routeStringArg: string) {
this.routeString = routeStringArg;
}
/**
* add a handler to do something with requests
* @param handlerArg
*/
public addHandler(handlerArg: Handler) {
this.handlerObjectMap.add(handlerArg);
}
/**
* add a express middleware
* @param middlewareArg
*/
public addExpressMiddleWare(middlewareArg: plugins.express.Application) {
this.expressMiddlewareObjectMap.add(middlewareArg);
}
}

View File

@ -0,0 +1,296 @@
import * as plugins from '../typedserver.plugins.js';
import { Route } from './classes.route.js';
import { Handler } from './classes.handler.js';
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
// export types
import { setupRobots } from './tools.robots.js';
import { setupManifest } from './tools.manifest.js';
import { Sitemap } from './classes.sitemap.js';
import { Feed } from './classes.feed.js';
import { IServerOptions } from '../typedserver.classes.typedserver.js'
export type TServerStatus = 'initiated' | 'running' | 'stopped';
/**
* can be used to spawn a server to answer http/https calls
* for constructor options see [[IServerOptions]]
*/
export class Server {
public httpServer: plugins.http.Server | plugins.https.Server;
public expressAppInstance: plugins.express.Application;
public routeObjectMap = new Array<Route>();
public options: IServerOptions;
public serverStatus: TServerStatus = 'initiated';
public feed: Feed;
public sitemap: Sitemap;
public executeAfterStartFunctions: (() => Promise<void>)[] = [];
// do stuff when server is ready
private startedDeferred = plugins.smartpromise.defer();
// tslint:disable-next-line:member-ordering
public startedPromise = this.startedDeferred.promise;
private socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
constructor(optionsArg: IServerOptions) {
this.options = {
...optionsArg,
};
}
/**
* allows updating of server options
* @param optionsArg
*/
public updateServerOptions(optionsArg: IServerOptions) {
Object.assign(this.options, optionsArg);
}
public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) {
this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
}
public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
this.executeAfterStartFunctions.push(async () => {
plugins.typedsocket.TypedSocket.createServer(typedrouter, this);
});
}
public addRoute(routeStringArg: string, handlerArg?: Handler) {
const route = new Route(this, routeStringArg);
if (handlerArg) {
route.addHandler(handlerArg);
}
this.routeObjectMap.push(route);
return route;
}
public addRouteBefore(routeStringArg: string, handlerArg?: Handler) {
const route = new Route(this, routeStringArg);
if (handlerArg) {
route.addHandler(handlerArg);
}
this.routeObjectMap.unshift(route);
return route;
}
public async start(portArg: number | string = this.options.port, doListen = true) {
const done = plugins.smartpromise.defer();
if (typeof portArg === 'string') {
portArg = parseInt(portArg);
}
this.expressAppInstance = plugins.express();
if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) {
console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy');
this.httpServer = plugins.http.createServer(this.expressAppInstance);
} else if (!this.httpServer) {
console.log('Got SSL certificate. Using it for the http server');
this.httpServer = plugins.https.createServer(
{
key: this.options.privateKey,
cert: this.options.publicKey,
},
this.expressAppInstance
);
} else {
console.log('Using externally supplied http server');
}
this.httpServer.keepAliveTimeout = 600 * 1000;
this.httpServer.headersTimeout = 600 * 1000;
// general request handlling
this.expressAppInstance.use((req, res, next) => {
req.on('error', () => {
req.destroy();
});
req.on('timeout', () => {
req.destroy();
});
next();
});
// forceSsl
if (this.options.forceSsl) {
this.expressAppInstance.set('forceSSLOptions', {
enable301Redirects: true,
trustXFPHeader: true,
sslRequiredMessage: 'SSL Required.',
});
this.expressAppInstance.use(plugins.expressForceSsl);
}
// cors
if (this.options.cors) {
const cors = plugins.cors({
allowedHeaders: '*',
methods: '*',
origin: '*',
});
this.expressAppInstance.use(cors);
this.expressAppInstance.options('/*', cors);
}
this.expressAppInstance.use((req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'unsafe-none');
res.setHeader('Access-Control-Allow-Origin', '*');
next();
});
// body parsing
this.expressAppInstance.use(plugins.bodyParser.json({ limit: 100000000 })); // for parsing application/json
this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// robots
if (this.options.robots && this.options.domain) {
await setupRobots(this, this.options.domain);
}
// manifest.json
if (this.options.manifest) {
await setupManifest(this.expressAppInstance, this.options.manifest);
}
// sitemaps
if (this.options.sitemap) {
this.sitemap = new Sitemap(this);
}
if (this.options.feed) {
// feed
this.feed = new Feed(this);
}
// appVersion
if (this.options.appVersion) {
this.expressAppInstance.use((req, res, next) => {
res.set('appversion', this.options.appVersion);
next();
});
this.addRoute(
'/appversion',
new Handler('GET', async (req, res) => {
res.write(this.options.appVersion);
res.end();
})
);
}
// set up routes in for express
await this.routeObjectMap.forEach(async (routeArg) => {
console.log(
`"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers`
);
const expressRoute = this.expressAppInstance.route(routeArg.routeString);
routeArg.handlerObjectMap.forEach(async (handler) => {
console.log(` -> ${handler.httpMethod}`);
switch (handler.httpMethod) {
case 'GET':
expressRoute.get(handler.handlerFunction);
return;
case 'POST':
expressRoute.post(handler.handlerFunction);
return;
case 'PUT':
expressRoute.put(handler.handlerFunction);
return;
case 'ALL':
expressRoute.all(handler.handlerFunction);
return;
case 'DELETE':
expressRoute.delete(handler.handlerFunction);
return;
default:
return;
}
});
});
if (this.options.defaultAnswer) {
this.expressAppInstance.get('/', async (request, response) => {
response.send(await this.options.defaultAnswer());
});
}
this.httpServer.on('connection', (connection: plugins.net.Socket) => {
this.socketMap.add(connection);
console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`);
const cleanupConnection = () => {
if (this.socketMap.checkForObject(connection)) {
this.socketMap.remove(connection);
console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
connection.destroy();
}
};
connection.on('close', () => {
cleanupConnection();
});
connection.on('error', () => {
cleanupConnection();
});
connection.on('end', () => {
cleanupConnection();
});
connection.on('timeout', () => {
cleanupConnection();
});
});
// finally listen on a port
if (doListen) {
this.httpServer.listen(portArg, '0.0.0.0', () => {
console.log(`now listening on ${portArg}!`);
this.startedDeferred.resolve();
this.serverStatus = 'running';
done.resolve();
});
} else {
console.log(
'The server does not listen on a network stack and instead expects to get handed requests by other mechanics'
);
}
await done.promise;
for (const executeAfterStartFunction of this.executeAfterStartFunctions) {
await executeAfterStartFunction();
}
}
public getHttpServer() {
return this.httpServer;
}
public getExpressAppInstance() {
return this.expressAppInstance;
}
public async stop() {
const done = plugins.smartpromise.defer();
if (this.httpServer) {
this.httpServer.close(async () => {
this.serverStatus = 'stopped';
done.resolve();
});
await this.socketMap.forEach(async (socket) => {
socket.destroy();
});
} else {
throw new Error('There is no Server to be stopped. Have you started it?');
}
return await done.promise;
}
/**
* allows handling requests and responses that come from other
* @param req
* @param res
*/
public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) {
this.expressAppInstance(req, res);
}
}

View File

@ -0,0 +1,68 @@
import { Server } from './classes.server.js';
import { Handler } from './classes.handler.js';
import * as plugins from '../typedserver.plugins.js';
import { IUrlInfo } from '@pushrocks/smartsitemap';
export class Sitemap {
public smartexpressRef: Server;
public smartSitemap = new plugins.smartsitemap.SmartSitemap();
public urls: plugins.smartsitemap.IUrlInfo[] = [];
/**
* handles the normal sitemap request
*/
public sitemapHandler = new Handler('GET', async (req, res) => {
const sitemapXmlString = await this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
res.type('.xml');
res.write(sitemapXmlString);
res.end();
});
/**
* handles the sitemap-news request
*/
public sitemapNewsHandler = new Handler('GET', async (req, res) => {
if (!this.smartexpressRef.options.articleGetterFunction) {
res.status(500);
res.write('no article getter function defined.');
res.end();
return;
}
const sitemapNewsXml = await this.smartSitemap.createSitemapNewsFromArticleArray(
await this.smartexpressRef.options.articleGetterFunction()
);
res.type('.xml');
res.write(sitemapNewsXml);
res.end();
});
constructor(smartexpressRefArg: Server) {
this.smartexpressRef = smartexpressRefArg;
this.smartexpressRef.addRouteBefore('/sitemap', this.sitemapHandler);
this.smartexpressRef.addRouteBefore('/sitemap-news', this.sitemapNewsHandler);
// lets set the default url
if (this.smartexpressRef.options.domain) {
this.urls.push({
url: `https://${this.smartexpressRef.options.domain}/`,
timestamp: Date.now(),
frequency: 'daily',
});
}
}
/**
* replaces the current urlsArray
* @param urlsArg
*/
public replaceUrls(urlsArg: IUrlInfo[]) {
this.urls = urlsArg;
}
/**
* adds urls to the current set of urls
*/
public addUrls(urlsArg: IUrlInfo[]) {
this.urls = this.urls.concat(this.urls, urlsArg);
}
}

6
ts/servertools/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './classes.server.js';
export * from './classes.route.js';
export * from './classes.handler.js';
export * from './classes.handlerstatic.js';
export * from './classes.handlerproxy.js';
export * from './classes.handlertypedrouter.js';

View File

@ -0,0 +1,14 @@
import * as plugins from '../typedserver.plugins.js';
export const setupManifest = async (
expressInstanceArg: plugins.express.Application,
manifestArg: plugins.smartmanifest.ISmartManifestConstructorOptions
) => {
const smartmanifestInstance = new plugins.smartmanifest.SmartManifest(manifestArg);
expressInstanceArg.get('/manifest.json', async (req, res) => {
res.status(200);
res.type('json');
res.write(smartmanifestInstance.jsonString());
res.end();
});
};

View File

@ -0,0 +1,33 @@
import * as plugins from '../typedserver.plugins.js';
import { Server } from './classes.server.js';
import { Handler } from './classes.handler.js';
export const setupRobots = async (smartexpressRefArg: Server, domainArg: string) => {
smartexpressRefArg.addRouteBefore(
'/robots.txt',
new Handler('GET', async (req, res) => {
res.type('text/plain');
res.send(`
User-agent: Googlebot-News
Disallow: /account
Disallow: /login
User-agent: *
Disallow: /account
Disallow: /login
${
smartexpressRefArg.options.blockWaybackMachine
? `
User-Agent: ia_archiver
Disallow: /
`
: ``
}
Sitemap: https://${domainArg}/sitemap
Sitemap: https://${domainArg}/sitemap-news
`);
})
);
};

View File

@ -0,0 +1,22 @@
import * as plugins from '../typedserver.plugins.js';
import { Server } from './classes.server.js';
import { Handler } from './classes.handler.js';
export const redirectFrom80To443 = async () => {
const smartexpressInstance = new Server({
cors: true,
forceSsl: true,
port: 80,
});
smartexpressInstance.addRoute(
'*',
new Handler('ALL', async (req, res) => {
res.redirect('https://' + req.headers.host + req.url);
})
);
await smartexpressInstance.start();
return smartexpressInstance;
};

View File

@ -1,12 +1,58 @@
import * as plugins from './typedserver.plugins.js';
import * as paths from './typedserver.paths.js';
import * as interfaces from './interfaces/index.js';
import * as servertools from './servertools/index.js';
export interface IEasyServerConstructorOptions {
serveDir: string;
injectReload: boolean;
port?: number;
export interface IServerOptions {
/**
* serve a particular directory
*/
serveDir?: string;
/**
* inject a reload script that takes care of live reloading
*/
injectReload?: boolean;
/**
* watch the serve directory?
*/
watch?: boolean;
cors: boolean;
/**
* a default answer given in case there is no other handler.
* @returns
*/
defaultAnswer?: () => Promise<string>;
/**
* will try to reroute traffic to an ssl connection using headers
*/
forceSsl?: boolean;
/**
* allows serving manifests
*/
manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
/**
* the port to listen on
* can be overwritten when actually starting the server
*/
port?: number | string;
publicKey?: string;
privateKey?: string;
sitemap?: boolean;
feed?: boolean;
robots?: boolean;
domain?: string;
/**
* convey information about the app being served
*/
appVersion?: string;
feedMetadata?: plugins.smartfeed.IFeedOptions;
articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
blockWaybackMachine?: boolean;
}
export class TypedServer {
@ -14,8 +60,8 @@ export class TypedServer {
// nothing here yet
// instance
public options: IEasyServerConstructorOptions;
public smartexpressInstance: plugins.smartexpress.Server;
public options: IServerOptions;
public serverInstance: servertools.Server;
public smartchokInstance: plugins.smartchok.Smartchok;
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
public serveHash: string = '000000';
@ -24,12 +70,13 @@ export class TypedServer {
public lastReload: number = Date.now();
public ended = false;
constructor(optionsArg: IEasyServerConstructorOptions) {
const standardOptions: IEasyServerConstructorOptions = {
constructor(optionsArg: IServerOptions) {
const standardOptions: IServerOptions = {
injectReload: true,
port: 3000,
serveDir: process.cwd(),
watch: true,
cors: true,
};
this.options = {
...standardOptions,
@ -42,16 +89,16 @@ export class TypedServer {
*/
public async start() {
// set the smartexpress instance
this.smartexpressInstance = new plugins.smartexpress.Server({
this.serverInstance = new servertools.Server({
port: this.options.port,
forceSsl: false,
cors: true,
});
// add routes to the smartexpress instance
this.smartexpressInstance.addRoute(
this.serverInstance.addRoute(
'/typedserver/:request',
new plugins.smartexpress.Handler('ALL', async (req, res) => {
new servertools.Handler('ALL', async (req, res) => {
switch (req.params.request) {
case 'devtools':
res.setHeader('Content-Type', 'text/javascript');
@ -74,9 +121,9 @@ export class TypedServer {
})
);
this.smartexpressInstance.addRoute(
this.serverInstance.addRoute(
'/*',
new plugins.smartexpress.HandlerStatic(this.options.serveDir, {
new servertools.HandlerStatic(this.options.serveDir, {
responseModifier: async (responseArg) => {
let fileString = responseArg.responseContent;
if (plugins.path.parse(responseArg.path).ext === '.html') {
@ -126,12 +173,12 @@ export class TypedServer {
await this.createServeDirHash();
// lets start the server
await this.smartexpressInstance.start();
await this.serverInstance.start();
console.log('open url in browser');
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
this.typedrouter,
this.smartexpressInstance
this.serverInstance
);
// await plugins.smartopen.openUrl(`http://testing.git.zone:${this.options.port}`);
@ -156,7 +203,7 @@ export class TypedServer {
public async stop() {
this.ended = true;
await this.smartexpressInstance.stop();
await this.serverInstance.stop();
await this.typedsocket.stop();
await this.smartchokInstance.stop();
}

View File

@ -1,7 +1,17 @@
// node native
import * as http from 'http';
import * as https from 'https';
import * as net from 'net';
import * as path from 'path';
export { path };
export { http, https, net, path };
// @tsclass scope
import * as tsclass from '@tsclass/tsclass';
export {
tsclass
}
// @apiglobal scope
import * as typedrequest from '@apiglobal/typedrequest';
@ -15,22 +25,43 @@ export {
}
// @pushrocks scope
import * as lik from '@pushrocks/lik';
import * as smartchok from '@pushrocks/smartchok';
import * as smartdelay from '@pushrocks/smartdelay';
import * as smartexpress from '@pushrocks/smartexpress';
import * as smartfeed from '@pushrocks/smartfeed';
import * as smartfile from '@pushrocks/smartfile';
import * as smartmanifest from '@pushrocks/smartmanifest';
import * as smartmime from '@pushrocks/smartmime';
import * as smartopen from '@pushrocks/smartopen';
import * as smartpath from '@pushrocks/smartpath';
import * as smartpromise from '@pushrocks/smartpromise';
import * as smartrequest from '@pushrocks/smartrequest';
import * as smartrx from '@pushrocks/smartrx';
import * as smartsitemap from '@pushrocks/smartsitemap';
import * as smarttime from '@pushrocks/smarttime';
export {
lik,
smartchok,
smartdelay,
smartexpress,
smartfeed,
smartfile,
smartmanifest,
smartmime,
smartopen,
smartpath,
smartpromise,
smartrequest,
smartsitemap,
smarttime,
smartrx,
};
// express
import bodyParser from 'body-parser';
import cors from 'cors';
import express from 'express';
// @ts-ignore
import expressForceSsl from 'express-force-ssl';
export { bodyParser, cors, express, expressForceSsl };

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiglobal/typedserver',
version: '2.0.37',
version: '2.0.38',
description: 'easy serving of static files'
}

View File

@ -3,6 +3,7 @@
"experimentalDecorators": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "nodenext"
"moduleResolution": "nodenext",
"esModuleInterop": true
}
}