diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/pkg/Makefile | 1 | ||||
-rw-r--r-- | src/pkg/websocket/Makefile | 9 | ||||
-rw-r--r-- | src/pkg/websocket/client.go | 136 | ||||
-rw-r--r-- | src/pkg/websocket/server.go | 73 | ||||
-rw-r--r-- | src/pkg/websocket/websocket.go | 138 | ||||
-rw-r--r-- | src/pkg/websocket/websocket_test.go | 61 |
6 files changed, 418 insertions, 0 deletions
diff --git a/src/pkg/Makefile b/src/pkg/Makefile index 2b5e76c40..9386a8f5a 100644 --- a/src/pkg/Makefile +++ b/src/pkg/Makefile @@ -98,6 +98,7 @@ DIRS=\ time\ unicode\ utf8\ + websocket\ xml\ NOTEST=\ diff --git a/src/pkg/websocket/Makefile b/src/pkg/websocket/Makefile new file mode 100644 index 000000000..ba1b7265d --- /dev/null +++ b/src/pkg/websocket/Makefile @@ -0,0 +1,9 @@ +include $(GOROOT)/src/Make.$(GOARCH) + +TARG=websocket +GOFILES=\ + client.go\ + server.go\ + websocket.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/src/pkg/websocket/client.go b/src/pkg/websocket/client.go new file mode 100644 index 000000000..bedaec02f --- /dev/null +++ b/src/pkg/websocket/client.go @@ -0,0 +1,136 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio"; + "http"; + "io"; + "net"; + "os"; +) + +type ProtocolError struct { + os.ErrorString; +} + +var ( + ErrBadStatus = &ProtocolError{"bad status"}; + ErrNoUpgrade = &ProtocolError{"no upgrade"}; + ErrBadUpgrade = &ProtocolError{"bad upgrade"}; + ErrNoWebSocketOrigin = &ProtocolError{"no WebSocket-Origin"}; + ErrBadWebSocketOrigin = &ProtocolError{"bad WebSocket-Origin"}; + ErrNoWebSocketLocation = &ProtocolError{"no WebSocket-Location"}; + ErrBadWebSocketLocation = &ProtocolError{"bad WebSocket-Location"}; + ErrNoWebSocketProtocol = &ProtocolError{"no WebSocket-Protocol"}; + ErrBadWebSocketProtocol = &ProtocolError{"bad WebSocket-Protocol"}; +) + +// newClient creates a new Web Socket client connection. +func newClient(resourceName, host, origin, location, protocol string, rwc io.ReadWriteCloser) (ws *Conn, err os.Error) { + br := bufio.NewReader(rwc); + bw := bufio.NewWriter(rwc); + err = handshake(resourceName, host, origin, location, protocol, br, bw); + if err != nil { + return + } + buf := bufio.NewReadWriter(br, bw); + ws = newConn(origin, location, protocol, buf, rwc); + return; +} + +// Dial opens new Web Socket client connection. +// +// A trivial example client is: +// +// package main +// +// import ( +// "websocket" +// "strings" +// ) +// +// func main() { +// ws, err := websocket.Dial("ws://localhost/ws", "", "http://localhost/"); +// if err != nil { +// panic("Dial: ", err.String()) +// } +// if _, err := ws.Write(strings.Bytes("hello, world!\n")); err != nil { +// panic("Write: ", err.String()) +// } +// var msg = make([]byte, 512); +// if n, err := ws.Read(msg); err != nil { +// panic("Read: ", err.String()) +// } +// // msg[0:n] +// } + +func Dial(url, protocol, origin string) (ws *Conn, err os.Error) { + parsedUrl, err := http.ParseURL(url); + if err != nil { + return + } + client, err := net.Dial("tcp", "", parsedUrl.Host); + if err != nil { + return + } + return newClient(parsedUrl.Path, parsedUrl.Host, origin, url, protocol, client); +} + +func handshake(resourceName, host, origin, location, protocol string, br *bufio.Reader, bw *bufio.Writer) (err os.Error) { + bw.WriteString("GET " + resourceName + " HTTP/1.1\r\n"); + bw.WriteString("Upgrade: WebSocket\r\n"); + bw.WriteString("Connection: Upgrade\r\n"); + bw.WriteString("Host: " + host + "\r\n"); + bw.WriteString("Origin: " + origin + "\r\n"); + if protocol != "" { + bw.WriteString("WebSocket-Protocol: " + protocol + "\r\n") + } + bw.WriteString("\r\n"); + bw.Flush(); + resp, err := http.ReadResponse(br); + if err != nil { + return + } + if resp.Status != "101 Web Socket Protocol Handshake" { + return ErrBadStatus + } + upgrade, found := resp.Header["Upgrade"]; + if !found { + return ErrNoUpgrade + } + if upgrade != "WebSocket" { + return ErrBadUpgrade + } + connection, found := resp.Header["Connection"]; + if !found || connection != "Upgrade" { + return ErrBadUpgrade + } + + ws_origin, found := resp.Header["Websocket-Origin"]; + if !found { + return ErrNoWebSocketOrigin + } + if ws_origin != origin { + return ErrBadWebSocketOrigin + } + ws_location, found := resp.Header["Websocket-Location"]; + if !found { + return ErrNoWebSocketLocation + } + if ws_location != location { + return ErrBadWebSocketLocation + } + if protocol != "" { + ws_protocol, found := resp.Header["Websocket-Protocol"]; + if !found { + return ErrNoWebSocketProtocol + } + if ws_protocol != protocol { + return ErrBadWebSocketProtocol + } + } + return; +} diff --git a/src/pkg/websocket/server.go b/src/pkg/websocket/server.go new file mode 100644 index 000000000..e2d8cecbb --- /dev/null +++ b/src/pkg/websocket/server.go @@ -0,0 +1,73 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "http"; + "io"; +) + +// Handler is a interface that use a WebSocket. +// +// A trivial example server is: +// +// package main +// +// import ( +// "http" +// "io" +// "websocket" +// ) +// +// // echo back the websocket. +// func EchoServer(ws *websocket.Conn) { +// io.Copy(ws, ws); +// } +// +// func main() { +// http.Handle("/echo", websocket.Handler(EchoServer)); +// err := http.ListenAndServe(":12345", nil); +// if err != nil { +// panic("ListenAndServe: ", err.String()) +// } +// } +type Handler func(*Conn) + +func (f Handler) ServeHTTP(c *http.Conn, req *http.Request) { + if req.Method != "GET" || req.Proto != "HTTP/1.1" || + req.Header["Upgrade"] != "WebSocket" || + req.Header["Connection"] != "Upgrade" { + c.WriteHeader(http.StatusNotFound); + io.WriteString(c, "must use websocket to connect here"); + return; + } + rwc, buf, err := c.Hijack(); + if err != nil { + panic("Hijack failed: ", err.String()); + return; + } + defer rwc.Close(); + origin := req.Header["Origin"]; + location := "ws://" + req.Host + req.URL.Path; + + // TODO(ukai): verify origin,location,protocol. + + buf.WriteString("HTTP/1.1 101 Web Socket Protocol Handshake\r\n"); + buf.WriteString("Upgrade: WebSocket\r\n"); + buf.WriteString("Connection: Upgrade\r\n"); + buf.WriteString("WebSocket-Origin: " + origin + "\r\n"); + buf.WriteString("WebSocket-Location: " + location + "\r\n"); + protocol := ""; + // canonical header key of WebSocket-Protocol. + if protocol, found := req.Header["Websocket-Protocol"]; found { + buf.WriteString("WebSocket-Protocol: " + protocol + "\r\n") + } + buf.WriteString("\r\n"); + if err := buf.Flush(); err != nil { + return + } + ws := newConn(origin, location, protocol, buf, rwc); + f(ws); +} diff --git a/src/pkg/websocket/websocket.go b/src/pkg/websocket/websocket.go new file mode 100644 index 000000000..0fd32cd4c --- /dev/null +++ b/src/pkg/websocket/websocket.go @@ -0,0 +1,138 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The websocket package implements Web Socket protocol server. +package websocket + +// References: +// The Web Socket protocol: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +// TODO(ukai): +// better logging. + +import ( + "bufio"; + "io"; + "net"; + "os"; +) + +type WebSocketAddr string + +func (addr WebSocketAddr) Network() string { return "websocket" } + +func (addr WebSocketAddr) String() string { return string(addr) } + +// Conn is an channels to communicate over Web Socket. +type Conn struct { + // An origin URI of the Web Socket. + Origin string; + // A location URI of the Web Socket. + Location string; + // A subprotocol of the Web Socket. + Protocol string; + + buf *bufio.ReadWriter; + rwc io.ReadWriteCloser; +} + +// newConn creates a new Web Socket. +func newConn(origin, location, protocol string, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { + if buf == nil { + br := bufio.NewReader(rwc); + bw := bufio.NewWriter(rwc); + buf = bufio.NewReadWriter(br, bw); + } + ws := &Conn{origin, location, protocol, buf, rwc}; + return ws; +} + +func (ws *Conn) Read(msg []byte) (n int, err os.Error) { + for { + frameByte, err := ws.buf.ReadByte(); + if err != nil { + return + } + if (frameByte & 0x80) == 0x80 { + length := 0; + for { + c, err := ws.buf.ReadByte(); + if err != nil { + return + } + if (c & 0x80) == 0x80 { + length = length*128 + int(c&0x7f) + } else { + break + } + } + for length > 0 { + _, err := ws.buf.ReadByte(); + if err != nil { + return + } + length--; + } + } else { + for { + c, err := ws.buf.ReadByte(); + if err != nil { + return + } + if c == '\xff' { + return + } + if frameByte == 0 { + if n+1 <= cap(msg) { + msg = msg[0 : n+1] + } + msg[n] = c; + n++; + } + if n >= cap(msg) { + err = os.E2BIG; + return; + } + } + } + } + return; +} + +func (ws *Conn) Write(msg []byte) (n int, err os.Error) { + ws.buf.WriteByte(0); + ws.buf.Write(msg); + ws.buf.WriteByte(0xff); + err = ws.buf.Flush(); + return len(msg), err; +} + +func (ws *Conn) Close() os.Error { return ws.rwc.Close() } + +func (ws *Conn) LocalAddr() net.Addr { return WebSocketAddr(ws.Origin) } + +func (ws *Conn) RemoteAddr() net.Addr { return WebSocketAddr(ws.Location) } + +func (ws *Conn) SetTimeout(nsec int64) os.Error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetTimeout(nsec) + } + return os.EINVAL; +} + +func (ws *Conn) SetReadTimeout(nsec int64) os.Error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetReadTimeout(nsec) + } + return os.EINVAL; +} + +func (ws *Conn) SetWriteTimeout(nsec int64) os.Error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetWriteTimeout(nsec) + } + return os.EINVAL; +} + +var _ net.Conn = (*Conn)(nil) // compile-time check that *Conn implements net.Conn. diff --git a/src/pkg/websocket/websocket_test.go b/src/pkg/websocket/websocket_test.go new file mode 100644 index 000000000..ed2505313 --- /dev/null +++ b/src/pkg/websocket/websocket_test.go @@ -0,0 +1,61 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes"; + "http"; + "io"; + "log"; + "net"; + "once"; + "strings"; + "testing"; +) + +var serverAddr string + +func echoServer(ws *Conn) { io.Copy(ws, ws) } + +func startServer() { + l, e := net.Listen("tcp", ":0"); // any available address + if e != nil { + log.Exitf("net.Listen tcp :0 %v", e) + } + serverAddr = l.Addr().String(); + log.Stderr("Test WebSocket server listening on ", serverAddr); + http.Handle("/echo", Handler(echoServer)); + go http.Serve(l, nil); +} + +func TestEcho(t *testing.T) { + once.Do(startServer); + + client, err := net.Dial("tcp", "", serverAddr); + if err != nil { + t.Fatal("dialing", err) + } + + ws, err := newClient("/echo", "localhost", "http://localhost", + "ws://localhost/echo", "", client); + if err != nil { + t.Errorf("WebSocket handshake error", err); + return; + } + msg := strings.Bytes("hello, world\n"); + if _, err := ws.Write(msg); err != nil { + t.Errorf("Write: error %v", err) + } + var actual_msg = make([]byte, 512); + n, err := ws.Read(actual_msg); + if err != nil { + t.Errorf("Read: error %v", err) + } + actual_msg = actual_msg[0:n]; + if !bytes.Equal(msg, actual_msg) { + t.Errorf("Echo: expected %q got %q", msg, actual_msg) + } + ws.Close(); +} |