mirror of
https://github.com/datarhei/core.git
synced 2025-10-04 15:42:57 +08:00
433 lines
9.9 KiB
Go
433 lines
9.9 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/datarhei/core/v16/app"
|
|
"github.com/datarhei/core/v16/http/api"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
)
|
|
|
|
var coreapp = app.Name
|
|
var coreversion = "~" + app.Version.MinorString()
|
|
|
|
type HTTPClient interface {
|
|
Do(req *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
type RestClient interface {
|
|
// String returns a string representation of the connection
|
|
String() string
|
|
|
|
// ID returns the ID of the connected datarhei Core
|
|
ID() string
|
|
|
|
// Address returns the address of the connected datarhei Core
|
|
Address() string
|
|
|
|
About() api.About // GET /
|
|
Ping() (bool, time.Duration) // GET /ping
|
|
|
|
Config() (api.Config, error) // GET /config
|
|
ConfigSet(config api.ConfigData) error // POST /config
|
|
ConfigReload() error // GET /config/reload
|
|
|
|
DiskFSList(sort, order string) ([]api.FileInfo, error) // GET /fs/disk
|
|
DiskFSHasFile(path string) bool // HEAD /fs/disk/{path}
|
|
DiskFSGetFile(path string) (io.ReadCloser, error) // GET /fs/disk/{path}
|
|
DiskFSDeleteFile(path string) error // DELETE /fs/disk/{path}
|
|
DiskFSAddFile(path string, data io.Reader) error // PUT /fs/disk/{path}
|
|
|
|
MemFSList(sort, order string) ([]api.FileInfo, error) // GET /fs/mem
|
|
MemFSHasFile(path string) bool // HEAD /fs/mem/{path}
|
|
MemFSGetFile(path string) (io.ReadCloser, error) // GET /fs/mem/{path}
|
|
MemFSDeleteFile(path string) error // DELETE /fs/mem/{path}
|
|
MemFSAddFile(path string, data io.Reader) error // PUT /fs/mem/{path}
|
|
|
|
Log() ([]api.LogEvent, error) // GET /log
|
|
|
|
Metadata(id, key string) (api.Metadata, error) // GET /metadata/{key}
|
|
MetadataSet(id, key string, metadata api.Metadata) error // PUT /metadata/{key}
|
|
|
|
Metrics(api.MetricsQuery) (api.MetricsResponse, error) // POST /metrics
|
|
|
|
ProcessList(id, filter []string) ([]api.Process, error) // GET /process
|
|
Process(id string, filter []string) (api.Process, error) // GET /process/{id}
|
|
ProcessAdd(p api.ProcessConfig) error // POST /process
|
|
ProcessDelete(id string) error // DELETE /process/{id}
|
|
ProcessCommand(id, command string) error // PUT /process/{id}/command
|
|
ProcessProbe(id string) (api.Probe, error) // GET /process/{id}/probe
|
|
ProcessConfig(id string) (api.ProcessConfig, error) // GET /process/{id}/config
|
|
ProcessReport(id string) (api.ProcessReport, error) // GET /process/{id}/report
|
|
ProcessState(id string) (api.ProcessState, error) // GET /process/{id}/state
|
|
ProcessMetadata(id, key string) (api.Metadata, error) // GET /process/{id}/metadata/{key}
|
|
ProcessMetadataSet(id, key string, metadata api.Metadata) error // PUT /process/{id}/metadata/{key}
|
|
|
|
RTMPChannels() ([]api.RTMPChannel, error) // GET /rtmp
|
|
SRTChannels() ([]api.SRTChannel, error) // GET /srt
|
|
|
|
Sessions(collectors []string) (api.SessionsSummary, error) // GET /session
|
|
SessionsActive(collectors []string) (api.SessionsActive, error) // GET /session/active
|
|
|
|
Skills() (api.Skills, error) // GET /skills
|
|
SkillsReload() error // GET /skills/reload
|
|
}
|
|
|
|
// Config is the configuration for a new REST API client.
|
|
type Config struct {
|
|
// Address is the address of the datarhei Core to connect to.
|
|
Address string
|
|
|
|
// Username and Password are credentials to authorize access to the API.
|
|
Username string
|
|
Password string
|
|
|
|
// Auth0Token is a valid Auth0 token to authorize access to the API.
|
|
Auth0Token string
|
|
|
|
// Client is a HTTPClient that will be used for the API calls. Optional.
|
|
Client HTTPClient
|
|
}
|
|
|
|
// restclient implements the RestClient interface.
|
|
type restclient struct {
|
|
address string
|
|
prefix string
|
|
accessToken string
|
|
refreshToken string
|
|
username string
|
|
password string
|
|
auth0Token string
|
|
client HTTPClient
|
|
about api.About
|
|
}
|
|
|
|
// New returns a new REST API client for the given config. The error is non-nil
|
|
// in case of an error.
|
|
func New(config Config) (RestClient, error) {
|
|
r := &restclient{
|
|
address: config.Address,
|
|
prefix: "/api",
|
|
username: config.Username,
|
|
password: config.Password,
|
|
auth0Token: config.Auth0Token,
|
|
client: config.Client,
|
|
}
|
|
|
|
u, err := url.Parse(r.address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
username := u.User.Username()
|
|
if len(username) != 0 {
|
|
r.username = username
|
|
}
|
|
|
|
if password, ok := u.User.Password(); ok {
|
|
r.password = password
|
|
}
|
|
|
|
u.User = nil
|
|
u.RawQuery = ""
|
|
u.Fragment = ""
|
|
|
|
r.address = u.String()
|
|
|
|
if r.client == nil {
|
|
r.client = &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
}
|
|
}
|
|
|
|
about, err := r.info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.about = about
|
|
|
|
if r.about.App != coreapp {
|
|
return nil, fmt.Errorf("didn't receive the expected API response (got: %s, want: %s)", r.about.Name, coreapp)
|
|
}
|
|
|
|
if len(r.about.ID) == 0 {
|
|
if err := r.login(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
c, _ := semver.NewConstraint(coreversion)
|
|
v, err := semver.NewVersion(r.about.Version.Number)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !c.Check(v) {
|
|
return nil, fmt.Errorf("the core version (%s) is not supported (%s)", r.about.Version.Number, coreversion)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (r restclient) String() string {
|
|
return fmt.Sprintf("%s %s (%s) %s @ %s", r.about.Name, r.about.Version.Number, r.about.Version.Arch, r.about.ID, r.address)
|
|
}
|
|
|
|
func (r *restclient) ID() string {
|
|
return r.about.ID
|
|
}
|
|
|
|
func (r *restclient) Address() string {
|
|
return r.address
|
|
}
|
|
|
|
func (r *restclient) About() api.About {
|
|
return r.about
|
|
}
|
|
|
|
func (r *restclient) Ping() (bool, time.Duration) {
|
|
req, err := http.NewRequest(http.MethodGet, r.address+"/ping", nil)
|
|
if err != nil {
|
|
return false, time.Duration(0)
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
status, body, err := r.request(req)
|
|
if err != nil {
|
|
return false, time.Since(start)
|
|
}
|
|
|
|
defer body.Close()
|
|
|
|
if status != 200 {
|
|
return false, time.Since(start)
|
|
}
|
|
|
|
return true, time.Since(start)
|
|
}
|
|
|
|
func (r *restclient) login() error {
|
|
login := api.Login{}
|
|
|
|
hasLocalJWT := false
|
|
useLocalJWT := false
|
|
hasAuth0 := false
|
|
useAuth0 := false
|
|
|
|
for _, auths := range r.about.Auths {
|
|
if auths == "localjwt" {
|
|
hasLocalJWT = true
|
|
break
|
|
} else if strings.HasPrefix(auths, "auth0 ") {
|
|
hasAuth0 = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasLocalJWT && !hasAuth0 {
|
|
return fmt.Errorf("the API doesn't provide any supported auth method")
|
|
}
|
|
|
|
if len(r.auth0Token) != 0 && hasAuth0 {
|
|
useAuth0 = true
|
|
}
|
|
|
|
if !useAuth0 {
|
|
if (len(r.username) != 0 || len(r.password) != 0) && hasLocalJWT {
|
|
useLocalJWT = true
|
|
}
|
|
}
|
|
|
|
if !useAuth0 && !useLocalJWT {
|
|
return fmt.Errorf("none of the provided auth credentials can be used")
|
|
}
|
|
|
|
if useLocalJWT {
|
|
login.Username = r.username
|
|
login.Password = r.password
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
e := json.NewEncoder(&buf)
|
|
e.Encode(login)
|
|
|
|
req, err := http.NewRequest("POST", r.address+r.prefix+"/login", &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
if useAuth0 {
|
|
req.Header.Add("Authorization", "Bearer "+r.auth0Token)
|
|
}
|
|
|
|
status, body, err := r.request(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if status != 200 {
|
|
return fmt.Errorf("wrong username and/or password")
|
|
}
|
|
|
|
data, _ := io.ReadAll(body)
|
|
|
|
jwt := api.JWT{}
|
|
|
|
json.Unmarshal(data, &jwt)
|
|
|
|
r.accessToken = jwt.AccessToken
|
|
r.refreshToken = jwt.RefreshToken
|
|
|
|
about, err := r.info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(about.ID) == 0 {
|
|
return fmt.Errorf("login to the API failed")
|
|
}
|
|
|
|
r.about = about
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *restclient) refresh() error {
|
|
req, err := http.NewRequest("GET", r.address+r.prefix+"/login/refresh", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Add("Authorization", "Bearer "+r.refreshToken)
|
|
|
|
status, body, err := r.request(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if status != 200 {
|
|
return fmt.Errorf("invalid refresh token")
|
|
}
|
|
|
|
data, _ := io.ReadAll(body)
|
|
|
|
jwt := api.JWTRefresh{}
|
|
|
|
json.Unmarshal(data, &jwt)
|
|
|
|
r.accessToken = jwt.AccessToken
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *restclient) info() (api.About, error) {
|
|
req, err := http.NewRequest("GET", r.address+r.prefix, nil)
|
|
if err != nil {
|
|
return api.About{}, err
|
|
}
|
|
|
|
if len(r.accessToken) != 0 {
|
|
req.Header.Add("Authorization", "Bearer "+r.accessToken)
|
|
}
|
|
|
|
status, body, err := r.request(req)
|
|
if err != nil {
|
|
return api.About{}, err
|
|
}
|
|
|
|
if status != 200 {
|
|
return api.About{}, fmt.Errorf("access to API failed (%d)", status)
|
|
}
|
|
|
|
data, _ := io.ReadAll(body)
|
|
|
|
about := api.About{}
|
|
|
|
json.Unmarshal(data, &about)
|
|
|
|
return about, nil
|
|
}
|
|
|
|
func (r *restclient) stream(method, path, contentType string, data io.Reader) (io.ReadCloser, error) {
|
|
req, err := http.NewRequest(method, r.address+r.prefix+"/v3"+path, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if method == "POST" || method == "PUT" {
|
|
req.Header.Add("Content-Type", contentType)
|
|
}
|
|
|
|
if len(r.accessToken) != 0 {
|
|
req.Header.Add("Authorization", "Bearer "+r.accessToken)
|
|
}
|
|
|
|
status, body, err := r.request(req)
|
|
if status == http.StatusUnauthorized {
|
|
if err := r.refresh(); err != nil {
|
|
if err := r.login(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+r.accessToken)
|
|
status, body, err = r.request(req)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if status < 200 || status >= 300 {
|
|
e := api.Error{}
|
|
|
|
defer body.Close()
|
|
|
|
x, _ := io.ReadAll(body)
|
|
|
|
json.Unmarshal(x, &e)
|
|
|
|
return nil, fmt.Errorf("%w", e)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func (r *restclient) call(method, path, contentType string, data io.Reader) ([]byte, error) {
|
|
body, err := r.stream(method, path, contentType, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer body.Close()
|
|
|
|
x, _ := io.ReadAll(body)
|
|
|
|
return x, nil
|
|
}
|
|
|
|
func (r *restclient) request(req *http.Request) (int, io.ReadCloser, error) {
|
|
resp, err := r.client.Do(req)
|
|
if err != nil {
|
|
return -1, nil, err
|
|
}
|
|
|
|
return resp.StatusCode, resp.Body, nil
|
|
}
|