Merge pull request #7 from mmatczuk/mmt/ctrlmsg

proto: ControlMessage improvements
This commit is contained in:
Michał Matczuk 2017-01-23 15:58:32 +01:00 committed by GitHub
commit e70ee0d534
9 changed files with 67 additions and 95 deletions

View file

@ -1,4 +1,3 @@
1. `ControlMessage` `String()` function for better logging
1. Dynamic `AllowedClient` management
1. Client driven configuration, on connect client sends it's configuration, server just needs to know the certificate id
1. Cli and file configuration https://ngrok.com/docs#config

View file

@ -131,19 +131,27 @@ func (c *Client) dial(network, addr string, config *tls.Config) (net.Conn, error
func (c *Client) serveHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
c.log.Info("Handshake: hello from server")
c.log.Info("Connected to server: %s", r.RemoteAddr)
http.Error(w, "Nice to see you", http.StatusOK)
return
}
msg, err := proto.ParseControlMessage(r.Header)
if err != nil {
c.log.Warning("Parsing control message failed: %s", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.log.Debug("Start proxying %v", msg)
c.config.Proxy(w, r.Body, msg)
c.log.Debug("Done proxying %v", msg)
c.log.Debug("Start %s", msg)
switch msg.Action {
case proto.Proxy:
c.config.Proxy(w, r.Body, msg)
default:
c.log.Warning("Unknown action: %s", msg)
http.Error(w, err.Error(), http.StatusBadRequest)
}
c.log.Debug("Done %s", msg)
}
// Stop closes the connection between client and server. After stopping client

View file

@ -81,7 +81,6 @@ func (p *HTTPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *proto.ControlMessag
return
}
req.URL.Host = msg.ForwardedBy
req.URL.Path = msg.URLPath
p.ServeHTTP(rw, req)
}

16
proto/action_string.go Normal file
View file

@ -0,0 +1,16 @@
// Code generated by "stringer -type=Action"; DO NOT EDIT
package proto
import "fmt"
const _Action_name = "Proxy"
var _Action_index = [...]uint8{0, 5}
func (i Action) String() string {
if i < 0 || i >= Action(len(_Action_index)-1) {
return fmt.Sprintf("Action(%d)", i)
}
return _Action_name[_Action_index[i]:_Action_index[i+1]]
}

View file

