blob: e283821969e60598676e6f4b5ed6d9533ef20c05 [file] [log] [blame]
// 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"
"bytes"
"container/vector"
"crypto/tls"
"fmt"
"http"
"io"
"net"
"os"
"rand"
"strings"
)
type ProtocolError struct {
os.ErrorString
}
var (
ErrBadScheme = os.ErrorString("bad scheme")
ErrBadStatus = &ProtocolError{"bad status"}
ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"}
ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"}
ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"}
ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"}
ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"}
secKeyRandomChars [0x30 - 0x21 + 0x7F - 0x3A]byte
)
type DialError struct {
URL string
Protocol string
Origin string
Error os.Error
}
func (e *DialError) String() string {
return "websocket.Dial " + e.URL + ": " + e.Error.String()
}
func init() {
i := 0
for ch := byte(0x21); ch < 0x30; ch++ {
secKeyRandomChars[i] = ch
i++
}
for ch := byte(0x3a); ch < 0x7F; ch++ {
secKeyRandomChars[i] = ch
i++
}
}
type handshaker func(resourceName, host, origin, location, protocol string, br *bufio.Reader, bw *bufio.Writer) os.Error
// newClient creates a new Web Socket client connection.
func newClient(resourceName, host, origin, location, protocol string, rwc io.ReadWriteCloser, handshake handshaker) (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 a new client connection to a Web Socket.
A trivial example client:
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([]byte("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())
}
// use msg[0:n]
}
*/
func Dial(url, protocol, origin string) (ws *Conn, err os.Error) {
var client net.Conn
parsedUrl, err := http.ParseURL(url)
if err != nil {
goto Error
}
switch parsedUrl.Scheme {
case "ws":
client, err = net.Dial("tcp", parsedUrl.Host)
case "wss":
client, err = tls.Dial("tcp", parsedUrl.Host, nil)
default:
err = ErrBadScheme
}
if err != nil {
goto Error
}
ws, err = newClient(parsedUrl.RawPath, parsedUrl.Host, origin, url, protocol, client, handshake)
if err != nil {
goto Error
}
return
Error:
return nil, &DialError{url, protocol, origin, err}
}
/*
Generates handshake key as described in 4.1 Opening handshake step 16 to 22.
cf. http://www.whatwg.org/specs/web-socket-protocol/
*/
func generateKeyNumber() (key string, number uint32) {
// 16. Let /spaces_n/ be a random integer from 1 to 12 inclusive.
spaces := rand.Intn(12) + 1
// 17. Let /max_n/ be the largest integer not greater than
// 4,294,967,295 divided by /spaces_n/
max := int(4294967295 / uint32(spaces))
// 18. Let /number_n/ be a random integer from 0 to /max_n/ inclusive.
number = uint32(rand.Intn(max + 1))
// 19. Let /product_n/ be the result of multiplying /number_n/ and
// /spaces_n/ together.
product := number * uint32(spaces)
// 20. Let /key_n/ be a string consisting of /product_n/, expressed
// in base ten using the numerals in the range U+0030 DIGIT ZERO (0)
// to U+0039 DIGIT NINE (9).
key = fmt.Sprintf("%d", product)
// 21. Insert between one and twelve random characters from the ranges
// U+0021 to U+002F and U+003A to U+007E into /key_n/ at random
// positions.
n := rand.Intn(12) + 1
for i := 0; i < n; i++ {
pos := rand.Intn(len(key)) + 1
ch := secKeyRandomChars[rand.Intn(len(secKeyRandomChars))]
key = key[0:pos] + string(ch) + key[pos:]
}
// 22. Insert /spaces_n/ U+0020 SPACE characters into /key_n/ at random
// positions other than the start or end of the string.
for i := 0; i < spaces; i++ {
pos := rand.Intn(len(key)-1) + 1
key = key[0:pos] + " " + key[pos:]
}
return
}
/*
Generates handshake key_3 as described in 4.1 Opening handshake step 26.
cf. http://www.whatwg.org/specs/web-socket-protocol/
*/
func generateKey3() (key []byte) {
// 26. Let /key3/ be a string consisting of eight random bytes (or
// equivalently, a random 64 bit integer encoded in big-endian order).
key = make([]byte, 8)
for i := 0; i < 8; i++ {
key[i] = byte(rand.Intn(256))
}
return
}
/*
Web Socket protocol handshake based on
http://www.whatwg.org/specs/web-socket-protocol/
(draft of http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol)
*/
func handshake(resourceName, host, origin, location, protocol string, br *bufio.Reader, bw *bufio.Writer) (err os.Error) {
// 4.1. Opening handshake.
// Step 5. send a request line.
bw.WriteString("GET " + resourceName + " HTTP/1.1\r\n")
// Step 6-14. push request headers in fields.
var fields vector.StringVector
fields.Push("Upgrade: WebSocket\r\n")
fields.Push("Connection: Upgrade\r\n")
fields.Push("Host: " + host + "\r\n")
fields.Push("Origin: " + origin + "\r\n")
if protocol != "" {
fields.Push("Sec-WebSocket-Protocol: " + protocol + "\r\n")
}
// TODO(ukai): Step 15. send cookie if any.
// Step 16-23. generate keys and push Sec-WebSocket-Key<n> in fields.
key1, number1 := generateKeyNumber()
key2, number2 := generateKeyNumber()
fields.Push("Sec-WebSocket-Key1: " + key1 + "\r\n")
fields.Push("Sec-WebSocket-Key2: " + key2 + "\r\n")
// Step 24. shuffle fields and send them out.
for i := 1; i < len(fields); i++ {
j := rand.Intn(i)
fields[i], fields[j] = fields[j], fields[i]
}
for i := 0; i < len(fields); i++ {
bw.WriteString(fields[i])
}
// Step 25. send CRLF.
bw.WriteString("\r\n")
// Step 26. generate 8 bytes random key.
key3 := generateKey3()
// Step 27. send it out.
bw.Write(key3)
if err = bw.Flush(); err != nil {
return
}
// Step 28-29, 32-40. read response from server.
resp, err := http.ReadResponse(br, &http.Request{Method: "GET"})
if err != nil {
return err
}
// Step 30. check response code is 101.
if resp.StatusCode != 101 {
return ErrBadStatus
}
// Step 41. check websocket headers.
if resp.Header.Get("Upgrade") != "WebSocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return ErrBadUpgrade
}
if resp.Header.Get("Sec-Websocket-Origin") != origin {
return ErrBadWebSocketOrigin
}
if resp.Header.Get("Sec-Websocket-Location") != location {
return ErrBadWebSocketLocation
}
if protocol != "" && resp.Header.Get("Sec-Websocket-Protocol") != protocol {
return ErrBadWebSocketProtocol
}
// Step 42-43. get expected data from challenge data.
expected, err := getChallengeResponse(number1, number2, key3)
if err != nil {
return err
}
// Step 44. read 16 bytes from server.
reply := make([]byte, 16)
if _, err = io.ReadFull(br, reply); err != nil {
return err
}
// Step 45. check the reply equals to expected data.
if !bytes.Equal(expected, reply) {
return ErrChallengeResponse
}
// WebSocket connection is established.
return
}
/*
Handshake described in (soon obsolete)
draft-hixie-thewebsocket-protocol-75.
*/
func draft75handshake(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, &http.Request{Method: "GET"})
if err != nil {
return
}
if resp.Status != "101 Web Socket Protocol Handshake" {
return ErrBadStatus
}
if resp.Header.Get("Upgrade") != "WebSocket" ||
resp.Header.Get("Connection") != "Upgrade" {
return ErrBadUpgrade
}
if resp.Header.Get("Websocket-Origin") != origin {
return ErrBadWebSocketOrigin
}
if resp.Header.Get("Websocket-Location") != location {
return ErrBadWebSocketLocation
}
if protocol != "" && resp.Header.Get("Websocket-Protocol") != protocol {
return ErrBadWebSocketProtocol
}
return
}