view controllers/externalwfs.go @ 345:b97b3172c61a

Add staging feature to more tables Added tables currently only have limited visibility for waterway_user but not yet policies for write access.
author Tom Gottfried <tom@intevation.de>
date Mon, 06 Aug 2018 15:19:05 +0200
parents e98033e3683a
children ad0e47c1fedf
line wrap: on
line source

package controllers

import (
	"compress/gzip"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"strings"

	"github.com/gorilla/mux"
	"golang.org/x/net/html/charset"

	"gemma.intevation.de/gemma/config"
)

func copyHeader(dst, src http.Header) {
	for k, vv := range src {
		log.Printf("%s: %v\n", k, vv)
		for _, v := range vv {
			dst.Add(k, v)
		}
	}
}

func cloneHeader(h http.Header) http.Header {
	h2 := make(http.Header, len(h))
	for k, vv := range h {
		log.Printf("clone: %s: %v\n", k, vv)
		vv2 := make([]string, len(vv))
		copy(vv2, vv)
		h2[k] = vv2
	}
	return h2
}

// Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
var hopHeaders = []string{
	"Connection",
	"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
	"Keep-Alive",
	"Proxy-Authenticate",
	"Proxy-Authorization",
	"Te",      // canonicalized version of "TE"
	"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
	"Transfer-Encoding",
	"Upgrade",
}

// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See RFC 2616, section 14.10.
func removeConnectionHeaders(h http.Header) {
	if c := h.Get("Connection"); c != "" {
		for _, f := range strings.Split(c, ",") {
			if f = strings.TrimSpace(f); f != "" {
				h.Del(f)
			}
		}
	}
}

func isXML(h http.Header) bool {
	for _, t := range h["Content-Type"] {
		t = strings.ToLower(t)
		if strings.Contains(t, "text/xml") ||
			strings.Contains(t, "application/xml") {
			return true
		}
	}
	return false
}

func externalWFSProxy(rw http.ResponseWriter, req *http.Request) {

	external := config.ExternalWFSs()
	if external == nil || len(external) == 0 {
		http.NotFound(rw, req)
		return
	}
	wfs := mux.Vars(req)["wfs"]

	alias, found := external[wfs]
	if !found {
		http.NotFound(rw, req)
		return
	}
	data, ok := alias.(map[string]interface{})
	if !ok {
		log.Printf("error: badly configured external wfs %s\n", wfs)
		http.Error(rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
		return
	}

	urlS, found := data["url"]
	if !found {
		log.Printf("error: missinf url fore xternal wfs %s\n", wfs)
		http.Error(rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
		return
	}

	prefix, ok := urlS.(string)
	if !ok {
		log.Printf("error: badly configured url for external wfs %s\n", wfs)
		http.Error(rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
		return
	}

	log.Printf("%v\n", prefix)
	url := prefix + "?" + req.URL.RawQuery
	log.Printf("%v\n", url)

	remoteReq, err := http.NewRequest(req.Method, url, req.Body)
	if err != nil {
		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusBadRequest)
		return
	}

	remoteReq.Header = cloneHeader(req.Header)
	removeConnectionHeaders(remoteReq.Header)

	// Remove hop-by-hop headers to the backend. Especially
	// important is "Connection" because we want a persistent
	// connection, regardless of what the client sent to us.
	for _, h := range hopHeaders {
		if remoteReq.Header.Get(h) != "" {
			remoteReq.Header.Del(h)
		}
	}

	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		// If we aren't the first proxy retain prior
		// X-Forwarded-For information as a comma+space
		// separated list and fold multiple headers into one.
		if prior, ok := remoteReq.Header["X-Forwarded-For"]; ok {
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
		}
		remoteReq.Header.Set("X-Forwarded-For", clientIP)
		log.Printf("X-Forwarded-For: %s\n", clientIP)
	}

	log.Printf("req: %v\n", remoteReq)

	resp, err := http.DefaultTransport.RoundTrip(remoteReq)
	//client := &http.Client{}
	//resp, err := client.Do(remoteReq)
	if err != nil {
		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusBadRequest)
		return
	}

	log.Printf("%v\n", resp.Header)

	xml := isXML(resp.Header)
	log.Printf("is xml: %t\n", xml)

	gzipped := strings.Contains(resp.Header.Get("Content-Encoding"), "gzip")
	if gzipped {
		resp.Header.Del("Content-Encoding")
	}

	removeConnectionHeaders(resp.Header)
	copyHeader(rw.Header(), resp.Header)

	rw.WriteHeader(resp.StatusCode)

	defer resp.Body.Close()

	if xml {
		to := useHTTPS(req) + "://" + req.Host
		if !strings.HasPrefix(req.URL.Path, "/") {
			to += "/"
		}
		to += req.URL.Path
		var r io.Reader = resp.Body
		if gzipped {
			var err error
			r, err = gzip.NewReader(resp.Body)
			if err != nil {
				log.Printf("gzip error: %v\n", err)
				http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusBadGateway)
				return
			}
		} else {
			r = resp.Body
		}
		log.Printf("rewrite %s to: %s\n", prefix, to)
		err = rewrite(rw, r, prefix, to)
	} else {
		log.Printf("no rewrite")
		_, err = io.Copy(rw, resp.Body)
	}

	if err != nil {
		log.Printf("copy error: %v\n", err)
	}
}

func rewrite(w io.Writer, r io.Reader, from, to string) 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 ns nsdef

tokens:
	for {
		tok, err := decoder.Token()
		switch {
		case tok == nil && err == io.EOF:
			break tokens
		case err != nil:
			return err
		}

		switch t := tok.(type) {
		case xml.StartElement:
			ns = ns.push()
			t = t.Copy()

			attr := make([]xml.Attr, len(t.Attr))

			//var lns string
			for i, at := range t.Attr {
				switch {
				case at.Name.Space == "xmlns":
					ns.define(at.Value, at.Name.Local)
					attr[i] = xml.Attr{Name: at.Name, Value: at.Value}
				default:
					attr[i] = xml.Attr{Name: at.Name, Value: replace(at.Value)}
				}
			}
			if s := ns.lookup(t.Name.Space); s != "" {
				t.Name.Space = ""
				t.Name.Local = s + ":" + t.Name.Local
			}
			t.Attr = attr
			tok = t

		case xml.CharData:
			tok = xml.CharData(replace(string(t)))

		case xml.EndElement:
			//log.Printf("lookup %s -> %s\n", t.Name.Space, ns.lookup(t.Name.Space))
			if s := ns.lookup(t.Name.Space); s != "" {
				t.Name.Space = ""
				t.Name.Local = s + ":" + t.Name.Local
				tok = t
			}
			ns = ns.pop()
		}
		if err := encoder.EncodeToken(tok); err != nil {
			return err
		}
	}

	return encoder.Flush()
}

type nsdef []map[string]string

func (n nsdef) lookup(ns string) string {
	for i := len(n) - 1; i >= 0; i-- {
		if s := n[i][ns]; s != "" {
			return s
		}
	}
	return ""
}

func (n nsdef) push() nsdef {
	return append(n, make(map[string]string))
}

func (n nsdef) pop() nsdef {
	if l := len(n); l > 0 {
		n[l-1] = nil
		n = n[:l-1]
	}
	return n
}

func (n nsdef) define(ns, s string) {
	if n != nil {
		n[len(n)-1][ns] = s
	}
}