Files
core/client/client.go
2023-05-12 12:59:01 +02:00

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
}