mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-30 18:11:39 +02:00
Add request message size setting
This commit is contained in:
@@ -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<string, unknown>) => await patchModel(model, { authentication }),
|
||||
async (authentication: Record<string, unknown>) =>
|
||||
await patchModel(model, { authentication }),
|
||||
[model],
|
||||
);
|
||||
|
||||
@@ -47,7 +51,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
||||
return (
|
||||
<EmptyStateText>
|
||||
<p>
|
||||
Auth plugin not found for <InlineCode>{model.authenticationType}</InlineCode>
|
||||
Auth plugin not found for{" "}
|
||||
<InlineCode>{model.authenticationType}</InlineCode>
|
||||
</p>
|
||||
</EmptyStateText>
|
||||
);
|
||||
@@ -56,11 +61,20 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
||||
if (inheritedAuth == null) {
|
||||
if (model.model === "workspace" || model.model === "folder") {
|
||||
return (
|
||||
<EmptyStateText className="flex-col gap-1">
|
||||
<p>
|
||||
Apply auth to all requests in <strong>{resolvedModelName(model)}</strong>
|
||||
</p>
|
||||
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">Documentation</Link>
|
||||
<EmptyStateText className="flex-col gap-3">
|
||||
<div className="not-italic flex flex-col items-center gap-3 text-center">
|
||||
<p className="max-w-md text-sm text-text-subtle">
|
||||
Choose an auth method to apply it to all requests in{" "}
|
||||
<strong className="font-semibold text-text-subtle">
|
||||
{resolvedModelName(model)}
|
||||
</strong>
|
||||
.
|
||||
</p>
|
||||
<AuthenticationTypeDropdown model={model} />
|
||||
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">
|
||||
Documentation
|
||||
</Link>
|
||||
</div>
|
||||
</EmptyStateText>
|
||||
);
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -169,6 +187,33 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticationTypeDropdown({ model }: Props) {
|
||||
const options = useAuthDropdownOptions(model);
|
||||
|
||||
if (options == null) return null;
|
||||
|
||||
return (
|
||||
<RadioDropdown
|
||||
items={options.items}
|
||||
itemsAfter={options.itemsAfter}
|
||||
itemsBefore={options.itemsBefore}
|
||||
value={options.value}
|
||||
onChange={options.onChange}
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="border"
|
||||
size="sm"
|
||||
rightSlot={
|
||||
<Icon icon="chevron_down" size="sm" className="text-text-subtle" />
|
||||
}
|
||||
>
|
||||
Select Auth
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticationDisabledInput({
|
||||
value,
|
||||
onChange,
|
||||
@@ -198,7 +243,11 @@ function AuthenticationDisabledInput({
|
||||
rightSlot={
|
||||
<div className="px-1 flex items-center">
|
||||
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
|
||||
{rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"}
|
||||
{rendered.isPending
|
||||
? "loading"
|
||||
: rendered.data
|
||||
? "enabled"
|
||||
: "disabled"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<SettingsList className="space-y-8">
|
||||
@@ -77,6 +108,22 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{supportsMessageSizeSettings && (
|
||||
<MessageSizeSettingRow
|
||||
settingDefinition={SETTING_REQUEST_MESSAGE_SIZE}
|
||||
setting={model.settingRequestMessageSize}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
SETTING_REQUEST_MESSAGE_SIZE.modelKey,
|
||||
model.settingRequestMessageSize,
|
||||
)}
|
||||
onChange={(settingRequestMessageSize) =>
|
||||
patchMessageSizeSettings(model, {
|
||||
settingRequestMessageSize,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<BooleanSettingRow
|
||||
settingDefinition={SETTING_VALIDATE_CERTIFICATES}
|
||||
setting={model.settingValidateCertificates}
|
||||
@@ -110,7 +157,9 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props)
|
||||
</SettingsSection>
|
||||
)}
|
||||
{supportsCookieSettings && (
|
||||
<SettingsSection title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}>
|
||||
<SettingsSection
|
||||
title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}
|
||||
>
|
||||
<BooleanSettingRow
|
||||
settingDefinition={SETTING_SEND_COOKIES}
|
||||
setting={model.settingSendCookies}
|
||||
@@ -158,46 +207,103 @@ export function countOverriddenSettings(model: ModelWithSettings) {
|
||||
settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
|
||||
}
|
||||
|
||||
return settings.filter((setting) => 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<CookieSettingsPatch>) {
|
||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
||||
if (model.model === "websocket_request")
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
throw new Error("Unsupported cookie settings model");
|
||||
function patchCookieSettings(
|
||||
model: ModelWithCookieSettings,
|
||||
patch: Partial<CookieSettingsPatch>,
|
||||
) {
|
||||
switch (model.model) {
|
||||
case "workspace":
|
||||
return patchModel(model, patch as Partial<Workspace>);
|
||||
case "folder":
|
||||
return patchModel(model, patch as Partial<Folder>);
|
||||
case "http_request":
|
||||
return patchModel(model, patch as Partial<HttpRequest>);
|
||||
case "websocket_request":
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
|
||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||
return patchModel(model, patch as Partial<HttpRequest>);
|
||||
function patchHttpSettings(
|
||||
model: ModelWithHttpSettings,
|
||||
patch: Partial<HttpSettingsPatch>,
|
||||
) {
|
||||
switch (model.model) {
|
||||
case "workspace":
|
||||
return patchModel(model, patch as Partial<Workspace>);
|
||||
case "folder":
|
||||
return patchModel(model, patch as Partial<Folder>);
|
||||
case "http_request":
|
||||
return patchModel(model, patch as Partial<HttpRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial<TlsSettingsPatch>) {
|
||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
||||
if (model.model === "websocket_request")
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||
function patchTlsSettings(
|
||||
model: ModelWithTlsSettings,
|
||||
patch: Partial<TlsSettingsPatch>,
|
||||
) {
|
||||
switch (model.model) {
|
||||
case "workspace":
|
||||
return patchModel(model, patch as Partial<Workspace>);
|
||||
case "folder":
|
||||
return patchModel(model, patch as Partial<Folder>);
|
||||
case "http_request":
|
||||
return patchModel(model, patch as Partial<HttpRequest>);
|
||||
case "websocket_request":
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
case "grpc_request":
|
||||
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings {
|
||||
function patchMessageSizeSettings(
|
||||
model: ModelWithMessageSizeSettings,
|
||||
patch: Partial<MessageSizeSettingsPatch>,
|
||||
) {
|
||||
switch (model.model) {
|
||||
case "workspace":
|
||||
return patchModel(model, patch as Partial<Workspace>);
|
||||
case "folder":
|
||||
return patchModel(model, patch as Partial<Folder>);
|
||||
case "websocket_request":
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
case "grpc_request":
|
||||
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<SettingRowNumber
|
||||
name={settingDefinition.modelKey}
|
||||
<SettingRow
|
||||
title={settingDefinition.title}
|
||||
description={settingDefinition.description}
|
||||
value={value}
|
||||
placeholder={`${settingDefinition.defaultValue}`}
|
||||
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
>
|
||||
<NumberUnitInput
|
||||
name={settingDefinition.modelKey}
|
||||
label={settingDefinition.title}
|
||||
unit="ms"
|
||||
value={`${value}`}
|
||||
placeholder={`${settingDefinition.defaultValue}`}
|
||||
validate={isValidInteger}
|
||||
onChange={(value) => onChange(parseInteger(value))}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -278,21 +397,18 @@ function IntegerSettingRow({
|
||||
overridden={overridden}
|
||||
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||
>
|
||||
<PlainInput
|
||||
hideLabel
|
||||
<NumberUnitInput
|
||||
name={settingDefinition.modelKey}
|
||||
label={settingDefinition.title}
|
||||
size="sm"
|
||||
type="number"
|
||||
unit="ms"
|
||||
value={`${value}`}
|
||||
placeholder={`${settingDefinition.defaultValue}`}
|
||||
defaultValue={`${value}`}
|
||||
containerClassName="!w-48"
|
||||
validate={(value) => 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 (
|
||||
<SettingRow
|
||||
title={settingDefinition.title}
|
||||
description={settingDefinition.description}
|
||||
>
|
||||
<MessageSizeInput
|
||||
name={settingDefinition.modelKey}
|
||||
label={settingDefinition.title}
|
||||
value={displayValue}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) => onChange(parseMegabytes(value))}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingOverrideRow
|
||||
title={settingDefinition.title}
|
||||
description={settingDefinition.description}
|
||||
overridden={overridden}
|
||||
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||
>
|
||||
<MessageSizeInput
|
||||
name={settingDefinition.modelKey}
|
||||
label={settingDefinition.title}
|
||||
value={displayValue}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) =>
|
||||
onChange({
|
||||
...setting,
|
||||
enabled: true,
|
||||
value: parseMegabytes(value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingOverrideRow>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageSizeInput({
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
name: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<NumberUnitInput
|
||||
name={name}
|
||||
label={label}
|
||||
unit="MB"
|
||||
value={value}
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
placeholder={placeholder}
|
||||
validate={isValidMegabytes}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<PlainInput
|
||||
hideLabel
|
||||
name={name}
|
||||
label={label}
|
||||
size="sm"
|
||||
type="number"
|
||||
inputMode={inputMode}
|
||||
step={step}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
containerClassName="!w-48"
|
||||
validate={validate}
|
||||
rightSlot={
|
||||
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle">
|
||||
{unit}
|
||||
</span>
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isInheritedSetting<T>(
|
||||
setting: T | { enabled?: boolean; value: T },
|
||||
): setting is { enabled?: boolean; value: T } {
|
||||
@@ -308,7 +559,7 @@ function isInheritedSetting<T>(
|
||||
|
||||
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<keyof WorkspaceSettings, "settingRequestTimeout">;
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-4">
|
||||
<Heading>General</Heading>
|
||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||
<p className="text-text-subtle">
|
||||
Configure general settings for update behavior and more.
|
||||
</p>
|
||||
</div>
|
||||
<SettingsList className="space-y-8">
|
||||
<CargoFeature feature="updater">
|
||||
@@ -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() {
|
||||
</SettingsSection>
|
||||
</CargoFeature>
|
||||
|
||||
<SettingsSection
|
||||
title={
|
||||
<>
|
||||
Workspace{" "}
|
||||
<span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
|
||||
{workspace.name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ModelSettingRowNumber
|
||||
model={workspace}
|
||||
modelKey={SETTING_REQUEST_TIMEOUT.modelKey}
|
||||
title={SETTING_REQUEST_TIMEOUT.title}
|
||||
description={SETTING_REQUEST_TIMEOUT.description}
|
||||
placeholder={`${SETTING_REQUEST_TIMEOUT.defaultValue}`}
|
||||
required
|
||||
validate={(value) => Number.parseInt(value, 10) >= 0}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey={SETTING_VALIDATE_CERTIFICATES.modelKey}
|
||||
title={SETTING_VALIDATE_CERTIFICATES.title}
|
||||
description={SETTING_VALIDATE_CERTIFICATES.description}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey={SETTING_FOLLOW_REDIRECTS.modelKey}
|
||||
title={SETTING_FOLLOW_REDIRECTS.title}
|
||||
description={SETTING_FOLLOW_REDIRECTS.description}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey={SETTING_SEND_COOKIES.modelKey}
|
||||
title={SETTING_SEND_COOKIES.title}
|
||||
description={SETTING_SEND_COOKIES.description}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey={SETTING_STORE_COOKIES.modelKey}
|
||||
title={SETTING_STORE_COOKIES.title}
|
||||
description={SETTING_STORE_COOKIES.description}
|
||||
/>
|
||||
</SettingsSection>
|
||||
{showWorkspaceSettingsMovedBanner && (
|
||||
<DismissibleBanner
|
||||
id="workspace-settings-moved-2026-06-30"
|
||||
color="info"
|
||||
className="p-4 max-w-xl mx-auto"
|
||||
>
|
||||
<p>
|
||||
Workspace specific settings have moved to{" "}
|
||||
<b>Workspace Settings</b>, accessible from the workspace switcher
|
||||
menu.
|
||||
</p>
|
||||
</DismissibleBanner>
|
||||
)}
|
||||
|
||||
<SettingsSection title="App Info">
|
||||
<SettingRow title="Version" description="Current Yaak version.">
|
||||
|
||||
@@ -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" ? (
|
||||
<span className="text-warning">{message}</span>
|
||||
) : message === "" ? (
|
||||
<em className="italic text-text-subtlest">No content</em>
|
||||
) : (
|
||||
@@ -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 !== ""
|
||||
|
||||
@@ -112,7 +112,9 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||
/>
|
||||
<WorkspaceEncryptionSetting layout="settings" size="xs" />
|
||||
<div className="mt-4">
|
||||
<WorkspaceEncryptionSetting layout="settings" size="xs" />
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<ModelSettingsEditor model={workspace} showSectionTitles />
|
||||
</SettingsList>
|
||||
|
||||
@@ -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<HTMLAttributes<HTMLInputElement>, "onKeyDownCapture"> & {
|
||||
onFocusRaw?: HTMLAttributes<HTMLInputElement>["onFocus"];
|
||||
Pick<InputHTMLAttributes<HTMLInputElement>, "inputMode" | "onKeyDownCapture" | "step"> & {
|
||||
onFocusRaw?: InputHTMLAttributes<HTMLInputElement>["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}
|
||||
/>
|
||||
|
||||
@@ -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<T extends string>(tabValue: T, model: AuthenticatedModel | null) {
|
||||
export function useAuthTab<T extends string>(
|
||||
tabValue: T,
|
||||
model: AuthenticatedModel | null,
|
||||
) {
|
||||
const options = useAuthDropdownOptions(model);
|
||||
|
||||
return useMemo<TabItem[]>(() => {
|
||||
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<RadioDropdownProps, "children"> | null {
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const inheritedAuth = useInheritedAuthentication(model);
|
||||
const ancestors = useModelAncestors(model);
|
||||
const parentModel = ancestors[0] ?? null;
|
||||
|
||||
return useMemo<TabItem[]>(() => {
|
||||
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" ? (
|
||||
<HStack space={1.5}>
|
||||
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
|
||||
?.shortLabel ?? "UNKNOWN"}
|
||||
<IconTooltip
|
||||
icon="zap_off"
|
||||
iconSize="xs"
|
||||
content="Authentication was inherited from an ancestor"
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
"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<void> }
|
||||
)[] = [];
|
||||
|
||||
// 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: (
|
||||
<Icon
|
||||
icon={parentModel.model === "workspace" ? "corner_right_up" : "folder_up"}
|
||||
/>
|
||||
),
|
||||
onSelect: async () => {
|
||||
const confirmed = await showConfirm({
|
||||
id: "promote-auth-confirm",
|
||||
title: "Promote Authentication",
|
||||
confirmText: "Promote",
|
||||
description: (
|
||||
<>
|
||||
Move authentication config to{" "}
|
||||
<InlineCode>{resolvedModelName(parentModel)}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
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" ? (
|
||||
<HStack space={1.5}>
|
||||
{authentication.find(
|
||||
(a) => a.name === inheritedAuth.authenticationType,
|
||||
)?.shortLabel ?? "UNKNOWN"}
|
||||
<IconTooltip
|
||||
icon="zap_off"
|
||||
iconSize="xs"
|
||||
content="Authentication was inherited from an ancestor"
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
"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<void>;
|
||||
}
|
||||
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: (
|
||||
<Icon
|
||||
icon={
|
||||
ancestorWithAuth.model === "workspace" ? "corner_right_down" : "folder_down"
|
||||
parentModel.model === "workspace"
|
||||
? "corner_right_up"
|
||||
: "folder_up"
|
||||
}
|
||||
/>
|
||||
),
|
||||
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 <InlineCode>{resolvedModelName(ancestorWithAuth)}</InlineCode>?
|
||||
This will override the current authentication but will not affect the{" "}
|
||||
{modelTypeLabel(ancestorWithAuth).toLowerCase()}.
|
||||
Move authentication config to{" "}
|
||||
<InlineCode>{resolvedModelName(parentModel)}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
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: (
|
||||
<Icon
|
||||
icon={
|
||||
ancestorWithAuth.model === "workspace"
|
||||
? "corner_right_down"
|
||||
: "folder_down"
|
||||
}
|
||||
/>
|
||||
),
|
||||
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{" "}
|
||||
<InlineCode>
|
||||
{resolvedModelName(ancestorWithAuth)}
|
||||
</InlineCode>
|
||||
? 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]);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ type ModelType = AnyModel["model"];
|
||||
type WorkspaceRequestSettings = Pick<
|
||||
Workspace,
|
||||
| "settingFollowRedirects"
|
||||
| "settingRequestMessageSize"
|
||||
| "settingRequestTimeout"
|
||||
| "settingSendCookies"
|
||||
| "settingStoreCookies"
|
||||
@@ -17,7 +18,9 @@ type ModelTypeWithSetting<K extends RequestSettingKey> = {
|
||||
[M in ModelType]: K extends keyof ModelForType<M> ? M : never;
|
||||
}[ModelType];
|
||||
|
||||
export type RequestSettingDefinition<K extends RequestSettingKey = RequestSettingKey> = {
|
||||
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",
|
||||
|
||||
@@ -295,7 +295,8 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
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::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
@@ -332,6 +333,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
&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<R: Runtime>(
|
||||
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::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
@@ -425,6 +428,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&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<R: Runtime>(
|
||||
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<R: Runtime>(
|
||||
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<R: Runtime>(
|
||||
&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<R: Runtime>(
|
||||
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<R: Runtime>(
|
||||
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<R: Runtime>(
|
||||
&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<R: Runtime>(
|
||||
.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<R: Runtime>(
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,37 @@ pub async fn cmd_ws_send<R: Runtime>(
|
||||
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
||||
) -> Result<WebsocketConnection> {
|
||||
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<R: Runtime>(
|
||||
connection: &WebsocketConnection,
|
||||
environment_id: Option<&str>,
|
||||
app_handle: &AppHandle<R>,
|
||||
window: &WebviewWindow<R>,
|
||||
ws_manager: &Mutex<WebsocketManager>,
|
||||
) -> Result<WebsocketConnection> {
|
||||
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<R: Runtime>(
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?;
|
||||
|
||||
Ok(connection)
|
||||
Ok(connection.clone())
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -299,6 +330,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
receive_tx,
|
||||
resolved_settings.validate_certificates.value,
|
||||
client_cert,
|
||||
resolved_settings.request_message_size.value,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
Generated
+4
@@ -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<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
|
||||
@@ -33,15 +33,21 @@ impl AutoReflectionClient {
|
||||
uri: &Uri,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
max_message_size: usize,
|
||||
) -> Result<Self> {
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RwLock<DescriptorPool>>,
|
||||
conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||
pub uri: Uri,
|
||||
use_reflection: bool,
|
||||
max_message_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -101,8 +98,15 @@ impl GrpcConnection {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<DynamicMessage>> {
|
||||
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<HttpsConnector<HttpConnector>, BoxBody>,
|
||||
uri: Uri,
|
||||
max_message_size: usize,
|
||||
) -> tonic::client::Grpc<Client<HttpsConnector<HttpConnector>, 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<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
request_message_size: i32,
|
||||
) -> Result<bool> {
|
||||
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<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
request_message_size: i32,
|
||||
) -> Result<Vec<ServiceDefinition>> {
|
||||
// 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<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
request_message_size: i32,
|
||||
) -> Result<GrpcConnection> {
|
||||
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<PathBuf>) -> Option<&DescriptorPool> {
|
||||
|
||||
@@ -119,9 +119,11 @@ pub async fn fill_pool_from_reflection(
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
max_message_size: usize,
|
||||
) -> Result<DescriptorPool> {
|
||||
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<String, String>,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
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<String, String>,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
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;
|
||||
|
||||
+6
-1
@@ -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<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
|
||||
@@ -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;
|
||||
@@ -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<bool>,
|
||||
pub follow_redirects: ResolvedSetting<bool>,
|
||||
pub request_timeout: ResolvedSetting<i32>,
|
||||
pub request_message_size: ResolvedSetting<i32>,
|
||||
pub send_cookies: ResolvedSetting<bool>,
|
||||
pub store_cookies: ResolvedSetting<bool>,
|
||||
}
|
||||
@@ -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<DnsOverride>,
|
||||
#[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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
+6
-1
@@ -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<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
|
||||
Generated
+4
@@ -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<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
|
||||
@@ -20,6 +20,7 @@ pub async fn ws_connect(
|
||||
headers: HeaderMap<HeaderValue>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
request_message_size: i32,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, 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<usize> {
|
||||
setting.try_into().ok().filter(|limit| *limit > 0)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
|
||||
|
||||
struct WebsocketConnection {
|
||||
max_message_size: Option<usize>,
|
||||
sink: WebsocketSink,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebsocketManager {
|
||||
connections:
|
||||
Arc<Mutex<HashMap<String, SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>>,
|
||||
connections: Arc<Mutex<HashMap<String, WebsocketConnection>>>,
|
||||
read_tasks: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
@@ -35,14 +42,20 @@ impl WebsocketManager {
|
||||
receive_tx: mpsc::Sender<Message>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
request_message_size: i32,
|
||||
) -> Result<Response> {
|
||||
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:?}");
|
||||
};
|
||||
}
|
||||
|
||||
+6
-1
@@ -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<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
|
||||
Reference in New Issue
Block a user