diff --git a/api/openapi.yaml b/api/openapi.yaml index 3193fce8..c2b48edd 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -21,6 +21,14 @@ components: error: type: string + Info: + type: object + properties: + version: + type: string + started: + type: string + AuthInternalUser: type: object properties: @@ -1134,6 +1142,25 @@ components: paths: + /v3/info: + get: + operationId: info + tags: [General] + summary: returns informations about the instance. + responses: + '200': + description: the request was successful. + content: + application/json: + schema: + $ref: '#/components/schemas/Info' + '500': + description: server error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /v3/auth/jwks/refresh: post: operationId: authJwksRefresh diff --git a/docs/2-usage/18-control-api.md b/docs/2-usage/18-control-api.md index f37b71c8..72315db1 100644 --- a/docs/2-usage/18-control-api.md +++ b/docs/2-usage/18-control-api.md @@ -1,6 +1,6 @@ # Control API -The server can be queried and controlled with an API, that can be enabled by setting the `api` parameter in the configuration: +The server can be queried and controlled with an API, that can be enabled by toggling the `api` parameter in the configuration: ```yml api: yes diff --git a/internal/api/api.go b/internal/api/api.go index 181a10be..79ef10b0 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -88,6 +88,8 @@ type apiParent interface { // API is an API server. type API struct { + Version string + Started time.Time Address string Encryption bool ServerKey string @@ -121,6 +123,8 @@ func (a *API) Initialize() error { group := router.Group("/v3") + group.GET("/info", a.onInfo) + group.POST("/auth/jwks/refresh", a.onAuthJwksRefresh) group.GET("/config/global/get", a.onConfigGlobalGet) @@ -538,6 +542,13 @@ func (a *API) onConfigPathsDelete(ctx *gin.Context) { ctx.Status(http.StatusOK) } +func (a *API) onInfo(ctx *gin.Context) { + ctx.JSON(http.StatusOK, &defs.APIInfo{ + Version: a.Version, + Started: a.Started, + }) +} + func (a *API) onAuthJwksRefresh(ctx *gin.Context) { a.AuthManager.RefreshJWTJWKS() ctx.Status(http.StatusOK) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index db10f86d..b9957abd 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -117,6 +117,34 @@ func TestPreflightRequest(t *testing.T) { require.Equal(t, byts, []byte{}) } +func TestInfo(t *testing.T) { + cnf := tempConf(t, "api: yes\n") + + api := API{ + Version: "v1.2.3", + Started: time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local), + Address: "localhost:9997", + ReadTimeout: conf.Duration(10 * time.Second), + Conf: cnf, + AuthManager: test.NilAuthManager, + Parent: &testParent{}, + } + err := api.Initialize() + require.NoError(t, err) + defer api.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/info", nil, &out) + require.Equal(t, map[string]interface{}{ + "started": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339), + "version": "v1.2.3", + }, out) +} + func TestConfigGlobalGet(t *testing.T) { cnf := tempConf(t, "api: yes\n") checked := false @@ -621,10 +649,10 @@ func TestRecordingsList(t *testing.T) { "name": "mypath1", "segments": []interface{}{ map[string]interface{}{ - "start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), }, map[string]interface{}{ - "start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), }, }, }, @@ -632,7 +660,7 @@ func TestRecordingsList(t *testing.T) { "name": "mypath2", "segments": []interface{}{ map[string]interface{}{ - "start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), }, }, }, @@ -680,10 +708,10 @@ func TestRecordingsGet(t *testing.T) { "name": "mypath1", "segments": []interface{}{ map[string]interface{}{ - "start": time.Date(2008, 11, 0o7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano), }, map[string]interface{}{ - "start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano), }, }, }, out) @@ -725,7 +753,7 @@ func TestRecordingsDeleteSegment(t *testing.T) { v := url.Values{} v.Set("path", "mypath1") - v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano)) u.RawQuery = v.Encode() req, err := http.NewRequest(http.MethodDelete, u.String(), nil) diff --git a/internal/core/core.go b/internal/core/core.go index 9c46c67e..6fd09e0e 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -41,6 +41,8 @@ import ( //go:embed VERSION var version []byte +var started = time.Now() + var defaultConfPaths = []string{ "rtsp-simple-server.yml", "mediamtx.yml", @@ -615,6 +617,8 @@ func (p *Core) createResources(initial bool) error { if p.conf.API && p.api == nil { i := &api.API{ + Version: string(version), + Started: started, Address: p.conf.APIAddress, Encryption: p.conf.APIEncryption, ServerKey: p.conf.APIServerKey, diff --git a/internal/defs/api.go b/internal/defs/api.go index 11abf500..c0aa11e9 100644 --- a/internal/defs/api.go +++ b/internal/defs/api.go @@ -55,6 +55,12 @@ type APIError struct { Error string `json:"error"` } +// APIInfo is a info response. +type APIInfo struct { + Version string `json:"version"` + Started time.Time `json:"started"` +} + // APIPathConfList is a list of path configurations. type APIPathConfList struct { ItemCount int `json:"itemCount"` diff --git a/internal/playback/on_get_test.go b/internal/playback/on_get_test.go index d4384248..d3cd4ee8 100644 --- a/internal/playback/on_get_test.go +++ b/internal/playback/on_get_test.go @@ -264,7 +264,7 @@ func TestOnGet(t *testing.T) { v := url.Values{} v.Set("path", "mypath") - v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) v.Set("duration", "3") v.Set("format", format) u.RawQuery = v.Encode() @@ -488,7 +488,7 @@ func TestOnGetDifferentInit(t *testing.T) { v := url.Values{} v.Set("path", "mypath") - v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) v.Set("duration", "2") v.Set("format", "fmp4") u.RawQuery = v.Encode() @@ -566,7 +566,7 @@ func TestOnGetNTPCompensation(t *testing.T) { v := url.Values{} v.Set("path", "mypath") - v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano)) v.Set("duration", "3") v.Set("format", "fmp4") u.RawQuery = v.Encode() diff --git a/internal/playback/on_list_test.go b/internal/playback/on_list_test.go index 22756dc5..10f9430e 100644 --- a/internal/playback/on_list_test.go +++ b/internal/playback/on_list_test.go @@ -91,18 +91,18 @@ func TestOnList(t *testing.T) { switch ca { case "filtered": - v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)) - v.Set("end", time.Date(2009, 11, 0o7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("end", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano)) case "filtered and gap": - v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) - v.Set("end", time.Date(2009, 11, 0o7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2008, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("end", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano)) case "start after duration": - v.Set("start", time.Date(2010, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2010, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) case "start before first": - v.Set("start", time.Date(2007, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) + v.Set("start", time.Date(2007, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano)) } u.RawQuery = v.Encode() @@ -130,15 +130,15 @@ func TestOnList(t *testing.T) { require.Equal(t, []interface{}{ map[string]interface{}{ "duration": float64(66), - "start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=66&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), }, map[string]interface{}{ "duration": float64(4), - "start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=4&path=mypath&start=" + - url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), }, }, out) @@ -146,15 +146,15 @@ func TestOnList(t *testing.T) { require.Equal(t, []interface{}{ map[string]interface{}{ "duration": float64(65), - "start": time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=65&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)), }, map[string]interface{}{ "duration": float64(2), - "start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=2&path=mypath&start=" + - url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), }, }, out) @@ -162,9 +162,9 @@ func TestOnList(t *testing.T) { require.Equal(t, []interface{}{ map[string]interface{}{ "duration": float64(4), - "start": time.Date(2008, 11, 0o7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=4&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano)), }, }, out) @@ -172,15 +172,15 @@ func TestOnList(t *testing.T) { require.Equal(t, []interface{}{ map[string]interface{}{ "duration": float64(62), - "start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=62&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), }, map[string]interface{}{ "duration": float64(1), - "start": time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=1&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)), }, }, out) } @@ -327,9 +327,9 @@ func TestOnListCachedDuration(t *testing.T) { require.Equal(t, []interface{}{ map[string]interface{}{ "duration": float64(50), - "start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), + "start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), "url": "http://localhost:9996/get?duration=50&path=mypath&start=" + - url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), + url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), }, }, out) } diff --git a/internal/recordstore/path_test.go b/internal/recordstore/path_test.go index 6a77504c..98d7532e 100644 --- a/internal/recordstore/path_test.go +++ b/internal/recordstore/path_test.go @@ -17,7 +17,7 @@ var pathCases = []struct { "standard", "%path/%Y-%m-%d_%H-%M-%S-%f.mp4", Path{ - Start: time.Date(2008, 11, 0o7, 11, 22, 4, 123456000, time.Local), + Start: time.Date(2008, 11, 7, 11, 22, 4, 123456000, time.Local), Path: "mypath", }, "mypath/2008-11-07_11-22-04-123456.mp4", diff --git a/internal/testapidocs/apidocs_test.go b/internal/testapidocs/apidocs_test.go index 2831a7b4..5b1137e2 100644 --- a/internal/testapidocs/apidocs_test.go +++ b/internal/testapidocs/apidocs_test.go @@ -44,6 +44,10 @@ func TestAPIDocs(t *testing.T) { openAPIKey string goStruct any }{ + { + "Info", + defs.APIInfo{}, + }, { "AuthInternalUser", conf.AuthInternalUser{},