outpost: rename proxy to outpost
This commit is contained in:
		
							
								
								
									
										2
									
								
								outpost/.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								outpost/.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Dockerfile.* | ||||
| .git | ||||
							
								
								
									
										2
									
								
								outpost/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								outpost/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| pkg/client/ | ||||
| pkg/models/ | ||||
							
								
								
									
										16
									
								
								outpost/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								outpost/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| FROM golang:1.15 AS builder | ||||
|  | ||||
| WORKDIR /work | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| RUN go build -o /work/proxy . | ||||
|  | ||||
| # Copy binary to alpine | ||||
| FROM gcr.io/distroless/base-debian10:debug | ||||
|  | ||||
| COPY --from=builder /work/proxy / | ||||
|  | ||||
| HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:4180/akprox/ping" ] | ||||
|  | ||||
| ENTRYPOINT ["/proxy"] | ||||
							
								
								
									
										15
									
								
								outpost/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								outpost/Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| all: clean generate build | ||||
|  | ||||
| generate: | ||||
| 	go get -u github.com/go-swagger/go-swagger/cmd/swagger | ||||
| 	swagger generate client -f ../swagger.yaml -A authentik -t pkg/ | ||||
|  | ||||
| run: | ||||
| 	go run -v . | ||||
|  | ||||
| clean: | ||||
| 	go mod tidy | ||||
| 	go clean . | ||||
|  | ||||
| build: | ||||
| 	go build -v . | ||||
							
								
								
									
										24
									
								
								outpost/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								outpost/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| # authentik Proxy | ||||
|  | ||||
| [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=3) | ||||
|  | ||||
|  | ||||
| Reverse Proxy based on [oauth2_proxy](https://github.com/oauth2-proxy/oauth2-proxy), completely managed and monitored by authentik. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| authentik Proxy is built to be configured by authentik itself, hence the only options you can directly give it are connection params. | ||||
|  | ||||
| The following environment variable are implemented: | ||||
|  | ||||
| `AUTHENTIK_HOST`: Full URL to the authentik instance with protocol, i.e. "https://authentik.company.tld" | ||||
|  | ||||
| `AUTHENTIK_TOKEN`: Token used to authenticate against authentik. This is generated after an Outpost instance is created. | ||||
|  | ||||
| `AUTHENTIK_INSECURE`: This environment variable can optionally be set to ignore the SSL Certificate of the authentik instance. Applies to both HTTP and WS connections. | ||||
|  | ||||
| ## Development | ||||
|  | ||||
| authentik Proxy uses an auto-generated API Client to communicate with authentik. This client is not kept in git. To generate the client locally, run `make generate`. | ||||
|  | ||||
| Afterwards you can build the proxy like any other Go project, using `go build`. | ||||
							
								
								
									
										104
									
								
								outpost/azure-pipelines.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								outpost/azure-pipelines.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| trigger: | ||||
|   - master | ||||
|  | ||||
| variables: | ||||
|   ${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}: | ||||
|     branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }} | ||||
|   ${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}: | ||||
|     branchName: $(System.PullRequest.SourceBranch) | ||||
|  | ||||
| stages: | ||||
|   - stage: generate | ||||
|     jobs: | ||||
|       - job: swagger_generate | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: CmdLine@2 | ||||
|             inputs: | ||||
|               script: | | ||||
|                 sudo apt install gnupg ca-certificates | ||||
|                 sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61 | ||||
|                 echo "deb https://dl.bintray.com/go-swagger/goswagger-debian ubuntu main" | sudo tee /etc/apt/sources.list.d/goswagger.list | ||||
|                 sudo apt update | ||||
|                 sudo apt install swagger | ||||
|                 mkdir -p $(go env GOPATH) | ||||
|                 swagger generate client -f ../swagger.yaml -A authentik -t pkg/ | ||||
|               workingDirectory: 'proxy/' | ||||
|           - task: PublishPipelineArtifact@1 | ||||
|             inputs: | ||||
|               targetPath: 'proxy/pkg/' | ||||
|               artifact: 'swagger_client' | ||||
|               publishLocation: 'pipeline' | ||||
|   - stage: lint | ||||
|     jobs: | ||||
|       - job: golint | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: Go@0 | ||||
|             inputs: | ||||
|               command: 'get' | ||||
|               arguments: '-u golang.org/x/lint/golint' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: CmdLine@2 | ||||
|             inputs: | ||||
|               script: | | ||||
|                 $(go list -f {{.Target}} golang.org/x/lint/golint) ./... | ||||
|               workingDirectory: 'proxy/' | ||||
|   - stage: build_go | ||||
|     jobs: | ||||
|       - job: build_go | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: Go@0 | ||||
|             inputs: | ||||
|               command: 'build' | ||||
|               workingDirectory: 'proxy/' | ||||
|   - stage: build_docker | ||||
|     jobs: | ||||
|       - job: build_proxy | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: Bash@3 | ||||
|             inputs: | ||||
|               targetType: 'inline' | ||||
|               script: | | ||||
|                 set -x | ||||
|                 echo '##vso[task.setvariable variable=branchName]$(echo "$(System.PullRequest.SourceBranch)" | sed "s/\//-/g")' | ||||
|           - task: Docker@2 | ||||
|             inputs: | ||||
|               containerRegistry: 'dockerhub' | ||||
|               repository: 'beryju/authentik-proxy' | ||||
|               command: 'buildAndPush' | ||||
|               Dockerfile: 'proxy/Dockerfile' | ||||
|               buildContext: 'proxy/' | ||||
|               tags: "gh-${{ variables.branchName }}" | ||||
							
								
								
									
										59
									
								
								outpost/cmd/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								outpost/cmd/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/server" | ||||
