install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
Method byte
|
||||
user string
|
||||
pass string
|
||||
header string
|
||||
h1nonce string
|
||||
}
|
||||
|
||||
const (
|
||||
AuthNone byte = iota
|
||||
AuthUnknown
|
||||
AuthBasic
|
||||
AuthDigest
|
||||
AuthTPLink // https://drmnsamoliu.github.io/video.html
|
||||
)
|
||||
|
||||
func NewAuth(user *url.Userinfo) *Auth {
|
||||
a := new(Auth)
|
||||
a.user = user.Username()
|
||||
a.pass, _ = user.Password()
|
||||
if a.user != "" {
|
||||
a.Method = AuthUnknown
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Auth) Read(res *Response) bool {
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
if len(auth) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch auth[:6] {
|
||||
case "Basic ":
|
||||
a.header = "Basic " + B64(a.user, a.pass)
|
||||
a.Method = AuthBasic
|
||||
return true
|
||||
case "Digest":
|
||||
realm := Between(auth, `realm="`, `"`)
|
||||
nonce := Between(auth, `nonce="`, `"`)
|
||||
|
||||
a.h1nonce = HexMD5(a.user, realm, a.pass) + ":" + nonce
|
||||
a.header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s"`,
|
||||
a.user, realm, nonce,
|
||||
)
|
||||
a.Method = AuthDigest
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Write(req *Request) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch a.Method {
|
||||
case AuthBasic:
|
||||
req.Header.Set("Authorization", a.header)
|
||||
case AuthDigest:
|
||||
// important to use String except RequestURL for RtspServer:
|
||||
// https://github.com/AlexxIT/go2rtc/issues/244
|
||||
uri := req.URL.String()
|
||||
h2 := HexMD5(req.Method, uri)
|
||||
response := HexMD5(a.h1nonce, h2)
|
||||
header := a.header + fmt.Sprintf(
|
||||
`, uri="%s", response="%s"`, uri, response,
|
||||
)
|
||||
req.Header.Set("Authorization", header)
|
||||
case AuthTPLink:
|
||||
req.URL.Host = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Validate(req *Request) (valid, empty bool) {
|
||||
if a == nil {
|
||||
return true, true
|
||||
}
|
||||
|
||||
header := req.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return false, true
|
||||
}
|
||||
|
||||
if a.Method == AuthUnknown {
|
||||
a.Method = AuthBasic
|
||||
a.header = "Basic " + B64(a.user, a.pass)
|
||||
}
|
||||
|
||||
return header == a.header, false
|
||||
}
|
||||
|
||||
func (a *Auth) ReadNone(res *Response) bool {
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
if strings.Contains(auth, "TP-LINK Streaming Media") {
|
||||
a.Method = AuthTPLink
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Auth) UserInfo() *url.Userinfo {
|
||||
return url.UserPassword(a.user, a.pass)
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
i = strings.Index(s, sub2)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
func HexMD5(s ...string) string {
|
||||
b := md5.Sum([]byte(strings.Join(s, ":")))
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func B64(s ...string) string {
|
||||
b := []byte(strings.Join(s, ":"))
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Dial - for RTSP(S|X) and RTMP(S|X)
|
||||
func Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {
|
||||
var address string
|
||||
var hostname string // without port
|
||||
if i := strings.IndexByte(u.Host, ':'); i > 0 {
|
||||
address = u.Host
|
||||
hostname = u.Host[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "rtsp", "rtsps", "rtspx":
|
||||
address = u.Host + ":554"
|
||||
case "rtmp":
|
||||
address = u.Host + ":1935"
|
||||
case "rtmps", "rtmpx":
|
||||
address = u.Host + ":443"
|
||||
}
|
||||
hostname = u.Host
|
||||
}
|
||||
|
||||
var secure *tls.Config
|
||||
|
||||
switch u.Scheme {
|
||||
case "rtsp", "rtmp":
|
||||
case "rtsps", "rtspx", "rtmps", "rtmpx":
|
||||
if u.Scheme[4] == 'x' || IsIP(hostname) {
|
||||
secure = &tls.Config{InsecureSkipVerify: true}
|
||||
} else {
|
||||
secure = &tls.Config{ServerName: hostname}
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", address, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if secure == nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, secure)
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme[4] == 'x' {
|
||||
u.Scheme = u.Scheme[:4] + "s"
|
||||
}
|
||||
|
||||
return tlsConn, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// Do - http.Client with support Digest Authorization
|
||||
func Do(req *http.Request) (*http.Response, error) {
|
||||
var secure *tls.Config
|
||||
|
||||
switch req.URL.Scheme {
|
||||
case "httpx":
|
||||
secure = insecureConfig
|
||||
req.URL.Scheme = "https"
|
||||
case "https":
|
||||
if hostname := req.URL.Hostname(); IsIP(hostname) {
|
||||
secure = insecureConfig
|
||||
}
|
||||
}
|
||||
|
||||
if secure != nil {
|
||||
ctx := context.WithValue(req.Context(), secureKey, secure)
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
dial := transport.DialContext
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := dial(ctx, network, addr)
|
||||
if pconn, ok := ctx.Value(connKey).(*net.Conn); ok {
|
||||
*pconn = conn
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := dial(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conf *tls.Config
|
||||
if v, ok := ctx.Value(secureKey).(*tls.Config); ok {
|
||||
conf = v
|
||||
} else if host, _, err := net.SplitHostPort(addr); err != nil {
|
||||
conf = &tls.Config{ServerName: addr}
|
||||
} else {
|
||||
conf = &tls.Config{ServerName: host}
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, conf)
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pconn, ok := ctx.Value(connKey).(*net.Conn); ok {
|
||||
*pconn = tlsConn
|
||||
}
|
||||
return tlsConn, err
|
||||
}
|
||||
|
||||
client = &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
user := req.URL.User
|
||||
|
||||
// Hikvision won't answer on Basic auth with any headers
|
||||
if strings.HasPrefix(req.URL.Path, "/ISAPI/") {
|
||||
req.URL.User = nil
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusUnauthorized && user != nil {
|
||||
Close(res)
|
||||
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
if !strings.HasPrefix(auth, "Digest") {
|
||||
return nil, errors.New("unsupported auth: " + auth)
|
||||
}
|
||||
|
||||
realm := Between(auth, `realm="`, `"`)
|
||||
nonce := Between(auth, `nonce="`, `"`)
|
||||
qop := Between(auth, `qop="`, `"`)
|
||||
|
||||
username := user.Username()
|
||||
password, _ := user.Password()
|
||||
ha1 := HexMD5(username, realm, password)
|
||||
|
||||
uri := req.URL.RequestURI()
|
||||
ha2 := HexMD5(req.Method, uri)
|
||||
|
||||
var header string
|
||||
|
||||
switch qop {
|
||||
case "":
|
||||
response := HexMD5(ha1, nonce, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||
username, realm, nonce, uri, response,
|
||||
)
|
||||
case "auth":
|
||||
nc := "00000001"
|
||||
cnonce := core.RandString(32, 64)
|
||||
response := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||
)
|
||||
default:
|
||||
return nil, errors.New("unsupported qop: " + auth)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", header)
|
||||
|
||||
if res, err = client.Do(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
|
||||
type key string
|
||||
|
||||
var connKey = key("conn")
|
||||
var secureKey = key("secure")
|
||||
|
||||
var insecureConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
|
||||
// this cipher suites disabled starting from https://tip.golang.org/doc/go1.22
|
||||
// but cameras can't work without them https://github.com/AlexxIT/go2rtc/issues/1172
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // insecure
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // insecure
|
||||
},
|
||||
}
|
||||
|
||||
func WithConn() (context.Context, *net.Conn) {
|
||||
pconn := new(net.Conn)
|
||||
return context.WithValue(context.Background(), connKey, pconn), pconn
|
||||
}
|
||||
|
||||
func Close(res *http.Response) {
|
||||
if res.Body != nil {
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func IsIP(hostname string) bool {
|
||||
return net.ParseIP(hostname) != nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const EndLine = "\r\n"
|
||||
|
||||
// Response like http.Response, but with any proto
|
||||
type Response struct {
|
||||
Status string
|
||||
StatusCode int
|
||||
Proto string
|
||||
Header textproto.MIMEHeader
|
||||
Body []byte
|
||||
Request *Request
|
||||
}
|
||||
|
||||
func (r Response) String() string {
|
||||
s := r.Proto + " " + r.Status + EndLine
|
||||
for k, v := range r.Header {
|
||||
s += k + ": " + v[0] + EndLine
|
||||
}
|
||||
s += EndLine
|
||||
if r.Body != nil {
|
||||
s += string(r.Body)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *Response) Write(w io.Writer) (err error) {
|
||||
_, err = w.Write([]byte(r.String()))
|
||||
return
|
||||
}
|
||||
|
||||
func ReadResponse(r *bufio.Reader) (*Response, error) {
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if line == "" {
|
||||
return nil, errors.New("empty response on RTSP request")
|
||||
}
|
||||
|
||||
ss := strings.SplitN(line, " ", 3)
|
||||
if len(ss) != 3 {
|
||||
return nil, fmt.Errorf("malformed response: %s", line)
|
||||
}
|
||||
|
||||
res := &Response{
|
||||
Status: ss[1] + " " + ss[2],
|
||||
Proto: ss[0],
|
||||
}
|
||||
|
||||
res.StatusCode, err = strconv.Atoi(ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Header, err = tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Length"); val != "" {
|
||||
var i int
|
||||
i, err = strconv.Atoi(val)
|
||||
res.Body = make([]byte, i)
|
||||
if _, err = io.ReadAtLeast(r, res.Body, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Request like http.Request, but with any proto
|
||||
type Request struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string
|
||||
Header textproto.MIMEHeader
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (r *Request) String() string {
|
||||
s := r.Method + " " + r.URL.String() + " " + r.Proto + EndLine
|
||||
for k, v := range r.Header {
|
||||
s += k + ": " + v[0] + EndLine
|
||||
}
|
||||
s += EndLine
|
||||
if r.Body != nil {
|
||||
s += string(r.Body)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *Request) Write(w io.Writer) (err error) {
|
||||
_, err = w.Write([]byte(r.String()))
|
||||
return
|
||||
}
|
||||
|
||||
func ReadRequest(r *bufio.Reader) (*Request, error) {
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss := strings.SplitN(line, " ", 3)
|
||||
if len(ss) != 3 {
|
||||
return nil, fmt.Errorf("wrong request: %s", line)
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
Method: ss[0],
|
||||
Proto: ss[2],
|
||||
}
|
||||
|
||||
req.URL, err = url.Parse(ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header, err = tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := req.Header.Get("Content-Length"); val != "" {
|
||||
var i int
|
||||
i, err = strconv.Atoi(val)
|
||||
req.Body = make([]byte, i)
|
||||
if _, err = io.ReadAtLeast(r, req.Body, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, one, two any) {
|
||||
if one != two {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
data := []byte(`RTSP/1.0 401 Unauthorized
|
||||
WWW-Authenticate: Digest realm="testrealm@host.com",
|
||||
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
|
||||
|
||||
`)
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
r := bufio.NewReader(buf)
|
||||
|
||||
res, err := ReadResponse(r)
|
||||
assert(t, err, nil)
|
||||
|
||||
assert(t, res.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const BinaryMessage = 2
|
||||
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
remain int
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn) *Client {
|
||||
return &Client{conn: conn}
|
||||
}
|
||||
|
||||
const finalBit = 0x80
|
||||
const maskBit = 0x80
|
||||
|
||||
func (w *Client) Read(b []byte) (n int, err error) {
|
||||
if w.remain == 0 {
|
||||
b2 := make([]byte, 2)
|
||||
if _, err = io.ReadFull(w.conn, b2); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
frameType := b2[0] & 0xF
|
||||
w.remain = int(b2[1] & 0x7F)
|
||||
|
||||
switch frameType {
|
||||
case BinaryMessage:
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported frame type: %d", frameType)
|
||||
}
|
||||
|
||||
switch w.remain {
|
||||
case 126:
|
||||
if _, err = io.ReadFull(w.conn, b2); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.remain = int(binary.BigEndian.Uint16(b2))
|
||||
case 127:
|
||||
b8 := make([]byte, 8)
|
||||
if _, err = io.ReadFull(w.conn, b8); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.remain = int(binary.BigEndian.Uint64(b8))
|
||||
}
|
||||
}
|
||||
|
||||
if w.remain > len(b) {
|
||||
n, err = io.ReadFull(w.conn, b)
|
||||
w.remain -= n
|
||||
return
|
||||
}
|
||||
|
||||
n, err = io.ReadFull(w.conn, b[:w.remain])
|
||||
w.remain = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Client) Write(b []byte) (n int, err error) {
|
||||
var data []byte
|
||||
var start byte
|
||||
|
||||
size := len(b)
|
||||
|
||||
switch {
|
||||
case size > 65535:
|
||||
start = 10
|
||||
data = make([]byte, size+14)
|
||||
data[1] = maskBit | 127
|
||||
binary.BigEndian.PutUint64(data[2:], uint64(size))
|
||||
case size > 125:
|
||||
start = 4
|
||||
data = make([]byte, size+8)
|
||||
data[1] = maskBit | 126
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
||||
default:
|
||||
start = 2
|
||||
data = make([]byte, size+6)
|
||||
data[1] = maskBit | byte(size)
|
||||
}
|
||||
|
||||
data[0] = BinaryMessage | finalBit
|
||||
|
||||
mask := data[start : start+4]
|
||||
msg := data[start+4:]
|
||||
|
||||
if _, err = cryptorand.Read(mask); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
msg[i] = b[i] ^ mask[i%4]
|
||||
}
|
||||
|
||||
return w.conn.Write(data)
|
||||
}
|
||||
|
||||
func (w *Client) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
func (w *Client) LocalAddr() net.Addr {
|
||||
return w.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (w *Client) RemoteAddr() net.Addr {
|
||||
return w.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (w *Client) SetDeadline(t time.Time) error {
|
||||
return w.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (w *Client) SetReadDeadline(t time.Time) error {
|
||||
return w.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (w *Client) SetWriteDeadline(t time.Time) error {
|
||||
return w.conn.SetWriteDeadline(t)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func Dial(address string) (net.Conn, error) {
|
||||
if strings.HasPrefix(address, "ws") {
|
||||
address = "http" + address[2:] // support http and https
|
||||
}
|
||||
|
||||
// using custom client for support Digest Auth
|
||||
// https://github.com/AlexxIT/go2rtc/issues/415
|
||||
ctx, pconn := tcp.WithConn()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", address, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, accept := GetKeyAccept()
|
||||
|
||||
// Version, Key, Protocol important for Axis cameras
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
req.Header.Set("Sec-WebSocket-Key", key)
|
||||
req.Header.Set("Sec-WebSocket-Protocol", "binary")
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusSwitchingProtocols {
|
||||
return nil, errors.New("wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
if res.Header.Get("Sec-Websocket-Accept") != accept {
|
||||
return nil, errors.New("wrong websocket accept")
|
||||
}
|
||||
|
||||
return NewClient(*pconn), nil
|
||||
}
|
||||
|
||||
func GetKeyAccept() (key, accept string) {
|
||||
b := make([]byte, 16)
|
||||
_, _ = cryptorand.Read(b)
|
||||
key = base64.StdEncoding.EncodeToString(b)
|
||||
|
||||
h := sha1.New()
|
||||
h.Write([]byte(key))
|
||||
h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
||||
accept = base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user