Welcome to mirror list, hosted at ThFree Co, Russian Federation.

conn.go « sidechannel « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 5af660fabaa25afe2ad4b00b83045c4cb80ffffd (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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package sidechannel

import (
	"fmt"
	"io"
	"net"

	"gitlab.com/gitlab-org/gitaly/v14/internal/git/pktline"
	"gitlab.com/gitlab-org/gitaly/v14/streamio"
)

// ServerConn and ClientConn implement an asymmetric framing protocol to
// exchange data between clients and servers in Sidechannel. A typical flow
// looks like following:
// - The client writes data into the connecton.
// - The client half-closes the connection. The server is aware of this event
// when reading operations return EOF.
// - The server writes the data back to the client, then close the connection.
// - The client read the data until EOF
//
// Half-close ability is important to signal the server that the client
// finishes data transformation. As sidechannel is built on top of Yamux
// stream, half-close ability is not supported. Therefore, we apply a
// length-prefix framing protocol, simiarly to Git pktline protocol, except we
// omit the band number. The close or half-close event are signaled by sending
// a flush packet.
//
// This is an example of the data written into the wire:
//
// | 4-byte length, including size of length itself.
// v
// 0009Hello0000
//          ^
//          | Flush packet signaling a half-close event
//
// Many methods in battle-tested pktline package are re-used to save us some
// times. At the moment, we don't need server-client half-closed ability. And
// it may affect the performance when wrapping huge data sent from the server.

const (
	// maxChunkSize is the maximum chunk size of data. The chunk size must include 4-byte
	// length prefix. This constant is different from MaxSidebandData because
	// we don't include the sideband number.
	maxChunkSize = pktline.MaxPktSize - 4
)

// ServerConn is a wrapper around net.Conn with the support of half-closed
// capacity for sidechannel. This struct is expected to be used by
// sidechannel's server only.
type ServerConn struct {
	conn net.Conn
	r    io.Reader
}

func newServerConn(c net.Conn) *ServerConn {
	scanner := pktline.NewScanner(c)
	reader := streamio.NewReader(func() ([]byte, error) {
		if !scanner.Scan() {
			if err := scanner.Err(); err != nil {
				return nil, err
			}
			// If there is any error while scanning, scanner.Err() returns a
			// non-nil error. If scanner.Err() returns nil, the connection
			// reaches end-of-file. However, the effect of returning io.EOF is
			// that we allow two kinds of streams: "000fhello world0000" (with
			// trailing 0000) and "000fhello world" (without trialing 0000).
			// Having optional behaviors like this is a source of complexity.
			// We should not allow "000fhello world" without the trailing 0000.
			return nil, io.ErrUnexpectedEOF
		}

		if pktline.IsFlush(scanner.Bytes()) {
			return nil, io.EOF
		}

		data := scanner.Bytes()
		if len(data) < 4 {
			return nil, fmt.Errorf("sidechannel: invalid packet %q", data)
		}

		// pktline treats 0001, 0002, or 0003 as magic empty packets
		// They are irrelevant to sidechannel, hence should be rejected
		if len(data) == 4 {
			if s := string(data); s == "0001" || s == "0002" || s == "0003" {
				return nil, fmt.Errorf("sidechannel: invalid header %s", string(data[3]))
			}
		}

		return data[4:], nil
	})

	return &ServerConn{conn: c, r: reader}
}

// Read reads up to len(p) bytes into p. It returns the number of bytes read or
// any error encountered. This struct overrides Read() to extract the data
// wrapped in a frame generated by ClientConn.Write().
func (cc *ServerConn) Read(p []byte) (n int, err error) {
	return cc.r.Read(p)
}

// Write writes data to the connection. This method fallbacks to underlying
// connection without any modificiation.
func (cc *ServerConn) Write(b []byte) (n int, err error) {
	return cc.conn.Write(b)
}

// Close closes the connection. This method fallbacks to underlying
// connection without any modificiation.
func (cc *ServerConn) Close() error {
	return cc.conn.Close()
}

// ClientConn is a wrapper around net.Conn with the support of half-closed
// capacity for sidechannel. This struct is expected to use by sidechannel's
// client only.
type ClientConn struct {
	conn        net.Conn
	writeClosed bool
}

func newClientConn(c net.Conn) *ClientConn {
	return &ClientConn{conn: c}
}

// Read reads data from the connection. This method fallbacks to underlying
// connection without any modificiation.
func (cc *ClientConn) Read(b []byte) (n int, err error) {
	return cc.conn.Read(b)
}

// Write writes len(p) bytes from p to the underlying data stream.  It returns
// the number of bytes written from p and any error encountered that caused the
// write to stop early. This method overrides Write() to wrap the writing data
// into a frame. The frame is then extracted and read by ServerConn.Read().
func (cc *ClientConn) Write(p []byte) (int, error) {
	if cc.writeClosed {
		return 0, fmt.Errorf("sidechannel: write into a half-closed connection")
	}

	var n int

	for len(p) > 0 {
		chunk := maxChunkSize
		if len(p) < chunk {
			chunk = len(p)
		}

		if _, err := fmt.Fprintf(cc.conn, "%04x", chunk+4); err != nil {
			return n, err
		}
		if _, err := cc.conn.Write(p[:chunk]); err != nil {
			return n, err
		}
		n += chunk
		p = p[chunk:]
	}

	return n, nil
}

func (cc *ClientConn) close() error {
	return cc.conn.Close()
}

// CloseWrite shuts down the writing side of the connection. After this call,
// any read operations from the server return EOF. The reading side is still
// functional so that the server is still able to write back to the client. Any
// attempt to write into a half-closed connection returns an error.
func (cc *ClientConn) CloseWrite() error {
	if cc.writeClosed {
		return nil
	}

	cc.writeClosed = true
	if err := pktline.WriteFlush(cc.conn); err != nil {
		return err
	}

	return nil
}