Compare commits

..

7 Commits

17 changed files with 126 additions and 44 deletions

View File

@@ -34,18 +34,18 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
let finalUrl = request.url || '';
const urlParams = (request.urlParameters ?? []).filter(onlyEnabled);
if (urlParams.length > 0) {
// Build url
// Build url
const [base, hash] = finalUrl.split('#');
const separator = base!.includes('?') ? '&' : '?';
const queryString = urlParams
.map(p => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join('&');
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
}
xs.push(quote(finalUrl));
xs.push(NEWLINE);
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push('--header', quote(`${h.name}: ${h.value}`));
@@ -53,7 +53,11 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
}
// Add form params
if (Array.isArray(request.body?.form)) {
const type = request.bodyType ?? 'none';
if (
(type === 'multipart/form-data' || type === 'application/x-www-form-urlencoded') &&
Array.isArray(request.body?.form)
) {
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
@@ -65,14 +69,14 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.query === 'string') {
} else if (type === 'graphql' && typeof request.body?.query === 'string') {
const body = {
query: request.body.query || '',
variables: maybeParseJSON(request.body.variables, undefined),
};
xs.push('--data', quote(JSON.stringify(body)));
xs.push(NEWLINE);
} else if (typeof request.body?.text === 'string') {
} else if (type !== 'none' && typeof request.body?.text === 'string') {
xs.push('--data', quote(request.body.text));
xs.push(NEWLINE);
}
@@ -116,4 +120,4 @@ function maybeParseJSON<T>(v: string, fallback: T) {
} catch {
return fallback;
}
}
}

View File

@@ -13,7 +13,7 @@ describe('exporter-curl', () => {
],
}),
).toEqual(
[`curl 'https://yaak.app/?a=aaa&b=bbb'`].join(` \\n `),
[`curl 'https://yaak.app?a=aaa&b=bbb'`].join(` \\n `),
);
});
@@ -218,4 +218,16 @@ describe('exporter-curl', () => {
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
});
});
test('Stale body data', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
bodyType: 'none',
body: {
text: 'ignore me',
}
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
});
});

View File

