Skip to content

Commit bf793ae

Browse files
authored
feat: support TUI (#26)
1 parent ce1c03d commit bf793ae

File tree

10 files changed

+1002
-111
lines changed

10 files changed

+1002
-111
lines changed

Cargo.lock

Lines changed: 294 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring
4444
tokio-stream = { version = "0.1.14", default-features = false, features = ["sync"] }
4545
tokio-tungstenite = { version = "0.23.1", features = ["rustls", "rustls-tls-webpki-roots"] }
4646
url = "2.5.0"
47+
crossterm = "0.28.1"
48+
ratatui = { version = "0.28.1", features = ["unstable-rendered-line-info"] }
49+
unicode-width = "0.2.0"
50+
log = "0.4.22"
51+
simplelog = "0.12.2"
4752

4853
[dev-dependencies]
4954
async-http-proxy = { version = "1.2.5", features = ["runtime-tokio"] }

README.md

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,23 @@ A lightweight proxy for capturing HTTP(S) and WS(S) traffic.
1111
- Support forward/reverse proxy
1212
- Support filtering
1313
- Support HTTP/HTTPS/WS/WSS protocols
14-
- Integrate web interface
14+
- Integrate terminal user interface (TUI)
15+
- Integrate web user interface (WebUI)
1516
- Integrate certificates installation webapp
1617
- Capture req/res bodies in a non-blocking, streaming manner
1718
- Export in Markdown, cURL, or HAR formats
19+
- Written in Rust, offering a single executable with no external dependencies
20+
21+
## Screenshots
22+
23+
**Terminal User Inferace**
24+
![proxyfor-tui](https://github.com/user-attachments/assets/01db8ada-0b0c-4ddc-bc3f-045cfdb67d64)
25+
26+
**Web User Inferace**
27+
![proxyfor-webui](https://github.com/user-attachments/assets/b13ffe34-69e5-4513-b904-243c391528b2)
28+
29+
**Dump all traffics**
30+
![proxyfor-dump](https://github.com/user-attachments/assets/eca3b38c-c2e9-404e-8990-e34feee7ae3c)
1831

1932
## Installation
2033

@@ -34,7 +47,7 @@ docker run -v ~/.proxyfor:/.proxyfor -p 8080:8080 --rm sigoden/proxyfor --web
3447

3548
Download from [Github Releases](https://github.com/sigoden/proxyfor/releases), unzip and add proxyfor to your $PATH.
3649

37-
## Usage
50+
## Proxy Type
3851

3952
### Forward Proxy
4053

@@ -45,8 +58,6 @@ $ proxyfor
4558
$ curl -x http://127.0.0.1:8080 httpbin.org/ip
4659
```
4760

48-
![forwarding-proxy](https://github.com/sigoden/proxyfor/assets/4012553/c40cc1be-b9e9-4846-9702-ad3610719b08)
49-
5061
### Reverse Proxy
5162

5263
The client directly requests `http://127.0.0.1:8080`.
@@ -58,16 +69,6 @@ $ proxyfor https://httpbin.org
5869
$ curl http://127.0.0.1:8080/ip
5970
```
6071

61-
## Web Interface
62-
63-
Proxyfor provides a web-based user interface that allows you to interactively inspect the HTTP traffic. All traffic is kept in memory, which means that it’s intended for small-ish samples.
64-
65-
```sh
66-
$ proxyfor --web
67-
```
68-
69-
![proxyfor-webui](https://github.com/sigoden/proxyfor/assets/4012553/fef38f0c-ff7d-4b90-a9f0-a8c10c44c38c)
70-
7172
## Command Line
7273

7374
```
@@ -80,46 +81,54 @@ Options:
8081
-l, --listen <ADDR> Listening ip and port address [default: 0.0.0.0:8080]
8182
-f, --filters <REGEX> Only inspect http(s) traffic whose `{method} {uri}` matches the regex
8283
-m, --mime-filters <VALUE> Only inspect http(s) traffic whose content-type matches the value
83-
-w, --web Enable web interface
84+
-W, --web Enable user-friendly web interface
85+
-T, --tui Eenter TUI
86+
-D, --dump Dump all traffics
8487
-h, --help Print help
8588
-V, --version Print version
8689
```
8790

88-
Change the ip and port.
91+
### Choosing User Interface
92+
93+
You can select different interfaces with the following commands:
8994

9095
```sh
91-
proxyfor -l 18080
92-
proxyfor -l 127.0.0.1
93-
proxyfor -l 127.0.0.1:18080
96+
proxyfor # Enter TUI
97+
proxyfor --web # Serve WebUI
98+
proxyfor --dump # Dump all traffics
99+
proxyfor --web --tui # Serve WebUI + Enter TUI
100+
proxyfor --web --dump # Serve WebUI + Dump all traffics
101+
proxyfor > proxyfor.md # Dump all traffics to markdown file
94102
```
95103

96-
Enable web interface with `-w/--web`
104+
### Changing IP and Port
105+
106+
You can specify different listening addresses:
97107

98108
```sh
99-
proxyfor --web
109+
proxyfor -l 18080
110+
proxyfor -l 127.0.0.1
111+
proxyfor -l 127.0.0.1:18080
100112
```
101113

102-
Use `-f/--filters` to filter traffic by matching `{method} {uri}`.
114+
### Filtering Traffic
115+
116+
Filter traffic by setting method and URI:
103117

104118
```sh
105119
proxyfor -f httpbin.org/ip -f httpbin.org/anything
106120
proxyfor -f '/^(get|post) https:\/\/httpbin.org/'
107121
```
108122

109-
Use `-m/--mime-filters` to filter traffic by content-type.
123+
Filter traffic based on content type:
110124

111125
```sh
112126
proxyfor -m application/json -m application/ld+json
113127
proxyfor -m text/
114128
```
115129

116-
Pipe it to a markdown file, then view the captured traffic using your favorite editor/IDE with syntax highlighting, folding, search capabilities.
117-
118-
```sh
119-
proxyfor > proxyfor.md
120-
```
121130

122-
## Certificates
131+
## CA Certificates
123132

124133
Proxyfor can decrypt encrypted traffic on the fly, as long as the client trusts proxyfor’s built-in certificate authority. Usually this means that the proxyfor CA certificate has to be installed on the client device.
125134

assets/index.html

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@
190190

191191
tbody .col-type {
192192
font-size: 8px;
193-
text-align: center;
194193
}
195194

196195
.col-size {
@@ -317,25 +316,27 @@
317316
<div data-tab="ws" class="tab hidden">Websocket</div>
318317
<div data-tab="err" class="tab hidden">Error</div>
319318
<div class="toolbox">
320-
<div class="dropdown export-dropdown">
321-
<button class="dropbtn">Export▾</button>
322-
<div class="dropdown-content">
323-
<a data-kind="markdown">Export all as Markdown</a>
324-
<a data-kind="curl">Export all as cURL</a>
325-
<a data-kind="har">Export all as HAR</a>
326-
</div>
327-
</div>
328319
<div class="dropdown copy-dropdown">
329320
<button class="dropbtn">Copy▾</button>
330321
<div class="dropdown-content">
331322
<a data-kind="markdown">Copy as Markdown</a>
332323
<a data-kind="curl">Copy as cURL</a>
333324
<a data-kind="har">Copy as HAR</a>
325+
<a data-kind="req-body">Copy Request Body</a>
334326
<a data-kind="res-body">Copy Response Body</a>
335327
</div>
336328
</div>
329+
<div class="dropdown export-dropdown">
330+
<button class="dropbtn">Export▾</button>
331+
<div class="dropdown-content">
332+
<a data-kind="markdown">Export all as Markdown</a>
333+
<a data-kind="curl">Export all as cURL</a>
334+
<a data-kind="har">Export all as HAR</a>
335+
</div>
336+
</div>
337337
<div class="certificate">
338-
<a href="/__proxyfor__/certificate/" target="_blank" title="Install CA certificate for HTTPS Proxy">Certificate</a>
338+
<a href="/__proxyfor__/certificate/" target="_blank"
339+
title="Install CA certificate for HTTPS Proxy">Certificate</a>
339340
</div>
340341
</div>
341342
</div>
@@ -392,7 +393,7 @@
392393
*/
393394

394395
/**
395-
* @typedef {"markdown"|"curl"|"har"|"res-body"} CopyKind
396+
* @typedef {"markdown"|"curl"|"har"|"req-body"|"res-body"} CopyKind
396397
*/
397398

398399
/**
@@ -686,19 +687,10 @@
686687
if (!currentTraffic) {
687688
return;
688689
}
689-
if (kind == "res-body") {
690-
if (currentTraffic.res_body) {
691-
let text = "";
692-
if (currentTraffic.res_body.encode == "base64") {
693-
let contentType = getContentType(currentTraffic.res_headers);
694-
text = `data:${contentType};base64,${currentTraffic.res_body.value}`;
695-
} else {
696-
text = currentTraffic.res_body.value;
697-
}
698-
if (text) {
699-
ClipboardJS.copy(text);
700-
}
701-
}
690+
if (kind == "req-body") {
691+
copyBody(currentTraffic.req_body)
692+
} else if (kind == "res-body") {
693+
copyBody(currentTraffic.res_body)
702694
} else {
703695
fetch(getTrafficEndpoint(selectedTrafficId) + `?${kind}`)
704696
.then(res => res.text())
@@ -986,6 +978,23 @@
986978
return value.startsWith('x-') ? value.substring(2) : value;
987979
}
988980

981+
/**
982+
* @param {BodyData} body
983+
*/
984+
function copyBody(body) {
985+
if (!body) return;
986+
let text = "";
987+
if (body.encode == "base64") {
988+
let contentType = getContentType(headers);
989+
text = `data:${contentType};base64,${body.value}`;
990+
} else {
991+
text = body.value;
992+
}
993+
if (text) {
994+
ClipboardJS.copy(text);
995+
}
996+
}
997+
989998
function base64ToBlob(base64String, contentType) {
990999
const byteCharacters = atob(base64String);
9911000
const byteArrays = [];
@@ -1006,34 +1015,36 @@
10061015

10071016
}
10081017

1009-
function formatSize(bytes) {
1010-
if (bytes === null) return "";
1011-
if (bytes === 0) return "0";
1018+
function formatSize(len) {
1019+
if (len === null) return "";
1020+
if (len === 0) return "0";
10121021
const prefix = ["b", "kb", "mb", "gb", "tb"];
10131022
let i = 0;
10141023
for (; i < prefix.length; i++) {
1015-
if (Math.pow(1024, i + 1) > bytes) {
1024+
if (Math.pow(1024, i + 1) > len) {
10161025
break;
10171026
}
10181027
}
10191028
let precision;
1020-
if (bytes % Math.pow(1024, i) === 0) precision = 0;
1029+
if (len % Math.pow(1024, i) === 0) precision = 0;
10211030
else precision = 1;
1022-
return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i];
1031+
return (len / Math.pow(1024, i)).toFixed(precision) + prefix[i];
10231032
}
10241033

1025-
function formatTimeDelta(milliseconds) {
1026-
let time = milliseconds;
1027-
if (time === null) return "";
1028-
if (time === 0) return "0";
1034+
function formatTimeDelta(delta) {
1035+
if (delta === null) return "";
1036+
if (delta === 0) return "0";
1037+
if (delta > 1000 && delta < 10000) {
1038+
return (delta / 1000.0).toFixed(2) + "s";
1039+
}
10291040
const prefix = ["ms", "s", "min", "h"];
10301041
const div = [1000, 60, 60];
10311042
let i = 0;
1032-
while (Math.abs(time) >= div[i] && i < div.length) {
1033-
time = time / div[i];
1043+
while (Math.abs(delta) >= div[i] && i < div.length) {
1044+
delta = delta / div[i];
10341045
i++;
10351046
}
1036-
return Math.round(time) + prefix[i];
1047+
return Math.round(delta) + prefix[i];
10371048
}
10381049

10391050
function formatDate(date) {

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
#[macro_use]
2+
extern crate log;
3+
14
pub mod cert;
25
pub mod filter;
36
pub mod server;
7+
pub mod state;
48
pub mod traffic;
9+
pub mod tui;
510

611
mod rewind;
7-
mod state;

0 commit comments

Comments
 (0)