| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- /*
- * Copyright (c) 2023, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package transforms
- import (
- "bytes"
- "context"
- "math"
- "net"
- "net/textproto"
- "strconv"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
- )
- type HTTPTransformerParameters struct {
- // ProtocolTransformName specifies the name associated with
- // ProtocolTransformSpec and is used for metrics.
- ProtocolTransformName string
- // ProtocolTransformSpec specifies a transform to apply to the HTTP request.
- // See: "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms".
- //
- // HTTP transforms include strategies discovered by the Geneva team,
- // https://geneva.cs.umd.edu.
- ProtocolTransformSpec Spec
- // ProtocolTransformSeed specifies the seed to use for generating random
- // data in the ProtocolTransformSpec transform. To replay a transform,
- // specify the same seed.
- ProtocolTransformSeed *prng.Seed
- }
- const (
- // httpTransformerReadWriteReqLineAndHeaders HTTPTransformer is waiting to
- // finish reading and writing the Request-Line, and headers, of the next
- // request.
- httpTransformerReadWriteReqLineAndHeaders = 0
- // httpTransformerReadWriteBody HTTPTransformer is waiting to finish
- // reading, and writing, the current request body.
- httpTransformerReadWriteBody = 1
- )
- // HTTPTransformer wraps a net.Conn, intercepting Write calls and applying the
- // specified protocol transform.
- //
- // The HTTP request to be written (input to the Write) is converted to a
- // string, transformed, and converted back to binary and then actually written
- // to the underlying net.Conn.
- //
- // HTTPTransformer is not safe for concurrent use.
- type HTTPTransformer struct {
- transform Spec
- seed *prng.Seed
- // state is the HTTPTransformer state. Possible values are
- // httpTransformerReadWriteReqLineAndHeaders and
- // httpTransformerReadWriteBody.
- state int64
- // b is used to buffer the accumulated bytes of the current request until
- // the Request-Line and all headers are received and written.
- b bytes.Buffer
- // remain is the number of remaining request bytes to write to the
- // underlying net.Conn. Set to the value of Content-Length (request body
- // bytes) plus the length of the transformed Request-Line, and headers,
- // once the Request-Line, and headers, of the current request are received.
- remain uint64
- net.Conn
- }
- // Write implements the net.Conn interface.
- //
- // Note: it is assumed that the underlying transport, net.Conn, is a reliable
- // stream transport, i.e. TCP, therefore it is required that the caller stop
- // calling Write() on an instance of HTTPTransformer after an error is returned
- // because, following this assumption, the connection will have failed when a
- // Write() call to the underlying net.Conn fails; a new connection must be
- // established, net.Conn, and wrapped with a new HTTPTransformer. For this
- // reason, the return value may be the number of bytes buffered internally
- // and not the number of bytes written to the underlying net.Conn when a non-nil
- // error is returned.
- //
- // Warning: Does not handle chunked encoding and multiple HTTP requests written
- // in a single Write(). Must be called synchronously.
- func (t *HTTPTransformer) Write(b []byte) (int, error) {
- if t.state == httpTransformerReadWriteReqLineAndHeaders {
- // Do not need to check return value. Applies to all subsequent
- // calls to t.b.Write() and this comment will not be repeated for
- // each. See https://github.com/golang/go/blob/1e9ff255a130200fcc4ec5e911d28181fce947d5/src/bytes/buffer.go#L164.
- t.b.Write(b)
- // Wait until the Request-Line, and all headers, have been read. Must
- // check all accumulated bytes incase the "\r\n\r\n" separator is
- // written over multiple Write() calls; from reading the go1.19.5
- // net/http code the entire HTTP request is written in a single Write()
- // call.
- sep := []byte("\r\n\r\n")
- headerBodyLines := bytes.SplitN(t.b.Bytes(), sep, 2) // split Request-Line, and headers, from body
- if len(headerBodyLines) <= 1 {
- // b buffered in t.b and the Request-Line, and all headers, have not
- // been recieved so another Write() call is expected.
- return len(b), nil
- } // else: Request-Line, and all headers, have been read
- // read Content-Length before applying transform
- var headerLines [][]byte
- lines := bytes.Split(headerBodyLines[0], []byte("\r\n"))
- if len(lines) > 1 {
- // skip Request-Line, e.g. "GET /foo HTTP/1.1"
- headerLines = lines[1:]
- }
- var cl []byte
- contentLengthHeader := []byte("Content-Length:")
- for _, header := range headerLines {
- if bytes.HasPrefix(header, contentLengthHeader) {
- cl = textproto.TrimBytes(header[len(contentLengthHeader):])
- break
- }
- }
- if len(cl) == 0 {
- // Irrecoverable error because either Content-Length header
- // is missing, or Content-Length header value is empty, e.g.
- // "Content-Length: ", and request body length cannot be
- // determined.
- return len(b), errors.TraceNew("Content-Length missing")
- }
- contentLength, err := strconv.ParseUint(string(cl), 10, 63)
- if err != nil {
- // Irrecoverable error because Content-Length is malformed and
- // request body length cannot be determined.
- return len(b), errors.Trace(err)
- }
- t.remain = contentLength
- // transform, and write, Request-Line and headers.
- reqLineAndHeadersLen := len(headerBodyLines[0]) + len(sep)
- reqLineAndHeaders := t.b.Bytes()[:reqLineAndHeadersLen]
- if t.transform != nil {
- newReqLineAndHeaders, err := t.transform.Apply(t.seed, reqLineAndHeaders)
- if err != nil {
- // TODO: consider logging an error and skiping transform
- // instead of returning an error, if the transform is broken
- // then all subsequent applications may fail.
- return len(b), errors.Trace(err)
- }
- // perf: only allocate new slice if length changed, otherwise the
- // transformed data can be copied directly over the original in t.b.
- if len(newReqLineAndHeaders) == len(reqLineAndHeaders) {
- // Do not need to check return value. It is guaranteed that
- // n == len(newReqLineAndHeaders) because t.b.Len() >= n if the
- // transformed data is the same size as the original data.
- copy(t.b.Bytes()[:reqLineAndHeadersLen], newReqLineAndHeaders)
- } else {
- // Copy any request body bytes received before resetting the
- // buffer.
- var reqBody []byte
- reqBodyLen := t.b.Len() - reqLineAndHeadersLen // number of request body bytes received
- if reqBodyLen > 0 {
- reqBody = make([]byte, reqBodyLen)
- copy(reqBody, t.b.Bytes()[reqLineAndHeadersLen:])
- }
- // Reset the buffer and write transformed Request-Line, and
- // headers, and any request body bytes received into it.
- t.b.Reset()
- t.b.Write(newReqLineAndHeaders)
- if len(reqBody) > 0 {
- t.b.Write(reqBody)
- }
- }
- reqLineAndHeaders = newReqLineAndHeaders
- }
- if math.MaxUint64-t.remain < uint64(len(reqLineAndHeaders)) {
- // Irrecoverable error because request is malformed:
- // Content-Length + len(reqLineAndHeaders) > math.MaxUint64.
- return len(b), errors.TraceNew("t.remain + uint64(len(header)) overflows")
- }
- t.remain += uint64(len(reqLineAndHeaders))
- if uint64(t.b.Len()) > t.remain {
- // Should never happen, multiple requests written in a single
- // Write() are not supported.
- return len(b), errors.TraceNew("multiple HTTP requests in single Write() not supported")
- }
- n, err := t.b.WriteTo(t.Conn)
- t.remain -= uint64(n)
- if t.remain > 0 {
- t.state = httpTransformerReadWriteBody
- }
- // Do not wrap any I/O err returned by Conn
- return len(b), err
- }
- // Request-Line, and headers, have been transformed. Write any remaining
- // bytes of these and then write request body.
- // Must write buffered bytes first, in-order, to write bytes to underlying
- // net.Conn in the same order they were received in.
- //
- // Already checked that t.b does not contain bytes of a subsequent HTTP
- // request when the Request-Line, and headers, are parsed, i.e. at this
- // point it is guaranteed that t.b.Len() <= t.remain.
- //
- // In practise the buffer will be empty by this point because its entire
- // contents will have been written in the first call to t.b.WriteTo(t.Conn)
- // when the Request-Line, and headers, are received, parsed, and
- // transformed; otherwise the underlying transport will have failed and the
- // caller will not invoke Write() again on this instance. See
- // HTTPTransformer.Write() comment.
- wrote, err := t.b.WriteTo(t.Conn)
- t.remain -= uint64(wrote)
- if err != nil {
- // b not written or buffered
- // Do not wrap any I/O err returned by Conn
- return 0, err
- }
- if uint64(len(b)) > t.remain {
- return len(b), errors.TraceNew("multiple HTTP requests in single Write() not supported")
- }
- n, err := t.Conn.Write(b)
- t.remain -= uint64(n)
- if t.remain <= 0 {
- // Entire request has been written. Return to waiting for next HTTP
- // request to arrive.
- t.state = httpTransformerReadWriteReqLineAndHeaders
- t.remain = 0
- }
- // Do not wrap any I/O err returned by Conn
- return n, err
- }
- // GetMetrics implements the common.MetricsSource interface.
- func (t *HTTPTransformer) GetMetrics() common.LogFields {
- // Relay any metrics from the underlying conn.
- m, ok := t.Conn.(common.MetricsSource)
- if ok {
- return m.GetMetrics()
- }
- return nil
- }
- func WrapDialerWithHTTPTransformer(dialer common.Dialer, params *HTTPTransformerParameters) common.Dialer {
- return func(ctx context.Context, network, addr string) (net.Conn, error) {
- conn, err := dialer(ctx, network, addr)
- if err != nil {
- return nil, errors.Trace(err)
- }
- return &HTTPTransformer{
- Conn: conn,
- transform: params.ProtocolTransformSpec,
- seed: params.ProtocolTransformSeed,
- }, nil
- }
- }
|