snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
347
plugins/http/guest-js/index.ts
Normal file
347
plugins/http/guest-js/index.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Make HTTP requests with the Rust backend.
|
||||
*
|
||||
* ## Security
|
||||
*
|
||||
* This API has a scope configuration that forces you to restrict the URLs that can be accessed using glob patterns.
|
||||
*
|
||||
* For instance, this scope configuration only allows making HTTP requests to all subdomains for `tauri.app` except for `https://private.tauri.app`:
|
||||
* ```json
|
||||
* {
|
||||
* "permissions": [
|
||||
* {
|
||||
* "identifier": "http:default",
|
||||
* "allow": [{ "url": "https://*.tauri.app" }],
|
||||
* "deny": [{ "url": "https://private.tauri.app" }]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
* Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/**
|
||||
* Configuration of a proxy that a Client should pass requests to.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface Proxy {
|
||||
/**
|
||||
* Proxy all traffic to the passed URL.
|
||||
*/
|
||||
all?: string | ProxyConfig
|
||||
/**
|
||||
* Proxy all HTTP traffic to the passed URL.
|
||||
*/
|
||||
http?: string | ProxyConfig
|
||||
/**
|
||||
* Proxy all HTTPS traffic to the passed URL.
|
||||
*/
|
||||
https?: string | ProxyConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed configuration of a single proxy server, used when a plain URL string is not enough.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ProxyConfig {
|
||||
/**
|
||||
* The URL of the proxy server.
|
||||
*/
|
||||
url: string
|
||||
/**
|
||||
* Set the `Proxy-Authorization` header using Basic auth.
|
||||
*/
|
||||
basicAuth?: {
|
||||
/**
|
||||
* The user name sent to the proxy server.
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* The password sent to the proxy server.
|
||||
*/
|
||||
password: string
|
||||
}
|
||||
/**
|
||||
* A configuration for filtering out requests that shouldn't be proxied.
|
||||
* Entries are expected to be comma-separated (whitespace between entries is ignored)
|
||||
*/
|
||||
noProxy?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to configure the Rust client used to make fetch requests
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
/**
|
||||
* Defines the maximum number of redirects the client should follow.
|
||||
* If set to 0, no redirects will be followed.
|
||||
*
|
||||
* When the `scopeRedirects` plugin configuration is enabled, every redirect must
|
||||
* also be allowed by the configured scope, otherwise the request fails
|
||||
* instead of being followed.
|
||||
*/
|
||||
maxRedirections?: number
|
||||
/** Timeout in milliseconds */
|
||||
connectTimeout?: number
|
||||
/**
|
||||
* Configuration of a proxy that a Client should pass requests to.
|
||||
*/
|
||||
proxy?: Proxy
|
||||
/**
|
||||
* Configuration for dangerous settings on the client such as disabling SSL verification.
|
||||
*/
|
||||
danger?: DangerousSettings
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for dangerous settings on the client such as disabling SSL verification.
|
||||
*
|
||||
* @since 2.3.0
|
||||
*/
|
||||
export interface DangerousSettings {
|
||||
/**
|
||||
* Disables SSL verification.
|
||||
*/
|
||||
acceptInvalidCerts?: boolean
|
||||
/**
|
||||
* Disables hostname verification.
|
||||
*/
|
||||
acceptInvalidHostnames?: boolean
|
||||
}
|
||||
|
||||
const ERROR_REQUEST_CANCELLED = 'Request cancelled'
|
||||
|
||||
/**
|
||||
* Fetch a resource from the network. It returns a `Promise` that resolves to the
|
||||
* `Response` to that `Request`, whether it is successful or not.
|
||||
*
|
||||
* The request is performed by the Rust backend instead of the webview, so it is not subject to
|
||||
* CORS, but the URL must be allowed by the plugin scope.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { fetch } from '@tauri-apps/plugin-http';
|
||||
* const response = await fetch("http://my.json.host/data.json");
|
||||
* console.log(response.status); // e.g. 200
|
||||
* console.log(response.statusText); // e.g. "OK"
|
||||
* const jsonData = await response.json();
|
||||
* ```
|
||||
*
|
||||
* @param input The resource to fetch, as a URL, a string or a `Request` object.
|
||||
* @param init The standard `fetch` request options, extended with the Rust client options from
|
||||
* {@linkcode ClientOptions}: `maxRedirections`, `connectTimeout`, `proxy` and `danger`. The
|
||||
* `signal` option can be used to abort the request.
|
||||
* @returns A promise resolving to the `Response` of the request.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export async function fetch(
|
||||
input: URL | Request | string,
|
||||
init?: RequestInit & ClientOptions
|
||||
): Promise<Response> {
|
||||
// Optimistically check for abort signal and avoid doing any work
|
||||
const signal = init?.signal
|
||||
if (signal?.aborted) {
|
||||
throw new Error(ERROR_REQUEST_CANCELLED)
|
||||
}
|
||||
|
||||
const maxRedirections = init?.maxRedirections
|
||||
const connectTimeout = init?.connectTimeout
|
||||
const proxy = init?.proxy
|
||||
const danger = init?.danger
|
||||
|
||||
// Remove these fields before creating the request
|
||||
if (init) {
|
||||
delete init.maxRedirections
|
||||
delete init.connectTimeout
|
||||
delete init.proxy
|
||||
delete init.danger
|
||||
}
|
||||
|
||||
const headers = init?.headers
|
||||
? init.headers instanceof Headers
|
||||
? init.headers
|
||||
: new Headers(init.headers)
|
||||
: new Headers()
|
||||
|
||||
const req = new Request(input, init)
|
||||
const buffer = await req.arrayBuffer()
|
||||
const data =
|
||||
buffer.byteLength !== 0 ? Array.from(new Uint8Array(buffer)) : null
|
||||
|
||||
// append new headers created by the browser `Request` implementation,
|
||||
// if not already declared by the caller of this function
|
||||
for (const [key, value] of req.headers) {
|
||||
if (!headers.get(key)) {
|
||||
headers.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const headersArray =
|
||||
headers instanceof Headers
|
||||
? Array.from(headers.entries())
|
||||
: Array.isArray(headers)
|
||||
? headers
|
||||
: Object.entries(headers)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const mappedHeaders: Array<[string, string]> = headersArray.map(
|
||||
([name, val]) => [
|
||||
name,
|
||||
// we need to ensure we have all header values as strings
|
||||
// eslint-disable-next-line
|
||||
typeof val === 'string' ? val : (val as any).toString()
|
||||
]
|
||||
)
|
||||
|
||||
// Optimistically check for abort signal and avoid doing any work on the Rust side
|
||||
if (signal?.aborted) {
|
||||
throw new Error(ERROR_REQUEST_CANCELLED)
|
||||
}
|
||||
|
||||
const rid = await invoke<number>('plugin:http|fetch', {
|
||||
clientConfig: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
headers: mappedHeaders,
|
||||
data,
|
||||
maxRedirections,
|
||||
connectTimeout,
|
||||
proxy,
|
||||
danger
|
||||
}
|
||||
})
|
||||
|
||||
const abort = () =>
|
||||
invoke('plugin:http|fetch_cancel', { rid }).catch(() => {})
|
||||
|
||||
// Optimistically check for abort signal
|
||||
// and avoid doing any work after doing intial work on the Rust side
|
||||
if (signal?.aborted) {
|
||||
// we don't care about the result of this promise
|
||||
void abort()
|
||||
throw new Error(ERROR_REQUEST_CANCELLED)
|
||||
}
|
||||
|
||||
signal?.addEventListener('abort', () => void abort())
|
||||
|
||||
interface FetchSendResponse {
|
||||
status: number
|
||||
statusText: string
|
||||
headers: [[string, string]]
|
||||
url: string
|
||||
rid: number
|
||||
}
|
||||
|
||||
const {
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
headers: responseHeaders,
|
||||
rid: responseRid
|
||||
} = await invoke<FetchSendResponse>('plugin:http|fetch_send', {
|
||||
rid
|
||||
})
|
||||
|
||||
let bodyDropped = false
|
||||
const dropBody = () => {
|
||||
if (bodyDropped) return Promise.resolve()
|
||||
bodyDropped = true
|
||||
return invoke('plugin:http|fetch_cancel_body', { rid: responseRid }).catch(
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
|
||||
const readChunk = async (
|
||||
controller: ReadableStreamDefaultController<Uint8Array>
|
||||
) => {
|
||||
let data: ArrayBuffer
|
||||
try {
|
||||
data = await invoke('plugin:http|fetch_read_body', {
|
||||
rid: responseRid
|
||||
})
|
||||
} catch (e) {
|
||||
// close the stream if an error occurs
|
||||
// and drop the body on Rust side
|
||||
controller.error(e)
|
||||
void dropBody()
|
||||
return
|
||||
}
|
||||
|
||||
const dataUint8 = new Uint8Array(data)
|
||||
const lastByte = dataUint8[dataUint8.byteLength - 1]
|
||||
const actualData = dataUint8.slice(0, dataUint8.byteLength - 1)
|
||||
|
||||
// close when the signal to close (last byte is 1) is sent from the IPC.
|
||||
if (lastByte === 1) {
|
||||
controller.close()
|
||||
return
|
||||
}
|
||||
|
||||
controller.enqueue(actualData)
|
||||
}
|
||||
|
||||
// no body for 101, 103, 204, 205 and 304
|
||||
// see https://fetch.spec.whatwg.org/#null-body-status
|
||||
const body = [101, 103, 204, 205, 304].includes(status)
|
||||
? null
|
||||
: new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
// listen for abort events to cancel reading
|
||||
signal?.addEventListener('abort', () => {
|
||||
controller.error(ERROR_REQUEST_CANCELLED)
|
||||
void dropBody()
|
||||
})
|
||||
},
|
||||
pull: (controller) => readChunk(controller),
|
||||
cancel: () => {
|
||||
// Ensure body resources are released on stream cancellation
|
||||
void dropBody()
|
||||
}
|
||||
})
|
||||
|
||||
const res = new Response(body, {
|
||||
status,
|
||||
statusText
|
||||
})
|
||||
|
||||
// `Response.url` cannot be set via the constructor, so we define it manually
|
||||
Object.defineProperty(res, 'url', { value: url, writable: false })
|
||||
|
||||
// Expose `set-cookie` via `response.headers` (and `getSetCookie()` where
|
||||
// supported). This is not Fetch-spec compliant for network responses in
|
||||
// browsers, where `set-cookie` is treated as a forbidden response
|
||||
// header and is generally not readable from JavaScript.
|
||||
Object.defineProperty(res, 'headers', {
|
||||
value: new Headers(responseHeaders),
|
||||
writable: false
|
||||
})
|
||||
|
||||
// Patch clone() per-instance so cloning preserves the overridden properties
|
||||
const originalClone = res.clone.bind(res)
|
||||
Object.defineProperty(res, 'clone', {
|
||||
value: () => {
|
||||
const cloned = originalClone()
|
||||
Object.defineProperty(cloned, 'url', { value: url, writable: false })
|
||||
Object.defineProperty(cloned, 'headers', {
|
||||
value: new Headers(responseHeaders),
|
||||
writable: false
|
||||
})
|
||||
return cloned
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
Reference in New Issue
Block a user