various: more http refactoring

This commit is contained in:
Koushik Dutta
2024-01-10 16:02:31 -08:00
parent 56bbf00edc
commit 5f02e6a272
26 changed files with 1064 additions and 383 deletions

View File

@@ -1,126 +1,12 @@
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, getHttpFetchParser, httpFetch, setDefaultHttpFetchAccept } from '@scrypted/server/src/http-fetch-helpers';
import { httpFetch, httpFetchParseIncomingMessage } from '@scrypted/server/src/fetch/http-fetch';
import type { IncomingMessage } from 'http';
export interface AuthFetchCredentialState {
username: string;
password: string;
[key: string]: any;
}
import type { Readable } from 'stream';
import { createAuthFetch } from '../../packages/auth-fetch/src/auth-fetch';
export interface AuthFetchOptions<T extends HttpFetchResponseType> extends HttpFetchOptions<T> {
credential: AuthFetchCredentialState;
}
export type { HttpFetchOptions, HttpFetchResponseType } from '@scrypted/server/src/fetch/http-fetch';
export type { AuthFetchCredentialState, AuthFetchOptions } from '../../packages/auth-fetch/src/auth-fetch';
async function getAuth(options: AuthFetchOptions<any>, method: string) {
if (!options.credential)
return;
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const { digest, basic } = options.credential as AuthFetchCredentialState & {
count?: number;
digest?: ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
basic?: ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
};
if (digest) {
options.credential.count ||= 0;
++options.credential.count;
const nc = ('00000000' + options.credential.count).slice(-8);
const cnonce = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const uri = new URL(options.url).pathname;
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');
const response = DIGEST.computeHash({
username: options.credential.username,
password: options.credential.password,
method,
uri,
nc,
cnonce,
algorithm: 'MD5',
qop: digest.data.qop!,
...digest.data,
});
const header = buildAuthorizationHeader(DIGEST, {
username: options.credential.username,
uri,
nc,
cnonce,
algorithm: digest.data.algorithm!,
qop: digest.data.qop!,
response,
...digest.data,
});
return header;
}
else if (basic) {
const { BASIC, buildAuthorizationHeader } = await import('http-auth-utils');
const header = buildAuthorizationHeader(BASIC, {
username: options.credential.username,
password: options.credential.password,
});
return header;
}
}
export async function authHttpFetch<T extends HttpFetchResponseType>(options: AuthFetchOptions<T>): ReturnType<typeof httpFetch<AuthFetchOptions<T>>> {
const method = options.method || 'GET';
const headers = new Headers(options.headers);
options.headers = headers;
setDefaultHttpFetchAccept(headers, options.responseType);
const initialHeader = await getAuth(options, method);
// try to provide an authorization if a session exists, but don't override Authorization if provided already.
// 401 will trigger a proper auth.
if (initialHeader && !headers.has('Authorization'))
headers.set('Authorization', initialHeader);
const initialResponse = await httpFetch({
...options,
ignoreStatusCode: true,
responseType: 'readable',
});
if (initialResponse.statusCode !== 401 || !options.credential) {
if (!options?.ignoreStatusCode)
checkStatus(initialResponse.statusCode);
return {
...initialResponse,
body: await getHttpFetchParser(options.responseType).parse(initialResponse.body),
};
}
let authenticateHeaders: string | string[] = initialResponse.headers.get('www-authenticate');
if (!authenticateHeaders)
throw new Error('Did not find WWW-Authenticate header.');
if (typeof authenticateHeaders === 'string')
authenticateHeaders = [authenticateHeaders];
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const parsedHeaders = authenticateHeaders.map(h => parseWWWAuthenticateHeader(h));
const digest = parsedHeaders.find(p => p.type === 'Digest') as ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
const basic = parsedHeaders.find(p => p.type === 'Basic') as ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
options.credential.digest = digest;
options.credential.basic = basic;
if (!digest && !basic)
throw new Error(`Unknown WWW-Authenticate type: ${parsedHeaders[0]?.type}`);
const header = await getAuth(options, method);
if (header)
headers.set('Authorization', header);
return httpFetch(options);
}
export const authHttpFetch = createAuthFetch<Readable, IncomingMessage>(httpFetch, httpFetchParseIncomingMessage);
function ensureType<T>(v: T) {
}
@@ -153,5 +39,12 @@ async function test() {
responseType: 'buffer',
});
ensureType<Buffer>(d.body);
const e = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
responseType: 'text',
});
ensureType<string>(e.body);
}

