Compare commits

...

1 Commits

Author SHA1 Message Date
Gregory Schier a9be57e6d9 Add request message size setting 2026-06-29 17:12:36 -07:00
22 changed files with 528 additions and 71 deletions
@@ -13,6 +13,7 @@ import {
modelSupportsSetting, modelSupportsSetting,
type RequestSettingDefinition, type RequestSettingDefinition,
SETTING_FOLLOW_REDIRECTS, SETTING_FOLLOW_REDIRECTS,
SETTING_REQUEST_MESSAGE_SIZE,
SETTING_REQUEST_TIMEOUT, SETTING_REQUEST_TIMEOUT,
SETTING_SEND_COOKIES, SETTING_SEND_COOKIES,
SETTING_STORE_COOKIES, SETTING_STORE_COOKIES,
@@ -22,21 +23,45 @@ import { Checkbox } from "./core/Checkbox";
import { PlainInput } from "./core/PlainInput"; import { PlainInput } from "./core/PlainInput";
import { import {
SettingOverrideRow, SettingOverrideRow,
SettingRow,
SettingRowBoolean, SettingRowBoolean,
SettingRowNumber, SettingRowNumber,
SettingsList, SettingsList,
SettingsSection, SettingsSection,
} from "./core/SettingRow"; } 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 { interface Props {
showSectionTitles?: boolean; showSectionTitles?: boolean;
model: ModelWithSettings; model: ModelWithSettings;
} }
type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; type ModelWithSettings =
| Workspace
| Folder
| HttpRequest
| WebsocketRequest
| GrpcRequest;
type ModelWithHttpSettings = Workspace | Folder | HttpRequest; type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
type ModelWithTlsSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; type ModelWithTlsSettings =
type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest; | Workspace
| Folder
| HttpRequest
| WebsocketRequest
| GrpcRequest;
type ModelWithCookieSettings =
| Workspace
| Folder
| HttpRequest
| WebsocketRequest;
type ModelWithMessageSizeSettings =
| Workspace
| Folder
| WebsocketRequest
| GrpcRequest;
type BooleanSetting = boolean | InheritedBoolSetting; type BooleanSetting = boolean | InheritedBoolSetting;
type IntegerSetting = number | InheritedIntSetting; type IntegerSetting = number | InheritedIntSetting;
type CookieSettingsPatch = { type CookieSettingsPatch = {
@@ -50,12 +75,19 @@ type HttpSettingsPatch = {
type TlsSettingsPatch = { type TlsSettingsPatch = {
settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"]; 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 ancestors = useModelAncestors(model);
const supportsHttpSettings = modelSupportsHttpSettings(model); const supportsHttpSettings = modelSupportsHttpSettings(model);
const supportsCookieSettings = modelSupportsCookieSettings(model); const supportsCookieSettings = modelSupportsCookieSettings(model);
const supportsTlsSettings = modelSupportsTlsSettings(model); const supportsTlsSettings = modelSupportsTlsSettings(model);
const supportsMessageSizeSettings = modelSupportsMessageSizeSettings(model);
return ( return (
<SettingsList className="space-y-8"> <SettingsList className="space-y-8">
@@ -77,6 +109,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 <BooleanSettingRow
settingDefinition={SETTING_VALIDATE_CERTIFICATES} settingDefinition={SETTING_VALIDATE_CERTIFICATES}
setting={model.settingValidateCertificates} setting={model.settingValidateCertificates}
@@ -110,7 +158,9 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props)
</SettingsSection> </SettingsSection>
)} )}
{supportsCookieSettings && ( {supportsCookieSettings && (
<SettingsSection title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}> <SettingsSection
title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}
>
<BooleanSettingRow <BooleanSettingRow
settingDefinition={SETTING_SEND_COOKIES} settingDefinition={SETTING_SEND_COOKIES}
setting={model.settingSendCookies} setting={model.settingSendCookies}
@@ -158,46 +208,93 @@ export function countOverriddenSettings(model: ModelWithSettings) {
settings.push(model.settingFollowRedirects, model.settingRequestTimeout); settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
} }
return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true) if (modelSupportsMessageSizeSettings(model)) {
.length; settings.push(model.settingRequestMessageSize);
}
return settings.filter(
(setting) => isInheritedSetting(setting) && setting.enabled === true,
).length;
} }
function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) { function patchCookieSettings(
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>); model: ModelWithCookieSettings,
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>); patch: Partial<CookieSettingsPatch>,
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>); ) {
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") if (model.model === "websocket_request")
return patchModel(model, patch as Partial<WebsocketRequest>); return patchModel(model, patch as Partial<WebsocketRequest>);
throw new Error("Unsupported cookie settings model"); throw new Error("Unsupported cookie settings model");
} }
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) { function patchHttpSettings(
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>); model: ModelWithHttpSettings,
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>); 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>); return patchModel(model, patch as Partial<HttpRequest>);
} }
function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial<TlsSettingsPatch>) { function patchTlsSettings(
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>); model: ModelWithTlsSettings,
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>); patch: Partial<TlsSettingsPatch>,
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>); ) {
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") if (model.model === "websocket_request")
return patchModel(model, patch as Partial<WebsocketRequest>); return patchModel(model, patch as Partial<WebsocketRequest>);
return patchModel(model, patch as Partial<GrpcRequest>); return patchModel(model, patch as Partial<GrpcRequest>);
} }
function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings { function patchMessageSizeSettings(
model: ModelWithMessageSizeSettings,
patch: Partial<MessageSizeSettingsPatch>,
) {
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 === "websocket_request")
return patchModel(model, patch as Partial<WebsocketRequest>);
return patchModel(model, patch as Partial<GrpcRequest>);
}
function modelSupportsHttpSettings(
model: ModelWithSettings,
): model is ModelWithHttpSettings {
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT); 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); 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); return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
} }
function modelSupportsMessageSizeSettings(
model: ModelWithSettings,
): model is ModelWithMessageSizeSettings {
return modelSupportsSetting(model, SETTING_REQUEST_MESSAGE_SIZE);
}
function BooleanSettingRow({ function BooleanSettingRow({
inheritedValue, inheritedValue,
setting, setting,
@@ -211,7 +308,11 @@ function BooleanSettingRow({
}) { }) {
const inherited = isInheritedSetting(setting); const inherited = isInheritedSetting(setting);
const overridden = inherited ? setting.enabled === true : false; 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) { if (!inherited) {
return ( return (
@@ -250,12 +351,18 @@ function IntegerSettingRow({
}: { }: {
inheritedValue: number; inheritedValue: number;
setting: IntegerSetting; setting: IntegerSetting;
settingDefinition: RequestSettingDefinition<"settingRequestTimeout">; settingDefinition: RequestSettingDefinition<
"settingRequestTimeout" | "settingRequestMessageSize"
>;
onChange: (setting: IntegerSetting) => void; onChange: (setting: IntegerSetting) => void;
}) { }) {
const inherited = isInheritedSetting(setting); const inherited = isInheritedSetting(setting);
const overridden = inherited ? setting.enabled === true : false; 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) { if (!inherited) {
return ( return (
@@ -300,6 +407,99 @@ 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 (
<PlainInput
hideLabel
name={name}
label={label}
size="sm"
type="number"
step={0.1}
placeholder={placeholder}
defaultValue={value}
containerClassName="!w-48"
validate={isValidMegabytes}
rightSlot={<span className="px-2 text-xs text-text-subtle">MB</span>}
onChange={onChange}
/>
);
}
function isInheritedSetting<T>( function isInheritedSetting<T>(
setting: T | { enabled?: boolean; value: T }, setting: T | { enabled?: boolean; value: T },
): setting is { enabled?: boolean; value: T } { ): setting is { enabled?: boolean; value: T } {
@@ -308,7 +508,7 @@ function isInheritedSetting<T>(
function resolveInheritedValue( function resolveInheritedValue(
ancestors: (Folder | Workspace)[], ancestors: (Folder | Workspace)[],
key: "settingRequestTimeout", key: "settingRequestTimeout" | "settingRequestMessageSize",
fallback: IntegerSetting, fallback: IntegerSetting,
): number; ): number;
function resolveInheritedValue( function resolveInheritedValue(
@@ -338,10 +538,36 @@ function resolveInheritedValue(
type WorkspaceSettings = Pick< type WorkspaceSettings = Pick<
Workspace, Workspace,
| "settingFollowRedirects" | "settingFollowRedirects"
| "settingRequestMessageSize"
| "settingRequestTimeout" | "settingRequestTimeout"
| "settingSendCookies" | "settingSendCookies"
| "settingStoreCookies" | "settingStoreCookies"
| "settingValidateCertificates" | "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 isValidMegabytes(value: string) {
if (value === "") return true;
const megabytes = Number(value);
return (
Number.isFinite(megabytes) &&
megabytes >= 0 &&
megabytes <= MAX_MESSAGE_SIZE_MB
);
}
@@ -64,6 +64,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
required, required,
rightSlot, rightSlot,
size = "md", size = "md",
step,
tint, tint,
type = "text", type = "text",
validate, validate,
@@ -210,6 +211,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
required={required} required={required}
step={step}
placeholder={placeholder} placeholder={placeholder}
onKeyDownCapture={onKeyDownCapture} onKeyDownCapture={onKeyDownCapture}
/> />
+24 -4
View File
@@ -5,6 +5,7 @@ type ModelType = AnyModel["model"];
type WorkspaceRequestSettings = Pick< type WorkspaceRequestSettings = Pick<
Workspace, Workspace,
| "settingFollowRedirects" | "settingFollowRedirects"
| "settingRequestMessageSize"
| "settingRequestTimeout" | "settingRequestTimeout"
| "settingSendCookies" | "settingSendCookies"
| "settingStoreCookies" | "settingStoreCookies"
@@ -17,7 +18,9 @@ type ModelTypeWithSetting<K extends RequestSettingKey> = {
[M in ModelType]: K extends keyof ModelForType<M> ? M : never; [M in ModelType]: K extends keyof ModelForType<M> ? M : never;
}[ModelType]; }[ModelType];
export type RequestSettingDefinition<K extends RequestSettingKey = RequestSettingKey> = { export type RequestSettingDefinition<
K extends RequestSettingKey = RequestSettingKey,
> = {
defaultValue: WorkspaceRequestSettings[K]; defaultValue: WorkspaceRequestSettings[K];
description: string; description: string;
modelKey: K; modelKey: K;
@@ -41,11 +44,26 @@ export const SETTING_REQUEST_TIMEOUT = defineRequestSetting({
title: "Request Timeout", 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({ export const SETTING_VALIDATE_CERTIFICATES = defineRequestSetting({
defaultValue: true, defaultValue: true,
description: "When disabled, skip validation of server certificates.", description: "When disabled, skip validation of server certificates.",
modelKey: "settingValidateCertificates", modelKey: "settingValidateCertificates",
models: ["workspace", "folder", "http_request", "websocket_request", "grpc_request"], models: [
"workspace",
"folder",
"http_request",
"websocket_request",
"grpc_request",
],
title: "Validate TLS certificates", title: "Validate TLS certificates",
}); });
@@ -59,7 +77,8 @@ export const SETTING_FOLLOW_REDIRECTS = defineRequestSetting({
export const SETTING_SEND_COOKIES = defineRequestSetting({ export const SETTING_SEND_COOKIES = defineRequestSetting({
defaultValue: true, 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", modelKey: "settingSendCookies",
models: ["workspace", "folder", "http_request", "websocket_request"], models: ["workspace", "folder", "http_request", "websocket_request"],
title: "Automatically send cookies", title: "Automatically send cookies",
@@ -67,7 +86,8 @@ export const SETTING_SEND_COOKIES = defineRequestSetting({
export const SETTING_STORE_COOKIES = defineRequestSetting({ export const SETTING_STORE_COOKIES = defineRequestSetting({
defaultValue: true, 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", modelKey: "settingStoreCookies",
models: ["workspace", "folder", "http_request", "websocket_request"], models: ["workspace", "folder", "http_request", "websocket_request"],
title: "Automatically store cookies", title: "Automatically store cookies",
+6 -2
View File
@@ -295,7 +295,8 @@ async fn cmd_grpc_reflect<R: Runtime>(
unrendered_request.folder_id.as_deref(), unrendered_request.folder_id.as_deref(),
environment_id, 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 plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone()); let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
@@ -332,6 +333,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
&metadata, &metadata,
resolved_settings.validate_certificates.value, resolved_settings.validate_certificates.value,
client_certificate, client_certificate,
resolved_settings.request_message_size.value,
) )
.await .await
.map_err(|e| GenericError(e.to_string()))?) .map_err(|e| GenericError(e.to_string()))?)
@@ -353,7 +355,8 @@ async fn cmd_grpc_go<R: Runtime>(
unrendered_request.folder_id.as_deref(), unrendered_request.folder_id.as_deref(),
environment_id, 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 plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone()); let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
@@ -425,6 +428,7 @@ async fn cmd_grpc_go<R: Runtime>(
&metadata, &metadata,
resolved_settings.validate_certificates.value, resolved_settings.validate_certificates.value,
client_cert.clone(), client_cert.clone(),
resolved_settings.request_message_size.value,
) )
.await; .await;
@@ -299,6 +299,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
receive_tx, receive_tx,
resolved_settings.validate_certificates.value, resolved_settings.validate_certificates.value,
client_cert, client_cert,
resolved_settings.request_message_size.value,
) )
.await .await
{ {
+4
View File
@@ -46,6 +46,7 @@ export type Folder = {
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting; settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type GrpcRequest = { export type GrpcRequest = {
@@ -69,6 +70,7 @@ export type GrpcRequest = {
*/ */
url: string; url: string;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type HttpRequest = { export type HttpRequest = {
@@ -146,6 +148,7 @@ export type WebsocketRequest = {
settingSendCookies: InheritedBoolSetting; settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type Workspace = { export type Workspace = {
@@ -162,6 +165,7 @@ export type Workspace = {
settingValidateCertificates: boolean; settingValidateCertificates: boolean;
settingFollowRedirects: boolean; settingFollowRedirects: boolean;
settingRequestTimeout: number; settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>; settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean; settingSendCookies: boolean;
settingStoreCookies: boolean; settingStoreCookies: boolean;
+9 -3
View File
@@ -33,15 +33,21 @@ impl AutoReflectionClient {
uri: &Uri, uri: &Uri,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
max_message_size: usize,
) -> Result<Self> { ) -> Result<Self> {
let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin( let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin(
get_transport(validate_certificates, client_cert.clone())?, get_transport(validate_certificates, client_cert.clone())?,
uri.clone(), uri.clone(),
); )
let client_v1alpha = v1alpha::server_reflection_client::ServerReflectionClient::with_origin( .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())?, get_transport(validate_certificates, client_cert.clone())?,
uri.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 }) Ok(AutoReflectionClient { use_v1alpha: false, client_v1, client_v1alpha })
} }
+80 -20
View File
@@ -33,16 +33,13 @@ use tonic::transport::Uri;
use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming}; use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming};
use yaak_tls::ClientCertificateConfig; 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)] #[derive(Clone)]
pub struct GrpcConnection { pub struct GrpcConnection {
pool: Arc<RwLock<DescriptorPool>>, pool: Arc<RwLock<DescriptorPool>>,
conn: Client<HttpsConnector<HttpConnector>, BoxBody>, conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
pub uri: Uri, pub uri: Uri,
use_reflection: bool, use_reflection: bool,
max_message_size: usize,
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@@ -101,7 +98,14 @@ impl GrpcConnection {
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
) -> Result<Response<DynamicMessage>> { ) -> Result<Response<DynamicMessage>> {
if self.use_reflection { if self.use_reflection {
reflect_types_for_message(self.pool.clone(), &self.uri, message, metadata, client_cert) reflect_types_for_message(
self.pool.clone(),
&self.uri,
message,
metadata,
client_cert,
self.max_message_size,
)
.await?; .await?;
} }
let method = &self.method(&service, &method).await?; let method = &self.method(&service, &method).await?;
@@ -111,8 +115,7 @@ impl GrpcConnection {
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?; let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
deserializer.end()?; deserializer.end()?;
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
let mut req = req_message.into_request(); let mut req = req_message.into_request();
decorate_req(metadata, &mut req)?; decorate_req(metadata, &mut req)?;
@@ -137,6 +140,7 @@ impl GrpcConnection {
message, message,
metadata, metadata,
client_cert, client_cert,
self.max_message_size,
) )
.await?; .await?;
@@ -176,6 +180,7 @@ impl GrpcConnection {
let md = metadata.clone(); let md = metadata.clone();
let use_reflection = self.use_reflection.clone(); let use_reflection = self.use_reflection.clone();
let client_cert = client_cert.clone(); let client_cert = client_cert.clone();
let max_message_size = self.max_message_size;
stream stream
.then(move |json| { .then(move |json| {
let pool = pool.clone(); let pool = pool.clone();
@@ -188,8 +193,15 @@ impl GrpcConnection {
let json_clone = json.clone(); let json_clone = json.clone();
async move { async move {
if use_reflection { if use_reflection {
if let Err(e) = if let Err(e) = reflect_types_for_message(
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await pool,
&uri,
&json,
&md,
client_cert,
max_message_size,
)
.await
{ {
warn!("Failed to resolve Any types: {e}"); warn!("Failed to resolve Any types: {e}");
} }
@@ -211,8 +223,7 @@ impl GrpcConnection {
.filter_map(|x| x) .filter_map(|x| x)
}; };
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
let path = method_desc_to_path(method); let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
@@ -243,6 +254,7 @@ impl GrpcConnection {
let md = metadata.clone(); let md = metadata.clone();
let use_reflection = self.use_reflection.clone(); let use_reflection = self.use_reflection.clone();
let client_cert = client_cert.clone(); let client_cert = client_cert.clone();
let max_message_size = self.max_message_size;
stream stream
.then(move |json| { .then(move |json| {
let pool = pool.clone(); let pool = pool.clone();
@@ -255,8 +267,15 @@ impl GrpcConnection {
let json_clone = json.clone(); let json_clone = json.clone();
async move { async move {
if use_reflection { if use_reflection {
if let Err(e) = if let Err(e) = reflect_types_for_message(
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await pool,
&uri,
&json,
&md,
client_cert,
max_message_size,
)
.await
{ {
warn!("Failed to resolve Any types: {e}"); warn!("Failed to resolve Any types: {e}");
} }
@@ -278,8 +297,7 @@ impl GrpcConnection {
.filter_map(|x| x) .filter_map(|x| x)
}; };
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
let path = method_desc_to_path(method); let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
@@ -307,8 +325,7 @@ impl GrpcConnection {
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?; let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
deserializer.end()?; deserializer.end()?;
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()) let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
let mut req = req_message.into_request(); let mut req = req_message.into_request();
decorate_req(metadata, &mut req)?; 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 /// Configuration for GrpcHandle to compile proto files
#[derive(Clone)] #[derive(Clone)]
pub struct GrpcConfig { pub struct GrpcConfig {
@@ -356,6 +390,7 @@ impl GrpcHandle {
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
request_message_size: i32,
) -> Result<bool> { ) -> Result<bool> {
let server_reflection = proto_files.is_empty(); let server_reflection = proto_files.is_empty();
let key = make_pool_key(id, uri, proto_files); let key = make_pool_key(id, uri, proto_files);
@@ -367,7 +402,14 @@ impl GrpcHandle {
let pool = if server_reflection { let pool = if server_reflection {
let full_uri = uri_from_str(uri)?; 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 { } else {
fill_pool_from_files(&self.config, proto_files).await fill_pool_from_files(&self.config, proto_files).await
}?; }?;
@@ -384,11 +426,20 @@ impl GrpcHandle {
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
request_message_size: i32,
) -> Result<Vec<ServiceDefinition>> { ) -> Result<Vec<ServiceDefinition>> {
// Ensure we have a pool; reflect only if missing // Ensure we have a pool; reflect only if missing
if self.get_pool(id, uri, proto_files).is_none() { if self.get_pool(id, uri, proto_files).is_none() {
info!("Reflecting gRPC services for {} at {}", id, uri); info!("Reflecting gRPC services for {} at {}", id, uri);
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert) self.reflect(
id,
uri,
proto_files,
metadata,
validate_certificates,
client_cert,
request_message_size,
)
.await?; .await?;
} }
@@ -429,8 +480,10 @@ impl GrpcHandle {
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
request_message_size: i32,
) -> Result<GrpcConnection> { ) -> Result<GrpcConnection> {
let use_reflection = proto_files.is_empty(); 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() { if self.get_pool(id, uri, proto_files).is_none() {
self.reflect( self.reflect(
id, id,
@@ -439,6 +492,7 @@ impl GrpcHandle {
metadata, metadata,
validate_certificates, validate_certificates,
client_cert.clone(), client_cert.clone(),
request_message_size,
) )
.await?; .await?;
} }
@@ -448,7 +502,13 @@ impl GrpcHandle {
.clone(); .clone();
let uri = uri_from_str(uri)?; let uri = uri_from_str(uri)?;
let conn = get_transport(validate_certificates, client_cert.clone())?; 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> { fn get_pool(&self, id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> Option<&DescriptorPool> {
+7 -3
View File
@@ -119,9 +119,11 @@ pub async fn fill_pool_from_reflection(
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
max_message_size: usize,
) -> Result<DescriptorPool> { ) -> Result<DescriptorPool> {
let mut pool = DescriptorPool::new(); 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? { for service in list_services(&mut client, metadata).await? {
if service == "grpc.reflection.v1alpha.ServerReflection" { if service == "grpc.reflection.v1alpha.ServerReflection" {
@@ -192,6 +194,7 @@ pub(crate) async fn reflect_types_for_message(
json: &str, json: &str,
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
max_message_size: usize,
) -> Result<()> { ) -> Result<()> {
// 1. Collect all Any types in the JSON // 1. Collect all Any types in the JSON
let mut extra_types = Vec::new(); let mut extra_types = Vec::new();
@@ -201,7 +204,7 @@ pub(crate) async fn reflect_types_for_message(
return Ok(()); // nothing to do 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 { for extra_type in extra_types {
{ {
let guard = pool.read().await; let guard = pool.read().await;
@@ -239,6 +242,7 @@ pub(crate) async fn reflect_types_for_dynamic_message(
message: &DynamicMessage, message: &DynamicMessage,
metadata: &BTreeMap<String, String>, metadata: &BTreeMap<String, String>,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
max_message_size: usize,
) -> Result<()> { ) -> Result<()> {
let mut extra_types = HashSet::new(); let mut extra_types = HashSet::new();
collect_any_types_from_dynamic_message(message, &mut extra_types); 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(()); 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 { for extra_type in extra_types {
{ {
let guard = pool.read().await; let guard = pool.read().await;
+4
View File
@@ -109,6 +109,7 @@ export type Folder = {
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting; settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type GraphQlIntrospection = { export type GraphQlIntrospection = {
@@ -184,6 +185,7 @@ export type GrpcRequest = {
*/ */
url: string; url: string;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type HttpRequest = { export type HttpRequest = {
@@ -482,6 +484,7 @@ export type WebsocketRequest = {
settingSendCookies: InheritedBoolSetting; settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type Workspace = { export type Workspace = {
@@ -498,6 +501,7 @@ export type Workspace = {
settingValidateCertificates: boolean; settingValidateCertificates: boolean;
settingFollowRedirects: boolean; settingFollowRedirects: boolean;
settingRequestTimeout: number; settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>; settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean; settingSendCookies: boolean;
settingStoreCookies: 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;
+47 -1
View File
@@ -21,6 +21,8 @@ use ts_rs::TS;
use yaak_database::{Result as DbResult, UpdateSource}; use yaak_database::{Result as DbResult, UpdateSource};
pub use yaak_database::{UpsertModelInfo, upsert_date}; pub use yaak_database::{UpsertModelInfo, upsert_date};
pub const DEFAULT_REQUEST_MESSAGE_SIZE: i32 = 64 * 1024 * 1024;
#[macro_export] #[macro_export]
macro_rules! impl_model { macro_rules! impl_model {
($t:ty, $variant:ident) => { ($t:ty, $variant:ident) => {
@@ -120,6 +122,7 @@ pub struct ResolvedHttpRequestSettings {
pub validate_certificates: ResolvedSetting<bool>, pub validate_certificates: ResolvedSetting<bool>,
pub follow_redirects: ResolvedSetting<bool>, pub follow_redirects: ResolvedSetting<bool>,
pub request_timeout: ResolvedSetting<i32>, pub request_timeout: ResolvedSetting<i32>,
pub request_message_size: ResolvedSetting<i32>,
pub send_cookies: ResolvedSetting<bool>, pub send_cookies: ResolvedSetting<bool>,
pub store_cookies: ResolvedSetting<bool>, pub store_cookies: ResolvedSetting<bool>,
} }
@@ -130,6 +133,7 @@ impl Default for ResolvedHttpRequestSettings {
validate_certificates: ResolvedSetting::default_source(true), validate_certificates: ResolvedSetting::default_source(true),
follow_redirects: ResolvedSetting::default_source(true), follow_redirects: ResolvedSetting::default_source(true),
request_timeout: ResolvedSetting::default_source(0), request_timeout: ResolvedSetting::default_source(0),
request_message_size: ResolvedSetting::default_source(DEFAULT_REQUEST_MESSAGE_SIZE),
send_cookies: ResolvedSetting::default_source(true), send_cookies: ResolvedSetting::default_source(true),
store_cookies: ResolvedSetting::default_source(true), store_cookies: ResolvedSetting::default_source(true),
} }
@@ -400,6 +404,8 @@ pub struct Workspace {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub setting_follow_redirects: bool, pub setting_follow_redirects: bool,
pub setting_request_timeout: i32, pub setting_request_timeout: i32,
#[serde(default = "default_request_message_size")]
pub setting_request_message_size: i32,
#[serde(default)] #[serde(default)]
pub setting_dns_overrides: Vec<DnsOverride>, pub setting_dns_overrides: Vec<DnsOverride>,
#[serde(default = "default_true")] #[serde(default = "default_true")]
@@ -445,6 +451,7 @@ impl UpsertModelInfo for Workspace {
(EncryptionKeyChallenge, self.encryption_key_challenge.into()), (EncryptionKeyChallenge, self.encryption_key_challenge.into()),
(SettingFollowRedirects, self.setting_follow_redirects.into()), (SettingFollowRedirects, self.setting_follow_redirects.into()),
(SettingRequestTimeout, self.setting_request_timeout.into()), (SettingRequestTimeout, self.setting_request_timeout.into()),
(SettingRequestMessageSize, self.setting_request_message_size.into()),
(SettingValidateCertificates, self.setting_validate_certificates.into()), (SettingValidateCertificates, self.setting_validate_certificates.into()),
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()), (SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
(SettingSendCookies, self.setting_send_cookies.into()), (SettingSendCookies, self.setting_send_cookies.into()),
@@ -463,7 +470,7 @@ impl UpsertModelInfo for Workspace {
WorkspaceIden::EncryptionKeyChallenge, WorkspaceIden::EncryptionKeyChallenge,
WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingRequestTimeout,
WorkspaceIden::SettingFollowRedirects, WorkspaceIden::SettingFollowRedirects,
WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingRequestMessageSize,
WorkspaceIden::SettingValidateCertificates, WorkspaceIden::SettingValidateCertificates,
WorkspaceIden::SettingDnsOverrides, WorkspaceIden::SettingDnsOverrides,
WorkspaceIden::SettingSendCookies, WorkspaceIden::SettingSendCookies,
@@ -491,6 +498,7 @@ impl UpsertModelInfo for Workspace {
authentication_type: row.get("authentication_type")?, authentication_type: row.get("authentication_type")?,
setting_follow_redirects: row.get("setting_follow_redirects")?, setting_follow_redirects: row.get("setting_follow_redirects")?,
setting_request_timeout: row.get("setting_request_timeout")?, 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_validate_certificates: row.get("setting_validate_certificates")?,
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(), setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
setting_send_cookies: row.get("setting_send_cookies")?, setting_send_cookies: row.get("setting_send_cookies")?,
@@ -962,6 +970,8 @@ pub struct Folder {
pub setting_validate_certificates: InheritedBoolSetting, pub setting_validate_certificates: InheritedBoolSetting,
pub setting_follow_redirects: InheritedBoolSetting, pub setting_follow_redirects: InheritedBoolSetting,
pub setting_request_timeout: InheritedIntSetting, pub setting_request_timeout: InheritedIntSetting,
#[serde(default = "default_request_message_size_setting")]
pub setting_request_message_size: InheritedIntSetting,
} }
impl UpsertModelInfo for Folder { impl UpsertModelInfo for Folder {
@@ -1009,6 +1019,10 @@ impl UpsertModelInfo for Folder {
), ),
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()), (SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.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::SettingValidateCertificates,
FolderIden::SettingFollowRedirects, FolderIden::SettingFollowRedirects,
FolderIden::SettingRequestTimeout, FolderIden::SettingRequestTimeout,
FolderIden::SettingRequestMessageSize,
] ]
} }
@@ -1041,6 +1056,7 @@ impl UpsertModelInfo for Folder {
let setting_validate_certificates: String = row.get("setting_validate_certificates")?; let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
let setting_follow_redirects: String = row.get("setting_follow_redirects")?; let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
let setting_request_timeout: String = row.get("setting_request_timeout")?; let setting_request_timeout: String = row.get("setting_request_timeout")?;
let setting_request_message_size: String = row.get("setting_request_message_size")?;
Ok(Self { Ok(Self {
id: row.get("id")?, id: row.get("id")?,
model: row.get("model")?, model: row.get("model")?,
@@ -1062,6 +1078,8 @@ impl UpsertModelInfo for Folder {
.unwrap_or_default(), .unwrap_or_default(),
setting_request_timeout: serde_json::from_str(&setting_request_timeout) setting_request_timeout: serde_json::from_str(&setting_request_timeout)
.unwrap_or_default(), .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_send_cookies: InheritedBoolSetting,
pub setting_store_cookies: InheritedBoolSetting, pub setting_store_cookies: InheritedBoolSetting,
pub setting_validate_certificates: InheritedBoolSetting, pub setting_validate_certificates: InheritedBoolSetting,
#[serde(default = "default_request_message_size_setting")]
pub setting_request_message_size: InheritedIntSetting,
} }
impl UpsertModelInfo for WebsocketRequest { impl UpsertModelInfo for WebsocketRequest {
@@ -1446,6 +1466,10 @@ impl UpsertModelInfo for WebsocketRequest {
SettingValidateCertificates, SettingValidateCertificates,
serde_json::to_string(&self.setting_validate_certificates)?.into(), 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::SettingSendCookies,
WebsocketRequestIden::SettingStoreCookies, WebsocketRequestIden::SettingStoreCookies,
WebsocketRequestIden::SettingValidateCertificates, WebsocketRequestIden::SettingValidateCertificates,
WebsocketRequestIden::SettingRequestMessageSize,
] ]
} }
@@ -1479,6 +1504,7 @@ impl UpsertModelInfo for WebsocketRequest {
let setting_send_cookies: String = row.get("setting_send_cookies")?; let setting_send_cookies: String = row.get("setting_send_cookies")?;
let setting_store_cookies: String = row.get("setting_store_cookies")?; let setting_store_cookies: String = row.get("setting_store_cookies")?;
let setting_validate_certificates: String = row.get("setting_validate_certificates")?; let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
let setting_request_message_size: String = row.get("setting_request_message_size")?;
Ok(Self { Ok(Self {
id: row.get("id")?, id: row.get("id")?,
model: row.get("model")?, 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_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates) setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
.unwrap_or_default(), .unwrap_or_default(),
setting_request_message_size: serde_json::from_str(&setting_request_message_size)
.unwrap_or_else(|_| default_request_message_size_setting()),
}) })
} }
} }
@@ -2039,6 +2067,8 @@ pub struct GrpcRequest {
/// Server URL (http for plaintext or https for secure) /// Server URL (http for plaintext or https for secure)
pub url: String, pub url: String,
pub setting_validate_certificates: InheritedBoolSetting, pub setting_validate_certificates: InheritedBoolSetting,
#[serde(default = "default_request_message_size_setting")]
pub setting_request_message_size: InheritedIntSetting,
} }
impl UpsertModelInfo for GrpcRequest { impl UpsertModelInfo for GrpcRequest {
@@ -2086,6 +2116,10 @@ impl UpsertModelInfo for GrpcRequest {
SettingValidateCertificates, SettingValidateCertificates,
serde_json::to_string(&self.setting_validate_certificates)?.into(), serde_json::to_string(&self.setting_validate_certificates)?.into(),
), ),
(
SettingRequestMessageSize,
serde_json::to_string(&self.setting_request_message_size)?.into(),
),
]) ])
} }
@@ -2105,6 +2139,7 @@ impl UpsertModelInfo for GrpcRequest {
GrpcRequestIden::Authentication, GrpcRequestIden::Authentication,
GrpcRequestIden::Metadata, GrpcRequestIden::Metadata,
GrpcRequestIden::SettingValidateCertificates, GrpcRequestIden::SettingValidateCertificates,
GrpcRequestIden::SettingRequestMessageSize,
] ]
} }
@@ -2115,6 +2150,7 @@ impl UpsertModelInfo for GrpcRequest {
let authentication: String = row.get("authentication")?; let authentication: String = row.get("authentication")?;
let metadata: String = row.get("metadata")?; let metadata: String = row.get("metadata")?;
let setting_validate_certificates: String = row.get("setting_validate_certificates")?; let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
let setting_request_message_size: String = row.get("setting_request_message_size")?;
Ok(Self { Ok(Self {
id: row.get("id")?, id: row.get("id")?,
model: row.get("model")?, model: row.get("model")?,
@@ -2134,6 +2170,8 @@ impl UpsertModelInfo for GrpcRequest {
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(), metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates) setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
.unwrap_or_default(), .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 +2722,14 @@ fn default_true() -> bool {
true 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 { fn default_http_method() -> String {
"GET".to_string() "GET".to_string()
} }
@@ -180,6 +180,14 @@ impl<'a> ClientDb<'a> {
} else { } else {
parent.request_timeout 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 { send_cookies: if folder.setting_send_cookies.enabled {
ResolvedSetting::from_model( ResolvedSetting::from_model(
folder.setting_send_cookies.value, folder.setting_send_cookies.value,
@@ -129,6 +129,14 @@ impl<'a> ClientDb<'a> {
} else { } else {
parent.validate_certificates 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 ..parent
}) })
} }
@@ -131,6 +131,7 @@ impl<'a> ClientDb<'a> {
} else { } else {
parent.request_timeout parent.request_timeout
}, },
request_message_size: parent.request_message_size,
send_cookies: if http_request.setting_send_cookies.enabled { send_cookies: if http_request.setting_send_cookies.enabled {
ResolvedSetting::from_model( ResolvedSetting::from_model(
http_request.setting_send_cookies.value, http_request.setting_send_cookies.value,
@@ -139,6 +139,14 @@ impl<'a> ClientDb<'a> {
} else { } else {
parent.validate_certificates 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 { send_cookies: if websocket_request.setting_send_cookies.enabled {
ResolvedSetting::from_model( ResolvedSetting::from_model(
websocket_request.setting_send_cookies.value, websocket_request.setting_send_cookies.value,
@@ -21,6 +21,7 @@ impl<'a> ClientDb<'a> {
&Workspace { &Workspace {
name: "Yaak".to_string(), name: "Yaak".to_string(),
setting_follow_redirects: true, setting_follow_redirects: true,
setting_request_message_size: crate::models::DEFAULT_REQUEST_MESSAGE_SIZE,
setting_validate_certificates: true, setting_validate_certificates: true,
..Default::default() ..Default::default()
}, },
@@ -102,6 +103,10 @@ impl<'a> ClientDb<'a> {
workspace.setting_request_timeout, workspace.setting_request_timeout,
AnyModel::Workspace(workspace.clone()), AnyModel::Workspace(workspace.clone()),
), ),
request_message_size: ResolvedSetting::from_model(
workspace.setting_request_message_size,
AnyModel::Workspace(workspace.clone()),
),
send_cookies: ResolvedSetting::from_model( send_cookies: ResolvedSetting::from_model(
workspace.setting_send_cookies, workspace.setting_send_cookies,
AnyModel::Workspace(workspace.clone()), AnyModel::Workspace(workspace.clone()),
+4
View File
@@ -108,6 +108,7 @@ export type Folder = {
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting; settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type GraphQlIntrospection = { export type GraphQlIntrospection = {
@@ -183,6 +184,7 @@ export type GrpcRequest = {
*/ */
url: string; url: string;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type HttpRequest = { export type HttpRequest = {
@@ -450,6 +452,7 @@ export type WebsocketRequest = {
settingSendCookies: InheritedBoolSetting; settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type Workspace = { export type Workspace = {
@@ -466,6 +469,7 @@ export type Workspace = {
settingValidateCertificates: boolean; settingValidateCertificates: boolean;
settingFollowRedirects: boolean; settingFollowRedirects: boolean;
settingRequestTimeout: number; settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>; settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean; settingSendCookies: boolean;
settingStoreCookies: boolean; settingStoreCookies: boolean;
+4
View File
@@ -46,6 +46,7 @@ export type Folder = {
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting; settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type GrpcRequest = { export type GrpcRequest = {
@@ -69,6 +70,7 @@ export type GrpcRequest = {
*/ */
url: string; url: string;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type HttpRequest = { export type HttpRequest = {
@@ -159,6 +161,7 @@ export type WebsocketRequest = {
settingSendCookies: InheritedBoolSetting; settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type Workspace = { export type Workspace = {
@@ -175,6 +178,7 @@ export type Workspace = {
settingValidateCertificates: boolean; settingValidateCertificates: boolean;
settingFollowRedirects: boolean; settingFollowRedirects: boolean;
settingRequestTimeout: number; settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>; settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean; settingSendCookies: boolean;
settingStoreCookies: boolean; settingStoreCookies: boolean;
+11 -1
View File
@@ -20,6 +20,7 @@ pub async fn ws_connect(
headers: HeaderMap<HeaderValue>, headers: HeaderMap<HeaderValue>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
request_message_size: i32,
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> { ) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
info!("Connecting to WS {url}"); info!("Connecting to WS {url}");
let tls_config = get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?; 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( let (stream, response) = connect_async_tls_with_config(
req, req,
Some(WebSocketConfig::default()), Some(websocket_config(request_message_size)),
false, false,
Some(Connector::Rustls(Arc::new(tls_config))), Some(Connector::Rustls(Arc::new(tls_config))),
) )
@@ -48,3 +49,12 @@ pub async fn ws_connect(
Ok((stream, response)) 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)
}
+28 -7
View File
@@ -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 crate::error::Result;
use futures_util::stream::SplitSink; use futures_util::stream::SplitSink;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
@@ -15,10 +16,16 @@ use tokio_tungstenite::tungstenite::http::HeaderValue;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use yaak_tls::ClientCertificateConfig; use yaak_tls::ClientCertificateConfig;
type WebsocketSink = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
struct WebsocketConnection {
max_message_size: Option<usize>,
sink: WebsocketSink,
}
#[derive(Clone)] #[derive(Clone)]
pub struct WebsocketManager { pub struct WebsocketManager {
connections: connections: Arc<Mutex<HashMap<String, WebsocketConnection>>>,
Arc<Mutex<HashMap<String, SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>>,
read_tasks: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>, read_tasks: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
} }
@@ -35,14 +42,20 @@ impl WebsocketManager {
receive_tx: mpsc::Sender<Message>, receive_tx: mpsc::Sender<Message>,
validate_certificates: bool, validate_certificates: bool,
client_cert: Option<ClientCertificateConfig>, client_cert: Option<ClientCertificateConfig>,
request_message_size: i32,
) -> Result<Response> { ) -> Result<Response> {
let tx = receive_tx.clone(); let tx = receive_tx.clone();
let max_message_size = message_size_limit(request_message_size);
let (stream, response) = 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(); 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 handle = {
let connection_id = id.to_string(); let connection_id = id.to_string();
@@ -76,7 +89,15 @@ impl WebsocketManager {
None => return Ok(()), None => return Ok(()),
Some(c) => c, 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(()) Ok(())
} }
@@ -84,7 +105,7 @@ impl WebsocketManager {
info!("Closing websocket"); info!("Closing websocket");
if let Some(mut connection) = self.connections.lock().await.remove(id) { if let Some(mut connection) = self.connections.lock().await.remove(id) {
// Wait a maximum of 1 second for the connection to close // 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:?}"); warn!("Failed to close websocket connection {e:?}");
}; };
} }
@@ -108,6 +108,7 @@ export type Folder = {
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingFollowRedirects: InheritedBoolSetting; settingFollowRedirects: InheritedBoolSetting;
settingRequestTimeout: InheritedIntSetting; settingRequestTimeout: InheritedIntSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type GraphQlIntrospection = { export type GraphQlIntrospection = {
@@ -183,6 +184,7 @@ export type GrpcRequest = {
*/ */
url: string; url: string;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type HttpRequest = { export type HttpRequest = {
@@ -450,6 +452,7 @@ export type WebsocketRequest = {
settingSendCookies: InheritedBoolSetting; settingSendCookies: InheritedBoolSetting;
settingStoreCookies: InheritedBoolSetting; settingStoreCookies: InheritedBoolSetting;
settingValidateCertificates: InheritedBoolSetting; settingValidateCertificates: InheritedBoolSetting;
settingRequestMessageSize: InheritedIntSetting;
}; };
export type Workspace = { export type Workspace = {
@@ -466,6 +469,7 @@ export type Workspace = {
settingValidateCertificates: boolean; settingValidateCertificates: boolean;
settingFollowRedirects: boolean; settingFollowRedirects: boolean;
settingRequestTimeout: number; settingRequestTimeout: number;
settingRequestMessageSize: number;
settingDnsOverrides: Array<DnsOverride>; settingDnsOverrides: Array<DnsOverride>;
settingSendCookies: boolean; settingSendCookies: boolean;
settingStoreCookies: boolean; settingStoreCookies: boolean;