improve request logging (closer to Apache Common Log)
This commit is contained in:
parent
c0160c1e4c
commit
b9b5e817fc
@ -135,3 +135,11 @@ Google Auth Proxy responds directly to the following endpoints. All other endpoi
|
|||||||
* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
|
* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
|
||||||
* /oauth2/start - a URL that will redirect to start the OAuth cycle
|
* /oauth2/start - a URL that will redirect to start the OAuth cycle
|
||||||
* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this ass the callback url.
|
* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this ass the callback url.
|
||||||
|
|
||||||
|
## Logging Format
|
||||||
|
|
||||||
|
Google Auth Proxy logs requests to stdout in a format similar to Apache Combined Log.
|
||||||
|
|
||||||
|
```
|
||||||
|
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
|
||||||
|
````
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
# "http://127.0.0.1:8080/"
|
# "http://127.0.0.1:8080/"
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
|
## Log requests to stdout
|
||||||
|
# request_logging = true
|
||||||
|
|
||||||
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
|
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
|
||||||
# pass_basic_auth = true
|
# pass_basic_auth = true
|
||||||
## pass the request Host Header to upstream
|
## pass the request Host Header to upstream
|
||||||
@ -51,3 +54,4 @@
|
|||||||
# cookie_expire = "168h"
|
# cookie_expire = "168h"
|
||||||
# cookie_secure = true
|
# cookie_secure = true
|
||||||
# cookie_httponly = true
|
# cookie_httponly = true
|
||||||
|
|
||||||
|
132
logging_handler.go
Normal file
132
logging_handler.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// largely adapted from https://github.com/gorilla/handlers/blob/master/handlers.go
|
||||||
|
// to add logging of request duration as last value (and drop referrer)
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
|
||||||
|
// code and body size
|
||||||
|
type responseLogger struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
status int
|
||||||
|
size int
|
||||||
|
upstream string
|
||||||
|
authInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) Header() http.Header {
|
||||||
|
return l.w.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) ExtractGAPMetadata() {
|
||||||
|
upstream := l.w.Header().Get("GAP-Upstream-Address")
|
||||||
|
if upstream != "" {
|
||||||
|
l.upstream = upstream
|
||||||
|
l.w.Header().Del("GAP-Upstream-Address")
|
||||||
|
}
|
||||||
|
authInfo := l.w.Header().Get("GAP-Auth")
|
||||||
|
if authInfo != "" {
|
||||||
|
l.authInfo = authInfo
|
||||||
|
l.w.Header().Del("GAP-Auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) Write(b []byte) (int, error) {
|
||||||
|
if l.status == 0 {
|
||||||
|
// The status will be StatusOK if WriteHeader has not been called yet
|
||||||
|
l.status = http.StatusOK
|
||||||
|
}
|
||||||
|
l.ExtractGAPMetadata()
|
||||||
|
size, err := l.w.Write(b)
|
||||||
|
l.size += size
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) WriteHeader(s int) {
|
||||||
|
l.ExtractGAPMetadata()
|
||||||
|
l.w.WriteHeader(s)
|
||||||
|
l.status = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) Status() int {
|
||||||
|
return l.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *responseLogger) Size() int {
|
||||||
|
return l.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends
|
||||||
|
type loggingHandler struct {
|
||||||
|
writer io.Writer
|
||||||
|
handler http.Handler
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggingHandler(out io.Writer, h http.Handler, v bool) http.Handler {
|
||||||
|
return loggingHandler{out, h, v}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
t := time.Now()
|
||||||
|
url := *req.URL
|
||||||
|
logger := &responseLogger{w: w}
|
||||||
|
h.handler.ServeHTTP(logger, req)
|
||||||
|
if !h.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logLine := buildLogLine(logger.authInfo, logger.upstream, req, url, t, logger.Status(), logger.Size())
|
||||||
|
h.writer.Write(logLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log entry for req similar to Apache Common Log Format.
|
||||||
|
// ts is the timestamp with which the entry should be logged.
|
||||||
|
// status, size are used to provide the response HTTP status and size.
|
||||||
|
func buildLogLine(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
|
||||||
|
if username == "" {
|
||||||
|
username = "-"
|
||||||
|
}
|
||||||
|
if upstream == "" {
|
||||||
|
upstream = "-"
|
||||||
|
}
|
||||||
|
if url.User != nil && username == "-" {
|
||||||
|
if name := url.User.Username(); name != "" {
|
||||||
|
username = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := req.Header.Get("X-Real-IP")
|
||||||
|
if client == "" {
|
||||||
|
client = req.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, _, err := net.SplitHostPort(client); err == nil {
|
||||||
|
client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := float64(time.Now().Sub(ts)) / float64(time.Second)
|
||||||
|
|
||||||
|
logLine := fmt.Sprintf("%s - %s [%s] %s %s %s %q %s %q %d %d %0.3f\n",
|
||||||
|
client,
|
||||||
|
username,
|
||||||
|
ts.Format("02/Jan/2006:15:04:05 -0700"),
|
||||||
|
req.Host,
|
||||||
|
req.Method,
|
||||||
|
upstream,
|
||||||
|
url.RequestURI(),
|
||||||
|
req.Proto,
|
||||||
|
req.UserAgent(),
|
||||||
|
status,
|
||||||
|
size,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
return []byte(logLine)
|
||||||
|
}
|
4
main.go
4
main.go
@ -47,6 +47,8 @@ func main() {
|
|||||||
flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
|
flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
|
||||||
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
|
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
|
||||||
|
|
||||||
|
flagSet.Bool("request-logging", true, "Log requests to stdout")
|
||||||
|
|
||||||
flagSet.Parse(os.Args[1:])
|
flagSet.Parse(os.Args[1:])
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
@ -112,7 +114,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Printf("listening on %s", listenAddr)
|
log.Printf("listening on %s", listenAddr)
|
||||||
|
|
||||||
server := &http.Server{Handler: oauthproxy}
|
server := &http.Server{Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging)}
|
||||||
err = server.Serve(listener)
|
err = server.Serve(listener)
|
||||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
log.Printf("ERROR: http.Serve() - %s", err)
|
log.Printf("ERROR: http.Serve() - %s", err)
|
||||||
|
@ -42,13 +42,23 @@ type OauthProxy struct {
|
|||||||
SignInMessage string
|
SignInMessage string
|
||||||
HtpasswdFile *HtpasswdFile
|
HtpasswdFile *HtpasswdFile
|
||||||
DisplayHtpasswdForm bool
|
DisplayHtpasswdForm bool
|
||||||
serveMux *http.ServeMux
|
serveMux http.Handler
|
||||||
PassBasicAuth bool
|
PassBasicAuth bool
|
||||||
skipAuthRegex []string
|
skipAuthRegex []string
|
||||||
compiledRegex []*regexp.Regexp
|
compiledRegex []*regexp.Regexp
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpstreamProxy struct {
|
||||||
|
upstream string
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("GAP-Upstream-Address", u.upstream)
|
||||||
|
u.handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
|
func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {
|
||||||
return httputil.NewSingleHostReverseProxy(target)
|
return httputil.NewSingleHostReverseProxy(target)
|
||||||
}
|
}
|
||||||
@ -85,7 +95,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
|
|||||||
} else {
|
} else {
|
||||||
setProxyDirector(proxy)
|
setProxyDirector(proxy)
|
||||||
}
|
}
|
||||||
serveMux.Handle(path, proxy)
|
serveMux.Handle(path, &UpstreamProxy{u.Host, proxy})
|
||||||
}
|
}
|
||||||
for _, u := range opts.CompiledRegex {
|
for _, u := range opts.CompiledRegex {
|
||||||
log.Printf("compiled skip-auth-regex => %q", u)
|
log.Printf("compiled skip-auth-regex => %q", u)
|
||||||
@ -338,7 +348,7 @@ func (p *OauthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (st
|
|||||||
}
|
}
|
||||||
// check auth
|
// check auth
|
||||||
if p.HtpasswdFile.Validate(user, passwd) {
|
if p.HtpasswdFile.Validate(user, passwd) {
|
||||||
log.Printf("authenticated %s via manual sign in", user)
|
log.Printf("authenticated %q via HtpasswdFile", user)
|
||||||
return user, true
|
return user, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
@ -366,7 +376,6 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if req.Header.Get("X-Real-IP") != "" {
|
if req.Header.Get("X-Real-IP") != "" {
|
||||||
remoteAddr += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
|
remoteAddr += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
|
||||||
}
|
}
|
||||||
log.Printf("%s %s %s", remoteAddr, req.Method, req.URL.RequestURI())
|
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
var user string
|
var user string
|
||||||
@ -461,7 +470,6 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("%s - invalid cookie session", remoteAddr)
|
|
||||||
p.SignInPage(rw, req, 403)
|
p.SignInPage(rw, req, 403)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -472,6 +480,11 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
req.Header["X-Forwarded-User"] = []string{user}
|
req.Header["X-Forwarded-User"] = []string{user}
|
||||||
req.Header["X-Forwarded-Email"] = []string{email}
|
req.Header["X-Forwarded-Email"] = []string{email}
|
||||||
}
|
}
|
||||||
|
if email == "" {
|
||||||
|
rw.Header().Set("GAP-Auth", user)
|
||||||
|
} else {
|
||||||
|
rw.Header().Set("GAP-Auth", email)
|
||||||
|
}
|
||||||
|
|
||||||
p.serveMux.ServeHTTP(rw, req)
|
p.serveMux.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
@ -493,7 +506,7 @@ func (p *OauthProxy) CheckBasicAuth(req *http.Request) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if p.HtpasswdFile.Validate(pair[0], pair[1]) {
|
if p.HtpasswdFile.Validate(pair[0], pair[1]) {
|
||||||
log.Printf("authenticated %s via basic auth", pair[0])
|
log.Printf("authenticated %q via basic auth", pair[0])
|
||||||
return pair[0], true
|
return pair[0], true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
|
@ -33,6 +33,8 @@ type Options struct {
|
|||||||
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
|
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
|
||||||
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
|
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
|
||||||
|
|
||||||
|
RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
|
||||||
|
|
||||||
// internal values that are set after config validation
|
// internal values that are set after config validation
|
||||||
redirectUrl *url.URL
|
redirectUrl *url.URL
|
||||||
proxyUrls []*url.URL
|
proxyUrls []*url.URL
|
||||||
@ -49,6 +51,7 @@ func NewOptions() *Options {
|
|||||||
CookieExpire: time.Duration(168) * time.Hour,
|
CookieExpire: time.Duration(168) * time.Hour,
|
||||||
PassBasicAuth: true,
|
PassBasicAuth: true,
|
||||||
PassHostHeader: true,
|
PassHostHeader: true,
|
||||||
|
RequestLogging: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user