11
packages/auth-fetch/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules
.DS_Store
.gcloud/
dist/
volume
scrypted.db
out
scrypted.db.bak
.exit
.update
.venv

580
packages/auth-fetch/package-lock.json generated Normal file
View File

@@ -0,0 +1,580 @@
{
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"follow-redirects": "^1.15.4",
"http-auth-utils": "^5.0.1"
},
"devDependencies": {
"@types/node": "^20.9.4",
"rimraf": "^5.0.5",
"typescript": "^5.3.2"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/http-auth-utils": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-5.0.1.tgz",
"integrity": "sha512-YPiLVYdwpBEWB85iWYg7V/ZW3mBfPLCTFQWEiPAA5CKXHJOAPbnJ0xDHLiE6KbPRvrYCwLuWqTG0fLfXVjMUcQ==",
"dependencies": {
"yerror": "^8.0.0"
},
"engines": {
"node": ">=18.16.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
"integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
"dev": true,
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yerror": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-8.0.0.tgz",
"integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==",
"engines": {
"node": ">=18.16.0"
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc --outDir dist",
"prepublishOnly": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@types/node": "^20.9.4",
"rimraf": "^5.0.5",
"typescript": "^5.3.2"
},
"dependencies": {
"follow-redirects": "^1.15.4",
"http-auth-utils": "^5.0.1"
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,130 @@
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, fetcher, getFetchMethod, setDefaultHttpFetchAccept } from '../../../server/src/fetch';
export interface AuthFetchCredentialState {
username: string;
password: string;
[key: string]: any;
}
export interface AuthFetchOptions {
credential?: AuthFetchCredentialState;
}
async function getAuth(options: AuthFetchOptions, url: string | URL, method: string) {
if (!options.credential)
return;
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const { digest, basic } = options.credential as AuthFetchCredentialState & {
count?: number;
digest?: ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
basic?: ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
};
if (digest) {
options.credential.count ||= 0;
++options.credential.count;
const nc = ('00000000' + options.credential.count).slice(-8);
const cnonce = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const uri = new URL(url).pathname;
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');
const response = DIGEST.computeHash({
username: options.credential.username,
password: options.credential.password,
method,
uri,
nc,
cnonce,
algorithm: 'MD5',
qop: digest.data.qop!,
...digest.data,
});
const header = buildAuthorizationHeader(DIGEST, {
username: options.credential.username,
uri,
nc,
cnonce,
algorithm: digest.data.algorithm!,
qop: digest.data.qop!,
response,
...digest.data,
});
return header;
}
else if (basic) {
const { BASIC, buildAuthorizationHeader } = await import('http-auth-utils');
const header = buildAuthorizationHeader(BASIC, {
username: options.credential.username,
password: options.credential.password,
});
return header;
}
}
export function createAuthFetch<B, M>(
h: fetcher<B, M>,
parser: (body: M, responseType: HttpFetchResponseType) => Promise<any>
) {
const authHttpFetch = async <T extends HttpFetchOptions<HttpFetchResponseType, B>>(options: T & AuthFetchOptions): ReturnType<typeof h<T>> => {
const method = getFetchMethod(options);
const headers = new Headers(options.headers);
options.headers = headers;
setDefaultHttpFetchAccept(headers, options.responseType);
const initialHeader = await getAuth(options, options.url, method);
// try to provide an authorization if a session exists, but don't override Authorization if provided already.
// 401 will trigger a proper auth.
if (initialHeader && !headers.has('Authorization'))
headers.set('Authorization', initialHeader);
const initialResponse = await h({
...options,
ignoreStatusCode: true,
responseType: 'readable',
});
if (initialResponse.statusCode !== 401 || !options.credential) {
if (!options?.ignoreStatusCode)
checkStatus(initialResponse.statusCode);
return {
...initialResponse,
body: await parser(initialResponse.body, options.responseType),
};
}
let authenticateHeaders: string | string[] = initialResponse.headers.get('www-authenticate');
if (!authenticateHeaders)
throw new Error('Did not find WWW-Authenticate header.');
if (typeof authenticateHeaders === 'string')
authenticateHeaders = [authenticateHeaders];
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const parsedHeaders = authenticateHeaders.map(h => parseWWWAuthenticateHeader(h));
const digest = parsedHeaders.find(p => p.type === 'Digest') as ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
const basic = parsedHeaders.find(p => p.type === 'Basic') as ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
options.credential.digest = digest;
options.credential.basic = basic;
if (!digest && !basic)
throw new Error(`Unknown WWW-Authenticate type: ${parsedHeaders[0]?.type}`);
const header = await getAuth(options, options.url, method);
if (header)
headers.set('Authorization', header);
return h(options);
}
return authHttpFetch;
}

View File

@@ -0,0 +1,19 @@
import { createAuthFetch } from "./auth-fetch";
import { httpFetch, httpFetchParseIncomingMessage } from '../../../server/src/fetch/http-fetch';
import type { Readable } from "stream";
import type { IncomingMessage } from "http";
import { domFetch, domFetchParseIncomingMessage } from "../../../server/src/fetch";
function init() {
try {
require('net');
require('events');
return createAuthFetch<Readable, IncomingMessage>(httpFetch, httpFetchParseIncomingMessage);
}
catch (e) {
}
return createAuthFetch<BodyInit, Response>(domFetch, domFetchParseIncomingMessage);
}
export const authFetch = init();

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
},
"include": [
"src/**/*"
],
}

