Skip to content

Commit c3d817e

Browse files
committed
Add basic auth, header logging, auth logging
1 parent ff428f2 commit c3d817e

File tree

4 files changed

+126
-8
lines changed

4 files changed

+126
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
## Added
9+
- Basic auth support, can provide `--basic-auth 'username:password'` which the proxy then checks for valid auth
10+
provided in the `Proxy-Authentication` header.
11+
- Can choose if auth should be logged with the `--log-auth` option.
12+
- Can choose to log all request headers using the `--log-headers` option.
813

914
## [v1.0.0]
1015
### Added

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ Code based on the guide here: <https://medium.com/@mlowicki/http-s-proxy-in-gola
77

88
## Features
99

10-
- Supports HTTP and HTTPS.
11-
- Supports choosing which port.
12-
- Supports printing binary version number.
13-
- Supports specifying paths to certificate and private key file to use.
10+
- HTTP and HTTPS.
11+
- Can choose which port to run on.
12+
- Can specify paths to certificate and private key file to use.
1413
- Logs each proxied connection.
15-
- Supports log options can be supplied using `glog`.
14+
- Log options can be supplied using `glog`.
1615
- Can choose the log verbosity with the `-v` flag.
1716
- Can choose to log to a file.
17+
- Basic authentication.
18+
- Can log request headers.
19+
- Can log failed authentication attempt details.
20+
- Printing version number.
1821

1922
## Install
2023

@@ -63,10 +66,16 @@ The program has the following options, you can see this list by using the `--hel
6366
Usage of simple-proxy:
6467
-alsologtostderr
6568
log to standard error as well as files
69+
-basic-auth string
70+
basic auth, format 'username:password', no auth if not provided
6671
-cert string
6772
path to cert file
6873
-key string
6974
path to key file
75+
-log-auth
76+
log failed proxy auth details
77+
-log-headers
78+
log request headers
7079
-log_backtrace_at value
7180
when logging hits line file:N, emit a stack trace
7281
-log_dir string

main.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"log"
88
"net/http"
99
"os"
10+
"strings"
11+
"time"
1012

1113
"github.com/golang/glog"
1214
"github.com/jthomperoo/simple-proxy/proxy"
@@ -36,6 +38,12 @@ func main() {
3638
flag.StringVar(&certPath, "cert", "", "path to cert file")
3739
var keyPath string
3840
flag.StringVar(&keyPath, "key", "", "path to key file")
41+
var basicAuth string
42+
flag.StringVar(&basicAuth, "basic-auth", "", "basic auth, format 'username:password', no auth if not provided")
43+
var logAuth bool
44+
flag.BoolVar(&logAuth, "log-auth", false, "log failed proxy auth details")
45+
var logHeaders bool
46+
flag.BoolVar(&logHeaders, "log-headers", false, "log request headers")
3947
var timeoutSecs int
4048
flag.IntVar(&timeoutSecs, "timeout", 10, "timeout in seconds")
4149
flag.Parse()
@@ -50,12 +58,33 @@ func main() {
5058
}
5159

5260
if protocol == httpsProtocol && (certPath == "" || keyPath == "") {
53-
glog.Fatalf("If using HTTPS protocol --cert and --key are required")
61+
glog.Fatalf("If using HTTPS protocol --cert and --key are required\n")
62+
}
63+
64+
var handler http.Handler
65+
if basicAuth == "" {
66+
handler = &proxy.ProxyHandler{
67+
Timeout: time.Duration(timeoutSecs) * time.Second,
68+
LogAuth: logAuth,
69+
LogHeaders: logHeaders,
70+
}
71+
} else {
72+
parts := strings.Split(basicAuth, ":")
73+
if len(parts) < 2 {
74+
glog.Fatalf("Invalid basic auth provided, must be in format 'username:password', auth: %s\n", basicAuth)
75+
}
76+
handler = &proxy.ProxyHandler{
77+
Timeout: time.Duration(timeoutSecs) * time.Second,
78+
Username: &parts[0],
79+
Password: &parts[1],
80+
LogAuth: logAuth,
81+
LogHeaders: logHeaders,
82+
}
5483
}
5584

5685
server := &http.Server{
5786
Addr: fmt.Sprintf(":%s", port),
58-
Handler: proxy.NewProxyHandler(timeoutSecs),
87+
Handler: handler,
5988
// Disable HTTP/2.
6089
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
6190
}

proxy/proxy.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package proxy
22

33
import (
4+
"encoding/base64"
45
"io"
56
"net"
67
"net/http"
8+
"strings"
79
"time"
810

911
"github.com/golang/glog"
@@ -16,11 +18,34 @@ func NewProxyHandler(timeoutSeconds int) *ProxyHandler {
1618
}
1719

1820
type ProxyHandler struct {
19-
Timeout time.Duration
21+
Timeout time.Duration
22+
Username *string
23+
Password *string
24+
LogAuth bool
25+
LogHeaders bool
2026
}
2127

2228
func (p *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2329
glog.V(1).Infof("Serving '%s' request from '%s' to '%s'\n", r.Method, r.RemoteAddr, r.Host)
30+
if p.LogHeaders {
31+
for name, values := range r.Header {
32+
for i, value := range values {
33+
glog.V(1).Infof("'%s': [%d] %s", name, i, value)
34+
}
35+
}
36+
}
37+
if p.Username != nil && p.Password != nil {
38+
username, password, ok := proxyBasicAuth(r)
39+
if !ok || username != *p.Username || password != *p.Password {
40+
if p.LogAuth {
41+
glog.Errorf("Unauthorized, username: %s, password: %s\n", username, password)
42+
} else {
43+
glog.Errorln("Unauthorized")
44+
}
45+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
46+
return
47+
}
48+
}
2449
if r.Method == http.MethodConnect {
2550
handleTunneling(w, r, p.Timeout)
2651
} else {
@@ -77,3 +102,53 @@ func copyHeader(dst, src http.Header) {
77102
}
78103
}
79104
}
105+
106+
func proxyBasicAuth(r *http.Request) (username, password string, ok bool) {
107+
auth := r.Header.Get("Proxy-Authorization")
108+
if auth == "" {
109+
return
110+
}
111+
return parseBasicAuth(auth)
112+
}
113+
114+
// parseBasicAuth parses an HTTP Basic Authentication string.
115+
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
116+
func parseBasicAuth(auth string) (username, password string, ok bool) {
117+
const prefix = "Basic "
118+
// Case insensitive prefix match. See Issue 22736.
119+
if len(auth) < len(prefix) || !equalFold(auth[:len(prefix)], prefix) {
120+
return
121+
}
122+
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
123+
if err != nil {
124+
return
125+
}
126+
cs := string(c)
127+
s := strings.IndexByte(cs, ':')
128+
if s < 0 {
129+
return
130+
}
131+
return cs[:s], cs[s+1:], true
132+
}
133+
134+
// EqualFold is strings.EqualFold, ASCII only. It reports whether s and t
135+
// are equal, ASCII-case-insensitively.
136+
func equalFold(s, t string) bool {
137+
if len(s) != len(t) {
138+
return false
139+
}
140+
for i := 0; i < len(s); i++ {
141+
if lower(s[i]) != lower(t[i]) {
142+
return false
143+
}
144+
}
145+
return true
146+
}
147+
148+
// lower returns the ASCII lowercase version of b.
149+
func lower(b byte) byte {
150+
if 'A' <= b && b <= 'Z' {
151+
return b + ('a' - 'A')
152+
}
153+
return b
154+
}

0 commit comments

Comments
 (0)