Mercurial > gemma
changeset 408:ac23905e64b1
Improve WFS proxy a lot. It now generates signed re-writings.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Wed, 15 Aug 2018 15:55:41 +0200 |
parents | cffd144c99ea |
children | cdd63547930a |
files | auth/session.go common/random.go config/config.go controllers/externalwfs.go controllers/pwreset.go controllers/routes.go misc/random.go |
diffstat | 7 files changed, 197 insertions(+), 131 deletions(-) [+] |
line wrap: on
line diff
--- a/auth/session.go Wed Aug 15 15:14:47 2018 +0200 +++ b/auth/session.go Wed Aug 15 15:55:41 2018 +0200 @@ -5,6 +5,7 @@ "io" "time" + "gemma.intevation.de/gemma/common" "gemma.intevation.de/gemma/misc" ) @@ -74,7 +75,7 @@ func GenerateSessionKey() string { return base64.URLEncoding.EncodeToString( - misc.GenerateRandomKey(sessionKeyLength)) + common.GenerateRandomKey(sessionKeyLength)) } func GenerateSession(user, password string) (string, *Session, error) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/random.go Wed Aug 15 15:55:41 2018 +0200 @@ -0,0 +1,48 @@ +package common + +import ( + "bytes" + "crypto/rand" + "io" + "log" + "math/big" +) + +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +func RandomString(n int) string { + + const ( + special = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" + + special + ) + + max := big.NewInt(int64(len(alphabet))) + out := make([]byte, n) + + for i := 0; i < 1000; i++ { + for i := range out { + v, err := rand.Int(rand.Reader, max) + if err != nil { + log.Panicf("error: %v\n", err) + } + out[i] = alphabet[v.Int64()] + } + // Ensure at least one special char. + if bytes.IndexAny(out, special) >= 0 { + return string(out) + } + } + log.Println("warn: Your random generator may be broken.") + out[0] = special[0] + return string(out) +}
--- a/config/config.go Wed Aug 15 15:14:47 2018 +0200 +++ b/config/config.go Wed Aug 15 15:55:41 2018 +0200 @@ -1,11 +1,16 @@ package config import ( + "crypto/sha256" + "fmt" "log" + "sync" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" + + "gemma.intevation.de/gemma/common" ) // This is not part of the persistent config. @@ -44,6 +49,40 @@ func GeoServerPassword() string { return viper.GetString("geoserver-password") } func GeoServerTables() []string { return viper.GetStringSlice("geoserver-tables") } +var ( + proxyKeyOnce sync.Once + proxyKey []byte + + proxyPrefixOnce sync.Once + proxyPrefix string +) + +func ProxyKey() []byte { + fetchKey := func() { + if proxyKey == nil { + key := []byte(viper.GetString("proxy-key")) + if len(key) == 0 { + key = common.GenerateRandomKey(64) + } + hash := sha256.New() + hash.Write(key) + proxyKey = hash.Sum(nil) + } + } + proxyKeyOnce.Do(fetchKey) + return proxyKey +} + +func ProxyPrefix() string { + fetchPrefix := func() { + if proxyPrefix == "" { + proxyPrefix = fmt.Sprintf("http://%s:%d", WebHost(), WebPort()) + } + } + proxyPrefixOnce.Do(fetchPrefix) + return proxyPrefix +} + var RootCmd = &cobra.Command{ Use: "gemma", Short: "gemma is a server for waterway monitoring and management", @@ -115,6 +154,10 @@ str("geoserver-user", "admin", "GeoServer user") str("geoserver-password", "geoserver", "GeoServer password") strSl("geoserver-tables", geoTables, "tables to publish with GeoServer") + + str("proxy-key", "", `signing key for proxy URLs. Defaults to random key.`) + str("proxy-prefix", "", `URL prefix of proxy. Defaults to "http://${web-host}:${web-port}"`) + } func initConfig() {
--- a/controllers/externalwfs.go Wed Aug 15 15:14:47 2018 +0200 +++ b/controllers/externalwfs.go Wed Aug 15 15:55:41 2018 +0200 @@ -3,104 +3,110 @@ import ( "compress/flate" "compress/gzip" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "encoding/xml" "io" "io/ioutil" "log" "net/http" "net/url" + "regexp" "strings" "time" + "gemma.intevation.de/gemma/config" "github.com/gorilla/mux" "golang.org/x/net/html/charset" - - "gemma.intevation.de/gemma/config" ) -// roundTripFunc is a helper type to make externalWFSDirector a http.RoundTripper. -type roundTripFunc func(*http.Request) (*http.Response, error) - -func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return rtf(req) +// proxyBlackList is a set of URLs that should not be rewritten by the proxy. +var proxyBlackList = map[string]struct{}{ + "http://www.w3.org/2001/XMLSchema-instance": struct{}{}, + "http://www.w3.org/1999/xlink": struct{}{}, + "http://www.w3.org/2001/XMLSchema": struct{}{}, + "http://www.w3.org/XML/1998/namespace": struct{}{}, + "http://www.opengis.net/wfs/2.0": struct{}{}, + "http://www.opengis.net/ows/1.1": struct{}{}, + "http://www.opengis.net/gml/3.2": struct{}{}, + "http://www.opengis.net/fes/2.0": struct{}{}, + "http://schemas.opengis.net/gml": struct{}{}, } -func externalWFSDirector(req *http.Request) { +func findEntry(entry string) (string, bool) { + external := config.ExternalWFSs() + if external == nil || len(external) == 0 { + return "", false + } + alias, found := external[entry] + if !found { + return "", false + } + data, ok := alias.(map[string]interface{}) + if !ok { + return "", false + } + urlS, found := data["url"] + if !found { + return "", false + } + url, ok := urlS.(string) + return url, ok +} + +func proxyDirector(req *http.Request) { + + log.Printf("proxyDirector: %s\n", req.RequestURI) abort := func(format string, args ...interface{}) { log.Printf(format, args...) panic(http.ErrAbortHandler) } - external := config.ExternalWFSs() - if external == nil || len(external) == 0 { - abort("No external WFS proxy config found\n") - } vars := mux.Vars(req) - wfs := vars["wfs"] - rest := vars["rest"] + + var s string - log.Printf("rest: %s\n", rest) + if entry, found := vars["entry"]; found { + if s, found = findEntry(entry); !found { + abort("Cannot find entry '%s'\n", entry) + } + } else { + expectedMAC, err := base64.URLEncoding.DecodeString(vars["hash"]) + if err != nil { + abort("Cannot base64 decode hash: %v\n", err) + } + url, err := base64.URLEncoding.DecodeString(vars["url"]) + if err != nil { + abort("Cannot base64 decode url: %v\n", err) + } - alias, found := external[wfs] - if !found { - abort("No config found for %s\n", wfs) - } - data, ok := alias.(map[string]interface{}) - if !ok { - abort("error: badly configured external WFS %s\n", wfs) + mac := hmac.New(sha256.New, config.ProxyKey()) + mac.Write(url) + messageMAC := mac.Sum(nil) + + s = string(url) + + if !hmac.Equal(messageMAC, expectedMAC) { + abort("HMAC of URL %s failed.\n", s) + } } - urlS, found := data["url"] - if !found { - abort("error: missing url for external WFS %s\n", wfs) - } - - prefix, ok := urlS.(string) - if !ok { - abort("error: badly configured url for external WFS %s\n", wfs) - } - - https := useHTTPS(req) - - log.Printf("%v\n", prefix) - nURL := prefix + "/" + rest + "?" + req.URL.RawQuery - log.Printf("%v\n", nURL) + nURL := s + "?" + req.URL.RawQuery + //log.Printf("%v\n", nURL) u, err := url.Parse(nURL) if err != nil { abort("Invalid url: %v\n", err) } req.URL = u - req.Header.Set("X-Gemma-From", prefix) - to := https + "://" + req.Host + "/api/externalwfs/" + wfs - req.Header.Set("X-Gemma-To", to) req.Host = u.Host - + //req.Header.Del("If-None-Match") //log.Printf("headers: %v\n", req.Header) } -func externalWFSTransport(req *http.Request) (*http.Response, error) { - - from := req.Header.Get("X-Gemma-From") - to := req.Header.Get("X-Gemma-To") - req.Header.Del("X-Gemma-From") - req.Header.Del("X-Gemma-To") - - // To prevent some caching effects. - req.Header.Del("If-None-Match") - - resp, err := http.DefaultTransport.RoundTrip(req) - if err != nil { - return nil, err - } - resp.Header.Set("X-Gemma-From", from) - resp.Header.Set("X-Gemma-To", to) - - return resp, err -} - type nopCloser struct { io.Writer } @@ -145,19 +151,12 @@ } } -func externalWFSModifyResponse(resp *http.Response) error { - - from := resp.Header.Get("X-Gemma-From") - to := resp.Header.Get("X-Gemma-To") - resp.Header.Del("X-Gemma-From") - resp.Header.Del("X-Gemma-To") +func proxyModifyResponse(resp *http.Response) error { if !isXML(resp.Header) { return nil } - log.Printf("rewrite from %s to %s\n", from, to) - pr, pw := io.Pipe() var ( @@ -185,7 +184,7 @@ force.Close() log.Printf("rewrite took %s\n", time.Since(start)) }() - if err := rewrite(w, r, from, to); err != nil { + if err := rewrite(w, r); err != nil { log.Printf("rewrite failed: %v\n", err) return } @@ -215,17 +214,35 @@ return false } -func rewrite(w io.Writer, r io.Reader, from, to string) error { +var replaceRe = regexp.MustCompile(`\b(https?://[^\s\?]*)`) + +func replace(s string) string { + + proxyKey := config.ProxyKey() + proxyPrefix := config.ProxyPrefix() + "/api/proxy/" + + return replaceRe.ReplaceAllStringFunc(s, func(s string) string { + if _, found := proxyBlackList[s]; found { + return s + } + mac := hmac.New(sha256.New, proxyKey) + b := []byte(s) + mac.Write(b) + expectedMAC := mac.Sum(nil) + + hash := base64.URLEncoding.EncodeToString(expectedMAC) + enc := base64.URLEncoding.EncodeToString(b) + return proxyPrefix + hash + "/" + enc + }) +} + +func rewrite(w io.Writer, r io.Reader) error { decoder := xml.NewDecoder(r) decoder.CharsetReader = charset.NewReaderLabel encoder := xml.NewEncoder(w) - replace := func(s string) string { - return strings.Replace(s, from, to, -1) - } - var n nsdef tokens:
--- a/controllers/pwreset.go Wed Aug 15 15:14:47 2018 +0200 +++ b/controllers/pwreset.go Wed Aug 15 15:55:41 2018 +0200 @@ -14,6 +14,7 @@ "github.com/gorilla/mux" "gemma.intevation.de/gemma/auth" + "gemma.intevation.de/gemma/common" "gemma.intevation.de/gemma/config" "gemma.intevation.de/gemma/misc" ) @@ -155,7 +156,7 @@ } func generateHash() string { - return hex.EncodeToString(misc.GenerateRandomKey(hashLength)) + return hex.EncodeToString(common.GenerateRandomKey(hashLength)) } func generateNewPassword() string { @@ -165,7 +166,7 @@ return strings.TrimSpace(string(out)) } // Use internal generator. - return misc.RandomString(20) + return common.RandomString(20) } func passwordResetRequest(
--- a/controllers/routes.go Wed Aug 15 15:14:47 2018 +0200 +++ b/controllers/routes.go Wed Aug 15 15:55:41 2018 +0200 @@ -52,13 +52,17 @@ }).Methods(http.MethodGet) // Proxy for external WFSs. - externalWFSProxy := &httputil.ReverseProxy{ - Director: externalWFSDirector, - Transport: roundTripFunc(externalWFSTransport), - ModifyResponse: externalWFSModifyResponse, + proxy := &httputil.ReverseProxy{ + Director: proxyDirector, + ModifyResponse: proxyModifyResponse, } - api.Handle("/externalwfs/{wfs}/{rest:.*}", externalWFSProxy). + api.Handle(`/proxy/{hash}/{url}`, proxy). + Methods( + http.MethodGet, http.MethodPost, + http.MethodPut, http.MethodDelete) + + api.Handle("/proxy/{entry}", proxy). Methods( http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete)
--- a/misc/random.go Wed Aug 15 15:14:47 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -package misc - -import ( - "bytes" - "crypto/rand" - "io" - "log" - "math/big" -) - -func GenerateRandomKey(length int) []byte { - k := make([]byte, length) - if _, err := io.ReadFull(rand.Reader, k); err != nil { - return nil - } - return k -} - -func RandomString(n int) string { - - const ( - special = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" - alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + - "abcdefghijklmnopqrstuvwxyz" + - "0123456789" + - special - ) - - max := big.NewInt(int64(len(alphabet))) - out := make([]byte, n) - - for i := 0; i < 1000; i++ { - for i := range out { - v, err := rand.Int(rand.Reader, max) - if err != nil { - log.Panicf("error: %v\n", err) - } - out[i] = alphabet[v.Int64()] - } - // Ensure at least one special char. - if bytes.IndexAny(out, special) >= 0 { - return string(out) - } - } - log.Println("warn: Your random generator may be broken.") - out[0] = special[0] - return string(out) -}