View File

@@ -1,27 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ts-node",
"type": "node",
"request": "launch",
"args": [
"${relativeFile}"
],
"runtimeArgs": [
"-r",
"ts-node/register"
],
"env": {
"SCRYPTED_USERNAME": "koush",
"SCRYPTED_PASSWORD": "k9copUSA",
},
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
}
]
}

View File

@@ -1,21 +1,12 @@
import { Camera, VideoCamera, VideoFrameGenerator } from '@scrypted/types';
import { connectScryptedClient } from '../dist/packages/client/src';
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
axiosConfig: {
httpsAgent,
}
});
console.log('server version', sdk.serverVersion);

View File

@@ -1,21 +1,12 @@
import { ObjectDetector, ObjectsDetected, ScryptedInterface } from '@scrypted/types';
import { connectScryptedClient } from '../dist/packages/client/src';
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
axiosConfig: {
httpsAgent,
}
});
console.log('server version', sdk.serverVersion);

View File

@@ -1,21 +1,12 @@
import { connectScryptedClient } from '../dist/packages/client/src';
import { OnOff } from '@scrypted/types';
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
axiosConfig: {
httpsAgent,
}
});
console.log('server version', sdk.serverVersion);

View File

@@ -1,23 +1,22 @@
{
"name": "@scrypted/client",
"version": "1.3.1",
"version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.1",
"version": "1.3.2",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.99",
"axios": "^0.25.0",
"@scrypted/types": "^0.3.4",
"engine.io-client": "^6.5.3",
"rimraf": "^5.0.5"
},
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^20.9.4",
"typescript": "^5.3.2"
"@types/node": "^20.10.8",
"typescript": "^5.3.3"
}
},
"node_modules/@isaacs/cliui": {
@@ -46,9 +45,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.2.99",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -65,9 +64,9 @@
}
},
"node_modules/@types/node": {
"version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
"integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -95,14 +94,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dependencies": {
"follow-redirects": "^1.14.7"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -191,25 +182,6 @@
"node": ">=10.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -470,9 +442,9 @@
}
},
"node_modules/typescript": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@@ -13,12 +13,11 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^20.9.4",
"typescript": "^5.3.2"
"@types/node": "^20.10.8",
"typescript": "^5.3.3"
},
"dependencies": {
"@scrypted/types": "^0.2.99",
"axios": "^0.25.0",
"@scrypted/types": "^0.3.4",
"engine.io-client": "^6.5.3",
"rimraf": "^5.0.5"
}

