view controllers/externalwfs.go @ 349:56f6c5ab0f3d

Made WFS proxy rewriting work (to some degrees).
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 06 Aug 2018 17:50:45 +0200
parents 9543ca97aa70
children 1ea90a22bd15
line wrap: on
line source

package controllers

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

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

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

type RoundTripFunc func(*http.Request) (*http.Response, error)

func (rtf RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return rtf(req)
}

func externalWFSDirector(req *http.Request) {

	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")
	}
	wfs := mux.Vars(req)["wfs"]

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

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

	log.Printf("%v\n", prefix)
	nURL := prefix + "?" + 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 := useHTTPS(req) + "://" + req.Host + "/api/externalwfs/" + wfs
	req.Header.Set("X-Gemma-To", to)

	req.Host = u.Host

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

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

	xml := isXML(resp.Header)

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

	if xml {
		log.Printf("rewrite from %s to %s\n", from, to)

		pr, pw := io.Pipe()

		var r io.ReadCloser
		var w io.WriteCloser
		if gzipped {
			var err error
			r, err = gzip.NewReader(resp.Body)
			if err != nil {
				return err
			}
			w = gzip.NewWriter(pw)
		} else {
			r = resp.Body
			w = pw
		}

		go func(force io.ReadCloser) {
			defer func() {
				w.Close()
				pw.Close()
				force.Close()
			}()
			/*
				if _, err := io.Copy(pw, r); err != nil {
					log.Printf("rewrite failed: %v\n", err)
					return
				}
			*/
			if err := rewrite(w, r, from, to); err != nil {
				log.Printf("rewrite failed: %v\n", err)
				return
			}
			log.Println("rewrite successful")
		}(resp.Body)

		resp.Body = pr
	}
	return nil
}

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