~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
112 lines · 2.5 KB Raw History
package httptunnel

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"strconv"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/tunnel"
)

const ResponseLimit = 32 << 20

type Protocol struct {
	Dialer tunnel.Dialer
}

func (p *Protocol) RoundTrip(req *http.Request) (*http.Response, error) {
	if p == nil || p.Dialer == nil {
		return nil, errors.New("httptunnel: no dialer")
	}
	if req.URL == nil || req.URL.Scheme != tunnel.KindWebRTC {
		return nil, errors.New("httptunnel: the request is not a webrtc endpoint")
	}
	ctx := req.Context()
	conn, err := p.Dialer.DialContext(ctx, tunnel.NetworkWebRTC, req.URL.Host)
	if err != nil {
		return nil, err
	}
	cancel := context.AfterFunc(ctx, func() {
		_ = conn.SetDeadline(time.Now())
		_ = conn.Close()
	})
	if deadline, ok := ctx.Deadline(); ok {
		_ = conn.SetDeadline(deadline)
	}
	out := req.Clone(ctx)
	out.URL.Scheme = "http"
	out.URL.Host = req.URL.Host
	out.Host = req.URL.Host
	out.Close = true
	if err := out.Write(conn); err != nil {
		cancel()
		_ = conn.Close()
		return nil, err
	}
	response, err := http.ReadResponse(bufio.NewReader(conn), out)
	if err != nil {
		cancel()
		_ = conn.Close()
		return nil, err
	}
	response.Body = &body{ReadCloser: response.Body, conn: conn, cancel: cancel}
	return response, nil
}

type body struct {
	io.ReadCloser

	conn net.Conn

	cancel func() bool

	once sync.Once
}

func (b *body) Close() error {
	err := b.ReadCloser.Close()
	b.once.Do(func() {
		b.cancel()
		_ = b.conn.Close()
	})
	return err
}

func Serve(ctx context.Context, conn net.Conn, handler http.Handler) error {
	if conn == nil {
		return errors.New("httptunnel: no connection")
	}
	if handler == nil {
		return errors.New("httptunnel: no handler")
	}
	request, err := http.ReadRequest(bufio.NewReader(conn))
	if err != nil {
		return err
	}
	request = request.WithContext(ctx)
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, request)
	response := recorder.Result()
	defer response.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(response.Body, ResponseLimit+1))
	if err != nil {
		return err
	}
	if len(raw) > ResponseLimit {
		return errors.New("httptunnel: the response is too large")
	}
	response.Body = io.NopCloser(bytes.NewReader(raw))
	response.ContentLength = int64(len(raw))
	response.Header.Del("Transfer-Encoding")
	response.Header.Set("Content-Length", strconv.Itoa(len(raw)))
	response.Close = true
	return response.Write(conn)
}