Handle arrays and proper types on headers and query strings

This commit is contained in:
Jonathan Barrow
2023-03-19 11:52:43 -04:00
parent b28aababbe
commit a95d4e10a6
18 changed files with 230 additions and 52 deletions

64
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"nodemailer": "^6.4.2",
"redis": "^4.3.1",
"tga": "^1.0.4",
"typescript-is": "^0.19.0",
"validator": "^13.7.0",
"xmlbuilder": "^13.0.2",
"xmlbuilder2": "0.0.4"
@@ -48,6 +49,7 @@
"@types/node": "^18.14.4",
"@types/node-rsa": "^1.1.1",
"@types/nodemailer": "^6.4.7",
"@types/qs": "^6.9.7",
"@types/validator": "^13.7.14",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
@@ -5465,6 +5467,11 @@
"node": ">= 0.6"
}
},
"node_modules/nested-error-stacks": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz",
"integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw=="
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -5976,6 +5983,12 @@
"@redis/time-series": "1.0.4"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"optional": true
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -6679,14 +6692,12 @@
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"devOptional": true
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
"dependencies": {
"tslib": "^1.8.1"
},
@@ -6762,7 +6773,6 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6771,6 +6781,24 @@
"node": ">=4.2.0"
}
},
"node_modules/typescript-is": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/typescript-is/-/typescript-is-0.19.0.tgz",
"integrity": "sha512-SAJEx2cxbQZhfOjDEjPnQJt1qRS1M3wrKbUwvsywVHWGbMgM1dcIf9gPWNDS1/dgTa/7Iexk2mmAHHsP9MeCsA==",
"dependencies": {
"nested-error-stacks": "^2",
"tsutils": "^3.17.1"
},
"engines": {
"node": ">=6.14.4"
},
"optionalDependencies": {
"reflect-metadata": ">=0.1.12"
},
"peerDependencies": {
"typescript": "^4.1.5"
}
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -11517,6 +11545,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"nested-error-stacks": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz",
"integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw=="
},
"next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -11892,6 +11925,12 @@
"@redis/time-series": "1.0.4"
}
},
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"optional": true
},
"regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -12440,14 +12479,12 @@
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"devOptional": true
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
@@ -12501,8 +12538,17 @@
"typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="
},
"typescript-is": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/typescript-is/-/typescript-is-0.19.0.tgz",
"integrity": "sha512-SAJEx2cxbQZhfOjDEjPnQJt1qRS1M3wrKbUwvsywVHWGbMgM1dcIf9gPWNDS1/dgTa/7Iexk2mmAHHsP9MeCsA==",
"requires": {
"nested-error-stacks": "^2",
"reflect-metadata": ">=0.1.12",
"tsutils": "^3.17.1"
}
},
"universalify": {
"version": "0.1.2",

View File

@@ -49,6 +49,7 @@
"nodemailer": "^6.4.2",
"redis": "^4.3.1",
"tga": "^1.0.4",
"typescript-is": "^0.19.0",
"validator": "^13.7.0",
"xmlbuilder": "^13.0.2",
"xmlbuilder2": "0.0.4"
@@ -64,6 +65,7 @@
"@types/node": "^18.14.4",
"@types/node-rsa": "^1.1.1",
"@types/nodemailer": "^6.4.7",
"@types/qs": "^6.9.7",
"@types/validator": "^13.7.14",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",

View File

@@ -1,9 +1,10 @@
import express from 'express';
import { getValueFromHeaders } from '@/util';
import { getUserBearer } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
async function APIMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): Promise<void> {
const authHeader: string | undefined = request.headers.authorization;
const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization');
if (!authHeader || !(authHeader.startsWith('Bearer'))) {
return next();

View File

@@ -1,5 +1,6 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromHeaders } from '@/util';
const VALID_CLIENT_ID_SECRET_PAIRS: { [key: string]: string } = {
// * 'Key' is the client ID, 'Value' is the client secret
@@ -14,8 +15,8 @@ function nintendoClientHeaderCheck(request: express.Request, response: express.R
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const clientId: string = request.headers['x-nintendo-client-id'] as string;
const clientSecret: string = request.headers['x-nintendo-client-secret'] as string;
const clientId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-id');
const clientSecret: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-secret');
if (
!clientId ||

View File

@@ -1,8 +1,9 @@
import express from 'express';
import NintendoCertificate from '@/nintendo-certificate';
import { getValueFromHeaders } from '@/util';
function deviceCertificateMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): void {
const certificate: string = request.headers['x-nintendo-device-cert'] as string;
const certificate: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
if (!certificate) {
return next();

View File

@@ -1,10 +1,11 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromHeaders } from '@/util';
import { getUserBasic, getUserBearer } from '@/database';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
async function PNIDMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
const authHeader: string | undefined = request.headers.authorization;
const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization');
if (!authHeader || !(authHeader.startsWith('Bearer') || authHeader.startsWith('Basic'))) {
return next();

View File

@@ -1,12 +1,18 @@
import crypto from 'node:crypto';
import express from 'express';
import ratelimit from 'express-rate-limit';
import { getValueFromHeaders } from '@/util';
export default ratelimit({
windowMs: 60 * 1000,
max: 1,
keyGenerator: (request: express.Request) => {
const data: string = request.headers['x-nintendo-device-cert'] as string;
let data: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert');
if (!data) {
data = request.ip;
}
return crypto.createHash('md5').update(data).digest('hex');
}
});

View File

@@ -1,11 +1,12 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { document as xmlParser } from 'xmlbuilder2';
import { getValueFromHeaders } from '@/util';
function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void {
if (request.method == 'POST' || request.method == 'PUT') {
const contentType: string | undefined = request.headers['content-type'];
const contentLength: string | undefined = request.headers['content-length'];
const contentType: string | undefined = getValueFromHeaders(request.headers, 'content-type');
const contentLength: string | undefined = getValueFromHeaders(request.headers, 'content-length');
let body: string = '';
if (

View File

@@ -42,7 +42,7 @@ const CTR_LFCS_B_PUB = Buffer.from([
]);
// Signature options
const SIGNATURE_SIZES: { [key: string]: SignatureSize } = {
const SIGNATURE_SIZES = {
RSA_4096_SHA1: <SignatureSize>{
SIZE: 0x200,
PADDING_SIZE: 0x3C
@@ -67,7 +67,7 @@ const SIGNATURE_SIZES: { [key: string]: SignatureSize } = {
SIZE: 0x3C,
PADDING_SIZE: 0x40
}
};
} as const;
class NintendoCertificate {
_certificate: Buffer;

View File

@@ -9,7 +9,7 @@ import morgan from 'morgan';
import xmlparser from '@/middleware/xml-parser';
import { connect as connectCache } from '@/cache';
import { connect as connectDatabase } from '@/database';
import { fullUrl } from '@/util';
import { fullUrl, getValueFromHeaders } from '@/util';
import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger';
import conntest from '@/services/conntest';
@@ -49,7 +49,11 @@ app.use(assets);
LOG_INFO('Creating 404 status handler');
app.use((request: express.Request, response: express.Response) => {
const url: string = fullUrl(request);
const deviceId: string = request.headers['X-Nintendo-Device-ID'] as string || 'Unknown';
let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
if (!deviceId) {
deviceId = 'Unknown';
}
LOG_WARN(`HTTP 404 at ${url} from ${deviceId}`);
@@ -66,7 +70,11 @@ LOG_INFO('Creating non-404 status handler');
app.use((error: any, request: express.Request, response: express.Response, _next: express.NextFunction) => {
const status: number = error.status || 500;
const url: string = fullUrl(request);
const deviceId: string = request.headers['X-Nintendo-Device-ID'] as string || 'Unknown';
let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID');
if (!deviceId) {
deviceId = 'Unknown';
}
LOG_WARN(`HTTP ${status} at ${url} from ${deviceId}: ${error.message}`);

View File

@@ -1,13 +1,13 @@
import express from 'express';
import moment from 'moment';
import { PNID } from '@/models/pnid';
import { sendEmailConfirmedEmail } from '@/util';
import { getValueFromQueryString, sendEmailConfirmedEmail } from '@/util';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
const router: express.Router = express.Router();
router.get('/verify', async (request: express.Request, response: express.Response) => {
const token: string = request.query.token as string;
const token: string | undefined = getValueFromQueryString(request.query, 'token');
if (!token || token.trim() == '') {
return response.status(400).json({

View File

@@ -4,7 +4,7 @@ import { getLocalCDNFile } from '@/cache';
const router: express.Router = express.Router();
router.get('/*', async (request: express.Request, response: express.Response) => {
const filePath: string = request.params[0] as string;
const filePath: string = request.params[0];
const file: Buffer = await getLocalCDNFile(filePath);

View File

@@ -1,5 +1,6 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromQueryString } from '@/util';
import { PNID } from '@/models/pnid';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
@@ -11,19 +12,26 @@ const router: express.Router = express.Router();
* Description: Maps between NNID usernames and PIDs
*/
router.get('/mapped_ids', async (request: express.Request, response: express.Response) => {
const inputType: string = request.query.input_type as string;
const outputType: string = request.query.output_type as string;
let inputList: string[];
const inputType: string | undefined = getValueFromQueryString(request.query, 'input_type');
const outputType: string | undefined = getValueFromQueryString(request.query, 'output_type');
const input: string | undefined = getValueFromQueryString(request.query, 'input');
if (!inputType || !outputType || !input) {
return response.status(400).send(xmlbuilder.create({
errors: {
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}
}).end());
}
let inputList: string[] = input.split(',');
let queryInput: string;
let queryOutput: string;
if (Array.isArray(request.query.input)) {
inputList = request.query.input as string[];
} else {
const input = request.query.input as string;
inputList = input.split(',');
}
inputList = inputList.filter(input => input); // * Remove null inputs
if (inputType === 'user_id') {

View File

@@ -1,5 +1,6 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromQueryString } from '@/util';
import { PNID } from '@/models/pnid';
import { config } from '@/config-manager';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
@@ -13,15 +14,22 @@ const router: express.Router = express.Router();
* Description: Returns a list of NNID miis
*/
router.get('/', async (request: express.Request, response: express.Response) => {
let pids: number[];
const input: string | undefined = getValueFromQueryString(request.query, 'pids');
if (Array.isArray(request.query.pids)) {
pids = request.query.pids.map(pid => Number(pid));
} else {
const input = request.query.pids as string;
pids = input.split(',').map(pid => Number(pid));
if (!input) {
return response.status(400).send(xmlbuilder.create({
errors: {
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}
}).end());
}
const pids: number[] = input.split(',').map(pid => Number(pid));
const results: HydratedPNIDDocument[] = await PNID.where('pid', pids);
const miis: {
data: string;

View File

@@ -7,7 +7,7 @@ import mongoose from 'mongoose';
import deviceCertificateMiddleware from '@/middleware/device-certificate';
import ratelimit from '@/middleware/ratelimit';
import { connection as databaseConnection, doesUserExist, getUserProfileJSONByPID } from '@/database';
import { nintendoPasswordHash, sendConfirmationEmail } from '@/util';
import { getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail } from '@/util';
import { PNID } from '@/models/pnid';
import { NEXAccount } from '@/models/nex-account';
import { LOG_ERROR } from '@/logger';
@@ -292,12 +292,25 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re
response.set('X-Nintendo-Date', new Date().getTime().toString());
const pnid: HydratedPNIDDocument | null = request.pnid;
const deviceId: string = request.headers['x-nintendo-device-id'] as string;
const acceptLanguage: string = request.headers['accept-language'] as string;
const platformId: string = request.headers['x-nintendo-platform-id'] as string;
const region: string = request.headers['x-nintendo-region'] as string;
const serialNumber: string = request.headers['x-nintendo-serial-number'] as string;
const systemVersion: string = request.headers['x-nintendo-system-version'] as string;
const deviceId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-id');
const acceptLanguage: string | undefined = getValueFromHeaders(request.headers, 'accept-language');
const platformId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-platform-id');
const region: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-region');
const serialNumber: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-serial-number');
const systemVersion: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-system-version');
if (!deviceId || !acceptLanguage || !platformId || !region || !serialNumber || !systemVersion) {
// TODO - Research these error more
return response.status(400).send(xmlbuilder.create({
errors: {
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}
}).end());
}
if (!pnid) {
// TODO - Research this error more

View File

@@ -2,7 +2,7 @@ import express from 'express';
import xmlbuilder from 'xmlbuilder';
import fs from 'fs-extra';
import { getServerByTitleId, getServer } from '@/database';
import { generateToken } from '@/util';
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { getServicePublicKey, getServiceSecretKey, getNEXPublicKey, getNEXSecretKey } from '@/cache';
import { NEXAccount } from '@/models/nex-account';
import { CryptoOptions } from '@/types/common/crypto-options';
@@ -35,7 +35,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
}).end());
}
const titleId: string = request.headers['x-nintendo-title-id'] as string;
const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
if (!titleId) {
// TODO - Research this error more
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const serverAccessLevel: string = pnid.get('server_access_level');
const server: HydratedServerDocument | null = await getServerByTitleId(titleId, serverAccessLevel);
@@ -121,7 +134,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
}).end());
}
const gameServerID: string = request.query.game_server_id as string;
const gameServerID: string | undefined = getValueFromQueryString(request.query, 'game_server_id');
if (!gameServerID) {
return response.send(xmlbuilder.create({
@@ -152,7 +165,19 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
const ip: string = server.ip;
const port: number = server.port;
const device: number = server.device;
const titleId: string = request.headers['x-nintendo-title-id'] as string;
const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
if (!titleId) {
// TODO - Research this error more
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const cryptoPath: string = `${__dirname}/../../../../certs/nex/${serverName}`;

View File

@@ -0,0 +1,3 @@
export interface SafeQs {
[key: string]: string | undefined
}

View File

@@ -5,6 +5,7 @@ import aws from 'aws-sdk';
import fs from 'fs-extra';
import express from 'express';
import mongoose from 'mongoose';
import { ParsedQs } from 'qs';
import { sendMail } from '@/mailer';
import { getServiceAESKey, getServicePrivateKey, getServiceSecretKey, getServicePublicKey } from '@/cache';
import { config, disabledFeatures } from '@/config-manager';
@@ -13,6 +14,8 @@ import { TokenOptions } from '@/types/common/token-options';
import { Token } from '@/types/common/token';
import { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import { MailerOptions } from '@/types/common/mailer-options';
import { SafeQs } from '@/types/common/safe-qs';
import { IncomingHttpHeaders } from 'node:http';
let s3: aws.S3;
@@ -320,4 +323,55 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
};
await sendMail(mailerOptions);
}
export function makeSafeQs(query: ParsedQs): SafeQs {
const entries = Object.entries(query);
const output: SafeQs = {};
for (const [key, value] of entries) {
if (typeof value !== 'string') {
// * ignore non-strings
continue;
}
output[key] = value;
}
return output;
}
export function getValueFromQueryString(qs: ParsedQs, key: string): string | undefined {
let property: string | ParsedQs | string[] | ParsedQs[] | SafeQs | undefined = qs[key];
let value: string | undefined;
if (property) {
if (Array.isArray(property)) {
property = property[0];
}
if (typeof property !== 'string') {
property = makeSafeQs(<ParsedQs>property);
value = (<SafeQs>property)[key];
} else {
value = <string>property;
}
}
return value;
}
export function getValueFromHeaders(headers: IncomingHttpHeaders, key: string): string | undefined {
let header: string | string[] | undefined = headers[key];
let value: string | undefined;
if (header) {
if (Array.isArray(header)) {
header = header[0];
}
value = header;
}
return value;
}