View File

@@ -1,5 +1,4 @@
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
import { Deferred } from "../../../common/src/deferred";
@@ -14,6 +13,7 @@ import { RpcPeer } from '../../../server/src/rpc';
import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/src/rpc-serializer';
import packageJson from '../package.json';
import { isIPAddress } from "./ip";
import { authFetch } from '../../auth-fetch/src/index'
const sourcePeerId = RpcPeer.generateId();
@@ -59,7 +59,6 @@ export interface ScryptedConnectionOptions {
local?: boolean;
webrtc?: boolean;
baseUrl?: string;
axiosConfig?: AxiosRequestConfig;
previousLoginResult?: ScryptedClientLoginResult;
}
@@ -90,10 +89,13 @@ function isRunningStandalone() {
export async function logoutScryptedClient(baseUrl?: string) {
const url = combineBaseUrl(baseUrl, 'logout');
const response = await axios(url, {
const response = await authFetch({
url,
withCredentials: true,
responseType: 'json',
rejectUnauthorized: false,
});
return response.data;
return response.body;
}
export function getCurrentBaseUrl() {
@@ -122,38 +124,43 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
maxAge = 365 * 24 * 60 * 60 * 1000;
const url = combineBaseUrl(baseUrl, 'login');
const response = await axios.post(url, {
username,
password,
change_password,
maxAge,
}, {
const response = await authFetch({
url,
body: {
username,
password,
change_password,
maxAge,
},
rejectUnauthorized: false,
withCredentials: true,
...options.axiosConfig,
responseType: 'json',
});
if (response.status !== 200)
throw new Error('status ' + response.status);
if (response.statusCode !== 200)
throw new Error('status ' + response.statusCode);
const { body } = response;
return {
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
token: response.data.token as string,
addresses: response.data.addresses as string[],
externalAddresses: response.data.externalAddresses as string[],
error: body.error as string,
authorization: body.authorization as string,
queryToken: body.queryToken as any,
token: body.token as string,
addresses: body.addresses as string[],
externalAddresses: body.externalAddresses as string[],
// the cloud plugin will include this header.
// should maybe move this into the cloud server itself.
scryptedCloud: response.headers['x-scrypted-cloud'] === 'true',
directAddress: response.headers['x-scrypted-direct-address'],
cloudAddress: response.headers['x-scrypted-cloud-address'],
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
directAddress: response.headers.get('x-scrypted-direct-address'),
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
};
}
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
let { baseUrl } = options || {};
let url = combineBaseUrl(baseUrl, 'login');
const headers: AxiosRequestHeaders = {};
const headers = new Headers();
if (options?.previousLoginResult?.queryToken) {
// headers.Authorization = options?.previousLoginResult?.authorization;
// const search = new URLSearchParams(options.previousLoginResult.queryToken);
@@ -161,32 +168,36 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
const token = options?.previousLoginResult.username + ":" + options.previousLoginResult.token;
const hash = Buffer.from(token).toString('base64');
headers.Authorization = `Basic ${hash}`;
headers.set('Authorization', `Basic ${hash}`);
}
const response = await axios.get(url, {
const response = await authFetch({
url,
withCredentials: true,
headers,
...options?.axiosConfig,
rejectUnauthorized: false,
responseType: 'json',
});
const { body } = response;
return {
baseUrl,
hostname: response.data.hostname as string,
redirect: response.data.redirect as string,
username: response.data.username as string,
expiration: response.data.expiration as number,
hasLogin: !!response.data.hasLogin,
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
token: response.data.token as string,
addresses: response.data.addresses as string[],
externalAddresses: response.data.externalAddresses as string[],
hostname: body.hostname as string,
redirect: body.redirect as string,
username: body.username as string,
expiration: body.expiration as number,
hasLogin: !!body.hasLogin,
error: body.error as string,
authorization: body.authorization as string,
queryToken: body.queryToken as any,
token: body.token as string,
addresses: body.addresses as string[],
externalAddresses: body.externalAddresses as string[],
// the cloud plugin will include this header.
// should maybe move this into the cloud server itself.
scryptedCloud: response.headers['x-scrypted-cloud'] === 'true',
directAddress: response.headers['x-scrypted-direct-address'],
cloudAddress: response.headers['x-scrypted-cloud-address'],
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
directAddress: response.headers.get('x-scrypted-direct-address'),
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
};
}
@@ -786,7 +797,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return value;
}
const { port, proxyId } = clusterObject;
const { port, proxyId } = clusterObject;
// check if object is already connected
const resolved = await resolveObject(proxyId, port);

View File

@@ -1,6 +1,5 @@
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { AuthFetchCredentialState, HttpFetchOptions, HttpFetchResponseType, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { Readable } from 'stream';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
import { getDeviceInfo } from './probe';
export enum AmcrestEvent {
@@ -31,17 +30,15 @@ export class AmcrestCameraClient {
};
}
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T, Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
};
const response = await authHttpFetch(options);
});
return response;
}

View File

@@ -1,7 +1,6 @@
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { IncomingMessage, RequestOptions } from 'http';
import { AuthFetchCredentialState, HttpFetchOptions, HttpFetchResponseType, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { IncomingMessage } from 'http';
import { Readable } from 'stream';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
import { getDeviceInfo } from './probe';
export function getChannel(channel: string) {
@@ -41,17 +40,15 @@ export class HikvisionCameraAPI {
};
}
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T, Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
};
const response = await authHttpFetch(options);
});
return response;
}

