Skip to content

Commit 12c0de9

Browse files
committed
Initial commit
0 parents  commit 12c0de9

File tree

7 files changed

+721
-0
lines changed

7 files changed

+721
-0
lines changed

.github/workflows/go.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# This workflow will build a golang project
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3+
4+
name: Go
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
pull_request:
10+
branches: [ "main" ]
11+
12+
jobs:
13+
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v4
21+
with:
22+
go-version: '1.23.1'
23+
24+
- name: Build
25+
run: go build -v ./...
26+
27+
- name: Test
28+
run: go test

README.md

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# `gows`
2+
Simple WebSocket ([RFC6455](https://datatracker.ietf.org/doc/rfc6455/)) library in Go
3+
4+
**What it offers:**
5+
- Simple way to upgrade an HTTP connection to a WebSocket connection
6+
- Simple way to serialize and deserialize individual WebSocket frames
7+
8+
**What it doesn't do:**
9+
- Doesn't handle message fragmentation, but you can do that yourself by reading `Fin` and `Opcode`. For more information, see section 5.4 of [RFC6455](https://datatracker.ietf.org/doc/rfc6455/)
10+
- Doesn't automatically respond to PING frames.
11+
12+
## Installation
13+
`go get github.com/zachshattuck/gows`
14+
15+
## Example Usage
16+
```go
17+
import (
18+
"github.com/zachshattuck/gows"
19+
)
20+
21+
func main() {
22+
23+
ln, err := net.Listen("tcp", "127.0.0.1:8080")
24+
if err != nil {
25+
fmt.Println("Failed to `net.Listen`: ", err)
26+
os.Exit(1)
27+
}
28+
29+
conn, err := ln.Accept()
30+
if err != nil {
31+
fmt.Println("Failed to `Accept` connection: ", err)
32+
os.Exit(1)
33+
}
34+
35+
// Will `Read` from the connection and send a `101 Switching Protocols` response
36+
// if valid, otherwise sends a `400 Bad Request` response.
37+
err := gows.UpgradeConnection(&conn, buf)
38+
if err != nil {
39+
fmt.Fprintln(os.Stderr, "Failed to upgrade: ", err)
40+
os.Exit(1)
41+
}
42+
43+
// Listen for WebSocket frames
44+
for {
45+
n, err := conn.Read(buf)
46+
if err != nil {
47+
fmt.Println("Failed to read: ", err)
48+
break
49+
}
50+
51+
frame, err := gows.DeserializeWebSocketFrame(buf[:n])
52+
if err != nil {
53+
fmt.Fprintln(os.Stderr, "Failed to deserialize frame: ", err)
54+
continue
55+
}
56+
57+
switch frame.Opcode {
58+
case gows.WS_OP_TEXT: // Handle text frame..
59+
case gows.WS_OP_BIN: // Handle binary frame..
60+
case gows.WS_OP_PING:
61+
fmt.Println("Ping frame, responding with pong...")
62+
pongFrame := gows.SerializeWebSocketFrame(gows.WebSocketFrame{
63+
Fin: 1,
64+
Rsv1: 0, Rsv2: 0, Rsv3: 0,
65+
Opcode: gows.WS_OP_PONG,
66+
IsMasked: 0,
67+
MaskKey: [4]byte{},
68+
Payload: frame.Payload,
69+
})
70+
conn.Write(pongFrame)
71+
}
72+
73+
}
74+
75+
}
76+
```

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/zachshattuck/gows
2+
3+
go 1.23.1

http.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package gows
2+
3+
import (
4+
"errors"
5+
"net"
6+
)
7+
8+
/*
9+
Given a param name and a buffer expected to be a valid HTTP request, this function
10+
will return a slice containing the value of that HTTP param, if it is found.
11+
*/
12+
func getHttpParam(buf []byte, paramName string) ([]byte, error) {
13+
14+
// Read until we match `paramName` completely, NOT including the ":"
15+
var correctByteCount int = 0
16+
var valueStartIdx int
17+
for i, b := range buf {
18+
if b != paramName[correctByteCount] {
19+
correctByteCount = 0
20+
continue
21+
}
22+
23+
// Previous character has to be start of buffer or '\n' (as part of CRLF)
24+
// NOTE: If the user provided a slice that was partway through a request, this could
25+
// produce wrong results. For example, if there were two params, "Test-Param1: {value}"
26+
// and "Param1: {value}", and the slice started at the 'P' in "Test-Param1", it could
27+
// extract that value as if it was just "Param1".
28+
if correctByteCount == 0 && !(i == 0 || buf[i-1] == '\n') {
29+
correctByteCount = 0
30+
continue
31+
}
32+
33+
correctByteCount++
34+
35+
if correctByteCount < len(paramName) {
36+
continue
37+
}
38+
39+
// Following character has to be ":"
40+
if i >= len(buf)-2 || buf[i+1] != ':' {
41+
correctByteCount = 0
42+
continue
43+
}
44+
45+
// we found the whole param!
46+
valueStartIdx = i + 2
47+
break
48+
}
49+
50+
if correctByteCount < len(paramName) {
51+
return nil, errors.New("param \"" + string(paramName) + "\" not found in buffer")
52+
}
53+
if valueStartIdx >= len(buf)-1 {
54+
return nil, errors.New("nothing in buffer after \"" + string(paramName) + ":\"")
55+
}
56+
57+
// Read all whitespace
58+
for {
59+
if buf[valueStartIdx] != ' ' {
60+
break
61+
}
62+
valueStartIdx++
63+
}
64+
65+
// Read until CRLF
66+
return readUntilCrlf(buf[valueStartIdx:])
67+
}
68+
69+
/* Reads from start of slice until CRLF. If no CRLF is found, it will return an error instead of the value so far. */
70+
func readUntilCrlf(buf []byte) ([]byte, error) {
71+
lastTokenIdx := -1337
72+
for i, b := range buf {
73+
if b == '\r' {
74+
lastTokenIdx = i
75+
} else if b == '\n' {
76+
if lastTokenIdx == i-1 {
77+
return buf[:lastTokenIdx], nil
78+
}
79+
}
80+
}
81+
82+
// we never found a valid CRLF
83+
return nil, errors.New("no CRLF found")
84+
}
85+
86+
func isValidUpgradeRequest(buf []byte) (bool, error) {
87+
// TODO: This doesn't verify a valid HTTP verb at all
88+
89+
// _, err := GetHttpParam(buf, "Host")
90+
// if err != nil {
91+
// return false, err
92+
// }
93+
94+
httpConnection, err := getHttpParam(buf, "Connection")
95+
if err != nil || (string(httpConnection) != "Upgrade" && string(httpConnection) != "upgrade") {
96+
return false, errors.New("invalid or nonexistent \"Connection\" param")
97+
}
98+
99+
httpUpgrade, err := getHttpParam(buf, "Upgrade")
100+
if err != nil || string(httpUpgrade) != "websocket" {
101+
return false, errors.New("invalid or nonexistent \"Upgrade\" param")
102+
}
103+
104+
httpWebSocketVersion, err := getHttpParam(buf, "Sec-WebSocket-Version")
105+
if err != nil || string(httpWebSocketVersion) != "13" {
106+
return false, errors.New("invalid or nonexistent \"Sec-WebSocket-Version\" param")
107+
}
108+
109+
_, err = getHttpParam(buf, "Sec-WebSocket-Key")
110+
if err != nil {
111+
return false, errors.New("invalid or nonexistent \"Sec-WebSocket-Key\" param")
112+
}
113+
114+
return true, nil
115+
}
116+
117+
func sendBadRequestResponse(conn *net.Conn) (int, error) {
118+
return (*conn).Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
119+
}

http_test.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package gows
2+
3+
import "testing"
4+
5+
/* Example WebSocket upgrade request, ripped straight from my browser. */
6+
var exampleHttpRequest = []byte("GET / HTTP/1.1\r\nHost: 127.0.0.1:8081\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\r\nUpgrade: websocket\r\nOrigin: http://localhost:8080\r\nSec-WebSocket-Version: 13\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: en-US,en;q=0.9\r\nSec-WebSocket-Key: D8KfDxohPIack4T9PAf3Ng==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n")
7+
8+
/* A longer WebSocket upgrade request, proxied by nginx. */
9+
var exampleHttpRequest2 = []byte("GET /ws HTTP/1.1\r\nUpgrade: websocket\r\nConnection: upgrade\r\nHost: 127.0.0.1:8081\r\naccept-encoding: gzip, br\r\nX-Forwarded-For: 1.2.3.4\r\nCF-RAY: 8c3d6a50b90875c8-SEA\r\nX-Forwarded-Proto: https\r\nCF-Visitor: {\"scheme\":\"https\"}\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\r\nOrigin: https://www.website.com\r\nSec-WebSocket-Version: 13\r\nAccept-Language: en-US,en;q=0.9\r\nSec-WebSocket-Key: ZFPbTE+Wekp3z+QNUR4R0Q==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\nCF-Connecting-IP: 1.2.3.4\r\ncdn-loop: cloudflare; loops=1\r\nCF-IPCountry: US\r\n\r\n")
10+
11+
func TestGetHttpParamValidProperty(t *testing.T) {
12+
got, err := getHttpParam(exampleHttpRequest, "Host")
13+
want := "127.0.0.1:8081"
14+
15+
if err != nil {
16+
t.Error("error:", err)
17+
}
18+
if string(got) != want {
19+
t.Errorf("got %q, wanted %q", got, want)
20+
}
21+
}
22+
23+
func TestGetHttpParamInvalidProperty(t *testing.T) {
24+
got, err := getHttpParam(exampleHttpRequest, " super duper nonsensical parameter!!!! 89740r3n3yr0932")
25+
26+
if err == nil {
27+
t.Errorf("did not error, got %q", got)
28+
}
29+
}
30+
31+
func TestGetHttpParamDuplicateKeyword(t *testing.T) {
32+
got, err := getHttpParam(exampleHttpRequest, "Upgrade")
33+
want := "websocket"
34+
35+
if err != nil {
36+
t.Error("error:", err)
37+
}
38+
if string(got) != want {
39+
t.Errorf("got %q, wanted %q", got, want)
40+
}
41+
}
42+
43+
func TestGetHttpParamExtremelyLongParameter(t *testing.T) {
44+
got, err := getHttpParam(exampleHttpRequest, "sd09 fus8-d90f js09df mus90d8f mu09sd8fy um90s8d ynf098sd7f n908sd 7fn90s8d7fn 908sd7n f09s8df7n 908sd7f 098sd7nf 098sd7nf 098sd7nf 098sd7fn 098sd7nf 098sd7fn 098sd7f 098sdf7 n09s8df7n 098sdf7n 09s8df7 n09s8df 709s8df7 n09s8df7 098sdf7 098sdf yunoiusdf hlksjdfh klsjdfkjsdhfkj sdhjflksdjf lksdj f098sd7f 908sduf iujsdhf kjshdf kjysud9f8 7sd98f sdkjf h,sjdhf kjsdfy 98sdf iusdnf kjsdhf kiusdyf 98sdyfi uhsdifu ysd98f sd98f jsd98f jsd9f j9sd8f j9s8df hisudfh lkjsdhf8sdy f98sdhf iujsdhf iousdyuf 98sdhf oijsdhf likudsfyg s98ydfgisu hdfsiog hsdf98g y9sd8fgjh s9d8fg u9isd8fgy 0987sdfg yhioudsfhg oisudfgh o87sdfhg 9sdfgy h098sdfhg isdufhg 98sdfh g9087sdfhg iosdufhg osjkdfhg lkjdsfh giusdfug98dsfgu g9p8sdfjg ;lksdfj g")
45+
want := ""
46+
47+
if err == nil || string(got) != want {
48+
t.Errorf("got %q, wanted %q", got, want)
49+
}
50+
}
51+
func TestGetHttpParamSelf(t *testing.T) {
52+
got, err := getHttpParam(exampleHttpRequest, "GET / HTTP/1.1\r\nHost: 127.0.0.1:8081\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\r\nUpgrade: websocket\r\nOrigin: http://localhost:8080\r\nSec-WebSocket-Version: 13\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: en-US,en;q=0.9\r\nSec-WebSocket-Key: D8KfDxohPIack4T9PAf3Ng==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n")
53+
54+
if err == nil {
55+
t.Errorf("did not error, got %q", got)
56+
}
57+
}
58+
59+
func TestReadUntilCrlfHttpVerb(t *testing.T) {
60+
got, err := readUntilCrlf(exampleHttpRequest)
61+
want := "GET / HTTP/1.1"
62+
63+
if err != nil {
64+
t.Error("error:", err)
65+
}
66+
if string(got) != want {
67+
t.Errorf("got %q, wanted %q", got, want)
68+
}
69+
}
70+
71+
func TestReadUntilCrlfHttpParamValue(t *testing.T) {
72+
got, err := readUntilCrlf(exampleHttpRequest[22:])
73+
want := "127.0.0.1:8081"
74+
75+
if err != nil {
76+
t.Error("error:", err)
77+
}
78+
if string(got) != want {
79+
t.Errorf("got %q, wanted %q", got, want)
80+
}
81+
}
82+
83+
func TestReadUntilCrlfRandomNoCrlf(t *testing.T) {
84+
got, err := readUntilCrlf([]byte("This is a nice string and all, but it doesn't have a Crlf."))
85+
86+
if err == nil {
87+
t.Errorf("did not error, got %q", got)
88+
}
89+
}
90+
91+
func TestReadUntilCrlfRandomWithCrlf(t *testing.T) {
92+
got, err := readUntilCrlf([]byte("This is a nice string and all, AND it has a Crlf.\r\n"))
93+
want := "This is a nice string and all, AND it has a Crlf."
94+
95+
if err != nil {
96+
t.Error("error:", err)
97+
}
98+
if string(got) != want {
99+
t.Errorf("got %q, wanted %q", got, want)
100+
}
101+
}
102+
103+
func TestReadUntilCrlfCursed1(t *testing.T) {
104+
got, err := readUntilCrlf([]byte("\r \n\n\n\n \n\n\n\n\r\n\n\n\n\n\n\n"))
105+
want := "\r \n\n\n\n \n\n\n\n"
106+
107+
if err != nil {
108+
t.Error("error:", err)
109+
}
110+
if string(got) != want {
111+
t.Errorf("got %q, wanted %q", got, want)
112+
}
113+
}
114+
func TestReadUntilCrlfCursed2(t *testing.T) {
115+
got, err := readUntilCrlf([]byte("\r\r\r\r\r\r\r\r \nr\n\n\n \n\n\n\n\n\n\n\n\n\n\n"))
116+
117+
if err == nil {
118+
t.Errorf("did not error, got %q", got)
119+
}
120+
}
121+
122+
func TestIsValidUpgradeRequestBasicGood(t *testing.T) {
123+
got, err := isValidUpgradeRequest(exampleHttpRequest)
124+
125+
if err != nil {
126+
t.Error("error:", err)
127+
}
128+
if got == false {
129+
t.Errorf("got invalid, expected valid")
130+
}
131+
}
132+
133+
func TestIsValidUpgradeRequestLongGood(t *testing.T) {
134+
got, err := isValidUpgradeRequest(exampleHttpRequest2)
135+
136+
if err != nil {
137+
t.Error("error:", err)
138+
}
139+
if got == false {
140+
t.Errorf("got invalid, expected valid")
141+
}
142+
}

0 commit comments

Comments
 (0)