Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
0ec00a5404 | |||
b0f48ba598 | |||
ec4a51668c | |||
07739bec27 | |||
9aebd59c08 | |||
be7f4c503e | |||
e1e1d4bf65 | |||
20ecb86a9e | |||
83890d7cab | |||
4c87ea8273 | |||
4be625a0d9 | |||
c305ca517a | |||
23dccae01b | |||
d5f8d215a2 | |||
3d4f8d1bbe | |||
4724629efa | |||
ff9aea12c3 | |||
910b9a495e | |||
7fdf0a71a7 | |||
bf2c6660f2 | |||
49afc16422 | |||
bb6f239075 | |||
5bd5916696 | |||
62df38d083 | |||
d7fe947107 | |||
dd426a4ca4 | |||
2a2d4dabe4 | |||
830682d382 | |||
d160a92bee | |||
cc421c3321 | |||
92ecef30d3 | |||
de4aab6df0 | |||
5624e9dc1f | |||
c7e40e4cde | |||
0704febfc0 | |||
b8b1a61ae5 | |||
45155bbce0 | |||
35bcf717cb | |||
4d3be1064d | |||
8ee72f9380 | |||
049ceb7d15 | |||
87a5337db3 | |||
2372e36367 | |||
748fc931c6 | |||
d5cdb94c73 | |||
59d6c7d5ec |
67
.gitea/workflows/default_nottags.yaml
Normal file
67
.gitea/workflows/default_nottags.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
110
.gitea/workflows/default_tags.yaml
Normal file
110
.gitea/workflows/default_tags.yaml
Normal file
@ -0,0 +1,110 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm prepare
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @gitzone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
41
package.json
41
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apiglobal/typedserver",
|
||||
"version": "2.0.35",
|
||||
"version": "2.0.58",
|
||||
"description": "easy serving of static files",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@ -39,27 +39,40 @@
|
||||
"dependencies": {
|
||||
"@apiglobal/typedrequest": "^2.0.12",
|
||||
"@apiglobal/typedrequest-interfaces": "^2.0.1",
|
||||
"@apiglobal/typedsocket": "^2.0.22",
|
||||
"@apiglobal/typedsocket": "^2.0.24",
|
||||
"@pushrocks/lik": "^6.0.2",
|
||||
"@pushrocks/smartchok": "^1.0.23",
|
||||
"@pushrocks/smartdelay": "^2.0.13",
|
||||
"@pushrocks/smartexpress": "^4.0.34",
|
||||
"@pushrocks/smartfile": "^10.0.7",
|
||||
"@pushrocks/smartdelay": "^3.0.1",
|
||||
"@pushrocks/smartenv": "^5.0.5",
|
||||
"@pushrocks/smartfeed": "^1.0.11",
|
||||
"@pushrocks/smartfile": "^10.0.25",
|
||||
"@pushrocks/smartlog": "^3.0.1",
|
||||
"@pushrocks/smartlog-destination-devtools": "^1.0.10",
|
||||
"@pushrocks/smartmanifest": "^2.0.2",
|
||||
"@pushrocks/smartmime": "^1.0.5",
|
||||
"@pushrocks/smartopen": "^2.0.0",
|
||||
"@pushrocks/smartpath": "^5.0.5",
|
||||
"@pushrocks/smartpromise": "^3.1.7",
|
||||
"@pushrocks/smartrx": "^3.0.0",
|
||||
"@pushrocks/webstore": "^2.0.5",
|
||||
"lit": "^2.7.0"
|
||||
"@pushrocks/smartpromise": "^4.0.2",
|
||||
"@pushrocks/smartrequest": "^2.0.15",
|
||||
"@pushrocks/smartrx": "^3.0.2",
|
||||
"@pushrocks/smartsitemap": "^2.0.1",
|
||||
"@pushrocks/smarttime": "^4.0.1",
|
||||
"@pushrocks/webstore": "^2.0.8",
|
||||
"@tsclass/tsclass": "^4.0.42",
|
||||
"@types/express": "^4.17.17",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^2.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.63",
|
||||
"@gitzone/tsbundle": "^2.0.6",
|
||||
"@gitzone/tsrun": "^1.2.39",
|
||||
"@gitzone/tsbuild": "^2.1.66",
|
||||
"@gitzone/tsbundle": "^2.0.8",
|
||||
"@gitzone/tsrun": "^1.2.42",
|
||||
"@gitzone/tstest": "^1.0.72",
|
||||
"@pushrocks/tapbundle": "^5.0.4",
|
||||
"@types/node": "^18.15.11"
|
||||
"@pushrocks/tapbundle": "^5.0.8",
|
||||
"@types/node": "^20.3.3"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
|
1670
pnpm-lock.yaml
generated
1670
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
# @apiglobal/typedserver
|
||||
# @pushrocks/typedserver
|
||||
easy serving of static files
|
||||
|
||||
## Availabililty and Links
|
||||
|
@ -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);
|
||||
});
|
128
test/test.server.ts
Normal file
128
test/test.server.ts
Normal file
@ -0,0 +1,128 @@
|
||||
// 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);
|
||||
});
|
||||
|
||||
// see if a demo request holds up
|
||||
tap.test('should issue a request', async (tools) => {
|
||||
const response = await smartrequest.postJson('http://127.0.0.1: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://127.0.0.1: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://127.0.0.1:3000/some/randompath/', {
|
||||
method: 'OPTIONS',
|
||||
});
|
||||
console.log(response.headers);
|
||||
});
|
||||
|
||||
tap.test('should exposer a sitemap', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/sitemap');
|
||||
console.log(await response.text());
|
||||
});
|
||||
|
||||
// ========
|
||||
// clean up
|
||||
// ========
|
||||
|
||||
tap.test('should stop the server', async () => {
|
||||
await testServer.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiglobal/typedserver',
|
||||
version: '2.0.35',
|
||||
version: '2.0.58',
|
||||
description: 'easy serving of static files'
|
||||
}
|
||||
|
@ -1 +1,10 @@
|
||||
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;
|
||||
|
@ -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';
|
||||
|
11
ts/interfaces/requestmodifier.ts
Normal file
11
ts/interfaces/requestmodifier.ts
Normal 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;
|
||||
}>;
|
11
ts/interfaces/responsemodifier.ts
Normal file
11
ts/interfaces/responsemodifier.ts
Normal 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;
|
||||
}>;
|
25
ts/interfaces/typedrequests.ts
Normal file
25
ts/interfaces/typedrequests.ts
Normal file
@ -0,0 +1,25 @@
|
||||
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 interface IReq_GetLatestServerChangeTime
|
||||
extends typedrequestInterfaces.implementsTR<
|
||||
typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetLatestServerChangeTime
|
||||
> {
|
||||
method: 'getLatestServerChangeTime';
|
||||
request: {};
|
||||
response: {
|
||||
time: number;
|
||||
};
|
||||
}
|
35
ts/servertools/classes.feed.ts
Normal file
35
ts/servertools/classes.feed.ts
Normal 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);
|
||||
}
|
||||
}
|
17
ts/servertools/classes.handler.ts
Normal file
17
ts/servertools/classes.handler.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import { type Request, type 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;
|
||||
}
|
||||
}
|
77
ts/servertools/classes.handlerproxy.ts
Normal file
77
ts/servertools/classes.handlerproxy.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
126
ts/servertools/classes.handlerstatic.ts
Normal file
126
ts/servertools/classes.handlerstatic.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
17
ts/servertools/classes.handlertypedrouter.ts
Normal file
17
ts/servertools/classes.handlertypedrouter.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
32
ts/servertools/classes.route.ts
Normal file
32
ts/servertools/classes.route.ts
Normal 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 { type 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);
|
||||
}
|
||||
}
|
294
ts/servertools/classes.server.ts
Normal file
294
ts/servertools/classes.server.ts
Normal file
@ -0,0 +1,294 @@
|
||||
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 { type 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 = 20 * 1000;
|
||||
|
||||
// general request handlling
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
req.setTimeout(60 * 1000);
|
||||
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', '*');
|
||||
res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Expires', new Date(Date.now()).toUTCString());
|
||||
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);
|
||||
}
|
||||
}
|
68
ts/servertools/classes.sitemap.ts
Normal file
68
ts/servertools/classes.sitemap.ts
Normal 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 { type 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
6
ts/servertools/index.ts
Normal 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';
|
14
ts/servertools/tools.manifest.ts
Normal file
14
ts/servertools/tools.manifest.ts
Normal 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();
|
||||
});
|
||||
};
|
33
ts/servertools/tools.robots.ts
Normal file
33
ts/servertools/tools.robots.ts
Normal 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
|
||||
`);
|
||||
})
|
||||
);
|
||||
};
|
22
ts/servertools/tools.sslredirect.ts
Normal file
22
ts/servertools/tools.sslredirect.ts
Normal 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;
|
||||
};
|
@ -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 server: servertools.Server;
|
||||
public smartchokInstance: plugins.smartchok.Smartchok;
|
||||
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
|
||||
public serveHash: string = '000000';
|
||||
@ -24,34 +70,24 @@ export class TypedServer {
|
||||
|
||||
public lastReload: number = Date.now();
|
||||
public ended = false;
|
||||
constructor(optionsArg: IEasyServerConstructorOptions) {
|
||||
const standardOptions: IEasyServerConstructorOptions = {
|
||||
injectReload: true,
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
const standardOptions: IServerOptions = {
|
||||
port: 3000,
|
||||
serveDir: process.cwd(),
|
||||
watch: true,
|
||||
injectReload: false,
|
||||
serveDir: null,
|
||||
watch: false,
|
||||
cors: true,
|
||||
};
|
||||
this.options = {
|
||||
...standardOptions,
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* inits and starts the server
|
||||
*/
|
||||
public async start() {
|
||||
// set the smartexpress instance
|
||||
this.smartexpressInstance = new plugins.smartexpress.Server({
|
||||
port: this.options.port,
|
||||
forceSsl: false,
|
||||
cors: true,
|
||||
});
|
||||
|
||||
this.server = new servertools.Server(this.options);
|
||||
// add routes to the smartexpress instance
|
||||
this.smartexpressInstance.addRoute(
|
||||
this.server.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');
|
||||
@ -73,67 +109,85 @@ export class TypedServer {
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.smartexpressInstance.addRoute(
|
||||
'/*',
|
||||
new plugins.smartexpress.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent;
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: '${this.lastReload}',
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script');
|
||||
/**
|
||||
* inits and starts the server
|
||||
*/
|
||||
public async start() {
|
||||
if (this.options.serveDir) {
|
||||
this.server.addRoute(
|
||||
'/*',
|
||||
new servertools.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent;
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: ${this.lastReload},
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script');
|
||||
}
|
||||
}
|
||||
}
|
||||
const headers = responseArg.headers;
|
||||
headers.appHash = this.serveHash;
|
||||
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||
headers['Pragma'] = 'no-cache';
|
||||
headers['Expires'] = '0';
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: fileString,
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
})
|
||||
);
|
||||
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir], {});
|
||||
if (this.options.watch) {
|
||||
const headers = responseArg.headers;
|
||||
headers.appHash = this.serveHash;
|
||||
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||
headers['Pragma'] = 'no-cache';
|
||||
headers['Expires'] = '0';
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: fileString,
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
})
|
||||
);
|
||||
} else if (this.options.injectReload) {
|
||||
throw new Error(
|
||||
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
|
||||
);
|
||||
}
|
||||
if (this.options.watch && this.options.serveDir) {
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir], {});
|
||||
await this.smartchokInstance.start();
|
||||
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
|
||||
await this.createServeDirHash();
|
||||
this.reload();
|
||||
});
|
||||
await this.createServeDirHash();
|
||||
}
|
||||
|
||||
await this.createServeDirHash();
|
||||
|
||||
// lets start the server
|
||||
await this.smartexpressInstance.start();
|
||||
console.log('open url in browser');
|
||||
await this.server.start();
|
||||
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
|
||||
this.typedrouter,
|
||||
this.smartexpressInstance
|
||||
this.server
|
||||
);
|
||||
|
||||
// lets setup typedrouter
|
||||
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async (reqDataArg) => {
|
||||
return {
|
||||
time: this.lastReload,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// console.log('open url in browser');
|
||||
// await plugins.smartopen.openUrl(`http://testing.git.zone:${this.options.port}`);
|
||||
}
|
||||
|
||||
@ -142,7 +196,9 @@ export class TypedServer {
|
||||
*/
|
||||
public async reload() {
|
||||
this.lastReload = Date.now();
|
||||
for (const connectionArg of await this.typedsocket.findAllTargetConnections(async () => true)) {
|
||||
for (const connectionArg of await this.typedsocket.findAllTargetConnectionsByTag(
|
||||
'typedserver_frontend'
|
||||
)) {
|
||||
const pushTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
@ -156,9 +212,11 @@ export class TypedServer {
|
||||
|
||||
public async stop() {
|
||||
this.ended = true;
|
||||
await this.smartexpressInstance.stop();
|
||||
await this.server.stop();
|
||||
await this.typedsocket.stop();
|
||||
await this.smartchokInstance.stop();
|
||||
if (this.smartchokInstance) {
|
||||
await this.smartchokInstance.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public async createServeDirHash() {
|
||||
|
@ -1,36 +1,61 @@
|
||||
// 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';
|
||||
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
|
||||
import * as typedsocket from '@apiglobal/typedsocket';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedrequestInterfaces,
|
||||
typedsocket,
|
||||
}
|
||||
export { typedrequest, typedrequestInterfaces, typedsocket };
|
||||
|
||||
// @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 };
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiglobal/typedserver',
|
||||
version: '2.0.35',
|
||||
version: '2.0.58',
|
||||
description: 'easy serving of static files'
|
||||
}
|
||||
|
@ -61,11 +61,15 @@ export class ReloadChecker {
|
||||
|
||||
public async checkReload(lastServerChange: number) {
|
||||
let reloadJustified = false;
|
||||
(await this.store.get(this.storeKey)) !== lastServerChange ? (reloadJustified = true) : null;
|
||||
let storedLastServerChange = await this.store.get(this.storeKey);
|
||||
if (storedLastServerChange && storedLastServerChange !== lastServerChange) {
|
||||
reloadJustified = true;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (reloadJustified) {
|
||||
this.store.set(this.storeKey, lastServerChange);
|
||||
const reloadText = `about to reload ${
|
||||
const reloadText = `upgrading... ${
|
||||
globalThis.globalSw ? '(purging the sw cache first...)' : ''
|
||||
}`;
|
||||
this.infoscreen.setText(reloadText);
|
||||
@ -98,8 +102,29 @@ export class ReloadChecker {
|
||||
this.typedrouter,
|
||||
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl()
|
||||
);
|
||||
this.typedsocket;
|
||||
logger.log('success', `ReloadChecker connected through typedsocket!`)
|
||||
this.typedsocket.addTag('typedserver_frontend', {});
|
||||
this.typedsocket.eventSubject.subscribe(async (eventArg) => {
|
||||
console.log(`typedsocket event subscription: ${eventArg}`);
|
||||
if (
|
||||
eventArg === 'disconnected' ||
|
||||
eventArg === 'disconnecting' ||
|
||||
eventArg === 'timedOut'
|
||||
) {
|
||||
this.backendConnectionLost = true;
|
||||
this.infoscreen.setText(`typedsocket ${eventArg}!`);
|
||||
} else if (eventArg === 'connected' && this.backendConnectionLost) {
|
||||
this.backendConnectionLost = false;
|
||||
this.infoscreen.setSuccess('typedsocket connected!');
|
||||
// lets check if a reload is necessary
|
||||
const getLatestServerChangeTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
'getLatestServerChangeTime'
|
||||
);
|
||||
const response = await getLatestServerChangeTime.fire({});
|
||||
this.checkReload(response.time);
|
||||
}
|
||||
});
|
||||
logger.log('success', `ReloadChecker connected through typedsocket!`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ export class TypedserverInfoscreen extends LitElement {
|
||||
];
|
||||
|
||||
public setText(textArg: string) {
|
||||
this.success = false;
|
||||
this.text = textArg;
|
||||
this.show();
|
||||
this.success = false;
|
||||
@ -80,18 +81,27 @@ export class TypedserverInfoscreen extends LitElement {
|
||||
this.text = textArg;
|
||||
this.show();
|
||||
this.success = true;
|
||||
setTimeout(() => this.hide(), 1000);
|
||||
}
|
||||
|
||||
private appended = false;
|
||||
public show () {
|
||||
this.appended = true;
|
||||
public async show () {
|
||||
document.body.append(this);
|
||||
this.appended = true;
|
||||
await plugins.smartdelay.delayFor(0);
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
mainbox.classList.add('show');
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.success = false;
|
||||
public async hide() {
|
||||
this.text = '';
|
||||
if (this.appended) {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
mainbox.classList.remove('show');
|
||||
}
|
||||
await plugins.smartdelay.delayFor(300);
|
||||
if (this.appended) {
|
||||
this.appended = false;
|
||||
document.body.removeChild(this);
|
||||
}
|
||||
}
|
||||
@ -99,7 +109,7 @@ export class TypedserverInfoscreen extends LitElement {
|
||||
|
||||
public render () {
|
||||
return html`
|
||||
<div class="mainbox ${this.show ? 'show' : ''} ${this.success ? 'success': ''}">${this.text}</div>
|
||||
<div class="mainbox ${this.success ? 'success': ''}">${this.text}</div>
|
||||
`
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext"
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user