|
|
|
// Copyright (c) 2025 Winlin
|
Proxy: Support proxy server for SRS. v7.0.16 (#4158)
Please note that the proxy server is a new architecture or the next
version of the Origin Cluster, which allows the publication of multiple
streams. The SRS origin cluster consists of a group of origin servers
designed to handle a large number of streams.
```text
+-----------------------+
+---+ SRS Proxy(Deployment) +------+---------------------+
+-----------------+ | +-----------+-----------+ + +
| LB(K8s Service) +--+ +(Redis/MESH) + SRS Origin Servers +
+-----------------+ | +-----------+-----------+ + (Deployment) +
+---+ SRS Proxy(Deployment) +------+---------------------+
+-----------------------+
```
The new origin cluster is designed as a collection of proxy servers. For
more information, see [Discussion
#3634](https://github.com/ossrs/srs/discussions/3634). If you prefer to
use the old origin cluster, please switch to a version before SRS 6.0.
A proxy server can be used for a set of origin servers, which are
isolated and dedicated origin servers. The main improvement in the new
architecture is to store the state for origin servers in the proxy
server, rather than using MESH to communicate between origin servers.
With a proxy server, you can deploy origin servers as stateless servers,
such as in a Kubernetes (K8s) deployment.
Now that the proxy server is a stateful server, it uses Redis to store
the states. For faster development, we use Go to develop the proxy
server, instead of C/C++. Therefore, the proxy server itself is also
stateless, with all states stored in the Redis server or cluster. This
makes the new origin cluster architecture very powerful and robust.
The proxy server is also an architecture designed to solve multiple
process bottlenecks. You can run hundreds of SRS origin servers with one
proxy server on the same machine. This solution can utilize multi-core
machines, such as servers with 128 CPUs. Thus, we can keep SRS
single-threaded and very simple. See
https://github.com/ossrs/srs/discussions/3665#discussioncomment-6474441
for details.
```text
+--------------------+
+-------+ SRS Origin Server +
+ +--------------------+
+
+-----------------------+ + +--------------------+
+ SRS Proxy(Deployment) +------+-------+ SRS Origin Server +
+-----------------------+ + +--------------------+
+
+ +--------------------+
+-------+ SRS Origin Server +
+--------------------+
```
Keep in mind that the proxy server for the Origin Cluster is designed to
handle many streams. To address the issue of many viewers, we will
enhance the Edge Cluster to support more protocols.
```text
+------------------+ +--------------------+
+ SRS Edge Server +--+ +-------+ SRS Origin Server +
+------------------+ + + +--------------------+
+ +
+------------------+ + +-----------------------+ + +--------------------+
+ SRS Edge Server +--+-----+ SRS Proxy(Deployment) +------+-------+ SRS Origin Server +
+------------------+ + +-----------------------+ + +--------------------+
+ +
+------------------+ + + +--------------------+
+ SRS Edge Server +--+ +-------+ SRS Origin Server +
+------------------+ +--------------------+
```
With the new Origin Cluster and Edge Cluster, you have a media system
capable of supporting a large number of streams and viewers. For
example, you can publish 10,000 streams, each with 100,000 viewers.
---------
Co-authored-by: Jacob Su <suzp1984@gmail.com>
5 months ago
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/json"
|
|
|
|
stdErr "errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"reflect"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"srs-proxy/errors"
|
|
|
|
"srs-proxy/logger"
|
|
|
|
)
|
|
|
|
|
|
|
|
func apiResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, data any) {
|
|
|
|
w.Header().Set("Server", fmt.Sprintf("%v/%v", Signature(), Version()))
|
|
|
|
|
|
|
|
b, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
apiError(ctx, w, r, errors.Wrapf(err, "marshal %v %v", reflect.TypeOf(data), data))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
w.Write(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
|
|
|
|
logger.Wf(ctx, "HTTP API error %+v", err)
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
fmt.Fprintln(w, fmt.Sprintf("%v", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiCORS(ctx context.Context, w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
// Always support CORS. Note that browser may send origin header for m3u8, but no origin header
|
|
|
|
// for ts. So we always response CORS header.
|
|
|
|
if true {
|
|
|
|
// SRS does not need cookie or credentials, so we disable CORS credentials, and use * for CORS origin,
|
|
|
|
// headers, expose headers and methods.
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
|
|
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
|
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "*")
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method == http.MethodOptions {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseGracefullyQuitTimeout() (time.Duration, error) {
|
|
|
|
if t, err := time.ParseDuration(envGraceQuitTimeout()); err != nil {
|
|
|
|
return 0, errors.Wrapf(err, "parse duration %v", envGraceQuitTimeout())
|
|
|
|
} else {
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseBody read the body from r, and unmarshal JSON to v.
|
|
|
|
func ParseBody(r io.ReadCloser, v interface{}) error {
|
|
|
|
b, err := ioutil.ReadAll(r)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "read body")
|
|
|
|
}
|
|
|
|
defer r.Close()
|
|
|
|
|
|
|
|
if len(b) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(b, v); err != nil {
|
|
|
|
return errors.Wrapf(err, "json unmarshal %v", string(b))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildStreamURL build as vhost/app/stream for stream URL r.
|
|
|
|
func buildStreamURL(r string) (string, error) {
|
|
|
|
u, err := url.Parse(r)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "parse url %v", r)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not domain or ip in hostname, it's __defaultVhost__.
|
|
|
|
defaultVhost := !strings.Contains(u.Hostname(), ".")
|
|
|
|
|
|
|
|
// If hostname is actually an IP address, it's __defaultVhost__.
|
|
|
|
if ip := net.ParseIP(u.Hostname()); ip.To4() != nil {
|
|
|
|
defaultVhost = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if defaultVhost {
|
|
|
|
return fmt.Sprintf("__defaultVhost__%v", u.Path), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore port, only use hostname as vhost.
|
|
|
|
return fmt.Sprintf("%v%v", u.Hostname(), u.Path), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// isPeerClosedError indicates whether peer object closed the connection.
|
|
|
|
func isPeerClosedError(err error) bool {
|
|
|
|
causeErr := errors.Cause(err)
|
|
|
|
|
|
|
|
if stdErr.Is(causeErr, io.EOF) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if stdErr.Is(causeErr, syscall.EPIPE) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if netErr, ok := causeErr.(*net.OpError); ok {
|
|
|
|
if sysErr, ok := netErr.Err.(*os.SyscallError); ok {
|
|
|
|
if stdErr.Is(sysErr.Err, syscall.ECONNRESET) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// convertURLToStreamURL convert the URL in HTTP request to special URLs. The unifiedURL is the URL
|
|
|
|
// in unified, foramt as scheme://vhost/app/stream without extensions. While the fullURL is the unifiedURL
|
|
|
|
// with extension.
|
|
|
|
func convertURLToStreamURL(r *http.Request) (unifiedURL, fullURL string) {
|
|
|
|
scheme := "http"
|
|
|
|
if r.TLS != nil {
|
|
|
|
scheme = "https"
|
|
|
|
}
|
|
|
|
|
|
|
|
hostname := "__defaultVhost__"
|
|
|
|
if strings.Contains(r.Host, ":") {
|
|
|
|
if v, _, err := net.SplitHostPort(r.Host); err == nil {
|
|
|
|
hostname = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var appStream, streamExt string
|
|
|
|
|
|
|
|
// Parse app/stream from query string.
|
|
|
|
q := r.URL.Query()
|
|
|
|
if app := q.Get("app"); app != "" {
|
|
|
|
appStream = "/" + app
|
|
|
|
}
|
|
|
|
if stream := q.Get("stream"); stream != "" {
|
|
|
|
appStream = fmt.Sprintf("%v/%v", appStream, stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse app/stream from path.
|
|
|
|
if appStream == "" {
|
|
|
|
streamExt = path.Ext(r.URL.Path)
|
|
|
|
appStream = strings.TrimSuffix(r.URL.Path, streamExt)
|
|
|
|
}
|
|
|
|
|
|
|
|
unifiedURL = fmt.Sprintf("%v://%v%v", scheme, hostname, appStream)
|
|
|
|
fullURL = fmt.Sprintf("%v%v", unifiedURL, streamExt)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// rtcIsSTUN returns true if data of UDP payload is a STUN packet.
|
|
|
|
func rtcIsSTUN(data []byte) bool {
|
|
|
|
return len(data) > 0 && (data[0] == 0 || data[0] == 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// rtcIsRTPOrRTCP returns true if data of UDP payload is a RTP or RTCP packet.
|
|
|
|
func rtcIsRTPOrRTCP(data []byte) bool {
|
|
|
|
return len(data) >= 12 && (data[0]&0xC0) == 0x80
|
|
|
|
}
|
|
|
|
|
|
|
|
// srtIsHandshake returns true if data of UDP payload is a SRT handshake packet.
|
|
|
|
func srtIsHandshake(data []byte) bool {
|
|
|
|
return len(data) >= 4 && binary.BigEndian.Uint32(data) == 0x80000000
|
|
|
|
}
|
|
|
|
|
|
|
|
// srtParseSocketID parse the socket id from the SRT packet.
|
|
|
|
func srtParseSocketID(data []byte) uint32 {
|
|
|
|
if len(data) >= 16 {
|
|
|
|
return binary.BigEndian.Uint32(data[12:])
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseIceUfragPwd parse the ice-ufrag and ice-pwd from the SDP.
|
|
|
|
func parseIceUfragPwd(sdp string) (ufrag, pwd string, err error) {
|
|
|
|
if true {
|
|
|
|
ufragRe := regexp.MustCompile(`a=ice-ufrag:([^\s]+)`)
|
|
|
|
ufragMatch := ufragRe.FindStringSubmatch(sdp)
|
|
|
|
if len(ufragMatch) <= 1 {
|
|
|
|
return "", "", errors.Errorf("no ice-ufrag in sdp %v", sdp)
|
|
|
|
}
|
|
|
|
ufrag = ufragMatch[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
if true {
|
|
|
|
pwdRe := regexp.MustCompile(`a=ice-pwd:([^\s]+)`)
|
|
|
|
pwdMatch := pwdRe.FindStringSubmatch(sdp)
|
|
|
|
if len(pwdMatch) <= 1 {
|
|
|
|
return "", "", errors.Errorf("no ice-pwd in sdp %v", sdp)
|
|
|
|
}
|
|
|
|
pwd = pwdMatch[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return ufrag, pwd, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseSRTStreamID parse the SRT stream id to host(optional) and resource(required).
|
|
|
|
// See https://ossrs.io/lts/en-us/docs/v7/doc/srt#srt-url
|
|
|
|
func parseSRTStreamID(sid string) (host, resource string, err error) {
|
|
|
|
if true {
|
|
|
|
hostRe := regexp.MustCompile(`h=([^,]+)`)
|
|
|
|
hostMatch := hostRe.FindStringSubmatch(sid)
|
|
|
|
if len(hostMatch) > 1 {
|
|
|
|
host = hostMatch[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if true {
|
|
|
|
resourceRe := regexp.MustCompile(`r=([^,]+)`)
|
|
|
|
resourceMatch := resourceRe.FindStringSubmatch(sid)
|
|
|
|
if len(resourceMatch) <= 1 {
|
|
|
|
return "", "", errors.Errorf("no resource in sid %v", sid)
|
|
|
|
}
|
|
|
|
resource = resourceMatch[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
return host, resource, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseListenEndpoint parse the listen endpoint as:
|
|
|
|
// port The tcp listen port, like 1935.
|
|
|
|
// protocol://ip:port The listen endpoint, like tcp://:1935 or tcp://0.0.0.0:1935
|
|
|
|
func parseListenEndpoint(ep string) (protocol string, ip net.IP, port uint16, err error) {
|
|
|
|
// If no colon in ep, it's port in string.
|
|
|
|
if !strings.Contains(ep, ":") {
|
|
|
|
if p, err := strconv.Atoi(ep); err != nil {
|
|
|
|
return "", nil, 0, errors.Wrapf(err, "parse port %v", ep)
|
|
|
|
} else {
|
|
|
|
return "tcp", nil, uint16(p), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Must be protocol://ip:port schema.
|
|
|
|
parts := strings.Split(ep, ":")
|
|
|
|
if len(parts) != 3 {
|
|
|
|
return "", nil, 0, errors.Errorf("invalid endpoint %v", ep)
|
|
|
|
}
|
|
|
|
|
|
|
|
if p, err := strconv.Atoi(parts[2]); err != nil {
|
|
|
|
return "", nil, 0, errors.Wrapf(err, "parse port %v", parts[2])
|
|
|
|
} else {
|
|
|
|
return parts[0], net.ParseIP(parts[1]), uint16(p), nil
|
|
|
|
}
|
|
|
|
}
|