Skip to content

Commit 5ab316a

Browse files
authored
Add button to copy the request as cURL (#150)
1 parent 6c24430 commit 5ab316a

File tree

2 files changed

+93
-1
lines changed

2 files changed

+93
-1
lines changed

tunnel/internal/client/dashboard/ui/src/lib/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type Tunnel = {
66
export type Request = {
77
ID: string;
88
Subdomain: string;
9+
Host: string;
910
Localport: number;
1011
Url: string;
1112
Method: string;

tunnel/internal/client/dashboard/ui/src/pages/RequestDetails.svelte

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { Button } from "$lib/components/ui/button";
55
import { currentRequest } from "$lib/store";
66
import { convertDateToHumanReadable } from "$lib/utils";
7-
import { ArrowLeft, ArrowUpRight, Clock, Loader, Play, RefreshCw } from "lucide-svelte";
7+
import { ArrowLeft, ArrowUpRight, Clock, Copy, Loader, Play, RefreshCw } from "lucide-svelte";
88
import Highlight from "svelte-highlight";
99
import json from "svelte-highlight/languages/json";
1010
import atomonelight from "svelte-highlight/styles/atom-one-light";
@@ -42,6 +42,88 @@
4242
replaying = false;
4343
}
4444
};
45+
46+
const generateCurlCommand = () => {
47+
if (!$currentRequest) return '';
48+
49+
// Construct full tunnel URL
50+
const tunnelUrl = `https://${$currentRequest.Host}${$currentRequest.Url}`;
51+
let curl = `curl -X ${$currentRequest.Method} '${tunnelUrl}'`;
52+
53+
// Add headers
54+
const contentType = $currentRequest.Headers?.['Content-Type']?.[0] || '';
55+
const isMultipartForm = contentType.startsWith('multipart/form-data');
56+
57+
if ($currentRequest.Headers) {
58+
Object.entries($currentRequest.Headers).forEach(([key, value]) => {
59+
if (Array.isArray(value) && value.length > 0 && key !== 'Content-Type' && key !== 'Content-Length') {
60+
curl += ` \\\n -H '${key}: ${value[0]}'`;
61+
}
62+
});
63+
}
64+
65+
// Add body if present
66+
if ($currentRequest.Body) {
67+
try {
68+
// First decode from base64
69+
const decodedBytes = atob($currentRequest.Body);
70+
71+
if (isMultipartForm) {
72+
// For multipart form data, we'll use -F instead of -d
73+
// Extract boundary from content type
74+
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
75+
if (boundaryMatch) {
76+
const boundary = boundaryMatch[1];
77+
const parts = decodedBytes.split('--' + boundary);
78+
79+
// Process each part
80+
parts.forEach(part => {
81+
if (part.trim() && !part.includes('--\r\n')) {
82+
const contentDispositionMatch = part.match(/Content-Disposition: form-data; name="([^"]+)"(?:; filename="([^"]+)")?/);
83+
if (contentDispositionMatch) {
84+
const name = contentDispositionMatch[1];
85+
const filename = contentDispositionMatch[2];
86+
87+
if (filename) {
88+
// For file uploads, use a placeholder
89+
curl += ` \\\n -F '${name}=@path/to/${filename}'`;
90+
} else {
91+
// For regular form fields, extract the value
92+
const value = part.split('\r\n\r\n')[1]?.trim();
93+
if (value) {
94+
curl += ` \\\n -F '${name}=${value}'`;
95+
}
96+
}
97+
}
98+
}
99+
});
100+
}
101+
} else {
102+
// For non-multipart data, use -d as before
103+
try {
104+
const decodedBody = decodeURIComponent(decodedBytes);
105+
curl += ` \\\n -d '${decodedBody}'`;
106+
} catch {
107+
curl += ` \\\n -d '${decodedBytes}'`;
108+
}
109+
}
110+
} catch (e) {
111+
curl += ` \\\n -d '${$currentRequest.Body}'`;
112+
}
113+
}
114+
115+
return curl;
116+
};
117+
118+
const copyCurlCommand = async () => {
119+
const curl = generateCurlCommand();
120+
try {
121+
await navigator.clipboard.writeText(curl);
122+
toast.success('Curl command copied to clipboard');
123+
} catch (error) {
124+
toast.error('Failed to copy curl command');
125+
}
126+
};
45127
</script>
46128

47129
<svelte:head>
@@ -107,6 +189,15 @@
107189
Replay
108190
{/if}
109191
</Button>
192+
<Button
193+
variant="outline"
194+
size="sm"
195+
on:click={copyCurlCommand}
196+
class="flex items-center gap-2"
197+
>
198+
<Copy class="w-4 h-4" />
199+
Copy as cURL
200+
</Button>
110201
</div>
111202
</div>
112203
</div>

0 commit comments

Comments
 (0)