diff --git a/package-lock.json b/package-lock.json index 82705f3e..d4c1a5af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "plugins/auth-basic", "plugins/auth-bearer", "plugins/auth-jwt", + "plugins/auth-ntlm", "plugins/auth-oauth2", "plugins/auth-oauth1", "plugins/filter-jsonpath", @@ -4295,6 +4296,10 @@ "resolved": "plugins/auth-jwt", "link": true }, + "node_modules/@yaak/auth-ntlm": { + "resolved": "plugins/auth-ntlm", + "link": true + }, "node_modules/@yaak/auth-oauth1": { "resolved": "plugins/auth-oauth1", "link": true @@ -6766,6 +6771,16 @@ "node": ">=6" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -9434,6 +9449,39 @@ "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "license": "MIT" }, + "node_modules/httpntlm": { + "version": "1.8.13", + "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz", + "integrity": "sha512-2F2FDPiWT4rewPzNMg3uPhNkP3NExENlUGADRUDPQvuftuUTGW98nLZtGemCIW3G40VhWZYgkIDcQFAwZ3mf2Q==", + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=2CKNJLZJBW8ZC" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/samdecrock" + } + ], + "dependencies": { + "des.js": "^1.0.1", + "httpreq": ">=0.4.22", + "js-md4": "^0.3.2", + "underscore": "~1.12.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/httpreq": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.1.tgz", + "integrity": "sha512-uhSZLPPD2VXXOSN8Cni3kIsoFHaU2pT/nySEU/fHr/ePbqHYr0jeiQRmUKLEirC09SFPsdMoA7LU7UXMd/w0Kw==", + "license": "MIT", + "engines": { + "node": ">= 6.15.1" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -9558,7 +9606,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -10341,6 +10388,12 @@ "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", "license": "MIT" }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, "node_modules/js-md5": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", @@ -12247,6 +12300,12 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -17602,6 +17661,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -18952,6 +19017,13 @@ "@types/jsonwebtoken": "^9.0.7" } }, + "plugins/auth-ntlm": { + "name": "@yaak/auth-ntlm", + "version": "0.1.0", + "dependencies": { + "httpntlm": "^1.8.13" + } + }, "plugins/auth-oauth1": { "name": "@yaak/auth-oauth1", "version": "0.1.0", diff --git a/package.json b/package.json index 5b07f086..e1b04a6f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "plugins/auth-basic", "plugins/auth-bearer", "plugins/auth-jwt", + "plugins/auth-ntlm", "plugins/auth-oauth2", "plugins/auth-oauth1", "plugins/filter-jsonpath", diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index 23411ae7..b6cbe26e 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -387,7 +387,7 @@ export type ImportResources = { workspaces: Array, environments: Arra export type ImportResponse = { resources: ImportResources, }; -export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; +export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, context: PluginContext, payload: InternalEventPayload, }; export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; @@ -403,7 +403,7 @@ export type OpenWindowRequest = { url: string, */ label: string, title?: string, size?: WindowSize, dataDirKey?: string, }; -export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, }; +export type PluginContext = { id: string, label: string | null, workspaceId: string | null, }; export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string, /** diff --git a/packages/plugin-runtime/src/PluginHandle.ts b/packages/plugin-runtime/src/PluginHandle.ts index 97d3cd75..29ef6874 100644 --- a/packages/plugin-runtime/src/PluginHandle.ts +++ b/packages/plugin-runtime/src/PluginHandle.ts @@ -1,3 +1,4 @@ +import { PluginContext } from '@yaakapp-internal/plugins'; import type { BootRequest, InternalEvent } from '@yaakapp/api'; import type { EventChannel } from './EventChannel'; import { PluginInstance, PluginWorkerData } from './PluginInstance'; @@ -6,14 +7,12 @@ export class PluginHandle { #instance: PluginInstance; constructor( - readonly pluginRefId: string, - readonly bootRequest: BootRequest, - readonly pluginToAppEvents: EventChannel, + pluginRefId: string, + context: PluginContext, + bootRequest: BootRequest, + pluginToAppEvents: EventChannel, ) { - const workerData: PluginWorkerData = { - pluginRefId: this.pluginRefId, - bootRequest: this.bootRequest, - }; + const workerData: PluginWorkerData = { pluginRefId, context, bootRequest }; this.#instance = new PluginInstance(workerData, pluginToAppEvents); } diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index fdb2b06b..d9519dec 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -13,7 +13,7 @@ import { InternalEvent, InternalEventPayload, ListCookieNamesResponse, - PluginWindowContext, + PluginContext, PromptTextResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse, @@ -25,7 +25,7 @@ import { import { Context, PluginDefinition } from '@yaakapp/api'; import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue'; import console from 'node:console'; -import { readFileSync, type Stats, statSync, watch } from 'node:fs'; +import { type Stats, statSync, watch } from 'node:fs'; import path from 'node:path'; import { EventChannel } from './EventChannel'; import { migrateTemplateFunctionSelectOptions } from './migrations'; @@ -33,12 +33,12 @@ import { migrateTemplateFunctionSelectOptions } from './migrations'; export interface PluginWorkerData { bootRequest: BootRequest; pluginRefId: string; + context: PluginContext; } export class PluginInstance { #workerData: PluginWorkerData; #mod: PluginDefinition; - #pkg: { name?: string; version?: string }; #pluginToAppEvents: EventChannel; #appToPluginEvents: EventChannel; @@ -52,18 +52,14 @@ export class PluginInstance { await this.#onMessage(event); }); - // Reload plugin if the JS or package.json changes - const windowContextNone: PluginWindowContext = { type: 'none' }; - this.#mod = {} as any; - this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8')); const fileChangeCallback = async () => { await this.#mod?.dispose?.(); this.#importModule(); - await this.#mod?.init?.(this.#newCtx({ type: 'none' })); + await this.#mod?.init?.(this.#newCtx(workerData.context)); return this.#sendPayload( - windowContextNone, + workerData.context, { type: 'reload_response', silent: false, @@ -90,14 +86,14 @@ export class PluginInstance { } async #onMessage(event: InternalEvent) { - const ctx = this.#newCtx(event.windowContext); + const ctx = this.#newCtx(event.context); - const { windowContext, payload, id: replyId } = event; + const { context, payload, id: replyId } = event; try { if (payload.type === 'boot_request') { await this.#mod?.init?.(ctx); - this.#sendPayload(windowContext, { type: 'boot_response' }, replyId); + this.#sendPayload(context, { type: 'boot_response' }, replyId); return; } @@ -106,7 +102,7 @@ export class PluginInstance { type: 'terminate_response', }; await this.terminate(); - this.#sendPayload(windowContext, payload, replyId); + this.#sendPayload(context, payload, replyId); return; } @@ -123,10 +119,10 @@ export class PluginInstance { // deno-lint-ignore no-explicit-any resources: reply.resources as any, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } else { - // Continue, to send back an empty reply + // Send back an empty reply (below) } } @@ -136,7 +132,7 @@ export class PluginInstance { payload: payload.content, mimeType: payload.type, }); - this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId); + this.#sendPayload(context, { type: 'filter_response', ...reply }, replyId); return; } @@ -154,7 +150,7 @@ export class PluginInstance { pluginRefId: this.#workerData.pluginRefId, actions: reply, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -172,7 +168,7 @@ export class PluginInstance { pluginRefId: this.#workerData.pluginRefId, actions: reply, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -181,7 +177,7 @@ export class PluginInstance { type: 'get_themes_response', themes: this.#mod.themes, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -203,7 +199,7 @@ export class PluginInstance { pluginRefId: this.#workerData.pluginRefId, functions, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -213,7 +209,7 @@ export class PluginInstance { ) { let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); if (templateFunction == null) { - this.#sendEmpty(windowContext, replyId); + this.#sendEmpty(context, replyId); return; } @@ -236,18 +232,17 @@ export class PluginInstance { pluginRefId: this.#workerData.pluginRefId, function: templateFunction, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) { - const replyPayload: InternalEventPayload = { type: 'get_http_authentication_summary_response', ...this.#mod.authentication, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -275,7 +270,7 @@ export class PluginInstance { pluginRefId: this.#workerData.pluginRefId, }; - this.#sendPayload(windowContext, replyPayload, replyId); + this.#sendPayload(context, replyPayload, replyId); return; } @@ -284,7 +279,7 @@ export class PluginInstance { if (typeof auth?.onApply === 'function') { applyFormInputDefaults(auth.args, payload.values); this.#sendPayload( - windowContext, + context, { type: 'call_http_authentication_response', ...(await auth.onApply(ctx, payload)), @@ -302,7 +297,7 @@ export class PluginInstance { const action = this.#mod.authentication.actions?.[payload.index]; if (typeof action?.onSelect === 'function') { await action.onSelect(ctx, payload.args); - this.#sendEmpty(windowContext, replyId); + this.#sendEmpty(context, replyId); return; } } @@ -314,7 +309,7 @@ export class PluginInstance { const action = this.#mod.httpRequestActions[payload.index]; if (typeof action?.onSelect === 'function') { await action.onSelect(ctx, payload.args); - this.#sendEmpty(windowContext, replyId); + this.#sendEmpty(context, replyId); return; } } @@ -326,7 +321,7 @@ export class PluginInstance { const action = this.#mod.grpcRequestActions[payload.index]; if (typeof action?.onSelect === 'function') { await action.onSelect(ctx, payload.args); - this.#sendEmpty(windowContext, replyId); + this.#sendEmpty(context, replyId); return; } } @@ -341,7 +336,7 @@ export class PluginInstance { try { const result = await fn.onRender(ctx, payload.args); this.#sendPayload( - windowContext, + context, { type: 'call_template_function_response', value: result ?? null, @@ -350,7 +345,7 @@ export class PluginInstance { ); } catch (err) { this.#sendPayload( - windowContext, + context, { type: 'call_template_function_response', value: null, @@ -365,12 +360,12 @@ export class PluginInstance { } catch (err) { const error = `${err}`.replace(/^Error:\s*/g, ''); console.log('Plugin call threw exception', payload.type, '→', error); - this.#sendPayload(windowContext, { type: 'error_response', error }, replyId); + this.#sendPayload(context, { type: 'error_response', error }, replyId); return; } // No matches, so send back an empty response so the caller doesn't block forever - this.#sendEmpty(windowContext, replyId); + this.#sendEmpty(context, replyId); } #pathMod() { @@ -393,7 +388,7 @@ export class PluginInstance { } #buildEventToSend( - windowContext: PluginWindowContext, + context: PluginContext, payload: InternalEventPayload, replyId: string | null = null, ): InternalEvent { @@ -403,16 +398,16 @@ export class PluginInstance { id: genId(), replyId, payload, - windowContext, + context, }; } #sendPayload( - windowContext: PluginWindowContext, + context: PluginContext, payload: InternalEventPayload, replyId: string | null, ): string { - const event = this.#buildEventToSend(windowContext, payload, replyId); + const event = this.#buildEventToSend(context, payload, replyId); this.#sendEvent(event); return event.id; } @@ -424,16 +419,16 @@ export class PluginInstance { this.#pluginToAppEvents.emit(event); } - #sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string { - return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId); + #sendEmpty(context: PluginContext, replyId: string | null = null): string { + return this.#sendPayload(context, { type: 'empty_response' }, replyId); } #sendAndWaitForReply>( - windowContext: PluginWindowContext, + context: PluginContext, payload: InternalEventPayload, ): Promise { // 1. Build event to send - const eventToSend = this.#buildEventToSend(windowContext, payload, null); + const eventToSend = this.#buildEventToSend(context, payload, null); // 2. Spawn listener in background const promise = new Promise((resolve) => { @@ -455,12 +450,12 @@ export class PluginInstance { } #sendAndListenForEvents( - windowContext: PluginWindowContext, + context: PluginContext, payload: InternalEventPayload, onEvent: (event: InternalEventPayload) => void, ): void { // 1. Build event to send - const eventToSend = this.#buildEventToSend(windowContext, payload, null); + const eventToSend = this.#buildEventToSend(context, payload, null); // 2. Listen for replies in the background this.#appToPluginEvents.listen((event: InternalEvent) => { @@ -473,11 +468,11 @@ export class PluginInstance { this.#sendEvent(eventToSend); } - #newCtx(windowContext: PluginWindowContext): Context { + #newCtx(context: PluginContext): Context { return { clipboard: { copyText: async (text) => { - await this.#sendAndWaitForReply(windowContext, { + await this.#sendAndWaitForReply(context, { type: 'copy_text_request', text, }); @@ -485,7 +480,7 @@ export class PluginInstance { }, toast: { show: async (args) => { - await this.#sendAndWaitForReply(windowContext, { + await this.#sendAndWaitForReply(context, { type: 'show_toast_request', // Handle default here because null/undefined both convert to None in Rust translation timeout: args.timeout === undefined ? 5000 : args.timeout, @@ -504,21 +499,21 @@ export class PluginInstance { onClose?.(); } }; - this.#sendAndListenForEvents(windowContext, payload, onEvent); + this.#sendAndListenForEvents(context, payload, onEvent); return { close: () => { const closePayload: InternalEventPayload = { type: 'close_window_request', label: args.label, }; - this.#sendPayload(windowContext, closePayload, null); + this.#sendPayload(context, closePayload, null); }, }; }, }, prompt: { text: async (args) => { - const reply: PromptTextResponse = await this.#sendAndWaitForReply(windowContext, { + const reply: PromptTextResponse = await this.#sendAndWaitForReply(context, { type: 'prompt_text_request', ...args, }); @@ -532,7 +527,7 @@ export class PluginInstance { ...args, } as const; const { httpResponses } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return httpResponses; @@ -545,7 +540,7 @@ export class PluginInstance { ...args, } as const; const { grpcRequest } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return grpcRequest; @@ -558,7 +553,7 @@ export class PluginInstance { ...args, } as const; const { httpRequest } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return httpRequest; @@ -569,7 +564,7 @@ export class PluginInstance { ...args, } as const; const { httpResponse } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return httpResponse; @@ -580,7 +575,7 @@ export class PluginInstance { ...args, } as const; const { httpRequest } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return httpRequest; @@ -593,7 +588,7 @@ export class PluginInstance { ...args, } as const; const { value } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return value; @@ -601,7 +596,7 @@ export class PluginInstance { listNames: async () => { const payload = { type: 'list_cookie_names_request' } as const; const { names } = await this.#sendAndWaitForReply( - windowContext, + context, payload, ); return names; @@ -614,20 +609,14 @@ export class PluginInstance { */ render: async (args) => { const payload = { type: 'template_render_request', ...args } as const; - const result = await this.#sendAndWaitForReply( - windowContext, - payload, - ); + const result = await this.#sendAndWaitForReply(context, payload); return result.data as any; }, }, store: { get: async (key: string) => { const payload = { type: 'get_key_value_request', key } as const; - const result = await this.#sendAndWaitForReply( - windowContext, - payload, - ); + const result = await this.#sendAndWaitForReply(context, payload); return result.value ? (JSON.parse(result.value) as T) : undefined; }, set: async (key: string, value: T) => { @@ -637,20 +626,17 @@ export class PluginInstance { key, value: valueStr, }; - await this.#sendAndWaitForReply(windowContext, payload); + await this.#sendAndWaitForReply(context, payload); }, delete: async (key: string) => { const payload = { type: 'delete_key_value_request', key } as const; - const result = await this.#sendAndWaitForReply( - windowContext, - payload, - ); + const result = await this.#sendAndWaitForReply(context, payload); return result.deleted; }, }, plugin: { reload: () => { - this.#sendPayload({ type: 'none' }, { type: 'reload_response', silent: true }, null); + this.#sendPayload(context, { type: 'reload_response', silent: true }, null); }, }, }; diff --git a/packages/plugin-runtime/src/index.ts b/packages/plugin-runtime/src/index.ts index d6c0c626..c1a363dc 100644 --- a/packages/plugin-runtime/src/index.ts +++ b/packages/plugin-runtime/src/index.ts @@ -39,7 +39,7 @@ async function handleIncoming(msg: string) { const pluginEvent: InternalEvent = JSON.parse(msg); // Handle special event to bootstrap plugin if (pluginEvent.payload.type === 'boot_request') { - const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents); + const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.context, pluginEvent.payload, pluginToAppEvents); plugins[pluginEvent.pluginRefId] = plugin; } diff --git a/plugins/auth-ntlm/package.json b/plugins/auth-ntlm/package.json new file mode 100644 index 00000000..e50c8521 --- /dev/null +++ b/plugins/auth-ntlm/package.json @@ -0,0 +1,20 @@ +{ + "name": "@yaak/auth-ntlm", + "displayName": "NTLM Authentication", + "description": "Authenticate requests using NTLM authentication", + "repository": { + "type": "git", + "url": "https://github.com/mountain-loop/yaak.git", + "directory": "plugins/auth-ntlm" + }, + "private": true, + "version": "0.1.0", + "scripts": { + "build": "yaakcli build", + "dev": "yaakcli dev", + "lint": "tsc --noEmit && eslint . --ext .ts,.tsx" + }, + "dependencies": { + "httpntlm": "^1.8.13" + } +} diff --git a/plugins/auth-ntlm/src/index.ts b/plugins/auth-ntlm/src/index.ts new file mode 100644 index 00000000..1e0d5fe4 --- /dev/null +++ b/plugins/auth-ntlm/src/index.ts @@ -0,0 +1,76 @@ +import type { PluginDefinition } from '@yaakapp/api'; + +import { ntlm } from 'httpntlm'; + +export const plugin: PluginDefinition = { + authentication: { + name: 'windows', + label: 'NTLM Auth', + shortLabel: 'NTLM', + args: [ + { + type: 'text', + name: 'username', + label: 'Username', + optional: true, + }, + { + type: 'text', + name: 'password', + label: 'Password', + optional: true, + password: true, + }, + { + type: 'accordion', + label: 'Advanced', + inputs: [ + { name: 'domain', label: 'Domain', type: 'text', optional: true }, + { name: 'workstation', label: 'Workstation', type: 'text', optional: true }, + ], + }, + ], + async onApply(ctx, { values, method, url }) { + const username = values.username ? String(values.username) : undefined; + const password = values.password ? String(values.password) : undefined; + const domain = values.domain ? String(values.domain) : undefined; + const workstation = values.workstation ? String(values.workstation) : undefined; + + const options = { + url, + username, + password, + workstation, + domain, + }; + + const type1 = ntlm.createType1Message(options); + + const negotiateResponse = await ctx.httpRequest.send({ + httpRequest: { + method, + url, + headers: [ + { name: 'Authorization', value: type1 }, + { name: 'Connection', value: 'keep-alive' }, + ], + }, + }); + + const wwwAuthenticateHeader = negotiateResponse.headers.find( + (h) => h.name.toLowerCase() === 'www-authenticate', + ); + + if (!wwwAuthenticateHeader?.value) { + throw new Error('Unable to find www-authenticate response header for NTLM'); + } + + const type2 = ntlm.parseType2Message(wwwAuthenticateHeader.value, (err: Error | null) => { + if (err != null) throw err; + }); + const type3 = ntlm.createType3Message(type2, options); + + return { setHeaders: [{ name: 'Authorization', value: type3 }] }; + }, + }, +}; diff --git a/plugins/auth-ntlm/src/modules.d.ts b/plugins/auth-ntlm/src/modules.d.ts new file mode 100644 index 00000000..0524954a --- /dev/null +++ b/plugins/auth-ntlm/src/modules.d.ts @@ -0,0 +1 @@ +declare module 'httpntlm'; diff --git a/plugins/auth-ntlm/tsconfig.json b/plugins/auth-ntlm/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/plugins/auth-ntlm/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 35246708..a8d5c142 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2790,7 +2790,7 @@ dependencies = [ "dbus-secret-service", "log", "security-framework 2.11.1", - "security-framework 3.2.0", + "security-framework 3.5.1", "windows-sys 0.60.2", "zeroize", ] @@ -3009,9 +3009,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] @@ -4763,9 +4763,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "once_cell", "ring", @@ -4784,7 +4784,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.5.1", ] [[package]] @@ -4799,9 +4799,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -4812,10 +4812,10 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.2.0", + "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4963,9 +4963,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", @@ -4976,9 +4976,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -5620,9 +5620,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f07c6590706b2fc0ab287b041cf5ce9c435b3850bdae5571e19d9d27584e89d" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" dependencies = [ "anyhow", "bytes", @@ -5664,7 +5664,6 @@ dependencies = [ "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -5673,9 +5672,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71be1f494b683ac439e6d61c16ab5c472c6f9c6ee78995b29556d9067c021a1" +checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" dependencies = [ "anyhow", "cargo_toml", @@ -5736,9 +5735,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7ce9aab979296b2f91e6fbf154207c2e3512b12ddca0b24bfa0e0cde6b2976" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" dependencies = [ "anyhow", "glob", @@ -5789,9 +5788,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" dependencies = [ "log", "raw-window-handle", @@ -5807,9 +5806,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" dependencies = [ "anyhow", "dunce", @@ -5891,9 +5890,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" +checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" dependencies = [ "encoding_rs", "log", @@ -5975,9 +5974,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3367f0b47df90e9195cd9f04a56b0055a2cba45aa11923c6c253d748778176fc" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" dependencies = [ "cookie", "dpi", @@ -6000,9 +5999,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d91d29ca680c545364cf75ba2f2e3c7ea2ab6376bfa3be26b56fa2463a5b5e" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" dependencies = [ "gtk", "http", @@ -7816,7 +7815,6 @@ dependencies = [ "cookie", "eventsource-client", "http", - "hyper-util", "log", "md5 0.8.0", "mime_guess", @@ -7842,7 +7840,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-stream", - "tower-service", "ts-rs", "uuid", "yaak-common", @@ -7948,9 +7945,18 @@ dependencies = [ name = "yaak-http" version = "0.1.0" dependencies = [ + "hyper-util", + "log", "regex", + "reqwest", + "reqwest_cookie_store", "rustls", "rustls-platform-verifier", + "serde", + "tauri", + "thiserror 2.0.17", + "tokio", + "tower-service", "urlencoding", "yaak-models", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0c3b9086..89dcdb2b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,7 +48,7 @@ chrono = { workspace = true, features = ["serde"] } cookie = "0.18.1" eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" } http = { version = "1.2.0", default-features = false } -log = "0.4.27" +log = { workspace = true } md5 = "0.8.0" mime_guess = "2.0.5" rand = "0.9.0" @@ -68,8 +68,6 @@ tauri-plugin-shell = { workspace = true } tauri-plugin-single-instance = { version = "2.3.4", features = ["deep-link"] } tauri-plugin-updater = "2.9.0" tauri-plugin-window-state = "2.4.0" -hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] } -tower-service = "0.3.3" thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.17" @@ -96,15 +94,16 @@ hex = "0.4.3" keyring = "3.6.3" reqwest = "0.12.20" reqwest_cookie_store = "0.8.0" -rustls = { version = "0.23.33", default-features = false } -rustls-platform-verifier = "0.6.1" +rustls = { version = "0.23.34", default-features = false } +rustls-platform-verifier = "0.6.2" serde = "1.0.228" serde_json = "1.0.145" sha2 = "0.10.9" -tauri = "2.9.0" -tauri-plugin = "2.5.0" -tauri-plugin-dialog = "2.4.0" -tauri-plugin-shell = "2.3.1" +log = "0.4.28" +tauri = "2.9.2" +tauri-plugin = "2.5.1" +tauri-plugin-dialog = "2.4.2" +tauri-plugin-shell = "2.3.3" thiserror = "2.0.17" tokio = "1.48.0" ts-rs = "11.1.0" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a7cacb0c..2ebf03ac 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2,7 +2,7 @@ use crate::error::Result; use tauri::{command, AppHandle, Manager, Runtime, State, WebviewWindow}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use yaak_crypto::manager::EncryptionManagerExt; -use yaak_plugins::events::{GetThemesResponse, PluginWindowContext}; +use yaak_plugins::events::{GetThemesResponse, PluginContext}; use yaak_plugins::manager::PluginManager; use yaak_plugins::native_template_functions::{ decrypt_secure_template_function, encrypt_secure_template_function, @@ -28,8 +28,8 @@ pub(crate) async fn cmd_decrypt_template( template: &str, ) -> Result { let app_handle = window.app_handle(); - let window_context = &PluginWindowContext::new(&window); - Ok(decrypt_secure_template_function(&app_handle, window_context, template)?) + let plugin_context = &PluginContext::new(&window); + Ok(decrypt_secure_template_function(&app_handle, plugin_context, template)?) } #[command] @@ -38,8 +38,8 @@ pub(crate) async fn cmd_secure_template( window: WebviewWindow, template: &str, ) -> Result { - let window_context = &PluginWindowContext::new(&window); - Ok(encrypt_secure_template_function(&app_handle, window_context, template)?) + let plugin_context = &PluginContext::new(&window); + Ok(encrypt_secure_template_function(&app_handle, plugin_context, template)?) } #[command] diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 7efbe8cc..d38ab994 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -16,6 +16,9 @@ pub enum Error { #[error(transparent)] CryptoError(#[from] yaak_crypto::error::Error), + #[error(transparent)] + HttpError(#[from] yaak_http::error::Error), + #[error(transparent)] GitError(#[from] yaak_git::error::Error), diff --git a/src-tauri/src/grpc.rs b/src-tauri/src/grpc.rs index 84bbe3cd..e2621de0 100644 --- a/src-tauri/src/grpc.rs +++ b/src-tauri/src/grpc.rs @@ -6,7 +6,7 @@ use tauri::{Manager, Runtime, WebviewWindow}; use yaak_grpc::{KeyAndValueRef, MetadataMap}; use yaak_models::models::GrpcRequest; use yaak_models::query_manager::QueryManagerExt; -use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader}; +use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, PluginContext}; use yaak_plugins::manager::PluginManager; pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap { @@ -81,7 +81,12 @@ pub(crate) async fn build_metadata( .collect(), }; let plugin_result = plugin_manager - .call_http_authentication(&window, &authentication_type, plugin_req) + .call_http_authentication( + &window, + &authentication_type, + plugin_req, + &PluginContext::new(window), + ) .await?; for header in plugin_result.set_headers.unwrap_or_default() { metadata.insert(header.name, header.value); diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 35b23567..fa715f6d 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -6,9 +6,9 @@ use http::header::{ACCEPT, USER_AGENT}; use http::{HeaderMap, HeaderName, HeaderValue}; use log::{debug, error, warn}; use mime_guess::Mime; -use reqwest::redirect::Policy; -use reqwest::{Method, NoProxy, Response}; -use reqwest::{Proxy, Url, multipart}; +use reqwest::{Method, Response}; +use reqwest::{Url, multipart}; +use reqwest_cookie_store::{CookieStore, CookieStoreMutex}; use serde_json::Value; use std::collections::BTreeMap; use std::path::PathBuf; @@ -21,6 +21,10 @@ use tokio::fs::{File, create_dir_all}; use tokio::io::AsyncWriteExt; use tokio::sync::watch::Receiver; use tokio::sync::{Mutex, oneshot}; +use yaak_http::client::{ + HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth, +}; +use yaak_http::manager::HttpConnectionManager; use yaak_models::models::{ Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, HttpResponseState, ProxySetting, ProxySettingAuth, @@ -28,12 +32,11 @@ use yaak_models::models::{ use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; use yaak_plugins::events::{ - CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose, + CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_templates::{RenderErrorBehavior, RenderOptions}; -use crate::dns::LocalhostResolver; pub async fn send_http_request( window: &WebviewWindow, @@ -42,9 +45,31 @@ pub async fn send_http_request( environment: Option, cookie_jar: Option, cancelled_rx: &mut Receiver, +) -> Result { + send_http_request_with_context( + window, + unrendered_request, + og_response, + environment, + cookie_jar, + cancelled_rx, + &PluginContext::new(window), + ) + .await +} + +pub async fn send_http_request_with_context( + window: &WebviewWindow, + unrendered_request: &HttpRequest, + og_response: &HttpResponse, + environment: Option, + cookie_jar: Option, + cancelled_rx: &mut Receiver, + plugin_context: &PluginContext, ) -> Result { let app_handle = window.app_handle().clone(); let plugin_manager = app_handle.state::(); + let connection_manager = app_handle.state::(); let settings = window.db().get_settings(); let workspace = window.db().get_workspace(&unrendered_request.workspace_id)?; let environment_id = environment.map(|e| e.id); @@ -72,11 +97,7 @@ pub async fn send_http_request( } }; - let cb = PluginTemplateCallback::new( - window.app_handle(), - &PluginWindowContext::new(window), - RenderPurpose::Send, - ); + let cb = PluginTemplateCallback::new(window.app_handle(), &plugin_context, RenderPurpose::Send); let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw, @@ -102,65 +123,33 @@ pub async fn send_http_request( } debug!("Sending request to {} {url_string}", request.method); - let mut client_builder = reqwest::Client::builder() - .redirect(match workspace.setting_follow_redirects { - true => Policy::limited(10), // TODO: Handle redirects natively - false => Policy::none(), - }) - .connection_verbose(true) - .gzip(true) - .brotli(true) - .deflate(true) - .dns_resolver(LocalhostResolver::new()) - .referer(false) - .tls_info(true); - - let tls_config = yaak_http::tls::get_config(workspace.setting_validate_certificates, true); - client_builder = client_builder.use_preconfigured_tls(tls_config); - - match settings.proxy { - Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(), + let proxy_setting = match settings.proxy { + None => HttpConnectionProxySetting::System, + Some(ProxySetting::Disabled) => HttpConnectionProxySetting::Disabled, Some(ProxySetting::Enabled { http, https, auth, - disabled, bypass, - }) if !disabled => { - debug!("Using proxy http={http} https={https} bypass={bypass}"); - if !http.is_empty() { - match Proxy::http(http) { - Ok(mut proxy) => { - if let Some(ProxySettingAuth { user, password }) = auth.clone() { - debug!("Using http proxy auth"); - proxy = proxy.basic_auth(user.as_str(), password.as_str()); + disabled, + }) => { + if disabled { + HttpConnectionProxySetting::System + } else { + HttpConnectionProxySetting::Enabled { + http, + https, + bypass, + auth: match auth { + None => None, + Some(ProxySettingAuth { user, password }) => { + Some(HttpConnectionProxySettingAuth { user, password }) } - proxy = proxy.no_proxy(NoProxy::from_string(&bypass)); - client_builder = client_builder.proxy(proxy); - } - Err(e) => { - warn!("Failed to apply http proxy {e:?}"); - } - }; - } - if !https.is_empty() { - match Proxy::https(https) { - Ok(mut proxy) => { - if let Some(ProxySettingAuth { user, password }) = auth { - debug!("Using https proxy auth"); - proxy = proxy.basic_auth(user.as_str(), password.as_str()); - } - proxy = proxy.no_proxy(NoProxy::from_string(&bypass)); - client_builder = client_builder.proxy(proxy); - } - Err(e) => { - warn!("Failed to apply https proxy {e:?}"); - } - }; + }, + } } } - _ => {} // Nothing to do for this one, as it is the default - } + }; // Add cookie store if specified let maybe_cookie_manager = match cookie_jar.clone() { @@ -179,23 +168,33 @@ pub async fn send_http_request( .map(|c| Ok(c)) .collect::>>(); - let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?; - let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store); + let cookie_store = CookieStore::from_cookies(cookies, true)?; + let cookie_store = CookieStoreMutex::new(cookie_store); let cookie_store = Arc::new(cookie_store); - client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store)); - - Some((cookie_store, cj)) + let cookie_provider = Arc::clone(&cookie_store); + Some((cookie_provider, cj)) } None => None, }; - if workspace.setting_request_timeout > 0 { - client_builder = client_builder.timeout(Duration::from_millis( - workspace.setting_request_timeout.unsigned_abs() as u64, - )); - } - - let client = client_builder.build()?; + let client = connection_manager + .get_client( + &plugin_context.id, + &HttpConnectionOptions { + follow_redirects: workspace.setting_follow_redirects, + validate_certificates: workspace.setting_validate_certificates, + proxy: proxy_setting, + cookie_provider: maybe_cookie_manager.as_ref().map(|(p, _)| Arc::clone(&p)), + timeout: if workspace.setting_request_timeout > 0 { + Some(Duration::from_millis( + workspace.setting_request_timeout.unsigned_abs() as u64 + )) + } else { + None + }, + }, + ) + .await?; // Render query parameters let mut query_params = Vec::new(); @@ -469,8 +468,9 @@ pub async fn send_http_request( }) .collect(), }; - let auth_result = - plugin_manager.call_http_authentication(&window, &authentication_type, req).await; + let auth_result = plugin_manager + .call_http_authentication(&window, &authentication_type, req, plugin_context) + .await; let plugin_result = match auth_result { Ok(r) => r, Err(e) => { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 42378983..a4b91970 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -23,7 +23,7 @@ use tauri::{Listener, Runtime}; use tauri::{Manager, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_log::fern::colors::ColoredLevelConfig; -use tauri_plugin_log::{Builder, Target, TargetKind}; +use tauri_plugin_log::{Builder, Target, TargetKind, log}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tokio::sync::Mutex; use tokio::task::block_in_place; @@ -44,7 +44,7 @@ use yaak_plugins::events::{ GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, InternalEvent, InternalEventPayload, JsonPrimitive, - PluginWindowContext, RenderPurpose, ShowToastRequest, + PluginContext, RenderPurpose, ShowToastRequest, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::plugin_meta::PluginMetadata; @@ -54,7 +54,6 @@ use yaak_templates::format_json::format_json; use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args}; mod commands; -mod dns; mod encoding; mod error; mod grpc; @@ -104,7 +103,7 @@ async fn cmd_template_tokens_to_string( ) -> YaakResult { let cb = PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Preview, ); let new_tokens = transform_args(tokens, &cb)?; @@ -126,7 +125,7 @@ async fn cmd_render_template( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Preview, ), &RenderOptions { @@ -170,7 +169,7 @@ async fn cmd_grpc_reflect( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -219,7 +218,7 @@ async fn cmd_grpc_go( environment_chain.clone(), &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -344,7 +343,7 @@ async fn cmd_grpc_go( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -416,7 +415,7 @@ async fn cmd_grpc_go( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -1162,7 +1161,7 @@ async fn cmd_install_plugin( app_handle: AppHandle, window: WebviewWindow, ) -> YaakResult { - plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory).await?; + plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory).await?; Ok(app_handle.db().upsert_plugin( &Plugin { @@ -1201,7 +1200,7 @@ async fn cmd_reload_plugins( window: WebviewWindow, plugin_manager: State<'_, PluginManager>, ) -> YaakResult<()> { - plugin_manager.initialize_all_plugins(&app_handle, &PluginWindowContext::new(&window)).await?; + plugin_manager.initialize_all_plugins(&app_handle, &PluginContext::new(&window)).await?; Ok(()) } @@ -1351,6 +1350,7 @@ pub fn run() { .plugin(yaak_crypto::init()) .plugin(yaak_fonts::init()) .plugin(yaak_git::init()) + .plugin(yaak_http::init()) .plugin(yaak_ws::init()) .plugin(yaak_sync::init()); @@ -1621,13 +1621,13 @@ async fn call_frontend( v.to_owned() } -fn get_window_from_window_context( +fn get_window_from_plugin_context( app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, ) -> Result> { - let label = match window_context { - PluginWindowContext::Label { label, .. } => label, - PluginWindowContext::None => { + let label = match &plugin_context.label { + Some(label) => label, + None => { return app_handle .webview_windows() .iter() @@ -1643,7 +1643,7 @@ fn get_window_from_window_context( .find_map(|(_, w)| if w.label() == label { Some(w.to_owned()) } else { None }); if window.is_none() { - error!("Failed to find window by {window_context:?}"); + error!("Failed to find window by {plugin_context:?}"); } Ok(window.ok_or(GenericError(format!("Failed to find window for {}", label)))?) diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index fe887531..9a0bc8c3 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -1,14 +1,14 @@ use crate::error::Result; -use crate::http_request::send_http_request; +use crate::http_request::send_http_request_with_context; use crate::render::{render_grpc_request, render_http_request, render_json_value}; use crate::window::{CreateWindowConfig, create_window}; use crate::{ - call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context, + call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_plugin_context, workspace_from_window, }; use chrono::Utc; use cookie::Cookie; -use log::error; +use log::{debug, error}; use tauri::{AppHandle, Emitter, Manager, Runtime}; use tauri_plugin_clipboard_manager::ClipboardExt; use yaak_common::window::WorkspaceWindowTrait; @@ -19,7 +19,7 @@ use yaak_models::util::UpdateSource; use yaak_plugins::events::{ Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, - InternalEventPayload, ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse, + InternalEventPayload, ListCookieNamesResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent, }; @@ -33,23 +33,21 @@ pub(crate) async fn handle_plugin_event( plugin_handle: &PluginHandle, ) -> Result> { // debug!("Got event to app {event:?}"); - let window_context = event.window_context.to_owned(); + let plugin_context = event.context.to_owned(); match event.clone().payload { InternalEventPayload::CopyTextRequest(req) => { app_handle.clipboard().write_text(req.text.as_str())?; Ok(Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))) } InternalEventPayload::ShowToastRequest(req) => { - match window_context { - PluginWindowContext::Label { label, .. } => { - app_handle.emit_to(label, "show_toast", req)? - } - _ => app_handle.emit("show_toast", req)?, + match plugin_context.label { + Some(label) => app_handle.emit_to(label, "show_toast", req)?, + None => app_handle.emit("show_toast", req)?, }; Ok(Some(InternalEventPayload::ShowToastResponse(EmptyPayload {}))) } InternalEventPayload::PromptTextRequest(_) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; Ok(call_frontend(&window, event).await) } InternalEventPayload::FindHttpResponsesRequest(req) => { @@ -68,7 +66,7 @@ pub(crate) async fn handle_plugin_event( }))) } InternalEventPayload::RenderGrpcRequestRequest(req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); @@ -78,7 +76,7 @@ pub(crate) async fn handle_plugin_event( req.grpc_request.folder_id.as_deref(), environment_id.as_deref(), )?; - let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); + let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose); let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw, }; @@ -89,7 +87,7 @@ pub(crate) async fn handle_plugin_event( }))) } InternalEventPayload::RenderHttpRequestRequest(req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); @@ -99,7 +97,7 @@ pub(crate) async fn handle_plugin_event( req.http_request.folder_id.as_deref(), environment_id.as_deref(), )?; - let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); + let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose); let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw, }; @@ -110,7 +108,7 @@ pub(crate) async fn handle_plugin_event( }))) } InternalEventPayload::TemplateRenderRequest(req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); @@ -130,7 +128,7 @@ pub(crate) async fn handle_plugin_event( folder_id.as_deref(), environment_id.as_deref(), )?; - let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); + let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose); let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw, }; @@ -140,7 +138,7 @@ pub(crate) async fn handle_plugin_event( InternalEventPayload::ErrorResponse(resp) => { error!("Plugin error: {}: {:?}", resp.error, resp); let toast_event = plugin_handle.build_event_to_send( - &window_context, + &plugin_context, &InternalEventPayload::ShowToastRequest(ShowToastRequest { message: format!( "Plugin error from {}: {}", @@ -172,7 +170,7 @@ pub(crate) async fn handle_plugin_event( if !req.silent { let info = plugin_handle.info(); let toast_event = plugin_handle.build_event_to_send( - &window_context, + &plugin_context, &InternalEventPayload::ShowToastRequest(ShowToastRequest { message: format!("Reloaded plugin {}@{}", info.name, info.version), icon: Some(Icon::Info), @@ -187,7 +185,7 @@ pub(crate) async fn handle_plugin_event( } } InternalEventPayload::SendHttpRequestRequest(req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let mut http_request = req.http_request; let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); @@ -211,13 +209,14 @@ pub(crate) async fn handle_plugin_event( )? }; - let http_response = send_http_request( + let http_response = send_http_request_with_context( &window, &http_request, &http_response, environment, cookie_jar, &mut tokio::sync::watch::channel(false).1, // No-op cancel channel + &plugin_context, ) .await?; @@ -240,7 +239,7 @@ pub(crate) async fn handle_plugin_event( }; if let Err(e) = create_window(app_handle, win_config) { let error_event = plugin_handle.build_event_to_send( - &window_context, + &plugin_context, &InternalEventPayload::ErrorResponse(ErrorResponse { error: format!("Failed to create window: {:?}", e), }), @@ -253,12 +252,12 @@ pub(crate) async fn handle_plugin_event( { let event_id = event.id.clone(); let plugin_handle = plugin_handle.clone(); - let window_context = window_context.clone(); + let plugin_context = plugin_context.clone(); tauri::async_runtime::spawn(async move { while let Some(url) = navigation_rx.recv().await { let url = url.to_string(); let event_to_send = plugin_handle.build_event_to_send( - &window_context, // NOTE: Sending existing context on purpose here + &plugin_context, // NOTE: Sending existing context on purpose here &InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }), Some(event_id.clone()), ); @@ -270,11 +269,11 @@ pub(crate) async fn handle_plugin_event( { let event_id = event.id.clone(); let plugin_handle = plugin_handle.clone(); - let window_context = window_context.clone(); + let plugin_context = plugin_context.clone(); tauri::async_runtime::spawn(async move { while let Some(_) = close_rx.recv().await { let event_to_send = plugin_handle.build_event_to_send( - &window_context, + &plugin_context, &InternalEventPayload::WindowCloseEvent, Some(event_id.clone()), ); @@ -309,7 +308,7 @@ pub(crate) async fn handle_plugin_event( }))) } InternalEventPayload::ListCookieNamesRequest(_req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let names = match cookie_jar_from_window(&window) { None => Vec::new(), Some(j) => j @@ -323,7 +322,7 @@ pub(crate) async fn handle_plugin_event( }))) } InternalEventPayload::GetCookieValueRequest(req) => { - let window = get_window_from_window_context(app_handle, &window_context)?; + let window = get_window_from_plugin_context(app_handle, &plugin_context)?; let value = match cookie_jar_from_window(&window) { None => None, Some(j) => j.cookies.into_iter().find_map(|c| match Cookie::parse(c.raw_cookie) { diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index e4311ade..3895a35e 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -1,11 +1,11 @@ use serde_json::Value; use std::collections::BTreeMap; -use yaak_http::apply_path_placeholders; +use yaak_http::path_placeholders::apply_path_placeholders; use yaak_models::models::{ Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, }; use yaak_models::render::make_vars_hashmap; -use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; +use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions, TemplateCallback}; pub async fn render_template( template: &str, diff --git a/src-tauri/yaak-crypto/Cargo.toml b/src-tauri/yaak-crypto/Cargo.toml index 24e437eb..cf3de89a 100644 --- a/src-tauri/yaak-crypto/Cargo.toml +++ b/src-tauri/yaak-crypto/Cargo.toml @@ -10,7 +10,7 @@ base32 = "0.5.1" # For encoding human-readable key base64 = "0.22.1" # For encoding in the database chacha20poly1305 = "0.10.1" keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] } -log = "0.4.26" +log = { workspace = true } serde = { workspace = true, features = ["derive"] } tauri = { workspace = true } thiserror = { workspace = true } diff --git a/src-tauri/yaak-git/Cargo.toml b/src-tauri/yaak-git/Cargo.toml index 7698382e..aede407d 100644 --- a/src-tauri/yaak-git/Cargo.toml +++ b/src-tauri/yaak-git/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] chrono = { workspace = true, features = ["serde"] } git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] } -log = "0.4.22" +log = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_yaml = "0.9.34" diff --git a/src-tauri/yaak-grpc/Cargo.toml b/src-tauri/yaak-grpc/Cargo.toml index a6314667..d721aa80 100644 --- a/src-tauri/yaak-grpc/Cargo.toml +++ b/src-tauri/yaak-grpc/Cargo.toml @@ -10,7 +10,7 @@ async-recursion = "1.1.1" dunce = "1.0.4" hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] } hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] } -log = "0.4.20" +log = { workspace = true } md5 = "0.7.0" prost = "0.13.4" prost-reflect = { version = "0.14.4", default-features = false, features = ["serde", "derive"] } diff --git a/src-tauri/yaak-http/Cargo.toml b/src-tauri/yaak-http/Cargo.toml index d3fdd5ce..2f6d547d 100644 --- a/src-tauri/yaak-http/Cargo.toml +++ b/src-tauri/yaak-http/Cargo.toml @@ -10,3 +10,12 @@ regex = "1.11.1" rustls = { workspace = true, default-features = false, features = ["ring"] } rustls-platform-verifier = { workspace = true } urlencoding = "2.1.3" +tauri = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks", "http2"] } +reqwest_cookie_store = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true, features = ["derive"] } +hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] } +tower-service = "0.3.3" +log = { workspace = true } diff --git a/src-tauri/yaak-http/src/client.rs b/src-tauri/yaak-http/src/client.rs new file mode 100644 index 00000000..3fb0cbd0 --- /dev/null +++ b/src-tauri/yaak-http/src/client.rs @@ -0,0 +1,133 @@ +use crate::dns::LocalhostResolver; +use crate::error::Result; +use crate::tls; +use log::{debug, warn}; +use reqwest::redirect::Policy; +use reqwest::{Client, Proxy}; +use reqwest_cookie_store::CookieStoreMutex; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub struct HttpConnectionProxySettingAuth { + pub user: String, + pub password: String, +} + +#[derive(Clone)] +pub enum HttpConnectionProxySetting { + Disabled, + System, + Enabled { + http: String, + https: String, + auth: Option, + bypass: String, + }, +} + +#[derive(Clone)] +pub struct HttpConnectionOptions { + pub follow_redirects: bool, + pub validate_certificates: bool, + pub proxy: HttpConnectionProxySetting, + pub cookie_provider: Option>, + pub timeout: Option, +} + +impl HttpConnectionOptions { + pub(crate) fn build_client(&self) -> Result { + let mut client = Client::builder() + .connection_verbose(true) + .gzip(true) + .brotli(true) + .deflate(true) + .referer(false) + .tls_info(true); + + // Configure TLS + client = client.use_preconfigured_tls(tls::get_config(self.validate_certificates, true)); + + // Configure DNS resolver + client = client.dns_resolver(LocalhostResolver::new()); + + // Configure redirects + client = client.redirect(match self.follow_redirects { + true => Policy::limited(10), // TODO: Handle redirects natively + false => Policy::none(), + }); + + // Configure cookie provider + if let Some(p) = &self.cookie_provider { + client = client.cookie_provider(Arc::clone(&p)); + } + + // Configure proxy + match self.proxy.clone() { + HttpConnectionProxySetting::System => { /* Default */ } + HttpConnectionProxySetting::Disabled => { + client = client.no_proxy(); + } + HttpConnectionProxySetting::Enabled { + http, + https, + auth, + bypass, + } => { + for p in build_enabled_proxy(http, https, auth, bypass) { + client = client.proxy(p) + } + } + } + + // Configure timeout + if let Some(d) = self.timeout { + client = client.timeout(d); + } + + Ok(client.build()?) + } +} + +fn build_enabled_proxy( + http: String, + https: String, + auth: Option, + bypass: String, +) -> Vec { + debug!("Using proxy http={http} https={https} bypass={bypass}"); + + let mut proxies = Vec::new(); + + if !http.is_empty() { + match Proxy::http(http) { + Ok(mut proxy) => { + if let Some(HttpConnectionProxySettingAuth { user, password }) = auth.clone() { + debug!("Using http proxy auth"); + proxy = proxy.basic_auth(user.as_str(), password.as_str()); + } + proxies.push(proxy.no_proxy(reqwest::NoProxy::from_string(&bypass))); + } + Err(e) => { + warn!("Failed to apply http proxy {e:?}"); + } + }; + } + + if !https.is_empty() { + match Proxy::https(https) { + Ok(mut proxy) => { + if let Some(HttpConnectionProxySettingAuth { user, password }) = auth { + debug!("Using https proxy auth"); + proxy = proxy.basic_auth(user.as_str(), password.as_str()); + } + proxies.push(proxy.no_proxy(reqwest::NoProxy::from_string(&bypass))); + } + Err(e) => { + warn!("Failed to apply https proxy {e:?}"); + } + }; + } + + proxies +} diff --git a/src-tauri/src/dns.rs b/src-tauri/yaak-http/src/dns.rs similarity index 97% rename from src-tauri/src/dns.rs rename to src-tauri/yaak-http/src/dns.rs index ef65d058..a41714f9 100644 --- a/src-tauri/src/dns.rs +++ b/src-tauri/yaak-http/src/dns.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use tower_service::Service; #[derive(Clone)] -pub(crate) struct LocalhostResolver { +pub struct LocalhostResolver { fallback: HyperGaiResolver, } diff --git a/src-tauri/yaak-http/src/error.rs b/src-tauri/yaak-http/src/error.rs new file mode 100644 index 00000000..3b129c18 --- /dev/null +++ b/src-tauri/yaak-http/src/error.rs @@ -0,0 +1,19 @@ +use serde::{Serialize, Serializer}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Client(#[from] reqwest::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +pub type Result = std::result::Result; diff --git a/src-tauri/yaak-http/src/lib.rs b/src-tauri/yaak-http/src/lib.rs index 156046d9..ce228285 100644 --- a/src-tauri/yaak-http/src/lib.rs +++ b/src-tauri/yaak-http/src/lib.rs @@ -1,185 +1,20 @@ +use crate::manager::HttpConnectionManager; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime}; + pub mod tls; +pub mod path_placeholders; +pub mod error; +pub mod manager; +pub mod dns; +pub mod client; -use yaak_models::models::HttpUrlParameter; - -pub fn apply_path_placeholders( - url: &str, - parameters: Vec, -) -> (String, Vec) { - let mut new_parameters = Vec::new(); - - let mut url = url.to_string(); - for p in parameters { - if !p.enabled || p.name.is_empty() { - continue; - } - - // Replace path parameters with values from URL parameters - let old_url_string = url.clone(); - url = replace_path_placeholder(&p, url.as_str()); - - // Remove as param if it modified the URL - if old_url_string == *url { - new_parameters.push(p); - } - } - - (url, new_parameters) -} - -fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String { - if !p.enabled { - return url.to_string(); - } - - if !p.name.starts_with(":") { - return url.to_string(); - } - - let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap(); - let result = re - .replace_all(url, |cap: ®ex::Captures| { - format!( - "{}{}{}", - cap[1].to_string(), - urlencoding::encode(p.value.as_str()), - cap[2].to_string() - ) +pub fn init() -> TauriPlugin { + Builder::new("yaak-http") + .setup(|app, _api| { + let manager = HttpConnectionManager::new(); + app.manage(manager); + Ok(()) }) - .into_owned(); - result -} - -#[cfg(test)] -mod placeholder_tests { - use crate::{apply_path_placeholders, replace_path_placeholder}; - use yaak_models::models::{HttpRequest, HttpUrlParameter}; - - #[test] - fn placeholder_middle() { - let p = HttpUrlParameter { - name: ":foo".into(), - value: "xxx".into(), - enabled: true, - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foo/bar"), - "https://example.com/xxx/bar", - ); - } - - #[test] - fn placeholder_end() { - let p = HttpUrlParameter { - name: ":foo".into(), - value: "xxx".into(), - enabled: true, - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foo"), - "https://example.com/xxx", - ); - } - - #[test] - fn placeholder_query() { - let p = HttpUrlParameter { - name: ":foo".into(), - value: "xxx".into(), - enabled: true, - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foo?:foo"), - "https://example.com/xxx?:foo", - ); - } - - #[test] - fn placeholder_missing() { - let p = HttpUrlParameter { - enabled: true, - name: "".to_string(), - value: "".to_string(), - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:missing"), - "https://example.com/:missing", - ); - } - - #[test] - fn placeholder_disabled() { - let p = HttpUrlParameter { - enabled: false, - name: ":foo".to_string(), - value: "xxx".to_string(), - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foo"), - "https://example.com/:foo", - ); - } - - #[test] - fn placeholder_prefix() { - let p = HttpUrlParameter { - name: ":foo".into(), - value: "xxx".into(), - enabled: true, - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foooo"), - "https://example.com/:foooo", - ); - } - - #[test] - fn placeholder_encode() { - let p = HttpUrlParameter { - name: ":foo".into(), - value: "Hello World".into(), - enabled: true, - id: None, - }; - assert_eq!( - replace_path_placeholder(&p, "https://example.com/:foo"), - "https://example.com/Hello%20World", - ); - } - - #[test] - fn apply_placeholder() { - let req = HttpRequest { - url: "example.com/:a/bar".to_string(), - url_parameters: vec![ - HttpUrlParameter { - name: "b".to_string(), - value: "bbb".to_string(), - enabled: true, - id: None, - }, - HttpUrlParameter { - name: ":a".to_string(), - value: "aaa".to_string(), - enabled: true, - id: None, - }, - ], - ..Default::default() - }; - - let (url, url_parameters) = apply_path_placeholders(&req.url, req.url_parameters); - - // Pattern match back to access it - assert_eq!(url, "example.com/aaa/bar"); - assert_eq!(url_parameters.len(), 1); - assert_eq!(url_parameters[0].name, "b"); - assert_eq!(url_parameters[0].value, "bbb"); - } + .build() } diff --git a/src-tauri/yaak-http/src/manager.rs b/src-tauri/yaak-http/src/manager.rs new file mode 100644 index 00000000..0fcb40c0 --- /dev/null +++ b/src-tauri/yaak-http/src/manager.rs @@ -0,0 +1,40 @@ +use crate::client::HttpConnectionOptions; +use crate::error::Result; +use log::info; +use reqwest::Client; +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::RwLock; + +pub struct HttpConnectionManager { + connections: Arc>>, + ttl: Duration, +} + +impl HttpConnectionManager { + pub fn new() -> Self { + Self { + connections: Arc::new(RwLock::new(BTreeMap::new())), + ttl: Duration::from_mins(10), + } + } + + pub async fn get_client(&self, id: &str, opt: &HttpConnectionOptions) -> Result { + let mut connections = self.connections.write().await; + + // Clean old connections + connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl); + + if let Some((c, last_used)) = connections.get_mut(id) { + info!("Re-using HTTP client {id}"); + *last_used = Instant::now(); + return Ok(c.clone()); + } + + info!("Building new HTTP client {id}"); + let c = opt.build_client()?; + connections.insert(id.into(), (c.clone(), Instant::now())); + Ok(c) + } +} diff --git a/src-tauri/yaak-http/src/path_placeholders.rs b/src-tauri/yaak-http/src/path_placeholders.rs new file mode 100644 index 00000000..c4469a6c --- /dev/null +++ b/src-tauri/yaak-http/src/path_placeholders.rs @@ -0,0 +1,183 @@ +use yaak_models::models::HttpUrlParameter; + +pub fn apply_path_placeholders( + url: &str, + parameters: Vec, +) -> (String, Vec) { + let mut new_parameters = Vec::new(); + + let mut url = url.to_string(); + for p in parameters { + if !p.enabled || p.name.is_empty() { + continue; + } + + // Replace path parameters with values from URL parameters + let old_url_string = url.clone(); + url = replace_path_placeholder(&p, url.as_str()); + + // Remove as param if it modified the URL + if old_url_string == *url { + new_parameters.push(p); + } + } + + (url, new_parameters) +} + +fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String { + if !p.enabled { + return url.to_string(); + } + + if !p.name.starts_with(":") { + return url.to_string(); + } + + let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap(); + let result = re + .replace_all(url, |cap: ®ex::Captures| { + format!( + "{}{}{}", + cap[1].to_string(), + urlencoding::encode(p.value.as_str()), + cap[2].to_string() + ) + }) + .into_owned(); + result +} + +#[cfg(test)] +mod placeholder_tests { + use crate::path_placeholders::{apply_path_placeholders, replace_path_placeholder}; + use yaak_models::models::{HttpRequest, HttpUrlParameter}; + + #[test] + fn placeholder_middle() { + let p = HttpUrlParameter { + name: ":foo".into(), + value: "xxx".into(), + enabled: true, + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foo/bar"), + "https://example.com/xxx/bar", + ); + } + + #[test] + fn placeholder_end() { + let p = HttpUrlParameter { + name: ":foo".into(), + value: "xxx".into(), + enabled: true, + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foo"), + "https://example.com/xxx", + ); + } + + #[test] + fn placeholder_query() { + let p = HttpUrlParameter { + name: ":foo".into(), + value: "xxx".into(), + enabled: true, + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foo?:foo"), + "https://example.com/xxx?:foo", + ); + } + + #[test] + fn placeholder_missing() { + let p = HttpUrlParameter { + enabled: true, + name: "".to_string(), + value: "".to_string(), + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:missing"), + "https://example.com/:missing", + ); + } + + #[test] + fn placeholder_disabled() { + let p = HttpUrlParameter { + enabled: false, + name: ":foo".to_string(), + value: "xxx".to_string(), + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foo"), + "https://example.com/:foo", + ); + } + + #[test] + fn placeholder_prefix() { + let p = HttpUrlParameter { + name: ":foo".into(), + value: "xxx".into(), + enabled: true, + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foooo"), + "https://example.com/:foooo", + ); + } + + #[test] + fn placeholder_encode() { + let p = HttpUrlParameter { + name: ":foo".into(), + value: "Hello World".into(), + enabled: true, + id: None, + }; + assert_eq!( + replace_path_placeholder(&p, "https://example.com/:foo"), + "https://example.com/Hello%20World", + ); + } + + #[test] + fn apply_placeholder() { + let req = HttpRequest { + url: "example.com/:a/bar".to_string(), + url_parameters: vec![ + HttpUrlParameter { + name: "b".to_string(), + value: "bbb".to_string(), + enabled: true, + id: None, + }, + HttpUrlParameter { + name: ":a".to_string(), + value: "aaa".to_string(), + enabled: true, + id: None, + }, + ], + ..Default::default() + }; + + let (url, url_parameters) = apply_path_placeholders(&req.url, req.url_parameters); + + // Pattern match back to access it + assert_eq!(url, "example.com/aaa/bar"); + assert_eq!(url_parameters.len(), 1); + assert_eq!(url_parameters[0].name, "b"); + assert_eq!(url_parameters[0].value, "bbb"); + } +} diff --git a/src-tauri/yaak-license/Cargo.toml b/src-tauri/yaak-license/Cargo.toml index 38dc136a..93f3229e 100644 --- a/src-tauri/yaak-license/Cargo.toml +++ b/src-tauri/yaak-license/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] chrono = "0.4.38" -log = "0.4.26" +log = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/src-tauri/yaak-license/src/license.rs b/src-tauri/yaak-license/src/license.rs index 2a4b1c56..bd417b52 100644 --- a/src-tauri/yaak-license/src/license.rs +++ b/src-tauri/yaak-license/src/license.rs @@ -1,7 +1,7 @@ use crate::error::Error::{ClientError, ServerError}; use crate::error::Result; use chrono::{NaiveDateTime, Utc}; -use log::{debug, info, warn}; +use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::ops::Add; use std::time::Duration; diff --git a/src-tauri/yaak-mac-window/Cargo.toml b/src-tauri/yaak-mac-window/Cargo.toml index 3d99a265..382af7b1 100644 --- a/src-tauri/yaak-mac-window/Cargo.toml +++ b/src-tauri/yaak-mac-window/Cargo.toml @@ -10,7 +10,7 @@ tauri-plugin = { workspace = true, features = ["build"] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.26.0" -log = "0.4.27" +log = { workspace = true } objc = "0.2.7" rand = "0.9.0" csscolorparser = "0.7.2" diff --git a/src-tauri/yaak-models/Cargo.toml b/src-tauri/yaak-models/Cargo.toml index 371e1172..efedc7b2 100644 --- a/src-tauri/yaak-models/Cargo.toml +++ b/src-tauri/yaak-models/Cargo.toml @@ -9,7 +9,7 @@ publish = false chrono = { version = "0.4.38", features = ["serde"] } hex = { workspace = true } include_dir = "0.7" -log = "0.4.22" +log = { workspace = true } nanoid = "0.4.0" r2d2 = "0.8.10" r2d2_sqlite = { version = "0.25.0" } diff --git a/src-tauri/yaak-plugins/Cargo.toml b/src-tauri/yaak-plugins/Cargo.toml index c4edc876..fa2d259a 100644 --- a/src-tauri/yaak-plugins/Cargo.toml +++ b/src-tauri/yaak-plugins/Cargo.toml @@ -12,7 +12,7 @@ dunce = "1.0.4" futures-util = "0.3.30" hex = { workspace = true } keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] } -log = "0.4.21" +log = { workspace = true } md5 = "0.7.0" path-slash = "0.2.1" rand = "0.9.0" diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index 23411ae7..b6cbe26e 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -387,7 +387,7 @@ export type ImportResources = { workspaces: Array, environments: Arra export type ImportResponse = { resources: ImportResources, }; -export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; +export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, context: PluginContext, payload: InternalEventPayload, }; export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; @@ -403,7 +403,7 @@ export type OpenWindowRequest = { url: string, */ label: string, title?: string, size?: WindowSize, dataDirKey?: string, }; -export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, }; +export type PluginContext = { id: string, label: string | null, workspaceId: string | null, }; export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string, /** diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index 6cd2c7ae..9be5e795 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -6,6 +6,7 @@ use yaak_common::window::WorkspaceWindowTrait; use yaak_models::models::{ Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace, }; +use yaak_models::util::generate_prefixed_id; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] @@ -15,7 +16,7 @@ pub struct InternalEvent { pub plugin_ref_id: String, pub plugin_name: String, pub reply_id: Option, - pub window_context: PluginWindowContext, + pub context: PluginContext, pub payload: InternalEventPayload, } @@ -29,32 +30,32 @@ pub(crate) struct InternalEventRawPayload { pub plugin_ref_id: String, pub plugin_name: String, pub reply_id: Option, - pub window_context: PluginWindowContext, + pub context: PluginContext, pub payload: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[serde(rename_all = "snake_case", tag = "type")] +#[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_events.ts")] -pub enum PluginWindowContext { - None, - Label { - label: String, - workspace_id: Option, - }, +pub struct PluginContext { + pub id: String, + pub label: Option, + pub workspace_id: Option, } -impl PluginWindowContext { - pub fn new(window: &WebviewWindow) -> Self { - Self::Label { - label: window.label().to_string(), - workspace_id: window.workspace_id(), +impl PluginContext { + pub fn new_empty() -> Self { + Self { + id: "default".to_string(), + label: None, + workspace_id: None, } } - pub fn new_no_workspace(window: &WebviewWindow) -> Self { - Self::Label { - label: window.label().to_string(), - workspace_id: None, + pub fn new(window: &WebviewWindow) -> Self { + Self { + label: Some(window.label().to_string()), + workspace_id: window.workspace_id(), + id: generate_prefixed_id("pctx"), } } } diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs index 2a84b986..69369438 100644 --- a/src-tauri/yaak-plugins/src/install.rs +++ b/src-tauri/yaak-plugins/src/install.rs @@ -2,7 +2,7 @@ use crate::api::{PluginVersion, download_plugin_archive, get_plugin}; use crate::checksum::compute_checksum; use crate::error::Error::PluginErr; use crate::error::Result; -use crate::events::PluginWindowContext; +use crate::events::PluginContext; use crate::manager::PluginManager; use chrono::Utc; use log::info; @@ -19,7 +19,7 @@ pub async fn delete_and_uninstall( ) -> Result { let plugin_manager = window.state::(); let plugin = window.db().delete_plugin_by_id(plugin_id, &UpdateSource::from_window(&window))?; - plugin_manager.uninstall(&PluginWindowContext::new(&window), plugin.directory.as_str()).await?; + plugin_manager.uninstall(&PluginContext::new(&window), plugin.directory.as_str()).await?; Ok(plugin) } @@ -55,7 +55,7 @@ pub async fn download_and_install( zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); - plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &plugin_dir_str).await?; + plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str).await?; window.db().upsert_plugin( &Plugin { diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 9a550b2e..a68fbf73 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -12,7 +12,7 @@ use crate::events::{ GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigRequest, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesRequest, GetThemesResponse, ImportRequest, - ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, + ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginContext, RenderPurpose, }; use crate::native_template_functions::{template_function_keyring, template_function_secure}; @@ -132,7 +132,7 @@ impl PluginManager { Ok(_) => { info!("Plugin runtime client connected!"); plugin_manager - .initialize_all_plugins(&app_handle, &PluginWindowContext::None) + .initialize_all_plugins(&app_handle, &PluginContext::new_empty()) .await .expect("Failed to reload plugins"); } @@ -195,19 +195,19 @@ impl PluginManager { [bundled_plugin_dirs, installed_plugin_dirs].concat() } - pub async fn uninstall(&self, window_context: &PluginWindowContext, dir: &str) -> Result<()> { + pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?; - self.remove_plugin(window_context, &plugin).await + self.remove_plugin(plugin_context, &plugin).await } async fn remove_plugin( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, plugin: &PluginHandle, ) -> Result<()> { // Terminate the plugin self.send_to_plugin_and_wait( - window_context, + plugin_context, plugin, &InternalEventPayload::TerminateRequest, ) @@ -223,11 +223,7 @@ impl PluginManager { Ok(()) } - pub async fn add_plugin_by_dir( - &self, - window_context: &PluginWindowContext, - dir: &str, - ) -> Result<()> { + pub async fn add_plugin_by_dir(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { info!("Adding plugin by dir {dir}"); let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await; @@ -244,7 +240,7 @@ impl PluginManager { let event = timeout( Duration::from_secs(5), self.send_to_plugin_and_wait( - window_context, + plugin_context, &plugin_handle, &InternalEventPayload::BootRequest(BootRequest { dir: dir.to_string(), @@ -268,19 +264,19 @@ impl PluginManager { pub async fn initialize_all_plugins( &self, app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, ) -> Result<()> { let start = Instant::now(); let candidates = self.list_plugin_dirs(app_handle).await; for candidate in candidates.clone() { // First remove the plugin if it exists if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await { - if let Err(e) = self.remove_plugin(window_context, &plugin).await { + if let Err(e) = self.remove_plugin(plugin_context, &plugin).await { error!("Failed to remove plugin {} {e:?}", candidate.dir); continue; } } - if let Err(e) = self.add_plugin_by_dir(window_context, candidate.dir.as_str()).await { + if let Err(e) = self.add_plugin_by_dir(plugin_context, candidate.dir.as_str()).await { warn!("Failed to add plugin {} {e:?}", candidate.dir); } } @@ -320,13 +316,13 @@ impl PluginManager { source_event: &InternalEvent, payload: &InternalEventPayload, ) -> Result<()> { - let window_context = source_event.to_owned().window_context; + let plugin_context = source_event.to_owned().context; let reply_id = Some(source_event.to_owned().id); let plugin = self .get_plugin_by_ref_id(source_event.plugin_ref_id.as_str()) .await .ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?; - let event = plugin.build_event_to_send_raw(&window_context, &payload, reply_id); + let event = plugin.build_event_to_send_raw(&plugin_context, &payload, reply_id); plugin.send(&event).await } @@ -350,27 +346,27 @@ impl PluginManager { async fn send_to_plugin_and_wait( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, plugin: &PluginHandle, payload: &InternalEventPayload, ) -> Result { let events = - self.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()]).await?; + self.send_to_plugins_and_wait(plugin_context, payload, vec![plugin.to_owned()]).await?; Ok(events.first().unwrap().to_owned()) } async fn send_and_wait( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, payload: &InternalEventPayload, ) -> Result> { let plugins = { self.plugins.lock().await.clone() }; - self.send_to_plugins_and_wait(window_context, payload, plugins).await + self.send_to_plugins_and_wait(plugin_context, payload, plugins).await } async fn send_to_plugins_and_wait( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, payload: &InternalEventPayload, plugins: Vec, ) -> Result> { @@ -380,7 +376,7 @@ impl PluginManager { // 1. Build the events with IDs and everything let events_to_send = plugins .iter() - .map(|p| p.build_event_to_send(window_context, payload, None)) + .map(|p| p.build_event_to_send(plugin_context, payload, None)) .collect::>(); // 2. Spawn thread to subscribe to incoming events and check reply ids @@ -433,7 +429,7 @@ impl PluginManager { ) -> Result> { let reply_events = self .send_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::GetThemesRequest(GetThemesRequest {}), ) .await?; @@ -454,7 +450,7 @@ impl PluginManager { ) -> Result> { let reply_events = self .send_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::GetGrpcRequestActionsRequest(EmptyPayload {}), ) .await?; @@ -475,7 +471,7 @@ impl PluginManager { ) -> Result> { let reply_events = self .send_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::GetHttpRequestActionsRequest(EmptyPayload {}), ) .await?; @@ -520,11 +516,11 @@ impl PluginManager { Some(v) => v, }; - let window_context = &PluginWindowContext::new(&window); + let plugin_context = &PluginContext::new(&window); let vars = &make_vars_hashmap(environment_chain); let cb = PluginTemplateCallback::new( window.app_handle(), - &window_context, + &plugin_context, RenderPurpose::Preview, ); // We don't want to fail for this op because the UI will not be able to list any auth types then @@ -536,7 +532,7 @@ impl PluginManager { let event = self .send_to_plugin_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &plugin, &InternalEventPayload::GetTemplateFunctionConfigRequest( GetTemplateFunctionConfigRequest { @@ -566,7 +562,7 @@ impl PluginManager { let plugin = self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?; let event = plugin.build_event_to_send( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::CallHttpRequestActionRequest(req), None, ); @@ -583,7 +579,7 @@ impl PluginManager { let plugin = self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?; let event = plugin.build_event_to_send( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::CallGrpcRequestActionRequest(req), None, ); @@ -595,10 +591,10 @@ impl PluginManager { &self, window: &WebviewWindow, ) -> Result> { - let window_context = PluginWindowContext::new(window); + let plugin_context = PluginContext::new(window); let reply_events = self .send_and_wait( - &window_context, + &plugin_context, &InternalEventPayload::GetHttpAuthenticationSummaryRequest(EmptyPayload {}), ) .await?; @@ -635,7 +631,7 @@ impl PluginManager { let vars = &make_vars_hashmap(environment_chain); let cb = PluginTemplateCallback::new( window.app_handle(), - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Preview, ); // We don't want to fail for this op because the UI will not be able to list any auth types then @@ -646,7 +642,7 @@ impl PluginManager { let context_id = format!("{:x}", md5::compute(model_id.to_string())); let event = self .send_to_plugin_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &plugin, &InternalEventPayload::GetHttpAuthenticationConfigRequest( GetHttpAuthenticationConfigRequest { @@ -681,7 +677,7 @@ impl PluginManager { vars, &PluginTemplateCallback::new( window.app_handle(), - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Preview, ), &RenderOptions { @@ -697,7 +693,7 @@ impl PluginManager { let context_id = format!("{:x}", md5::compute(model_id.to_string())); self.send_to_plugin_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &plugin, &InternalEventPayload::CallHttpAuthenticationActionRequest( CallHttpAuthenticationActionRequest { @@ -719,6 +715,7 @@ impl PluginManager { window: &WebviewWindow, auth_name: &str, req: CallHttpAuthenticationRequest, + plugin_context: &PluginContext, ) -> Result { let disabled = match req.values.get("disabled") { Some(JsonPrimitive::Boolean(v)) => v.clone(), @@ -742,7 +739,7 @@ impl PluginManager { let event = self .send_to_plugin_and_wait( - &PluginWindowContext::new(window), + plugin_context, &plugin, &InternalEventPayload::CallHttpAuthenticationRequest(req), ) @@ -761,10 +758,10 @@ impl PluginManager { &self, window: &WebviewWindow, ) -> Result> { - let window_context = PluginWindowContext::new(window); + let plugin_context = PluginContext::new(window); let reply_events = self .send_and_wait( - &window_context, + &plugin_context, &InternalEventPayload::GetTemplateFunctionSummaryRequest(EmptyPayload {}), ) .await?; @@ -787,7 +784,7 @@ impl PluginManager { pub async fn call_template_function( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, fn_name: &str, values: HashMap, purpose: RenderPurpose, @@ -798,7 +795,7 @@ impl PluginManager { }; let events = self - .send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req)) + .send_and_wait(plugin_context, &InternalEventPayload::CallTemplateFunctionRequest(req)) .await .map_err(|e| RenderError(format!("Failed to call template function {e:}")))?; @@ -832,7 +829,7 @@ impl PluginManager { ) -> Result { let reply_events = self .send_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &InternalEventPayload::ImportRequest(ImportRequest { content: content.to_string(), }), @@ -872,7 +869,7 @@ impl PluginManager { let event = self .send_to_plugin_and_wait( - &PluginWindowContext::new(window), + &PluginContext::new(window), &plugin, &InternalEventPayload::FilterRequest(FilterRequest { filter: filter.to_string(), diff --git a/src-tauri/yaak-plugins/src/native_template_functions.rs b/src-tauri/yaak-plugins/src/native_template_functions.rs index 94cbd41f..b0548f2c 100644 --- a/src-tauri/yaak-plugins/src/native_template_functions.rs +++ b/src-tauri/yaak-plugins/src/native_template_functions.rs @@ -1,5 +1,5 @@ use crate::events::{ - FormInput, FormInputBase, FormInputText, PluginWindowContext, RenderPurpose, TemplateFunction, + FormInput, FormInputBase, FormInputText, PluginContext, RenderPurpose, TemplateFunction, TemplateFunctionArg, }; use crate::template_callback::PluginTemplateCallback; @@ -65,13 +65,10 @@ pub(crate) fn template_function_keyring() -> TemplateFunction { pub fn template_function_secure_run( app_handle: &AppHandle, args: HashMap, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, ) -> Result { - match window_context.clone() { - PluginWindowContext::Label { - workspace_id: Some(wid), - .. - } => { + match plugin_context.workspace_id.clone() { + Some(wid) => { let value = args.get("value").map(|v| v.to_owned()).unwrap_or_default(); let value = match value { serde_json::Value::String(s) => s, @@ -97,13 +94,13 @@ pub fn template_function_secure_run( let r = String::from_utf8(r).map_err(|e| RenderError(e.to_string()))?; Ok(r) } - _ => Err(RenderError("workspace_id missing from window context".to_string())), + _ => Err(RenderError("workspace_id missing from plugin context".to_string())), } } pub fn template_function_secure_transform_arg( app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, arg_name: &str, arg_value: &str, ) -> Result { @@ -111,11 +108,8 @@ pub fn template_function_secure_transform_arg( return Ok(arg_value.to_string()); } - match window_context.clone() { - PluginWindowContext::Label { - workspace_id: Some(wid), - .. - } => { + match plugin_context.workspace_id.clone() { + Some(wid) => { if arg_value.is_empty() { return Ok("".to_string()); } @@ -132,13 +126,13 @@ pub fn template_function_secure_transform_arg( let r = BASE64_STANDARD.encode(r); Ok(format!("YENC_{}", r)) } - _ => Err(RenderError("workspace_id missing from window context".to_string())), + _ => Err(RenderError("workspace_id missing from plugin context".to_string())), } } pub fn decrypt_secure_template_function( app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, template: &str, ) -> Result { let mut parsed = Parser::new(template).parse()?; @@ -159,7 +153,7 @@ pub fn decrypt_secure_template_function( } } new_tokens.push(Token::Raw { - text: template_function_secure_run(app_handle, args_map, window_context)?, + text: template_function_secure_run(app_handle, args_map, plugin_context)?, }); } t => { @@ -175,10 +169,10 @@ pub fn decrypt_secure_template_function( pub fn encrypt_secure_template_function( app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, template: &str, ) -> Result { - let decrypted = decrypt_secure_template_function(&app_handle, window_context, template)?; + let decrypted = decrypt_secure_template_function(&app_handle, plugin_context, template)?; let tokens = Tokens { tokens: vec![Token::Tag { val: Val::Fn { @@ -193,7 +187,7 @@ pub fn encrypt_secure_template_function( Ok(transform_args( tokens, - &PluginTemplateCallback::new(app_handle, window_context, RenderPurpose::Preview), + &PluginTemplateCallback::new(app_handle, plugin_context, RenderPurpose::Preview), )? .to_string()) } diff --git a/src-tauri/yaak-plugins/src/plugin_handle.rs b/src-tauri/yaak-plugins/src/plugin_handle.rs index fd6fe61d..1a9cad69 100644 --- a/src-tauri/yaak-plugins/src/plugin_handle.rs +++ b/src-tauri/yaak-plugins/src/plugin_handle.rs @@ -1,5 +1,5 @@ use crate::error::Result; -use crate::events::{InternalEvent, InternalEventPayload, PluginWindowContext}; +use crate::events::{InternalEvent, InternalEventPayload, PluginContext}; use crate::plugin_meta::{PluginMetadata, get_plugin_meta}; use crate::util::gen_id; use std::path::Path; @@ -33,16 +33,16 @@ impl PluginHandle { pub fn build_event_to_send( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, payload: &InternalEventPayload, reply_id: Option, ) -> InternalEvent { - self.build_event_to_send_raw(window_context, payload, reply_id) + self.build_event_to_send_raw(plugin_context, payload, reply_id) } pub(crate) fn build_event_to_send_raw( &self, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, payload: &InternalEventPayload, reply_id: Option, ) -> InternalEvent { @@ -53,7 +53,7 @@ impl PluginHandle { plugin_name: dir.file_name().unwrap().to_str().unwrap().to_string(), reply_id, payload: payload.clone(), - window_context: window_context.clone(), + context: plugin_context.clone(), } } diff --git a/src-tauri/yaak-plugins/src/server_ws.rs b/src-tauri/yaak-plugins/src/server_ws.rs index 4088fd86..43581370 100644 --- a/src-tauri/yaak-plugins/src/server_ws.rs +++ b/src-tauri/yaak-plugins/src/server_ws.rs @@ -76,10 +76,11 @@ impl PluginRuntimeServerWebsocket { return; } - let event = match serde_json::from_str::(&msg.into_text().unwrap()) { + let msg_text = msg.into_text().unwrap(); + let event = match serde_json::from_str::(&msg_text) { Ok(e) => e, Err(e) => { - error!("Failed to decode plugin event {e:?}"); + error!("Failed to decode plugin event {e:?} -> {msg_text}"); continue; } }; @@ -98,7 +99,7 @@ impl PluginRuntimeServerWebsocket { payload, plugin_ref_id: event.plugin_ref_id, plugin_name: event.plugin_name, - window_context: event.window_context, + context: event.context, reply_id: event.reply_id, }; diff --git a/src-tauri/yaak-plugins/src/template_callback.rs b/src-tauri/yaak-plugins/src/template_callback.rs index f09f7d35..a3158b8d 100644 --- a/src-tauri/yaak-plugins/src/template_callback.rs +++ b/src-tauri/yaak-plugins/src/template_callback.rs @@ -1,4 +1,4 @@ -use crate::events::{PluginWindowContext, RenderPurpose}; +use crate::events::{PluginContext, RenderPurpose}; use crate::manager::PluginManager; use crate::native_template_functions::{ template_function_keychain_run, template_function_secure_run, @@ -13,19 +13,19 @@ use yaak_templates::error::Result; pub struct PluginTemplateCallback { app_handle: AppHandle, render_purpose: RenderPurpose, - window_context: PluginWindowContext, + plugin_context: PluginContext, } impl PluginTemplateCallback { pub fn new( app_handle: &AppHandle, - window_context: &PluginWindowContext, + plugin_context: &PluginContext, render_purpose: RenderPurpose, ) -> PluginTemplateCallback { PluginTemplateCallback { render_purpose, app_handle: app_handle.to_owned(), - window_context: window_context.to_owned(), + plugin_context: plugin_context.to_owned(), } } } @@ -37,7 +37,7 @@ impl TemplateCallback for PluginTemplateCallback { let fn_name = if fn_name == "Response" { "response" } else { fn_name }; if fn_name == "secure" { - return template_function_secure_run(&self.app_handle, args, &self.window_context); + return template_function_secure_run(&self.app_handle, args, &self.plugin_context); } else if fn_name == "keychain" || fn_name == "keyring" { return template_function_keychain_run(args); } @@ -45,7 +45,7 @@ impl TemplateCallback for PluginTemplateCallback { let plugin_manager = &*self.app_handle.state::(); let resp = plugin_manager .call_template_function( - &self.window_context, + &self.plugin_context, fn_name, args, self.render_purpose.to_owned(), @@ -58,7 +58,7 @@ impl TemplateCallback for PluginTemplateCallback { if fn_name == "secure" { return template_function_secure_transform_arg( &self.app_handle, - &self.window_context, + &self.plugin_context, arg_name, arg_value, ); diff --git a/src-tauri/yaak-sync/Cargo.toml b/src-tauri/yaak-sync/Cargo.toml index 5b50fd8b..91875803 100644 --- a/src-tauri/yaak-sync/Cargo.toml +++ b/src-tauri/yaak-sync/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] chrono = { workspace = true, features = ["serde"] } hex = { workspace = true } -log = "0.4.22" +log = { workspace = true } notify = "8.0.0" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/src-tauri/yaak-templates/Cargo.toml b/src-tauri/yaak-templates/Cargo.toml index 90d92a78..f3fc2510 100644 --- a/src-tauri/yaak-templates/Cargo.toml +++ b/src-tauri/yaak-templates/Cargo.toml @@ -19,4 +19,4 @@ tokio = { workspace = true, features = ["macros", "rt"] } ts-rs = { workspace = true } wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } serde-wasm-bindgen = "0.6.5" -log = "0.4.27" +log = { workspace = true } diff --git a/src-tauri/yaak-ws/Cargo.toml b/src-tauri/yaak-ws/Cargo.toml index ddd8d269..7a7b54aa 100644 --- a/src-tauri/yaak-ws/Cargo.toml +++ b/src-tauri/yaak-ws/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] futures-util = "0.3.31" -log = "0.4.20" +log = { workspace = true } md5 = "0.7.0" reqwest_cookie_store = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/src-tauri/yaak-ws/src/commands.rs b/src-tauri/yaak-ws/src/commands.rs index f378d1ca..083a0ff1 100644 --- a/src-tauri/yaak-ws/src/commands.rs +++ b/src-tauri/yaak-ws/src/commands.rs @@ -10,7 +10,7 @@ use tauri::{AppHandle, Runtime, State, Url, WebviewWindow}; use tokio::sync::{Mutex, mpsc}; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::http::HeaderValue; -use yaak_http::apply_path_placeholders; +use yaak_http::path_placeholders::apply_path_placeholders; use yaak_models::models::{ HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent, WebsocketEventType, WebsocketRequest, @@ -18,7 +18,7 @@ use yaak_models::models::{ use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; use yaak_plugins::events::{ - CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose, + CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -124,7 +124,7 @@ pub(crate) async fn send( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -203,7 +203,7 @@ pub(crate) async fn connect( environment_chain, &PluginTemplateCallback::new( &app_handle, - &PluginWindowContext::new(&window), + &PluginContext::new(&window), RenderPurpose::Send, ), &RenderOptions { @@ -283,7 +283,12 @@ pub(crate) async fn connect( .collect(), }; let plugin_result = plugin_manager - .call_http_authentication(&window, &authentication_type, plugin_req) + .call_http_authentication( + &window, + &authentication_type, + plugin_req, + &PluginContext::new(&window), + ) .await?; for header in plugin_result.set_headers.unwrap_or_default() { match (HeaderName::from_str(&header.name), HeaderValue::from_str(&header.value)) { diff --git a/src-web/components/DynamicForm.tsx b/src-web/components/DynamicForm.tsx index 1ff88501..9313318e 100644 --- a/src-web/components/DynamicForm.tsx +++ b/src-web/components/DynamicForm.tsx @@ -187,7 +187,7 @@ function FormInputs>({ summary={input.label} className={classNames('!mb-auto', disabled && 'opacity-disabled')} > -
+