Files
gortsplib/client_http_tunnel.go
2025-09-15 19:00:50 +02:00

183 lines
3.6 KiB
Go

package gortsplib
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
type clientHTTPTunnel struct {
readChan net.Conn
readBuf *bufio.Reader
writeChan net.Conn
}
func (c *clientHTTPTunnel) Read(p []byte) (n int, err error) {
return c.readBuf.Read(p)
}
func (c *clientHTTPTunnel) Write(p []byte) (n int, err error) {
return c.writeChan.Write([]byte(base64.StdEncoding.EncodeToString(p)))
}
func (c *clientHTTPTunnel) Close() error {
c.readChan.Close()
c.writeChan.Close()
return nil
}
func (c *clientHTTPTunnel) LocalAddr() net.Addr {
return c.readChan.LocalAddr()
}
func (c *clientHTTPTunnel) RemoteAddr() net.Addr {
return c.readChan.RemoteAddr()
}
func (c *clientHTTPTunnel) SetDeadline(_ time.Time) error {
panic("unimplemented")
}
func (c *clientHTTPTunnel) SetReadDeadline(t time.Time) error {
return c.readChan.SetReadDeadline(t)
}
func (c *clientHTTPTunnel) SetWriteDeadline(t time.Time) error {
return c.writeChan.SetWriteDeadline(t)
}
func newClientHTTPTunnel(
ctx context.Context,
dialContext func(ctx context.Context, network, address string) (net.Conn, error),
addr string,
tlsConfig *tls.Config,
) (net.Conn, error) {
c := &clientHTTPTunnel{}
var err error
c.readChan, err = dialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
if tlsConfig != nil {
c.readChan = tls.Client(c.readChan, tlsConfig)
}
ok := false
defer func() {
if !ok {
c.readChan.Close()
}
}()
ctxCheckerReadDone := make(chan struct{})
defer func() { <-ctxCheckerReadDone }()
ctxCheckerReadTerminate := make(chan struct{})
defer close(ctxCheckerReadTerminate)
go func() {
defer close(ctxCheckerReadDone)
select {
case <-ctx.Done():
c.readChan.Close()
case <-ctxCheckerReadTerminate:
}
}()
tunnelID := strings.ReplaceAll(uuid.New().String(), "-", "")
// do not use http.Request
// since Content-Length requires a Body of same size
_, err = c.readChan.Write([]byte(
"GET / HTTP/1.1\r\n" +
"Host: " + addr + "\r\n" +
"X-Sessioncookie: " + tunnelID + "\r\n" +
"Accept: application/x-rtsp-tunnelled\r\n" +
"Content-Length: 30000\r\n" +
"\r\n",
))
if err != nil {
return nil, err
}
c.readBuf = bufio.NewReader(c.readChan)
res, err := http.ReadResponse(c.readBuf, nil)
if err != nil {
return nil, err
}
res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %v", res.StatusCode)
}
c.writeChan, err = dialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
if tlsConfig != nil {
c.writeChan = tls.Client(c.writeChan, tlsConfig)
}
defer func() {
if !ok {
c.writeChan.Close()
}
}()
ctxCheckerWriteDone := make(chan struct{})
defer func() { <-ctxCheckerWriteDone }()
ctxCheckerWriteTerminate := make(chan struct{})
defer close(ctxCheckerWriteTerminate)
go func() {
defer close(ctxCheckerWriteDone)
select {
case <-ctx.Done():
c.writeChan.Close()
case <-ctxCheckerWriteTerminate:
}
}()
// do not use http.Request
// since Content-Length requires a Body of same size
_, err = c.writeChan.Write([]byte(
"POST / HTTP/1.1\r\n" +
"Host: " + addr + "\r\n" +
"X-Sessioncookie: " + tunnelID + "\r\n" +
"Content-Type: application/x-rtsp-tunnelled\r\n" +
"Content-Length: 30000\r\n" +
"\r\n",
))
if err != nil {
return nil, err
}
writeBuf := bufio.NewReader(c.writeChan)
res, err = http.ReadResponse(writeBuf, nil)
if err != nil {
return nil, err
}
res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %v", res.StatusCode)
}
ok = true
return c, nil
}