Skip to content

Commit

Permalink
✨ GM_xhr支持document和stream
Browse files Browse the repository at this point in the history
  • Loading branch information
CodFrm committed Dec 17, 2022
1 parent 9445dbc commit eedf0c1
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 13 deletions.
58 changes: 56 additions & 2 deletions src/runtime/background/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ import PermissionVerify, {
ConfirmParam,
IPermissionVerify,
} from "./permission_verify";
import { dealXhr, getIcon, listenerWebRequest, setXhrHeader } from "./utils";
import {
dealFetch,
dealXhr,
getFetchHeader,
getIcon,
listenerWebRequest,
setXhrHeader,
uint8ToArray,
} from "./utils";

// GMApi,处理脚本的GM API调用请求

Expand Down Expand Up @@ -150,6 +158,49 @@ export default class GMApi {
return this.valueManager.setValue(request.script, key, value, sender);
}

// 处理GM_xmlhttpRequest fetch的情况,先只处理ReadableStream的情况
// 且不考虑复杂的情况
CAT_fetch(request: Request, channel: Channel): Promise<any> {
const config = <GMSend.XHRDetails>request.params[0];
const { url } = config;
return fetch(url, {
method: config.method || "GET",
body: <any>config.data,
headers: getFetchHeader(this.systemConfig.scriptCatFlag, config),
})
.then((resp) => {
const send = dealFetch(
this.systemConfig.scriptCatFlag,
config,
resp,
1
);
const reader = resp.body?.getReader();
if (!reader) {
throw new Error("read is not found");
}
const { scriptCatFlag } = this.systemConfig;
reader.read().then(function read({ done, value }) {
if (done) {
const data = dealFetch(scriptCatFlag, config, resp, 4);
channel.send({ event: "onreadystatechange", data });
channel.send({ event: "onload", data });
channel.send({ event: "onloadend", data });
channel.disChannel();
} else {
channel.send({ event: "onstream", data: Array.from(value) });
reader.read().then(read);
}
});
channel.send({ event: "onloadstart", data: send });
send.readyState = 2;
channel.send({ event: "onreadystatechange", data: send });
})
.catch((e) => {
channel.throw(e);
});
}

@PermissionVerify.API({
confirm: (request: Request) => {
const config = <GMSend.XHRDetails>request.params[0];
Expand Down Expand Up @@ -181,7 +232,10 @@ export default class GMApi {
})
async GM_xmlhttpRequest(request: Request, channel: Channel): Promise<any> {
const config = <GMSend.XHRDetails>request.params[0];

if (config.responseType === "stream") {
// 只有fetch支持ReadableStream
return this.CAT_fetch(request, channel);
}
const xhr = new XMLHttpRequest();
xhr.open(
config.method || "GET",
Expand Down
56 changes: 56 additions & 0 deletions src/runtime/background/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,39 @@ export function setXhrHeader(
}
}

export function getFetchHeader(
headerFlag: string,
config: GMSend.XHRDetails
): any {
const headers: { [key: string]: string } = {};
headers[`${headerFlag}-gm-xhr`] = "true";
if (config.headers) {
Object.keys(config.headers).forEach((key) => {
const lowKey = key.toLowerCase();
if (
unsafeHeaders[lowKey] ||
lowKey.startsWith("sec-") ||
lowKey.startsWith("proxy-")
) {
headers[`${headerFlag}-${lowKey}`] = config.headers![key]!;
} else {
// 直接设置header
headers[key] = config.headers![key]!;
}
});
}
if (config.maxRedirects !== undefined) {
headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString();
}
if (config.cookie) {
headers[`${headerFlag}-cookie`] = config.cookie;
}
if (config.anonymous) {
headers[`${headerFlag}-anonymous`] = "true";
}
return headers;
}

export async function dealXhr(
headerFlag: string,
config: GMSend.XHRDetails,
Expand Down Expand Up @@ -329,6 +362,29 @@ export async function dealXhr(
return Promise.resolve(respond);
}

export function dealFetch(
headerFlag: string,
config: GMSend.XHRDetails,
response: Response,
readyState: 0 | 1 | 2 | 3 | 4
) {
const removeXCat = new RegExp(`${headerFlag}-`, "g");
let respHeader = "";
response.headers &&
response.headers.forEach((value, key) => {
respHeader += `${key.replace(removeXCat, "")}: ${value}\n`;
});
const respond: GMTypes.XHRResponse = {
finalUrl: response.url || config.url,
readyState,
status: response.status,
statusText: response.statusText,
responseHeaders: respHeader,
responseType: config.responseType,
};
return respond;
}

export function getIcon(script: Script): string {
return (
(script.metadata.icon && script.metadata.icon[0]) ||
Expand Down
14 changes: 14 additions & 0 deletions src/runtime/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,19 @@ export default class ContentRuntime {
return Promise.resolve(url);
}
);
// 处理CAT_fetchDocument
this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "document";
xhr.open("GET", data);
xhr.onload = () => {
resolve({
relatedTarget: xhr.response,
});
};
xhr.send();
});
});
}
}
73 changes: 66 additions & 7 deletions src/runtime/content/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,37 @@ export default class GMApi {
return this.message.syncSend("CAT_fetchBlob", url);
}