View File

@@ -1,6 +1,5 @@
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { checkStatus } from '../../../server/src/http-fetch-helpers';
import { checkStatus } from '../../../server/src/fetch';
export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
const response = await authHttpFetch({

View File

@@ -1,12 +1,7 @@
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { HttpFetchOptions, HttpFetchResponseType, AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { EventEmitter } from 'events';
import https, { RequestOptions } from 'https';
import https from 'https';
import { Readable } from 'stream';
import { FetchParser, HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
const onvif = require('onvif');
const { Cam } = onvif;
@@ -71,17 +66,15 @@ export class OnvifCameraAPI {
};
}
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T, Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
};
const response = await authHttpFetch(options);
});
return response;
}

View File

@@ -2,7 +2,7 @@ import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scry
import { EventEmitter } from 'events';
import https, { RequestOptions } from 'https';
import { PassThrough, Readable } from 'stream';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/fetch/http-fetch';
import { getMotionState, reolinkHttpsAgent } from './probe';
import { PanTiltZoomCommand } from "@scrypted/sdk";
@@ -69,17 +69,15 @@ export class ReolinkCameraClient {
};
}
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T, Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
};
const response = await authHttpFetch(options);
});
return response;
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.81.0",
"version": "0.82.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.81.0",
"version": "0.82.0",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",

View File