@@ -111,7 +111,7 @@ pub async fn send_http_request<R: Runtime>(
.referer(false)
.tls_info(true);
let tls_config = yaak_http::tls::get_config(workspace.setting_validate_certificates);
let tls_config = yaak_http::tls::get_config(workspace.setting_validate_certificates, true);
client_builder = client_builder.use_preconfigured_tls(tls_config);
match settings.proxy {

View File

@@ -1389,12 +1389,15 @@ pub fn run() {
} => {
let w = app_handle.get_webview_window(&label).unwrap();
let h = app_handle.clone();
// Run update check whenever the window is focused
tauri::async_runtime::spawn(async move {
let val: State<'_, Mutex<YaakUpdater>> = h.state();
let update_mode = get_update_mode(&w).await.unwrap();
if let Err(e) = val.lock().await.maybe_check(&w, update_mode).await {
warn!("Failed to check for updates {e:?}");
if w.db().get_settings().autoupdate {
let val: State<'_, Mutex<YaakUpdater>> = h.state();
let update_mode = get_update_mode(&w).await.unwrap();
if let Err(e) = val.lock().await.maybe_check(&w, update_mode).await {
warn!("Failed to check for updates {e:?}");
};
};
});

View File

@@ -4,8 +4,11 @@ use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use tonic::body::BoxBody;
// I think ALPN breaks this because we're specifying http2_only
const WITH_ALPN: bool = false;
pub(crate) fn get_transport(validate_certificates: bool) -> Client<HttpsConnector<HttpConnector>, BoxBody> {
let tls_config = yaak_http::tls::get_config(validate_certificates);
let tls_config = yaak_http::tls::get_config(validate_certificates, WITH_ALPN);
let mut http = HttpConnector::new();
http.enforce_http(false);

View File

@@ -5,7 +5,7 @@ use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_platform_verifier::BuilderVerifierExt;
use std::sync::Arc;
pub fn get_config(validate_certificates: bool) -> ClientConfig {
pub fn get_config(validate_certificates: bool, with_alpn: bool) -> ClientConfig {
let arc_crypto_provider = Arc::new(ring::default_provider());
let config_builder = ClientConfig::builder_with_provider(arc_crypto_provider)
.with_safe_default_protocol_versions()
@@ -19,8 +19,11 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
.with_custom_certificate_verifier(Arc::new(NoVerifier))
.with_no_client_auth()
};
// Required for http/2 support
client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
if with_alpn {
client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
}
client
}

View File

@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, autoupdate: boolean, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN autoupdate BOOLEAN DEFAULT true NOT NULL;

View File

@@ -120,6 +120,7 @@ pub struct Settings {
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub autoupdate: bool,
}
impl UpsertModelInfo for Settings {
@@ -168,6 +169,7 @@ impl UpsertModelInfo for Settings {
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
(UpdateChannel, self.update_channel.into()),
(Autoupdate, self.autoupdate.into()),
(ColoredMethods, self.colored_methods.into()),
(Proxy, proxy.into()),
])
@@ -190,6 +192,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::ThemeDark,
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
SettingsIden::Autoupdate,
SettingsIden::ColoredMethods,
]
}
@@ -219,6 +222,7 @@ impl UpsertModelInfo for Settings {
theme_light: row.get("theme_light")?,
hide_window_controls: row.get("hide_window_controls")?,
update_channel: row.get("update_channel")?,
autoupdate: row.get("autoupdate")?,
colored_methods: row.get("colored_methods")?,
})
}

View File

@@ -31,6 +31,7 @@ impl<'a> DbContext<'a> {
theme_dark: "yaak-dark".to_string(),
theme_light: "yaak-light".to_string(),
update_channel: "stable".to_string(),
autoupdate: true,
colored_methods: false,
};
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")

View File

@@ -1,12 +1,12 @@
use crate::error::Result;
use log::info;
use log::{info, warn};
use serde;
use serde::Deserialize;
use std::net::SocketAddr;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_shell::process::CommandEvent;
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use tokio::sync::watch::Receiver;
#[derive(Deserialize, Default)]
@@ -44,10 +44,10 @@ pub async fn start_nodejs_plugin_runtime<R: Runtime>(
while let Some(event) = child_rx.recv().await {
match event {
CommandEvent::Stderr(line) => {
print!("{}", String::from_utf8(line).unwrap());
warn!("{}", String::from_utf8_lossy(&line).trim_end_matches(&['\n', '\r'][..]));
}
CommandEvent::Stdout(line) => {
print!("{}", String::from_utf8(line).unwrap());
info!("{}", String::from_utf8_lossy(&line).trim_end_matches(&['\n', '\r'][..]));
}
_ => {}
}

View File

@@ -7,16 +7,19 @@ use tokio_tungstenite::tungstenite::handshake::client::Response;
use tokio_tungstenite::tungstenite::http::HeaderValue;
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_tungstenite::{
connect_async_tls_with_config, Connector, MaybeTlsStream, WebSocketStream,
Connector, MaybeTlsStream, WebSocketStream, connect_async_tls_with_config,
};
// Enabling ALPN breaks websocket requests
const WITH_ALPN: bool = false;
pub(crate) async fn ws_connect(
url: &str,
headers: HeaderMap<HeaderValue>,
validate_certificates: bool,
) -> crate::error::Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
info!("Connecting to WS {url}");
let tls_config = yaak_http::tls::get_config(validate_certificates);
let tls_config = yaak_http::tls::get_config(validate_certificates, WITH_ALPN);
let mut req = url.into_client_request()?;
let req_headers = req.headers_mut();
@@ -34,4 +37,4 @@ pub(crate) async fn ws_connect(
)
.await?;
Ok((stream, response))
}
}

View File

@@ -43,7 +43,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
return (
<VStack className="flex-col-reverse mb-3" space={3}>
{/* Buttons on top so they get focus first */}
<HStack space={2} justifyContent="start" className="flex-row-reverse">
<HStack space={2} justifyContent="start" className="flex-row-reverse mt-3">
<Button
color="primary"
variant="border"
@@ -135,9 +135,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
<table className="w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th />
<th className="text-text-subtlest">Added File Paths</th>
<th />
<th className="text-text-subtlest" colSpan={3}>Added File Paths</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">

View File

@@ -49,6 +49,21 @@ export function SettingsGeneral() {
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
<Select
name="autoupdate"
value={settings.autoupdate ? 'auto' : 'manual'}
label="Update Behavior"
labelPosition="left"
size="sm"
labelClassName="w-[14rem]"
onChange={(v) => patchModel(settings, { autoupdate: v === 'auto' })}
options={[
{ label: 'Automatic', value: 'auto' },
{ label: 'Manual', value: 'manual' },
]}
/>
<Select
name="switchWorkspaceBehavior"
label="Workspace Window Behavior"

View File

@@ -1,5 +1,5 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
interface Props {
@@ -8,7 +8,23 @@ interface Props {
export function SvgViewer({ response }: Props) {
const rawTextBody = useResponseBodyText(response);
if (rawTextBody.data == null) return null;
const src = `data:image/svg+xml;base64,${btoa(rawTextBody.data)}`;
const [src, setSrc] = useState<string | null>(null);
useEffect(() => {
if (!rawTextBody.data) {
return setSrc(null);
}
const blob = new Blob([rawTextBody.data], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
setSrc(url);
return () => URL.revokeObjectURL(url);
}, [rawTextBody.data]);
if (src == null) {
return null;
}
return <img src={src} alt="Response preview" className="max-w-full max-h-full pb-2" />;
}

View File

@@ -21,9 +21,10 @@ export function WebPageViewer({ response }: Props) {
<div className="h-full pb-3">
<iframe
key={body ? 'has-body' : 'no-body'}
title="Response preview"
title="Yaak response preview"
srcDoc={contentForIframe}
sandbox="allow-scripts allow-same-origin"
sandbox="allow-scripts allow-forms"
referrerPolicy="no-referrer"
className="h-full w-full rounded border border-border-subtle"
/>
</div>

View File

@@ -108,12 +108,11 @@ export function useIntrospectGraphQL(
return;
}
try {
const schema = buildClientSchema(JSON.parse(introspection.data.content).data);
setSchema(schema);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
setError('message' in e ? e.message : String(e));
const parseResult = tryParseIntrospectionToSchema(introspection.data.content);
if ('error' in parseResult) {
setError(parseResult.error);
} else {
setSchema(parseResult.schema);
}
}, [introspection.data?.content]);
@@ -135,7 +134,26 @@ export function useCurrentGraphQLSchema(request: HttpRequest) {
return useMemo(() => {
if (result.data == null) return null;
if (result.data.content == null || result.data.content === '') return null;
const schema = buildClientSchema(JSON.parse(result.data.content).data);
return schema;
const r = tryParseIntrospectionToSchema(result.data.content);
return 'error' in r ? null : r.schema;
}, [result.data]);
}
function tryParseIntrospectionToSchema(
content: string,
): { schema: GraphQLSchema } | { error: string } {
let parsedResponse;
try {
parsedResponse = JSON.parse(content).data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
return { error: String('message' in e ? e.message : e) };
}
try {
return { schema: buildClientSchema(parsedResponse, {}) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
return { error: String('message' in e ? e.message : e) };
}
}