Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/alsa/alsa_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strconv"
"strings"

"github.com/AlexxIT/go2rtc/internal/api"
api "github.com/AlexxIT/go2rtc/internal/api/server"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/alsa"
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
Expand Down
258 changes: 13 additions & 245 deletions internal/api/api.go
Original file line number Diff line number Diff line change
@@ -1,246 +1,39 @@
package api

import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"

"github.com/AlexxIT/go2rtc/internal/api/server"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"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
server.Init()
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)
}
server.HandleFunc("api", apiHandler)
server.HandleFunc("api/config", configHandler)
server.HandleFunc("api/exit", exitHandler)
server.HandleFunc("api/restart", restartHandler)
server.HandleFunc("api/log", logHandler)
}

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)
server.ResponseJSON(w, app.Info)
}

func exitHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -258,6 +51,9 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
return
}

// Stop all streams before exiting.
streams.StopAll()

os.Exit(code)
}

Expand Down Expand Up @@ -286,36 +82,8 @@ func logHandler(w http.ResponseWriter, r *http.Request) {
_, _ = app.MemoryLog.WriteTo(w)
case "DELETE":
app.MemoryLog.Reset()
Response(w, "OK", "text/plain")
server.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)
}
3 changes: 2 additions & 1 deletion internal/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"os"

"github.com/AlexxIT/go2rtc/internal/api/server"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
)
Expand All @@ -23,7 +24,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
return
}
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
Response(w, data, "application/yaml")
server.Response(w, data, "application/yaml")

case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
Expand Down
21 changes: 21 additions & 0 deletions internal/api/server/handle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package server

import (
"net/http"
"slices"
)

// 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)
}
Loading