diff --git a/package-lock.json b/package-lock.json index ef21fac5c..2dbdcd45f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "expo-web-browser": "~12.3.2", "i18n-js": "^3.8.0", "lodash-es": "^4.17.21", + "protobufjs": "^7.2.6", "qs": "^6.11.2", "react": "18.2.0", "react-native": "0.72.10", @@ -4328,6 +4329,60 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@react-native-community/cli": { "version": "11.3.10", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.10.tgz", @@ -13462,6 +13517,11 @@ "node": ">=6" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -15675,6 +15735,29 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index c60e9c7ba..7d4dbaff4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "expo-web-browser": "~12.3.2", "i18n-js": "^3.8.0", "lodash-es": "^4.17.21", + "protobufjs": "^7.2.6", "qs": "^6.11.2", "react": "18.2.0", "react-native": "0.72.10", diff --git a/src/plugins/helpers/fetch.ts b/src/plugins/helpers/fetch.ts index 489b18802..fd23491c9 100644 --- a/src/plugins/helpers/fetch.ts +++ b/src/plugins/helpers/fetch.ts @@ -1,4 +1,5 @@ import { getUserAgent } from '@hooks/persisted/useUserAgent'; +import { parse as parseProto } from 'protobufjs'; type FetchInit = { headers?: Record | Headers; @@ -111,3 +112,95 @@ export const fetchText = async ( return ''; } }; + +function base64ToBytesArr(str: string) { + const abc = [ + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + ]; // base64 alphabet + let result = []; + + for (let i = 0; i < str.length / 4; i++) { + let chunk = [...str.slice(4 * i, 4 * i + 4)]; + let bin = chunk + .map(x => abc.indexOf(x).toString(2).padStart(6, '0')) + .join(''); + let bytes = bin.match(/.{1,8}/g)?.map(x => +('0b' + x)) || []; + result.push( + ...bytes.slice( + 0, + 3 - Number(str[4 * i + 2] === '=') - Number(str[4 * i + 3] === '='), + ), + ); + } + return result; +} + +interface ProtoRequestInit { + // merged .proto file + proto: string; + requestType: string; + requestData?: any; + responseType: string; +} + +const BYTE_MARK = BigInt((1 << 8) - 1); + +export const fetchProto = async function ( + protoInit: ProtoRequestInit, + url: string, + init?: FetchInit, +) { + const protoRoot = parseProto(protoInit.proto).root; + const RequestMessge = protoRoot.lookupType(protoInit.requestType); + if (RequestMessge.verify(protoInit.requestData)) { + throw new Error('Invalid Proto'); + } + // encode request data + const encodedrequest = RequestMessge.encode(protoInit.requestData).finish(); + const requestLength = BigInt(encodedrequest.length); + const headers = new Uint8Array( + Array(5) + .fill(0) + .map((v, idx) => { + if (idx === 0) { + return 0; + } + return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK); + }), + ); + init = await makeInit(init); + const bodyArray = new Uint8Array(headers.length + encodedrequest.length); + bodyArray.set(headers, 0); + bodyArray.set(encodedrequest, headers.length); + + return fetch(url, { + method: 'POST', + ...init, + body: bodyArray, + } as RequestInit) + .then(r => r.blob()) + .then(blob => { + // decode response data + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onloadend = () => { + const payload = new Uint8Array( + base64ToBytesArr( + fr.result.slice(FILE_READER_PREFIX_LENGTH) as string, + ), + ); + const length = Number( + BigInt(payload[1] << 24) | + BigInt(payload[2] << 16) | + BigInt(payload[3] << 8) | + BigInt(payload[4]), + ); + const ResponseMessage = protoRoot.lookupType(protoInit.responseType); + resolve(ResponseMessage.decode(payload.slice(5, 5 + length))); + }; + fr.onerror = () => reject(); + fr.onabort = () => reject(); + fr.readAsDataURL(blob); + }); + }); +}; diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 246e7b73b..6542943eb 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -10,7 +10,7 @@ import qs from 'qs'; import { NovelStatus, Plugin, PluginItem } from './types'; import { FilterTypes } from './types/filterTypes'; import { isUrlAbsolute } from './helpers/isAbsoluteUrl'; -import { fetchApi, fetchFile, fetchText } from './helpers/fetch'; +import { fetchApi, fetchFile, fetchProto, fetchText } from './helpers/fetch'; import { defaultCover } from './helpers/constants'; import { encode, decode } from 'urlencode'; import TextFile from '@native/TextFile'; @@ -23,7 +23,7 @@ const packages: Record = { 'qs': qs, 'urlencode': { encode, decode }, '@libs/novelStatus': { NovelStatus }, - '@libs/fetch': { fetchApi, fetchFile, fetchText }, + '@libs/fetch': { fetchApi, fetchFile, fetchText, fetchProto }, '@libs/isAbsoluteUrl': { isUrlAbsolute }, '@libs/filterInputs': { FilterTypes }, '@libs/defaultCover': { defaultCover },