@@ -1,109 +1,46 @@
import { once } from 'events';
import { http, https } from 'follow-redirects';
import { IncomingMessage } from 'http';
import type { Readable } from 'stream';
export type HttpFetchResponseType = 'json' | 'text' | 'buffer' | 'readable';
export interface HttpFetchOptions<T extends HttpFetchResponseType> {
url: string | URL;
family?: 4 | 6;
method?: string;
headers?: HeadersInit;
timeout?: number;
rejectUnauthorized?: boolean;
ignoreStatusCode?: boolean;
body?: Readable;
responseType?: T;
}
export interface HttpFetchJsonOptions extends HttpFetchOptions<'json'> {
}
export interface HttpFetchBufferOptions extends HttpFetchOptions<'buffer'> {
}
export interface HttpFetchTextOptions extends HttpFetchOptions<'text'> {
}
export interface HttpFetchReadableOptions extends HttpFetchOptions<'readable'> {
}
import type events from 'events';
import type stream from 'stream';
import type followRedirects from 'follow-redirects';
import type { IncomingMessage } from 'http';
import { Readable } from 'stream';
import { HttpFetchBufferOptions, HttpFetchJsonOptions, HttpFetchOptions, HttpFetchReadableOptions, HttpFetchResponse, HttpFetchResponseType, HttpFetchTextOptions, checkStatus, createStringOrBufferBody, getFetchMethod, setDefaultHttpFetchAccept } from '.';
export type { HttpFetchBufferOptions, HttpFetchJsonOptions, HttpFetchOptions, HttpFetchReadableOptions, HttpFetchResponse, HttpFetchResponseType, HttpFetchTextOptions, checkStatus, setDefaultHttpFetchAccept } from '.';
async function readMessageBuffer(response: IncomingMessage) {
const buffers: Buffer[] = [];
response.on('data', buffer => buffers.push(buffer));
const { once } = require('events') as typeof events;
await once(response, 'end');
return Buffer.concat(buffers);
}
export interface FetchParser<T> {
accept: string;
parse(message: IncomingMessage): Promise<T>;
}
const TextParser: FetchParser<string> = {
accept: 'text/plain',
async parse(message: IncomingMessage) {
return (await readMessageBuffer(message)).toString();
}
}
const JSONParser: FetchParser<any> = {
accept: 'application/json',
async parse(message: IncomingMessage) {
return JSON.parse((await readMessageBuffer(message)).toString());
}
}
const BufferParser: FetchParser<Buffer> = {
accept: undefined as string,
async parse(message: IncomingMessage) {
return readMessageBuffer(message);
}
}
const StreamParser: FetchParser<IncomingMessage> = {
accept: undefined as string,
async parse(message: IncomingMessage) {
return message;
}
}
export async function getNpmPackageInfo(pkg: string) {
const { body } = await httpFetch({
url: `https://registry.npmjs.org/${pkg}`,
// force ipv4 in case of busted ipv6.
family: 4,
responseType: 'json',
});
return body;
}
export function getHttpFetchAccept(responseType: HttpFetchResponseType) {
const { accept } = getHttpFetchParser(responseType);
return accept;
}
export function setDefaultHttpFetchAccept(headers: Headers, responseType: HttpFetchResponseType) {
if (headers.has('Accept'))
return;
const { accept } = getHttpFetchParser(responseType);
if (accept)
headers.set('Accept', accept);
}
export function fetchStatusCodeOk(statusCode: number) {
return statusCode >= 200 && statusCode <= 299;
}
export function checkStatus(statusCode: number) {
if (!fetchStatusCodeOk(statusCode))
throw new Error(`http response statusCode ${statusCode}`);
}
export interface HttpFetchResponse<T> {
statusCode: number;
headers: Headers;
body: T;
}
export function getHttpFetchParser(responseType: HttpFetchResponseType) {
switch (responseType) {
case 'json':
@@ -116,21 +53,23 @@ export function getHttpFetchParser(responseType: HttpFetchResponseType) {
return BufferParser;
}
export function parseResponseType(readable: IncomingMessage, responseType: HttpFetchResponseType) {
export function httpFetchParseIncomingMessage(readable: IncomingMessage, responseType: HttpFetchResponseType) {
return getHttpFetchParser(responseType).parse(readable);
}
export async function httpFetch<T extends HttpFetchOptions<HttpFetchResponseType>>(options: T): Promise<HttpFetchResponse<
export async function httpFetch<T extends HttpFetchOptions<HttpFetchResponseType, Readable>>(options: T): Promise<HttpFetchResponse<
// first one serves as default.
T extends HttpFetchBufferOptions ? Buffer
: T extends HttpFetchTextOptions ? string
: T extends HttpFetchReadableOptions ? IncomingMessage
: T extends HttpFetchJsonOptions ? any : Buffer
T extends HttpFetchBufferOptions<Readable> ? Buffer
: T extends HttpFetchTextOptions<Readable> ? string
: T extends HttpFetchReadableOptions<Readable> ? IncomingMessage
: T extends HttpFetchJsonOptions<Readable> ? any : Buffer
>> {
const headers = new Headers(options.headers);
setDefaultHttpFetchAccept(headers, options.responseType);
const parser = getHttpFetchParser(options.responseType);
const { once } = require('events') as typeof events;
const { PassThrough } = require('stream') as typeof stream;
const { http, https } = require('follow-redirects') as typeof followRedirects;
const { url } = options;
const isSecure = url.toString().startsWith('https:');
@@ -147,12 +86,19 @@ export async function httpFetch<T extends HttpFetchOptions<HttpFetchResponseType
}
const request = proto.request(url, {
method: options.method,
method: getFetchMethod(options),
rejectUnauthorized: options.rejectUnauthorized,
family: options.family,
headers: nodeHeaders,
timeout: options.timeout,
});
let { body } = options;
if (body && !(body instanceof Readable)) {
body = new PassThrough();
body = body.write(Buffer.from(createStringOrBufferBody(headers, body)));
}
if (options.body)
options.body.pipe(request);
else
@@ -179,7 +125,7 @@ export async function httpFetch<T extends HttpFetchOptions<HttpFetchResponseType
return {
statusCode: response.statusCode,
headers: incomingHeaders,
body: await parser.parse(response),
body: await httpFetchParseIncomingMessage(response, options.responseType),
};
}

140
server/src/fetch/index.ts Normal file
View File

@@ -0,0 +1,140 @@
export type HttpFetchResponseType = 'json' | 'text' | 'buffer' | 'readable';
export interface HttpFetchOptions<T extends HttpFetchResponseType, B> {
url: string | URL;
family?: 4 | 6;
method?: string;
headers?: HeadersInit;
timeout?: number;
rejectUnauthorized?: boolean;
ignoreStatusCode?: boolean;
body?: B | string | ArrayBufferView | any;
responseType?: T;
withCredentials?: boolean;
}
export interface HttpFetchJsonOptions<B> extends HttpFetchOptions<'json', B> {
}
export interface HttpFetchBufferOptions<B> extends HttpFetchOptions<'buffer', B> {
}
export interface HttpFetchTextOptions<B> extends HttpFetchOptions<'text', B> {
}
export interface HttpFetchReadableOptions<B> extends HttpFetchOptions<'readable', B> {
}
export function fetchStatusCodeOk(statusCode: number) {
return statusCode >= 200 && statusCode <= 299;
}
export function checkStatus(statusCode: number) {
if (!fetchStatusCodeOk(statusCode))
throw new Error(`http response statusCode ${statusCode}`);
}
export function getFetchMethod(options: HttpFetchOptions<any, any>) {
const method = options.method || (options.body ? 'POST' : 'GET');
return method;
}
export interface HttpFetchResponse<T> {
statusCode: number;
headers: Headers;
body: T;
}
export type fetcher<B, M> = <T extends HttpFetchOptions<HttpFetchResponseType, B>>(options: T) => Promise<HttpFetchResponse<
// first one serves as default.
T extends HttpFetchBufferOptions<B> ? Buffer
: T extends HttpFetchTextOptions<B> ? string
: T extends HttpFetchReadableOptions<B> ? M
: T extends HttpFetchJsonOptions<B> ? any : Buffer
>>;
export function getHttpFetchAccept(responseType: HttpFetchResponseType) {
switch (responseType) {
case 'json':
return 'application/json';
case 'text':
return 'text/plain';
}
return;
}
export function setDefaultHttpFetchAccept(headers: Headers, responseType: HttpFetchResponseType) {
if (headers.has('Accept'))
return;
const accept = getHttpFetchAccept(responseType);
if (accept)
headers.set('Accept', accept);
}
export function createStringOrBufferBody(headers: Headers, body: any) {
let contentType: string;
if (typeof body === 'object') {
body = JSON.stringify(body);
contentType = 'application/json';
}
else if (typeof body === 'string') {
contentType = 'text/plain';
}
if (!headers.has('Content-Type'))
headers.set('Content-Type', contentType);
return body;
}
export async function domFetchParseIncomingMessage(response: Response, responseType: HttpFetchResponseType) {
switch (responseType) {
case 'json':
return response.json();
case 'text':
return response.text();
case 'readable':
return response;
}
return new Uint8Array(await response.arrayBuffer());
}
export async function domFetch<T extends HttpFetchOptions<HttpFetchResponseType, BodyInit>>(options: T): Promise<HttpFetchResponse<
// first one serves as default.
T extends HttpFetchBufferOptions<BodyInit> ? Buffer
: T extends HttpFetchTextOptions<BodyInit> ? string
: T extends HttpFetchReadableOptions<BodyInit> ? Response
: T extends HttpFetchJsonOptions<BodyInit> ? any : Buffer
>> {
const headers = new Headers(options.headers);
setDefaultHttpFetchAccept(headers, options.responseType);
let { body } = options;
if (body && !(body instanceof ReadableStream)) {
body = createStringOrBufferBody(headers, body);
}
const { url } = options;
const response = await fetch(url, {
method: getFetchMethod(options),
headers,
signal: options.timeout ? AbortSignal.timeout(options.timeout) : undefined,
body,
});
if (!options?.ignoreStatusCode) {
try {
checkStatus(response.status);
}
catch (e) {
response.arrayBuffer().catch(() => { });
throw e;
}
}
return {
statusCode: response.status,
headers: response.headers,
body: await domFetchParseIncomingMessage(response, options.responseType),
};
}

View File

@@ -20,7 +20,7 @@ import WebSocket, { Server as WebSocketServer } from "ws";
import { computeClusterObjectHash } from './cluster/cluster-hash';
import { ClusterObject } from './cluster/connect-rpc-object';
import { Plugin, PluginDevice, ScryptedAlert, ScryptedUser } from './db-types';
import { getNpmPackageInfo, httpFetch } from './http-fetch-helpers';
import { httpFetch } from './fetch/http-fetch';
import { createResponseInterface } from './http-interfaces';
import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults';
import { IOServer } from './io';
@@ -42,7 +42,7 @@ import { AddressSettings } from './services/addresses';
import { Alerts } from './services/alerts';
import { CORSControl } from './services/cors';
import { Info } from './services/info';
import { PluginComponent } from './services/plugin';
import { getNpmPackageInfo, PluginComponent } from './services/plugin';
import { ServiceControl } from './services/service-control';
import { UsersService } from './services/users';
import { getState, ScryptedStateManager, setState } from './state';

View File

@@ -16,13 +16,13 @@ import semver from 'semver';
import { install as installSourceMapSupport } from 'source-map-support';
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
import { Plugin, ScryptedUser, Settings } from './db-types';
import { getNpmPackageInfo } from './http-fetch-helpers';
import Level from './level';
import { PluginError } from './plugin/plugin-error';
import { getScryptedVolume } from './plugin/plugin-volume';
import { RPCResultError } from './rpc';
import { ScryptedRuntime } from './runtime';
import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
import { getNpmPackageInfo } from './services/plugin';
import { setScryptedUserPassword, UsersService } from './services/users';
import { sleep } from './sleep';
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';

View File

@@ -1,11 +1,22 @@
import { ScryptedInterfaceProperty, ScryptedNativeId } from "@scrypted/types";
import semver from 'semver';
import { Plugin } from '../db-types';
import { getNpmPackageInfo } from "../http-fetch-helpers";
import { hasMixinCycle } from "../mixin/mixin-cycle";
import { ScryptedRuntime } from "../runtime";
import { sleep } from "../sleep";
import { getState } from "../state";
import { httpFetch } from "../fetch/http-fetch";
export async function getNpmPackageInfo(pkg: string) {
const { body } = await httpFetch({
url: `https://registry.npmjs.org/${pkg}`,
// force ipv4 in case of busted ipv6.
family: 4,
responseType: 'json',
});
return body;
}
export class PluginComponent {
scrypted: ScryptedRuntime;

View File

@@ -1,5 +1,5 @@
import fs from 'fs';
import { httpFetch } from "../http-fetch-helpers";
import { httpFetch } from "../fetch/http-fetch";
import { ScryptedRuntime } from "../runtime";
export class ServiceControl {