Skip to content

Commit 3923363

Browse files
authored
Merge pull request #147 from h4sh5/log-user-agent
Add option to log user agent
2 parents be72262 + 8dc23e2 commit 3923363

File tree

9 files changed

+188
-4
lines changed

9 files changed

+188
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
*.exe
22
cmd/simplehttpserver/simplehttpserver
3+
simplehttpserver

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ SimpleHTTPserver is a go enhanced version of the well known python simplehttpser
2929
- HTTP/S Web Server
3030
- File Server with arbitrary directory support
3131
- HTTP request/response dump
32+
- JSON request logging to file
3233
- Configurable ip address and listening port
3334
- Configurable HTTP/TCP server with customizable response via YAML template
3435

@@ -72,6 +73,7 @@ This will display help for the tool. Here are all the switches it supports.
7273
| `-silent` | Show only results | `simplehttpserver -silent` |
7374
| `-py` | Emulate Python Style | `simplehttpserver -py` |
7475
| `-header` | HTTP response header (can be used multiple times) | `simplehttpserver -header 'X-Powered-By: Go'` |
76+
| `-json-log` | JSON log file path for request logging | `simplehttpserver -json-log /tmp/requests.json` |
7577

7678
### Running simplehttpserver in the current folder
7779

@@ -121,12 +123,45 @@ To upload files use the following curl request with basic auth header:
121123
curl -v --user 'root:root' --upload-file file.txt http://localhost:8000/file.txt
122124
```
123125

126+
### Running simplehttpserver with JSON request logging
127+
128+
This will run the tool and log all HTTP requests to a JSON file:
129+
130+
```sh
131+
simplehttpserver -json-log /tmp/requests.json
132+
133+
2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/...
134+
```
135+
136+
The JSON log file will contain structured request data including:
137+
- Timestamp, remote address, HTTP method, URL, protocol
138+
- Status code, response size, user agent
139+
- Request headers, request body, response body
140+
141+
Example JSON log entry:
142+
```json
143+
{
144+
"timestamp": "2021-01-11T21:41:15Z",
145+
"remote_addr": "127.0.0.1:50181",
146+
"method": "GET",
147+
"url": "/",
148+
"proto": "HTTP/1.1",
149+
"status_code": 200,
150+
"size": 383,
151+
"user_agent": "curl/7.68.0",
152+
"headers": {
153+
"Accept": "*/*",
154+
"User-Agent": "curl/7.68.0"
155+
}
156+
}
157+
```
158+
124159
### Running TCP server with custom responses
125160

126161
This will run the tool as TLS TCP server and enable custom responses based on YAML templates:
127162

128163
```sh
129-
simplehttpserver -rule rules.yaml -tcp -tls -domain localhost
164+
simplehttpserver -rules rules.yaml -tcp -tls -domain localhost
130165
```
131166

132167
The rules are written as follows:

internal/runner/options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Options struct {
2828
Verbose bool
2929
EnableUpload bool
3030
EnableTCP bool
31+
LogUA bool
3132
RulesFile string
3233
TCPWithTLS bool
3334
Version bool
@@ -39,6 +40,7 @@ type Options struct {
3940
Python bool
4041
CORS bool
4142
HTTPHeaders HTTPHeaders
43+
JSONLogFile string
4244
}
4345

4446
// ParseOptions parses the command line options for application
@@ -76,11 +78,13 @@ func ParseOptions() *Options {
7678
flagSet.BoolVar(&options.Python, "py", false, "Emulate Python Style"),
7779
flagSet.BoolVar(&options.CORS, "cors", false, "Enable Cross-Origin Resource Sharing (CORS)"),
7880
flagSet.Var(&options.HTTPHeaders, "header", "Add HTTP Response Header (name: value), can be used multiple times"),
81+
flagSet.StringVar(&options.JSONLogFile, "json-log", "", "JSON log file path for request logging"),
7982
)
8083

8184
flagSet.CreateGroup("debug", "Debug",
8285
flagSet.BoolVar(&options.Version, "version", false, "Show version of the software"),
8386
flagSet.BoolVar(&options.Verbose, "verbose", false, "Verbose"),
87+
flagSet.BoolVar(&options.LogUA, "log-ua", false, "Log User Agent"),
8488
)
8589

8690
if err := flagSet.Parse(); err != nil {

internal/runner/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func New(options *Options) (*Runner, error) {
5959
httpServer, err := httpserver.New(&httpserver.Options{
6060
Folder: r.options.Folder,
6161
EnableUpload: r.options.EnableUpload,
62+
LogUA: r.options.LogUA,
6263
ListenAddress: r.options.ListenAddress,
6364
TLS: r.options.HTTPS,
6465
Certificate: r.options.TLSCertificate,
@@ -75,6 +76,7 @@ func New(options *Options) (*Runner, error) {
7576
Python: r.options.Python,
7677
CORS: r.options.CORS,
7778
HTTPHeaders: r.options.HTTPHeaders,
79+
JSONLogFile: r.options.JSONLogFile,
7880
})
7981
if err != nil {
8082
return nil, err

pkg/httpserver/httpserver.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
type Options struct {
1515
Folder string
1616
EnableUpload bool
17+
LogUA bool // enable logging user agent
1718
ListenAddress string
1819
TLS bool
1920
Certificate string
@@ -30,12 +31,14 @@ type Options struct {
3031
Python bool
3132
CORS bool
3233
HTTPHeaders []HTTPHeader
34+
JSONLogFile string
3335
}
3436

3537
// HTTPServer instance
3638
type HTTPServer struct {
37-
options *Options
38-
layers http.Handler
39+
options *Options
40+
layers http.Handler
41+
jsonLogger *JSONLogger
3942
}
4043

4144
// LayerHandler is the interface of all layer funcs
@@ -46,6 +49,16 @@ func New(options *Options) (*HTTPServer, error) {
4649
var h HTTPServer
4750
EnableUpload = options.EnableUpload
4851
EnableVerbose = options.Verbose
52+
EnableLogUA = options.LogUA
53+
54+
// Initialize JSON logger if specified
55+
if options.JSONLogFile != "" {
56+
jsonLogger, err := NewJSONLogger(options.JSONLogFile)
57+
if err != nil {
58+
return nil, err
59+
}
60+
h.jsonLogger = jsonLogger
61+
}
4962
folder, err := filepath.Abs(options.Folder)
5063
if err != nil {
5164
return nil, err
@@ -127,5 +140,8 @@ func (t *HTTPServer) ListenAndServeTLS() error {
127140

128141
// Close the service
129142
func (t *HTTPServer) Close() error {
143+
if t.jsonLogger != nil {
144+
return t.jsonLogger.Close()
145+
}
130146
return nil
131147
}

pkg/httpserver/jsonlogger.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package httpserver
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"sync"
9+
"time"
10+
)
11+
12+
// JSONLogEntry represents a single HTTP request log entry in JSON format
13+
type JSONLogEntry struct {
14+
Timestamp string `json:"timestamp"`
15+
RemoteAddr string `json:"remote_addr"`
16+
Method string `json:"method"`
17+
URL string `json:"url"`
18+
Proto string `json:"proto"`
19+
StatusCode int `json:"status_code"`
20+
Size int `json:"size"`
21+
UserAgent string `json:"user_agent,omitempty"`
22+
Headers map[string]string `json:"headers,omitempty"`
23+
RequestBody string `json:"request_body,omitempty"`
24+
ResponseBody string `json:"response_body,omitempty"`
25+
}
26+
27+
// JSONLogger handles writing JSON log entries to a file
28+
type JSONLogger struct {
29+
file *os.File
30+
mutex sync.Mutex
31+
enable bool
32+
}
33+
34+
// NewJSONLogger creates a new JSON logger instance
35+
func NewJSONLogger(filePath string) (*JSONLogger, error) {
36+
if filePath == "" {
37+
return &JSONLogger{enable: false}, nil
38+
}
39+
40+
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to open JSON log file: %w", err)
43+
}
44+
45+
return &JSONLogger{
46+
file: file,
47+
enable: true,
48+
}, nil
49+
}
50+
51+
// LogRequest logs an HTTP request to the JSON file
52+
func (jl *JSONLogger) LogRequest(r *http.Request, statusCode, size int, userAgent string, headers map[string]string, requestBody, responseBody string) error {
53+
if !jl.enable {
54+
return nil
55+
}
56+
57+
entry := JSONLogEntry{
58+
Timestamp: time.Now().Format(time.RFC3339),
59+
RemoteAddr: r.RemoteAddr,
60+
Method: r.Method,
61+
URL: r.URL.String(),
62+
Proto: r.Proto,
63+
StatusCode: statusCode,
64+
Size: size,
65+
UserAgent: userAgent,
66+
Headers: headers,
67+
RequestBody: requestBody,
68+
ResponseBody: responseBody,
69+
}
70+
71+
jl.mutex.Lock()
72+
defer jl.mutex.Unlock()
73+
74+
jsonData, err := json.Marshal(entry)
75+
if err != nil {
76+
return fmt.Errorf("failed to marshal JSON log entry: %w", err)
77+
}
78+
79+
_, err = jl.file.Write(append(jsonData, '\n'))
80+
if err != nil {
81+
return fmt.Errorf("failed to write JSON log entry: %w", err)
82+
}
83+
84+
return nil
85+
}
86+
87+
// Close closes the JSON log file
88+
func (jl *JSONLogger) Close() error {
89+
if jl.file != nil {
90+
return jl.file.Close()
91+
}
92+
return nil
93+
}

pkg/httpserver/loglayer.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"net/http"
66
"net/http/httputil"
77
"time"
8+
89
"github.com/projectdiscovery/gologger"
910
)
1011

1112
// Convenience globals
1213
var (
1314
EnableUpload bool
1415
EnableVerbose bool
16+
EnableLogUA bool
1517
)
1618

1719
func (t *HTTPServer) shouldDumpBody(bodysize int64) bool {
@@ -29,12 +31,41 @@ func (t *HTTPServer) loglayer(handler http.Handler) http.Handler {
2931
lrw := newLoggingResponseWriter(w, t.options.MaxDumpBodySize)
3032
handler.ServeHTTP(lrw, r)
3133

34+
// Log to JSON file if JSON logger is enabled
35+
if t.jsonLogger != nil {
36+
// Extract headers
37+
headers := make(map[string]string)
38+
for name, values := range r.Header {
39+
if len(values) > 0 {
40+
headers[name] = values[0]
41+
}
42+
}
43+
44+
// Extract request body from fullRequest
45+
requestBody := ""
46+
if len(fullRequest) > 0 {
47+
// Find the double CRLF that separates headers from body
48+
bodyStart := bytes.Index(fullRequest, []byte("\r\n\r\n"))
49+
if bodyStart != -1 && bodyStart+4 < len(fullRequest) {
50+
requestBody = string(fullRequest[bodyStart+4:])
51+
}
52+
}
53+
54+
// Log to JSON file
55+
_ = t.jsonLogger.LogRequest(r, lrw.statusCode, lrw.Size, r.UserAgent(), headers, requestBody, string(lrw.Data))
56+
}
57+
58+
// Continue with existing console logging
3259
if EnableVerbose {
3360
headers := new(bytes.Buffer)
3461
lrw.Header().Write(headers) //nolint
3562
gologger.Print().Msgf("\n[%s]\nRemote Address: %s\n%s\n%s %d %s\n%s\n%s\n", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, string(fullRequest), r.Proto, lrw.statusCode, http.StatusText(lrw.statusCode), headers.String(), string(lrw.Data))
3663
} else {
37-
gologger.Print().Msgf("[%s] %s \"%s %s %s\" %d %d", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, lrw.Size)
64+
if EnableLogUA {
65+
gologger.Print().Msgf("[%s] %s \"%s %s %s\" %d %d - %s", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, lrw.Size, r.UserAgent())
66+
} else {
67+
gologger.Print().Msgf("[%s] %s \"%s %s %s\" %d %d", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, lrw.Size)
68+
}
3869
}
3970
})
4071
}

pkg/httpserver/pythonliststyle.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
<html>
1818
<head>
1919
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
20+
<meta name="viewport" content="width=device-width, initial-scale=1">
2021
<title>Directory listing for %s</title>
2122
</head>
2223
<body>

test/pythonliststyle_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestServePythonStyleHtmlPageForDirectories(t *testing.T) {
1515
<html>
1616
<head>
1717
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
18+
<meta name="viewport" content="width=device-width, initial-scale=1">
1819
<title>Directory listing for /</title>
1920
</head>
2021
<body>

0 commit comments

Comments
 (0)