srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy/websocket.go
blob: 10a934c2e4933483cde5c826c47779bb4288da5d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// WebSocket interception: after a client's Upgrade: websocket request
// gets a matching 101 Switching Protocols response back from the origin
// (see forward's WS branch), the connection stops being HTTP request/
// response and becomes a long-lived, bidirectional, message-framed
// stream (RFC 6455) instead. mitmux relays every frame byte-for-byte
// unmodified in both directions - this is capture, not tampering - while
// decoding each one's payload for display, recorded to the ws_messages
// table tagged to the upgrade request's own history entry.
//
// Deliberately one row per frame, not per logical message: RFC 6455
// lets a single message span several frames (opcode 0x0 continuation,
// FIN unset until the last one), which mitmux does not reassemble.
// Real-world WebSocket traffic - JSON events, chat messages, game state
// - is overwhelmingly single-frame; reassembly would need buffering an
// unbounded number of pending fragmented messages per connection for a
// case that's rare in practice, which isn't a trade worth making here.
package proxy

import (
	"encoding/binary"
	"fmt"
	"io"
	"net/http"
	"strings"
)

// WebSocket opcodes (RFC 6455 section 5.2).
const (
	wsOpContinuation = 0x0
	wsOpText         = 0x1
	wsOpBinary       = 0x2
	wsOpClose        = 0x8
	wsOpPing         = 0x9
	wsOpPong         = 0xa
)

// isWebSocketUpgradeRequest reports whether r is asking to upgrade to a
// WebSocket connection (Connection: Upgrade plus Upgrade: websocket).
// forward() uses this to keep those two headers off stripHopByHop's list
// for this one request - RFC 7230 correctly treats Connection/Upgrade as
// hop-by-hop for a normal request, but stripping them here would delete
// the very signal the origin needs to recognize the upgrade at all,
// turning every WebSocket connection attempt into a silent 426.
func isWebSocketUpgradeRequest(r *http.Request) bool {
	return headerHasToken(r.Header, "Connection", "upgrade") &&
		strings.EqualFold(r.Header.Get("Upgrade"), "websocket")
}

// isWebSocketUpgradeResponse reports whether resp is a successful
// WebSocket upgrade (101 Switching Protocols, with Connection: Upgrade
// and Upgrade: websocket) - checking the response rather than the
// request it answers, since a 101 only ever comes back from an origin
// that accepted the upgrade, and that's the one thing forward() actually
// needs to know before handing the connection off.
func isWebSocketUpgradeResponse(resp *http.Response) bool {
	return resp.StatusCode == http.StatusSwitchingProtocols &&
		headerHasToken(resp.Header, "Connection", "upgrade") &&
		strings.EqualFold(resp.Header.Get("Upgrade"), "websocket")
}

func headerHasToken(h http.Header, name, token string) bool {
	for _, v := range h.Values(name) {
		for _, part := range strings.Split(v, ",") {
			if strings.EqualFold(strings.TrimSpace(part), token) {
				return true
			}
		}
	}
	return false
}

// relayWSFrame reads exactly one RFC 6455 frame from src, writes the
// same raw bytes to dst unmodified, and returns the frame's opcode and
// decoded (unmasked) payload for capture. Masking is direction-
// dependent - client-to-server frames are always masked, server-to-
// client frames never are - but relayed bytes are whatever was actually
// read, so this works correctly regardless of which direction it's
// called for.
func relayWSFrame(src io.Reader, dst io.Writer) (opcode byte, payload []byte, err error) {
	hdr := make([]byte, 2)
	if _, err = io.ReadFull(src, hdr); err != nil {
		return 0, nil, err
	}
	opcode = hdr[0] & 0x0f
	masked := hdr[1]&0x80 != 0
	length := uint64(hdr[1] & 0x7f)

	raw := append([]byte(nil), hdr...)

	switch length {
	case 126:
		ext := make([]byte, 2)
		if _, err = io.ReadFull(src, ext); err != nil {
			return 0, nil, err
		}
		raw = append(raw, ext...)
		length = uint64(binary.BigEndian.Uint16(ext))
	case 127:
		ext := make([]byte, 8)
		if _, err = io.ReadFull(src, ext); err != nil {
			return 0, nil, err
		}
		raw = append(raw, ext...)
		length = binary.BigEndian.Uint64(ext)
	}

	var maskKey [4]byte
	if masked {
		if _, err = io.ReadFull(src, maskKey[:]); err != nil {
			return 0, nil, err
		}
		raw = append(raw, maskKey[:]...)
	}

	// Bounded the same way request/response body capture is (see
	// maxCaptureBytes) - a length field mitmux doesn't control shouldn't
	// be able to force an unbounded read/allocation. Relaying (not just
	// capturing) is refused too: a frame this large is already well
	// outside normal WebSocket usage, and guessing at a partial relay
	// would corrupt the stream's framing for whichever side reads next.
	if length > maxCaptureBytes {
		return 0, nil, fmt.Errorf("websocket frame too large (%d bytes, over the %d limit)", length, maxCaptureBytes)
	}

	body := make([]byte, length)
	if _, err = io.ReadFull(src, body); err != nil {
		return 0, nil, err
	}
	raw = append(raw, body...)

	if _, err = dst.Write(raw); err != nil {
		return 0, nil, err
	}

	if !masked {
		return opcode, body, nil
	}
	payload = make([]byte, length)
	for i := range payload {
		payload[i] = body[i] ^ maskKey[i%4]
	}
	return opcode, payload, nil
}

// pumpWS relays frames from src to dst until one fails to read/write or
// a close frame (opcode 0x8) passes through, calling capture with each
// frame's opcode and decoded payload as it goes.
func pumpWS(src io.Reader, dst io.Writer, capture func(opcode byte, payload []byte)) {
	for {
		opcode, payload, err := relayWSFrame(src, dst)
		if err != nil {
			return
		}
		capture(opcode, payload)
		if opcode == wsOpClose {
			return
		}
	}
}