| ) | ||||
|  | ||||
| const helpMessage = `authentik proxy | ||||
|  | ||||
| Required environment variables: | ||||
|  - AUTHENTIK_HOST: URL to connect to (format "http://authentik.company") | ||||
|  - AUTHENTIK_TOKEN: Token to authenticate with | ||||
|  - AUTHENTIK_INSECURE: Skip SSL Certificate verification` | ||||
|  | ||||
| // RunServer main entrypoint, runs the full server | ||||
| func RunServer() { | ||||
| 	pbURL, found := os.LookupEnv("AUTHENTIK_HOST") | ||||
| 	if !found { | ||||
| 		fmt.Println("env AUTHENTIK_HOST not set!") | ||||
| 		fmt.Println(helpMessage) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	pbToken, found := os.LookupEnv("AUTHENTIK_TOKEN") | ||||
| 	if !found { | ||||
| 		fmt.Println("env AUTHENTIK_TOKEN not set!") | ||||
| 		fmt.Println(helpMessage) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	pbURLActual, err := url.Parse(pbURL) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		fmt.Println(helpMessage) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	rand.Seed(time.Now().UnixNano()) | ||||
|  | ||||
| 	ac := server.NewAPIController(*pbURLActual, pbToken) | ||||
|  | ||||
| 	interrupt := make(chan os.Signal, 1) | ||||
| 	signal.Notify(interrupt, os.Interrupt) | ||||
|  | ||||
| 	ac.Start() | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-interrupt: | ||||
| 			ac.Shutdown() | ||||
| 			os.Exit(0) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										40
									
								
								outpost/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								outpost/go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| module github.com/BeryJu/authentik/outpost | ||||
|  | ||||
| go 1.14 | ||||
|  | ||||
| require ( | ||||
| 	cloud.google.com/go v0.64.0 // indirect | ||||
| 	github.com/coreos/go-oidc v2.2.1+incompatible | ||||
| 	github.com/getsentry/sentry-go v0.9.0 | ||||
| 	github.com/go-openapi/errors v0.19.9 | ||||
| 	github.com/go-openapi/runtime v0.19.24 | ||||
| 	github.com/go-openapi/strfmt v0.19.11 | ||||
| 	github.com/go-openapi/swag v0.19.12 | ||||
| 	github.com/go-openapi/validate v0.20.0 | ||||
| 	github.com/go-redis/redis/v7 v7.4.0 // indirect | ||||
| 	github.com/go-swagger/go-swagger v0.25.0 // indirect | ||||
| 	github.com/gorilla/handlers v1.5.1 // indirect | ||||
| 	github.com/gorilla/websocket v1.4.2 | ||||
| 	github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a | ||||
| 	github.com/justinas/alice v1.2.0 | ||||
| 	github.com/kr/pretty v0.2.1 // indirect | ||||
| 	github.com/magiconair/properties v1.8.4 // indirect | ||||
| 	github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc | ||||
| 	github.com/pelletier/go-toml v1.8.1 // indirect | ||||
| 	github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect | ||||
| 	github.com/recws-org/recws v1.2.2 | ||||
| 	github.com/sirupsen/logrus v1.7.0 | ||||
| 	github.com/spf13/afero v1.5.1 // indirect | ||||
| 	github.com/spf13/cast v1.3.1 // indirect | ||||
| 	github.com/spf13/jwalterweatherman v1.1.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	github.com/spf13/viper v1.7.1 // indirect | ||||
| 	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect | ||||
| 	golang.org/x/mod v0.4.0 // indirect | ||||
| 	golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect | ||||
| 	golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect | ||||
| 	golang.org/x/text v0.3.5 // indirect | ||||
| 	golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e // indirect | ||||
| 	gopkg.in/ini.v1 v1.62.0 // indirect | ||||
| 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect | ||||
| ) | ||||
							
								
								
									
										1105
									
								
								outpost/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1105
									
								
								outpost/go.sum
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								outpost/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								outpost/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"github.com/BeryJu/authentik/outpost/cmd" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	log.SetLevel(log.DebugLevel) | ||||
| 	cmd.RunServer() | ||||
| } | ||||
							
								
								
									
										30
									
								
								outpost/pkg/proxy/claims.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								outpost/pkg/proxy/claims.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type Claims struct { | ||||
| 	Proxy struct { | ||||
| 		UserAttributes map[string]interface{} `json:"user_attributes"` | ||||
| 	} `json:"ak_proxy"` | ||||
| } | ||||
|  | ||||
| func (c *Claims) FromIDToken(idToken string) error { | ||||
| 	// id_token is a base64 encode ID token payload | ||||
| 	// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo | ||||
| 	jwt := strings.Split(idToken, ".") | ||||
| 	jwtData := strings.TrimSuffix(jwt[1], "=") | ||||
| 	b, err := base64.RawURLEncoding.DecodeString(jwtData) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = json.Unmarshal(b, c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										69
									
								
								outpost/pkg/proxy/cookies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								outpost/pkg/proxy/cookies.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/util" | ||||
| ) | ||||
|  | ||||
| // 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) | ||||
| } | ||||
|  | ||||
| func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie { | ||||
| 	cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains) | ||||
|  | ||||
| 	if cookieDomain != "" { | ||||
| 		domain := util.GetRequestHost(req) | ||||
| 		if h, _, err := net.SplitHostPort(domain); err == nil { | ||||
| 			domain = h | ||||
| 		} | ||||
| 		if !strings.HasSuffix(domain, cookieDomain) { | ||||
| 			p.logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &http.Cookie{ | ||||
| 		Name:     name, | ||||
| 		Value:    value, | ||||
| 		Path:     p.CookiePath, | ||||
| 		Domain:   cookieDomain, | ||||
| 		HttpOnly: p.CookieHTTPOnly, | ||||
| 		Secure:   p.CookieSecure, | ||||
| 		Expires:  now.Add(expiration), | ||||
| 		SameSite: cookies.ParseSameSite(p.CookieSameSite), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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) error { | ||||
| 	return p.sessionStore.Clear(rw, req) | ||||
| } | ||||
|  | ||||
| // LoadCookiedSession reads the user's authentication details from the request | ||||
| func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) { | ||||
| 	return p.sessionStore.Load(req) | ||||
| } | ||||
|  | ||||
| // SaveSession creates a new session cookie value and sets this on the response | ||||
| func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error { | ||||
| 	return p.sessionStore.Save(rw, req, s) | ||||
| } | ||||
							
								
								
									
										233
									
								
								outpost/pkg/proxy/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								outpost/pkg/proxy/oauth.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,233 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/ip" | ||||
| ) | ||||
|  | ||||
| // 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 != "" { | ||||
| 		return p.redirectURL.String() | ||||
| 	} | ||||
| 	u := *p.redirectURL | ||||
| 	if u.Scheme == "" { | ||||
| 		if p.CookieSecure { | ||||
| 			u.Scheme = httpsScheme | ||||
| 		} else { | ||||
| 			u.Scheme = httpScheme | ||||
| 		} | ||||
| 	} | ||||
| 	u.Host = host | ||||
| 	return u.String() | ||||
| } | ||||
|  | ||||
| func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) { | ||||
| 	if code == "" { | ||||
| 		return nil, errors.New("missing code") | ||||
| 	} | ||||
| 	redirectURI := p.GetRedirectURI(host) | ||||
| 	s, err = p.provider.Redeem(ctx, redirectURI, code) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if s.Email == "" { | ||||
| 		s.Email, err = p.provider.GetEmailAddress(ctx, s) | ||||
| 	} | ||||
|  | ||||
| 	if s.PreferredUsername == "" { | ||||
| 		s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s) | ||||
| 		if err != nil && err.Error() == "not implemented" { | ||||
| 			err = nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if s.User == "" { | ||||
| 		s.User, err = p.provider.GetUserName(ctx, s) | ||||
| 		if err != nil && err.Error() == "not implemented" { | ||||
| 			err = nil | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // 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 { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirect = req.Header.Get("X-Auth-Request-Redirect") | ||||
| 	if req.Form.Get("rd") != "" { | ||||
| 		redirect = req.Form.Get("rd") | ||||
| 	} | ||||
| 	if !p.IsValidRedirect(redirect) { | ||||
| 		// Use RequestURI to preserve ?query | ||||
| 		redirect = req.URL.RequestURI() | ||||
| 		if strings.HasPrefix(redirect, p.ProxyPrefix) { | ||||
| 			redirect = "/" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // IsValidRedirect checks whether the redirect URL is whitelisted | ||||
| func (p *OAuthProxy) IsValidRedirect(redirect string) bool { | ||||
| 	switch { | ||||
| 	case redirect == "": | ||||
| 		// The user didn't specify a redirect, should fallback to `/` | ||||
| 		return false | ||||
| 	case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect): | ||||
| 		return true | ||||
| 	case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"): | ||||
| 		redirectURL, err := url.Parse(redirect) | ||||
| 		if err != nil { | ||||
| 			p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect) | ||||
| 			return false | ||||
| 		} | ||||
| 		redirectHostname := redirectURL.Hostname() | ||||
|  | ||||
| 		for _, domain := range p.whitelistDomains { | ||||
| 			domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, ".")) | ||||
| 			if domainHostname == "" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) { | ||||
| 				// the domain names match, now validate the ports | ||||
| 				// if the whitelisted domain's port is '*', allow all ports | ||||
| 				// if the whitelisted domain contains a specific port, only allow that port | ||||
| 				// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https | ||||
| 				redirectPort := redirectURL.Port() | ||||
| 				if (domainPort == "*") || | ||||
| 					(domainPort == redirectPort) || | ||||
| 					(domainPort == "" && redirectPort == "") { | ||||
| 					return true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect) | ||||
| 		return false | ||||
| 	default: | ||||
| 		p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect) | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // IsWhitelistedRequest is used to check if auth should be skipped for this request | ||||
| func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) 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) bool { | ||||
| 	for _, u := range p.compiledRegex { | ||||
| 		if u.MatchString(path) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // OAuthStart starts the OAuth2 authentication flow | ||||
| func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { | ||||
| 	prepareNoCache(rw) | ||||
| 	nonce, err := encryption.Nonce() | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error obtaining nonce: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	p.SetCSRFCookie(rw, req, nonce) | ||||
| 	redirect, err := p.GetRedirect(req) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error obtaining redirect: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	redirectURI := p.GetRedirectURI(req.Host) | ||||
| 	http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound) | ||||
| } | ||||
|  | ||||
| // OAuthCallback is the OAuth2 authentication flow callback that finishes the | ||||
| // OAuth2 authentication flow | ||||
| func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { | ||||
| 	remoteAddr := ip.GetClientString(p.realClientIPParser, req, true) | ||||
|  | ||||
| 	// finish the oauth cycle | ||||
| 	err := req.ParseForm() | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error while parsing OAuth2 callback: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	errorString := req.Form.Get("error") | ||||
| 	if errorString != "" { | ||||
| 		p.logger.Errorf("Error while parsing OAuth2 callback: %s", errorString) | ||||
| 		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code")) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error redeeming code during OAuth2 callback: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	s := strings.SplitN(req.Form.Get("state"), ":", 2) | ||||
| 	if len(s) != 2 { | ||||
| 		p.logger.Error("Error while parsing OAuth2 state: invalid length") | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State") | ||||
| 		return | ||||
| 	} | ||||
| 	nonce := s[0] | ||||
| 	redirect := s[1] | ||||
| 	c, err := req.Cookie(p.CSRFCookieName) | ||||
| 	if err != nil { | ||||
| 		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unable to obtain CSRF cookie") | ||||
| 		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	p.ClearCSRFCookie(rw, req) | ||||
| 	if c.Value != nonce { | ||||
| 		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: CSRF token mismatch, potential attack") | ||||
| 		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !p.IsValidRedirect(redirect) { | ||||
| 		redirect = "/" | ||||
| 	} | ||||
|  | ||||
| 	// set cookie, or deny | ||||
| 	if p.provider.ValidateGroup(session.Email) { | ||||
| 		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Infof("Authenticated via OAuth2: %s", session) | ||||
| 		err := p.SaveSession(rw, req, session) | ||||
| 		if err != nil { | ||||
| 			p.logger.Printf("Error saving session state for %s: %v", remoteAddr, err) | ||||
| 			p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		http.Redirect(rw, req, redirect, http.StatusFound) | ||||
| 	} else { | ||||
| 		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unauthorized") | ||||
| 		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										481
									
								
								outpost/pkg/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										481
									
								
								outpost/pkg/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,481 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	b64 "encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/coreos/go-oidc" | ||||
| 	"github.com/justinas/alice" | ||||
| 	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||
| 	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/sessions" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/upstream" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/providers" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	httpScheme  = "http" | ||||
| 	httpsScheme = "https" | ||||
|  | ||||
| 	applicationJSON = "application/json" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// ErrNeedsLogin means the user should be redirected to the login page | ||||
| 	ErrNeedsLogin = errors.New("redirect to login page") | ||||
|  | ||||
| 	// Used to check final redirects are not susceptible to open redirects. | ||||
| 	// Matches //, /\ and both of these with whitespace in between (eg / / or / \). | ||||
| 	invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`) | ||||
| ) | ||||
|  | ||||
| // OAuthProxy is the main authentication proxy | ||||
| type OAuthProxy struct { | ||||
| 	CookieSeed     string | ||||
| 	CookieName     string | ||||
| 	CSRFCookieName string | ||||
| 	CookieDomains  []string | ||||
| 	CookiePath     string | ||||
| 	CookieSecure   bool | ||||
| 	CookieHTTPOnly bool | ||||
| 	CookieExpire   time.Duration | ||||
| 	CookieRefresh  time.Duration | ||||
| 	CookieSameSite string | ||||
|  | ||||
| 	RobotsPath        string | ||||
| 	SignInPath        string | ||||
| 	SignOutPath       string | ||||
| 	OAuthStartPath    string | ||||
| 	OAuthCallbackPath string | ||||
| 	AuthOnlyPath      string | ||||
| 	UserInfoPath      string | ||||
|  | ||||
| 	redirectURL                *url.URL // the url to receive requests at | ||||
| 	whitelistDomains           []string | ||||
| 	provider                   providers.Provider | ||||
| 	sessionStore               sessionsapi.SessionStore | ||||
| 	ProxyPrefix                string | ||||
| 	serveMux                   http.Handler | ||||
| 	SetXAuthRequest            bool | ||||
| 	SetBasicAuth               bool | ||||
| 	PassUserHeaders            bool | ||||
| 	BasicAuthUserAttribute     string | ||||
| 	BasicAuthPasswordAttribute string | ||||
| 	PassAccessToken            bool | ||||
| 	SetAuthorization           bool | ||||
| 	PassAuthorization          bool | ||||
| 	PreferEmailToUser          bool | ||||
| 	skipAuthRegex              []string | ||||
| 	skipAuthPreflight          bool | ||||
| 	skipAuthStripHeaders       bool | ||||
| 	mainJwtBearerVerifier      *oidc.IDTokenVerifier | ||||
| 	extraJwtBearerVerifiers    []*oidc.IDTokenVerifier | ||||
| 	compiledRegex              []*regexp.Regexp | ||||
| 	templates                  *template.Template | ||||
| 	realClientIPParser         ipapi.RealClientIPParser | ||||
|  | ||||
| 	sessionChain alice.Chain | ||||
|  | ||||
| 	logger *log.Entry | ||||
| } | ||||
|  | ||||
| // NewOAuthProxy creates a new instance of OAuthProxy from the options provided | ||||
| func NewOAuthProxy(opts *options.Options) (*OAuthProxy, error) { | ||||
| 	logger := log.WithField("component", "proxy").WithField("client-id", opts.ClientID) | ||||
| 	sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error initialising session store: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	templates := getTemplates() | ||||
| 	proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix) | ||||
| 	upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error initialising upstream proxy: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	for _, u := range opts.GetCompiledRegex() { | ||||
| 		logger.Printf("compiled skip-auth-regex => %q", u) | ||||
| 	} | ||||
|  | ||||
| 	redirectURL := opts.GetRedirectURL() | ||||
| 	if redirectURL.Path == "" { | ||||
| 		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) | ||||
| 	} | ||||
|  | ||||
| 	logger.Printf("proxy instance configured for Client ID: %s", opts.ClientID) | ||||
|  | ||||
| 	sessionChain := buildSessionChain(opts, sessionStore) | ||||
|  | ||||
| 	return &OAuthProxy{ | ||||
| 		CookieName:     opts.Cookie.Name, | ||||
| 		CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"), | ||||
| 		CookieSeed:     opts.Cookie.Secret, | ||||
| 		CookieDomains:  opts.Cookie.Domains, | ||||
| 		CookiePath:     opts.Cookie.Path, | ||||
| 		CookieSecure:   opts.Cookie.Secure, | ||||
| 		CookieHTTPOnly: opts.Cookie.HTTPOnly, | ||||
| 		CookieExpire:   opts.Cookie.Expire, | ||||
| 		CookieRefresh:  opts.Cookie.Refresh, | ||||
| 		CookieSameSite: opts.Cookie.SameSite, | ||||
|  | ||||
| 		RobotsPath:        "/robots.txt", | ||||
| 		SignInPath:        fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), | ||||
| 		SignOutPath:       fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), | ||||
| 		OAuthStartPath:    fmt.Sprintf("%s/start", opts.ProxyPrefix), | ||||
| 		OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), | ||||
| 		AuthOnlyPath:      fmt.Sprintf("%s/auth", opts.ProxyPrefix), | ||||
| 		UserInfoPath:      fmt.Sprintf("%s/userinfo", opts.ProxyPrefix), | ||||
|  | ||||
| 		ProxyPrefix:             opts.ProxyPrefix, | ||||
| 		provider:                opts.GetProvider(), | ||||
| 		sessionStore:            sessionStore, | ||||
| 		serveMux:                upstreamProxy, | ||||
| 		redirectURL:             redirectURL, | ||||
| 		whitelistDomains:        opts.WhitelistDomains, | ||||
| 		skipAuthRegex:           opts.SkipAuthRegex, | ||||
| 		skipAuthPreflight:       opts.SkipAuthPreflight, | ||||
| 		skipAuthStripHeaders:    opts.SkipAuthStripHeaders, | ||||
| 		mainJwtBearerVerifier:   opts.GetOIDCVerifier(), | ||||
| 		extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(), | ||||
| 		compiledRegex:           opts.GetCompiledRegex(), | ||||
| 		realClientIPParser:      opts.GetRealClientIPParser(), | ||||
| 		SetXAuthRequest:         opts.SetXAuthRequest, | ||||
| 		SetBasicAuth:            opts.SetBasicAuth, | ||||
| 		PassUserHeaders:         opts.PassUserHeaders, | ||||
| 		PassAccessToken:         opts.PassAccessToken, | ||||
| 		SetAuthorization:        opts.SetAuthorization, | ||||
| 		PassAuthorization:       opts.PassAuthorization, | ||||
| 		PreferEmailToUser:       opts.PreferEmailToUser, | ||||
| 		templates:               templates, | ||||
|  | ||||
| 		sessionChain: sessionChain, | ||||
|  | ||||
| 		logger: logger, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore) alice.Chain { | ||||
| 	chain := alice.New(middleware.NewScope()) | ||||
|  | ||||
| 	chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{ | ||||
| 		SessionStore:           sessionStore, | ||||
| 		RefreshPeriod:          opts.Cookie.Refresh, | ||||
| 		RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded, | ||||
| 		ValidateSessionState:   opts.GetProvider().ValidateSessionState, | ||||
| 	})) | ||||
|  | ||||
| 	return chain | ||||
| } | ||||
|  | ||||
| // RobotsTxt disallows scraping pages from the OAuthProxy | ||||
| func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) { | ||||
| 	_, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /") | ||||
| 	if err != nil { | ||||
| 		p.logger.Printf("Error writing robots.txt: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	rw.WriteHeader(http.StatusOK) | ||||
| } | ||||
|  | ||||
| // ErrorPage writes an error response | ||||
| func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) { | ||||
| 	rw.WriteHeader(code) | ||||
| 	t := struct { | ||||
| 		Title       string | ||||
| 		Message     string | ||||
| 		ProxyPrefix string | ||||
| 	}{ | ||||
| 		Title:       fmt.Sprintf("%d %s", code, title), | ||||
| 		Message:     message, | ||||
| 		ProxyPrefix: p.ProxyPrefix, | ||||
| 	} | ||||
| 	err := p.templates.ExecuteTemplate(rw, "error.html", t) | ||||
| 	if err != nil { | ||||
| 		p.logger.Printf("Error rendering error.html template: %v", err) | ||||
| 		http.Error(rw, "Internal Server Error", http.StatusInternalServerError) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // splitHostPort separates host and port. If the port is not valid, it returns | ||||
| // the entire input as host, and it doesn't check the validity of the host. | ||||
| // Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. | ||||
| // *** taken from net/url, modified validOptionalPort() to accept ":*" | ||||
| func splitHostPort(hostport string) (host, port string) { | ||||
| 	host = hostport | ||||
|  | ||||
| 	colon := strings.LastIndexByte(host, ':') | ||||
| 	if colon != -1 && validOptionalPort(host[colon:]) { | ||||
| 		host, port = host[:colon], host[colon+1:] | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | ||||
| 		host = host[1 : len(host)-1] | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // validOptionalPort reports whether port is either an empty string | ||||
| // or matches /^:\d*$/ | ||||
| // *** taken from net/url, modified to accept ":*" | ||||
| func validOptionalPort(port string) bool { | ||||
| 	if port == "" || port == ":*" { | ||||
| 		return true | ||||
| 	} | ||||
| 	if port[0] != ':' { | ||||
| 		return false | ||||
| 	} | ||||
| 	for _, b := range port[1:] { | ||||
| 		if b < '0' || b > '9' { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en | ||||
| var noCacheHeaders = map[string]string{ | ||||
| 	"Expires":         time.Unix(0, 0).Format(time.RFC1123), | ||||
| 	"Cache-Control":   "no-cache, no-store, must-revalidate, max-age=0", | ||||
| 	"X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ | ||||
| } | ||||
|  | ||||
| // prepareNoCache prepares headers for preventing browser caching. | ||||
| func prepareNoCache(w http.ResponseWriter) { | ||||
| 	// Set NoCache headers | ||||
| 	for k, v := range noCacheHeaders { | ||||
| 		w.Header().Set(k, v) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||||
| 	if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) { | ||||
| 		prepareNoCache(rw) | ||||
| 	} | ||||
|  | ||||
| 	switch path := req.URL.Path; { | ||||
| 	case path == p.RobotsPath: | ||||
| 		p.RobotsTxt(rw) | ||||
| 	case p.IsWhitelistedRequest(req): | ||||
| 		p.SkipAuthProxy(rw, req) | ||||
| 	case path == p.SignInPath: | ||||
| 		p.OAuthStart(rw, req) | ||||
| 	case path == p.SignOutPath: | ||||
| 		p.SignOut(rw, req) | ||||
| 	case path == p.OAuthStartPath: | ||||
| 		p.OAuthStart(rw, req) | ||||
| 	case path == p.OAuthCallbackPath: | ||||
| 		p.OAuthCallback(rw, req) | ||||
| 	case path == p.AuthOnlyPath: | ||||
| 		p.AuthenticateOnly(rw, req) | ||||
| 	case path == p.UserInfoPath: | ||||
| 		p.UserInfo(rw, req) | ||||
| 	default: | ||||
| 		p.Proxy(rw, req) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| //UserInfo endpoint outputs session email and preferred username in JSON format | ||||
| func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { | ||||
|  | ||||
| 	session, err := p.getAuthenticatedSession(rw, req) | ||||
| 	if err != nil { | ||||
| 		http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | ||||
| 		return | ||||
| 	} | ||||
| 	userInfo := struct { | ||||
| 		Email             string `json:"email"` | ||||
| 		PreferredUsername string `json:"preferredUsername,omitempty"` | ||||
| 	}{ | ||||
| 		Email:             session.Email, | ||||
| 		PreferredUsername: session.PreferredUsername, | ||||
| 	} | ||||
| 	rw.Header().Set("Content-Type", "application/json") | ||||
| 	rw.WriteHeader(http.StatusOK) | ||||
| 	err = json.NewEncoder(rw).Encode(userInfo) | ||||
| 	if err != nil { | ||||
| 		p.logger.Printf("Error encoding user info: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SignOut sends a response to clear the authentication cookie | ||||
| func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { | ||||
| 	redirect, err := p.GetRedirect(req) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error obtaining redirect: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	err = p.ClearSessionCookie(rw, req) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error clearing session cookie: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	http.Redirect(rw, req, redirect, http.StatusFound) | ||||
| } | ||||
|  | ||||
| // AuthenticateOnly checks whether the user is currently logged in | ||||
| func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { | ||||
| 	session, err := p.getAuthenticatedSession(rw, req) | ||||
| 	if err != nil { | ||||
| 		http.Error(rw, "unauthorized request", http.StatusUnauthorized) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// we are authenticated | ||||
| 	p.addHeadersForProxying(rw, req, session) | ||||
| 	rw.WriteHeader(http.StatusAccepted) | ||||
| } | ||||
|  | ||||
| // SkipAuthProxy proxies whitelisted requests and skips authentication | ||||
| func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) { | ||||
| 	if p.skipAuthStripHeaders { | ||||
| 		p.stripAuthHeaders(req) | ||||
| 	} | ||||
| 	p.serveMux.ServeHTTP(rw, req) | ||||
| } | ||||
|  | ||||
| // 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) { | ||||
| 	session, err := p.getAuthenticatedSession(rw, req) | ||||
| 	switch err { | ||||
| 	case nil: | ||||
| 		// we are authenticated | ||||
| 		p.addHeadersForProxying(rw, req, session) | ||||
| 		p.serveMux.ServeHTTP(rw, req) | ||||
|  | ||||
| 	case ErrNeedsLogin: | ||||
| 		// we need to send the user to a login screen | ||||
| 		if isAjax(req) { | ||||
| 			// no point redirecting an AJAX request | ||||
| 			p.ErrorJSON(rw, http.StatusUnauthorized) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		p.OAuthStart(rw, req) | ||||
|  | ||||
| 	default: | ||||
| 		// unknown error | ||||
| 		p.logger.Errorf("Unexpected internal error: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, | ||||
| 			"Internal Error", "Internal Error") | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| // getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so | ||||
| // Returns nil, ErrNeedsLogin if user needs to login. | ||||
| // Set-Cookie headers may be set on the response as a side-effect of calling this method. | ||||
| func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) { | ||||
| 	var session *sessionsapi.SessionState | ||||
|  | ||||
| 	getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||||
| 		session = middleware.GetRequestScope(req).Session | ||||
| 	})) | ||||
| 	getSession.ServeHTTP(rw, req) | ||||
|  | ||||
| 	if session == nil { | ||||
| 		return nil, ErrNeedsLogin | ||||
| 	} | ||||
|  | ||||
| 	return session, nil | ||||
| } | ||||
|  | ||||
| // addHeadersForProxying adds the appropriate headers the request / response for proxying | ||||
| func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) { | ||||
| 	req.Header["X-Forwarded-User"] = []string{session.User} | ||||
| 	if session.Email != "" { | ||||
| 		req.Header["X-Forwarded-Email"] = []string{session.Email} | ||||
| 	} | ||||
|  | ||||
| 	if session.PreferredUsername != "" { | ||||
| 		req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername} | ||||
| 		req.Header["X-Auth-Username"] = []string{session.PreferredUsername} | ||||
| 	} else { | ||||
| 		req.Header.Del("X-Forwarded-Preferred-Username") | ||||
| 		req.Header.Del("X-Auth-Username") | ||||
| 	} | ||||
|  | ||||
| 	claims := Claims{} | ||||
| 	err := claims.FromIDToken(session.IDToken) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Warning("Failed to parse IDToken") | ||||
| 	} | ||||
| 	userAttributes := claims.Proxy.UserAttributes | ||||
| 	// Attempt to set basic auth based on user's attributes | ||||
| 	if p.SetBasicAuth { | ||||
| 		var ok bool | ||||
| 		var password string | ||||
| 		if password, ok = userAttributes[p.BasicAuthPasswordAttribute].(string); !ok { | ||||
| 			password = "" | ||||
| 		} | ||||
| 		// Check if we should use email or a custom attribute as username | ||||
| 		var username string | ||||
| 		if username, ok = userAttributes[p.BasicAuthUserAttribute].(string); !ok { | ||||
| 			username = session.Email | ||||
| 		} | ||||
| 		authVal := b64.StdEncoding.EncodeToString([]byte(username + ":" + password)) | ||||
| 		req.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)} | ||||
| 	} | ||||
| 	// Check if user has additional headers set that we should sent | ||||
| 	if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]string); ok { | ||||
| 		if additionalHeaders == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		for key, value := range additionalHeaders { | ||||
| 			req.Header.Set(key, value) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex | ||||
| func (p *OAuthProxy) stripAuthHeaders(req *http.Request) { | ||||
| 	if p.PassUserHeaders { | ||||
| 		req.Header.Del("X-Forwarded-User") | ||||
| 		req.Header.Del("X-Forwarded-Email") | ||||
| 		req.Header.Del("X-Forwarded-Preferred-Username") | ||||
| 	} | ||||
|  | ||||
| 	if p.PassAccessToken { | ||||
| 		req.Header.Del("X-Forwarded-Access-Token") | ||||
| 	} | ||||
|  | ||||
| 	if p.PassAuthorization { | ||||
| 		req.Header.Del("Authorization") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // isAjax checks if a request is an ajax request | ||||
| func isAjax(req *http.Request) bool { | ||||
| 	acceptValues := req.Header.Values("Accept") | ||||
| 	const ajaxReq = applicationJSON | ||||
| 	for _, v := range acceptValues { | ||||
| 		if v == ajaxReq { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // ErrorJSON returns the error code with an application/json mime type | ||||
| func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) { | ||||
| 	rw.Header().Set("Content-Type", applicationJSON) | ||||
| 	rw.WriteHeader(code) | ||||
| } | ||||
							
								
								
									
										28
									
								
								outpost/pkg/proxy/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								outpost/pkg/proxy/templates.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"html/template" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func getTemplates() *template.Template { | ||||
| 	t, err := template.New("foo").Parse(`{{define "error.html"}} | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" charset="utf-8"> | ||||
| <head> | ||||
| 	<title>{{.Title}}</title> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
| </head> | ||||
| <body> | ||||
| 	<h2>{{.Title}}</h2> | ||||
| 	<p>{{.Message}}</p> | ||||
| 	<hr> | ||||
| 	<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p> | ||||
| </body> | ||||
| </html>{{end}}`) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("failed parsing template %s", err) | ||||
| 	} | ||||
| 	return t | ||||
| } | ||||
							
								
								
									
										225
									
								
								outpost/pkg/server/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								outpost/pkg/server/api.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg" | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/client" | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/client/outposts" | ||||
| 	"github.com/getsentry/sentry-go" | ||||
| 	"github.com/go-openapi/runtime" | ||||
| 	"github.com/recws-org/recws" | ||||
|  | ||||
| 	httptransport "github.com/go-openapi/runtime/client" | ||||
| 	"github.com/go-openapi/strfmt" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| const ConfigLogLevel = "log_level" | ||||
| const ConfigErrorReportingEnabled = "error_reporting_enabled" | ||||
| const ConfigErrorReportingEnvironment = "error_reporting_environment" | ||||
|  | ||||
| // APIController main controller which connects to the authentik api via http and ws | ||||
| type APIController struct { | ||||
| 	client *client.Authentik | ||||
| 	auth   runtime.ClientAuthInfoWriter | ||||
| 	token  string | ||||
|  | ||||
| 	server *Server | ||||
|  | ||||
| 	commonOpts *options.Options | ||||
|  | ||||
| 	lastBundleHash string | ||||
| 	logger         *log.Entry | ||||
|  | ||||
| 	reloadOffset time.Duration | ||||
|  | ||||
| 	wsConn *recws.RecConn | ||||
| } | ||||
|  | ||||
| func getCommonOptions() *options.Options { | ||||
| 	commonOpts := options.NewOptions() | ||||
| 	commonOpts.Cookie.Name = "authentik_proxy" | ||||
| 	commonOpts.Cookie.Expire = 24 * time.Hour | ||||
| 	commonOpts.EmailDomains = []string{"*"} | ||||
| 	commonOpts.ProviderType = "oidc" | ||||
| 	commonOpts.ProxyPrefix = "/akprox" | ||||
| 	commonOpts.Logging.SilencePing = true | ||||
| 	commonOpts.SetAuthorization = false | ||||
| 	commonOpts.Scope = "openid email profile ak_proxy" | ||||
| 	return commonOpts | ||||
| } | ||||
|  | ||||
| func doGlobalSetup(config map[string]interface{}) { | ||||
| 	log.SetFormatter(&log.JSONFormatter{}) | ||||
| 	switch config[ConfigLogLevel].(string) { | ||||
| 	case "debug": | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 	case "info": | ||||
| 		log.SetLevel(log.InfoLevel) | ||||
| 	case "warning": | ||||
| 		log.SetLevel(log.WarnLevel) | ||||
| 	case "error": | ||||
| 		log.SetLevel(log.ErrorLevel) | ||||
| 	default: | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 	} | ||||
| 	log.WithField("version", pkg.VERSION).Info("Starting authentik proxy") | ||||
|  | ||||
| 	var dsn string | ||||
| 	if config[ConfigErrorReportingEnabled].(bool) { | ||||
| 		dsn = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | ||||
| 		log.Debug("Error reporting enabled") | ||||
| 	} | ||||
|  | ||||
| 	err := sentry.Init(sentry.ClientOptions{ | ||||
| 		Dsn:         dsn, | ||||
| 		Environment: config[ConfigErrorReportingEnvironment].(string), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("sentry.Init: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	defer sentry.Flush(2 * time.Second) | ||||
| } | ||||
|  | ||||
| func getTLSTransport() http.RoundTripper { | ||||
| 	value, set := os.LookupEnv("AUTHENTIK_INSECURE") | ||||
| 	if !set { | ||||
| 		value = "false" | ||||
| 	} | ||||
| 	tlsTransport, err := httptransport.TLSTransport(httptransport.TLSClientOptions{ | ||||
| 		InsecureSkipVerify: strings.ToLower(value) == "true", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return tlsTransport | ||||
| } | ||||
|  | ||||
| // NewAPIController initialise new API Controller instance from URL and API token | ||||
| func NewAPIController(pbURL url.URL, token string) *APIController { | ||||
| 	transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme}) | ||||
| 	transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)) | ||||
|  | ||||
| 	// create the transport | ||||
| 	auth := httptransport.BasicAuth("", token) | ||||
|  | ||||
| 	// create the API client, with the transport | ||||
| 	apiClient := client.New(transport, strfmt.Default) | ||||
|  | ||||
| 	// Because we don't know the outpost UUID, we simply do a list and pick the first | ||||
| 	// The service account this token belongs to should only have access to a single outpost | ||||
| 	outposts, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	outpost := outposts.Payload.Results[0] | ||||
| 	doGlobalSetup(outpost.Config.(map[string]interface{})) | ||||
|  | ||||
| 	ac := &APIController{ | ||||
| 		client: apiClient, | ||||
| 		auth:   auth, | ||||
| 		token:  token, | ||||
|  | ||||
| 		logger:     log.WithField("component", "api-controller"), | ||||
| 		commonOpts: getCommonOptions(), | ||||
| 		server:     NewServer(), | ||||
|  | ||||
| 		reloadOffset: time.Duration(rand.Intn(10)) * time.Second, | ||||
|  | ||||
| 		lastBundleHash: "", | ||||
| 	} | ||||
| 	ac.logger.Debugf("HA Reload offset: %s", ac.reloadOffset) | ||||
| 	ac.initWS(pbURL, outpost.Pk) | ||||
| 	return ac | ||||
| } | ||||
|  | ||||
| func (a *APIController) bundleProviders() ([]*providerBundle, error) { | ||||
| 	providers, err := a.client.Outposts.OutpostsProxyList(outposts.NewOutpostsProxyListParams(), a.auth) | ||||
| 	if err != nil { | ||||
| 		a.logger.WithError(err).Error("Failed to fetch providers") | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// Check provider hash to see if anything is changed | ||||
| 	hasher := sha512.New() | ||||
| 	bin, _ := providers.Payload.MarshalBinary() | ||||
| 	hash := hex.EncodeToString(hasher.Sum(bin)) | ||||
| 	if hash == a.lastBundleHash { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	a.lastBundleHash = hash | ||||
|  | ||||
| 	bundles := make([]*providerBundle, len(providers.Payload.Results)) | ||||
|  | ||||
| 	for idx, provider := range providers.Payload.Results { | ||||
| 		externalHost, err := url.Parse(*provider.ExternalHost) | ||||
| 		if err != nil { | ||||
| 			log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||
| 		} | ||||
| 		bundles[idx] = &providerBundle{ | ||||
| 			a:    a, | ||||
| 			Host: externalHost.Host, | ||||
| 		} | ||||
| 		bundles[idx].Build(provider) | ||||
| 	} | ||||
| 	return bundles, nil | ||||
| } | ||||
|  | ||||
| func (a *APIController) updateHTTPServer(bundles []*providerBundle) { | ||||
| 	newMap := make(map[string]*providerBundle) | ||||
| 	for _, bundle := range bundles { | ||||
| 		newMap[bundle.Host] = bundle | ||||
| 	} | ||||
| 	a.logger.Debug("Swapped maps") | ||||
| 	a.server.Handlers = newMap | ||||
| } | ||||
|  | ||||
| // UpdateIfRequired Updates the HTTP Server config if required, automatically swaps the handlers | ||||
| func (a *APIController) UpdateIfRequired() error { | ||||
| 	bundles, err := a.bundleProviders() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if bundles == nil { | ||||
| 		a.logger.Debug("Providers have not changed, not updating") | ||||
| 		return nil | ||||
| 	} | ||||
| 	a.updateHTTPServer(bundles) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Start Starts all handlers, non-blocking | ||||
| func (a *APIController) Start() error { | ||||
| 	err := a.UpdateIfRequired() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting HTTP Server...") | ||||
| 		a.server.ServeHTTP() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting HTTPs Server...") | ||||
| 		a.server.ServeHTTPS() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting WS Handler...") | ||||
| 		a.startWSHandler() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting WS Health notifier...") | ||||
| 		a.startWSHealth() | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										136
									
								
								outpost/pkg/server/api_bundle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								outpost/pkg/server/api_bundle.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/client/crypto" | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/models" | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg/proxy" | ||||
| 	"github.com/jinzhu/copier" | ||||
| 	"github.com/justinas/alice" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/validation" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| type providerBundle struct { | ||||
| 	http.Handler | ||||
|  | ||||
| 	a     *APIController | ||||
| 	proxy *proxy.OAuthProxy | ||||
| 	Host  string | ||||
|  | ||||
| 	cert *tls.Certificate | ||||
| } | ||||
|  | ||||
| func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *options.Options { | ||||
| 	externalHost, err := url.Parse(*provider.ExternalHost) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||
| 		return nil | ||||
| 	} | ||||
| 	providerOpts := &options.Options{} | ||||
| 	copier.Copy(&providerOpts, &pb.a.commonOpts) | ||||
| 	providerOpts.ClientID = provider.ClientID | ||||
| 	providerOpts.ClientSecret = provider.ClientSecret | ||||
|  | ||||
| 	providerOpts.Cookie.Secret = provider.CookieSecret | ||||
| 	providerOpts.Cookie.Secure = externalHost.Scheme == "https" | ||||
|  | ||||
| 	providerOpts.SkipOIDCDiscovery = true | ||||
| 	providerOpts.OIDCIssuerURL = *provider.OidcConfiguration.Issuer | ||||
| 	providerOpts.LoginURL = *provider.OidcConfiguration.AuthorizationEndpoint | ||||
| 	providerOpts.RedeemURL = *provider.OidcConfiguration.TokenEndpoint | ||||
| 	providerOpts.OIDCJwksURL = *provider.OidcConfiguration.JwksURI | ||||
| 	providerOpts.ProfileURL = *provider.OidcConfiguration.UserinfoEndpoint | ||||
|  | ||||
| 	if provider.SkipPathRegex != "" { | ||||
| 		skipRegexes := strings.Split(provider.SkipPathRegex, "\n") | ||||
| 		providerOpts.SkipAuthRegex = skipRegexes | ||||
| 	} | ||||
|  | ||||
| 	providerOpts.UpstreamServers = []options.Upstream{ | ||||
| 		{ | ||||
| 			ID:                    "default", | ||||
| 			URI:                   *provider.InternalHost, | ||||
| 			Path:                  "/", | ||||
| 			InsecureSkipTLSVerify: *&provider.InternalHostSslValidation, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if provider.Certificate != nil { | ||||
| 		pb.a.logger.WithField("provider", provider.ClientID).Debug("Enabling TLS") | ||||
| 		cert, err := pb.a.client.Crypto.CryptoCertificatekeypairsRead(&crypto.CryptoCertificatekeypairsReadParams{ | ||||
| 			Context: context.Background(), | ||||
| 			KpUUID:  *provider.Certificate, | ||||
| 		}, pb.a.auth) | ||||
| 		if err != nil { | ||||
| 			pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch certificate") | ||||
| 			return providerOpts | ||||
| 		} | ||||
| 		x509cert, err := tls.X509KeyPair([]byte(*cert.Payload.CertificateData), []byte(cert.Payload.KeyData)) | ||||
| 		if err != nil { | ||||
| 			pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate") | ||||
| 			return providerOpts | ||||
| 		} | ||||
| 		pb.cert = &x509cert | ||||
| 		pb.a.logger.WithField("provider", provider.ClientID).WithField("certificate-key-pair", *cert.Payload.Name).Debug("Loaded certificates") | ||||
| 	} | ||||
| 	return providerOpts | ||||
| } | ||||
|  | ||||
| func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) { | ||||
| 	opts := pb.prepareOpts(provider) | ||||
|  | ||||
| 	chain := alice.New() | ||||
|  | ||||
| 	if opts.ForceHTTPS { | ||||
| 		_, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress) | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("FATAL: invalid HTTPS address %q: %v", opts.HTTPAddress, err) | ||||
| 		} | ||||
| 		chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort)) | ||||
| 	} | ||||
|  | ||||
| 	healthCheckPaths := []string{opts.PingPath} | ||||
| 	healthCheckUserAgents := []string{opts.PingUserAgent} | ||||
| 	if opts.GCPHealthChecks { | ||||
| 		healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check") | ||||
| 		healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0") | ||||
| 	} | ||||
|  | ||||
| 	// To silence logging of health checks, register the health check handler before | ||||
| 	// the logging handler | ||||
| 	if opts.Logging.SilencePing { | ||||
| 		chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler) | ||||
| 	} else { | ||||
| 		chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents)) | ||||
| 	} | ||||
|  | ||||
| 	err := validation.Validate(opts) | ||||
| 	if err != nil { | ||||
| 		log.Printf("%s", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	oauthproxy, err := proxy.NewOAuthProxy(opts) | ||||
| 	if err != nil { | ||||
| 		log.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	if *&provider.BasicAuthEnabled { | ||||
| 		oauthproxy.SetBasicAuth = true | ||||
| 		oauthproxy.BasicAuthUserAttribute = provider.BasicAuthUserAttribute | ||||
| 		oauthproxy.BasicAuthPasswordAttribute = provider.BasicAuthPasswordAttribute | ||||
| 	} | ||||
|  | ||||
| 	pb.proxy = oauthproxy | ||||
| 	pb.Handler = chain.Then(oauthproxy) | ||||
| } | ||||
							
								
								
									
										20
									
								
								outpost/pkg/server/api_uag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								outpost/pkg/server/api_uag.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| package server | ||||
|  | ||||
| import "net/http" | ||||
|  | ||||
| func SetUserAgent(inner http.RoundTripper, userAgent string) http.RoundTripper { | ||||
| 	return &addUGA{ | ||||
| 		inner: inner, | ||||
| 		Agent: userAgent, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type addUGA struct { | ||||
| 	inner http.RoundTripper | ||||
| 	Agent string | ||||
| } | ||||
|  | ||||
| func (ug *addUGA) RoundTrip(r *http.Request) (*http.Response, error) { | ||||
| 	r.Header.Set("User-Agent", ug.Agent) | ||||
| 	return ug.inner.RoundTrip(r) | ||||
| } | ||||
							
								
								
									
										117
									
								
								outpost/pkg/server/api_ws.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								outpost/pkg/server/api_ws.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/BeryJu/authentik/outpost/pkg" | ||||
| 	"github.com/go-openapi/strfmt" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"github.com/recws-org/recws" | ||||
| ) | ||||
|  | ||||
| func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) { | ||||
| 	pathTemplate := "%s://%s/ws/outpost/%s/" | ||||
| 	scheme := strings.ReplaceAll(pbURL.Scheme, "http", "ws") | ||||
|  | ||||
| 	authHeader := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("Basic :%s", ac.token))) | ||||
|  | ||||
| 	header := http.Header{ | ||||
| 		"Authorization": []string{authHeader}, | ||||
| 		"User-Agent":    []string{fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)}, | ||||
| 	} | ||||
|  | ||||
| 	value, set := os.LookupEnv("AUTHENTIK_INSECURE") | ||||
| 	if !set { | ||||
| 		value = "false" | ||||
| 	} | ||||
|  | ||||
| 	ws := &recws.RecConn{ | ||||
| 		NonVerbose: true, | ||||
| 		TLSClientConfig: &tls.Config{ | ||||
| 			InsecureSkipVerify: strings.ToLower(value) == "true", | ||||
| 		}, | ||||
| 	} | ||||
| 	ws.Dial(fmt.Sprintf(pathTemplate, scheme, pbURL.Host, outpostUUID.String()), header) | ||||
|  | ||||
| 	ac.logger.WithField("component", "ws").WithField("outpost", outpostUUID.String()).Debug("connecting to authentik") | ||||
|  | ||||
| 	ac.wsConn = ws | ||||
| 	// Send hello message with our version | ||||
| 	msg := websocketMessage{ | ||||
| 		Instruction: WebsocketInstructionHello, | ||||
| 		Args: map[string]interface{}{ | ||||
| 			"version": pkg.VERSION, | ||||
| 		}, | ||||
| 	} | ||||
| 	err := ws.WriteJSON(msg) | ||||
| 	if err != nil { | ||||
| 		ac.logger.WithField("component", "ws").WithError(err).Warning("Failed to hello to authentik") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Shutdown Gracefully stops all workers, disconnects from websocket | ||||
| func (ac *APIController) Shutdown() { | ||||
| 	// Cleanly close the connection by sending a close message and then | ||||
| 	// waiting (with timeout) for the server to close the connection. | ||||
| 	err := ac.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) | ||||
| 	if err != nil { | ||||
| 		ac.logger.Println("write close:", err) | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (ac *APIController) startWSHandler() { | ||||
| 	notConnectedBackoff := 1 | ||||
| 	for { | ||||
| 		if !ac.wsConn.IsConnected() { | ||||
| 			notConnectedWait := time.Duration(notConnectedBackoff) * time.Second | ||||
| 			ac.logger.WithField("loop", "ws-handler").WithField("wait", notConnectedWait).Info("Not connected, trying again...") | ||||
| 			time.Sleep(notConnectedWait) | ||||
| 			notConnectedBackoff += notConnectedBackoff | ||||
| 			continue | ||||
| 		} | ||||
| 		var wsMsg websocketMessage | ||||
| 		err := ac.wsConn.ReadJSON(&wsMsg) | ||||
| 		if err != nil { | ||||
| 			ac.logger.WithField("loop", "ws-handler").Println("read:", err) | ||||
| 			ac.wsConn.CloseAndReconnect() | ||||
| 			continue | ||||
| 		} | ||||
| 		if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { | ||||
| 			time.Sleep(ac.reloadOffset) | ||||
| 			err := ac.UpdateIfRequired() | ||||
| 			if err != nil { | ||||
| 				ac.logger.WithField("loop", "ws-handler").WithError(err).Debug("Failed to update") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ac *APIController) startWSHealth() { | ||||
| 	for ; true; <-time.Tick(time.Second * 10) { | ||||
| 		if !ac.wsConn.IsConnected() { | ||||
| 			continue | ||||
| 		} | ||||
| 		aliveMsg := websocketMessage{ | ||||
| 			Instruction: WebsocketInstructionHello, | ||||
| 			Args: map[string]interface{}{ | ||||
| 				"version": pkg.VERSION, | ||||
| 			}, | ||||
| 		} | ||||
| 		err := ac.wsConn.WriteJSON(aliveMsg) | ||||
| 		ac.logger.WithField("loop", "ws-health").Debug("hello'd") | ||||
| 		if err != nil { | ||||
| 			ac.logger.WithField("loop", "ws-health").Println("write:", err) | ||||
| 			ac.wsConn.CloseAndReconnect() | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								outpost/pkg/server/api_ws_msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								outpost/pkg/server/api_ws_msg.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| package server | ||||
|  | ||||
| type websocketInstruction int | ||||
|  | ||||
| const ( | ||||
| 	// WebsocketInstructionAck Code used to acknowledge a previous message | ||||
| 	WebsocketInstructionAck websocketInstruction = 0 | ||||
| 	// WebsocketInstructionHello Code used to send a healthcheck keepalive | ||||
| 	WebsocketInstructionHello websocketInstruction = 1 | ||||
| 	// WebsocketInstructionTriggerUpdate Code received to trigger a config update | ||||
| 	WebsocketInstructionTriggerUpdate websocketInstruction = 2 | ||||
| ) | ||||
|  | ||||
| type websocketMessage struct { | ||||
| 	Instruction websocketInstruction   `json:"instruction"` | ||||
| 	Args        map[string]interface{} `json:"args"` | ||||
| } | ||||
							
								
								
									
										63
									
								
								outpost/pkg/server/cert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								outpost/pkg/server/cert.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"crypto/x509/pkix" | ||||
| 	"encoding/pem" | ||||
| 	"math/big" | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func generateSelfSignedCert() (tls.Certificate, error) { | ||||
|  | ||||
| 	priv, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to generate private key: %v", err) | ||||
| 		return tls.Certificate{}, err | ||||
| 	} | ||||
|  | ||||
| 	keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | ||||
|  | ||||
| 	notBefore := time.Now() | ||||
| 	notAfter := notBefore.Add(365 * 24 * time.Hour) | ||||
|  | ||||
| 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||||
| 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to generate serial number: %v", err) | ||||
| 		return tls.Certificate{}, err | ||||
| 	} | ||||
|  | ||||
| 	template := x509.Certificate{ | ||||
| 		SerialNumber: serialNumber, | ||||
| 		Subject: pkix.Name{ | ||||
| 			Organization: []string{"authentik"}, | ||||
| 			CommonName:   "authentik Proxy default certificate", | ||||
| 		}, | ||||
| 		NotBefore: notBefore, | ||||
| 		NotAfter:  notAfter, | ||||
|  | ||||
| 		KeyUsage:              keyUsage, | ||||
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||||
| 		BasicConstraintsValid: true, | ||||
| 	} | ||||
|  | ||||
| 	template.DNSNames = []string{"*"} | ||||
|  | ||||
| 	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) | ||||
| 	privBytes, err := x509.MarshalPKCS8PrivateKey(priv) | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	privPemByes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) | ||||
| 	return tls.X509KeyPair(pemBytes, privPemByes) | ||||
| } | ||||
							
								
								
									
										122
									
								
								outpost/pkg/server/middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								outpost/pkg/server/middleware.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // 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 | ||||
| } | ||||
|  | ||||
| // Header returns the ResponseWriter's Header | ||||
| func (l *responseLogger) Header() http.Header { | ||||
| 	return l.w.Header() | ||||
| } | ||||
|  | ||||
| // Support Websocket | ||||
| func (l *responseLogger) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) { | ||||
| 	if hj, ok := l.w.(http.Hijacker); ok { | ||||
| 		return hj.Hijack() | ||||
| 	} | ||||
| 	return nil, nil, errors.New("http.Hijacker is not available on writer") | ||||
| } | ||||
|  | ||||
| // 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 != "" { | ||||
| 		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") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| 		l.status = http.StatusOK | ||||
| 	} | ||||
| 	l.ExtractGAPMetadata() | ||||
| 	size, err := l.w.Write(b) | ||||
| 	l.size += size | ||||
| 	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 the response size | ||||
| func (l *responseLogger) Size() int { | ||||
| 	return l.size | ||||
| } | ||||
|  | ||||
| // Flush sends any buffered data to the client | ||||
| func (l *responseLogger) Flush() { | ||||
| 	if flusher, ok := l.w.(http.Flusher); ok { | ||||
| 		flusher.Flush() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // loggingHandler is the http.Handler implementation for LoggingHandler | ||||
| type loggingHandler struct { | ||||
| 	handler http.Handler | ||||
| 	logger  *log.Entry | ||||
| } | ||||
|  | ||||
| // LoggingHandler provides an http.Handler which logs requests to the HTTP server | ||||
| func LoggingHandler(h http.Handler) http.Handler { | ||||
| 	return loggingHandler{ | ||||
| 		handler: h, | ||||
| 		logger:  log.WithField("component", "http-server"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	t := time.Now() | ||||
| 	url := *req.URL | ||||
| 	responseLogger := &responseLogger{w: w} | ||||
| 	h.handler.ServeHTTP(responseLogger, req) | ||||
| 	duration := float64(time.Since(t)) / float64(time.Second) | ||||
| 	h.logger.WithFields(log.Fields{ | ||||
| 		"Client":          req.RemoteAddr, | ||||
| 		"Host":            req.Host, | ||||
| 		"Protocol":        req.Proto, | ||||
| 		"RequestDuration": fmt.Sprintf("%0.3f", duration), | ||||
| 		"RequestMethod":   req.Method, | ||||
| 		"ResponseSize":    responseLogger.Size(), | ||||
| 		"StatusCode":      responseLogger.Status(), | ||||
| 		"Timestamp":       t, | ||||
| 		"Upstream":        responseLogger.upstream, | ||||
| 		"UserAgent":       req.UserAgent(), | ||||
| 		"Username":        responseLogger.authInfo, | ||||
| 	}).Info(url.RequestURI()) | ||||
| 	// logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, , ) | ||||
| } | ||||
							
								
								
									
										150
									
								
								outpost/pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								outpost/pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // Server represents an HTTP server | ||||
| type Server struct { | ||||
| 	Handlers map[string]*providerBundle | ||||
|  | ||||
| 	stop   chan struct{} // channel for waiting shutdown | ||||
| 	logger *log.Entry | ||||
|  | ||||
| 	defaultCert tls.Certificate | ||||
| } | ||||
|  | ||||
| // NewServer initialise a new HTTP Server | ||||
| func NewServer() *Server { | ||||
| 	defaultCert, err := generateSelfSignedCert() | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	return &Server{ | ||||
| 		Handlers:    make(map[string]*providerBundle), | ||||
| 		logger:      log.WithField("component", "http-server"), | ||||
| 		defaultCert: defaultCert, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ServeHTTP constructs a net.Listener and starts handling HTTP requests | ||||
| func (s *Server) ServeHTTP() { | ||||
| 	listenAddress := "0.0.0.0:4180" | ||||
| 	listener, err := net.Listen("tcp", listenAddress) | ||||
| 	if err != nil { | ||||
| 		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) | ||||
| 	} | ||||
| 	s.logger.Printf("listening on %s", listener.Addr()) | ||||
| 	s.serve(listener) | ||||
| 	s.logger.Printf("closing %s", listener.Addr()) | ||||
| } | ||||
|  | ||||
| func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||
| 	handler, ok := s.Handlers[info.ServerName] | ||||
| 	if !ok { | ||||
| 		s.logger.WithField("server-name", info.ServerName).Debug("Handler does not exist") | ||||
| 		return &s.defaultCert, nil | ||||
| 	} | ||||
| 	if handler.cert == nil { | ||||
| 		s.logger.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate") | ||||
| 		return &s.defaultCert, nil | ||||
| 	} | ||||
| 	return handler.cert, nil | ||||
| } | ||||
|  | ||||
| // ServeHTTPS constructs a net.Listener and starts handling HTTPS requests | ||||
| func (s *Server) ServeHTTPS() { | ||||
| 	listenAddress := "0.0.0.0:4443" | ||||
| 	config := &tls.Config{ | ||||
| 		MinVersion:     tls.VersionTLS12, | ||||
| 		MaxVersion:     tls.VersionTLS12, | ||||
| 		GetCertificate: s.getCertificates, | ||||
| 	} | ||||
|  | ||||
| 	ln, err := net.Listen("tcp", listenAddress) | ||||
| 	if err != nil { | ||||
| 		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) | ||||
| 	} | ||||
| 	s.logger.Printf("listening on %s", ln.Addr()) | ||||
|  | ||||
| 	tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) | ||||
| 	s.serve(tlsListener) | ||||
| 	s.logger.Printf("closing %s", tlsListener.Addr()) | ||||
| } | ||||
|  | ||||
| func (s *Server) handler(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.URL.Path == "/akprox/ping" { | ||||
| 		w.WriteHeader(204) | ||||
| 		return | ||||
| 	} | ||||
| 	handler, ok := s.Handlers[r.Host] | ||||
| 	if !ok { | ||||
| 		// If we only have one handler, host name switching doesn't matter | ||||
| 		if len(s.Handlers) == 1 { | ||||
| 			for k := range s.Handlers { | ||||
| 				s.Handlers[k].ServeHTTP(w, r) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 		s.logger.WithField("host", r.Host).Debug("Host header does not match any we know of") | ||||
| 		s.logger.Printf("%v+\n", s.Handlers) | ||||
| 		w.WriteHeader(400) | ||||
| 		return | ||||
| 	} | ||||
| 	s.logger.WithField("host", r.Host).Debug("passing request from host head") | ||||
| 	handler.ServeHTTP(w, r) | ||||
| } | ||||
|  | ||||
| func (s *Server) serve(listener net.Listener) { | ||||
| 	srv := &http.Server{Handler: http.HandlerFunc(s.handler)} | ||||
|  | ||||
| 	// See https://golang.org/pkg/net/http/#Server.Shutdown | ||||
| 	idleConnsClosed := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		<-s.stop // wait notification for stopping server | ||||
|  | ||||
| 		// We received an interrupt signal, shut down. | ||||
| 		if err := srv.Shutdown(context.Background()); err != nil { | ||||
| 			// Error from closing listeners, or context timeout: | ||||
| 			s.logger.Printf("HTTP server Shutdown: %v", err) | ||||
| 		} | ||||
| 		close(idleConnsClosed) | ||||
| 	}() | ||||
|  | ||||
| 	err := srv.Serve(listener) | ||||
| 	if err != nil && !errors.Is(err, http.ErrServerClosed) { | ||||
| 		s.logger.Errorf("ERROR: http.Serve() - %s", err) | ||||
| 	} | ||||
| 	<-idleConnsClosed | ||||
| } | ||||
|  | ||||
| // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted | ||||
| // connections. It's used by ListenAndServe and ListenAndServeTLS so | ||||
| // dead TCP connections (e.g. closing laptop mid-download) eventually | ||||
| // go away. | ||||
| type tcpKeepAliveListener struct { | ||||
| 	*net.TCPListener | ||||
| } | ||||
|  | ||||
| func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { | ||||
| 	tc, err := ln.AcceptTCP() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	err = tc.SetKeepAlive(true) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error setting Keep-Alive: %v", err) | ||||
| 	} | ||||
| 	err = tc.SetKeepAlivePeriod(3 * time.Minute) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error setting Keep-Alive period: %v", err) | ||||
| 	} | ||||
| 	return tc, nil | ||||
| } | ||||
							
								
								
									
										3
									
								
								outpost/pkg/version.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								outpost/pkg/version.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| package pkg | ||||
|  | ||||
| const VERSION = "0.14.2-stable" | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer