fix(core): update
This commit is contained in:
22
ts/index.ts
Normal file
22
ts/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export * from './npm-publicserver.classes.uipublicserver';
|
||||
|
||||
import { UiPublicServer } from '.';
|
||||
|
||||
process.env.UIP_ENV = process.env.BACKEND_URL.includes('develop-backend') ? 'dev' : 'prod';
|
||||
|
||||
export const defaultPublicServer = new UiPublicServer({
|
||||
port: 3000,
|
||||
packageBaseDirectory: './public/',
|
||||
npmRegistryUrl: 'https://registry.npmjs.org/',
|
||||
allowedPackages: [
|
||||
'@pushrocks/smartfile'
|
||||
],
|
||||
mode: process.env.UIP_ENV === 'dev' ? 'dev' : 'prod',
|
||||
log: false,
|
||||
});
|
||||
|
||||
export const runCli = async () => {
|
||||
|
||||
}
|
||||
|
||||
export const stop = async () => {}
|
||||
12
ts/interfaces.ts
Normal file
12
ts/interfaces.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as url from 'url';
|
||||
export interface ISimpleRequest {
|
||||
headers: {};
|
||||
parsedUrl: url.URL;
|
||||
}
|
||||
|
||||
export interface ISimpleResponse {
|
||||
headers: { [key: string]: string };
|
||||
status: number;
|
||||
body: string | Buffer;
|
||||
}
|
||||
export type IRenderFunction = (req: ISimpleRequest) => Promise<ISimpleResponse>;
|
||||
20
ts/logging.ts
Normal file
20
ts/logging.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as plugins from './plugins';
|
||||
|
||||
export const logger = new plugins.splunkLogging.Logger({
|
||||
token: '',
|
||||
});
|
||||
|
||||
logger.send({
|
||||
message: {
|
||||
package: '',
|
||||
subFolder: '',
|
||||
filePath: '',
|
||||
status: 200,
|
||||
},
|
||||
severity: 'info',
|
||||
metadata: {
|
||||
host: 'ui-publicserver',
|
||||
source: 'nodejs',
|
||||
sourcetype: 'process',
|
||||
},
|
||||
});
|
||||
403
ts/npm-publicserver.classes.uipublicserver.ts
Normal file
403
ts/npm-publicserver.classes.uipublicserver.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import * as interfaces from './interfaces';
|
||||
import * as plugins from './plugins';
|
||||
import * as paths from './paths';
|
||||
import * as ntml from './ntml';
|
||||
|
||||
export interface IPublicServerOptions {
|
||||
packageBaseDirectory?: string;
|
||||
npmRegistryUrl?: string;
|
||||
allowedPackages?: string[];
|
||||
port?: number;
|
||||
mode: 'dev' | 'prod';
|
||||
log?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* the main public server instance
|
||||
*/
|
||||
export class UiPublicServer {
|
||||
public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
public readme = new plugins.smartmarkdown.SmartMarkdown().markdownToHtml(
|
||||
plugins.smartfile.fs.toStringSync(plugins.path.join(paths.packageDir, 'readme.md'))
|
||||
);
|
||||
public startedAt: string;
|
||||
private server: plugins.http.Server;
|
||||
private npmRegistry: plugins.smartnpm.NpmRegistry;
|
||||
|
||||
private corsDev = '*';
|
||||
private corsProd = ''; // TODO: Define allowed URLs
|
||||
|
||||
public defaultOptions: IPublicServerOptions = {
|
||||
packageBaseDirectory: './',
|
||||
npmRegistryUrl: 'https://registry.npmjs.org',
|
||||
allowedPackages: [],
|
||||
port: 8080,
|
||||
mode: 'dev',
|
||||
log: true,
|
||||
};
|
||||
public options: IPublicServerOptions;
|
||||
|
||||
constructor(optionsArg?: IPublicServerOptions) {
|
||||
// lets create an npm instance for the registry that we are talking to
|
||||
this.options = {
|
||||
...this.defaultOptions,
|
||||
...optionsArg,
|
||||
};
|
||||
|
||||
if (!this.options.packageBaseDirectory.endsWith('/')) {
|
||||
this.options.packageBaseDirectory += '/';
|
||||
}
|
||||
|
||||
this.npmRegistry = new plugins.smartnpm.NpmRegistry({
|
||||
npmRegistryUrl: this.options.npmRegistryUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the server
|
||||
*/
|
||||
public async startServer() {
|
||||
console.log('starting the uipublicserver');
|
||||
const done = plugins.smartpromise.defer();
|
||||
const expressApplication = plugins.express();
|
||||
|
||||
const shouldCompress = (req: plugins.express.Request, res: plugins.express.Response) => {
|
||||
if (req.headers['x-no-compression']) {
|
||||
// don't compress responses with this request header
|
||||
return false;
|
||||
}
|
||||
|
||||
// fallback to standard filter function
|
||||
return plugins.compression.filter(req, res);
|
||||
};
|
||||
|
||||
expressApplication.use(plugins.compression({ filter: shouldCompress }));
|
||||
|
||||
if (this.options.mode === 'dev') {
|
||||
expressApplication.use('/peek/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
const simpleResponse: interfaces.ISimpleResponse = await this.renderPeek({
|
||||
headers: req.headers,
|
||||
parsedUrl,
|
||||
});
|
||||
for (const header of Object.keys(simpleResponse.headers)) {
|
||||
res.setHeader(header, simpleResponse.headers[header]);
|
||||
}
|
||||
res.status(simpleResponse.status);
|
||||
res.write(simpleResponse.body);
|
||||
res.end();
|
||||
});
|
||||
|
||||
expressApplication.use('/readme/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
const simpleResponse: interfaces.ISimpleResponse = await this.renderReadme({
|
||||
headers: req.headers,
|
||||
parsedUrl,
|
||||
});
|
||||
for (const header of Object.keys(simpleResponse.headers)) {
|
||||
res.setHeader(header, simpleResponse.headers[header]);
|
||||
}
|
||||
res.status(simpleResponse.status);
|
||||
res.write(simpleResponse.body);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
expressApplication.use('/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
const simpleResponse: interfaces.ISimpleResponse = await this.render({
|
||||
headers: req.headers,
|
||||
parsedUrl,
|
||||
});
|
||||
for (const header of Object.keys(simpleResponse.headers)) {
|
||||
res.setHeader(header, simpleResponse.headers[header]);
|
||||
}
|
||||
// CORS
|
||||
res.setHeader('ui-public-server-version', this.projectinfo.version);
|
||||
res.setHeader('access-control-allow-origin', true ? this.corsDev : this.corsProd); // TODO: replace true with check for env
|
||||
res.status(simpleResponse.status);
|
||||
res.write(simpleResponse.body);
|
||||
res.end();
|
||||
});
|
||||
|
||||
// note: We assume that ssl termination is done in reverse proxy in kubernetes. so simple http should suffice here.
|
||||
this.server = expressApplication.listen(this.options.port, () => {
|
||||
console.log(`listening on port ${this.options.port}`);
|
||||
this.startedAt = new Date().toISOString();
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
public disableLogging() {
|
||||
this.options.log = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* stops the server
|
||||
*/
|
||||
public async stopServer() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
if (!this.server) {
|
||||
console.log('server does not seem to be started, so no need to stop it');
|
||||
return;
|
||||
}
|
||||
this.server.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
public requestMap: { [key: string]: plugins.smartpromise.Deferred<any> } = {};
|
||||
|
||||
/**
|
||||
* renders a response using interfaces.ISimpleRequest and interfaces.ISimpleResponse
|
||||
* @param req
|
||||
*/
|
||||
private render: interfaces.IRenderFunction = async (req) => {
|
||||
const pathArray = req.parsedUrl.pathname.split('/');
|
||||
|
||||
if (pathArray.length < 3) {
|
||||
const textArray = [
|
||||
`serving javascript to the web for Lossless GmbH.<br/>`,
|
||||
`Running in mode: <b>${this.options.mode}</b><br/><br/>`,
|
||||
];
|
||||
if (this.options.mode === 'dev') {
|
||||
textArray.push(
|
||||
`<b>Wondering what we serve?</b> <a href="/peek/">Lets take a peek!</a><br/>`
|
||||
);
|
||||
textArray.push(`<b>Documentation:</b> <a href="/readme/">Read the docs</a>`);
|
||||
}
|
||||
return {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
status: 201,
|
||||
body: await plugins.litNtml.html`
|
||||
${ntml.getBody(this, textArray)}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
// lets care about the npm organization
|
||||
const npmOrg = pathArray[1];
|
||||
if (!npmOrg.startsWith('@')) {
|
||||
console.log('malformed npmorg');
|
||||
return {
|
||||
status: 500,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `npmorg "${npmOrg}" must start with @`,
|
||||
};
|
||||
}
|
||||
|
||||
// lets care about the packageName
|
||||
const npmPackage = pathArray[2];
|
||||
const packageName = `${npmOrg}/${npmPackage}`;
|
||||
if (this.options.log) {
|
||||
console.log(
|
||||
`got a request for package ${packageName} on registry ${this.npmRegistry.options.npmRegistryUrl}`
|
||||
);
|
||||
}
|
||||
if (!this.options.allowedPackages.includes(packageName)) {
|
||||
return {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
status: 503,
|
||||
body: await plugins.litNtml.html`
|
||||
the requested package is not allowlisted for public access
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
// lets care about the inner package path
|
||||
let filePath = this.options.packageBaseDirectory;
|
||||
let first = true;
|
||||
for (let i = 3; i < pathArray.length; i++) {
|
||||
if (first) {
|
||||
filePath += './';
|
||||
first = false;
|
||||
} else {
|
||||
filePath += '/';
|
||||
}
|
||||
filePath += pathArray[i];
|
||||
}
|
||||
|
||||
// lets care about version and disttag
|
||||
|
||||
const version = req.parsedUrl.searchParams.get('version') || '';
|
||||
const distTag = req.parsedUrl.searchParams.get('disttag') || '';
|
||||
|
||||
const requestDescriptor = `${packageName}/${filePath}/${distTag}/${version}`;
|
||||
|
||||
let smartfile: plugins.smartfile.Smartfile;
|
||||
|
||||
// protect against parallel requests
|
||||
if (this.requestMap[requestDescriptor]) {
|
||||
smartfile = await this.requestMap[requestDescriptor].promise;
|
||||
} else {
|
||||
this.requestMap[requestDescriptor] = plugins.smartpromise.defer();
|
||||
smartfile = await this.npmRegistry
|
||||
.getFileFromPackage(packageName, filePath, {
|
||||
version,
|
||||
distTag,
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
this.requestMap[requestDescriptor].resolve(smartfile);
|
||||
delete this.requestMap[requestDescriptor];
|
||||
}
|
||||
|
||||
if (!smartfile) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: await ntml.getBody(
|
||||
this,
|
||||
`${packageName}@${version} does not have a file at "${filePath}"`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'cache-control': `max-age=${
|
||||
(version
|
||||
? plugins.smarttime.getMilliSecondsFromUnits({ months: 1 })
|
||||
: plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })) / 1000
|
||||
}`,
|
||||
'content-length': smartfile.contentBuffer.length.toString(),
|
||||
'content-type': plugins.smartmime.detectMimeType(smartfile.path),
|
||||
},
|
||||
status: 200,
|
||||
body: smartfile.contentBuffer,
|
||||
};
|
||||
};
|
||||
|
||||
private renderPeek: interfaces.IRenderFunction = async (req) => {
|
||||
const pathArray = req.parsedUrl.pathname.split('/');
|
||||
const npmOrg = pathArray[1];
|
||||
const npmPackage = pathArray[2];
|
||||
const npmPackageName = `${npmOrg}/${npmPackage}`;
|
||||
const distTag = req.parsedUrl.searchParams.get('disttag');
|
||||
const version = req.parsedUrl.searchParams.get('version');
|
||||
|
||||
// lets care about cors
|
||||
|
||||
if (!npmOrg || !npmPackage || !this.options.allowedPackages.includes(npmPackageName)) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="../">../ -> Go back</a><br/>`,
|
||||
`<b>allowlisted packages:</b><br/>`,
|
||||
...this.options.allowedPackages.map((packageArg) => {
|
||||
return `
|
||||
<div>"${packageArg}" -> mapping to "${this.options.packageBaseDirectory}" inside the package <a href="/peek/${packageArg}">Peek...</a></div>
|
||||
`;
|
||||
}),
|
||||
])}
|
||||
`,
|
||||
};
|
||||
}
|
||||
const result = await this.npmRegistry
|
||||
.getFilesFromPackage(npmPackageName, plugins.path.join(this.options.packageBaseDirectory), {
|
||||
version: version,
|
||||
distTag: distTag,
|
||||
})
|
||||
.catch((err) => {
|
||||
return;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="/peek/">/peek/ -> Go to peek overview</a><br/>`,
|
||||
`<b>Not found: package ${npmPackageName}@${
|
||||
version || 'latest'
|
||||
} is not available on the supplied registry</b><br/>`,
|
||||
])}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
const packageInfo = await this.npmRegistry.getPackageInfo(npmPackageName);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="/peek/">/peek/ -> Go to peek overview</a><br/><br/>`,
|
||||
`<h1>${npmPackageName}@${version || distTag || '<i>inferred</i> latest'}</h1>`,
|
||||
`<b>Versions:</b> ${packageInfo.allVersions
|
||||
.map(
|
||||
(versionArg) =>
|
||||
`<a href="./${npmPackage}?version=${versionArg.version}">${versionArg.version}</a> | `
|
||||
)
|
||||
.join('')}<br/><br/>`,
|
||||
`<b>DistTags:</b> ${
|
||||
packageInfo.allDistTags
|
||||
.map(
|
||||
(distTagArg) =>
|
||||
`<a href="./${npmPackage}?disttag=${distTagArg.name}">${distTagArg.name} (${distTagArg.targetVersion})</a> | `
|
||||
)
|
||||
.join('') || 'no dist tags found!'
|
||||
}<br/><br/>`,
|
||||
`<b>File Overview at version and path:</b><br/>`,
|
||||
`"<b>${npmPackageName}@${
|
||||
version || distTag || '<i>inferred</i> latest'
|
||||
}</b>" under internal path "<b>${this.options.packageBaseDirectory}</b>"<br/></br>`,
|
||||
...result.map((smartfile) => {
|
||||
const displayPath = smartfile.path.replace(
|
||||
plugins.path.join('package', this.options.packageBaseDirectory),
|
||||
''
|
||||
);
|
||||
return `<a href="/${npmPackageName}/${displayPath}?version=${version || ''}&disttag=${
|
||||
distTag || ''
|
||||
}">${displayPath}</a><br/>`;
|
||||
}),
|
||||
])}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
private renderReadme: interfaces.IRenderFunction = async (req) => {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: await ntml.getBody(this, [
|
||||
`<style>
|
||||
pre {
|
||||
display: block;
|
||||
background: #fafafa;
|
||||
border: 1px dotted #CCC;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>`,
|
||||
this.readme,
|
||||
]),
|
||||
};
|
||||
};
|
||||
}
|
||||
59
ts/ntml/body.ts
Normal file
59
ts/ntml/body.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { UiPublicServer } from '../npm-publicserver.classes.uipublicserver';
|
||||
import * as plugins from '../plugins';
|
||||
export const getBody = async (uipublicServerArg: UiPublicServer, contentArg: string | string[]) => {
|
||||
return await plugins.litNtml.html`
|
||||
<head></head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 20px;
|
||||
background: #eeeeeb;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: #ffffff;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
border: 1px dashed #CCCCCC;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-bottom: 60px;
|
||||
position:relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.logo {
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px dotted #CCCCCC;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-right: -20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
<div class="main">
|
||||
<div class="logo">
|
||||
<img src="https://assetbroker.lossless.one/brandfiles/lossless/svg-minimal-bright.svg" onclick="window.location.href = 'https://lossless.com'" />
|
||||
</div>
|
||||
${contentArg}
|
||||
<div class="footer">
|
||||
UiPublicServer v${uipublicServerArg.projectinfo.version} |
|
||||
running since ${uipublicServerArg.startedAt} |
|
||||
<a href="https://lossless.gmbh" target="_blank">Legal Info</a></div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
};
|
||||
1
ts/ntml/index.ts
Normal file
1
ts/ntml/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './body';
|
||||
3
ts/paths.ts
Normal file
3
ts/paths.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as plugins from './plugins';
|
||||
|
||||
export const packageDir = plugins.path.join(__dirname, '../');
|
||||
36
ts/plugins.ts
Normal file
36
ts/plugins.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// node native
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import * as path from 'path';
|
||||
|
||||
export { http, url, path };
|
||||
|
||||
// @pushrocks scope (maintained by Lossless GmbH)
|
||||
import * as projectinfo from '@pushrocks/projectinfo';
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartfile from '@pushrocks/smartfile';
|
||||
import * as smartmime from '@pushrocks/smartmime';
|
||||
import * as smartmarkdown from '@pushrocks/smartmarkdown';
|
||||
import * as smartnpm from '@pushrocks/smartnpm';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smarttime from '@pushrocks/smarttime';
|
||||
|
||||
export {
|
||||
projectinfo,
|
||||
smartdelay,
|
||||
smartfile,
|
||||
smartmime,
|
||||
smartmarkdown,
|
||||
smartnpm,
|
||||
smartpromise,
|
||||
smarttime,
|
||||
};
|
||||
|
||||
// unscoped packages
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
import * as litNtml from 'lit-ntml';
|
||||
import * as promClient from 'prom-client';
|
||||
import splunkLogging from 'splunk-logging';
|
||||
|
||||
export { compression, express, litNtml, promClient, splunkLogging };
|
||||
Reference in New Issue
Block a user