diff --git a/cmd/drone-server/flags.go b/cmd/drone-server/flags.go index 965872169e..efb7cc83c0 100644 --- a/cmd/drone-server/flags.go +++ b/cmd/drone-server/flags.go @@ -1,4 +1,5 @@ // Copyright 2019 Laszlo Fogas +// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +12,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. package main @@ -31,6 +34,11 @@ var flags = []cli.Flag{ Name: "server-host", Usage: "server fully qualified url (://)", }, + cli.StringFlag{ + EnvVar: "WOODPECKER_HOST_INTERNAL", + Name: "server-host-internal", + Usage: "server internal fully qualified url (://)", + }, cli.StringFlag{ EnvVar: "DRONE_SERVER_ADDR,WOODPECKER_SERVER_ADDR", Name: "server-addr", @@ -500,4 +508,14 @@ var flags = []cli.Flag{ Name: "keepalive-min-time", Usage: "server-side enforcement policy on the minimum amount of time a client should wait before sending a keepalive ping.", }, + cli.BoolFlag{ + EnvVar: "WOODPECKER_GITEA_REV_PROXY_AUTH", + Name: "gitea-rev-proxy-auth", + Usage: "enable gitea authentication using HTTP header specified in WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER", + }, + cli.StringFlag{ + EnvVar: "WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER", + Name: "gitea-rev-proxy-auth-header", + Usage: "HTTP header with gitea authenticated user login", + }, } diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index 8c909e0594..472595b104 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -1,4 +1,5 @@ // Copyright 2018 Drone.IO Inc. +// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +12,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. package main @@ -75,6 +78,18 @@ func server(c *cli.Context) error { ) } + if c.String("server-host-internal") != "" && !strings.Contains(c.String("server-host-internal"), "://") { + logrus.Fatalln( + "WOODPECKER_HOST_INTERNAL must be :// format", + ) + } + + if c.String("server-host-internal") != "" && strings.HasSuffix(c.String("server-host-internal"), "/") { + logrus.Fatalln( + "WOODPECKER_HOST_INTERNAL must not have trailing slash", + ) + } + remote_, err := SetupRemote(c) if err != nil { logrus.Fatal(err) @@ -225,6 +240,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) { droneserver.Config.Server.Key = c.String("server-key") droneserver.Config.Server.Pass = c.String("agent-secret") droneserver.Config.Server.Host = strings.TrimRight(c.String("server-host"), "/") + droneserver.Config.Server.HostInternal = strings.TrimRight(c.String("server-host-internal"), "/") droneserver.Config.Server.Port = c.String("server-addr") droneserver.Config.Server.RepoConfig = c.String("repo-config") droneserver.Config.Server.SessionExpires = c.Duration("session-expires") diff --git a/cmd/drone-server/setup.go b/cmd/drone-server/setup.go index 8650309104..006896c3b8 100644 --- a/cmd/drone-server/setup.go +++ b/cmd/drone-server/setup.go @@ -1,4 +1,5 @@ // Copyright 2018 Drone.IO Inc. +// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +12,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. package main @@ -119,12 +122,14 @@ func setupGogs(c *cli.Context) (remote.Remote, error) { func setupGitea(c *cli.Context) (remote.Remote, error) { if !c.IsSet("gitea-client") { return gitea.New(gitea.Opts{ - URL: c.String("gitea-server"), - Context: c.String("gitea-context"), - Username: c.String("gitea-git-username"), - Password: c.String("gitea-git-password"), - PrivateMode: c.Bool("gitea-private-mode"), - SkipVerify: c.Bool("gitea-skip-verify"), + URL: c.String("gitea-server"), + Context: c.String("gitea-context"), + Username: c.String("gitea-git-username"), + Password: c.String("gitea-git-password"), + PrivateMode: c.Bool("gitea-private-mode"), + SkipVerify: c.Bool("gitea-skip-verify"), + RevProxyAuth: c.Bool("gitea-rev-proxy-auth"), + RevProxyAuthHeader: c.String("gitea-rev-proxy-auth-header"), }) } return gitea.NewOauth(gitea.Opts{ diff --git a/remote/gitea/gitea.go b/remote/gitea/gitea.go index a552dcf50e..56766b6431 100644 --- a/remote/gitea/gitea.go +++ b/remote/gitea/gitea.go @@ -31,24 +31,29 @@ import ( // Opts defines configuration options. type Opts struct { - URL string // Gitea server url. - Context string // Context to display in status check - Client string // OAuth2 Client ID - Secret string // OAuth2 Client Secret - Username string // Optional machine account username. - Password string // Optional machine account password. - PrivateMode bool // Gitea is running in private mode. - SkipVerify bool // Skip ssl verification. + URL string // Gitea server url. + Context string // Context to display in status check + Client string // OAuth2 Client ID + Secret string // OAuth2 Client Secret + Username string // Optional machine account username. + Password string // Optional machine account password. + PrivateMode bool // Gitea is running in private mode. + SkipVerify bool // Skip ssl verification. + RevProxyAuth bool // Enable reverse proxy authentication using RevProxyAuthHeader. + RevProxyAuthHeader string // Name of HTTP header with username for reverse proxy authentication. } type client struct { - URL string - Context string - Machine string - Username string - Password string - PrivateMode bool - SkipVerify bool + URL string + Context string + Machine string + Username string + Password string + PrivateMode bool + SkipVerify bool + RevProxyAuth bool + RevProxyAuthHeader string + RevProxyAuthHeaderValue string } const ( @@ -117,26 +122,36 @@ func New(opts Opts) (remote.Remote, error) { url.Host = host } return &client{ - URL: opts.URL, - Context: opts.Context, - Machine: url.Host, - Username: opts.Username, - Password: opts.Password, - PrivateMode: opts.PrivateMode, - SkipVerify: opts.SkipVerify, + URL: opts.URL, + Context: opts.Context, + Machine: url.Host, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + RevProxyAuth: opts.RevProxyAuth, + RevProxyAuthHeader: opts.RevProxyAuthHeader, }, nil } // Login authenticates an account with Gitea using basic authentication. The // Gitea account details are returned when the user is successfully authenticated. func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { - var ( + + var username string + var password string + + if c.RevProxyAuth { + username = req.Header.Get(c.RevProxyAuthHeader) + c.RevProxyAuthHeaderValue = username + password = "" + } else { username = req.FormValue("username") password = req.FormValue("password") - ) + } - // if the username or password is empty we re-direct to the login screen. - if len(username) == 0 || len(password) == 0 { + // Redirect to the login screen if user credentials are incomplete. + if len(username) == 0 || (!c.RevProxyAuth && len(password) == 0) { http.Redirect(res, req, "/login/form", http.StatusSeeOther) return nil, nil } @@ -394,6 +409,22 @@ func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return parseHook(r) } +// authTransport forwards authentication HTTP header to gitea. +type authTransport struct { + headerName string + headerValue string + underlyingTransport http.RoundTripper +} + +func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add(t.headerName, t.headerValue) + if t.underlyingTransport != nil { + return t.underlyingTransport.RoundTrip(req) + } else { + return http.DefaultTransport.RoundTrip(req) + } +} + // helper function to return the Gitea client with Token func (c *client) newClientToken(token string) (*gitea.Client, error) { httpClient := &http.Client{} @@ -402,6 +433,15 @@ func (c *client) newClientToken(token string) (*gitea.Client, error) { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } + // Forward authentication header in every HTTP request to Gitea + // in reverse proxy authentication mode. + if c.RevProxyAuth { + httpClient.Transport = &authTransport{ + headerName: c.RevProxyAuthHeader, + headerValue: c.RevProxyAuthHeaderValue, + underlyingTransport: httpClient.Transport, + } + } return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient)) } diff --git a/server/repo.go b/server/repo.go index 0288cb9195..c6356b489b 100644 --- a/server/repo.go +++ b/server/repo.go @@ -1,4 +1,5 @@ // Copyright 2018 Drone.IO Inc. +// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +12,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. package server @@ -73,9 +76,14 @@ func PostRepo(c *gin.Context) { return } + host := httputil.GetURL(c.Request) + if Config.Server.HostInternal != "" { + host = Config.Server.HostInternal + } + link := fmt.Sprintf( "%s/hook?access_token=%s", - httputil.GetURL(c.Request), + host, sig, ) @@ -203,7 +211,12 @@ func DeleteRepo(c *gin.Context) { } } - remote.Deactivate(user, repo, httputil.GetURL(c.Request)) + host := httputil.GetURL(c.Request) + if Config.Server.HostInternal != "" { + host = Config.Server.HostInternal + } + + remote.Deactivate(user, repo, host) c.JSON(200, repo) } @@ -222,6 +235,10 @@ func RepairRepo(c *gin.Context) { // reconstruct the link host := httputil.GetURL(c.Request) + if Config.Server.HostInternal != "" { + host = Config.Server.HostInternal + } + link := fmt.Sprintf( "%s/hook?access_token=%s", host, @@ -308,6 +325,10 @@ func MoveRepo(c *gin.Context) { // reconstruct the link host := httputil.GetURL(c.Request) + if Config.Server.HostInternal != "" { + host = Config.Server.HostInternal + } + link := fmt.Sprintf( "%s/hook?access_token=%s", host, diff --git a/server/rpc.go b/server/rpc.go index 42b4b22ccb..2c218ec477 100644 --- a/server/rpc.go +++ b/server/rpc.go @@ -71,6 +71,7 @@ var Config = struct { Key string Cert string Host string + HostInternal string Port string Pass string RepoConfig string diff --git a/shared/token/token.go b/shared/token/token.go index 0753a18464..b1b4f98e26 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -1,4 +1,5 @@ // Copyright 2018 Drone.IO Inc. +// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +12,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. package token @@ -53,12 +56,15 @@ func Parse(raw string, fn SecretFunc) (*Token, error) { func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { var token = r.Header.Get("Authorization") - // first we attempt to get the token from the + // First we attempt to get the token from the // authorization header. if len(token) != 0 { token = r.Header.Get("Authorization") fmt.Sscanf(token, "Bearer %s", &token) - return Parse(token, fn) + + if parsedToken, err := Parse(token, fn); err == nil { + return parsedToken, nil + } } // then we attempt to get the token from the