summaryrefslogtreecommitdiffstats
path: root/extras/hs-test/infra/connect_udp_client.go
blob: a30ad3a769af05e0af379572e3cbc80a3d470e18 (plain)
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
package hst

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/quic-go/quic-go/http3"
	"github.com/quic-go/quic-go/quicvarint"
)

type CapsuleParseError struct {
	Err error
}

func (e *CapsuleParseError) Error() string {
	return e.Err.Error()
}

type ConnectUdpClient struct {
	log     bool
	suite   *HstSuite
	timeout time.Duration
	Conn    net.Conn
}

func (s *HstSuite) NewConnectUdpClient(timeout time.Duration, log bool) *ConnectUdpClient {
	client := &ConnectUdpClient{log: log, suite: s, timeout: timeout}
	return client
}

func writeConnectUdpReq(target string) []byte {
	var b bytes.Buffer

	fmt.Fprintf(&b, "GET %s HTTP/1.1\r\n", target)
	u, _ := url.Parse(target)
	fmt.Fprintf(&b, "Host: %s\r\n", u.Host)
	fmt.Fprintf(&b, "User-Agent: hs-test\r\n")
	fmt.Fprintf(&b, "Connection: Upgrade\r\n")
	fmt.Fprintf(&b, "Upgrade: connect-udp\r\n")
	fmt.Fprintf(&b, "Capsule-Protocol: ?1\r\n")
	io.WriteString(&b, "\r\n")

	return b.Bytes()
}

func (c *ConnectUdpClient) Dial(proxyAddress, targetUri string) error {
	req := writeConnectUdpReq(targetUri)
	conn, err := net.DialTimeout("tcp", proxyAddress, c.timeout)
	if err != nil {
		return err
	}

	if c.log {
		c.suite.Log("* Connected to proxy")
	}

	conn.SetDeadline(time.Now().Add(time.Second * c.timeout))
	_, err = conn.Write(req)
	if err != nil {
		return err
	}

	r := bufio.NewReader(conn)
	resp, err := http.ReadResponse(r, nil)
	if err != nil {
		return err
	}

	if c.log {
		c.suite.Log(DumpHttpResp(resp, true))
	}

	if resp.StatusCode != http.StatusSwitchingProtocols {
		return errors.New("request failed: " + resp.Status)
	}
	if resp.Header.Get("Connection") != "upgrade" || resp.Header.Get("Upgrade") != "connect-udp" || resp.Header.Get("Capsule-Protocol") != "?1" {
		conn.Close()
		return errors.New("invalid response")
	}

	if c.log {
		c.suite.Log("* CONNECT-UDP tunnel established")
	}
	c.Conn = conn
	return nil
}

func (c *ConnectUdpClient) Close() error {
	return c.Conn.Close()
}

func (c *ConnectUdpClient) WriteCapsule(capsuleType http3.CapsuleType, payload []byte) error {
	err := c.Conn.SetWriteDeadline(time.Now().Add(c.timeout))
	if err != nil {
		return err
	}
	var buf bytes.Buffer
	err = http3.WriteCapsule(&buf, capsuleType, payload)
	if err != nil {
		return err
	}
	_, err = c.Conn.Write(buf.Bytes())
	if err != nil {
		return err
	}
	return nil
}

func (c *ConnectUdpClient) WriteDgramCapsule(payload []byte) error {
	b := make([]byte, 0)
	b = quicvarint.Append(b, 0)
	b = append(b, payload...)
	return c.WriteCapsule(0, b)
}

func (c *ConnectUdpClient) ReadDgramCapsule() ([]byte, error) {
	err := c.Conn.SetReadDeadline(time.Now().Add(c.timeout))
	if err != nil {
		return nil, err
	}
	r := bufio.NewReader(c.Conn)
	capsuleType, payloadReader, err := http3.ParseCapsule(r)
	if err != nil {
		return nil, err
	}
	if capsuleType != 0 {
		return nil, &CapsuleParseError{errors.New("capsule type should be 0")}
	}
	b := make([]byte, 1024)
	n, err := payloadReader.Read(b)
	if err != nil {
		return nil, err
	}
	if n < 3 {
		return nil, &CapsuleParseError{errors.New("response payload too short")}
	}
	if b[0] != 0 {
		return nil, &CapsuleParseError{errors.New("context id should be 0")}
	}
	return b[1:n], nil
}