aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDimitri Sokolyuk <demon@dim13.org>2016-03-30 13:31:08 +0200
committerDimitri Sokolyuk <demon@dim13.org>2016-03-30 13:31:08 +0200
commit7355b100f7419e74a46b5bf1465c8dbcb06bee7d (patch)
treee11c8755986cedd3b67e3c9b56c475aeaa66897a
parentc4a03a036e87ca921a105e923e156a491cc56738 (diff)
parent3071e59632c668696f8bf1c6d9e3bc07b61f6391 (diff)
Merge branch 'refactor'
Conflicts: main.go route.go
-rw-r--r--Dockerfile2
-rw-r--r--README.md17
-rw-r--r--cmd/goxy/main.go23
-rw-r--r--cmd/goxyctl/main.go74
-rw-r--r--data.go26
-rw-r--r--goxyctl/main.go87
-rw-r--r--main.go39
-rw-r--r--route.go75
-rw-r--r--rpc.go51
-rw-r--r--server.go64
-rw-r--r--server_test.go123
-rw-r--r--ws.go12
12 files changed, 359 insertions, 234 deletions
diff --git a/Dockerfile b/Dockerfile
index ae1638b..343a2e3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
FROM golang
ADD . /go/src/dim13.org/goxy
-RUN go install dim13.org/goxy
+RUN go install dim13.org/goxy/cmd/goxy
VOLUME /go/src/dim13.org/goxy/data
WORKDIR /go/src/dim13.org/goxy
ENTRYPOINT /go/bin/goxy
diff --git a/README.md b/README.md
index 8d9a1d8..2d6ddc5 100644
--- a/README.md
+++ b/README.md
@@ -60,3 +60,20 @@ Total: 1 100 223.2 37 1424
docker build -t goxy .
docker run -d --name goxy -p 192.168.243.5:80:80 -p 192.168.243.5:443:443 --net testnet goxy
+
+## Mapping
+
+### Host scheme
+ http://host/path -> http only
+ https://host/path -> http redirect to https, cert required
+ ws, wss -- ?
+
+### Upstream scheme
+ http://backend/path
+ https://backend/path
+ ws://backend/path
+ wss://backend/path
+
+## TODO
+- rewrite for 2 (3?) independend mappings (http, https, ws, wss)
+- improve testing
diff --git a/cmd/goxy/main.go b/cmd/goxy/main.go
new file mode 100644
index 0000000..f4ce583
--- /dev/null
+++ b/cmd/goxy/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "flag"
+ "log"
+
+ "dim13.org/goxy"
+
+ _ "net/http/pprof"
+)
+
+var data = flag.String("data", "data/goxy.json", "persistent storage file")
+
+func main() {
+ flag.Parse()
+
+ server, err := goxy.NewServer(*data)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Fatal(server.Start())
+}
diff --git a/cmd/goxyctl/main.go b/cmd/goxyctl/main.go
new file mode 100644
index 0000000..dd7c86d
--- /dev/null
+++ b/cmd/goxyctl/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "flag"
+ "io/ioutil"
+ "log"
+
+ "dim13.org/goxy"
+)
+
+var (
+ rpcserver = flag.String("server", ":http-alt", "RPC Server")
+ host = flag.String("host", "", "Host URL")
+ upstream = flag.String("up", "", "Upstream URL")
+ keyfile = flag.String("key", "", "TLS Key file")
+ certfile = flag.String("cert", "", "TLS Cert file")
+ remove = flag.Bool("remove", false, "Remove host")
+)
+
+func getEntry() (e goxy.Entry, err error) {
+ e.Host = *host
+ e.Upstream = *upstream
+ if *certfile != "" && *keyfile != "" {
+ e.Cert, err = ioutil.ReadFile(*certfile)
+ if err != nil {
+ return
+ }
+ e.Key, err = ioutil.ReadFile(*keyfile)
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+func send(server string, e goxy.Entry, del bool) error {
+ client, err := goxy.DialRPC(server)
+ if err != nil {
+ return err
+ }
+ defer client.Close()
+
+ switch {
+ case e.Host != "" && e.Upstream != "":
+ log.Println("Add", e)
+ return client.Call("GoXY.Add", e, nil)
+ case e.Host != "" && del:
+ log.Println("Del", e.Host)
+ return client.Call("GoXY.Del", e.Host, nil)
+ default:
+ var r goxy.Route
+ err := client.Call("GoXY.List", struct{}{}, &r)
+ if err != nil {
+ return err
+ }
+ for k, v := range r {
+ log.Println(k, v)
+ }
+ }
+ return nil
+}
+
+func main() {
+ flag.Parse()
+
+ e, err := getEntry()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if err := send(*rpcserver, e, *remove); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/data.go b/data.go
deleted file mode 100644
index 474afae..0000000
--- a/data.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package main
-
-import (
- "encoding/gob"
- "os"
-)
-
-// Save routes to persistent file
-func (r Route) Save(fname string) error {
- fd, err := os.Create(fname)
- if err != nil {
- return err
- }
- defer fd.Close()
- return gob.NewEncoder(fd).Encode(r)
-}
-
-// Load routes from persistent file
-func (r *Route) Load(fname string) error {
- fd, err := os.Open(fname)
- if err != nil {
- return err
- }
- defer fd.Close()
- return gob.NewDecoder(fd).Decode(r)
-}
diff --git a/goxyctl/main.go b/goxyctl/main.go
deleted file mode 100644
index 5fbc1e8..0000000
--- a/goxyctl/main.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package main
-
-import (
- "flag"
- "io/ioutil"
- "log"
- "net/rpc"
-)
-
-var (
- rpcserver = flag.String("server", ":http-alt", "RPC Server port")
- servername = flag.String("host", "", "ServerName")
- upstream = flag.String("upstream", "", "Upstream URL")
- keyfile = flag.String("key", "", "TLS Key file")
- certfile = flag.String("cert", "", "TLS Cert file")
- remove = flag.Bool("remove", false, "Remove route")
-)
-
-// Entry contains routing settings
-type Entry struct {
- ServerName string
- Upstream string
- Cert []byte
- Key []byte
-}
-
-func (e Entry) String() string {
- ret := e.ServerName + " → " + e.Upstream
- if e.Cert != nil && e.Key != nil {
- ret += " with TLS"
- }
- return ret
-}
-
-func loadCert(certFile, keyFile string) ([]byte, []byte) {
- if certFile == "" || keyFile == "" {
- return nil, nil
- }
- cert, err := ioutil.ReadFile(certFile)
- if err != nil {
- log.Fatal(err)
- }
- key, err := ioutil.ReadFile(keyFile)
- if err != nil {
- log.Fatal(err)
- }
- return cert, key
-}
-
-func send(server string, e Entry, del bool) error {
- client, err := rpc.DialHTTP("tcp", server)
- if err != nil {
- return err
- }
- defer client.Close()
-
- switch {
- case e.ServerName != "" && e.Upstream != "":
- log.Println("Add", e)
- return client.Call("GoXY.Add", e, nil)
- case e.ServerName != "" && del:
- log.Println("Del", e)
- return client.Call("GoXY.Del", e, nil)
- default:
- var r []Entry
- err = client.Call("GoXY.List", struct{}{}, &r)
- if err != nil {
- return err
- }
- for _, e := range r {
- log.Println(e)
- }
- }
- return nil
-}
-
-func main() {
- var e Entry
- flag.Parse()
-
- e.ServerName, e.Upstream = *servername, *upstream
- e.Cert, e.Key = loadCert(*certfile, *keyfile)
-
- if err := send(*rpcserver, e, *remove); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index 89dca80..0000000
--- a/main.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package main
-
-import (
- "crypto/tls"
- "flag"
- "log"
- "net/http"
-
- _ "net/http/pprof"
-)
-
-var (
- data = flag.String("data", "data/goxy.gob", "persistent storage file")
- route = make(Route)
- server = http.Server{
- Handler: http.NewServeMux(),
- TLSConfig: &tls.Config{GetCertificate: route.GetCertificate},
- }
-)
-
-func main() {
- flag.Parse()
-
- if err := route.Load(*data); err != nil {
- log.Println(err)
- }
-
- if err := route.Restore(); err != nil {
- log.Fatal(err)
- }
-
- errc := make(chan error)
-
- go func() { errc <- server.ListenAndServe() }()
- go func() { errc <- server.ListenAndServeTLS("", "") }()
- go func() { errc <- http.ListenAndServe(":http-alt", nil) }()
-
- log.Fatal(<-errc)
-}
diff --git a/route.go b/route.go
index acc3d4e..93a18d3 100644
--- a/route.go
+++ b/route.go
@@ -1,26 +1,15 @@
-package main
+package goxy
import (
"crypto/tls"
+ "encoding/json"
"errors"
- "net/http"
- "net/http/httputil"
- "net/url"
- "strings"
+ "os"
)
// Route defines a set of routes including correspondent TLS certificates
type Route map[string]Entry
-// Entry holds routing settings
-type Entry struct {
- ServerName string
- Upstream string
- Cert []byte
- Key []byte
- cert *tls.Certificate
-}
-
// GetCertificate returns certificate for SNI negotiation
func (r Route) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
if e, ok := r[h.ServerName]; ok && e.cert != nil {
@@ -29,47 +18,37 @@ func (r Route) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error)
return nil, errors.New("no cert for " + h.ServerName)
}
-func NewReverseProxy(target *url.URL) *httputil.ReverseProxy {
- director := func(req *http.Request) {
- req.URL.Scheme = target.Scheme
- req.URL.Host = target.Host
+// Save routes to persistent file
+func (r Route) Save(fname string) error {
+ fd, err := os.Create(fname)
+ if err != nil {
+ return err
}
- return &httputil.ReverseProxy{Director: director}
+ defer fd.Close()
+ return json.NewEncoder(fd).Encode(r)
}
-// Restore and update routes from in-memory state
-func (r Route) Restore() error {
- mux := http.NewServeMux()
- for k, v := range route {
- if v.Cert != nil && v.Key != nil {
- cert, err := tls.X509KeyPair(v.Cert, v.Key)
- if err != nil {
- return err
- }
- v.cert = &cert
- r[k] = v
- }
- up, err := url.Parse(v.Upstream)
- if err != nil {
- return err
- }
- if !strings.Contains(v.ServerName, "/") {
- v.ServerName += "/"
- }
- //mux.Handle(v.ServerName, httputil.NewSingleHostReverseProxy(up))
- switch up.Scheme {
- case "ws":
- mux.Handle(v.ServerName, NewWebSocketProxy(up))
- default:
- mux.Handle(v.ServerName, NewReverseProxy(up))
- }
+// Load routes from persistent file
+func (r *Route) Load(fname string) error {
+ fd, err := os.Open(fname)
+ if err != nil {
+ return err
}
- server.Handler = mux
- return nil
+ defer fd.Close()
+ return json.NewDecoder(fd).Decode(r)
+}
+
+// Entry holds routing settings
+type Entry struct {
+ Host string // HostName
+ Upstream string // URL
+ Cert []byte // PEM
+ Key []byte // PEM
+ cert *tls.Certificate // Parsed
}
func (e Entry) String() string {
- ret := e.ServerName + " → " + e.Upstream
+ ret := e.Host + " → " + e.Upstream
if e.cert != nil {
ret += " with TLS"
}
diff --git a/rpc.go b/rpc.go
index 62a6a3f..e6a674d 100644
--- a/rpc.go
+++ b/rpc.go
@@ -1,38 +1,45 @@
-package main
+package goxy
-import (
- "log"
- "net/rpc"
-)
+import "net/rpc"
-// GoXY defines RPC interface
-type GoXY struct{}
+type GoXY struct {
+ server *Server
+}
func init() {
- rpc.Register(GoXY{})
rpc.HandleHTTP()
}
+func RegisterRPC(s *Server) error {
+ return rpc.Register(&GoXY{s})
+}
+
+func DialRPC(server string) (*rpc.Client, error) {
+ return rpc.DialHTTP("tcp", server)
+}
+
// Add adds a new route
-func (GoXY) Add(e Entry, _ *struct{}) error {
- log.Println("Add route", e)
- defer route.Save(*data)
- route[e.ServerName] = e
- return route.Restore()
+func (s *GoXY) Add(e Entry, _ *struct{}) error {
+ defer s.server.Save(s.server.DataFile)
+ s.server.Route[e.Host] = e
+ return s.server.Update()
}
// Del removes a route
-func (GoXY) Del(e Entry, _ *struct{}) error {
- log.Println("Del route", e)
- defer route.Save(*data)
- delete(route, e.ServerName)
- return route.Restore()
+func (s *GoXY) Del(host string, _ *struct{}) error {
+ defer s.server.Save(s.server.DataFile)
+ delete(s.server.Route, host)
+ return s.server.Update()
+}
+
+// Get returns Entry
+func (s *GoXY) Get(host string, e *Entry) error {
+ *e = s.server.Route[host]
+ return nil
}
// List routes
-func (GoXY) List(_ struct{}, r *[]Entry) error {
- for _, v := range route {
- *r = append(*r, v)
- }
+func (s GoXY) List(_ struct{}, r *Route) error {
+ *r = s.server.Route
return nil
}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..9300497
--- /dev/null
+++ b/server.go
@@ -0,0 +1,64 @@
+package goxy
+
+import (
+ "crypto/tls"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strings"
+)
+
+type Server struct {
+ DataFile string
+ Route
+ http.Server
+}
+
+func NewServer(dataFile string) (*Server, error) {
+ r := make(Route)
+ s := http.Server{TLSConfig: &tls.Config{GetCertificate: r.GetCertificate}}
+ server := &Server{Route: r, Server: s, DataFile: dataFile}
+ if dataFile != "" {
+ server.Load(dataFile)
+ }
+ RegisterRPC(server)
+ return server, server.Update()
+}
+
+// Update routes from in-memory state
+func (s *Server) Update() error {
+ mux := http.NewServeMux()
+ for k, v := range s.Route {
+ if v.Cert != nil && v.Key != nil {
+ cert, err := tls.X509KeyPair(v.Cert, v.Key)
+ if err != nil {
+ return err
+ }
+ v.cert = &cert
+ s.Route[k] = v
+ }
+ up, err := url.Parse(v.Upstream)
+ if err != nil {
+ return err
+ }
+ if !strings.Contains(v.Host, "/") {
+ v.Host += "/"
+ }
+ switch up.Scheme {
+ case "ws":
+ mux.Handle(v.Host, NewWebSocketProxy(up))
+ default:
+ mux.Handle(v.Host, httputil.NewSingleHostReverseProxy(up))
+ }
+ }
+ s.Server.Handler = mux
+ return nil
+}
+
+func (s *Server) Start() error {
+ errc := make(chan error)
+ go func() { errc <- s.ListenAndServe() }()
+ go func() { errc <- s.ListenAndServeTLS("", "") }()
+ go func() { errc <- http.ListenAndServe(":http-alt", nil) }()
+ return <-errc
+}
diff --git a/server_test.go b/server_test.go
new file mode 100644
index 0000000..ee72cb0
--- /dev/null
+++ b/server_test.go
@@ -0,0 +1,123 @@
+package goxy
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "testing"
+
+ "golang.org/x/net/websocket"
+)
+
+const (
+ cannary = "hello from backend"
+ dataFile = "test.json"
+)
+
+func TestReverseProxy(t *testing.T) {
+ // Backend server
+ backServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, cannary)
+ }))
+ defer backServer.Close()
+
+ // Websocket echo server
+ wsServer := httptest.NewServer(websocket.Handler(func(ws *websocket.Conn) {
+ io.Copy(ws, ws)
+ }))
+ defer wsServer.Close()
+ wsURL, err := url.Parse(wsServer.URL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ // RPC Server
+ rpcServer := httptest.NewServer(nil)
+ defer rpcServer.Close()
+ rpcURL, err := url.Parse(rpcServer.URL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Frontend server
+ frontServer := httptest.NewServer(nil)
+ defer frontServer.Close()
+ frontURL, err := url.Parse(frontServer.URL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Initialize proxy server
+ server, err := NewServer(dataFile)
+ if err != nil {
+ t.Error(err)
+ }
+ defer os.Remove(dataFile)
+
+ // Add routing entries
+ rpcClient, err := DialRPC(rpcURL.Host)
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Test HTTP proxy
+ e := Entry{
+ Host: frontURL.Host,
+ Upstream: backServer.URL,
+ }
+ if err := rpcClient.Call("GoXY.Add", e, nil); err != nil {
+ t.Error(err)
+ }
+
+ frontServer.Config.Handler = server.Handler
+
+ resp, err := http.Get(frontServer.URL)
+ if err != nil {
+ t.Error(err)
+ }
+ defer resp.Body.Close()
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Error(err)
+ }
+
+ if string(b) != cannary {
+ t.Error("got", string(b), "expected", cannary)
+ }
+
+ rpcClient.Call("GoXY.Del", frontURL.Host, nil)
+
+ // Test WebSocket proxy
+ e = Entry{
+ Host: frontURL.Host,
+ Upstream: "ws://" + wsURL.Host,
+ }
+ if err := rpcClient.Call("GoXY.Add", e, nil); err != nil {
+ t.Error(err)
+ }
+
+ frontServer.Config.Handler = server.Handler
+
+ ws, err := websocket.Dial("ws://"+frontURL.Host, "", "http://localhost")
+ if err != nil {
+ t.Error(err)
+ }
+
+ if _, err := ws.Write([]byte(cannary)); err != nil {
+ t.Error(err)
+ }
+ msg := make([]byte, len(cannary))
+ if _, err := ws.Read(msg); err != nil {
+ t.Error(err)
+ }
+ if !bytes.Equal(msg, []byte(cannary)) {
+ t.Error("got", string(msg), "expected", cannary)
+ }
+
+ rpcClient.Call("GoXY.Del", frontURL.Host, nil)
+}
diff --git a/ws.go b/ws.go
index d9bfc3d..5d90254 100644
--- a/ws.go
+++ b/ws.go
@@ -1,11 +1,10 @@
-package main
+package goxy
import (
"io"
"net"
"net/http"
"net/url"
- "strings"
)
type WebSocketProxy struct {
@@ -58,12 +57,3 @@ func NewWebSocketProxy(target *url.URL) *WebSocketProxy {
}
return &WebSocketProxy{Director: director}
}
-
-func isWebsocket(req *http.Request) bool {
- conn := req.Header.Get("Connection")
- if strings.ToLower(conn) == "upgrade" {
- upgrade := req.Header.Get("Upgrade")
- return strings.ToLower(upgrade) == "websocket"
- }
- return false
-}