Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,6 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

# custom ignore rules
test.json
13 changes: 10 additions & 3 deletions build.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
## Windows

```bash
uv run pyinstaller --icon=icons/netcat-logo.ico --add-data "icons;icons" --onefile --windowed --name "nc-command-builder" main.py
uv run pyinstaller --icon=icons/netcat-logo.ico --add-data "icons;icons" --onefile --windowed --name "nc-command-builder-win" main.py
```

Output: `dist/nc-command-builder.exe`

## macOS

```bash
uv run pyinstaller --icon=icons/netcat-logo.png --add-data "icons:icons" --onefile --windowed --name "nc-command-builder" main.py
uv run pyinstaller \
--windowed \
--onedir \
--name "nc-command-builder-mac" \
--icon icons/netcat-logo.png \
--add-data "icons:icons" \
--osx-bundle-identifier "com.swarfte.nc-command-builder" \
main.py
```

Output: `dist/nc-command-builder.app`
Expand All @@ -38,7 +45,7 @@ Output: `dist/nc-command-builder.app`
## Linux

```bash
uv run pyinstaller --onefile --windowed --name "nc-command-builder" main.py
uv run pyinstaller --onefile --windowed --name "nc-command-builder-linux" main.py
```

Output: `dist/nc-command-builder`
Expand Down
87 changes: 79 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import os
import re
import shlex
import sys
import textwrap
Expand Down Expand Up @@ -42,13 +43,81 @@

# ── Helpers ──────────────────────────────────────────────────────────────────

def payload_to_printf(raw: str, mode: str) -> str:
"""Convert payload to a printf-safe string based on mode."""
# Characters safe to keep literal in URL-encoded output (RFC 3986 unreserved
# plus HTTP/URL structural characters that must stay readable).
_URL_SAFE = frozenset(
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz'
'0123456789'
'-._~/:?=&@!*,()'
)


def _url_encode_uri(uri: str, pct: str) -> str:
"""URL-encode a URI using %%XX (printf) or %XX (echo -e) format."""
return ''.join(
f'{pct}{ord(ch):02X}' if ch not in _URL_SAFE else ch
for ch in uri
Comment on lines +59 to +60
)


def _escape_for_single_quotes(line: str, pct: str) -> str:
"""Escape a line for use inside shell single quotes.

Single-quoted strings can't contain literal '. We encode it as
{pct}27 (the URL-encoded form). Backslashes are doubled so that
printf/echo-e interpret \\ as a literal backslash.
"""
return line.replace('\\', '\\\\').replace("'", f'{pct}27')
Comment on lines +67 to +71


def payload_to_printf(raw: str, mode: str, send_method: str = "printf") -> str:
"""Convert payload to a printf/echo-safe string based on mode.

Plain text → minimal escaping, output inside double quotes.
Escapes → URL-encode style, output inside single quotes.
"""
if mode == "Plain text":
escaped = raw.replace("\\", "\\\\").replace('"', '\\"')
return escaped
# Minimal escaping — payload and preview should look the same.
# Only escape characters that would break the shell double-quote string.
return raw.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`")

elif mode == "Escapes (\\r\\n, \\x41)":
return raw.replace("\n", "\\r\\n")
# URL-encode the URI portion of HTTP request lines; keep
# headers/body literal. Output targets single-quoted
# printf '...' (or echo -e '...').
pct = "%%" if send_method == "printf" else "%"
lines = raw.replace('\r\n', '\n').replace('\r', '\n').split('\n')
result_lines = []
is_http = False

for i, line in enumerate(lines):
if i == 0:
# Detect HTTP request line: METHOD URI HTTP/x.x
# Greedy .* captures the full URI (which may contain spaces).
m = re.match(r'^(\w+)\s+(.*)\s+(HTTP/\S+)$', line)
if m:
is_http = True
method, uri, version = m.groups()
result_lines.append(
f'{method} {_url_encode_uri(uri, pct)} {version}'
)
else:
# Non-HTTP first line — escape for single-quote safety
result_lines.append(_escape_for_single_quotes(line, pct))
else:
# Headers / body — keep literal, only escape ' and \
result_lines.append(_escape_for_single_quotes(line, pct))

result = '\\r\\n'.join(result_lines)
# HTTP requires headers to end with a blank line (\r\n\r\n).
# Auto-append if the user didn't include one.
if is_http:
if not result.endswith('\\r\\n'):
result += '\\r\\n'
if not result.endswith('\\r\\n\\r\\n'):
result += '\\r\\n'
Comment on lines +114 to +119
return result
elif mode == "Hex (41 42 43)":
hex_clean = raw.strip().replace(",", " ").split()
parts = []
Expand Down Expand Up @@ -104,11 +173,13 @@ def build_command(
# ── payload pipeline prefix ──
payload_prefix = ""
if payload.strip() and not is_listen:
printf_str = payload_to_printf(payload, payload_mode)
printf_str = payload_to_printf(payload, payload_mode, send_method)
is_escapes = payload_mode == "Escapes (\\r\\n, \\x41)"
quote = "'" if is_escapes else '"'
if send_method == "printf":
payload_prefix = f'printf "{printf_str}" | '
payload_prefix = f"printf {quote}{printf_str}{quote} | "
else:
payload_prefix = f'echo -e "{printf_str}" | '
payload_prefix = f"echo -e {quote}{printf_str}{quote} | "

# ── nc binary ──
nc_bin = "ncat" if flavor_key == "ncat" else "nc"
Expand Down
Loading