@GMContext.API()
public CAT_fetchDocument(url: string): Promise<Document | undefined> {
return new Promise((resolve) => {
let el: Document | undefined;
(<MessageContent>this.message).sendCallback(
"CAT_fetchDocument",
url,
(resp) => {
el = <Document>(
(<unknown>(
(<MessageContent>this.message).getAndDelRelatedTarget(
resp.relatedTarget
)
))
);
resolve(el);
}
);
});
}

// 辅助GM_xml发送blob数据
@GMContext.API()
public CAT_createBlobUrl(blob: Blob): Promise<string> {
return this.message.syncSend("CAT_createBlobUrl", blob);
}

// 用于脚本跨域请求,需要@connect domain指定允许的域名
@GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl"] })
@GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
})
public GM_xmlhttpRequest(details: GMTypes.XHRDetails) {
let connect: Channel;

Expand Down Expand Up @@ -300,24 +323,45 @@ export default class GMApi {
}
}

let readerStream: ReadableStream<Uint8Array> | undefined;
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
// 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob
// 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象
const responseType = details.responseType?.toLocaleLowerCase();
const warpResponse = (old: Function) => {
if (responseType === "stream") {
readerStream = new ReadableStream<Uint8Array>({
start(ctrl) {
controller = ctrl;
},
});
}
return async (xhr: GMTypes.XHRResponse) => {
if (xhr.response) {
const resp = await this.CAT_fetchBlob(<string>xhr.response);
if (details.responseType === "arraybuffer") {
xhr.response = await resp.arrayBuffer();
if (responseType === "document") {
xhr.response = await this.CAT_fetchDocument(<string>xhr.response);
xhr.responseXML = xhr.response;
xhr.responseType = "document";
} else {
xhr.response = resp;
const resp = await this.CAT_fetchBlob(<string>xhr.response);
if (responseType === "arraybuffer") {
xhr.response = await resp.arrayBuffer();
} else {
xhr.response = resp;
}
}
}
if (responseType === "stream") {
xhr.response = readerStream;
}
old(xhr);
};
};
if (
details.responseType?.toLowerCase() === "arraybuffer" ||
details.responseType?.toLocaleLowerCase() === "blob"
responseType === "arraybuffer" ||
responseType === "blob" ||
responseType === "document" ||
responseType === "stream"
) {
if (details.onload) {
details.onload = warpResponse(details.onload);
Expand All @@ -328,6 +372,15 @@ export default class GMApi {
if (details.onloadend) {
details.onloadend = warpResponse(details.onloadend);
}
// document类型读取blob,然后在content页转化为document对象
if (responseType === "document") {
param.responseType = "blob";
}
if (responseType === "stream") {
if (details.onloadstart) {
details.onloadstart = warpResponse(details.onloadstart);
}
}
}

connect = this.connect("GM_xmlhttpRequest", [param], (resp: any) => {
Expand All @@ -338,6 +391,9 @@ export default class GMApi {
break;
case "onloadend":
details.onloadend && details.onloadend(data);
if (readerStream) {
controller?.close();
}
break;
case "onloadstart":
details.onloadstart && details.onloadstart(data);
Expand All @@ -357,6 +413,9 @@ export default class GMApi {
case "onabort":
details.onabort && details.onabort();
break;
case "onstream":
controller?.enqueue(new Uint8Array(resp.data));
break;
default:
LoggerCore.getLogger().warn("GM_xmlhttpRequest resp is error", {
resp,
Expand Down
8 changes: 7 additions & 1 deletion src/types/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ declare namespace GMSend {
binary?: boolean;
timeout?: number;
context?: CONTEXT_TYPE;
responseType?: "text" | "arraybuffer" | "blob" | "json";
responseType?:
| "text"
| "arraybuffer"
| "blob"
| "json"
| "document"
| "stream";
overrideMimeType?: string;
anonymous?: boolean;
fetch?: boolean;
Expand Down
18 changes: 15 additions & 3 deletions src/types/scriptcat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,16 @@ declare namespace GMTypes {
responseHeaders?: string;
status?: number;
statusText?: string;
response?: any;
response?: string | Blob | ArrayBuffer | Document | ReadableStream | null;
responseText?: string;
responseXML?: Document | null;
responseType?: "text" | "arraybuffer" | "blob" | "json";
responseType?:
| "text"
| "arraybuffer"
| "blob"
| "json"
| "document"
| "stream";
}

interface XHRProgress extends XHRResponse {
Expand All @@ -280,7 +286,13 @@ declare namespace GMTypes {
binary?: boolean;
timeout?: number;
context?: ContextType;
responseType?: "text" | "arraybuffer" | "blob" | "json";
responseType?:
| "text"
| "arraybuffer"
| "blob"
| "json"
| "document"
| "stream"; // stream 在当前版本是一个较为简陋的实现
overrideMimeType?: string;
anonymous?: boolean;
fetch?: boolean;
Expand Down

0 comments on commit eedf0c1

Please sign in to comment.