mirror of
https://github.com/kerberos-io/agent.git
synced 2026-10-19 17:38:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949762c91b | ||
|
|
b805b84d0a | ||
|
|
a57b6310b2 | ||
|
|
2ec6faffe1 | ||
|
|
d40c544584 |
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
8080
|
||||
8082
|
||||
],
|
||||
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download && bash ./verify-moq-devcontainer.sh"
|
||||
}
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@
|
||||
"-action",
|
||||
"run",
|
||||
"-port",
|
||||
"8080"
|
||||
"8082"
|
||||
],
|
||||
"envFile": "${workspaceFolder}/machinery/.env.local",
|
||||
"buildFlags": "--tags dynamic,moq",
|
||||
|
||||
@@ -602,7 +602,10 @@ The `web` is a **React** project which is the main entry point for an end user t
|
||||
cd ui
|
||||
yarn start
|
||||
|
||||
This will start a webserver and launches the web app on port `3000`.
|
||||
This starts the web app on port `3000`. In development, the UI connects to the
|
||||
Agent API on port `8082`. Override that port when needed:
|
||||
|
||||
REACT_APP_AGENT_API_PORT=9090 yarn start
|
||||
|
||||

|
||||
|
||||
@@ -618,9 +621,14 @@ You can simply run the `machinery` using following commands.
|
||||
|
||||
git clone https://github.com/kerberos-io/agent
|
||||
cd machinery
|
||||
go run main.go -action run -port 80
|
||||
go run main.go -action run -port 8082
|
||||
|
||||
This will launch the Kerberos Agent and run a webserver on port `80`. You can change the port by your own preference. We strongly support the usage of [Goland](https://www.jetbrains.com/go/) or [Visual Studio Code](https://code.visualstudio.com/), as it comes with all the debugging and linting features built in.
|
||||
This will launch the Kerberos Agent and run a webserver on port `8082`, matching
|
||||
the UI development default. You can change the port together with
|
||||
`REACT_APP_AGENT_API_PORT`. We strongly support the usage of
|
||||
[Goland](https://www.jetbrains.com/go/) or
|
||||
[Visual Studio Code](https://code.visualstudio.com/), as it comes with all the
|
||||
debugging and linting features built in.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1454,13 +1454,19 @@ func HandleRealtimeProcessing(processingCursor *packets.QueueCursor, configurati
|
||||
// @Summary Will verify the hub connectivity.
|
||||
// @Description Will verify the hub connectivity.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func VerifyHub(c *gin.Context) {
|
||||
func VerifyHub(c *gin.Context, configuredHubURI string) {
|
||||
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
|
||||
if err == nil {
|
||||
hubURI := config.HubURI
|
||||
hubURI, err := configuredHubVerificationURI(config.HubURI, configuredHubURI)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIResponse{
|
||||
Data: "cloud.VerifyHub(): " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
publicKey := config.HubKey
|
||||
privateKey := config.HubPrivateKey
|
||||
|
||||
@@ -1473,9 +1479,9 @@ func VerifyHub(c *gin.Context) {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
client = &http.Client{Transport: tr, CheckRedirect: rejectHubVerificationRedirect}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
client = &http.Client{CheckRedirect: rejectHubVerificationRedirect}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
@@ -1524,7 +1530,7 @@ func VerifyHub(c *gin.Context) {
|
||||
// @Summary Will verify the persistence.
|
||||
// @Description Will verify the persistence.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
func VerifyPersistence(c *gin.Context, configDirectory, configuredHubURI string) {
|
||||
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
@@ -1537,6 +1543,7 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
if config.HubURI == "" ||
|
||||
config.HubKey == "" ||
|
||||
config.HubPrivateKey == "" ||
|
||||
config.S3 == nil ||
|
||||
config.S3.Region == "" {
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): Kerberos Hub not properly configured."
|
||||
log.Error(msg)
|
||||
@@ -1544,25 +1551,24 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
Data: msg,
|
||||
})
|
||||
} else {
|
||||
|
||||
// Open test-480p.mp4
|
||||
file, err := os.Open(configDirectory + "/data/test-480p.mp4")
|
||||
hubURI, err := configuredHubVerificationURI(config.HubURI, configuredHubURI)
|
||||
if err != nil {
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): error reading test-480p.mp4: " + err.Error()
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): " + err.Error()
|
||||
log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
c.JSON(http.StatusBadRequest, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", config.HubURI+"/storage/upload", file)
|
||||
req, err := http.NewRequest(http.MethodHead, hubURI+"/storage/upload", nil)
|
||||
if err != nil {
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): error reading Kerberos Hub HEAD request, " + config.HubURI + "/storage: " + err.Error()
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): error creating Kerberos Hub upload authorization request: " + err.Error()
|
||||
log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
@@ -1580,9 +1586,9 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
client = &http.Client{Transport: tr, CheckRedirect: rejectHubVerificationRedirect}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
client = &http.Client{CheckRedirect: rejectHubVerificationRedirect}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
@@ -1598,14 +1604,17 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
Data: msg,
|
||||
})
|
||||
} else {
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): upload not allowed using the configured credentials"
|
||||
msg := hubPersistenceVerificationError(resp.StatusCode)
|
||||
log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): Error creating Kerberos Hub request"
|
||||
msg := "cloud.VerifyPersistence(kerberoshub): Kerberos Hub upload authorization request failed"
|
||||
if err != nil {
|
||||
msg += ": " + err.Error()
|
||||
}
|
||||
log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
@@ -1774,6 +1783,35 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
}
|
||||
}
|
||||
|
||||
func configuredHubVerificationURI(requested, configured string) (string, error) {
|
||||
requested = strings.TrimRight(strings.TrimSpace(requested), "/")
|
||||
configured = strings.TrimRight(strings.TrimSpace(configured), "/")
|
||||
if configured == "" {
|
||||
return "", errors.New("save the Hub URI before verifying it")
|
||||
}
|
||||
if requested != configured {
|
||||
return "", errors.New("the Hub URI does not match the saved configuration; save it before verifying")
|
||||
}
|
||||
return configured, nil
|
||||
}
|
||||
|
||||
func rejectHubVerificationRedirect(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
func hubPersistenceVerificationError(statusCode int) string {
|
||||
reason := "Kerberos Hub rejected upload authorization"
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
reason += ": the credentials or device/project assignment are invalid, or the storage limit was reached"
|
||||
case http.StatusUnauthorized:
|
||||
reason += ": the account has no active subscription or the subscription expired"
|
||||
case http.StatusForbidden:
|
||||
reason += ": the device is muted"
|
||||
}
|
||||
return "cloud.VerifyPersistence(kerberoshub): " + reason + " (HTTP " + strconv.Itoa(statusCode) + ")"
|
||||
}
|
||||
|
||||
// VerifySecondaryPersistence godoc
|
||||
// @Router /api/persistence/secondary/verify [post]
|
||||
// @ID verify-secondary-persistence
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
agentonvif "github.com/kerberos-io/agent/machinery/src/onvif"
|
||||
goonvif "github.com/kerberos-io/onvif"
|
||||
@@ -18,6 +21,202 @@ import (
|
||||
goonvifxsd "github.com/kerberos-io/onvif/xsd/onvif"
|
||||
)
|
||||
|
||||
func TestVerifyPersistenceKerberosHubUsesAuthorizationHead(t *testing.T) {
|
||||
var requestMethod string
|
||||
var requestBody []byte
|
||||
var requestHeaders http.Header
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestMethod = r.Method
|
||||
requestHeaders = r.Header.Clone()
|
||||
requestBody, _ = io.ReadAll(r.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := models.Config{
|
||||
Cloud: "kerberoshub",
|
||||
Key: "camera-1",
|
||||
Name: "front-door",
|
||||
HubURI: server.URL,
|
||||
HubKey: "project-public",
|
||||
HubPrivateKey: "project-private",
|
||||
S3: &models.S3{Region: "eu"},
|
||||
}
|
||||
payload, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/persistence/verify", bytes.NewReader(payload))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
VerifyPersistence(ctx, t.TempDir(), server.URL)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if requestMethod != http.MethodHead {
|
||||
t.Fatalf("Hub request method = %q, want HEAD", requestMethod)
|
||||
}
|
||||
if len(requestBody) != 0 {
|
||||
t.Fatalf("Hub request body has %d bytes, want none", len(requestBody))
|
||||
}
|
||||
for header, want := range map[string]string{
|
||||
"X-Kerberos-Storage-Device": "camera-1",
|
||||
"X-Kerberos-Hub-PublicKey": "project-public",
|
||||
"X-Kerberos-Hub-PrivateKey": "project-private",
|
||||
"X-Kerberos-Hub-Region": "eu",
|
||||
"X-Kerberos-Storage-Capture": "IPCamera",
|
||||
} {
|
||||
if got := requestHeaders.Get(header); got != want {
|
||||
t.Errorf("%s = %q, want %q", header, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPersistenceKerberosHubRejectsCrossHostRedirect(t *testing.T) {
|
||||
var redirectedHeaders http.Header
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectedHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
var originalHeaders http.Header
|
||||
hub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
originalHeaders = r.Header.Clone()
|
||||
http.Redirect(w, r, target.URL+"/storage/upload", http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer hub.Close()
|
||||
|
||||
config := models.Config{
|
||||
Cloud: "kerberoshub",
|
||||
Key: "camera-1",
|
||||
Name: "front-door",
|
||||
HubURI: hub.URL,
|
||||
HubKey: "project-public",
|
||||
HubPrivateKey: "project-private",
|
||||
S3: &models.S3{Region: "eu"},
|
||||
}
|
||||
payload, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/persistence/verify", bytes.NewReader(payload))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
VerifyPersistence(ctx, t.TempDir(), hub.URL)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body = %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
||||
}
|
||||
for _, header := range []string{"X-Kerberos-Hub-PublicKey", "X-Kerberos-Hub-PrivateKey"} {
|
||||
if originalHeaders.Get(header) == "" {
|
||||
t.Errorf("%s was not sent to the configured Hub", header)
|
||||
}
|
||||
if redirectedHeaders != nil {
|
||||
t.Errorf("redirect target received a request with %s=%q", header, redirectedHeaders.Get(header))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPersistenceKerberosHubRejectsUnsavedURI(t *testing.T) {
|
||||
requestCount := 0
|
||||
untrusted := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
requestCount++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer untrusted.Close()
|
||||
|
||||
config := models.Config{
|
||||
Cloud: "kerberoshub",
|
||||
Key: "camera-1",
|
||||
Name: "front-door",
|
||||
HubURI: untrusted.URL,
|
||||
HubKey: "project-public",
|
||||
HubPrivateKey: "project-private",
|
||||
S3: &models.S3{Region: "eu"},
|
||||
}
|
||||
payload, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/persistence/verify", bytes.NewReader(payload))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
VerifyPersistence(ctx, t.TempDir(), "https://saved-hub.example.com")
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body = %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
||||
}
|
||||
if requestCount != 0 {
|
||||
t.Fatalf("untrusted Hub received %d requests, want none", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyHubRejectsUnsavedURI(t *testing.T) {
|
||||
requestCount := 0
|
||||
untrusted := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
requestCount++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer untrusted.Close()
|
||||
|
||||
config := models.Config{
|
||||
HubURI: untrusted.URL,
|
||||
HubKey: "project-public",
|
||||
HubPrivateKey: "project-private",
|
||||
}
|
||||
payload, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/hub/verify", bytes.NewReader(payload))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
VerifyHub(ctx, "https://saved-hub.example.com")
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body = %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
||||
}
|
||||
if requestCount != 0 {
|
||||
t.Fatalf("untrusted Hub received %d requests, want none", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubPersistenceVerificationErrorExplainsStatus(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
status int
|
||||
want string
|
||||
}{
|
||||
{status: http.StatusBadRequest, want: "credentials or device/project assignment"},
|
||||
{status: http.StatusUnauthorized, want: "no active subscription"},
|
||||
{status: http.StatusForbidden, want: "device is muted"},
|
||||
{status: http.StatusBadGateway, want: "HTTP 502"},
|
||||
} {
|
||||
t.Run(http.StatusText(test.status), func(t *testing.T) {
|
||||
if got := hubPersistenceVerificationError(test.status); !strings.Contains(got, test.want) {
|
||||
t.Fatalf("hubPersistenceVerificationError(%d) = %q, want it to contain %q", test.status, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatFailureLogOmitsHubResponseBody(t *testing.T) {
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
|
||||
@@ -65,12 +65,12 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
|
||||
// Will verify the hub settings.
|
||||
api.POST("/hub/verify", func(c *gin.Context) {
|
||||
cloud.VerifyHub(c)
|
||||
cloud.VerifyHub(c, configuration.Config.HubURI)
|
||||
})
|
||||
|
||||
// Will verify the persistence settings.
|
||||
api.POST("/persistence/verify", func(c *gin.Context) {
|
||||
cloud.VerifyPersistence(c, configDirectory)
|
||||
cloud.VerifyPersistence(c, configDirectory, configuration.Config.HubURI)
|
||||
})
|
||||
|
||||
// Will verify the secondary persistence settings.
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
const { hostname, host, protocol } = window.location;
|
||||
const websocketprotocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
const devAgentPort = process.env.REACT_APP_AGENT_API_PORT || '8082';
|
||||
|
||||
// Uncomment this when using codespaces or other special DNS names (which you can't control)
|
||||
// replace this with the DNS name of the kerberos agent server (the codespace url)
|
||||
@@ -9,9 +10,9 @@ const dev = {
|
||||
ENV: 'dev',
|
||||
// Comment the below lines, when using codespaces or other special DNS names (which you can't control)
|
||||
HOSTNAME: hostname,
|
||||
API_URL: `${protocol}//${hostname}:8080/api`,
|
||||
URL: `${protocol}//${hostname}:8080`,
|
||||
WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
|
||||
API_URL: `${protocol}//${hostname}:${devAgentPort}/api`,
|
||||
URL: `${protocol}//${hostname}:${devAgentPort}`,
|
||||
WS_URL: `${websocketprotocol}//${hostname}:${devAgentPort}/ws`,
|
||||
MODE: window['env']['mode'],
|
||||
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
|
||||
// HOSTNAME: externalHost,
|
||||
|
||||
Reference in New Issue
Block a user