Skip to content

Commit

Permalink
feat: copy to curl, compact context menus
Browse files Browse the repository at this point in the history
  • Loading branch information
boiln committed Jan 25, 2025
1 parent a31c85f commit c437540
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 42 deletions.
21 changes: 21 additions & 0 deletions src/components/session/SessionViewer/ContentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Toggle } from "@/components/ui/toggle";
import { useMessageFormatter } from "@/hooks/session/useMessageFormatter";
import { useToast } from "@/hooks/use-toast";
import { decodeBase64 } from "@/lib/burpParser";
import { toCurl } from "@/lib/toCurl";
import type { ContentPanelProps } from "@/types/session";

import { ContentContextMenu } from "../shared/ContentContextMenu";
Expand Down Expand Up @@ -158,6 +159,26 @@ export function ContentPanel({
});
}
},
curl:
type === "request" && item
? async () => {
try {
const curl = toCurl(item);
await copyTextToClipboard(curl);
toast({
description: "Copied curl command to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy curl command:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
}
: undefined,
};

// Don't attempt to render until mounted
Expand Down
58 changes: 32 additions & 26 deletions src/components/session/SessionViewer/TableContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import {
Copy,
MessageCircle,
Paintbrush2,
Link,
Hash,
FileCode,
Globe,
Clock,
FileJson,
FileText,
} from "lucide-react";
import { Copy, MessageCircle, Paintbrush2, Link, Globe, Clock, Terminal } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import type { BurpItem, HighlightColor } from "@/types/burp";
import { toCurl } from "@/lib/toCurl";

interface TableContextMenuProps {
children: React.ReactNode;
Expand Down Expand Up @@ -85,57 +75,70 @@ export function TableContextMenu({
navigator.clipboard.writeText(fullUrl);
};

const handleCopyCurl = () => {
const curl = toCurl(item);
copyToClipboard(curl, "curl command");
};

return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuContent className="min-w-[180px] p-1">
{/* Copy Options - Only show in single item mode */}
{!isBulkOperation && (
<>
<ContextMenuSub>
<ContextMenuSubTrigger>
<ContextMenuSubTrigger className="h-7 px-2">
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem onClick={handleCopyUrl}>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={handleCopyUrl}>
<Link className="mr-2 h-4 w-4" />
Copy URL
URL
</ContextMenuItem>
<ContextMenuItem
className="h-7 px-2"
onClick={() => copyToClipboard(item.host.ip, "IP")}
>
<Globe className="mr-2 h-4 w-4" />
Copy IP
IP
</ContextMenuItem>
<ContextMenuItem
className="h-7 px-2"
onClick={() => copyToClipboard(item.time, "Time")}
>
<Clock className="mr-2 h-4 w-4" />
Copy Time
Time
</ContextMenuItem>
<ContextMenuSeparator className="my-0.5" />
<ContextMenuItem className="h-7 px-2" onClick={handleCopyCurl}>
<Terminal className="mr-2 h-4 w-4" />
cURL (bash)
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuSeparator className="my-0.5" />
</>
)}

{/* Highlight Options */}
<ContextMenuSub>
<ContextMenuSubTrigger>
<ContextMenuSubTrigger className="h-7 px-2">
<Paintbrush2 className="mr-2 h-4 w-4" />
{isBulkOperation ? "Bulk Highlight" : "Highlight"}
{isBulkOperation ? "Highlight" : "Highlight"}
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem onClick={() => onHighlight(null)}>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={() => onHighlight(null)}>
<div className="mr-2 h-4 w-4 rounded-full border border-border" />
None
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator className="my-0.5" />
{HIGHLIGHT_COLORS.map((color) => (
<ContextMenuItem
key={color.value}
className="h-7 px-2"
onClick={() => onHighlight(color.value)}
>
<div className={`mr-2 h-4 w-4 rounded-full ${color.class}`} />
Expand All @@ -146,7 +149,10 @@ export function TableContextMenu({
</ContextMenuSub>

{/* Comment Option */}
<ContextMenuItem onClick={() => setShowCommentDialog(true)}>
<ContextMenuItem
className="h-7 px-2"
onClick={() => setShowCommentDialog(true)}
>
<MessageCircle className="mr-2 h-4 w-4" />
{isBulkOperation
? "Add Comment"
Expand Down
42 changes: 26 additions & 16 deletions src/components/session/shared/ContentContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState, useEffect } from "react";

import { Copy, FileCode, FileJson, FileText, Link, FileInput, Cookie, Code } from "lucide-react";
import { Copy, FileCode, FileText, Link, FileInput, Cookie, Code, Terminal } from "lucide-react";

import {
ContextMenu,
Expand All @@ -24,6 +24,7 @@ interface ContentContextMenuProps {
headers: () => void;
cookies: () => void;
payload: () => void;
curl?: () => void;
};
}

Expand Down Expand Up @@ -94,56 +95,65 @@ export function ContentContextMenu({ children, onCopy }: ContentContextMenuProps
return (
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuContent className="min-w-[180px] p-1">
{hasSelection && (
<>
<ContextMenuItem onClick={handleCopySelection}>
<ContextMenuItem className="h-7 px-2" onClick={handleCopySelection}>
<Copy className="mr-2 h-4 w-4" />
<span>Copy Selection</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator className="my-0.5" />
<ContextMenuSub>
<ContextMenuSubTrigger className="flex items-center">
<ContextMenuSubTrigger className="h-7 px-2">
<FileCode className="mr-2 h-4 w-4" />
<span>Decode Selection</span>
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem onClick={handleDecode.url}>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={handleDecode.url}>
<Link className="mr-2 h-4 w-4" />
URL Decode
</ContextMenuItem>
<ContextMenuItem onClick={handleDecode.base64}>
<ContextMenuItem className="h-7 px-2" onClick={handleDecode.base64}>
<FileText className="mr-2 h-4 w-4" />
Base64 Decode
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuSeparator className="my-0.5" />
</>
)}
<ContextMenuSub>
<ContextMenuSubTrigger className="flex items-center">
<ContextMenuSubTrigger className="h-7 px-2">
<Copy className="mr-2 h-4 w-4" />
<span>Copy</span>
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem onClick={onCopy.raw}>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={onCopy.raw}>
<Code className="mr-2 h-4 w-4" />
Raw
</ContextMenuItem>
<ContextMenuItem onClick={onCopy.headers}>
<ContextMenuItem className="h-7 px-2" onClick={onCopy.headers}>
<FileInput className="mr-2 h-4 w-4" />
Headers
</ContextMenuItem>
<ContextMenuItem onClick={onCopy.cookies}>
<ContextMenuItem className="h-7 px-2" onClick={onCopy.cookies}>
<Cookie className="mr-2 h-4 w-4" />
Cookies
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={onCopy.payload}>
<ContextMenuSeparator className="my-0.5" />
<ContextMenuItem className="h-7 px-2" onClick={onCopy.payload}>
<FileText className="mr-2 h-4 w-4" />
Payload
</ContextMenuItem>
{onCopy.curl && (
<>
<ContextMenuSeparator className="my-0.5" />
<ContextMenuItem className="h-7 px-2" onClick={onCopy.curl}>
<Terminal className="mr-2 h-4 w-4" />
cURL (bash)
</ContextMenuItem>
</>
)}
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
Expand Down
75 changes: 75 additions & 0 deletions src/lib/toCurl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { BurpItem } from "@/types/burp";

export function toCurl(item: BurpItem): string {
const curl_parts: string[] = [];

// Start with the curl command and method
curl_parts.push(`curl --path-as-is -i -s -k -X $'${item.method}'`);

// Parse the request headers and body from the decoded request
const requestLines = item.request.decodedValue.split("\n");
const headers: Record<string, string> = {};
let bodyStartIndex = -1;
let cookieHeader = "";

// Skip the first line as it's the request line
for (let i = 1; i < requestLines.length; i++) {
const line = requestLines[i].trim();
if (line === "") {
bodyStartIndex = i + 1;
break;
}
const [name, ...valueParts] = line.split(":");
if (name && valueParts.length > 0) {
const headerName = name.trim();
const headerValue = valueParts.join(":").trim();

// Handle cookies separately
if (headerName.toLowerCase() === "cookie") {
cookieHeader = headerValue;
} else {
headers[headerName] = headerValue;
}
}
}

// Add headers to curl command (excluding Cookie header as we'll handle it separately)
const headerParts: string[] = [];
for (const [name, value] of Object.entries(headers)) {
headerParts.push(`-H $'${name}: ${value}'`);
}
if (headerParts.length > 0) {
curl_parts.push(headerParts.join(" "));
}

// Add cookies in a single -b flag if they exist
if (cookieHeader) {
curl_parts.push(`-b $'${cookieHeader}'`);
}

// Handle request body if it exists
if (bodyStartIndex !== -1) {
const body = requestLines.slice(bodyStartIndex).join("\n").trim();
if (body) {
const contentType = headers["content-type"] || headers["Content-Type"];
if (contentType?.includes("application/json")) {
// If content type is JSON, try to parse and stringify to ensure proper formatting
try {
const jsonBody = JSON.parse(body);
curl_parts.push(`--data $'${JSON.stringify(jsonBody)}'`);
} catch {
// If parsing fails, use the raw body
curl_parts.push(`--data $'${body}'`);
}
} else {
curl_parts.push(`--data $'${body}'`);
}
}
}

// Add the URL
curl_parts.push(`$'${item.host.value}${item.url}'`);

// Join with line continuation
return curl_parts.join(" \\\n ");
}

0 comments on commit c437540

Please sign in to comment.