diff --git a/env_options.go b/env_options.go index b918428e..d54f5af2 100644 --- a/env_options.go +++ b/env_options.go @@ -6,8 +6,14 @@ import ( "strings" ) +// EnvOptions holds program options loaded from the process environment type EnvOptions map[string]interface{} +// LoadEnvForStruct loads environment variables for each field in an options +// struct passed into it. +// +// Fields in the options struct must have an `env` and `cfg` tag to be read +// from the environment func (cfg EnvOptions) LoadEnvForStruct(options interface{}) { val := reflect.ValueOf(options).Elem() typ := val.Type() diff --git a/htpasswd.go b/htpasswd.go index 9a0c5049..1fe21cde 100644 --- a/htpasswd.go +++ b/htpasswd.go @@ -14,10 +14,12 @@ import ( // Lookup passwords in a htpasswd file // Passwords must be generated with -B for bcrypt or -s for SHA1. +// HtpasswdFile represents the structure of an htpasswd file type HtpasswdFile struct { Users map[string]string } +// NewHtpasswdFromFile constructs an HtpasswdFile from the file at the path given func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) { r, err := os.Open(path) if err != nil { @@ -27,6 +29,7 @@ func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) { return NewHtpasswd(r) } +// NewHtpasswd consctructs an HtpasswdFile from an io.Reader (opened file) func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) { csvReader := csv.NewReader(file) csvReader.Comma = ':' @@ -44,6 +47,7 @@ func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) { return h, nil } +// Validate checks a users password against the HtpasswdFile entries func (h *HtpasswdFile) Validate(user string, password string) bool { realPassword, exists := h.Users[user] if !exists { diff --git a/http.go b/http.go index 6b0012cd..4456e396 100644 --- a/http.go +++ b/http.go @@ -9,11 +9,13 @@ import ( "time" ) +// Server represents an HTTP server type Server struct { Handler http.Handler Opts *Options } +// ListenAndServe will serve traffic on HTTP or HTTPS depending on TLS options func (s *Server) ListenAndServe() { if s.Opts.TLSKeyFile != "" || s.Opts.TLSCertFile != "" { s.ServeHTTPS() @@ -22,9 +24,10 @@ func (s *Server) ListenAndServe() { } } +// ServeHTTP constructs a net.Listener and starts handling HTTP requests func (s *Server) ServeHTTP() { HTTPAddress := s.Opts.HTTPAddress - scheme := "" + var scheme string i := strings.Index(HTTPAddress, "://") if i > -1 { @@ -57,6 +60,7 @@ func (s *Server) ServeHTTP() { log.Printf("HTTP: closing %s", listener.Addr()) } +// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests func (s *Server) ServeHTTPS() { addr := s.Opts.HTTPSAddress config := &tls.Config{ diff --git a/logging_handler.go b/logging_handler.go index 540b5409..8db2d3eb 100644 --- a/logging_handler.go +++ b/logging_handler.go @@ -27,10 +27,13 @@ type responseLogger struct { authInfo string } +// Header returns the ResponseWriter's Header func (l *responseLogger) Header() http.Header { return l.w.Header() } +// ExtractGAPMetadata extracts and removes GAP headers from the ResponseWriter's +// Header func (l *responseLogger) ExtractGAPMetadata() { upstream := l.w.Header().Get("GAP-Upstream-Address") if upstream != "" { @@ -44,6 +47,7 @@ func (l *responseLogger) ExtractGAPMetadata() { } } +// Write writes the response using the ResponseWriter func (l *responseLogger) Write(b []byte) (int, error) { if l.status == 0 { // The status will be StatusOK if WriteHeader has not been called yet @@ -55,16 +59,19 @@ func (l *responseLogger) Write(b []byte) (int, error) { return size, err } +// WriteHeader writes the status code for the Response func (l *responseLogger) WriteHeader(s int) { l.ExtractGAPMetadata() l.w.WriteHeader(s) l.status = s } +// Status returns the response status code func (l *responseLogger) Status() int { return l.status } +// Size returns teh response size func (l *responseLogger) Size() int { return l.size } @@ -94,6 +101,7 @@ type loggingHandler struct { logTemplate *template.Template } +// LoggingHandler provides an http.Handler which logs requests to the HTTP server func LoggingHandler(out io.Writer, h http.Handler, v bool, requestLoggingTpl string) http.Handler { return loggingHandler{ writer: out, diff --git a/oauthproxy.go b/oauthproxy.go index 9972d83f..cc5fa141 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -20,12 +20,16 @@ import ( ) const ( + // SignatureHeader is the name of the request header containing the GAP Signature + // Part of hmacauth SignatureHeader = "GAP-Signature" httpScheme = "http" httpsScheme = "https" ) +// SignatureHeaders contains the headers to be signed by the hmac algorithm +// Part of hmacauth var SignatureHeaders = []string{ "Content-Length", "Content-Md5", @@ -39,6 +43,7 @@ var SignatureHeaders = []string{ "Gap-Auth", } +// OAuthProxy is the main authentication proxy type OAuthProxy struct { CookieSeed string CookieName string @@ -79,12 +84,15 @@ type OAuthProxy struct { Footer string } +// UpstreamProxy represents an upstream server to proxy to type UpstreamProxy struct { upstream string handler http.Handler auth hmacauth.HmacAuth } +// ServeHTTP proxies requests to the upstream provider while signing the +// request headers func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("GAP-Upstream-Address", u.upstream) if u.auth != nil { @@ -94,9 +102,12 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { u.handler.ServeHTTP(w, r) } +// NewReverseProxy creates a new reverse proxy for proxying requests to upstream +// servers func NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) { return httputil.NewSingleHostReverseProxy(target) } + func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) { director := proxy.Director proxy.Director = func(req *http.Request) { @@ -107,6 +118,7 @@ func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) { req.URL.RawQuery = "" } } + func setProxyDirector(proxy *httputil.ReverseProxy) { director := proxy.Director proxy.Director = func(req *http.Request) { @@ -116,10 +128,13 @@ func setProxyDirector(proxy *httputil.ReverseProxy) { req.URL.RawQuery = "" } } + +// NewFileServer creates a http.Handler to serve files from the filesystem func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) } +// NewOAuthProxy creates a new instance of OOuthProxy from the options provided func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { serveMux := http.NewServeMux() var auth hmacauth.HmacAuth @@ -214,6 +229,8 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { } } +// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will +// redirect clients to once authenticated func (p *OAuthProxy) GetRedirectURI(host string) string { // default to the request Host if not set if p.redirectURL.Host != "" { @@ -259,6 +276,8 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e return } +// MakeSessionCookie creates an http.Cookie containing the authenticated user's +// authentication details func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { if value != "" { value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now) @@ -270,6 +289,7 @@ func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expirati return p.makeCookie(req, p.CookieName, value, expiration, now) } +// MakeCSRFCookie creates a cookie for CSRF func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { return p.makeCookie(req, p.CSRFCookieName, value, expiration, now) } @@ -296,14 +316,19 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex } } +// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's +// session func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) { http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now())) } +// SetCSRFCookie adds a CSRF cookie to the response func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) { http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now())) } +// ClearSessionCookie creates a cookie to unset the user's authentication cookie +// stored in the user's session func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) { clr := p.MakeSessionCookie(req, "", time.Hour*-1, time.Now()) http.SetCookie(rw, clr) @@ -316,10 +341,12 @@ func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Reques } } +// SetSessionCookie adds the user's session cookie to the response func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) { http.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now())) } +// LoadCookiedSession reads the user's authentication details from the request func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) { var age time.Duration c, err := req.Cookie(p.CookieName) @@ -341,6 +368,7 @@ func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionSt return session, age, nil } +// SaveSession creates a new session cookie value and sets this on the response func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error { value, err := p.provider.CookieForSession(s, p.CookieCipher) if err != nil { @@ -350,16 +378,19 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *p return nil } +// RobotsTxt disallows scraping pages from the OAuthProxy func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "User-agent: *\nDisallow: /") } +// PingPage responds 200 OK to requests func (p *OAuthProxy) PingPage(rw http.ResponseWriter) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "OK") } +// ErrorPage writes an error response func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) { log.Printf("ErrorPage %d %s %s", code, title, message) rw.WriteHeader(code) @@ -375,6 +406,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m p.templates.ExecuteTemplate(rw, "error.html", t) } +// SignInPage writes the sing in template to the response func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) { p.ClearSessionCookie(rw, req) rw.WriteHeader(code) @@ -407,6 +439,7 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code p.templates.ExecuteTemplate(rw, "sign_in.html", t) } +// ManualSignIn handles basic auth logins to the proxy func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) { if req.Method != "POST" || p.HtpasswdFile == nil { return "", false @@ -424,6 +457,8 @@ func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (st return "", false } +// GetRedirect reads the query parameter to get the URL to redirect clients to +// once authenticated with the OAuthProxy func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) { err = req.ParseForm() if err != nil { @@ -438,11 +473,13 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) return } +// IsWhitelistedRequest is used to check if auth should be skipped for this request func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) { isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) } +// IsWhitelistedPath is used to check if the request path is allowed without auth func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) { for _, u := range p.compiledRegex { ok = u.MatchString(path) @@ -484,6 +521,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } +// SignIn serves a page prompting users to sign in func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { redirect, err := p.GetRedirect(req) if err != nil { @@ -505,11 +543,13 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { } } +// SignOut sends a response to clear the authentication cookie func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { p.ClearSessionCookie(rw, req) http.Redirect(rw, req, "/", 302) } +// OAuthStart starts the OAuth2 authentication flow func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { nonce, err := cookie.Nonce() if err != nil { @@ -526,6 +566,8 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302) } +// OAuthCallback is the OAuth2 authentication flow callback that finishes the +// OAuth2 authentication flow func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { remoteAddr := getRemoteAddr(req) @@ -587,6 +629,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } } +// AuthenticateOnly checks whether the user is currently logged in func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { status := p.Authenticate(rw, req) if status == http.StatusAccepted { @@ -596,6 +639,8 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) } } +// Proxy proxies the user request if the user is authenticated else it prompts +// them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { status := p.Authenticate(rw, req) if status == http.StatusInternalServerError { @@ -612,6 +657,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { } } +// Authenticate checks whether a user is authenticated func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int { var saveSession, clearSession, revalidated bool remoteAddr := getRemoteAddr(req) @@ -711,6 +757,8 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int return http.StatusAccepted } +// CheckBasicAuth checks the requests Authorization header for basic auth +// credentials and authenticates these against the proxies HtpasswdFile func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) { if p.HtpasswdFile == nil { return nil, nil diff --git a/options.go b/options.go index 4120587d..bb412d20 100644 --- a/options.go +++ b/options.go @@ -89,11 +89,13 @@ type Options struct { oidcVerifier *oidc.IDTokenVerifier } +// SignatureData holds hmacauth signature hash and key type SignatureData struct { hash crypto.Hash key string } +// NewOptions constructs a new Options with defaulted values func NewOptions() *Options { return &Options{ ProxyPrefix: "/oauth2", @@ -126,6 +128,8 @@ func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string return parsed, msgs } +// Validate checks that required options are set and validates those that they +// are of the correct format func (o *Options) Validate() error { if o.SSLInsecureSkipVerify { // TODO: Accept a certificate bundle. diff --git a/string_array.go b/string_array.go index 10f0ce34..5a624bee 100644 --- a/string_array.go +++ b/string_array.go @@ -4,13 +4,16 @@ import ( "strings" ) +// StringArray is a type alias for a slice of strings type StringArray []string +// Set appends a string to the StringArray func (a *StringArray) Set(s string) error { *a = append(*a, s) return nil } +// String joins elements of the StringArray into a single comma separated string func (a *StringArray) String() string { return strings.Join(*a, ",") } diff --git a/validator.go b/validator.go index df25f358..67e19d3d 100644 --- a/validator.go +++ b/validator.go @@ -10,11 +10,13 @@ import ( "unsafe" ) +// UserMap holds information from the authenticated emails file type UserMap struct { usersFile string m unsafe.Pointer } +// NewUserMap parses the authenticated emails file into a new UserMap func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap { um := &UserMap{usersFile: usersFile} m := make(map[string]bool) @@ -30,12 +32,15 @@ func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap { return um } +// IsValid checks if an email is allowed func (um *UserMap) IsValid(email string) (result bool) { m := *(*map[string]bool)(atomic.LoadPointer(&um.m)) _, result = m[email] return } +// LoadAuthenticatedEmailsFile loads the authenticated emails file from disk +// and parses the contents as CSV func (um *UserMap) LoadAuthenticatedEmailsFile() { r, err := os.Open(um.usersFile) if err != nil { @@ -91,6 +96,7 @@ func newValidatorImpl(domains []string, usersFile string, return validator } +// NewValidator constructs a function to validate email addresses func NewValidator(domains []string, usersFile string) func(string) bool { return newValidatorImpl(domains, usersFile, nil, func() {}) } diff --git a/version.go b/version.go index e5b063a1..c89a1f68 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,4 @@ package main +// VERSION contains version information const VERSION = "2.2.1-alpha" diff --git a/watcher.go b/watcher.go index 6cda7d9c..28cd4c2a 100644 --- a/watcher.go +++ b/watcher.go @@ -11,6 +11,8 @@ import ( fsnotify "gopkg.in/fsnotify/fsnotify.v1" ) +// WaitForReplacement waits for a file to exist on disk and then starts a watch +// for the file func WaitForReplacement(filename string, op fsnotify.Op, watcher *fsnotify.Watcher) { const sleepInterval = 50 * time.Millisecond @@ -30,6 +32,7 @@ func WaitForReplacement(filename string, op fsnotify.Op, } } +// WatchForUpdates performs an action every time a file on disk is updated func WatchForUpdates(filename string, done <-chan bool, action func()) { filename = filepath.Clean(filename) watcher, err := fsnotify.NewWatcher()