mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 14:13:28 +00:00
various: more http refactoring
This commit is contained in:
@@ -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
11
packages/auth-fetch/.gitignore
vendored
Normal 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
580
packages/auth-fetch/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/auth-fetch/package.json
Normal file
23
packages/auth-fetch/package.json
Normal 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"
|
||||
}
|
||||
130
packages/auth-fetch/src/auth-fetch.ts
Normal file
130
packages/auth-fetch/src/auth-fetch.ts
Normal 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;
|
||||
}
|
||||
19
packages/auth-fetch/src/index.ts
Normal file
19
packages/auth-fetch/src/index.ts
Normal 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();
|
||||
16
packages/auth-fetch/tsconfig.json
Normal file
16
packages/auth-fetch/tsconfig.json
Normal 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/**/*"
|
||||
],
|
||||
}
|
||||
27
packages/client/.vscode/launch.json
vendored
27
packages/client/.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
56
packages/client/package-lock.json
generated
56
packages/client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
140
server/src/fetch/index.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user