From 3fb0424046981b8a852b61d18c40a8e5abd338e0 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar <45962551+tarunKoyalwar@users.noreply.github.com> Date: Sat, 24 Feb 2024 04:58:51 +0530 Subject: [PATCH] refactor + misc improvements (#499) * refactor + misc improvements * support for http://proxify vhost * feat: yaml multidoc writer * fix lint error * update docs * fix -sr empty file issue * typo fix * show warnings instead of errors * fix missing body issue * implement filter/match in jsonl/yaml * add more request variables * you new yaml library + multiline strings * fix content length + body length issue * fix boolean logic error --- .gitignore | 6 +- README.md | 79 +++++-- go.mod | 28 +-- go.sum | 62 +++-- internal/runner/options.go | 13 +- internal/runner/runner.go | 1 + pkg/logger/elastic/elasticsearch.go | 2 +- pkg/logger/file/file.go | 2 +- pkg/logger/jsonl/jsonl.go | 39 ++++ pkg/logger/kafka/kafka.go | 2 +- pkg/logger/logger.go | 337 ++++++++++++++-------------- pkg/logger/writer.go | 38 ++++ pkg/logger/yaml/yaml.go | 43 ++++ pkg/types/userdata.go | 103 +++++++-- pkg/util/util.go | 22 +- proxy.go | 328 +++++++++++++++------------ 16 files changed, 717 insertions(+), 388 deletions(-) create mode 100644 pkg/logger/jsonl/jsonl.go create mode 100644 pkg/logger/writer.go create mode 100644 pkg/logger/yaml/yaml.go diff --git a/.gitignore b/.gitignore index 669e6c04..3f018aab 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ cmd/mitmrelay/mitmrelay dist/* .vscode -.devcontainer \ No newline at end of file +.devcontainer +**/proxify_logs.jsonl +**/proxify_logs.yaml +/proxify +**/proxify_logs \ No newline at end of file diff --git a/README.md b/README.md index 04956c3b..8a92488b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@
@@ -61,17 +59,20 @@ proxify -h
This will display help for the tool. Here are all the switches it supports.
-```shell
+```console
+Swiss Army Knife Proxy for rapid deployments. Supports multiple operations such as request/response dump,filtering and manipulation via DSL language, upstream HTTP/Socks5 proxy
+
Usage:
./proxify [flags]
Flags:
OUTPUT:
- -o, -output string Output Directory to store HTTP proxy logs (default "logs")
- -dump-req Dump only HTTP requests to output file
- -dump-resp Dump only HTTP responses to output file
- -j, -jsonl write output in JSONL(ines) format
- -oca, -out-ca string Generate and Save CA File to filename
+ -sr, -store-resposne store raw http request / response to output directory (default proxify_logs)
+ -o, -output output file to store proxify logs (default proxify_logs.jsonl)
+ -of, -output-format string output format (jsonl/yaml) (default "jsonl")
+ -dump-req Dump only HTTP requests to output file
+ -dump-resp Dump only HTTP responses to output file
+ -oca, -out-ca string Generate and Save CA File to filename
UPDATE:
-up, -update update proxify to latest version
@@ -85,7 +86,7 @@ FILTER:
NETWORK:
-ha, -http-addr string Listening HTTP IP and Port address (ip:port) (default "127.0.0.1:8888")
- -sa, -socks-addr string Listening SOCKS IP and Port address (ip:port) (default "127.0.0.1:10080")
+ -sa, -socks-addr Listening SOCKS IP and Port address (ip:port) (default 127.0.0.1:10080)
-da, -dns-addr string Listening DNS IP and Port address (ip:port)
-dm, -dns-mapping string Domain to IP DNS mapping (eg domain:ip,domain:ip,..)
-r, -resolver string Custom DNS resolvers to use (ip:port)
@@ -100,19 +101,20 @@ EXPORT:
CONFIGURATION:
-config string path to the proxify configuration file
- -ec, -export-config string proxify export module configuration file ($HOME/.config/proxify/export-config.yaml)
- -config-directory string override the default config path ($HOME/.config/proxify)
+ -ec, -export-config string proxify export module configuration file (default "$CONFIG/export-config.yaml")
+ -config-directory string override the default config path (default "$CONFIG/proxify")
-cert-cache-size int Number of certificates to cache (default 256)
-a, -allow string[] Allowed list of IP/CIDR's to be proxied
-d, -deny string[] Denied list of IP/CIDR's to be proxied
-pt, -passthrough string[] List of passthrough domains
DEBUG:
- -nc, -no-color No Color (default true)
+ -nc, -no-color No Color
-version Version
-silent Silent
-v, -verbose Verbose
-vv, -very-verbose Very Verbose
+
```
### Running Proxify
@@ -152,14 +154,57 @@ proxify -socks5-proxy 127.0.0.1:9050
### Dump all the HTTP/HTTPS traffic
-Dump all the traffic into separate files with request followed by the response:
+Proxify supports three output formats: **JSONL**, **YAML** and **Files**.
-```shell
-proxify -output logs
+**JSONL** (default):
+
+In Json Lines format each Http Request/Response pair is stored as json object in a single line.
+
+```json
+{"timestamp":"2024-02-20T01:56:49+05:30","url":"https://scanme.sh:443","request":{"header":{"Connection":"close","User-Agent":"curl/8.1.2","host":"scanme.sh:443","method":"CONNECT","path":"","scheme":"https"},"raw":"CONNECT scanme.sh:443 HTTP/1.1\r\nHost: scanme.sh:443\r\nConnection: close\r\nUser-Agent: curl/8.1.2\r\n\r\n"},"response":{"header":{"Content-Length":"0"},"raw":"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"}}
+{"timestamp":"2024-02-20T01:56:49+05:30","url":"https://scanme.sh/","request":{"header":{"Accept":"*/*","Connection":"close","User-Agent":"curl/8.1.2","host":"scanme.sh","method":"GET","path":"/","scheme":"https"},"raw":"GET / HTTP/1.1\r\nHost: scanme.sh\r\nAccept: */*\r\nConnection: close\r\nUser-Agent: curl/8.1.2\r\n\r\n"},"response":{"header":{"Content-Type":"text/plain; charset=utf-8","Date":"Mon, 19 Feb 2024 20:26:49 GMT"},"body":"ok","raw":"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nDate: Mon, 19 Feb 2024 20:26:49 GMT\r\n\r\n"}}
```
-As default, proxied requests/responses are stored in the **logs** folder. Additionally, **dump-req** or **dump-resp** flag can be used for saving specific part of the request to the file.
+**Yaml MultiDoc**:
+
+In the YAML MultiDoc format, each HTTP request and response pair is encapsulated as a separate document.All Documents in output yaml file are seperated by `---` to allow stream parsing and consumption.
+
+```console
+proxify -output-format yaml
+```
+
+```yaml
+timestamp: "2024-02-20T01:40:40+05:30"
+url: https://scanme.sh:443
+request:
+ header:
+ Connection: close
+ User-Agent: curl/8.1.2
+ host: scanme.sh:443
+ method: CONNECT
+ path: ""
+ scheme: https
+ body: ""
+ raw: "CONNECT scanme.sh:443 HTTP/1.1\r\nHost: scanme.sh:443\r\nConnection: close\r\nUser-Agent: curl/8.1.2\r\n\r\n"
+response:
+ header:
+ Content-Length: "0"
+ body: ""
+ raw: "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
+---
+timestamp: "2024-02-20T01:40:40+05:30"
+...
+```
+
+**Files**:
+
+In Files format, each HTTP request and response pair is stored in separate files with the request followed by the response. Filenames are in format of `{{Host}}-{{randstr}}.txt`. Additionally, **dump-req** or **dump-resp** flag can be used for saving specific part of the request to the file.
+
+```console
+proxify -store-response
+```
+>Note: When using `-store-response` both jsonl and files are generated.
### Hostname mapping with Local DNS resolver
diff --git a/go.mod b/go.mod
index 35c3e16e..8a57a8c1 100644
--- a/go.mod
+++ b/go.mod
@@ -9,24 +9,26 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/elastic/go-elasticsearch/v7 v7.17.10
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819
+ github.com/goccy/go-yaml v1.11.3
github.com/haxii/fastproxy v0.5.37
github.com/pkg/errors v0.9.1
github.com/projectdiscovery/dsl v0.0.43
github.com/projectdiscovery/fastdialer v0.0.59
github.com/projectdiscovery/goflags v0.1.39
github.com/projectdiscovery/gologger v1.1.12
- github.com/projectdiscovery/martian/v3 v3.0.0-20230412114616-98e3a0a6994a
+ github.com/projectdiscovery/martian/v3 v3.0.0-20240219194442-fed3b744f477
github.com/projectdiscovery/roundrobin v0.0.6
github.com/projectdiscovery/tinydns v0.0.29
- github.com/projectdiscovery/utils v0.0.79
- golang.org/x/net v0.17.0
+ github.com/projectdiscovery/utils v0.0.80-0.20240219143814-1bd72bb71244
+ golang.org/x/net v0.21.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/docker/go-units v0.5.0 // indirect
- github.com/klauspost/pgzip v1.2.5 // indirect
+ github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mholt/archiver/v3 v3.5.1 // indirect
+ golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
)
require (
@@ -36,7 +38,7 @@ require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
- github.com/andybalholm/brotli v1.0.6 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/glamour v0.6.0 // indirect
@@ -52,13 +54,13 @@ require (
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e // indirect
- github.com/fatih/color v1.15.0 // indirect
+ github.com/fatih/color v1.16.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
- github.com/gorilla/css v1.0.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
@@ -71,13 +73,13 @@ require (
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/jwt v0.1.8 // indirect
- github.com/klauspost/compress v1.16.7 // indirect
+ github.com/klauspost/compress v1.17.6 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
- github.com/microcosm-cc/bluemonday v1.0.25 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/miekg/dns v1.1.56 // indirect
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -86,7 +88,7 @@ require (
github.com/muesli/termenv v0.15.1 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
- github.com/pierrec/lz4/v4 v4.1.17 // indirect
+ github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
github.com/projectdiscovery/hmap v0.0.39 // indirect
@@ -121,11 +123,11 @@ require (
github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/oauth2 v0.11.0 // indirect
- golang.org/x/sys v0.16.0 // indirect
+ golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 03d4266e..f7b34d49 100644
--- a/go.sum
+++ b/go.sum
@@ -21,8 +21,8 @@ github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YT
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
-github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@@ -79,8 +79,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -90,8 +90,16 @@ github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXb
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
+github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -133,8 +141,9 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -173,11 +182,12 @@ github.com/kataras/jwt v0.1.8 h1:u71baOsYD22HWeSOg32tCHbczPjdCk7V4MMeJqTtmGk=
github.com/kataras/jwt v0.1.8/go.mod h1:Q5j2IkcIHnfwy+oNY3TVWuEBJNw0ADgCcXK9CaZwV4o=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
-github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
+github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -186,6 +196,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -193,8 +205,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
@@ -202,8 +214,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
-github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
-github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
+github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
+github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
@@ -239,8 +251,8 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
-github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
+github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -261,8 +273,8 @@ github.com/projectdiscovery/hmap v0.0.39 h1:R33JJzPz8TMUm1TJQ/X5zVJbmyTS64EttZ08
github.com/projectdiscovery/hmap v0.0.39/go.mod h1:wEPoEIVGPdHsc9EnE+ERUdgNyp29zZn6gACOALylHOg=
github.com/projectdiscovery/mapcidr v1.1.16 h1:rjj1w5D6hbTsUQXYClLcGdfBEy9bryclgi70t0vBggo=
github.com/projectdiscovery/mapcidr v1.1.16/go.mod h1:rGqpBhStdwOQ2uS62QM9qPsybwMwIhT7CTd2bxoHs8Q=
-github.com/projectdiscovery/martian/v3 v3.0.0-20230412114616-98e3a0a6994a h1:TpQ/kMGhsFIv/9hytTU+xXaL+yl2xnTkxLqQRlV6+zE=
-github.com/projectdiscovery/martian/v3 v3.0.0-20230412114616-98e3a0a6994a/go.mod h1:wPvVUl2C/XOFacugXwsUp65GN0upmKfwKMyimA/AAaM=
+github.com/projectdiscovery/martian/v3 v3.0.0-20240219194442-fed3b744f477 h1:VJaBELAC5Hw+kc+ylrFF5nSf7Wasnb9mZxMlF/kR3gg=
+github.com/projectdiscovery/martian/v3 v3.0.0-20240219194442-fed3b744f477/go.mod h1:wPvVUl2C/XOFacugXwsUp65GN0upmKfwKMyimA/AAaM=
github.com/projectdiscovery/networkpolicy v0.0.7 h1:AwHqBRXBqDQgnWzBMuoJtHBNEYBw+NFp/4qIK688x7o=
github.com/projectdiscovery/networkpolicy v0.0.7/go.mod h1:CK0CnFoLF1Nou6mY7P4WODSAxhPN8g8g7XpapgEP8tI=
github.com/projectdiscovery/retryabledns v1.0.56 h1:Rk/fvBSNjw4vzbHRSSoFz3Bkn9uaRSk0UE/IsEBl0cQ=
@@ -271,8 +283,8 @@ github.com/projectdiscovery/roundrobin v0.0.6 h1:zoJAFRgP9XK7B+iKSjR+djRAuDYxnc5
github.com/projectdiscovery/roundrobin v0.0.6/go.mod h1:vTxcWqNLyMH6VE2Q/hsNNvDHFLiIzHozC1rLLT/vocQ=
github.com/projectdiscovery/tinydns v0.0.29 h1:0xBFo+jJcEpf/zlYtxD4fQNeqrDvhzBrOfmpXdKKJKY=
github.com/projectdiscovery/tinydns v0.0.29/go.mod h1:ic2bJKVMexcpxeBTtO1klR2sddmk4jV6+WDZptr1VxU=
-github.com/projectdiscovery/utils v0.0.79 h1:ptO3Qo2e24SK5w5yvDk2whsvSEIk7gSX+RNhBQPRKqc=
-github.com/projectdiscovery/utils v0.0.79/go.mod h1:tBFlI+1warN7y7hKpFf6pqqOszvufENofy9Md0qlZQo=
+github.com/projectdiscovery/utils v0.0.80-0.20240219143814-1bd72bb71244 h1:vqMyenRxz07UC5m+uvRdI+18fBw96+wD6JAJizy9up8=
+github.com/projectdiscovery/utils v0.0.80-0.20240219143814-1bd72bb71244/go.mod h1:avG/w3yO6v/P2jY/rjfjW8mByRbpY+lEBO8wASCh9yM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/quic-go v0.37.7 h1:AgKsQLZ1+YCwZd2GYhBUsJDYZwEkA5gENtAjb+MxONU=
github.com/quic-go/quic-go v0.37.7/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
@@ -382,8 +394,8 @@ golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -418,8 +430,8 @@ golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
@@ -457,8 +469,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -492,6 +504,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
diff --git a/internal/runner/options.go b/internal/runner/options.go
index 2e18cce2..ab746c8f 100644
--- a/internal/runner/options.go
+++ b/internal/runner/options.go
@@ -4,6 +4,7 @@ import (
"math"
"os"
"path/filepath"
+ "strings"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger"
@@ -28,6 +29,7 @@ var (
type Options struct {
OutputDirectory string
OutputFile string // for storing the jsonl output
+ OutputFormat string
LoggerConfig string
ConfigDir string
CertCacheSize int
@@ -72,11 +74,11 @@ func ParseOptions() (*Options, error) {
flagSet.CreateGroup("output", "Output",
// Todo: flagSet.BoolVar(&options.Dump, "dump", true, "Dump HTTP requests/response to output file"),
- flagSet.DynamicVarP(&options.OutputDirectory, "store-resposne", "sr", "proxify_logs", "store raw http request / response to output directory"),
+ flagSet.DynamicVarP(&options.OutputDirectory, "store-response", "sr", "proxify_logs", "store raw http request / response to output directory"),
flagSet.DynamicVarP(&options.OutputFile, "output", "o", "proxify_logs.jsonl", "output file to store proxify logs"),
+ flagSet.StringVarP(&options.OutputFormat, "output-format", "of", "jsonl", "output format (jsonl/yaml)"),
flagSet.BoolVar(&options.DumpRequest, "dump-req", false, "Dump only HTTP requests to output file"),
flagSet.BoolVar(&options.DumpResponse, "dump-resp", false, "Dump only HTTP responses to output file"),
- flagSet.BoolVarP(&options.OutputJsonl, "jsonl", "j", false, "write output in JSONL(ines) format"),
flagSet.StringVarP(&options.OutCAFile, "out-ca", "oca", "", "Generate and Save CA File to filename"),
)
@@ -122,7 +124,7 @@ func ParseOptions() (*Options, error) {
silent, verbose, veryVerbose := false, false, false
flagSet.CreateGroup("debug", "debug",
- flagSet.BoolVarP(&options.NoColor, "no-color", "nc", true, "No Color"),
+ flagSet.BoolVarP(&options.NoColor, "no-color", "nc", false, "No Color"),
flagSet.BoolVar(&options.Version, "version", false, "Version"),
flagSet.BoolVar(&silent, "silent", false, "Silent"),
flagSet.BoolVarP(&verbose, "verbose", "v", false, "Verbose"),
@@ -183,6 +185,11 @@ func ParseOptions() (*Options, error) {
if options.OutputFile == "" {
options.OutputFile = "proxify_logs.jsonl"
}
+
+ if options.OutputFormat == "yaml" {
+ options.OutputFile = strings.ReplaceAll(options.OutputFile, "jsonl", "yaml")
+ }
+
return options, nil
}
diff --git a/internal/runner/runner.go b/internal/runner/runner.go
index cbb6ffe0..d7a5a292 100644
--- a/internal/runner/runner.go
+++ b/internal/runner/runner.go
@@ -39,6 +39,7 @@ func NewRunner(options *Options) (*Runner, error) {
ListenAddrHTTP: options.ListenAddrHTTP,
ListenAddrSocks5: options.ListenAddrSocks5,
OutputFile: options.OutputFile,
+ OutputFormat: options.OutputFormat,
OutputDirectory: options.OutputDirectory,
RequestDSL: options.RequestDSL,
ResponseDSL: options.ResponseDSL,
diff --git a/pkg/logger/elastic/elasticsearch.go b/pkg/logger/elastic/elasticsearch.go
index 18a5b305..44c2e02c 100644
--- a/pkg/logger/elastic/elasticsearch.go
+++ b/pkg/logger/elastic/elasticsearch.go
@@ -68,7 +68,7 @@ func New(option *Options) (*Client, error) {
}
// Store saves a passed log event in elasticsearch
-func (c *Client) Save(data types.OutputData) error {
+func (c *Client) Save(data types.HTTPTransaction) error {
var doc map[string]interface{}
if data.Userdata.HasResponse {
doc = map[string]interface{}{
diff --git a/pkg/logger/file/file.go b/pkg/logger/file/file.go
index 622a44ee..2cfd6a91 100644
--- a/pkg/logger/file/file.go
+++ b/pkg/logger/file/file.go
@@ -42,7 +42,7 @@ func New(option *Options) (*Client, error) {
}
// Store writes the log to the file
-func (c *Client) Save(data types.OutputData) error {
+func (c *Client) Save(data types.HTTPTransaction) error {
var err error
logFile := fmt.Sprintf("%s.%s", data.Name, "txt")
if c.options.OutputFolder != "" {
diff --git a/pkg/logger/jsonl/jsonl.go b/pkg/logger/jsonl/jsonl.go
new file mode 100644
index 00000000..785fbf88
--- /dev/null
+++ b/pkg/logger/jsonl/jsonl.go
@@ -0,0 +1,39 @@
+package jsonl
+
+import (
+ "encoding/json"
+ "os"
+ "strings"
+
+ "github.com/projectdiscovery/proxify/pkg/types"
+)
+
+// JsonLinesWriter is a writer for json lines
+type JsonLinesWriter struct {
+ f *os.File
+}
+
+// NewJsonLinesWriter creates a new json lines writer
+func NewJsonLinesWriter(filePath string) (*JsonLinesWriter, error) {
+ file, err := os.Create(filePath)
+ if err != nil {
+ return nil, err
+ }
+ return &JsonLinesWriter{f: file}, nil
+}
+
+// Write writes a http transaction to the file.
+func (j *JsonLinesWriter) Write(data *types.HTTPRequestResponseLog) error {
+ binx, err := json.Marshal(data)
+ if err != nil {
+ return err
+ }
+ _, _ = j.f.WriteString(strings.ReplaceAll(string(binx), "\n", "\\n")) // escape new lines
+ _, _ = j.f.WriteString("\n")
+ return nil
+}
+
+// Close closes the file writer.
+func (j *JsonLinesWriter) Close() error {
+ return j.f.Close()
+}
diff --git a/pkg/logger/kafka/kafka.go b/pkg/logger/kafka/kafka.go
index e6a26dbe..dae01348 100644
--- a/pkg/logger/kafka/kafka.go
+++ b/pkg/logger/kafka/kafka.go
@@ -40,7 +40,7 @@ func New(option *Options) (*Client, error) {
}
// Store passes the message to kafka
-func (c *Client) Save(data types.OutputData) error {
+func (c *Client) Save(data types.HTTPTransaction) error {
msg := &sarama.ProducerMessage{
Topic: c.topic,
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index da5de107..ee28ca7b 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -1,14 +1,11 @@
package logger
import (
- "bytes"
- "encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"strings"
- "sync"
"time"
"github.com/asaskevich/govalidator"
@@ -16,6 +13,8 @@ import (
"github.com/projectdiscovery/proxify/pkg/logger/elastic"
"github.com/projectdiscovery/proxify/pkg/logger/file"
"github.com/projectdiscovery/proxify/pkg/logger/kafka"
+ "github.com/projectdiscovery/utils/conversion"
+ pdhttpUtils "github.com/projectdiscovery/utils/http"
stringsutil "github.com/projectdiscovery/utils/strings"
"github.com/projectdiscovery/proxify/pkg/types"
@@ -29,35 +28,32 @@ const (
type OptionsLogger struct {
Verbosity types.Verbosity
- OutputFolder string
- OutputFile string
- DumpRequest bool
- DumpResponse bool
- OutputJsonl bool
- MaxSize int
+ OutputFolder string // when output is written to multiple files
+ OutputFile string // when output is written to single file
+ OutputFormat string // jsonl or yaml
+ DumpRequest bool // dump request to file
+ DumpResponse bool // dump response to file
+ MaxSize int // max size of the output
Elastic *elastic.Options
Kafka *kafka.Options
}
type Store interface {
- Save(data types.OutputData) error
+ Save(data types.HTTPTransaction) error
}
type Logger struct {
options *OptionsLogger
- asyncqueue chan types.OutputData
- jsonLogMap sync.Map
+ asyncqueue chan types.HTTPTransaction
Store []Store
+ sWriter OutputFileWriter // sWriter is the structured writer
}
// NewLogger instance
func NewLogger(options *OptionsLogger) *Logger {
logger := &Logger{
options: options,
- asyncqueue: make(chan types.OutputData, 1000),
- }
- if options.OutputJsonl {
- logger.jsonLogMap = sync.Map{}
+ asyncqueue: make(chan types.HTTPTransaction, 1000),
}
if options.Elastic.Addr != "" {
store, err := elastic.New(options.Elastic)
@@ -82,8 +78,6 @@ func NewLogger(options *OptionsLogger) *Logger {
}
store, err := file.New(&file.Options{
OutputFolder: options.OutputFolder,
- OutputJsonl: options.OutputJsonl,
- OutputFile: options.OutputFile,
})
if err != nil {
gologger.Warning().Msgf("Error while creating file logger: %s", err)
@@ -91,73 +85,141 @@ func NewLogger(options *OptionsLogger) *Logger {
logger.Store = append(logger.Store, store)
}
- go logger.AsyncWrite()
+ // setup structured writer
+ if options.OutputFormat != "" {
+ sWriter, err := NewOutputFileWriter(options.OutputFormat, options.OutputFile)
+ if err != nil {
+ gologger.Warning().Msgf("Error while creating structured writer: %s", err)
+ } else {
+ logger.sWriter = sWriter
+ }
+ }
+ go logger.AsyncWrite()
return logger
}
+// LogRequest and user data
+func (l *Logger) LogRequest(req *http.Request, userdata types.UserData) error {
+ // No-op for now , since proxify isn't intended to fail instead return 502
+ // and request can be accessed via response.Request
+ return nil
+}
+
+// LogResponse and user data
+func (l *Logger) LogResponse(resp *http.Response, userdata types.UserData) error {
+ if resp == nil {
+ return nil
+ }
+ // send to writer channel
+ l.asyncqueue <- types.HTTPTransaction{
+ Userdata: userdata,
+ Response: resp,
+ Request: resp.Request,
+ }
+ return nil
+}
+
// AsyncWrite data
func (l *Logger) AsyncWrite() {
- for outputdata := range l.asyncqueue {
- if len(l.Store) > 0 {
- if !l.options.DumpRequest && !l.options.DumpResponse {
- outputdata.PartSuffix = ""
- } else if l.options.DumpRequest && !outputdata.Userdata.HasResponse {
- outputdata.PartSuffix = ".request"
- } else if l.options.DumpResponse && outputdata.Userdata.HasResponse {
- outputdata.PartSuffix = ".response"
+ for httpData := range l.asyncqueue {
+ if httpData.Request == nil {
+ // we can't do anything without request
+ continue
+ }
+ // we have better options to handle this
+ // i.e Buffer reuse and normalizing request/response body (removing encoding etc)
+ reqDump, err := httputil.DumpRequest(httpData.Request, true)
+ if err != nil {
+ gologger.Warning().Msgf("Error while dumping request: %s", err)
+ }
+
+ // debug log request if true
+ l.debugLogRequest(reqDump, httpData.Request)
+
+ var respChain *pdhttpUtils.ResponseChain
+ if httpData.Response != nil {
+ respChainx := pdhttpUtils.NewResponseChain(httpData.Response, 4096)
+ if err := respChainx.Fill(); err == nil {
+ respChain = respChainx
} else {
- continue
- }
- outputdata.Name = fmt.Sprintf("%s%s-%s", outputdata.Userdata.Host, outputdata.PartSuffix, outputdata.Userdata.ID)
- if outputdata.Userdata.HasResponse && !(l.options.DumpRequest || l.options.DumpResponse) {
- if outputdata.Userdata.Match {
- outputdata.Name = outputdata.Name + ".match"
- }
- }
- outputdata.Format = dataWithoutNewLine
- if !strings.HasSuffix(string(outputdata.Data), "\n") {
- outputdata.Format = dataWithNewLine
+ gologger.Warning().Msgf("responseChain: Error while dumping response: %s", err)
}
-
- outputdata.DataString = fmt.Sprintf(outputdata.Format, outputdata.Data)
- if outputdata.Userdata.HasResponse {
- outputdata.Format = "\n" + outputdata.Format
+ }
+ // debug log response if true
+ if respChain != nil {
+ if err := l.debugLogResponse(respChain); err != nil {
+ gologger.Warning().Msgf("Error while logging response: %s", err)
}
- outputdata.RawData = []byte(fmt.Sprintf(outputdata.Format, outputdata.RawData))
+ }
- if l.options.MaxSize > 0 {
- outputdata.DataString = stringsutil.Truncate(outputdata.DataString, l.options.MaxSize)
- outputdata.RawData = []byte(stringsutil.Truncate(string(outputdata.RawData), l.options.MaxSize))
- }
+ // first write to structured writer
+ if l.sWriter != nil {
+ func() {
+ // if matchers were given only store those that match
+ if httpData.Userdata.Match != nil {
+ if !*httpData.Userdata.Match {
+ return
+ }
+ }
- for _, store := range l.Store {
- err := store.Save(outputdata)
+ sData := &types.HTTPRequestResponseLog{
+ Timestamp: time.Now().Format(time.RFC3339),
+ URL: httpData.Request.URL.String(),
+ }
+ defer func() {
+ // write to structured writer with whatever data we have
+ err := l.sWriter.Write(sData)
+ if err != nil {
+ gologger.Warning().Msgf("Error while logging: %s", err)
+ }
+ }()
+ sRequest, err := types.NewHttpRequestData(httpData.Request)
if err != nil {
- gologger.Warning().Msgf("Error while logging: %s", err)
+ gologger.Warning().Msgf("Error while creating request: %s", err)
+ return
+ }
+ sData.Request = sRequest
+ if respChain != nil {
+ sResponse, err := types.NewHttpResponseData(respChain)
+ if err != nil {
+ gologger.Warning().Msgf("Error while creating response: %s", err)
+ }
+ sData.Response = sResponse
}
+ }()
+ }
+
+ // write to other writers
+ if len(l.Store) > 0 {
+ // write request first
+ outputData := httpData
+ // outputData.Data = reqDump
+ outputData.RawData = reqDump
+ outputData.Userdata.HasResponse = false
+ l.storeWriter(outputData)
+
+ // write response if available
+ if respChain != nil {
+ // outputData.Data = respChain.FullResponse().Bytes()
+ outputData.RawData = respChain.FullResponse().Bytes()
+ outputData.Userdata.HasResponse = true
+ l.storeWriter(outputData)
}
}
}
}
-// LogRequest and user data
-func (l *Logger) LogRequest(req *http.Request, userdata types.UserData) error {
- reqdump, err := httputil.DumpRequest(req, true)
- if err != nil {
- return err
- }
- if l.options.OutputJsonl {
- outputData := types.HTTPRequestResponseLog{}
- if err := fillJsonRequestData(req, &outputData); err != nil {
- return err
- }
- l.jsonLogMap.Store(userdata.ID, outputData)
- }
- if l.options.OutputFolder != "" {
- l.asyncqueue <- types.OutputData{RawData: reqdump, Userdata: userdata}
+// Close logger instance
+func (l *Logger) Close() {
+ if l.sWriter != nil {
+ l.sWriter.Close()
}
+ close(l.asyncqueue)
+}
+// debugLogRequest logs the request to the console if debugging is enabled
+func (l *Logger) debugLogRequest(reqdump []byte, req *http.Request) {
if l.options.Verbosity >= types.VerbosityVeryVerbose {
contentType := req.Header.Get("Content-Type")
b, _ := io.ReadAll(req.Body)
@@ -166,121 +228,64 @@ func (l *Logger) LogRequest(req *http.Request, userdata types.UserData) error {
}
gologger.Silent().Msgf("%s", string(reqdump))
}
- return nil
-}
-
-func isASCIICheckRequired(contentType string) bool {
- return stringsutil.ContainsAny(contentType, "application/octet-stream", "application/x-www-form-urlencoded")
}
-// LogResponse and user data
-func (l *Logger) LogResponse(resp *http.Response, userdata types.UserData) error {
- if resp == nil {
- return nil
- }
- respdump, err := httputil.DumpResponse(resp, true)
- if err != nil {
- return err
- }
- respdumpNoBody, err := httputil.DumpResponse(resp, false)
- if err != nil {
- return err
- }
- var data []byte
- if l.options.OutputJsonl {
- defer l.jsonLogMap.Delete(userdata.ID)
- outputData := types.HTTPRequestResponseLog{}
- filledOutputReq, ok := l.jsonLogMap.Load(userdata.ID)
- if !ok {
- if err := fillJsonRequestData(resp.Request, &outputData); err != nil {
- return err
- }
- } else {
- outputData = filledOutputReq.(types.HTTPRequestResponseLog)
- }
- if err := fillJsonResponseData(resp, &outputData); err != nil {
- return err
- }
- data, err = json.Marshal(outputData)
- if err != nil {
- return err
- }
- }
-
- l.asyncqueue <- types.OutputData{RawData: respdump, Data: data, Userdata: userdata}
-
+// debugLogResponse logs the response to the console if debugging is enabled
+func (l *Logger) debugLogResponse(respChain *pdhttpUtils.ResponseChain) error {
if l.options.Verbosity >= types.VerbosityVeryVerbose {
- contentType := resp.Header.Get("Content-Type")
- bodyBytes := bytes.TrimPrefix(respdump, respdumpNoBody)
- if isASCIICheckRequired(contentType) && !govalidator.IsPrintableASCII(string(bodyBytes)) {
- gologger.Silent().Msgf("%s", string(respdumpNoBody))
+ contentType := respChain.Response().Header.Get("Content-Type")
+ if isASCIICheckRequired(contentType) && !govalidator.IsPrintableASCII(conversion.String(respChain.Body().Bytes())) {
+ gologger.Silent().Msgf("%s", respChain.Headers().String())
} else {
- gologger.Silent().Msgf("%s", string(respdump))
+ gologger.Silent().Msgf("%s", respChain.FullResponse().String())
}
}
return nil
}
-// Close logger instance
-func (l *Logger) Close() {
- close(l.asyncqueue)
-}
-
-func fillJsonRequestData(req *http.Request, outputData *types.HTTPRequestResponseLog) error {
- outputData.Timestamp = time.Now().Format(time.RFC3339)
- outputData.URL = req.URL.String()
- // Extract headers from the request
- reqHeaders := make(map[string]string)
- // basic header info
- reqHeaders["scheme"] = req.URL.Scheme
- reqHeaders["method"] = req.Method
- reqHeaders["path"] = req.URL.Path
- reqHeaders["host"] = req.URL.Host
- for key, values := range req.Header {
- reqHeaders[key] = strings.Join(values, ", ")
- }
- outputData.Request.Header = reqHeaders
- // Extract body from the request
- reqBody, err := io.ReadAll(req.Body)
- if err != nil {
- return err
+// storeWriter writes the data to the store (file, kafka, elastic)
+// this can be refactored to make it more readable and scalable
+// with improved interface and probably use of structure http data
+// instead of raw bytes
+func (l *Logger) storeWriter(outputdata types.HTTPTransaction) {
+ if !l.options.DumpRequest && !l.options.DumpResponse {
+ outputdata.PartSuffix = ""
+ } else if l.options.DumpRequest && !outputdata.Userdata.HasResponse {
+ outputdata.PartSuffix = ".request"
+ } else if l.options.DumpResponse && outputdata.Userdata.HasResponse {
+ outputdata.PartSuffix = ".response"
+ } else {
+ return
}
- defer req.Body.Close()
- req.Body = io.NopCloser(strings.NewReader(string(reqBody)))
- if err != nil {
- return err
+ outputdata.Name = fmt.Sprintf("%s%s-%s", outputdata.Userdata.Host, outputdata.PartSuffix, outputdata.Userdata.ID)
+ if outputdata.Userdata.HasResponse && !(l.options.DumpRequest || l.options.DumpResponse) {
+ if outputdata.Userdata.Match != nil && *outputdata.Userdata.Match {
+ outputdata.Name = outputdata.Name + ".match"
+ }
}
- outputData.Request.Body = string(reqBody)
- // Extract raw request
- reqdumpNoBody, err := httputil.DumpRequest(req, false)
- if err != nil {
- return err
+ outputdata.Format = dataWithoutNewLine
+ if !strings.HasSuffix(string(outputdata.Data), "\n") {
+ outputdata.Format = dataWithNewLine
}
- outputData.Request.Raw = string(reqdumpNoBody)
- return nil
-}
-func fillJsonResponseData(resp *http.Response, outputData *types.HTTPRequestResponseLog) error {
- outputData.Timestamp = time.Now().Format(time.RFC3339)
- // Extract headers from the response
- respHeaders := make(map[string]string)
- for key, values := range resp.Header {
- respHeaders[key] = strings.Join(values, ", ")
+ outputdata.DataString = fmt.Sprintf(outputdata.Format, outputdata.Data)
+ if outputdata.Userdata.HasResponse {
+ outputdata.Format = "\n" + outputdata.Format
}
- outputData.Response.Header = respHeaders
- // Extract body from the response
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- return err
+ outputdata.RawData = []byte(fmt.Sprintf(outputdata.Format, outputdata.RawData))
+
+ if l.options.MaxSize > 0 {
+ outputdata.DataString = stringsutil.Truncate(outputdata.DataString, l.options.MaxSize)
+ outputdata.RawData = []byte(stringsutil.Truncate(string(outputdata.RawData), l.options.MaxSize))
}
- defer resp.Body.Close()
- resp.Body = io.NopCloser(strings.NewReader(string(respBody)))
- outputData.Response.Body = string(respBody)
- // Extract raw response
- respdumpNoBody, err := httputil.DumpResponse(resp, false)
- if err != nil {
- return err
+ for _, store := range l.Store {
+ err := store.Save(outputdata)
+ if err != nil {
+ gologger.Warning().Msgf("Error while logging: %s", err)
+ }
}
- outputData.Response.Raw = string(respdumpNoBody)
- return nil
+}
+
+func isASCIICheckRequired(contentType string) bool {
+ return stringsutil.ContainsAny(contentType, "application/octet-stream", "application/x-www-form-urlencoded")
}
diff --git a/pkg/logger/writer.go b/pkg/logger/writer.go
new file mode 100644
index 00000000..61372af4
--- /dev/null
+++ b/pkg/logger/writer.go
@@ -0,0 +1,38 @@
+package logger
+
+import (
+ "errors"
+
+ "github.com/projectdiscovery/proxify/pkg/logger/jsonl"
+ "github.com/projectdiscovery/proxify/pkg/logger/yaml"
+ "github.com/projectdiscovery/proxify/pkg/types"
+)
+
+var (
+ _ OutputFileWriter = &jsonl.JsonLinesWriter{}
+ // multi doc yaml writer with --- separator
+ _ OutputFileWriter = &yaml.YamlMultiDocWriter{}
+
+ ErrorInvalidFormat = errors.New("invalid format: expected jsonl or yaml")
+)
+
+// OutputFileWriter is an interface for writing structured
+// data to a file.
+type OutputFileWriter interface {
+ // Write writes a http transaction to the file.
+ Write(data *types.HTTPRequestResponseLog) error
+ // Close closes the file writer.
+ Close() error
+}
+
+// NewOutputFileWriter creates a new output file writer
+func NewOutputFileWriter(format, filePath string) (OutputFileWriter, error) {
+ switch format {
+ case "jsonl":
+ return jsonl.NewJsonLinesWriter(filePath)
+ case "yaml":
+ return yaml.NewYamlMultiDocWriter(filePath)
+ default:
+ return nil, ErrorInvalidFormat
+ }
+}
diff --git a/pkg/logger/yaml/yaml.go b/pkg/logger/yaml/yaml.go
new file mode 100644
index 00000000..d83464b5
--- /dev/null
+++ b/pkg/logger/yaml/yaml.go
@@ -0,0 +1,43 @@
+package yaml
+
+import (
+ "os"
+
+ "github.com/goccy/go-yaml"
+ "github.com/projectdiscovery/proxify/pkg/types"
+)
+
+// YamlMultiDocWriter is a writer for yaml multi doc
+type YamlMultiDocWriter struct {
+ f *os.File
+ enc *yaml.Encoder
+}
+
+// NewYamlMultiDocWriter creates a new yaml multi doc writer
+func NewYamlMultiDocWriter(filePath string) (*YamlMultiDocWriter, error) {
+ file, err := os.Create(filePath)
+ if err != nil {
+ return nil, err
+ }
+ enc := yaml.NewEncoder(file, yaml.UseLiteralStyleIfMultiline(true), yaml.UseSingleQuote(true))
+ return &YamlMultiDocWriter{f: file, enc: enc}, nil
+}
+
+// Write writes a http transaction to the file.
+func (y *YamlMultiDocWriter) Write(data *types.HTTPRequestResponseLog) error {
+ if err := y.enc.Encode(data); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Close closes the file writer.
+func (y *YamlMultiDocWriter) Close() error {
+ if y.enc != nil {
+ y.enc.Close()
+ }
+ if y.f != nil {
+ _ = y.f.Close()
+ }
+ return nil
+}
diff --git a/pkg/types/userdata.go b/pkg/types/userdata.go
index 1edd6d43..44d44b65 100644
--- a/pkg/types/userdata.go
+++ b/pkg/types/userdata.go
@@ -1,13 +1,27 @@
package types
+import (
+ "io"
+ "net/http"
+ "net/http/httputil"
+ "strings"
+
+ pdhttpUtils "github.com/projectdiscovery/utils/http"
+)
+
+// UserData is context used to identify a http
+// transaction and its state like match, response etc.
type UserData struct {
ID string
- Match bool
+ Match *bool
HasResponse bool
Host string
}
-type OutputData struct {
+// HTTPTransaction is a struct for http transaction
+// it contains data of every request/response obtained
+// from proxy
+type HTTPTransaction struct {
Userdata UserData
RawData []byte
Data []byte
@@ -15,19 +29,80 @@ type OutputData struct {
Name string
PartSuffix string
Format string
+
+ Request *http.Request
+ Response *http.Response
}
+// HTTPRequestResponseLog is a struct for http request and response log
+// it is a processed version of http transaction for logging
+// in more structured format than just raw bytes
type HTTPRequestResponseLog struct {
- Timestamp string `json:"timestamp,omitempty"`
- URL string `json:"url,omitempty"`
- Request struct {
- Header map[string]string `json:"header,omitempty"`
- Body string `json:"body,omitempty"`
- Raw string `json:"raw,omitempty"`
- } `json:"request,omitempty"`
- Response struct {
- Header map[string]string `json:"header,omitempty"`
- Body string `json:"body,omitempty"`
- Raw string `json:"raw,omitempty"`
- } `json:"response,omitempty"`
+ Timestamp string `json:"timestamp,omitempty"`
+ URL string `json:"url,omitempty"`
+ Request *HTTPRequest `json:"request,omitempty"`
+ Response *HTTPResponse `json:"response,omitempty"`
+}
+
+// HTTPRequest is a struct for http request
+type HTTPRequest struct {
+ Header map[string]string `json:"header,omitempty"`
+ Body string `json:"body,omitempty"`
+ Raw string `json:"raw,omitempty"`
+}
+
+// NewHttpRequestData creates a new HttpRequest with data extracted from an http.Request
+func NewHttpRequestData(req *http.Request) (*HTTPRequest, error) {
+ httpRequest := &HTTPRequest{
+ Header: make(map[string]string),
+ }
+
+ // Extract headers from the request
+ httpRequest.Header["scheme"] = req.URL.Scheme
+ httpRequest.Header["method"] = req.Method
+ httpRequest.Header["path"] = req.URL.Path
+ httpRequest.Header["host"] = req.URL.Host
+ for key, values := range req.Header {
+ httpRequest.Header[key] = strings.Join(values, ", ")
+ }
+
+ // Extract body from the request
+ reqBody, err := io.ReadAll(req.Body)
+ if err != nil {
+ return nil, err
+ }
+ defer req.Body.Close()
+ req.Body = io.NopCloser(strings.NewReader(string(reqBody)))
+ httpRequest.Body = string(reqBody)
+
+ // Extract raw request
+ reqdumpNoBody, err := httputil.DumpRequest(req, false)
+ if err != nil {
+ return nil, err
+ }
+ httpRequest.Raw = string(reqdumpNoBody)
+
+ return httpRequest, nil
+}
+
+// HTTPResponse is a struct for http response
+type HTTPResponse struct {
+ Header map[string]string `json:"header,omitempty"`
+ Body string `json:"body,omitempty"`
+ Raw string `json:"raw,omitempty"`
+}
+
+// NewHttpResponseData creates a new HttpResponse with data extracted from an http.Response
+func NewHttpResponseData(resp *pdhttpUtils.ResponseChain) (*HTTPResponse, error) {
+ httpResponse := &HTTPResponse{
+ Header: make(map[string]string),
+ }
+ // Extract headers from the response
+ for key, values := range resp.Response().Header {
+ httpResponse.Header[key] = strings.Join(values, ", ")
+ }
+ httpResponse.Body = resp.Body().String()
+ httpResponse.Raw = resp.Headers().String() // doesn't include body
+
+ return httpResponse, nil
}
diff --git a/pkg/util/util.go b/pkg/util/util.go
index d8bc7db3..44221d94 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -10,8 +10,8 @@ import (
"strings"
)
-// HTTPRequesToMap Converts HTTP Request to Matcher Map
-func HTTPRequesToMap(req *http.Request) (map[string]interface{}, error) {
+// HTTPRequestToMap Converts HTTP Request to Matcher Map
+func HTTPRequestToMap(req *http.Request) (map[string]interface{}, error) {
m := make(map[string]interface{})
var headers string
for k, v := range req.Header {
@@ -37,6 +37,12 @@ func HTTPRequesToMap(req *http.Request) (map[string]interface{}, error) {
reqdumpString := string(reqdump)
m["raw"] = reqdumpString
m["request"] = reqdumpString
+ m["method"] = req.Method
+ m["path"] = req.URL.Path
+ m["host"] = req.URL.Host
+ m["scheme"] = req.URL.Scheme
+ m["url"] = req.URL.String()
+ m["query"] = req.URL.Query().Encode()
return m, nil
}
@@ -82,3 +88,15 @@ func MatchAnyRegex(regexes []string, data string) bool {
}
return false
}
+
+// EvalBoolSlice evaluates a slice of bools using a logical AND
+func EvalBoolSlice(slice []bool) bool {
+ if len(slice) == 0 {
+ return false
+ }
+ result := slice[0]
+ for _, b := range slice {
+ result = result && b
+ }
+ return result
+}
diff --git a/proxy.go b/proxy.go
index dfa15560..e3e84bd0 100644
--- a/proxy.go
+++ b/proxy.go
@@ -9,6 +9,7 @@ import (
"log"
"net"
"net/http"
+ "net/http/httptest"
"net/http/httputil"
"net/url"
"os"
@@ -33,6 +34,8 @@ import (
rbtransport "github.com/projectdiscovery/roundrobin/transport"
"github.com/projectdiscovery/tinydns"
errorutil "github.com/projectdiscovery/utils/errors"
+ readerUtil "github.com/projectdiscovery/utils/reader"
+ stringsutil "github.com/projectdiscovery/utils/strings"
"golang.org/x/net/proxy"
)
@@ -51,6 +54,7 @@ type Options struct {
ListenAddrSocks5 string
OutputDirectory string
OutputFile string
+ OutputFormat string
RequestDSL []string
ResponseDSL []string
UpstreamHTTPProxies []string
@@ -81,6 +85,116 @@ type Proxy struct {
tinydns *tinydns.TinyDNS
rbhttp *rbtransport.RoundTransport
rbsocks5 *rbtransport.RoundTransport
+ proxifyMux *http.ServeMux // serve banner page and static files
+ listenAddr string
+}
+
+func NewProxy(options *Options) (*Proxy, error) {
+
+ switch options.Verbosity {
+ case types.VerbositySilent:
+ martianlog.SetLevel(martianlog.Silent)
+ case types.VerbosityVerbose:
+ martianlog.SetLevel(martianlog.Info)
+ case types.VerbosityVeryVerbose:
+ martianlog.SetLevel(martianlog.Debug)
+ default:
+ martianlog.SetLevel(martianlog.Error)
+ }
+
+ logger := logger.NewLogger(&logger.OptionsLogger{
+ Verbosity: options.Verbosity,
+ OutputFile: options.OutputFile,
+ OutputFormat: options.OutputFormat,
+ OutputFolder: options.OutputDirectory,
+ DumpRequest: options.DumpRequest,
+ DumpResponse: options.DumpResponse,
+ MaxSize: options.MaxSize,
+ Elastic: options.Elastic,
+ Kafka: options.Kafka,
+ })
+
+ var tdns *tinydns.TinyDNS
+
+ fastdialerOptions := fastdialer.DefaultOptions
+ fastdialerOptions.EnableFallback = true
+ fastdialerOptions.Deny = options.Deny
+ fastdialerOptions.Allow = options.Allow
+ if options.ListenDNSAddr != "" {
+ dnsmapping := make(map[string]*tinydns.DnsRecord)
+ for _, record := range strings.Split(options.DNSMapping, ",") {
+ data := strings.Split(record, ":")
+ if len(data) != 2 {
+ continue
+ }
+ dnsmapping[data[0]] = &tinydns.DnsRecord{A: []string{data[1]}}
+ }
+ var err error
+ tdns, err = tinydns.New(&tinydns.Options{
+ ListenAddress: options.ListenDNSAddr,
+ Net: "udp",
+ UpstreamServers: []string{options.DNSFallbackResolver},
+ DnsRecords: dnsmapping,
+ })
+ if err != nil {
+ return nil, err
+ }
+ fastdialerOptions.BaseResolvers = []string{"127.0.0.1" + options.ListenDNSAddr}
+ }
+ dialer, err := fastdialer.NewDialer(fastdialerOptions)
+ if err != nil {
+ return nil, err
+ }
+
+ var rbhttp, rbsocks5 *rbtransport.RoundTransport
+ if len(options.UpstreamHTTPProxies) > 0 {
+ rbhttp, err = rbtransport.NewWithOptions(options.UpstreamProxyRequestsNumber, options.UpstreamHTTPProxies...)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if len(options.UpstreamSock5Proxies) > 0 {
+ rbsocks5, err = rbtransport.NewWithOptions(options.UpstreamProxyRequestsNumber, options.UpstreamSock5Proxies...)
+ if err != nil {
+ return nil, err
+ }
+ }
+ pmux, err := getProxifyServerMux()
+ if err != nil {
+ return nil, err
+ }
+
+ proxy := &Proxy{
+ logger: logger,
+ options: options,
+ Dialer: dialer,
+ tinydns: tdns,
+ rbhttp: rbhttp,
+ rbsocks5: rbsocks5,
+ proxifyMux: pmux,
+ }
+
+ if err := proxy.setupHTTPProxy(); err != nil {
+ return nil, err
+ }
+
+ var socks5proxy *socks5.Server
+ if options.ListenAddrSocks5 != "" {
+ socks5Config := &socks5.Config{
+ Dial: proxy.httpTunnelDialer,
+ }
+ if options.Verbosity <= types.VerbositySilent {
+ socks5Config.Logger = log.New(io.Discard, "", log.Ltime|log.Lshortfile)
+ }
+ socks5proxy, err = socks5.New(socks5Config)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ proxy.socks5proxy = socks5proxy
+
+ return proxy, nil
}
// ModifyRequest
@@ -94,21 +208,31 @@ func (p *Proxy) ModifyRequest(req *http.Request) error {
Host: req.Host,
}
+ if stringsutil.EqualFoldAny(req.Host, "proxify", "proxify:443", "proxify:80", p.listenAddr) {
+ // hijack if this is true
+ return p.hijackNServe(req, ctx)
+ }
+
// If callbacks are given use them (for library use cases)
if p.options.OnRequestCallback != nil {
return p.options.OnRequestCallback(req, ctx)
}
+ boolSlice := []bool{}
for _, expr := range p.options.RequestDSL {
- if !userData.Match {
- m, _ := util.HTTPRequesToMap(req)
- v, err := dsl.EvalExpr(expr, m)
- if err != nil {
- gologger.Warning().Msgf("Could not evaluate request dsl: %s\n", err)
- }
- userData.Match = err == nil && v.(bool)
+ m, _ := util.HTTPRequestToMap(req)
+ v, err := dsl.EvalExpr(expr, m)
+ if err != nil {
+ gologger.Warning().Msgf("Could not evaluate request dsl: %s\n", err)
}
+ boolSlice = append(boolSlice, err == nil && v.(bool))
+ }
+ // evaluate bool array to get match status
+ if len(boolSlice) > 0 {
+ tmp := util.EvalBoolSlice(boolSlice)
+ userData.Match = &tmp
}
+
ctx.Set("user-data", userData)
// perform match and replace
@@ -129,31 +253,39 @@ func (p *Proxy) ModifyResponse(resp *http.Response) error {
}
}
if userData == nil {
- gologger.Error().Msgf("something went wrong got response without userData")
+ gologger.Warning().Msgf("something went wrong got response without userData")
// pass empty struct to avoid panic
userData = &types.UserData{}
}
userData.HasResponse = true
+ // if content-length is zero and remove header
+ if resp.ContentLength == 0 {
+ resp.Header.Del("Content-Length")
+ }
+
// If callbacks are given use them (for library use cases)
if p.options.OnResponseCallback != nil {
return p.options.OnResponseCallback(resp, ctx)
}
- // TODO: match in request seems to be seperate from response
- // but share same `Match` value. investigate this
- matchStatus := false
+ boolSlice := []bool{}
for _, expr := range p.options.ResponseDSL {
- if !matchStatus {
- m, _ := util.HTTPResponseToMap(resp)
- v, err := dsl.EvalExpr(expr, m)
- if err != nil {
- gologger.Warning().Msgf("Could not evaluate response dsl: %s\n", err)
- }
- matchStatus = err == nil && v.(bool)
+ m, _ := util.HTTPResponseToMap(resp)
+ v, err := dsl.EvalExpr(expr, m)
+ if err != nil {
+ gologger.Warning().Msgf("Could not evaluate response dsl: %s\n", err)
+ }
+ boolSlice = append(boolSlice, err == nil && v.(bool))
+ }
+ if len(boolSlice) > 0 {
+ tmp := util.EvalBoolSlice(boolSlice)
+ // finalize
+ if userData.Match != nil {
+ tmp = *userData.Match && tmp
}
+ userData.Match = &tmp
}
- userData.Match = matchStatus
// perform match and replace
if len(p.options.ResponseMatchReplaceDSL) != 0 {
_ = p.MatchReplaceResponse(resp)
@@ -212,8 +344,8 @@ func (p *Proxy) MatchReplaceRequest(req *http.Request) error {
// MatchReplaceRequest strings or regex
func (p *Proxy) MatchReplaceResponse(resp *http.Response) error {
- // Set Content-Length to zero to allow automatic calculation
- resp.ContentLength = 0
+ // // Set Content-Length to zero to allow automatic calculation
+ resp.ContentLength = -1
// lazy mode - dump request
respdump, err := httputil.DumpResponse(resp, true)
@@ -244,8 +376,14 @@ func (p *Proxy) MatchReplaceResponse(resp *http.Response) error {
// closes old body to allow memory reuse
resp.Body.Close()
resp.Header = responseNew.Header
- resp.Body = responseNew.Body
- resp.ContentLength = responseNew.ContentLength
+ resp.Body, err = readerUtil.NewReusableReadCloser(responseNew.Body)
+ if err != nil {
+ return err
+ }
+ if resp.ContentLength == 0 {
+ resp.Header.Del("Content-Length")
+ }
+ // resp.ContentLength = responseNew.ContentLength
return nil
}
@@ -276,18 +414,10 @@ func (p *Proxy) Run() error {
if err != nil {
gologger.Fatal().Msgf("failed to setup listener got %v", err)
}
- // serve web page to download ca cert
+ p.listenAddr = l.Addr().String()
wg.Add(1)
go func() {
defer wg.Done()
-
- gologger.Fatal().Msgf("%v", serveWebPage(l))
- }()
-
- wg.Add(1)
- go func() {
- defer wg.Done()
-
gologger.Fatal().Msgf("%v", p.httpProxy.Serve(l))
}()
}
@@ -322,6 +452,8 @@ func (p *Proxy) Run() error {
return nil
}
+func (p *Proxy) Stop() {}
+
// setupHTTPProxy configures proxy with settings
func (p *Proxy) setupHTTPProxy() error {
hp := martian.NewProxy()
@@ -379,121 +511,31 @@ func (p *Proxy) getRoundTripper() (http.RoundTripper, error) {
return roundtrip, nil
}
-func (p *Proxy) Stop() {
- // p.httpProxy.Close()
+func (p *Proxy) httpTunnelDialer(ctx context.Context, network, addr string) (net.Conn, error) {
+ return p.socks5tunnel.MakeTunnel(nil, nil, p.bufioPool, addr)
}
-func NewProxy(options *Options) (*Proxy, error) {
-
- switch options.Verbosity {
- case types.VerbositySilent:
- martianlog.SetLevel(martianlog.Silent)
- case types.VerbosityVerbose:
- martianlog.SetLevel(martianlog.Info)
- case types.VerbosityVeryVerbose:
- martianlog.SetLevel(martianlog.Debug)
- default:
- martianlog.SetLevel(martianlog.Error)
- }
-
- logger := logger.NewLogger(&logger.OptionsLogger{
- Verbosity: options.Verbosity,
- OutputFile: options.OutputFile,
- OutputFolder: options.OutputDirectory,
- DumpRequest: options.DumpRequest,
- DumpResponse: options.DumpResponse,
- OutputJsonl: options.OutputJsonl,
- MaxSize: options.MaxSize,
- Elastic: options.Elastic,
- Kafka: options.Kafka,
- })
-
- var tdns *tinydns.TinyDNS
-
- fastdialerOptions := fastdialer.DefaultOptions
- fastdialerOptions.EnableFallback = true
- fastdialerOptions.Deny = options.Deny
- fastdialerOptions.Allow = options.Allow
- if options.ListenDNSAddr != "" {
- dnsmapping := make(map[string]*tinydns.DnsRecord)
- for _, record := range strings.Split(options.DNSMapping, ",") {
- data := strings.Split(record, ":")
- if len(data) != 2 {
- continue
- }
- dnsmapping[data[0]] = &tinydns.DnsRecord{A: []string{data[1]}}
- }
- var err error
- tdns, err = tinydns.New(&tinydns.Options{
- ListenAddress: options.ListenDNSAddr,
- Net: "udp",
- UpstreamServers: []string{options.DNSFallbackResolver},
- DnsRecords: dnsmapping,
- })
- if err != nil {
- return nil, err
- }
- fastdialerOptions.BaseResolvers = []string{"127.0.0.1" + options.ListenDNSAddr}
- }
- dialer, err := fastdialer.NewDialer(fastdialerOptions)
+func (p *Proxy) hijackNServe(req *http.Request, ctx *martian.Context) error {
+ conn, brw, err := ctx.Session().Hijack()
if err != nil {
- return nil, err
- }
-
- var rbhttp, rbsocks5 *rbtransport.RoundTransport
- if len(options.UpstreamHTTPProxies) > 0 {
- rbhttp, err = rbtransport.NewWithOptions(options.UpstreamProxyRequestsNumber, options.UpstreamHTTPProxies...)
- if err != nil {
- return nil, err
- }
- }
- if len(options.UpstreamSock5Proxies) > 0 {
- rbsocks5, err = rbtransport.NewWithOptions(options.UpstreamProxyRequestsNumber, options.UpstreamSock5Proxies...)
- if err != nil {
- return nil, err
- }
- }
-
- proxy := &Proxy{
- logger: logger,
- options: options,
- Dialer: dialer,
- tinydns: tdns,
- rbhttp: rbhttp,
- rbsocks5: rbsocks5,
- }
-
- if err := proxy.setupHTTPProxy(); err != nil {
- return nil, err
+ return err
}
-
- var socks5proxy *socks5.Server
- if options.ListenAddrSocks5 != "" {
- socks5Config := &socks5.Config{
- Dial: proxy.httpTunnelDialer,
- }
- if options.Verbosity <= types.VerbositySilent {
- socks5Config.Logger = log.New(io.Discard, "", log.Ltime|log.Lshortfile)
- }
- socks5proxy, err = socks5.New(socks5Config)
- if err != nil {
- return nil, err
- }
+ defer conn.Close()
+ rec := httptest.NewRecorder()
+ p.proxifyMux.ServeHTTP(rec, req)
+ resp := rec.Result()
+ resp.Close = true
+ if err := resp.Write(brw); err != nil {
+ gologger.Warning().Msgf("failed to write response: %v", err)
}
-
- proxy.socks5proxy = socks5proxy
-
- return proxy, nil
-}
-
-func (p *Proxy) httpTunnelDialer(ctx context.Context, network, addr string) (net.Conn, error) {
- return p.socks5tunnel.MakeTunnel(nil, nil, p.bufioPool, addr)
+ brw.Flush()
+ return nil
}
-func serveWebPage(l net.Listener) error {
+func getProxifyServerMux() (*http.ServeMux, error) {
cwd, err := os.Getwd()
if err != nil {
- return fmt.Errorf("failed to get current working directory: %v", err)
+ return nil, fmt.Errorf("failed to get current working directory: %v", err)
}
absStaticDirPath := strings.Join([]string{strings.Split(cwd, "cmd")[0], "static"}, "/")
@@ -505,20 +547,16 @@ func serveWebPage(l net.Listener) error {
buffer, err := certs.GetRawCA()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- gologger.Error().Msgf("failed to get raw CA: %v", err)
+ gologger.Warning().Msgf("failed to get raw CA: %v", err)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=\"proxify.pem\"")
if _, err := w.Write(buffer.Bytes()); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- gologger.Error().Msgf("failed to write raw CA: %v", err)
+ gologger.Warning().Msgf("failed to write raw CA: %v", err)
return
}
})
-
- server := &http.Server{
- Handler: mux,
- }
- return server.Serve(l)
+ return mux, nil
}