commit fb2697bca0b99bc4771c9b4111f654733ba7f903 Author: David Fifield david@bamsoftware.com Date: Sat Nov 10 21:32:01 2012 -0800
First draft of websocket-client. --- .gitignore | 1 + websocket-transport/Makefile | 16 ++++ websocket-transport/pt.go | 106 +++++++++++++++++++++++++++ websocket-transport/socks.go | 82 +++++++++++++++++++++ websocket-transport/websocket-client.go | 120 +++++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore index 493cc74..24f143b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /facilitator.log *.pyc /dist +/websocket-transport/websocket-client diff --git a/websocket-transport/Makefile b/websocket-transport/Makefile new file mode 100644 index 0000000..14ffac0 --- /dev/null +++ b/websocket-transport/Makefile @@ -0,0 +1,16 @@ +PROGRAMS = websocket-client + +all: $(PROGRAMS) + +websocket-client: websocket-client.go socks.go pt.go + +%: %.go + go build -o $@ $^ + +clean: + rm -f $(PROGRAMS) + +fmt: + go fmt + +.PHONY: all clean fmt diff --git a/websocket-transport/pt.go b/websocket-transport/pt.go new file mode 100644 index 0000000..9eaed84 --- /dev/null +++ b/websocket-transport/pt.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "fmt" + "net" + "os" + "strings" +) + +// Escape a string so it contains no byte values over 127 and doesn't contain +// any of the characters '\x00', '\n', or '\'. +func escape(s string) string { + var buf bytes.Buffer + for _, b := range []byte(s) { + if b == '\n' { + buf.WriteString("\n") + } else if b == '\' { + buf.WriteString("\\") + } else if 0 < b && b < 128 { + buf.WriteByte(b) + } else { + fmt.Fprintf(&buf, "\x%02x", b) + } + } + return buf.String() +} + +func ptLine(keyword string, v ...string) { + var buf bytes.Buffer + buf.WriteString(keyword) + for _, x := range v { + buf.WriteString(" " + escape(x)) + } + fmt.Println(buf.String()) +} + +func ptEnvError(msg string) { + ptLine("ENV-ERROR", msg) + os.Exit(1) +} + +func ptVersionError(msg string) { + ptLine("VERSION-ERROR", msg) + os.Exit(1) +} + +func ptCmethodError(methodName, msg string) { + ptLine("CMETHOD-ERROR", methodName, msg) + os.Exit(1) +} + +func ptGetManagedTransportVer() string { + const transportVersion = "1" + for _, offered := range strings.Split(os.Getenv("TOR_PT_MANAGED_TRANSPORT_VER"), ",") { + if offered == transportVersion { + return offered + } + } + return "" +} + +func ptGetClientTransports(supported []string) []string { + clientTransports := os.Getenv("TOR_PT_CLIENT_TRANSPORTS") + if clientTransports == "" { + ptEnvError("no TOR_PT_CLIENT_TRANSPORTS environment variable") + } + if clientTransports == "*" { + return supported + } + + result := make([]string, 0) + for _, requested := range strings.Split(clientTransports, ",") { + for _, methodName := range supported { + if requested == methodName { + result = append(result, methodName) + } + } + } + return result +} + +func ptCmethod(name string, socks string, addr net.Addr) { + ptLine("CMETHOD", name, socks, addr.String()) +} + +func ptCmethodsDone() { + ptLine("CMETHODS", "DONE") +} + +func ptClientSetup(methodNames []string) []string { + ver := ptGetManagedTransportVer() + if ver == "" { + ptVersionError("no-version") + } else { + ptLine("VERSION", ver) + } + + methods := ptGetClientTransports(methodNames) + if len(methods) == 0 { + ptCmethodsDone() + os.Exit(1) + } + + return methods +} diff --git a/websocket-transport/socks.go b/websocket-transport/socks.go new file mode 100644 index 0000000..33df918 --- /dev/null +++ b/websocket-transport/socks.go @@ -0,0 +1,82 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "net" +) + +const ( + socksVersion = 0x04 + socksCmdConnect = 0x01 + socksResponseVersion = 0x00 + socksRequestGranted = 0x5a + socksRequestFailed = 0x5b +) + +func readSocks4aConnect(s io.Reader) (string, error) { + r := bufio.NewReader(s) + + var h [8]byte + n, err := io.ReadFull(r, h[:]) + if err != nil { + return "", errors.New(fmt.Sprintf("after %d bytes of SOCKS header: %s", n, err)) + } + if h[0] != socksVersion { + return "", errors.New(fmt.Sprintf("SOCKS header had version 0x%02x, not 0x%02x", h[0], socksVersion)) + } + if h[1] != socksCmdConnect { + return "", errors.New(fmt.Sprintf("SOCKS header had command 0x%02x, not 0x%02x", h[1], socksCmdConnect)) + } + + _, err = r.ReadBytes('\x00') + if err != nil { + return "", errors.New(fmt.Sprintf("reading SOCKS userid: %s", n, err)) + } + + var port int + var host string + + port = int(h[2])<<8 | int(h[3])<<0 + if h[4] == 0 && h[5] == 0 && h[6] == 0 && h[7] != 0 { + hostBytes, err := r.ReadBytes('\x00') + if err != nil { + return "", errors.New(fmt.Sprintf("reading SOCKS4a destination: %s", err)) + } + host = string(hostBytes[:len(hostBytes)-1]) + } else { + host = net.IPv4(h[4], h[5], h[6], h[7]).String() + } + + if r.Buffered() != 0 { + return "", errors.New(fmt.Sprintf("%d bytes left after SOCKS header", r.Buffered())) + } + + return fmt.Sprintf("%s:%d", host, port), nil +} + +func sendSocks4aResponse(w io.Writer, code byte, addr *net.TCPAddr) error { + var resp [8]byte + resp[0] = socksResponseVersion + resp[1] = code + resp[2] = byte((addr.Port >> 8) & 0xff) + resp[3] = byte((addr.Port >> 0) & 0xff) + resp[4] = addr.IP[0] + resp[5] = addr.IP[1] + resp[6] = addr.IP[2] + resp[7] = addr.IP[3] + _, err := w.Write(resp[:]) + return err +} + +var emptyAddr = net.TCPAddr{net.IPv4(0, 0, 0, 0), 0} + +func sendSocks4aResponseGranted(w io.Writer, addr *net.TCPAddr) error { + return sendSocks4aResponse(w, socksRequestGranted, addr) +} + +func sendSocks4aResponseFailed(w io.Writer) error { + return sendSocks4aResponse(w, socksRequestFailed, &emptyAddr) +} diff --git a/websocket-transport/websocket-client.go b/websocket-transport/websocket-client.go new file mode 100644 index 0000000..5c1c882 --- /dev/null +++ b/websocket-transport/websocket-client.go @@ -0,0 +1,120 @@ +package main + +import ( + "code.google.com/p/go.net/websocket" + "fmt" + "io" + "net" + "net/url" + "os" + "time" +) + +const socksTimeout = 2 + +func logDebug(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", v...) +} + +func proxy(local *net.TCPConn, ws *websocket.Conn) error { + // Local-to-WebSocket read loop. + go func() { + n, err := io.Copy(ws, local) + logDebug("end local-to-WebSocket %d %s", n, err) + }() + + // WebSocket-to-local read loop. + go func() { + n, err := io.Copy(local, ws) + logDebug("end WebSocket-to-local %d %s", n, err) + }() + + select {} + return nil +} + +func handleConnection(conn *net.TCPConn) error { + defer conn.Close() + + conn.SetDeadline(time.Now().Add(socksTimeout * time.Second)) + dest, err := readSocks4aConnect(conn) + if err != nil { + sendSocks4aResponseFailed(conn) + return err + } + // Disable deadline. + conn.SetDeadline(time.Time{}) + logDebug("SOCKS request for %s", dest) + + // We need the parsed IP and port for the SOCKS reply. + destAddr, err := net.ResolveTCPAddr("tcp", dest) + if err != nil { + sendSocks4aResponseFailed(conn) + return err + } + + wsUrl := url.URL{Scheme: "ws", Host: dest} + ws, err := websocket.Dial(wsUrl.String(), "", wsUrl.String()) + if err != nil { + sendSocks4aResponseFailed(conn) + return err + } + defer ws.Close() + logDebug("WebSocket connection to %s", ws.Config().Location.String()) + + sendSocks4aResponseGranted(conn, destAddr) + + return proxy(conn, ws) +} + +func socksAcceptLoop(ln *net.TCPListener) error { + for { + socks, err := ln.AcceptTCP() + if err != nil { + return err + } + go func() { + err := handleConnection(socks) + if err != nil { + logDebug("SOCKS from %s: %s", socks.RemoteAddr(), err) + } + }() + } + return nil +} + +func startListener(addrStr string) (*net.TCPListener, error) { + addr, err := net.ResolveTCPAddr("tcp", addrStr) + if err != nil { + return nil, err + } + ln, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, err + } + go func() { + err := socksAcceptLoop(ln) + if err != nil { + logDebug("accept: %s", err) + } + }() + return ln, nil +} + +func main() { + const ptMethodName = "websocket" + var socksAddrStrs = [...]string{"127.0.0.1:0", "[::1]:0"} + + ptClientSetup([]string{ptMethodName}) + + for _, socksAddrStr := range socksAddrStrs { + ln, err := startListener(socksAddrStr) + if err != nil { + ptCmethodError(ptMethodName, err.Error()) + } + ptCmethod(ptMethodName, "socks4", ln.Addr()) + } + ptCmethodsDone() + + select {} +}
tor-commits@lists.torproject.org