mirror of
https://github.com/aler9/gortsplib
synced 2026-04-22 16:27:06 +08:00
client: fix RTSP-over-HTTP tunnel request target (#1041)
This commit is contained in:
@@ -1133,7 +1133,7 @@ func (c *Client) destroyWriter() {
|
||||
c.writerMutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) connOpen() error {
|
||||
func (c *Client) connOpen(u *base.URL) error {
|
||||
if c.nconn != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -1156,7 +1156,7 @@ func (c *Client) connOpen() error {
|
||||
case TunnelHTTP:
|
||||
var err error
|
||||
nconn, err = newClientTunnelHTTP(dialCtx, addr, (c.Scheme == schemeRTSPS),
|
||||
c.TLSConfig, c.DialContext, c.DialTLSContext)
|
||||
c.TLSConfig, c.DialContext, c.DialTLSContext, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1385,7 +1385,7 @@ func (c *Client) doOptions(u *base.URL) (*base.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.connOpen()
|
||||
err = c.connOpen(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1436,7 +1436,7 @@ func (c *Client) doDescribe(u *base.URL) (*description.Session, *base.Response,
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = c.connOpen()
|
||||
err = c.connOpen(u)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -1549,7 +1549,7 @@ func (c *Client) doAnnounce(u *base.URL, desc *description.Session) (*base.Respo
|
||||
return nil, fmt.Errorf("recording with UDP multicast is not supported")
|
||||
}
|
||||
|
||||
err = c.connOpen()
|
||||
err = c.connOpen(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1638,7 +1638,7 @@ func (c *Client) doSetup(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.connOpen()
|
||||
err = c.connOpen(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+188
-4
@@ -709,10 +709,10 @@ func TestClientTunnelHTTP(t *testing.T) {
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
URL: &url.URL{
|
||||
Path: "/",
|
||||
Path: "/teststream",
|
||||
},
|
||||
Host: "localhost:8554",
|
||||
RequestURI: "/",
|
||||
RequestURI: "/teststream",
|
||||
Header: http.Header{
|
||||
"Accept": []string{"application/x-rtsp-tunnelled"},
|
||||
"Content-Length": []string{"30000"},
|
||||
@@ -755,10 +755,10 @@ func TestClientTunnelHTTP(t *testing.T) {
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
URL: &url.URL{
|
||||
Path: "/",
|
||||
Path: "/teststream",
|
||||
},
|
||||
Host: "localhost:8554",
|
||||
RequestURI: "/",
|
||||
RequestURI: "/teststream",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/x-rtsp-tunnelled"},
|
||||
"Content-Length": []string{"30000"},
|
||||
@@ -844,6 +844,190 @@ func TestClientTunnelHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientTunnelHTTPPathQuery(t *testing.T) {
|
||||
for _, ca := range []string{"http", "https"} {
|
||||
t.Run(ca, func(t *testing.T) {
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if ca == "http" {
|
||||
l, err = net.Listen("tcp", "localhost:8554")
|
||||
require.NoError(t, err)
|
||||
defer l.Close()
|
||||
} else {
|
||||
var cert tls.Certificate
|
||||
cert, err = tls.X509KeyPair(serverCert, serverKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
l, err = tls.Listen("tcp", "localhost:8554", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
require.NoError(t, err)
|
||||
defer l.Close()
|
||||
}
|
||||
|
||||
var scheme string
|
||||
if ca == "http" {
|
||||
scheme = "rtsp"
|
||||
} else {
|
||||
scheme = "rtsps"
|
||||
}
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
defer func() { <-serverDone }()
|
||||
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
|
||||
nconn1, err2 := l.Accept()
|
||||
require.NoError(t, err2)
|
||||
defer nconn1.Close()
|
||||
|
||||
buf1 := bufio.NewReader(nconn1)
|
||||
req1, err2 := http.ReadRequest(buf1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
require.Equal(t, &http.Request{
|
||||
Method: http.MethodGet,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
URL: &url.URL{
|
||||
Path: "/teststream",
|
||||
RawQuery: "param=value",
|
||||
},
|
||||
Host: "localhost:8554",
|
||||
RequestURI: "/teststream?param=value",
|
||||
Header: http.Header{
|
||||
"Accept": []string{"application/x-rtsp-tunnelled"},
|
||||
"Content-Length": []string{"30000"},
|
||||
"X-Sessioncookie": req1.Header["X-Sessioncookie"],
|
||||
},
|
||||
ContentLength: 30000,
|
||||
Body: req1.Body,
|
||||
}, req1)
|
||||
|
||||
require.NotEmpty(t, req1.Header.Get("X-Sessioncookie"))
|
||||
|
||||
h := http.Header{}
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Content-Type", "application/x-rtsp-tunnelled")
|
||||
h.Set("Pragma", "no-cache")
|
||||
res := http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: req1.ProtoMinor,
|
||||
Header: h,
|
||||
ContentLength: -1,
|
||||
}
|
||||
var resBuf bytes.Buffer
|
||||
res.Write(&resBuf) //nolint:errcheck
|
||||
_, err = nconn1.Write(resBuf.Bytes())
|
||||
require.NoError(t, err)
|
||||
|
||||
nconn2, err2 := l.Accept()
|
||||
require.NoError(t, err2)
|
||||
defer nconn2.Close()
|
||||
|
||||
buf2 := bufio.NewReader(nconn2)
|
||||
req2, err2 := http.ReadRequest(buf2)
|
||||
require.NoError(t, err2)
|
||||
|
||||
require.Equal(t, &http.Request{
|
||||
Method: http.MethodPost,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
URL: &url.URL{
|
||||
Path: "/teststream",
|
||||
RawQuery: "param=value",
|
||||
},
|
||||
Host: "localhost:8554",
|
||||
RequestURI: "/teststream?param=value",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/x-rtsp-tunnelled"},
|
||||
"Content-Length": []string{"30000"},
|
||||
"X-Sessioncookie": req2.Header["X-Sessioncookie"],
|
||||
},
|
||||
ContentLength: 30000,
|
||||
Body: req2.Body,
|
||||
}, req2)
|
||||
|
||||
require.Equal(t, req1.Header.Get("X-Sessioncookie"), req2.Header.Get("X-Sessioncookie"))
|
||||
|
||||
h = http.Header{}
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Content-Type", "application/x-rtsp-tunnelled")
|
||||
h.Set("Pragma", "no-cache")
|
||||
res = http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: req1.ProtoMinor,
|
||||
Header: h,
|
||||
ContentLength: -1,
|
||||
}
|
||||
resBuf = bytes.Buffer{}
|
||||
res.Write(&resBuf) //nolint:errcheck
|
||||
_, err = nconn2.Write(resBuf.Bytes())
|
||||
require.NoError(t, err)
|
||||
|
||||
conn := conn.NewConn(bufio.NewReader(base64streamreader.New(buf2)), nconn1)
|
||||
|
||||
req, err2 := conn.ReadRequest()
|
||||
require.NoError(t, err2)
|
||||
require.Equal(t, base.Options, req.Method)
|
||||
|
||||
err2 = conn.WriteResponse(&base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
Header: base.Header{
|
||||
"Public": base.HeaderValue{strings.Join([]string{
|
||||
string(base.Describe),
|
||||
}, ", ")},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err2)
|
||||
|
||||
req, err2 = conn.ReadRequest()
|
||||
require.NoError(t, err2)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL(scheme+"://localhost:8554/teststream?param=value"), req.URL)
|
||||
|
||||
medias := []*description.Media{testH264Media}
|
||||
|
||||
err2 = conn.WriteResponse(&base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
Header: base.Header{
|
||||
"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
|
||||
"Content-Base": base.HeaderValue{"/relative-content-base"},
|
||||
},
|
||||
Body: mediasToSDP(medias),
|
||||
})
|
||||
require.NoError(t, err2)
|
||||
}()
|
||||
|
||||
u, err := base.ParseURL(scheme + "://localhost:8554/teststream?param=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
c := Client{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Tunnel: TunnelHTTP,
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = c.Start()
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
_, res, err := c.Describe(u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientTunnelWebSocket(t *testing.T) {
|
||||
for _, ca := range []string{"ws", "wss"} {
|
||||
t.Run(ca, func(t *testing.T) {
|
||||
|
||||
+23
-2
@@ -12,6 +12,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
||||
)
|
||||
|
||||
type clientTunnelHTTP struct {
|
||||
@@ -61,6 +63,7 @@ func newClientTunnelHTTP(
|
||||
tlsConfig *tls.Config,
|
||||
dialContext func(ctx context.Context, network, address string) (net.Conn, error),
|
||||
dialTLSContext func(ctx context.Context, network string, addr string) (net.Conn, error),
|
||||
u *base.URL,
|
||||
) (net.Conn, error) {
|
||||
c := &clientTunnelHTTP{}
|
||||
|
||||
@@ -123,11 +126,12 @@ func newClientTunnelHTTP(
|
||||
}()
|
||||
|
||||
tunnelID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
requestTarget := clientTunnelHTTPRequestTarget(u)
|
||||
|
||||
// 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" +
|
||||
"GET " + requestTarget + " HTTP/1.1\r\n" +
|
||||
"Host: " + addr + "\r\n" +
|
||||
"X-Sessioncookie: " + tunnelID + "\r\n" +
|
||||
"Accept: application/x-rtsp-tunnelled\r\n" +
|
||||
@@ -189,7 +193,7 @@ func newClientTunnelHTTP(
|
||||
// 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" +
|
||||
"POST " + requestTarget + " HTTP/1.1\r\n" +
|
||||
"Host: " + addr + "\r\n" +
|
||||
"X-Sessioncookie: " + tunnelID + "\r\n" +
|
||||
"Content-Type: application/x-rtsp-tunnelled\r\n" +
|
||||
@@ -214,3 +218,20 @@ func newClientTunnelHTTP(
|
||||
ok = true
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func clientTunnelHTTPRequestTarget(u *base.URL) string {
|
||||
if u == nil {
|
||||
return "/"
|
||||
}
|
||||
|
||||
ret := u.Path
|
||||
if ret == "" {
|
||||
ret = "/"
|
||||
}
|
||||
|
||||
if u.RawQuery != "" {
|
||||
ret += "?" + u.RawQuery
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package gortsplib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
||||
)
|
||||
|
||||
func TestClientTunnelHTTPRequestTarget(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
streamURL *base.URL
|
||||
expectedTarget string
|
||||
}{
|
||||
{
|
||||
name: "nil url",
|
||||
streamURL: nil,
|
||||
expectedTarget: "/",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
streamURL: mustParseURL("rtsp://localhost:8554"),
|
||||
expectedTarget: "/",
|
||||
},
|
||||
{
|
||||
name: "path with query",
|
||||
streamURL: mustParseURL("rtsp://localhost:8554/teststream?param=value"),
|
||||
expectedTarget: "/teststream?param=value",
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
require.Equal(t, testCase.expectedTarget, clientTunnelHTTPRequestTarget(testCase.streamURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user