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)
-}