@ -12,7 +12,7 @@ type Action int
// ControlMessage actions.
const (
RequestClientSession Action = iota
Proxy Action = iota
)
// ControlMessage headers
@ -21,8 +21,8 @@ const (
)
// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp",
// "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only),
// "unix", "unixgram" and "unixpacket".
// "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only),
// "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket".
const (
HTTP = "http"
TCP = "tcp"
@ -31,18 +31,23 @@ const (
UNIX = "unix"
)
// ControlMessage is sent from server to client to establish tunneled connection.
// ControlMessage is sent from server to client to establish tunneled
// connection.
type ControlMessage struct {
Action Action
Protocol string
ForwardedFor string
ForwardedBy string
URLPath string
}
var xffRegexp = regexp.MustCompile("(for|by|proto|path)=([^;$]+)")
func (c *ControlMessage) String() string {
return fmt.Sprintf("%s %s: %s<-%s", c.Action, c.Protocol, c.ForwardedBy, c.ForwardedFor)
}
// ParseControlMessage creates new ControlMessage based on "Forwarded" http header.
var xffRegexp = regexp.MustCompile("(for|proto|by)=([^;$]+)")
// ParseControlMessage creates new ControlMessage based on "Forwarded" http
// header.
func ParseControlMessage(h http.Header) (*ControlMessage, error) {
v := h.Get(ForwardedHeader)
if v == "" {
@ -59,22 +64,17 @@ func ParseControlMessage(h http.Header) (*ControlMessage, error) {
msg.ForwardedBy = i[2]
case "proto":
msg.Protocol = i[2]
case "path":
msg.URLPath = i[2]
}
}
return msg, nil
}
// WriteTo writes ControlMessage to "Forwarded" http header, "by" and "for" parameters
// take form of full IP and port.
// Update writes ControlMessage to "Forwarded" http header, "by" and "for"
// parameters take form of full IP and port.
//
// If the server receiving proxied requests requires some address-based functionality,
// this parameter MAY instead contain an IP address (and, potentially, a port number)
//
// See https://tools.ietf.org/html/rfc7239.
func (c *ControlMessage) WriteTo(h http.Header) {
h.Set(ForwardedHeader, fmt.Sprintf("for=%s; by=%s; proto=%s; path=%s",
c.ForwardedFor, c.ForwardedBy, c.Protocol, c.URLPath))
// See Forwarded header specification https://tools.ietf.org/html/rfc7239.
func (c *ControlMessage) Update(h http.Header) {
v := fmt.Sprintf("for=%s; proto=%s; by=%s", c.ForwardedFor, c.Protocol, c.ForwardedBy)
h.Set(ForwardedHeader, v)
}

View file

@ -11,11 +11,10 @@ func TestControlMessage_WriteParse(t *testing.T) {
Protocol: "tcp",
ForwardedFor: "127.0.0.1:58104",
ForwardedBy: "127.0.0.1:7777",
URLPath: "/some/path",
}
h := make(http.Header)
msg.WriteTo(h)
var h = http.Header{}
msg.Update(h)
actual, err := ParseControlMessage(h)
if err != nil {
t.Errorf("Parse error %s", err)

View file

@ -160,7 +160,7 @@ func (s *Server) handleClient(conn net.Conn) {
goto reject
}
s.log.Info("Client %q connected from %q", client.ID, conn.RemoteAddr().String())
s.log.Info("Connected to client %s (%q)", conn.RemoteAddr(), client.ID)
return
@ -207,7 +207,7 @@ func (s *Server) listen(l net.Listener, client *AllowedClient) {
l.Addr().Network(), conn.RemoteAddr(), l.Addr().String())
msg := &proto.ControlMessage{
Action: proto.RequestClientSession,
Action: proto.Proxy,
Protocol: l.Addr().Network(),
ForwardedFor: conn.RemoteAddr().String(),
ForwardedBy: l.Addr().String(),
@ -227,11 +227,7 @@ func (s *Server) proxyConn(host string, conn net.Conn, msg *proto.ControlMessage
defer pr.Close()
defer pw.Close()
req, err := http.NewRequest(http.MethodPut, clientURL(host), pr)
if err != nil {
return fmt.Errorf("request creation error: %s", err)
}
msg.WriteTo(req.Header)
req := proxyRequest(host, msg, pr)
go transfer("local to remote", pw, conn)
@ -262,11 +258,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// RoundTrip is http.RoundTriper implementation.
func (s *Server) RoundTrip(r *http.Request) (*http.Response, error) {
msg := &proto.ControlMessage{
Action: proto.RequestClientSession,
Action: proto.Proxy,
Protocol: proto.HTTP,
ForwardedFor: r.RemoteAddr,
ForwardedBy: r.Host,
URLPath: r.URL.Path,
}
return s.proxyHTTP(trimPort(r.Host), r, msg)
}
@ -276,11 +271,7 @@ func (s *Server) proxyHTTP(host string, r *http.Request, msg *proto.ControlMessa
defer pr.Close()
defer pw.Close()
req, err := http.NewRequest(http.MethodPut, clientURL(host), pr)
if err != nil {
return nil, fmt.Errorf("request creation error: %s", err)
}
msg.WriteTo(req.Header)
req := proxyRequest(host, msg, pr)
go func() {
cw := &countWriter{pw, 0}

View file

@ -12,12 +12,9 @@ import (
"net"
"net/http"
"os"
"path"
"path/filepath"
"github.com/koding/logging"
"github.com/mmatczuk/tunnel"
"github.com/mmatczuk/tunnel/proto"
)
// EchoHTTP starts serving HTTP requests on listener l, it accepts connections,
@ -48,57 +45,6 @@ func EchoTCP(l net.Listener) {
}
}
// InMemoryFileServer scans directory dir, loads all files to memory and returns
// a http ProxyFunc that maps URL paths to relative filesystem paths i.e. file
// ./data/foo/bar.zip will be available under URL host:port/data/foo/bar.zip.
func InMemoryFileServer(dir string) (tunnel.ProxyFunc, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return nil, fmt.Errorf("failed to get directory absoute path %q: %s", dir, err)
}
prefix, _ := path.Split(dir)
mux := make(map[string][]byte)
visit := func(path string, f os.FileInfo, err error) error {
if f.IsDir() {
return nil
}
if err != nil {
return err
}
b, err := ResponseBytes(path)
if err != nil {
return err
}
mux[path[len(prefix)-1:]] = b
return nil
}
if err := filepath.Walk(dir, visit); err != nil {
return nil, fmt.Errorf("failed to read directory %q: %s", dir, err)
}
return func(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
b, ok := mux[msg.URLPath]
if !ok {
logging.Warning("Resource not found for %v", msg)
resp := &http.Response{
Status: "404 Not found",
StatusCode: 404,
Proto: "HTTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: make(http.Header),
}
resp.Write(w)
}
w.Write(b)
}, nil
}
// ResponseBytes returns http response containing file as body.
func ResponseBytes(file string) ([]byte, error) {
resp := &http.Response{

View file

@ -7,8 +7,22 @@ import (
"net/http"
"github.com/koding/logging"
"github.com/mmatczuk/tunnel/proto"
)
func proxyRequest(host string, msg *proto.ControlMessage, r io.Reader) *http.Request {
if msg.Action != proto.Proxy {
panic("Invalid action")
}
req, err := http.NewRequest(http.MethodPut, clientURL(host), r)
if err != nil {
panic(fmt.Sprintf("Could not create request: %s", err))
}
msg.Update(req.Header)
return req
}
func clientURL(host string) string {
return fmt.Sprint("https://", host)
}