install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# HTTP API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
|
||||
The HTTP API is described in [OpenAPI](../../website/api/openapi.yaml) format. It can be explored in [interactive viewer](https://go2rtc.org/api/). WebSocket API described [here](ws/README.md).
|
||||
|
||||
The project's static HTML and JS files are located in the [www](../../www/README.md) folder. An external developer can use them as a basis for integrating go2rtc into their project or for developing a custom web interface for go2rtc.
|
||||
|
||||
The contents of `www` folder are built into go2rtc when building, but you can use configuration to specify an external folder as the source of static files.
|
||||
|
||||
## Configuration
|
||||
|
||||
**Important!** go2rtc passes requests from localhost and Unix sockets without HTTP authorization, even if you have it configured. It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||
|
||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change the API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- all files from `static_dir` hosted on root path: `/`
|
||||
- you can use raw TLS cert/key content or path to files
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||
username: "admin" # default "", Basic auth for WebUI
|
||||
password: "pass" # default "", Basic auth for WebUI
|
||||
local_auth: true # default false, Enable auth check for localhost requests
|
||||
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
|
||||
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||
origin: "*" # default "", allow CORS requests (only * supported)
|
||||
tls_listen: ":443" # default "", enable HTTPS server
|
||||
tls_cert: | # default "", PEM-encoded fullchain certificate for HTTPS
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----
|
||||
tls_key: | # default "", PEM-encoded private key for HTTPS
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
|
||||
```
|
||||
|
||||
**PS:**
|
||||
|
||||
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
|
||||
- MP4 over WebSocket was created only for Apple iOS because it doesn't support file streaming
|
||||
@@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
LocalAuth bool `yaml:"local_auth"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
TLSListen string `yaml:"tls_listen"`
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
UnixListen string `yaml:"unix_listen"`
|
||||
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
// default config
|
||||
cfg.Mod.Listen = ":1984"
|
||||
|
||||
// load config from YAML
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
|
||||
return
|
||||
}
|
||||
|
||||
allowPaths = cfg.Mod.AllowPaths
|
||||
basePath = cfg.Mod.BasePath
|
||||
log = app.GetLogger("api")
|
||||
|
||||
initStatic(cfg.Mod.StaticDir)
|
||||
|
||||
HandleFunc("api", apiHandler)
|
||||
HandleFunc("api/config", configHandler)
|
||||
HandleFunc("api/exit", exitHandler)
|
||||
HandleFunc("api/restart", restartHandler)
|
||||
HandleFunc("api/log", logHandler)
|
||||
|
||||
Handler = http.DefaultServeMux // 4th
|
||||
|
||||
if cfg.Mod.Origin == "*" {
|
||||
Handler = middlewareCORS(Handler) // 3rd
|
||||
}
|
||||
|
||||
if cfg.Mod.Username != "" {
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
|
||||
}
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
Handler = middlewareLog(Handler) // 1st
|
||||
}
|
||||
|
||||
if cfg.Mod.Listen != "" {
|
||||
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||
Port, _ = strconv.Atoi(port)
|
||||
go listen("tcp", cfg.Mod.Listen)
|
||||
}
|
||||
|
||||
if cfg.Mod.UnixListen != "" {
|
||||
_ = syscall.Unlink(cfg.Mod.UnixListen)
|
||||
go listen("unix", cfg.Mod.UnixListen)
|
||||
}
|
||||
|
||||
// Initialize the HTTPS server
|
||||
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||
}
|
||||
}
|
||||
|
||||
func listen(network, address string) {
|
||||
ln, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api] listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] listen")
|
||||
|
||||
server := http.Server{
|
||||
Handler: Handler,
|
||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||
}
|
||||
if err = server.Serve(ln); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}
|
||||
|
||||
func tlsListen(network, address, certFile, keyFile string) {
|
||||
var cert tls.Certificate
|
||||
var err error
|
||||
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
|
||||
// check if file path
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
} else {
|
||||
// if text file content
|
||||
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api] tls listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||
|
||||
server := &http.Server{
|
||||
Handler: Handler,
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
if err = server.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||
}
|
||||
}
|
||||
|
||||
var Port int
|
||||
|
||||
const (
|
||||
MimeJSON = "application/json"
|
||||
MimeText = "text/plain"
|
||||
)
|
||||
|
||||
var Handler http.Handler
|
||||
|
||||
// HandleFunc handle pattern with relative path:
|
||||
// - "api/streams" => "{basepath}/api/streams"
|
||||
// - "/streams" => "/streams"
|
||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||
if len(pattern) == 0 || pattern[0] != '/' {
|
||||
pattern = basePath + "/" + pattern
|
||||
}
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
|
||||
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
|
||||
return
|
||||
}
|
||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||
http.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// ResponseJSON important always add Content-Type
|
||||
// so go won't need to call http.DetectContentType
|
||||
func ResponseJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", MimeJSON)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func ResponsePrettyJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", MimeJSON)
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
|
||||
func Response(w http.ResponseWriter, body any, contentType string) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch v := body.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(v)
|
||||
case string:
|
||||
_, _ = w.Write([]byte(v))
|
||||
default:
|
||||
_, _ = fmt.Fprint(w, body)
|
||||
}
|
||||
}
|
||||
|
||||
const StreamNotFound = "stream not found"
|
||||
|
||||
var allowPaths []string
|
||||
var basePath string
|
||||
var log zerolog.Logger
|
||||
|
||||
func middlewareLog(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func isLoopback(remoteAddr string) bool {
|
||||
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
|
||||
}
|
||||
|
||||
func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if localAuth || !isLoopback(r.RemoteAddr) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func middlewareCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
app.Info["host"] = r.Host
|
||||
mu.Unlock()
|
||||
|
||||
ResponseJSON(w, app.Info)
|
||||
}
|
||||
|
||||
func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s := r.URL.Query().Get("code")
|
||||
code, err := strconv.Atoi(s)
|
||||
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
|
||||
if err != nil || code < 0 || code > 125 {
|
||||
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[api] restart %s", path)
|
||||
|
||||
go syscall.Exec(path, os.Args, os.Environ())
|
||||
}
|
||||
|
||||
func logHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// Send current state of the log file immediately
|
||||
w.Header().Set("Content-Type", "application/jsonlines")
|
||||
_, _ = app.MemoryLog.WriteTo(w)
|
||||
case "DELETE":
|
||||
app.MemoryLog.Reset()
|
||||
Response(w, "OK", "text/plain")
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Info string `json:"info,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
func ResponseSources(w http.ResponseWriter, sources []*Source) {
|
||||
if len(sources) == 0 {
|
||||
http.Error(w, "no sources", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var response = struct {
|
||||
Sources []*Source `json:"sources"`
|
||||
}{
|
||||
Sources: sources,
|
||||
}
|
||||
ResponseJSON(w, response)
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, err error) {
|
||||
log.Error().Err(err).Caller(1).Send()
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInsufficientStorage)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if app.ConfigPath == "" {
|
||||
http.Error(w, "", http.StatusGone)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
data, err := os.ReadFile(app.ConfigPath)
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
|
||||
Response(w, data, "application/yaml")
|
||||
|
||||
case "POST", "PATCH":
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "PATCH" {
|
||||
// no need to validate after merge
|
||||
data, err = mergeYAML(app.ConfigPath, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// validate config
|
||||
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
|
||||
// Read the contents of the first YAML file
|
||||
data1, err := os.ReadFile(file1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal the first YAML file into a map
|
||||
var config1 map[string]any
|
||||
if err = yaml.Unmarshal(data1, &config1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal the second YAML document into a map
|
||||
var config2 map[string]any
|
||||
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge the two maps
|
||||
config1 = merge(config1, config2)
|
||||
|
||||
// Marshal the merged map into YAML
|
||||
return yaml.Marshal(&config1)
|
||||
}
|
||||
|
||||
func merge(dst, src map[string]any) map[string]any {
|
||||
for k, v := range src {
|
||||
if vv, ok := dst[k]; ok {
|
||||
switch vv := vv.(type) {
|
||||
case map[string]any:
|
||||
v := v.(map[string]any)
|
||||
dst[k] = merge(vv, v)
|
||||
case []any:
|
||||
v := v.([]any)
|
||||
dst[k] = v
|
||||
default:
|
||||
dst[k] = v
|
||||
}
|
||||
} else {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/www"
|
||||
)
|
||||
|
||||
func initStatic(staticDir string) {
|
||||
var root http.FileSystem
|
||||
if staticDir != "" {
|
||||
log.Info().Str("dir", staticDir).Msg("[api] serve static")
|
||||
root = http.Dir(staticDir)
|
||||
} else {
|
||||
root = http.FS(www.Static)
|
||||
}
|
||||
|
||||
base := len(basePath)
|
||||
fileServer := http.FileServer(root)
|
||||
|
||||
HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
|
||||
if base > 0 {
|
||||
r.URL.Path = r.URL.Path[base:]
|
||||
}
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# WebSocket
|
||||
|
||||
Endpoint: `/api/ws`
|
||||
|
||||
Query parameters:
|
||||
|
||||
- `src` (required) - Stream name
|
||||
|
||||
### WebRTC
|
||||
|
||||
Request SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/offer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Response SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/answer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Request/response candidate:
|
||||
|
||||
- empty value also allowed and optional
|
||||
|
||||
```json
|
||||
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
|
||||
```
|
||||
|
||||
### MSE
|
||||
|
||||
Request:
|
||||
|
||||
- codecs list optional
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
|
||||
```
|
||||
|
||||
### HLS
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
|
||||
```
|
||||
|
||||
### MJPEG
|
||||
|
||||
Request/response:
|
||||
|
||||
```json
|
||||
{"type":"mjpeg"}
|
||||
```
|
||||
@@ -0,0 +1,227 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Origin string `yaml:"origin"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("api")
|
||||
|
||||
initWS(cfg.Mod.Origin)
|
||||
|
||||
api.HandleFunc("api/ws", apiWS)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value any `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) String() (value string) {
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(v any) error {
|
||||
b, err := json.Marshal(m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
|
||||
func HandleFunc(msgType string, handler WSHandler) {
|
||||
wsHandlers[msgType] = handler
|
||||
}
|
||||
|
||||
var wsHandlers = make(map[string]WSHandler)
|
||||
|
||||
func initWS(origin string) {
|
||||
wsUp = &websocket.Upgrader{
|
||||
ReadBufferSize: 4096, // for SDP
|
||||
WriteBufferSize: 512 * 1024, // 512K
|
||||
}
|
||||
|
||||
switch origin {
|
||||
case "":
|
||||
// same origin + ignore port
|
||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
o, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if o.Host == r.Host {
|
||||
return true
|
||||
}
|
||||
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||
return o.Host[:i] == r.Host
|
||||
}
|
||||
return false
|
||||
}
|
||||
case "*":
|
||||
// any origin
|
||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := wsUp.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
origin := r.Header.Get("Origin")
|
||||
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
|
||||
return
|
||||
}
|
||||
|
||||
tr := &Transport{Request: r}
|
||||
tr.OnWrite(func(msg any) error {
|
||||
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||
|
||||
if data, ok := msg.([]byte); ok {
|
||||
return ws.WriteMessage(websocket.BinaryMessage, data)
|
||||
} else {
|
||||
return ws.WriteJSON(msg)
|
||||
}
|
||||
})
|
||||
|
||||
for {
|
||||
msg := new(Message)
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
}
|
||||
_ = ws.Close()
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
errMsg := creds.SecretString(err.Error())
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
tr.Close()
|
||||
}
|
||||
|
||||
var wsUp *websocket.Upgrader
|
||||
|
||||
type Transport struct {
|
||||
Request *http.Request
|
||||
|
||||
ctx map[any]any
|
||||
|
||||
closed bool
|
||||
mx sync.Mutex
|
||||
wrmx sync.Mutex
|
||||
|
||||
onChange func()
|
||||
onWrite func(msg any) error
|
||||
onClose []func()
|
||||
}
|
||||
|
||||
func (t *Transport) OnWrite(f func(msg any) error) {
|
||||
t.mx.Lock()
|
||||
if t.onChange != nil {
|
||||
t.onChange()
|
||||
}
|
||||
t.onWrite = f
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) Write(msg any) {
|
||||
t.wrmx.Lock()
|
||||
_ = t.onWrite(msg)
|
||||
t.wrmx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) Close() {
|
||||
t.mx.Lock()
|
||||
for _, f := range t.onClose {
|
||||
f()
|
||||
}
|
||||
t.closed = true
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) OnChange(f func()) {
|
||||
t.mx.Lock()
|
||||
t.onChange = f
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) OnClose(f func()) {
|
||||
t.mx.Lock()
|
||||
if t.closed {
|
||||
f()
|
||||
} else {
|
||||
t.onClose = append(t.onClose, f)
|
||||
}
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
// WithContext - run function with Context variable
|
||||
func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
||||
t.mx.Lock()
|
||||
if t.ctx == nil {
|
||||
t.ctx = map[any]any{}
|
||||
}
|
||||
f(t.ctx)
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) Writer() io.Writer {
|
||||
return &writer{t: t}
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
t *Transport
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
w.t.wrmx.Lock()
|
||||
if err = w.t.onWrite(p); err == nil {
|
||||
n = len(p)
|
||||
}
|
||||
w.t.wrmx.Unlock()
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user