From 961ad36c736e08bed79baedb9962c020940d5d9a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 29 Jun 2026 16:29:42 -0700 Subject: [PATCH] Add request message size setting --- .../components/HttpAuthenticationEditor.tsx | 71 +++- .../components/ModelSettingsEditor.tsx | 383 +++++++++++++++--- .../components/Settings/SettingsGeneral.tsx | 87 ++-- .../components/WebsocketResponsePane.tsx | 18 +- .../components/WorkspaceSettingsDialog.tsx | 4 +- .../components/core/PlainInput.tsx | 11 +- apps/yaak-client/hooks/useAuthTab.tsx | 283 +++++++------ apps/yaak-client/lib/requestSettings.ts | 28 +- crates-tauri/yaak-app-client/src/lib.rs | 24 +- crates-tauri/yaak-app-client/src/ws_ext.rs | 34 +- crates/yaak-git/bindings/gen_models.ts | 4 + crates/yaak-grpc/src/client.rs | 16 +- crates/yaak-grpc/src/manager.rs | 104 ++++- crates/yaak-grpc/src/reflection.rs | 10 +- crates/yaak-models/bindings/gen_models.ts | 7 +- .../20260629000000_request-message-size.sql | 7 + crates/yaak-models/src/models.rs | 49 ++- crates/yaak-models/src/queries/folders.rs | 8 + .../yaak-models/src/queries/grpc_requests.rs | 8 + .../yaak-models/src/queries/http_requests.rs | 1 + .../src/queries/websocket_requests.rs | 8 + crates/yaak-models/src/queries/workspaces.rs | 5 + crates/yaak-plugins/bindings/gen_models.ts | 7 +- crates/yaak-sync/bindings/gen_models.ts | 4 + crates/yaak-ws/src/connect.rs | 12 +- crates/yaak-ws/src/error.rs | 4 +- crates/yaak-ws/src/manager.rs | 36 +- .../src/bindings/gen_models.ts | 7 +- 28 files changed, 930 insertions(+), 310 deletions(-) create mode 100644 crates/yaak-models/migrations/20260629000000_request-message-size.sql diff --git a/apps/yaak-client/components/HttpAuthenticationEditor.tsx b/apps/yaak-client/components/HttpAuthenticationEditor.tsx index ec08c115..278ea922 100644 --- a/apps/yaak-client/components/HttpAuthenticationEditor.tsx +++ b/apps/yaak-client/components/HttpAuthenticationEditor.tsx @@ -10,14 +10,17 @@ import { HStack, Icon, InlineCode } from "@yaakapp-internal/ui"; import { useCallback } from "react"; import { openFolderSettings } from "../commands/openFolderSettings"; import { openWorkspaceSettings } from "../commands/openWorkspaceSettings"; +import { useAuthDropdownOptions } from "../hooks/useAuthTab"; import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig"; import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication"; import { useRenderTemplate } from "../hooks/useRenderTemplate"; import { resolvedModelName } from "../lib/resolvedModelName"; +import { Button } from "./core/Button"; import { Dropdown, type DropdownItem } from "./core/Dropdown"; import { IconButton } from "./core/IconButton"; import { Input, type InputProps } from "./core/Input"; import { Link } from "./core/Link"; +import { RadioDropdown } from "./core/RadioDropdown"; import { SegmentedControl } from "./core/SegmentedControl"; import { DynamicForm } from "./DynamicForm"; import { EmptyStateText } from "./EmptyStateText"; @@ -35,7 +38,8 @@ export function HttpAuthenticationEditor({ model }: Props) { ); const handleChange = useCallback( - async (authentication: Record) => await patchModel(model, { authentication }), + async (authentication: Record) => + await patchModel(model, { authentication }), [model], ); @@ -47,7 +51,8 @@ export function HttpAuthenticationEditor({ model }: Props) { return (

- Auth plugin not found for {model.authenticationType} + Auth plugin not found for{" "} + {model.authenticationType}

); @@ -56,11 +61,20 @@ export function HttpAuthenticationEditor({ model }: Props) { if (inheritedAuth == null) { if (model.model === "workspace" || model.model === "folder") { return ( - -

- Apply auth to all requests in {resolvedModelName(model)} -

- Documentation + +
+

+ Choose an auth method to apply it to all requests in{" "} + + {resolvedModelName(model)} + + . +

+ + + Documentation + +
); } @@ -83,7 +97,8 @@ export function HttpAuthenticationEditor({ model }: Props) { type="submit" className="underline hover:text-text" onClick={() => { - if (inheritedAuth.model === "folder") openFolderSettings(inheritedAuth.id, "auth"); + if (inheritedAuth.model === "folder") + openFolderSettings(inheritedAuth.id, "auth"); else openWorkspaceSettings("auth"); }} > @@ -103,7 +118,8 @@ export function HttpAuthenticationEditor({ model }: Props) { hideLabel name="enabled" value={ - model.authentication.disabled === false || model.authentication.disabled == null + model.authentication.disabled === false || + model.authentication.disabled == null ? "__TRUE__" : model.authentication.disabled === true ? "__FALSE__" @@ -151,7 +167,9 @@ export function HttpAuthenticationEditor({ model }: Props) { className="w-full" stateKey={`auth.${model.id}.dynamic`} value={model.authentication.disabled} - onChange={(v) => handleChange({ ...model.authentication, disabled: v })} + onChange={(v) => + handleChange({ ...model.authentication, disabled: v }) + } /> )} @@ -169,6 +187,33 @@ export function HttpAuthenticationEditor({ model }: Props) { ); } +function AuthenticationTypeDropdown({ model }: Props) { + const options = useAuthDropdownOptions(model); + + if (options == null) return null; + + return ( + + + + ); +} + function AuthenticationDisabledInput({ value, onChange, @@ -198,7 +243,11 @@ function AuthenticationDisabledInput({ rightSlot={
- {rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"} + {rendered.isPending + ? "loading" + : rendered.data + ? "enabled" + : "disabled"}
} diff --git a/apps/yaak-client/components/ModelSettingsEditor.tsx b/apps/yaak-client/components/ModelSettingsEditor.tsx index 8d37913d..bfe2e722 100644 --- a/apps/yaak-client/components/ModelSettingsEditor.tsx +++ b/apps/yaak-client/components/ModelSettingsEditor.tsx @@ -13,6 +13,7 @@ import { modelSupportsSetting, type RequestSettingDefinition, SETTING_FOLLOW_REDIRECTS, + SETTING_REQUEST_MESSAGE_SIZE, SETTING_REQUEST_TIMEOUT, SETTING_SEND_COOKIES, SETTING_STORE_COOKIES, @@ -22,21 +23,44 @@ import { Checkbox } from "./core/Checkbox"; import { PlainInput } from "./core/PlainInput"; import { SettingOverrideRow, + SettingRow, SettingRowBoolean, - SettingRowNumber, SettingsList, SettingsSection, } from "./core/SettingRow"; +const BYTES_PER_MB = 1024 * 1024; +const MAX_REQUEST_MESSAGE_SIZE_BYTES = 2_147_483_647; +const MAX_MESSAGE_SIZE_MB = MAX_REQUEST_MESSAGE_SIZE_BYTES / BYTES_PER_MB; + interface Props { showSectionTitles?: boolean; model: ModelWithSettings; } -type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; +type ModelWithSettings = + | Workspace + | Folder + | HttpRequest + | WebsocketRequest + | GrpcRequest; type ModelWithHttpSettings = Workspace | Folder | HttpRequest; -type ModelWithTlsSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; -type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest; +type ModelWithTlsSettings = + | Workspace + | Folder + | HttpRequest + | WebsocketRequest + | GrpcRequest; +type ModelWithCookieSettings = + | Workspace + | Folder + | HttpRequest + | WebsocketRequest; +type ModelWithMessageSizeSettings = + | Workspace + | Folder + | WebsocketRequest + | GrpcRequest; type BooleanSetting = boolean | InheritedBoolSetting; type IntegerSetting = number | InheritedIntSetting; type CookieSettingsPatch = { @@ -50,12 +74,19 @@ type HttpSettingsPatch = { type TlsSettingsPatch = { settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"]; }; +type MessageSizeSettingsPatch = { + settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"]; +}; -export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) { +export function ModelSettingsEditor({ + model, + showSectionTitles = false, +}: Props) { const ancestors = useModelAncestors(model); const supportsHttpSettings = modelSupportsHttpSettings(model); const supportsCookieSettings = modelSupportsCookieSettings(model); const supportsTlsSettings = modelSupportsTlsSettings(model); + const supportsMessageSizeSettings = modelSupportsMessageSizeSettings(model); return ( @@ -77,6 +108,22 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) } /> )} + {supportsMessageSizeSettings && ( + + patchMessageSizeSettings(model, { + settingRequestMessageSize, + }) + } + /> + )} )} {supportsCookieSettings && ( - + isInheritedSetting(setting) && setting.enabled === true) - .length; + if (modelSupportsMessageSizeSettings(model)) { + settings.push(model.settingRequestMessageSize); + } + + return settings.filter( + (setting) => isInheritedSetting(setting) && setting.enabled === true, + ).length; } -function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial) { - if (model.model === "workspace") return patchModel(model, patch as Partial); - if (model.model === "folder") return patchModel(model, patch as Partial); - if (model.model === "http_request") return patchModel(model, patch as Partial); - if (model.model === "websocket_request") - return patchModel(model, patch as Partial); - throw new Error("Unsupported cookie settings model"); +function patchCookieSettings( + model: ModelWithCookieSettings, + patch: Partial, +) { + switch (model.model) { + case "workspace": + return patchModel(model, patch as Partial); + case "folder": + return patchModel(model, patch as Partial); + case "http_request": + return patchModel(model, patch as Partial); + case "websocket_request": + return patchModel(model, patch as Partial); + } } -function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial) { - if (model.model === "workspace") return patchModel(model, patch as Partial); - if (model.model === "folder") return patchModel(model, patch as Partial); - return patchModel(model, patch as Partial); +function patchHttpSettings( + model: ModelWithHttpSettings, + patch: Partial, +) { + switch (model.model) { + case "workspace": + return patchModel(model, patch as Partial); + case "folder": + return patchModel(model, patch as Partial); + case "http_request": + return patchModel(model, patch as Partial); + } } -function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial) { - if (model.model === "workspace") return patchModel(model, patch as Partial); - if (model.model === "folder") return patchModel(model, patch as Partial); - if (model.model === "http_request") return patchModel(model, patch as Partial); - if (model.model === "websocket_request") - return patchModel(model, patch as Partial); - return patchModel(model, patch as Partial); +function patchTlsSettings( + model: ModelWithTlsSettings, + patch: Partial, +) { + switch (model.model) { + case "workspace": + return patchModel(model, patch as Partial); + case "folder": + return patchModel(model, patch as Partial); + case "http_request": + return patchModel(model, patch as Partial); + case "websocket_request": + return patchModel(model, patch as Partial); + case "grpc_request": + return patchModel(model, patch as Partial); + } } -function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings { +function patchMessageSizeSettings( + model: ModelWithMessageSizeSettings, + patch: Partial, +) { + switch (model.model) { + case "workspace": + return patchModel(model, patch as Partial); + case "folder": + return patchModel(model, patch as Partial); + case "websocket_request": + return patchModel(model, patch as Partial); + case "grpc_request": + return patchModel(model, patch as Partial); + } +} + +function modelSupportsHttpSettings( + model: ModelWithSettings, +): model is ModelWithHttpSettings { return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT); } -function modelSupportsCookieSettings(model: ModelWithSettings): model is ModelWithCookieSettings { +function modelSupportsCookieSettings( + model: ModelWithSettings, +): model is ModelWithCookieSettings { return modelSupportsSetting(model, SETTING_SEND_COOKIES); } -function modelSupportsTlsSettings(model: ModelWithSettings): model is ModelWithTlsSettings { +function modelSupportsTlsSettings( + model: ModelWithSettings, +): model is ModelWithTlsSettings { return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES); } +function modelSupportsMessageSizeSettings( + model: ModelWithSettings, +): model is ModelWithMessageSizeSettings { + return modelSupportsSetting(model, SETTING_REQUEST_MESSAGE_SIZE); +} + function BooleanSettingRow({ inheritedValue, setting, @@ -211,7 +317,11 @@ function BooleanSettingRow({ }) { const inherited = isInheritedSetting(setting); const overridden = inherited ? setting.enabled === true : false; - const value = inherited ? (overridden ? setting.value : inheritedValue) : setting; + const value = inherited + ? overridden + ? setting.value + : inheritedValue + : setting; if (!inherited) { return ( @@ -255,19 +365,28 @@ function IntegerSettingRow({ }) { const inherited = isInheritedSetting(setting); const overridden = inherited ? setting.enabled === true : false; - const value = inherited ? (overridden ? setting.value : inheritedValue) : setting; + const value = inherited + ? overridden + ? setting.value + : inheritedValue + : setting; if (!inherited) { return ( - value === "" || Number.parseInt(value, 10) >= 0} - onChange={(value) => onChange(value)} - /> + > + onChange(parseInteger(value))} + /> + ); } @@ -278,21 +397,18 @@ function IntegerSettingRow({ overridden={overridden} onResetOverride={() => onChange({ ...setting, enabled: false })} > - value === "" || Number.parseInt(value, 10) >= 0} + validate={isValidInteger} onChange={(value) => onChange({ ...setting, enabled: true, - value: Number.parseInt(value, 10) || 0, + value: parseInteger(value), }) } /> @@ -300,6 +416,141 @@ function IntegerSettingRow({ ); } +function MessageSizeSettingRow({ + inheritedValue, + setting, + settingDefinition, + onChange, +}: { + inheritedValue: number; + setting: IntegerSetting; + settingDefinition: RequestSettingDefinition<"settingRequestMessageSize">; + onChange: (setting: IntegerSetting) => void; +}) { + const inherited = isInheritedSetting(setting); + const overridden = inherited ? setting.enabled === true : false; + const value = inherited + ? overridden + ? setting.value + : inheritedValue + : setting; + const displayValue = formatMegabytes(value); + const placeholder = formatMegabytes(settingDefinition.defaultValue); + + if (!inherited) { + return ( + + onChange(parseMegabytes(value))} + /> + + ); + } + + return ( + onChange({ ...setting, enabled: false })} + > + + onChange({ + ...setting, + enabled: true, + value: parseMegabytes(value), + }) + } + /> + + ); +} + +function MessageSizeInput({ + label, + name, + onChange, + placeholder, + value, +}: { + label: string; + name: string; + onChange: (value: string) => void; + placeholder: string; + value: string; +}) { + return ( + + ); +} + +function NumberUnitInput({ + inputMode, + label, + name, + onChange, + placeholder, + step, + unit, + validate, + value, +}: { + inputMode?: "decimal" | "numeric"; + label: string; + name: string; + onChange: (value: string) => void; + placeholder: string; + step?: number | "any"; + unit: string; + validate: (value: string) => boolean; + value: string; +}) { + return ( + + {unit} + + } + onChange={onChange} + /> + ); +} + function isInheritedSetting( setting: T | { enabled?: boolean; value: T }, ): setting is { enabled?: boolean; value: T } { @@ -308,7 +559,7 @@ function isInheritedSetting( function resolveInheritedValue( ancestors: (Folder | Workspace)[], - key: "settingRequestTimeout", + key: "settingRequestTimeout" | "settingRequestMessageSize", fallback: IntegerSetting, ): number; function resolveInheritedValue( @@ -338,10 +589,46 @@ function resolveInheritedValue( type WorkspaceSettings = Pick< Workspace, | "settingFollowRedirects" + | "settingRequestMessageSize" | "settingRequestTimeout" | "settingSendCookies" | "settingStoreCookies" | "settingValidateCertificates" >; -type BooleanWorkspaceSettingKey = Exclude; +type BooleanWorkspaceSettingKey = Exclude< + keyof WorkspaceSettings, + "settingRequestTimeout" | "settingRequestMessageSize" +>; + +function formatMegabytes(bytes: number) { + const megabytes = bytes / BYTES_PER_MB; + return Number.isInteger(megabytes) + ? `${megabytes}` + : megabytes.toFixed(3).replace(/\.?0+$/, ""); +} + +function parseMegabytes(value: string) { + const megabytes = Number(value); + return Number.isFinite(megabytes) ? Math.round(megabytes * BYTES_PER_MB) : 0; +} + +function parseInteger(value: string) { + const parsed = Number(value); + return Number.isFinite(parsed) ? Math.trunc(parsed) : 0; +} + +function isValidInteger(value: string) { + const parsed = Number(value); + return value === "" || (Number.isInteger(parsed) && parsed >= 0); +} + +function isValidMegabytes(value: string) { + if (value === "") return true; + const megabytes = Number(value); + return ( + Number.isFinite(megabytes) && + megabytes >= 0 && + megabytes <= MAX_MESSAGE_SIZE_MB + ); +} diff --git a/apps/yaak-client/components/Settings/SettingsGeneral.tsx b/apps/yaak-client/components/Settings/SettingsGeneral.tsx index 42220d7b..91c16f0c 100644 --- a/apps/yaak-client/components/Settings/SettingsGeneral.tsx +++ b/apps/yaak-client/components/Settings/SettingsGeneral.tsx @@ -2,22 +2,14 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { patchModel, settingsAtom } from "@yaakapp-internal/models"; import { Heading, VStack } from "@yaakapp-internal/ui"; import { useAtomValue } from "jotai"; -import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace"; import { useCheckForUpdates } from "../../hooks/useCheckForUpdates"; import { appInfo } from "../../lib/appInfo"; -import { - SETTING_FOLLOW_REDIRECTS, - SETTING_REQUEST_TIMEOUT, - SETTING_SEND_COOKIES, - SETTING_STORE_COOKIES, - SETTING_VALIDATE_CERTIFICATES, -} from "../../lib/requestSettings"; import { revealInFinderText } from "../../lib/reveal"; import { CargoFeature } from "../CargoFeature"; +import { DismissibleBanner } from "../core/DismissibleBanner"; import { IconButton } from "../core/IconButton"; import { ModelSettingRowBoolean, - ModelSettingRowNumber, ModelSettingSelectControl, SettingValue, SettingRow, @@ -27,20 +19,26 @@ import { SettingsSection, } from "../core/SettingRow"; +const WORKSPACE_SETTINGS_MOVED_AT = "2026-06-30"; + export function SettingsGeneral() { - const workspace = useAtomValue(activeWorkspaceAtom); const settings = useAtomValue(settingsAtom); const checkForUpdates = useCheckForUpdates(); - if (settings == null || workspace == null) { + if (settings == null) { return null; } + const showWorkspaceSettingsMovedBanner = + settings.createdAt.slice(0, 10) < WORKSPACE_SETTINGS_MOVED_AT; + return (
General -

Configure general settings for update behavior and more.

+

+ Configure general settings for update behavior and more. +

@@ -76,7 +74,9 @@ export function SettingsGeneral() { description="Choose whether updates are installed automatically or manually." name="autoupdate" value={settings.autoupdate ? "auto" : "manual"} - onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })} + onChange={(v) => + patchModel(settings, { autoupdate: v === "auto" }) + } options={[ { label: "Automatic", value: "auto" }, { label: "Manual", value: "manual" }, @@ -108,54 +108,19 @@ export function SettingsGeneral() {
- - Workspace{" "} - - {workspace.name} - - - } - > - Number.parseInt(value, 10) >= 0} - /> - - - - - - - - - + {showWorkspaceSettingsMovedBanner && ( + +

+ Workspace specific settings have moved to{" "} + Workspace Settings, accessible from the workspace switcher + menu. +

+
+ )} diff --git a/apps/yaak-client/components/WebsocketResponsePane.tsx b/apps/yaak-client/components/WebsocketResponsePane.tsx index e3284db2..42806e4e 100644 --- a/apps/yaak-client/components/WebsocketResponsePane.tsx +++ b/apps/yaak-client/components/WebsocketResponsePane.tsx @@ -105,10 +105,18 @@ function WebsocketEventRow({ : ""; const iconColor = - messageType === "close" || messageType === "open" ? "secondary" : isServer ? "info" : "primary"; + messageType === "error" + ? "warning" + : messageType === "close" || messageType === "open" + ? "secondary" + : isServer + ? "info" + : "primary"; const icon = - messageType === "close" || messageType === "open" + messageType === "error" + ? "alert_triangle" + : messageType === "close" || messageType === "open" ? "info" : isServer ? "arrow_big_down_dash" @@ -119,6 +127,8 @@ function WebsocketEventRow({ "Disconnected from server" ) : messageType === "open" ? ( "Connected to server" + ) : messageType === "error" ? ( + {message} ) : message === "" ? ( No content ) : ( @@ -170,7 +180,9 @@ function WebsocketEventDetail({ ? "Connection Closed" : event.messageType === "open" ? "Connection Open" - : `Message ${event.isServer ? "Received" : "Sent"}`; + : event.messageType === "error" + ? "WebSocket Error" + : `Message ${event.isServer ? "Received" : "Sent"}`; const actions: EventDetailAction[] = message !== "" diff --git a/apps/yaak-client/components/WorkspaceSettingsDialog.tsx b/apps/yaak-client/components/WorkspaceSettingsDialog.tsx index fff5ac95..f90351ba 100644 --- a/apps/yaak-client/components/WorkspaceSettingsDialog.tsx +++ b/apps/yaak-client/components/WorkspaceSettingsDialog.tsx @@ -112,7 +112,9 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) { onCreateNewWorkspace={hide} onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })} /> - +
+ +
diff --git a/apps/yaak-client/components/core/PlainInput.tsx b/apps/yaak-client/components/core/PlainInput.tsx index 4361bba3..0aba672a 100644 --- a/apps/yaak-client/components/core/PlainInput.tsx +++ b/apps/yaak-client/components/core/PlainInput.tsx @@ -1,6 +1,6 @@ import { HStack } from "@yaakapp-internal/ui"; import classNames from "classnames"; -import type { FocusEvent, HTMLAttributes, ReactNode } from "react"; +import type { FocusEvent, InputHTMLAttributes, ReactNode } from "react"; import { forwardRef, useCallback, @@ -28,10 +28,9 @@ export type PlainInputProps = Omit< | "extraExtensions" | "forcedEnvironmentId" > & - Pick, "onKeyDownCapture"> & { - onFocusRaw?: HTMLAttributes["onFocus"]; + Pick, "inputMode" | "onKeyDownCapture" | "step"> & { + onFocusRaw?: InputHTMLAttributes["onFocus"]; type?: "text" | "password" | "number"; - step?: number; hideObscureToggle?: boolean; labelRightSlot?: ReactNode; }; @@ -52,6 +51,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun labelClassName, labelPosition = "top", labelRightSlot, + inputMode, leftSlot, name, onBlur, @@ -64,6 +64,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun required, rightSlot, size = "md", + step, tint, type = "text", validate, @@ -204,12 +205,14 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun autoComplete="off" autoCapitalize="off" autoCorrect="off" + inputMode={inputMode} onChange={(e) => handleChange(e.target.value)} onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))} className={classNames(commonClassName, "h-full disabled:opacity-disabled")} onFocus={handleFocus} onBlur={handleBlur} required={required} + step={step} placeholder={placeholder} onKeyDownCapture={onKeyDownCapture} /> diff --git a/apps/yaak-client/hooks/useAuthTab.tsx b/apps/yaak-client/hooks/useAuthTab.tsx index 91e7c99d..e094b6e5 100644 --- a/apps/yaak-client/hooks/useAuthTab.tsx +++ b/apps/yaak-client/hooks/useAuthTab.tsx @@ -5,6 +5,7 @@ import { useMemo } from "react"; import { openFolderSettings } from "../commands/openFolderSettings"; import { openWorkspaceSettings } from "../commands/openWorkspaceSettings"; import { IconTooltip } from "../components/core/IconTooltip"; +import type { RadioDropdownProps } from "../components/core/RadioDropdown"; import type { TabItem } from "../components/core/Tabs/Tabs"; import { capitalize } from "../lib/capitalize"; import { showConfirm } from "../lib/confirm"; @@ -14,156 +15,192 @@ import type { AuthenticatedModel } from "./useInheritedAuthentication"; import { useInheritedAuthentication } from "./useInheritedAuthentication"; import { useModelAncestors } from "./useModelAncestors"; -export function useAuthTab(tabValue: T, model: AuthenticatedModel | null) { +export function useAuthTab( + tabValue: T, + model: AuthenticatedModel | null, +) { + const options = useAuthDropdownOptions(model); + + return useMemo(() => { + if (model == null || options == null) return []; + + const tab: TabItem = { + value: tabValue, + label: "Auth", + options, + }; + + return [tab]; + }, [model, options, tabValue]); +} + +export function useAuthDropdownOptions( + model: AuthenticatedModel | null, +): Omit | null { const authentication = useHttpAuthenticationSummaries(); const inheritedAuth = useInheritedAuthentication(model); const ancestors = useModelAncestors(model); const parentModel = ancestors[0] ?? null; - return useMemo(() => { - if (model == null) return []; + return useMemo(() => { + if (model == null) return null; - const tab: TabItem = { - value: tabValue, - label: "Auth", - options: { - value: model.authenticationType, - items: [ - ...authentication.map((a) => ({ - label: a.label || "UNKNOWN", - shortLabel: a.shortLabel, - value: a.name, - })), - { type: "separator" }, - { - label: "Inherit from Parent", - shortLabel: - inheritedAuth != null && inheritedAuth.authenticationType !== "none" ? ( - - {authentication.find((a) => a.name === inheritedAuth.authenticationType) - ?.shortLabel ?? "UNKNOWN"} - - - ) : ( - "Auth" - ), - value: null, - }, - { label: "No Auth", shortLabel: "No Auth", value: "none" }, - ], - itemsAfter: (() => { - const actions: ( - | { type: "separator"; label: string } - | { label: string; leftSlot: React.ReactNode; onSelect: () => Promise } - )[] = []; - - // Promote: move auth from current model up to parent - if ( - parentModel && - model.authenticationType && - model.authenticationType !== "none" && - (parentModel.authenticationType == null || parentModel.authenticationType === "none") - ) { - actions.push( - { type: "separator", label: "Actions" }, - { - label: `Promote to ${capitalize(parentModel.model)}`, - leftSlot: ( - - ), - onSelect: async () => { - const confirmed = await showConfirm({ - id: "promote-auth-confirm", - title: "Promote Authentication", - confirmText: "Promote", - description: ( - <> - Move authentication config to{" "} - {resolvedModelName(parentModel)}? - - ), - }); - if (confirmed) { - await patchModel(model, { authentication: {}, authenticationType: null }); - await patchModel(parentModel, { - authentication: model.authentication, - authenticationType: model.authenticationType, - }); - - if (parentModel.model === "folder") { - openFolderSettings(parentModel.id, "auth"); - } else { - openWorkspaceSettings("auth"); - } - } - }, - }, - ); - } - - // Copy from ancestor: copy auth config down to current model - const ancestorWithAuth = ancestors.find( - (a) => a.authenticationType != null && a.authenticationType !== "none", - ); - if (ancestorWithAuth) { - if (actions.length === 0) { - actions.push({ type: "separator", label: "Actions" }); + return { + value: model.authenticationType, + items: [ + ...authentication.map((a) => ({ + label: a.label || "UNKNOWN", + shortLabel: a.shortLabel, + value: a.name, + })), + { type: "separator" }, + { + label: "Inherit from Parent", + shortLabel: + inheritedAuth != null && + inheritedAuth.authenticationType !== "none" ? ( + + {authentication.find( + (a) => a.name === inheritedAuth.authenticationType, + )?.shortLabel ?? "UNKNOWN"} + + + ) : ( + "Auth" + ), + value: null, + }, + { label: "No Auth", shortLabel: "No Auth", value: "none" }, + ], + itemsAfter: (() => { + const actions: ( + | { type: "separator"; label: string } + | { + label: string; + leftSlot: React.ReactNode; + onSelect: () => Promise; } - actions.push({ - label: `Copy from ${modelTypeLabel(ancestorWithAuth)}`, + )[] = []; + + // Promote: move auth from current model up to parent + if ( + parentModel && + model.authenticationType && + model.authenticationType !== "none" && + (parentModel.authenticationType == null || + parentModel.authenticationType === "none") + ) { + actions.push( + { type: "separator", label: "Actions" }, + { + label: `Promote to ${capitalize(parentModel.model)}`, leftSlot: ( ), onSelect: async () => { const confirmed = await showConfirm({ - id: "copy-auth-confirm", - title: "Copy Authentication", - confirmText: "Copy", + id: "promote-auth-confirm", + title: "Promote Authentication", + confirmText: "Promote", description: ( <> - Copy{" "} - {authentication.find((a) => a.name === ancestorWithAuth.authenticationType) - ?.label ?? "authentication"}{" "} - config from {resolvedModelName(ancestorWithAuth)}? - This will override the current authentication but will not affect the{" "} - {modelTypeLabel(ancestorWithAuth).toLowerCase()}. + Move authentication config to{" "} + {resolvedModelName(parentModel)}? ), }); if (confirmed) { await patchModel(model, { - authentication: { ...ancestorWithAuth.authentication }, - authenticationType: ancestorWithAuth.authenticationType, + authentication: {}, + authenticationType: null, }); + await patchModel(parentModel, { + authentication: model.authentication, + authenticationType: model.authenticationType, + }); + + if (parentModel.model === "folder") { + openFolderSettings(parentModel.id, "auth"); + } else { + openWorkspaceSettings("auth"); + } } }, - }); - } + }, + ); + } - return actions.length > 0 ? actions : undefined; - })(), - onChange: async (authenticationType) => { - let authentication: Folder["authentication"] = model.authentication; - if (model.authenticationType !== authenticationType) { - authentication = { - // Reset auth if changing types - }; + // Copy from ancestor: copy auth config down to current model + const ancestorWithAuth = ancestors.find( + (a) => + a.authenticationType != null && a.authenticationType !== "none", + ); + if (ancestorWithAuth) { + if (actions.length === 0) { + actions.push({ type: "separator", label: "Actions" }); } - await patchModel(model, { authentication, authenticationType }); - }, + actions.push({ + label: `Copy from ${modelTypeLabel(ancestorWithAuth)}`, + leftSlot: ( + + ), + onSelect: async () => { + const confirmed = await showConfirm({ + id: "copy-auth-confirm", + title: "Copy Authentication", + confirmText: "Copy", + description: ( + <> + Copy{" "} + {authentication.find( + (a) => a.name === ancestorWithAuth.authenticationType, + )?.label ?? "authentication"}{" "} + config from{" "} + + {resolvedModelName(ancestorWithAuth)} + + ? This will override the current authentication but will not + affect the {modelTypeLabel(ancestorWithAuth).toLowerCase()}. + + ), + }); + if (confirmed) { + await patchModel(model, { + authentication: { ...ancestorWithAuth.authentication }, + authenticationType: ancestorWithAuth.authenticationType, + }); + } + }, + }); + } + + return actions.length > 0 ? actions : undefined; + })(), + onChange: async (authenticationType) => { + let authentication: Folder["authentication"] = model.authentication; + if (model.authenticationType !== authenticationType) { + authentication = { + // Reset auth if changing types + }; + } + await patchModel(model, { authentication, authenticationType }); }, }; - - return [tab]; - }, [authentication, inheritedAuth, model, parentModel, tabValue, ancestors]); + }, [authentication, inheritedAuth, model, parentModel, ancestors]); } diff --git a/apps/yaak-client/lib/requestSettings.ts b/apps/yaak-client/lib/requestSettings.ts index f33b3877..0fd8fa94 100644 --- a/apps/yaak-client/lib/requestSettings.ts +++ b/apps/yaak-client/lib/requestSettings.ts @@ -5,6 +5,7 @@ type ModelType = AnyModel["model"]; type WorkspaceRequestSettings = Pick< Workspace, | "settingFollowRedirects" + | "settingRequestMessageSize" | "settingRequestTimeout" | "settingSendCookies" | "settingStoreCookies" @@ -17,7 +18,9 @@ type ModelTypeWithSetting = { [M in ModelType]: K extends keyof ModelForType ? M : never; }[ModelType]; -export type RequestSettingDefinition = { +export type RequestSettingDefinition< + K extends RequestSettingKey = RequestSettingKey, +> = { defaultValue: WorkspaceRequestSettings[K]; description: string; modelKey: K; @@ -41,11 +44,26 @@ export const SETTING_REQUEST_TIMEOUT = defineRequestSetting({ title: "Request Timeout", }); +export const SETTING_REQUEST_MESSAGE_SIZE = defineRequestSetting({ + defaultValue: 64 * 1024 * 1024, + description: + "Maximum gRPC or WebSocket message size in MB. Set to 0 to disable.", + modelKey: "settingRequestMessageSize", + models: ["workspace", "folder", "websocket_request", "grpc_request"], + title: "Message Size Limit", +}); + export const SETTING_VALIDATE_CERTIFICATES = defineRequestSetting({ defaultValue: true, description: "When disabled, skip validation of server certificates.", modelKey: "settingValidateCertificates", - models: ["workspace", "folder", "http_request", "websocket_request", "grpc_request"], + models: [ + "workspace", + "folder", + "http_request", + "websocket_request", + "grpc_request", + ], title: "Validate TLS certificates", }); @@ -59,7 +77,8 @@ export const SETTING_FOLLOW_REDIRECTS = defineRequestSetting({ export const SETTING_SEND_COOKIES = defineRequestSetting({ defaultValue: true, - description: "Attach matching cookies from the active cookie jar to outgoing requests.", + description: + "Attach matching cookies from the active cookie jar to outgoing requests.", modelKey: "settingSendCookies", models: ["workspace", "folder", "http_request", "websocket_request"], title: "Automatically send cookies", @@ -67,7 +86,8 @@ export const SETTING_SEND_COOKIES = defineRequestSetting({ export const SETTING_STORE_COOKIES = defineRequestSetting({ defaultValue: true, - description: "Save cookies from Set-Cookie response headers to the active cookie jar.", + description: + "Save cookies from Set-Cookie response headers to the active cookie jar.", modelKey: "settingStoreCookies", models: ["workspace", "folder", "http_request", "websocket_request"], title: "Automatically store cookies", diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index bf6d02ce..420c60f5 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -295,7 +295,8 @@ async fn cmd_grpc_reflect( unrendered_request.folder_id.as_deref(), environment_id, )?; - let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; + let resolved_settings = + app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; let plugin_manager = Arc::new((*app_handle.state::()).clone()); let encryption_manager = Arc::new((*app_handle.state::()).clone()); @@ -332,6 +333,7 @@ async fn cmd_grpc_reflect( &metadata, resolved_settings.validate_certificates.value, client_certificate, + resolved_settings.request_message_size.value, ) .await .map_err(|e| GenericError(e.to_string()))?) @@ -353,7 +355,8 @@ async fn cmd_grpc_go( unrendered_request.folder_id.as_deref(), environment_id, )?; - let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; + let resolved_settings = + app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; let plugin_manager = Arc::new((*app_handle.state::()).clone()); let encryption_manager = Arc::new((*app_handle.state::()).clone()); @@ -425,6 +428,7 @@ async fn cmd_grpc_go( &metadata, resolved_settings.validate_certificates.value, client_cert.clone(), + resolved_settings.request_message_size.value, ) .await; @@ -714,7 +718,7 @@ async fn cmd_grpc_go( Some(s) => GrpcEvent { error: Some(s.message().to_string()), status: Some(s.code() as i32), - content: "Failed to connect".to_string(), + content: "Request failed".to_string(), metadata: metadata_to_map(s.metadata().clone()), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() @@ -722,7 +726,7 @@ async fn cmd_grpc_go( None => GrpcEvent { error: Some(e.message), status: Some(Code::Unknown as i32), - content: "Failed to connect".to_string(), + content: "Request failed".to_string(), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() }, @@ -738,7 +742,7 @@ async fn cmd_grpc_go( &GrpcEvent { error: Some(e.to_string()), status: Some(Code::Unknown as i32), - content: "Failed to connect".to_string(), + content: "Request failed".to_string(), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() }, @@ -781,7 +785,7 @@ async fn cmd_grpc_go( Some(s) => GrpcEvent { error: Some(s.message().to_string()), status: Some(s.code() as i32), - content: "Failed to connect".to_string(), + content: "Stream failed".to_string(), metadata: metadata_to_map(s.metadata().clone()), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() @@ -789,7 +793,7 @@ async fn cmd_grpc_go( None => GrpcEvent { error: Some(e.message), status: Some(Code::Unknown as i32), - content: "Failed to connect".to_string(), + content: "Stream failed".to_string(), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() }, @@ -806,7 +810,7 @@ async fn cmd_grpc_go( &GrpcEvent { error: Some(e.to_string()), status: Some(Code::Unknown as i32), - content: "Failed to connect".to_string(), + content: "Stream failed".to_string(), event_type: GrpcEventType::ConnectionEnd, ..base_event.clone() }, @@ -878,7 +882,8 @@ async fn cmd_grpc_go( .db() .upsert_grpc_event( &GrpcEvent { - content: status.to_string(), + content: "Stream failed".to_string(), + error: Some(status.message().to_string()), status: Some(status.code() as i32), metadata: metadata_to_map(status.metadata().clone()), event_type: GrpcEventType::ConnectionEnd, @@ -887,6 +892,7 @@ async fn cmd_grpc_go( &UpdateSource::from_window_label(window.label()), ) .unwrap(); + break; } } } diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs index e627f867..088c7ea1 100644 --- a/crates-tauri/yaak-app-client/src/ws_ext.rs +++ b/crates-tauri/yaak-app-client/src/ws_ext.rs @@ -50,6 +50,37 @@ pub async fn cmd_ws_send( ws_manager: State<'_, Mutex>, ) -> Result { let connection = app_handle.db().get_websocket_connection(connection_id)?; + + match send_websocket_message(&connection, environment_id, &app_handle, &window, &ws_manager) + .await + { + Ok(connection) => Ok(connection), + Err(e) => { + app_handle.db().upsert_websocket_event( + &WebsocketEvent { + connection_id: connection.id.clone(), + request_id: connection.request_id.clone(), + workspace_id: connection.workspace_id.clone(), + is_server: false, + message_type: WebsocketEventType::Error, + message: e.to_string().into(), + ..Default::default() + }, + &UpdateSource::from_window_label(window.label()), + )?; + + Ok(connection) + } + } +} + +async fn send_websocket_message( + connection: &WebsocketConnection, + environment_id: Option<&str>, + app_handle: &AppHandle, + window: &WebviewWindow, + ws_manager: &Mutex, +) -> Result { let unrendered_request = app_handle.db().get_websocket_request(&connection.request_id)?; let environment_chain = app_handle.db().resolve_environments( &unrendered_request.workspace_id, @@ -91,7 +122,7 @@ pub async fn cmd_ws_send( &UpdateSource::from_window_label(window.label()), )?; - Ok(connection) + Ok(connection.clone()) } #[command] @@ -299,6 +330,7 @@ pub async fn cmd_ws_connect( receive_tx, resolved_settings.validate_certificates.value, client_cert, + resolved_settings.request_message_size.value, ) .await { diff --git a/crates/yaak-git/bindings/gen_models.ts b/crates/yaak-git/bindings/gen_models.ts index f6bff97c..fa90a9b6 100644 --- a/crates/yaak-git/bindings/gen_models.ts +++ b/crates/yaak-git/bindings/gen_models.ts @@ -46,6 +46,7 @@ export type Folder = { settingValidateCertificates: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting; settingRequestTimeout: InheritedIntSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type GrpcRequest = { @@ -69,6 +70,7 @@ export type GrpcRequest = { */ url: string; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type HttpRequest = { @@ -146,6 +148,7 @@ export type WebsocketRequest = { settingSendCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type Workspace = { @@ -162,6 +165,7 @@ export type Workspace = { settingValidateCertificates: boolean; settingFollowRedirects: boolean; settingRequestTimeout: number; + settingRequestMessageSize: number; settingDnsOverrides: Array; settingSendCookies: boolean; settingStoreCookies: boolean; diff --git a/crates/yaak-grpc/src/client.rs b/crates/yaak-grpc/src/client.rs index 06b201d1..45dedd19 100644 --- a/crates/yaak-grpc/src/client.rs +++ b/crates/yaak-grpc/src/client.rs @@ -33,15 +33,21 @@ impl AutoReflectionClient { uri: &Uri, validate_certificates: bool, client_cert: Option, + max_message_size: usize, ) -> Result { let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin( get_transport(validate_certificates, client_cert.clone())?, uri.clone(), - ); - let client_v1alpha = v1alpha::server_reflection_client::ServerReflectionClient::with_origin( - get_transport(validate_certificates, client_cert.clone())?, - uri.clone(), - ); + ) + .max_decoding_message_size(max_message_size) + .max_encoding_message_size(max_message_size); + let client_v1alpha = + v1alpha::server_reflection_client::ServerReflectionClient::with_origin( + get_transport(validate_certificates, client_cert.clone())?, + uri.clone(), + ) + .max_decoding_message_size(max_message_size) + .max_encoding_message_size(max_message_size); Ok(AutoReflectionClient { use_v1alpha: false, client_v1, client_v1alpha }) } diff --git a/crates/yaak-grpc/src/manager.rs b/crates/yaak-grpc/src/manager.rs index 38720e36..40556bee 100644 --- a/crates/yaak-grpc/src/manager.rs +++ b/crates/yaak-grpc/src/manager.rs @@ -33,16 +33,13 @@ use tonic::transport::Uri; use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming}; use yaak_tls::ClientCertificateConfig; -/// Maximum size for a single gRPC message (64 MB). -/// Tonic defaults to 4 MB, which is too small for large responses. -const GRPC_MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024; - #[derive(Clone)] pub struct GrpcConnection { pool: Arc>, conn: Client, BoxBody>, pub uri: Uri, use_reflection: bool, + max_message_size: usize, } #[derive(Default, Debug)] @@ -101,8 +98,15 @@ impl GrpcConnection { client_cert: Option, ) -> Result> { if self.use_reflection { - reflect_types_for_message(self.pool.clone(), &self.uri, message, metadata, client_cert) - .await?; + reflect_types_for_message( + self.pool.clone(), + &self.uri, + message, + metadata, + client_cert, + self.max_message_size, + ) + .await?; } let method = &self.method(&service, &method).await?; let input_message = method.input(); @@ -111,8 +115,7 @@ impl GrpcConnection { let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?; deserializer.end()?; - let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) - .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE); + let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size); let mut req = req_message.into_request(); decorate_req(metadata, &mut req)?; @@ -137,6 +140,7 @@ impl GrpcConnection { message, metadata, client_cert, + self.max_message_size, ) .await?; @@ -176,6 +180,7 @@ impl GrpcConnection { let md = metadata.clone(); let use_reflection = self.use_reflection.clone(); let client_cert = client_cert.clone(); + let max_message_size = self.max_message_size; stream .then(move |json| { let pool = pool.clone(); @@ -188,8 +193,15 @@ impl GrpcConnection { let json_clone = json.clone(); async move { if use_reflection { - if let Err(e) = - reflect_types_for_message(pool, &uri, &json, &md, client_cert).await + if let Err(e) = reflect_types_for_message( + pool, + &uri, + &json, + &md, + client_cert, + max_message_size, + ) + .await { warn!("Failed to resolve Any types: {e}"); } @@ -211,8 +223,7 @@ impl GrpcConnection { .filter_map(|x| x) }; - let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) - .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE); + let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size); let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); @@ -243,6 +254,7 @@ impl GrpcConnection { let md = metadata.clone(); let use_reflection = self.use_reflection.clone(); let client_cert = client_cert.clone(); + let max_message_size = self.max_message_size; stream .then(move |json| { let pool = pool.clone(); @@ -255,8 +267,15 @@ impl GrpcConnection { let json_clone = json.clone(); async move { if use_reflection { - if let Err(e) = - reflect_types_for_message(pool, &uri, &json, &md, client_cert).await + if let Err(e) = reflect_types_for_message( + pool, + &uri, + &json, + &md, + client_cert, + max_message_size, + ) + .await { warn!("Failed to resolve Any types: {e}"); } @@ -278,8 +297,7 @@ impl GrpcConnection { .filter_map(|x| x) }; - let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) - .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE); + let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size); let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); @@ -307,8 +325,7 @@ impl GrpcConnection { let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?; deserializer.end()?; - let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) - .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE); + let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size); let mut req = req_message.into_request(); decorate_req(metadata, &mut req)?; @@ -320,6 +337,23 @@ impl GrpcConnection { } } +fn grpc_client( + conn: Client, BoxBody>, + uri: Uri, + max_message_size: usize, +) -> tonic::client::Grpc, BoxBody>> { + tonic::client::Grpc::with_origin(conn, uri) + .max_decoding_message_size(max_message_size) + .max_encoding_message_size(max_message_size) +} + +fn message_size_limit(setting: i32) -> usize { + match setting.try_into() { + Ok(0) | Err(_) => usize::MAX, + Ok(limit) => limit, + } +} + /// Configuration for GrpcHandle to compile proto files #[derive(Clone)] pub struct GrpcConfig { @@ -356,6 +390,7 @@ impl GrpcHandle { metadata: &BTreeMap, validate_certificates: bool, client_cert: Option, + request_message_size: i32, ) -> Result { let server_reflection = proto_files.is_empty(); let key = make_pool_key(id, uri, proto_files); @@ -367,7 +402,14 @@ impl GrpcHandle { let pool = if server_reflection { let full_uri = uri_from_str(uri)?; - fill_pool_from_reflection(&full_uri, metadata, validate_certificates, client_cert).await + fill_pool_from_reflection( + &full_uri, + metadata, + validate_certificates, + client_cert, + message_size_limit(request_message_size), + ) + .await } else { fill_pool_from_files(&self.config, proto_files).await }?; @@ -384,12 +426,21 @@ impl GrpcHandle { metadata: &BTreeMap, validate_certificates: bool, client_cert: Option, + request_message_size: i32, ) -> Result> { // Ensure we have a pool; reflect only if missing if self.get_pool(id, uri, proto_files).is_none() { info!("Reflecting gRPC services for {} at {}", id, uri); - self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert) - .await?; + self.reflect( + id, + uri, + proto_files, + metadata, + validate_certificates, + client_cert, + request_message_size, + ) + .await?; } let pool = self @@ -429,8 +480,10 @@ impl GrpcHandle { metadata: &BTreeMap, validate_certificates: bool, client_cert: Option, + request_message_size: i32, ) -> Result { let use_reflection = proto_files.is_empty(); + let max_message_size = message_size_limit(request_message_size); if self.get_pool(id, uri, proto_files).is_none() { self.reflect( id, @@ -439,6 +492,7 @@ impl GrpcHandle { metadata, validate_certificates, client_cert.clone(), + request_message_size, ) .await?; } @@ -448,7 +502,13 @@ impl GrpcHandle { .clone(); let uri = uri_from_str(uri)?; let conn = get_transport(validate_certificates, client_cert.clone())?; - Ok(GrpcConnection { pool: Arc::new(RwLock::new(pool)), use_reflection, conn, uri }) + Ok(GrpcConnection { + pool: Arc::new(RwLock::new(pool)), + use_reflection, + conn, + uri, + max_message_size, + }) } fn get_pool(&self, id: &str, uri: &str, proto_files: &Vec) -> Option<&DescriptorPool> { diff --git a/crates/yaak-grpc/src/reflection.rs b/crates/yaak-grpc/src/reflection.rs index 7ea95266..e154d4b8 100644 --- a/crates/yaak-grpc/src/reflection.rs +++ b/crates/yaak-grpc/src/reflection.rs @@ -119,9 +119,11 @@ pub async fn fill_pool_from_reflection( metadata: &BTreeMap, validate_certificates: bool, client_cert: Option, + max_message_size: usize, ) -> Result { let mut pool = DescriptorPool::new(); - let mut client = AutoReflectionClient::new(uri, validate_certificates, client_cert)?; + let mut client = + AutoReflectionClient::new(uri, validate_certificates, client_cert, max_message_size)?; for service in list_services(&mut client, metadata).await? { if service == "grpc.reflection.v1alpha.ServerReflection" { @@ -192,6 +194,7 @@ pub(crate) async fn reflect_types_for_message( json: &str, metadata: &BTreeMap, client_cert: Option, + max_message_size: usize, ) -> Result<()> { // 1. Collect all Any types in the JSON let mut extra_types = Vec::new(); @@ -201,7 +204,7 @@ pub(crate) async fn reflect_types_for_message( return Ok(()); // nothing to do } - let mut client = AutoReflectionClient::new(uri, false, client_cert)?; + let mut client = AutoReflectionClient::new(uri, false, client_cert, max_message_size)?; for extra_type in extra_types { { let guard = pool.read().await; @@ -239,6 +242,7 @@ pub(crate) async fn reflect_types_for_dynamic_message( message: &DynamicMessage, metadata: &BTreeMap, client_cert: Option, + max_message_size: usize, ) -> Result<()> { let mut extra_types = HashSet::new(); collect_any_types_from_dynamic_message(message, &mut extra_types); @@ -247,7 +251,7 @@ pub(crate) async fn reflect_types_for_dynamic_message( return Ok(()); } - let mut client = AutoReflectionClient::new(uri, false, client_cert)?; + let mut client = AutoReflectionClient::new(uri, false, client_cert, max_message_size)?; for extra_type in extra_types { { let guard = pool.read().await; diff --git a/crates/yaak-models/bindings/gen_models.ts b/crates/yaak-models/bindings/gen_models.ts index ec7a4e4b..287e9b4e 100644 --- a/crates/yaak-models/bindings/gen_models.ts +++ b/crates/yaak-models/bindings/gen_models.ts @@ -109,6 +109,7 @@ export type Folder = { settingValidateCertificates: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting; settingRequestTimeout: InheritedIntSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type GraphQlIntrospection = { @@ -184,6 +185,7 @@ export type GrpcRequest = { */ url: string; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type HttpRequest = { @@ -456,7 +458,8 @@ export type WebsocketEvent = { messageType: WebsocketEventType; }; -export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text"; +export type WebsocketEventType = + "binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text"; export type WebsocketMessageType = "text" | "binary"; @@ -482,6 +485,7 @@ export type WebsocketRequest = { settingSendCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type Workspace = { @@ -498,6 +502,7 @@ export type Workspace = { settingValidateCertificates: boolean; settingFollowRedirects: boolean; settingRequestTimeout: number; + settingRequestMessageSize: number; settingDnsOverrides: Array; settingSendCookies: boolean; settingStoreCookies: boolean; diff --git a/crates/yaak-models/migrations/20260629000000_request-message-size.sql b/crates/yaak-models/migrations/20260629000000_request-message-size.sql new file mode 100644 index 00000000..a6dd95ab --- /dev/null +++ b/crates/yaak-models/migrations/20260629000000_request-message-size.sql @@ -0,0 +1,7 @@ +ALTER TABLE workspaces ADD COLUMN setting_request_message_size INTEGER DEFAULT 67108864 NOT NULL; + +ALTER TABLE folders ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL; + +ALTER TABLE websocket_requests ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL; + +ALTER TABLE grpc_requests ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL; diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index ac4fa367..4dd59248 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -21,6 +21,8 @@ use ts_rs::TS; use yaak_database::{Result as DbResult, UpdateSource}; pub use yaak_database::{UpsertModelInfo, upsert_date}; +pub const DEFAULT_REQUEST_MESSAGE_SIZE: i32 = 64 * 1024 * 1024; + #[macro_export] macro_rules! impl_model { ($t:ty, $variant:ident) => { @@ -120,6 +122,7 @@ pub struct ResolvedHttpRequestSettings { pub validate_certificates: ResolvedSetting, pub follow_redirects: ResolvedSetting, pub request_timeout: ResolvedSetting, + pub request_message_size: ResolvedSetting, pub send_cookies: ResolvedSetting, pub store_cookies: ResolvedSetting, } @@ -130,6 +133,7 @@ impl Default for ResolvedHttpRequestSettings { validate_certificates: ResolvedSetting::default_source(true), follow_redirects: ResolvedSetting::default_source(true), request_timeout: ResolvedSetting::default_source(0), + request_message_size: ResolvedSetting::default_source(DEFAULT_REQUEST_MESSAGE_SIZE), send_cookies: ResolvedSetting::default_source(true), store_cookies: ResolvedSetting::default_source(true), } @@ -400,6 +404,8 @@ pub struct Workspace { #[serde(default = "default_true")] pub setting_follow_redirects: bool, pub setting_request_timeout: i32, + #[serde(default = "default_request_message_size")] + pub setting_request_message_size: i32, #[serde(default)] pub setting_dns_overrides: Vec, #[serde(default = "default_true")] @@ -445,6 +451,7 @@ impl UpsertModelInfo for Workspace { (EncryptionKeyChallenge, self.encryption_key_challenge.into()), (SettingFollowRedirects, self.setting_follow_redirects.into()), (SettingRequestTimeout, self.setting_request_timeout.into()), + (SettingRequestMessageSize, self.setting_request_message_size.into()), (SettingValidateCertificates, self.setting_validate_certificates.into()), (SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()), (SettingSendCookies, self.setting_send_cookies.into()), @@ -463,7 +470,7 @@ impl UpsertModelInfo for Workspace { WorkspaceIden::EncryptionKeyChallenge, WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingFollowRedirects, - WorkspaceIden::SettingRequestTimeout, + WorkspaceIden::SettingRequestMessageSize, WorkspaceIden::SettingValidateCertificates, WorkspaceIden::SettingDnsOverrides, WorkspaceIden::SettingSendCookies, @@ -491,6 +498,7 @@ impl UpsertModelInfo for Workspace { authentication_type: row.get("authentication_type")?, setting_follow_redirects: row.get("setting_follow_redirects")?, setting_request_timeout: row.get("setting_request_timeout")?, + setting_request_message_size: row.get("setting_request_message_size")?, setting_validate_certificates: row.get("setting_validate_certificates")?, setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(), setting_send_cookies: row.get("setting_send_cookies")?, @@ -962,6 +970,8 @@ pub struct Folder { pub setting_validate_certificates: InheritedBoolSetting, pub setting_follow_redirects: InheritedBoolSetting, pub setting_request_timeout: InheritedIntSetting, + #[serde(default = "default_request_message_size_setting")] + pub setting_request_message_size: InheritedIntSetting, } impl UpsertModelInfo for Folder { @@ -1009,6 +1019,10 @@ impl UpsertModelInfo for Folder { ), (SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()), (SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()), + ( + SettingRequestMessageSize, + serde_json::to_string(&self.setting_request_message_size)?.into(), + ), ]) } @@ -1027,6 +1041,7 @@ impl UpsertModelInfo for Folder { FolderIden::SettingValidateCertificates, FolderIden::SettingFollowRedirects, FolderIden::SettingRequestTimeout, + FolderIden::SettingRequestMessageSize, ] } @@ -1041,6 +1056,7 @@ impl UpsertModelInfo for Folder { let setting_validate_certificates: String = row.get("setting_validate_certificates")?; let setting_follow_redirects: String = row.get("setting_follow_redirects")?; let setting_request_timeout: String = row.get("setting_request_timeout")?; + let setting_request_message_size: String = row.get("setting_request_message_size")?; Ok(Self { id: row.get("id")?, model: row.get("model")?, @@ -1062,6 +1078,8 @@ impl UpsertModelInfo for Folder { .unwrap_or_default(), setting_request_timeout: serde_json::from_str(&setting_request_timeout) .unwrap_or_default(), + setting_request_message_size: serde_json::from_str(&setting_request_message_size) + .unwrap_or_else(|_| default_request_message_size_setting()), }) } } @@ -1398,6 +1416,8 @@ pub struct WebsocketRequest { pub setting_send_cookies: InheritedBoolSetting, pub setting_store_cookies: InheritedBoolSetting, pub setting_validate_certificates: InheritedBoolSetting, + #[serde(default = "default_request_message_size_setting")] + pub setting_request_message_size: InheritedIntSetting, } impl UpsertModelInfo for WebsocketRequest { @@ -1446,6 +1466,10 @@ impl UpsertModelInfo for WebsocketRequest { SettingValidateCertificates, serde_json::to_string(&self.setting_validate_certificates)?.into(), ), + ( + SettingRequestMessageSize, + serde_json::to_string(&self.setting_request_message_size)?.into(), + ), ]) } @@ -1466,6 +1490,7 @@ impl UpsertModelInfo for WebsocketRequest { WebsocketRequestIden::SettingSendCookies, WebsocketRequestIden::SettingStoreCookies, WebsocketRequestIden::SettingValidateCertificates, + WebsocketRequestIden::SettingRequestMessageSize, ] } @@ -1479,6 +1504,7 @@ impl UpsertModelInfo for WebsocketRequest { let setting_send_cookies: String = row.get("setting_send_cookies")?; let setting_store_cookies: String = row.get("setting_store_cookies")?; let setting_validate_certificates: String = row.get("setting_validate_certificates")?; + let setting_request_message_size: String = row.get("setting_request_message_size")?; Ok(Self { id: row.get("id")?, model: row.get("model")?, @@ -1499,6 +1525,8 @@ impl UpsertModelInfo for WebsocketRequest { setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(), setting_validate_certificates: serde_json::from_str(&setting_validate_certificates) .unwrap_or_default(), + setting_request_message_size: serde_json::from_str(&setting_request_message_size) + .unwrap_or_else(|_| default_request_message_size_setting()), }) } } @@ -1509,6 +1537,7 @@ impl UpsertModelInfo for WebsocketRequest { pub enum WebsocketEventType { Binary, Close, + Error, Frame, Open, Ping, @@ -2039,6 +2068,8 @@ pub struct GrpcRequest { /// Server URL (http for plaintext or https for secure) pub url: String, pub setting_validate_certificates: InheritedBoolSetting, + #[serde(default = "default_request_message_size_setting")] + pub setting_request_message_size: InheritedIntSetting, } impl UpsertModelInfo for GrpcRequest { @@ -2086,6 +2117,10 @@ impl UpsertModelInfo for GrpcRequest { SettingValidateCertificates, serde_json::to_string(&self.setting_validate_certificates)?.into(), ), + ( + SettingRequestMessageSize, + serde_json::to_string(&self.setting_request_message_size)?.into(), + ), ]) } @@ -2105,6 +2140,7 @@ impl UpsertModelInfo for GrpcRequest { GrpcRequestIden::Authentication, GrpcRequestIden::Metadata, GrpcRequestIden::SettingValidateCertificates, + GrpcRequestIden::SettingRequestMessageSize, ] } @@ -2115,6 +2151,7 @@ impl UpsertModelInfo for GrpcRequest { let authentication: String = row.get("authentication")?; let metadata: String = row.get("metadata")?; let setting_validate_certificates: String = row.get("setting_validate_certificates")?; + let setting_request_message_size: String = row.get("setting_request_message_size")?; Ok(Self { id: row.get("id")?, model: row.get("model")?, @@ -2134,6 +2171,8 @@ impl UpsertModelInfo for GrpcRequest { metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(), setting_validate_certificates: serde_json::from_str(&setting_validate_certificates) .unwrap_or_default(), + setting_request_message_size: serde_json::from_str(&setting_request_message_size) + .unwrap_or_else(|_| default_request_message_size_setting()), }) } } @@ -2684,6 +2723,14 @@ fn default_true() -> bool { true } +fn default_request_message_size() -> i32 { + DEFAULT_REQUEST_MESSAGE_SIZE +} + +fn default_request_message_size_setting() -> InheritedIntSetting { + InheritedIntSetting { enabled: false, value: DEFAULT_REQUEST_MESSAGE_SIZE } +} + fn default_http_method() -> String { "GET".to_string() } diff --git a/crates/yaak-models/src/queries/folders.rs b/crates/yaak-models/src/queries/folders.rs index c2be0823..290bf0c1 100644 --- a/crates/yaak-models/src/queries/folders.rs +++ b/crates/yaak-models/src/queries/folders.rs @@ -180,6 +180,14 @@ impl<'a> ClientDb<'a> { } else { parent.request_timeout }, + request_message_size: if folder.setting_request_message_size.enabled { + ResolvedSetting::from_model( + folder.setting_request_message_size.value, + AnyModel::Folder(folder.clone()), + ) + } else { + parent.request_message_size + }, send_cookies: if folder.setting_send_cookies.enabled { ResolvedSetting::from_model( folder.setting_send_cookies.value, diff --git a/crates/yaak-models/src/queries/grpc_requests.rs b/crates/yaak-models/src/queries/grpc_requests.rs index 4fb96aa3..a074f07b 100644 --- a/crates/yaak-models/src/queries/grpc_requests.rs +++ b/crates/yaak-models/src/queries/grpc_requests.rs @@ -129,6 +129,14 @@ impl<'a> ClientDb<'a> { } else { parent.validate_certificates }, + request_message_size: if grpc_request.setting_request_message_size.enabled { + ResolvedSetting::from_model( + grpc_request.setting_request_message_size.value, + AnyModel::GrpcRequest(grpc_request.clone()), + ) + } else { + parent.request_message_size + }, ..parent }) } diff --git a/crates/yaak-models/src/queries/http_requests.rs b/crates/yaak-models/src/queries/http_requests.rs index b05d9afe..2c50e25d 100644 --- a/crates/yaak-models/src/queries/http_requests.rs +++ b/crates/yaak-models/src/queries/http_requests.rs @@ -131,6 +131,7 @@ impl<'a> ClientDb<'a> { } else { parent.request_timeout }, + request_message_size: parent.request_message_size, send_cookies: if http_request.setting_send_cookies.enabled { ResolvedSetting::from_model( http_request.setting_send_cookies.value, diff --git a/crates/yaak-models/src/queries/websocket_requests.rs b/crates/yaak-models/src/queries/websocket_requests.rs index 6e9c4114..296ea3c4 100644 --- a/crates/yaak-models/src/queries/websocket_requests.rs +++ b/crates/yaak-models/src/queries/websocket_requests.rs @@ -139,6 +139,14 @@ impl<'a> ClientDb<'a> { } else { parent.validate_certificates }, + request_message_size: if websocket_request.setting_request_message_size.enabled { + ResolvedSetting::from_model( + websocket_request.setting_request_message_size.value, + AnyModel::WebsocketRequest(websocket_request.clone()), + ) + } else { + parent.request_message_size + }, send_cookies: if websocket_request.setting_send_cookies.enabled { ResolvedSetting::from_model( websocket_request.setting_send_cookies.value, diff --git a/crates/yaak-models/src/queries/workspaces.rs b/crates/yaak-models/src/queries/workspaces.rs index fd97cc47..c77d7e38 100644 --- a/crates/yaak-models/src/queries/workspaces.rs +++ b/crates/yaak-models/src/queries/workspaces.rs @@ -21,6 +21,7 @@ impl<'a> ClientDb<'a> { &Workspace { name: "Yaak".to_string(), setting_follow_redirects: true, + setting_request_message_size: crate::models::DEFAULT_REQUEST_MESSAGE_SIZE, setting_validate_certificates: true, ..Default::default() }, @@ -102,6 +103,10 @@ impl<'a> ClientDb<'a> { workspace.setting_request_timeout, AnyModel::Workspace(workspace.clone()), ), + request_message_size: ResolvedSetting::from_model( + workspace.setting_request_message_size, + AnyModel::Workspace(workspace.clone()), + ), send_cookies: ResolvedSetting::from_model( workspace.setting_send_cookies, AnyModel::Workspace(workspace.clone()), diff --git a/crates/yaak-plugins/bindings/gen_models.ts b/crates/yaak-plugins/bindings/gen_models.ts index 38647316..d0ba2f1d 100644 --- a/crates/yaak-plugins/bindings/gen_models.ts +++ b/crates/yaak-plugins/bindings/gen_models.ts @@ -108,6 +108,7 @@ export type Folder = { settingValidateCertificates: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting; settingRequestTimeout: InheritedIntSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type GraphQlIntrospection = { @@ -183,6 +184,7 @@ export type GrpcRequest = { */ url: string; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type HttpRequest = { @@ -426,7 +428,8 @@ export type WebsocketEvent = { messageType: WebsocketEventType; }; -export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text"; +export type WebsocketEventType = + "binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text"; export type WebsocketRequest = { model: "websocket_request"; @@ -450,6 +453,7 @@ export type WebsocketRequest = { settingSendCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type Workspace = { @@ -466,6 +470,7 @@ export type Workspace = { settingValidateCertificates: boolean; settingFollowRedirects: boolean; settingRequestTimeout: number; + settingRequestMessageSize: number; settingDnsOverrides: Array; settingSendCookies: boolean; settingStoreCookies: boolean; diff --git a/crates/yaak-sync/bindings/gen_models.ts b/crates/yaak-sync/bindings/gen_models.ts index 74b67d22..9f270475 100644 --- a/crates/yaak-sync/bindings/gen_models.ts +++ b/crates/yaak-sync/bindings/gen_models.ts @@ -46,6 +46,7 @@ export type Folder = { settingValidateCertificates: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting; settingRequestTimeout: InheritedIntSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type GrpcRequest = { @@ -69,6 +70,7 @@ export type GrpcRequest = { */ url: string; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type HttpRequest = { @@ -159,6 +161,7 @@ export type WebsocketRequest = { settingSendCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type Workspace = { @@ -175,6 +178,7 @@ export type Workspace = { settingValidateCertificates: boolean; settingFollowRedirects: boolean; settingRequestTimeout: number; + settingRequestMessageSize: number; settingDnsOverrides: Array; settingSendCookies: boolean; settingStoreCookies: boolean; diff --git a/crates/yaak-ws/src/connect.rs b/crates/yaak-ws/src/connect.rs index 84a24109..1c282a18 100644 --- a/crates/yaak-ws/src/connect.rs +++ b/crates/yaak-ws/src/connect.rs @@ -20,6 +20,7 @@ pub async fn ws_connect( headers: HeaderMap, validate_certificates: bool, client_cert: Option, + request_message_size: i32, ) -> Result<(WebSocketStream>, Response)> { info!("Connecting to WS {url}"); let tls_config = get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?; @@ -34,7 +35,7 @@ pub async fn ws_connect( let (stream, response) = connect_async_tls_with_config( req, - Some(WebSocketConfig::default()), + Some(websocket_config(request_message_size)), false, Some(Connector::Rustls(Arc::new(tls_config))), ) @@ -48,3 +49,12 @@ pub async fn ws_connect( Ok((stream, response)) } + +fn websocket_config(request_message_size: i32) -> WebSocketConfig { + let max_message_size = message_size_limit(request_message_size); + WebSocketConfig::default().max_message_size(max_message_size).max_frame_size(max_message_size) +} + +pub(crate) fn message_size_limit(setting: i32) -> Option { + setting.try_into().ok().filter(|limit| *limit > 0) +} diff --git a/crates/yaak-ws/src/error.rs b/crates/yaak-ws/src/error.rs index 571c0cef..14036338 100644 --- a/crates/yaak-ws/src/error.rs +++ b/crates/yaak-ws/src/error.rs @@ -4,7 +4,7 @@ use tokio_tungstenite::tungstenite; #[derive(Error, Debug)] pub enum Error { - #[error("WebSocket error: {0}")] + #[error("{0}")] WebSocketErr(#[from] tungstenite::Error), #[error(transparent)] @@ -16,7 +16,7 @@ pub enum Error { #[error(transparent)] TlsError(#[from] yaak_tls::error::Error), - #[error("WebSocket error: {0}")] + #[error("{0}")] GenericError(String), } diff --git a/crates/yaak-ws/src/manager.rs b/crates/yaak-ws/src/manager.rs index 77f4aa75..76f5a201 100644 --- a/crates/yaak-ws/src/manager.rs +++ b/crates/yaak-ws/src/manager.rs @@ -1,4 +1,5 @@ -use crate::connect::ws_connect; +use crate::connect::{message_size_limit, ws_connect}; +use crate::error::Error::GenericError; use crate::error::Result; use futures_util::stream::SplitSink; use futures_util::{SinkExt, StreamExt}; @@ -15,10 +16,16 @@ use tokio_tungstenite::tungstenite::http::HeaderValue; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use yaak_tls::ClientCertificateConfig; +type WebsocketSink = SplitSink>, Message>; + +struct WebsocketConnection { + max_message_size: Option, + sink: WebsocketSink, +} + #[derive(Clone)] pub struct WebsocketManager { - connections: - Arc>, Message>>>>, + connections: Arc>>, read_tasks: Arc>>>, } @@ -35,14 +42,20 @@ impl WebsocketManager { receive_tx: mpsc::Sender, validate_certificates: bool, client_cert: Option, + request_message_size: i32, ) -> Result { let tx = receive_tx.clone(); + let max_message_size = message_size_limit(request_message_size); let (stream, response) = - ws_connect(url, headers, validate_certificates, client_cert).await?; + ws_connect(url, headers, validate_certificates, client_cert, request_message_size) + .await?; let (write, mut read) = stream.split(); - self.connections.lock().await.insert(id.to_string(), write); + self.connections + .lock() + .await + .insert(id.to_string(), WebsocketConnection { max_message_size, sink: write }); let handle = { let connection_id = id.to_string(); @@ -70,13 +83,20 @@ impl WebsocketManager { } pub async fn send(&mut self, id: &str, msg: Message) -> Result<()> { - debug!("Send websocket message {msg:?}"); let mut connections = self.connections.lock().await; let connection = match connections.get_mut(id) { None => return Ok(()), Some(c) => c, }; - connection.send(msg).await?; + if let Some(limit) = connection.max_message_size { + let message_size = msg.len(); + if message_size > limit { + return Err(GenericError(format!( + "WebSocket message too large: found {message_size} bytes, the limit is {limit} bytes" + ))); + } + } + connection.sink.send(msg).await?; Ok(()) } @@ -84,7 +104,7 @@ impl WebsocketManager { info!("Closing websocket"); if let Some(mut connection) = self.connections.lock().await.remove(id) { // Wait a maximum of 1 second for the connection to close - if let Err(e) = connection.close().await { + if let Err(e) = connection.sink.close().await { warn!("Failed to close websocket connection {e:?}"); }; } diff --git a/packages/plugin-runtime-types/src/bindings/gen_models.ts b/packages/plugin-runtime-types/src/bindings/gen_models.ts index 38647316..d0ba2f1d 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_models.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_models.ts @@ -108,6 +108,7 @@ export type Folder = { settingValidateCertificates: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting; settingRequestTimeout: InheritedIntSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type GraphQlIntrospection = { @@ -183,6 +184,7 @@ export type GrpcRequest = { */ url: string; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type HttpRequest = { @@ -426,7 +428,8 @@ export type WebsocketEvent = { messageType: WebsocketEventType; }; -export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text"; +export type WebsocketEventType = + "binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text"; export type WebsocketRequest = { model: "websocket_request"; @@ -450,6 +453,7 @@ export type WebsocketRequest = { settingSendCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting; + settingRequestMessageSize: InheritedIntSetting; }; export type Workspace = { @@ -466,6 +470,7 @@ export type Workspace = { settingValidateCertificates: boolean; settingFollowRedirects: boolean; settingRequestTimeout: number; + settingRequestMessageSize: number; settingDnsOverrides: Array; settingSendCookies: boolean; settingStoreCookies: boolean;