pomerium/proxy/proxy.go
2019-05-25 15:06:32 -04:00

269 lines
8.2 KiB
Go
Executable file

package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"encoding/base64"
"errors"
"fmt"
"html/template"
stdlog "log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/proxy/clients"
)
const (
// HeaderJWT is the header key containing JWT signed user details.
HeaderJWT = "x-pomerium-jwt-assertion"
// HeaderUserID is the header key containing the user's id.
HeaderUserID = "x-pomerium-authenticated-user-id"
// HeaderEmail is the header key containing the user's email.
HeaderEmail = "x-pomerium-authenticated-user-email"
// HeaderGroups is the header key containing the user's groups.
HeaderGroups = "x-pomerium-authenticated-user-groups"
)
// ValidateOptions checks that proper configuration settings are set to create
// a proper Proxy instance
func ValidateOptions(o *config.Options) error {
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
if err != nil {
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
}
if len(decoded) != 32 {
return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded))
}
if len(o.Policies) == 0 {
return errors.New("missing setting: no policies defined")
}
if o.AuthenticateURL == nil {
return errors.New("missing setting: authenticate-service-url")
}
if o.AuthenticateURL.Scheme != "https" {
return errors.New("authenticate-service-url must be a valid https url")
}
if o.AuthorizeURL == nil {
return errors.New("missing setting: authorize-service-url")
}
if o.AuthorizeURL.Scheme != "https" {
return errors.New("authorize-service-url must be a valid https url")
}
if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret")
}
decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret)
if err != nil {
return fmt.Errorf("cookie secret is invalid base64: %v", err)
}
if len(decodedCookieSecret) != 32 {
return fmt.Errorf("cookie secret expects 32 bytes but got %d", len(decodedCookieSecret))
}
if len(o.SigningKey) != 0 {
_, err := base64.StdEncoding.DecodeString(o.SigningKey)
if err != nil {
return fmt.Errorf("signing key is invalid base64: %v", err)
}
}
return nil
}
// Proxy stores all the information associated with proxying a request.
type Proxy struct {
SharedKey string
// authenticate service
AuthenticateURL *url.URL
AuthenticateClient clients.Authenticator
// authorize service
AuthorizeClient clients.Authorizer
// session
cipher cryptutil.Cipher
csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore
redirectURL *url.URL
templates *template.Template
routeConfigs map[string]*routeConfig
}
type routeConfig struct {
mux http.Handler
policy policy.Policy
}
// New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate.
func New(opts *config.Options) (*Proxy, error) {
if opts == nil {
return nil, errors.New("options cannot be nil")
}
if err := ValidateOptions(opts); err != nil {
return nil, err
}
// error explicitly handled by validate
decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
cipher, err := cryptutil.NewCipher(decodedSecret)
if err != nil {
return nil, fmt.Errorf("cookie-secret error: %s", err.Error())
}
cookieStore, err := sessions.NewCookieStore(
&sessions.CookieStoreOptions{
Name: opts.CookieName,
CookieDomain: opts.CookieDomain,
CookieSecure: opts.CookieSecure,
CookieHTTPOnly: opts.CookieHTTPOnly,
CookieExpire: opts.CookieExpire,
CookieCipher: cipher,
})
if err != nil {
return nil, err
}
p := &Proxy{
routeConfigs: make(map[string]*routeConfig),
// services
AuthenticateURL: opts.AuthenticateURL,
// session state
cipher: cipher,
csrfStore: cookieStore,
sessionStore: cookieStore,
SharedKey: opts.SharedKey,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
}
for _, route := range opts.Policies {
proxy := NewReverseProxy(route.Destination)
handler, err := NewReverseProxyHandler(opts, proxy, &route)
if err != nil {
return nil, err
}
p.Handle(route.Source.Host, handler, &route)
log.Info().Str("src", route.Source.Host).Str("dst", route.Destination.Host).Msg("proxy: new route")
}
p.AuthenticateClient, err = clients.NewAuthenticateClient("grpc",
&clients.Options{
Addr: opts.AuthenticateURL.Host,
InternalAddr: opts.AuthenticateInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
CAFile: opts.CAFile,
})
if err != nil {
return nil, err
}
p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc",
&clients.Options{
Addr: opts.AuthorizeURL.Host,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
CAFile: opts.CAFile,
})
return p, err
}
// UpstreamProxy stores information for proxying the request to the upstream.
type UpstreamProxy struct {
name string
cookieName string
handler http.Handler
signer cryptutil.JWTSigner
}
// deleteUpstreamCookies deletes the session cookie from the request header string.
func deleteUpstreamCookies(req *http.Request, cookieName string) {
headers := []string{}
for _, cookie := range req.Cookies() {
if cookie.Name != cookieName {
headers = append(headers, cookie.String())
}
}
req.Header.Set("Cookie", strings.Join(headers, ";"))
}
func (u *UpstreamProxy) signRequest(r *http.Request) {
if u.signer != nil {
jwt, err := u.signer.SignJWT(
r.Header.Get(HeaderUserID),
r.Header.Get(HeaderEmail),
r.Header.Get(HeaderGroups))
if err == nil {
r.Header.Set(HeaderJWT, jwt)
}
}
}
// ServeHTTP signs the http request and deletes cookie headers
// before calling the upstream's ServeHTTP function.
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
deleteUpstreamCookies(r, u.cookieName)
u.signRequest(r)
u.handler.ServeHTTP(w, r)
}
// NewReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and
// base path provided in target. NewReverseProxy rewrites the Host header.
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to)
sublogger := log.With().Str("proxy", to.Host).Logger()
proxy.ErrorLog = stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0)
// todo(bdd): default is already http.DefaultTransport)
// proxy.Transport = defaultUpstreamTransport
director := proxy.Director
proxy.Director = func(req *http.Request) {
// Identifies the originating IP addresses of a client connecting to
// a web server through an HTTP proxy or a load balancer.
req.Header.Add("X-Forwarded-Host", req.Host)
director(req)
req.Host = to.Host
}
return proxy
}
// NewReverseProxyHandler applies handler specific options to a given route.
func NewReverseProxyHandler(o *config.Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
up := &UpstreamProxy{
name: route.Destination.Host,
handler: proxy,
cookieName: o.CookieName,
}
if len(o.SigningKey) != 0 {
decodedSigningKey, _ := base64.StdEncoding.DecodeString(o.SigningKey)
signer, err := cryptutil.NewES256Signer(decodedSigningKey, route.Source.Host)
if err != nil {
return nil, err
}
up.signer = signer
}
timeout := o.DefaultUpstreamTimeout
if route.UpstreamTimeout != 0 {
timeout = route.UpstreamTimeout
}
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout)
return http.TimeoutHandler(up, timeout, timeoutMsg), nil
}
// urlParse wraps url.Parse to add a scheme if none-exists.
// https://github.com/golang/go/issues/12585
func urlParse(uri string) (*url.URL, error) {
if !strings.Contains(uri, "://") {
uri = fmt.Sprintf("https://%s", uri)
}
return url.ParseRequestURI(uri)
}