Merge c52cfa2a6b
into cfa7d348a2
This commit is contained in:
commit
96a6683f35
177
main.go
177
main.go
|
@ -6,9 +6,9 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pborman/uuid"
|
|
||||||
"github.com/golang/groupcache/lru"
|
"github.com/golang/groupcache/lru"
|
||||||
"github.com/namsral/flag"
|
"github.com/namsral/flag"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
@ -20,56 +20,70 @@ import (
|
||||||
|
|
||||||
var nonceCache = lru.New(20)
|
var nonceCache = lru.New(20)
|
||||||
|
|
||||||
func main() {
|
type Config struct {
|
||||||
|
ListenUriPtr *string
|
||||||
|
ProxyUriPtr *string
|
||||||
|
OriginUriPtr *string
|
||||||
|
SsoSecretPtr *string
|
||||||
|
SsoUriPtr *string
|
||||||
|
BasicAuthPtr *string
|
||||||
|
UsernameHeaderPtr *string
|
||||||
|
CookieSecret string
|
||||||
|
}
|
||||||
|
|
||||||
listenUriPtr := flag.String("listen-url", "", "uri to listen on eg: localhost:2001. leave blank to set equal to proxy-url")
|
func main() {
|
||||||
proxyUriPtr := flag.String("proxy-url", "", "outer url of this host eg: http://secrets.example.com")
|
config := new(Config)
|
||||||
originUriPtr := flag.String("origin-url", "", "origin to proxy eg: http://localhost:2002")
|
|
||||||
ssoSecretPtr := flag.String("sso-secret", "", "SSO secret for origin")
|
config.ListenUriPtr = flag.String("listen-url", "", "uri to listen on eg: localhost:2001. leave blank to set equal to proxy-url")
|
||||||
ssoUriPtr := flag.String("sso-url", "", "SSO endpoint eg: http://discourse.forum.com")
|
config.ProxyUriPtr = flag.String("proxy-url", "", "outer url of this host eg: http://secrets.example.com")
|
||||||
|
config.OriginUriPtr = flag.String("origin-url", "", "origin to proxy eg: http://localhost:2002")
|
||||||
|
config.SsoSecretPtr = flag.String("sso-secret", "", "SSO secret for origin")
|
||||||
|
config.SsoUriPtr = flag.String("sso-url", "", "SSO endpoint eg: http://discourse.forum.com")
|
||||||
|
config.BasicAuthPtr = flag.String("basic-auth", "", "HTTP Basic authentication credentials to let through directly")
|
||||||
|
config.UsernameHeaderPtr = flag.String("username-header", "Discourse-User-Name", "Request header to pass authenticated username into")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
originUrl, err := url.Parse(*originUriPtr)
|
originUrl, err := url.Parse(*config.OriginUriPtr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
log.Fatal("invalid origin url")
|
log.Fatal("invalid origin url")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = url.Parse(*ssoUriPtr)
|
_, err = url.Parse(*config.SsoUriPtr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
log.Fatal("invalid sso url, should point at Discourse site with enable sso")
|
log.Fatal("invalid sso url, should point at Discourse site with enable sso")
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyUrl, err2 := url.Parse(*proxyUriPtr)
|
proxyUrl, err2 := url.Parse(*config.ProxyUriPtr)
|
||||||
|
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
log.Fatal("invalid proxy uri")
|
log.Fatal("invalid proxy uri")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *listenUriPtr == "" {
|
if *config.ListenUriPtr == "" {
|
||||||
log.Println("Defaulting to listening on the proxy url")
|
log.Println("Defaulting to listening on the proxy url")
|
||||||
*listenUriPtr = proxyUrl.Host
|
*config.ListenUriPtr = proxyUrl.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
if *proxyUriPtr == "" || *originUriPtr == "" || *ssoSecretPtr == "" || *ssoUriPtr == "" || *listenUriPtr == "" {
|
if *config.ProxyUriPtr == "" || *config.OriginUriPtr == "" || *config.SsoSecretPtr == "" || *config.SsoUriPtr == "" || *config.ListenUriPtr == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieSecret := uuid.New()
|
config.CookieSecret = uuid.New()
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(originUrl)
|
proxy := httputil.NewSingleHostReverseProxy(originUrl)
|
||||||
|
|
||||||
handler := redirectIfCookieMissing(proxy, *ssoSecretPtr, cookieSecret, *ssoUriPtr, *proxyUriPtr)
|
handler := authProxyHandler(proxy, config)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: *listenUriPtr,
|
Addr: *config.ListenUriPtr,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
|
@ -79,59 +93,96 @@ func main() {
|
||||||
log.Fatal(server.ListenAndServe())
|
log.Fatal(server.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoUri, proxyHost string) http.Handler {
|
func authProxyHandler(handler http.Handler, config *Config) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie("__discourse_proxy")
|
if checkAuthorizationHeader(handler, r, w, config) {
|
||||||
|
|
||||||
var username string
|
|
||||||
|
|
||||||
if err == nil && cookie != nil {
|
|
||||||
username, err = parseCookie(cookie.Value, cookieSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
r.Header.Set("Discourse-User-Name", username)
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
redirectIfNoCookie(handler, r, w, config)
|
||||||
query := r.URL.Query()
|
|
||||||
sso := query.Get("sso")
|
|
||||||
sig := query.Get("sig")
|
|
||||||
|
|
||||||
if len(sso) == 0 {
|
|
||||||
url := ssoUri + "/session/sso_provider?" + sso_payload(ssoSecret, proxyHost, r.URL.String())
|
|
||||||
http.Redirect(w, r, url, 302)
|
|
||||||
} else {
|
|
||||||
decoded, _ := base64.StdEncoding.DecodeString(sso)
|
|
||||||
decodedString := string(decoded)
|
|
||||||
parsedQuery, _ := url.ParseQuery(decodedString)
|
|
||||||
|
|
||||||
username := parsedQuery["username"]
|
|
||||||
admin := parsedQuery["admin"]
|
|
||||||
nonce := parsedQuery["nonce"]
|
|
||||||
|
|
||||||
if len(nonce) > 0 && len(admin) > 0 && len(username) > 0 && admin[0] == "true" {
|
|
||||||
returnUrl, err := getReturnUrl(ssoSecret, sso, sig, nonce[0])
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(w, "Invalid request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have a valid auth
|
|
||||||
expiration := time.Now().Add(365 * 24 * time.Hour)
|
|
||||||
|
|
||||||
cookie := http.Cookie{Name: "__discourse_proxy", Value: signCookie(username[0], cookieSecret), Expires: expiration, HttpOnly: true}
|
|
||||||
http.SetCookie(w, &cookie)
|
|
||||||
// works around weird safari stuff
|
|
||||||
fmt.Fprintf(w, "<html><head></head><body><script>window.location = '%v'</script></body>", returnUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkAuthorizationHeader(handler http.Handler, r *http.Request, w http.ResponseWriter, config *Config) bool {
|
||||||
|
if *config.BasicAuthPtr == "" {
|
||||||
|
// Can't auth if we don't have anything to auth against
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header := r.Header.Get("Authorization")
|
||||||
|
if len(auth_header) < 6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth_header[0:6] == "Basic " {
|
||||||
|
b_creds, _ := base64.StdEncoding.DecodeString(auth_header[6:])
|
||||||
|
creds := string(b_creds)
|
||||||
|
if creds == *config.BasicAuthPtr {
|
||||||
|
colon_idx := strings.Index(creds, ":")
|
||||||
|
if colon_idx == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
username := creds[0:colon_idx]
|
||||||
|
r.Header.Set(*config.UsernameHeaderPtr, username)
|
||||||
|
r.Header.Del("Authorization")
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectIfNoCookie(handler http.Handler, r *http.Request, w http.ResponseWriter, config *Config) {
|
||||||
|
cookie, err := r.Cookie("__discourse_proxy")
|
||||||
|
|
||||||
|
var username string
|
||||||
|
|
||||||
|
if err == nil && cookie != nil {
|
||||||
|
username, err = parseCookie(cookie.Value, config.CookieSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
r.Header.Set(*config.UsernameHeaderPtr, username)
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
sso := query.Get("sso")
|
||||||
|
sig := query.Get("sig")
|
||||||
|
|
||||||
|
if len(sso) == 0 {
|
||||||
|
url := *config.SsoUriPtr + "/session/sso_provider?" + sso_payload(*config.SsoSecretPtr, *config.ProxyUriPtr, r.URL.String())
|
||||||
|
http.Redirect(w, r, url, 302)
|
||||||
|
} else {
|
||||||
|
decoded, _ := base64.StdEncoding.DecodeString(sso)
|
||||||
|
decodedString := string(decoded)
|
||||||
|
parsedQuery, _ := url.ParseQuery(decodedString)
|
||||||
|
|
||||||
|
username := parsedQuery["username"]
|
||||||
|
admin := parsedQuery["admin"]
|
||||||
|
nonce := parsedQuery["nonce"]
|
||||||
|
|
||||||
|
if len(nonce) > 0 && len(admin) > 0 && len(username) > 0 && admin[0] == "true" {
|
||||||
|
returnUrl, err := getReturnUrl(*config.SsoSecretPtr, sso, sig, nonce[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "Invalid request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have a valid auth
|
||||||
|
expiration := time.Now().Add(365 * 24 * time.Hour)
|
||||||
|
|
||||||
|
cookie := http.Cookie{Name: "__discourse_proxy", Value: signCookie(username[0], config.CookieSecret), Expires: expiration, HttpOnly: true}
|
||||||
|
http.SetCookie(w, &cookie)
|
||||||
|
|
||||||
|
// works around weird safari stuff
|
||||||
|
fmt.Fprintf(w, "<html><head></head><body><script>window.location = '%v'</script></body>", returnUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getReturnUrl(secret string, payload string, sig string, nonce string) (returnUrl string, err error) {
|
func getReturnUrl(secret string, payload string, sig string, nonce string) (returnUrl string, err error) {
|
||||||
value, gotNonce := nonceCache.Get(nonce)
|
value, gotNonce := nonceCache.Get(nonce)
|
||||||
returnUrl = value.(string)
|
returnUrl = value.(string)
|
||||||
|
|
Loading…
Reference in New Issue