Compare commits

..

51 Commits

Author SHA1 Message Date
Cedric Verstraeten
3f58f26dfd decrypt recordings through the UI automatically using the existing AES key, you can still use the decrypt action or openssl afterwards 2023-10-23 14:38:29 +02:00
Cedric Verstraeten
a8d5f56f1e hotfix - build error encryption key value 2023-10-23 11:07:54 +02:00
Cédric Verstraeten
1eb62d80c7 add encryption + end-to-end encryption to feature list 2023-10-23 10:59:13 +02:00
Cedric Verstraeten
e474a62dbc Add hindi #119 + allow recordings encryption + decryption tooling. 2023-10-23 10:56:36 +02:00
Cédric Verstraeten
f29b952001 Merge pull request #119 from fadkeabhi/feat#47-add-hindi-language-support
Added transaltions for hindi language
2023-10-22 22:15:07 +02:00
Cedric Verstraeten
38247ac9f6 Add italian to language selector #115 2023-10-22 20:00:10 +02:00
Cédric Verstraeten
580f17028a Merge pull request #115 from LeoSpyke/master
i18n: adds Italian locale
2023-10-22 19:56:55 +02:00
Cedric Verstraeten
48d933a561 backwards compatible when no encryption key was added in previous config 2023-10-20 14:35:09 +02:00
Cedric Verstraeten
0c70ab6158 Refactor MQTT endpoints + Introduce End-to-End encryption using RSA and AES keys + finetune PTZ 2023-10-20 13:31:02 +02:00
ABHISHEK FADAKE
839185dac8 Added transaltions for hindi language 2023-10-03 19:24:47 +05:30
LeoSpyke
ba6cdef9d5 i18n(it): translate persistence and bugfix 2023-09-15 08:17:12 +00:00
LeoSpyke
bedb3c0d7f Merge branch 'kerberos-io:master' into master 2023-09-14 12:47:46 +02:00
Leonardo Papini
2539255940 i18n: Italian translations 2023-09-14 12:47:28 +02:00
Cedric Verstraeten
24136f8b15 we didn't reset the main configuration, causing some config vars still to be set 2023-09-14 10:47:18 +02:00
Cedric Verstraeten
910bb3c079 merging timetable was giving issues 2023-09-14 10:13:50 +02:00
Cedric Verstraeten
47f4c19617 Update Config.go 2023-09-13 08:14:25 +02:00
Cedric Verstraeten
280a81809a Update Config.go 2023-09-12 22:38:26 +02:00
Cedric Verstraeten
59358acb30 add logging + empty friendly name 2023-09-12 15:17:56 +02:00
Cedric Verstraeten
ebd655ac73 Allow remote configuration through MQTT + restructure config method 2023-09-12 10:50:36 +02:00
Cedric Verstraeten
6325e37aae empty presets caused hub connection failing 2023-09-07 08:16:46 +02:00
Cedric Verstraeten
ecabc47847 integrate ondevice configurated presets 2023-08-30 14:12:07 +02:00
Cedric Verstraeten
31cc3d8939 Rely on continuous move will fix the PTZFunctions later 2023-08-29 14:53:48 +02:00
Cedric Verstraeten
c71cb71d08 We should reenable debugging, modifying to Info for now 2023-08-29 14:43:14 +02:00
Cedric Verstraeten
65a739ea75 logging PTZ functions 2023-08-29 14:30:25 +02:00
Cedric Verstraeten
410a62e9ef Some cameras do not support AbsoluteMovement, therefore we'll simulate it with ContinuousMove and a polling mechanism 2023-08-28 09:30:08 +02:00
Cedric Verstraeten
aa76dd1ec8 enable PTZ preset + introduce new MQTT messaging between Hub and Agent (introduction e2e encryption) 2023-08-25 09:05:53 +02:00
Cedric Verstraeten
384448d123 panic when no mongodb + remove files when no longer available + do not cleanup recordings by default, however cleanup when recordings have been uploaded 2023-07-31 08:49:34 +02:00
Cedric Verstraeten
414f74758c remove curly brackets 2023-07-26 19:22:19 +02:00
Cedric Verstraeten
25403ccdab dont restart if previously was not set! https://github.com/kerberos-io/agent/issues/110 2023-07-12 17:48:43 +02:00
Cedric Verstraeten
4c03132b83 Fail agent when no mongodb can be reached in Kerberos Factory deployment 2023-07-12 09:58:31 +02:00
Cedric Verstraeten
470f8f1cb6 some deployments might miss the variable, such as Kerberos Factory, we'll default these values to "true" 2023-07-11 21:57:07 +02:00
Cedric Verstraeten
5308376a67 add terraform deployment example 2023-07-04 21:04:16 +02:00
Cedric Verstraeten
2b112d29cf further detail snap deployment 2023-07-01 11:52:13 +02:00
Cedric Verstraeten
20d2517e74 add snapcraft 2023-07-01 07:42:12 +02:00
Cedric Verstraeten
12902e2482 disable snapcraft for the time being 2023-06-29 23:15:34 +02:00
Cedric Verstraeten
baca44beef Update docker.yml 2023-06-29 21:07:12 +02:00
Cedric Verstraeten
d7580744e2 Update docker.yml 2023-06-29 20:52:58 +02:00
Cedric Verstraeten
04f4bc9bf2 Update docker.yml 2023-06-29 20:47:56 +02:00
Cedric Verstraeten
d879174f4c add user to lxd group for snapcraft build 2023-06-29 20:40:28 +02:00
Cedric Verstraeten
5a1a62a723 Update docker.yml 2023-06-29 20:31:18 +02:00
Cedric Verstraeten
c519b01092 add snapcraft and try to snap the build 2023-06-29 20:23:11 +02:00
Cedric Verstraeten
c2ff7ff785 add missing custom config directory references 2023-06-29 20:10:16 +02:00
Cedric Verstraeten
44ec8c0534 try upgrading the dockerfile 2023-06-29 14:06:41 +02:00
Cedric Verstraeten
21c0e01137 add additional environment variables to tweak the internal agent "disable motion, disable liveview" 2023-06-29 12:28:44 +02:00
Cedric Verstraeten
f7ced6056d update to port 80 + allow frontend to take into account a custom config directory 2023-06-28 20:24:41 +02:00
Cedric Verstraeten
00917e3f88 add flag arguments instead of absolute arguments (we now support names)
added option to define the config location, can be different than the relative location of the agent binary
2023-06-28 19:28:07 +02:00
Cedric Verstraeten
bcfed04a07 add AGENT_TLS_INSECURE to enable Insecure TLS mode 2023-06-28 17:09:29 +02:00
Cedric Verstraeten
bf97bd72f1 add osusergo 2023-06-28 08:41:51 +02:00
Cedric Verstraeten
4b8b6bf66a fix balena links 2023-06-26 08:22:12 +02:00
Cedric Verstraeten
4b6c25bb85 documentation for balena apps 2023-06-25 23:25:42 +02:00
Cedric Verstraeten
729b38999e add deploy with balena 2023-06-25 22:14:23 +02:00
57 changed files with 2881 additions and 351 deletions

View File

@@ -43,6 +43,7 @@ jobs:
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{steps.short-sha.outputs.sha}} --output type=tar,dest=output-${{matrix.architecture}}.tar .
- name: Strip binary
run: mkdir -p output/ && tar -xf output-${{matrix.architecture}}.tar -C output && rm output-${{matrix.architecture}}.tar && cd output/ && tar -cf ../agent-${{matrix.architecture}}.tar -C home/agent . && rm -rf output
# We'll make a GitHub release and push the build (tar) as an artifact
- uses: rickstaa/action-create-tag@v1
with:
tag: ${{ steps.short-sha.outputs.sha }}
@@ -54,6 +55,17 @@ jobs:
name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }}
artifacts: "agent-${{matrix.architecture}}.tar"
# Taken from GoReleaser's own release workflow.
# The available Snapcraft Action has some bugs described in the issue below.
# The mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715.
#- name: Setup Snapcraft
# run: |
# sudo apt-get update
# sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
# mkdir -p $HOME/.cache/snapcraft/download
# mkdir -p $HOME/.cache/snapcraft/stage-packages
#- name: Use Snapcraft
# run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
build-other:
runs-on: ubuntu-latest
permissions:

View File

@@ -10,7 +10,7 @@ ENV GOSUMDB=off
##########################################
# Installing some additional dependencies.
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get upgrade -y && apt-get update && apt-get install -y --no-install-recommends \
git build-essential cmake pkg-config unzip libgtk2.0-dev \
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
rm -rf /var/lib/apt/lists/*
@@ -32,7 +32,7 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
go mod download && \
go build -tags timetzdata,netgo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
mkdir -p /agent && \
mv main /agent && \
mv version /agent && \
@@ -147,4 +147,4 @@ HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
# Leeeeettttt'ssss goooooo!!!
# Run the shizzle from the right working directory.
WORKDIR /home/agent
CMD ["./main", "run", "opensource", "80"]
CMD ["./main", "-action", "run", "-port", "80"]

View File

@@ -18,6 +18,7 @@
[![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE)
<a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a>
[![Discord Shield](https://discordapp.com/api/guilds/1039619181731135499/widget.png?style=shield)](https://discord.gg/Bj77Vqfp2G)
[![kerberosio](https://snapcraft.io/kerberosio/badge.svg)](https://snapcraft.io/kerberosio)
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
@@ -41,6 +42,7 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
1. [Quickstart - Docker](#quickstart---docker)
2. [Quickstart - Balena](#quickstart---balena)
3. [Quickstart - Snap](#quickstart---snap)
### Introduction
@@ -78,12 +80,19 @@ If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our
## Quickstart - Balena
Run Kerberos Agent with Balena super powers. Monitor your agent with seamless remote access, and an encrypted https endpoint.
Checkout our fleet on [Balena Hub](https://hub.balena.io/fleets?0%5B0%5D%5Bn%5D=any&0%5B0%5D%5Bo%5D=full_text_search&0%5B0%5D%5Bv%5D=agent), and add your agent.
Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Monitor your Kerberos Agent with seamless remote access, over the air updates, an encrypted public `https` endpoint and many more. Checkout our application `video-surveillance` on [Balena Hub](https://hub.balena.io/apps/2064752/video-surveillance), and create your first or fleet of Kerberos Agent(s).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
**_Work In Progress_** - Currently we only support IP and USB Cameras, we have [an approach for leveraging the Raspberry Pi camera](https://github.com/kerberos-io/camera-to-rtsp), but this isn't working as expected with Balena. If you require this, you'll need to use the traditional Docker deployment with sidecar as mentioned above.
## Quickstart - Snap
Run Kerberos Agent with our [Snapcraft package](https://snapcraft.io/kerberosio).
snap install kerberosio
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following
sudo kerberosio.agent -action=run -port=80
## A world of Kerberos Agents
@@ -100,8 +109,10 @@ This repository contains everything you'll need to know about our core product,
- Single camera per instance (e.g. one container per camera).
- Primary and secondary stream setup (record full-res, stream low-res).
- Low resolution streaming through MQTT and full resolution streaming through WebRTC.
- End-to-end encryption through MQTT using RSA and AES.
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
- Post- and pre-recording on motion detection.
- Encryption at rest using AES-256-CBC.
- Ability to create fragmented recordings, and streaming though HLS fMP4.
- [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc.
- Cloud storage/persistance: Kerberos Hub, Kerberos Vault and Dropbox. [(WIP: Minio, Storj, Google Drive, FTP etc.)](https://github.com/kerberos-io/agent/issues/95)
@@ -122,6 +133,8 @@ We have documented the different deployment models [in the `deployments` directo
- [Red Hat OpenShift with Ansible](https://github.com/kerberos-io/agent/tree/master/deployments#4-red-hat-ansible-and-openshift)
- [Terraform](https://github.com/kerberos-io/agent/tree/master/deployments#5-terraform)
- [Salt](https://github.com/kerberos-io/agent/tree/master/deployments#6-salt)
- [Balena](https://github.com/kerberos-io/agent/tree/master/deployments#8-balena)
- [Snap](https://github.com/kerberos-io/agent/tree/master/deployments#9-snap)
By default your Kerberos Agents will store all its configuration and recordings inside the container. To help you automate and have a more consistent data governance, you can attach volumes to configure and persist data of your Kerberos Agents, and/or configure each Kerberos Agent through environment variables.
@@ -136,6 +149,20 @@ The default username and password for the Kerberos Agent is:
**_Please note that you change the username and password for a final installation, see [Configure with environment variables](#configure-with-environment-variables) below._**
## Encryption
You can encrypt your recordings and outgoing MQTT messages with your own AES and RSA keys by enabling the encryption settings. Once enabled all your recordings will be encrypted using AES-256-CBC and your symmetric key. You can either use the default `openssl` toolchain to decrypt the recordings with your AES key, as following:
openssl aes-256-cbc -d -md md5 -in encrypted.mp4 -out decrypted.mp4 -k your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
, and additionally you can decrypt a folder of recordings, using the Kerberos Agent binary as following:
go run main.go -action decrypt ./data/recordings your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
or for a single file:
go run main.go -action decrypt ./data/recordings/video.mp4 your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
## Configure and persist with volume mounts
An example of how to mount a host directory is shown below using `docker`, but is applicable for [all the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
@@ -166,6 +193,7 @@ Next to attaching the configuration file, it is also possible to override the co
| Name | Description | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `AGENT_MODE` | You can choose to run this in 'release' for production, and or 'demo' for showcasing. | "release" |
| `AGENT_TLS_INSECURE` | Specify if you want to use `InsecureSkipVerify` for the internal HTTP client. | "false" |
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" |
@@ -184,8 +212,11 @@ Next to attaching the configuration file, it is also possible to override the co
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
| `AGENT_CAPTURE_MOTION` | Toggle for enabling or disabling motion. | "true" |
| `AGENT_CAPTURE_LIVEVIEW` | Toggle for enabling or disabling liveview. | "true" |
| `AGENT_CAPTURE_SNAPSHOTS` | Toggle for enabling or disabling snapshot generation. | "true" |
| `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" |
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous or motion based recording. | "false" |
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous "true" or motion "false". | "false" |
| `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" |
| `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" |
| `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" |
@@ -212,6 +243,11 @@ Next to attaching the configuration file, it is also possible to override the co
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_DROPBOX_ACCESS_TOKEN` | The Access Token from your Dropbox app, that is used to leverage the Dropbox SDK. | "" |
| `AGENT_DROPBOX_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_ENCRYPTION` | Enable 'true' or disable 'false' end-to-end encryption for MQTT messages. | "false" |
| `AGENT_ENCRYPTION_RECORDINGS` | Enable 'true' or disable 'false' end-to-end encryption for recordings. | "false" |
| `AGENT_ENCRYPTION_FINGERPRINT` | The fingerprint of the keypair (public/private keys), so you know which one to use. | "" |
| `AGENT_ENCRYPTION_PRIVATE_KEY` | The private key (assymetric/RSA) to decryptand sign requests send over MQTT. | "" |
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt request send over MQTT. | "" |
## Contribute with Codespaces
@@ -234,9 +270,9 @@ On opening of the GitHub Codespace, some dependencies will be installed. Once th
const dev = {
ENV: 'dev',
HOSTNAME: externalHost,
//API_URL: `${protocol}//${hostname}:8080/api`,
//URL: `${protocol}//${hostname}:8080`,
//WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
//API_URL: `${protocol}//${hostname}:80/api`,
//URL: `${protocol}//${hostname}:80`,
//WS_URL: `${websocketprotocol}//${hostname}:80/ws`,
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
API_URL: `${protocol}//${externalHost}/api`,
@@ -249,7 +285,7 @@ Go and open two terminals one for the `ui` project and one for the `machinery` p
1. Terminal A:
cd machinery/
go run main.go run camera 80
go run main.go -action run -port 80
2. Terminal B:
@@ -290,7 +326,7 @@ You can simply run the `machinery` using following commands.
git clone https://github.com/kerberos-io/agent
cd machinery
go run main.go run mycameraname 80
go run main.go -action run -port 80
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 builtin.

View File

@@ -14,6 +14,7 @@ We will discuss following deployment models.
- [5. Kerberos Factory](#5-kerberos-factory)
- [6. Terraform](#6-terraform)
- [7. Salt](#7-salt)
- [8. Balena](#8-balena)
## 0. Static binary
@@ -53,8 +54,26 @@ All of the previously deployments, `docker`, `kubernetes` and `openshift` are gr
## 6. Terraform
To be written
Terraform is a tool for infrastructure provisioning to build infrastructure through code, often called Infrastructure as Code. So, Terraform allows you to automate and manage your infrastructure, your platform, and the services that run on that platform. By using Terraform you can deploy your Kerberos Agents remotely at scale.
> Learn more [about Kerberos Agent with Terraform](https://github.com/kerberos-io/agent/tree/master/deployments/terraform).
## 7. Salt
To be written
## 8. Balena
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
Together with the Balena.io team we've build a Balena App, called [`video-surveillance`](https://hub.balena.io/apps/2064752/video-surveillance), which any can use to deploy a video surveillance system in a matter of minutes with all the expected management features you can think of.
> Learn more [about Kerberos Agent with Balena](https://github.com/kerberos-io/agent/tree/master/deployments/balena).
## 9. Snap
The Snap Store, also known as the Ubuntu Store , is a commercial centralized software store operated by Canonical. Similar to AppImage or Flatpak the Snap Store is able to provide up to date software no matter what version of Linux you are running and how old your libraries are.
We have published our own snap `Kerberos Agent` on the Snap Store, allowing you to seamless install a Kerberos Agent on your Linux devive.
> Learn more [about Kerberos Agent with Snap](https://github.com/kerberos-io/agent/tree/master/deployments/snap).

View File

@@ -1,8 +1,31 @@
# Deployment with Balena
If you're looking for a way to quickly and easily get up and running with an Kerberos Agent for your home network, this is the project for you.
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
We provide two mechanisms to deploy Kerberos Agent to your Balena Cloud fleet:
We provide two mechanisms to deploy Kerberos Agent to a Balena Cloud fleet:
1. Use Kerberos Agent as [a block part of your larger application](https://github.com/kerberos-io/balena-agent-block).
2. Use Kerberos Agent as [a stand-a-lone application](https://github.com/kerberos-io/balena-agent).
1. Use Kerberos Agent as [a block part of your application](https://github.com/kerberos-io/balena-agent-block).
2. Use Kerberos Agent as [a stand-alone application](https://github.com/kerberos-io/balena-agent).
## Block
Within Balena you can build the concept of a block, which is the equivalent of container image or a function in a typical programming language. The idea of blocks, you can find a more thorough explanation [here](https://docs.balena.io/learn/develop/blocks/), is that you can compose and combine multiple `blocks` to level up to the concept an `app`.
You as a developer can choose which `blocks` you would like to use, to build the desired `application` state you prefer. For example you can use the [Kerberos Agent block](https://hub.balena.io/blocks/2064662/agent) to compose a video surveillance system as part of your existing set of blocks.
You can the `Kerberos Agent` block by defining following elements in your `compose` file.
agent:
image: bh.cr/kerberos_io/agent
## App
Next to building individual `blocks` you as a developer can also decide to build up an application, composed of one or more `blocks` or third-party containers, and publish it as an `app` to the Balena Hub. This is exactly [what we've done..](https://hub.balena.io/apps/2064752/video-surveillance)
On Balena Hub we have created the []`video-surveillance` application](https://hub.balena.io/apps/2064752/video-surveillance) that utilises the [Kerberos Agent `block`](https://hub.balena.io/blocks/2064662/agent). The idea of this application is that utilises the foundation of our Kerberos Agent, but that it might include more `blocks` over time to increase and improve functionalities from other community projects.
To deploy the application you can simply press below `Deploy button` or you can navigate to the [Balena Hub apps page](https://hub.balena.io/apps/2064752/video-surveillance).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
You can find the source code, `balena.yaml` and `docker-compose.yaml` files in the [`balena-agent` repository](https://github.com/kerberos-io/balena-agent).

View File

@@ -0,0 +1,15 @@
# Deployment with Snap Store
By browsing to the Snap Store, you'll be able [to find our own snap `Kerberos Agent`](https://snapcraft.io/kerberosio). You can either install the `Kerberos Agent` through the command line.
snap install kerberosio
Or use the Desktop client to have a visual interface.
![Kerberos Agent on Snap Store](./snapstore.png)
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following.
sudo kerberosio.agent -action=run -port=80
If successfull you'll be able to browse to port `80` or if you defined a different port. This will open the Kerberos Agent interface.

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

View File

@@ -0,0 +1,41 @@
# Deployment with Terraform
If you are using Terraform as part of your DevOps stack, you might utilise it to deploy your Kerberos Agents. Within this deployment folder we have added an example Terraform file `docker.tf`, which installs the Kerberos Agent `docker` container on a remote system over `SSH`. We might create our own provider in the future, or add additional examples for example `snap`, `kubernetes`, etc.
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
terraform init
Once initialised you should see similar output:
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of telkomindonesia/linux from the dependency lock file
- Using previously-installed telkomindonesia/linux v0.7.0
Go and open the `docker.tf` file and locate the `linux` provider, modify following credentials accordingly. Make sure they match for creating an `SSH` connection.
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
Apply the `docker.tf` file, to install `docker` and the `kerberos/agent` docker container.
terraform apply
Once done you should see following output, and you should be able to reach the remote machine on port `80` or if configured differently the specified port you've defined.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
linux_script.install_docker_kerberos_agent: Modifying... [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
linux_script.install_docker_kerberos_agent: Modifications complete after 3s [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

View File

@@ -0,0 +1,47 @@
terraform {
required_providers {
linux = {
source = "TelkomIndonesia/linux"
version = "0.7.0"
}
}
}
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
locals {
image = "kerberos/agent"
version = "latest"
port = 80
}
resource "linux_script" "install_docker" {
lifecycle_commands {
create = "apt update && apt install -y $PACKAGE_NAME"
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
update = "apt update && apt install -y $PACKAGE_NAME"
delete = "apt remove -y $PACKAGE_NAME"
}
environment = {
PACKAGE_NAME = "docker"
}
}
resource "linux_script" "install_docker_kerberos_agent" {
lifecycle_commands {
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
read = "docker inspect agent"
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
delete = "docker rm agent --force"
}
environment = {
IMAGE = local.image
VERSION = local.version
PORT = local.port
}
}

View File

@@ -10,7 +10,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["run", "cameraname", "8080"],
"args": ["-action", "run"],
"envFile": "${workspaceFolder}/.env",
"buildFlags": "--tags dynamic",
},

View File

@@ -111,5 +111,6 @@
"hub_key": "",
"hub_private_key": "",
"hub_site": "",
"condition_uri": ""
"condition_uri": "",
"encryption": {}
}

View File

@@ -54,6 +54,35 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
@@ -112,6 +141,35 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
@@ -317,8 +375,15 @@ const docTemplate = `{
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -621,6 +686,17 @@ const docTemplate = `{
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -46,6 +46,35 @@
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
@@ -104,6 +133,35 @@
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
@@ -309,8 +367,15 @@
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -613,6 +678,17 @@
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -2,8 +2,13 @@ basePath: /
definitions:
models.APIResponse:
properties:
can_pan_tilt:
type: boolean
can_zoom:
type: boolean
data: {}
message: {}
ptz_functions: {}
type: object
models.Authentication:
properties:
@@ -202,6 +207,13 @@ definitions:
tilt:
type: number
type: object
models.OnvifPreset:
properties:
onvif_credentials:
$ref: '#/definitions/models.OnvifCredentials'
preset:
type: string
type: object
models.OnvifZoom:
properties:
onvif_credentials:
@@ -310,6 +322,25 @@ paths:
summary: Will return the ONVIF capabilities for the specific camera.
tags:
- camera
/api/camera/onvif/gotopreset:
post:
description: Will activate the desired ONVIF preset.
operationId: camera-onvif-gotopreset
parameters:
- description: OnvifPreset
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifPreset'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will activate the desired ONVIF preset.
tags:
- camera
/api/camera/onvif/login:
post:
description: Try to login into ONVIF supported camera.
@@ -348,6 +379,25 @@ paths:
summary: Panning or/and tilting the camera.
tags:
- camera
/api/camera/onvif/presets:
post:
description: Will return the ONVIF presets for the specific camera.
operationId: camera-onvif-presets
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF presets for the specific camera.
tags:
- camera
/api/camera/onvif/zoom:
post:
description: Zooming in or out the camera.

View File

@@ -3,7 +3,7 @@ module github.com/kerberos-io/agent/machinery
go 1.19
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
require (
github.com/InVisionApp/conjungo v1.1.0
@@ -25,7 +25,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.58
github.com/kerberos-io/onvif v0.0.5
github.com/kerberos-io/onvif v0.0.7
github.com/minio/minio-go/v6 v6.0.57
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7

View File

@@ -266,8 +266,8 @@ github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+Mrar
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
github.com/kerberos-io/joy4 v1.0.58/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/onvif v0.0.5 h1:kq9mnHZkih9Jl4DyIJ4Rzt++Y3DDKy3nI8S2ESEfZ5w=
github.com/kerberos-io/onvif v0.0.5/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kerberos-io/onvif v0.0.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
github.com/kerberos-io/onvif v0.0.7/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=

View File

@@ -2,12 +2,15 @@ package main
import (
"context"
"flag"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/routers"
"github.com/kerberos-io/agent/machinery/src/utils"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
@@ -49,10 +52,23 @@ func main() {
}
// Start the show ;)
action := os.Args[1]
// We'll parse the flags (named variables), and start the agent.
var action string
var configDirectory string
var name string
var port string
var timeout string
flag.StringVar(&action, "action", "version", "Tell us what you want do 'run' or 'version'")
flag.StringVar(&configDirectory, "config", ".", "Where is the configuration stored")
flag.StringVar(&name, "name", "agent", "Provide a name for the agent")
flag.StringVar(&port, "port", "80", "On which port should the agent run")
flag.StringVar(&timeout, "timeout", "2000", "Number of milliseconds to wait for the ONVIF discovery to complete")
flag.Parse()
timezone, _ := time.LoadLocation("CET")
log.Log.Init(timezone)
log.Log.Init(configDirectory, timezone)
switch action {
@@ -60,14 +76,25 @@ func main() {
log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
case "discover":
timeout := os.Args[2]
log.Log.Info(timeout)
case "decrypt":
log.Log.Info("Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
symmetricKey := []byte(flag.Arg(1))
if symmetricKey == nil || len(symmetricKey) == 0 {
log.Log.Fatal("Main: symmetric key should not be empty")
return
}
if len(symmetricKey) != 32 {
log.Log.Fatal("Main: symmetric key should be 32 bytes")
return
}
utils.Decrypt(flag.Arg(0), symmetricKey)
case "run":
{
name := os.Args[2]
port := os.Args[3]
// Print Kerberos.io ASCII art
utils.PrintASCIIArt()
@@ -82,28 +109,28 @@ func main() {
configuration.Port = port
// Open this configuration either from Kerberos Agent or Kerberos Factory.
components.OpenConfig(&configuration)
configService.OpenConfig(configDirectory, &configuration)
// We will override the configuration with the environment variables
components.OverrideWithEnvironmentVariables(&configuration)
configService.OverrideWithEnvironmentVariables(&configuration)
// Printing final configuration
utils.PrintConfiguration(&configuration)
// Check the folder permissions, it might be that we do not have permissions to write
// recordings, update the configuration or save snapshots.
utils.CheckDataDirectoryPermissions()
utils.CheckDataDirectoryPermissions(configDirectory)
// Set timezone
timezone, _ := time.LoadLocation(configuration.Config.Timezone)
log.Log.Init(timezone)
log.Log.Init(configDirectory, timezone)
// Check if we have a device Key or not, if not
// we will generate one.
if configuration.Config.Key == "" {
key := utils.RandStringBytesMaskImpr(30)
configuration.Config.Key = key
err := components.StoreConfig(configuration.Config)
err := configService.StoreConfig(configDirectory, configuration.Config)
if err == nil {
log.Log.Info("Main: updated unique key for agent to: " + key)
} else {
@@ -121,10 +148,10 @@ func main() {
CancelContext: &cancel,
HandleBootstrap: make(chan string, 1),
}
go components.Bootstrap(&configuration, &communication)
go components.Bootstrap(configDirectory, &configuration, &communication)
// Start the REST API.
routers.StartWebserver(&configuration, &communication)
routers.StartWebserver(configDirectory, &configuration, &communication)
}
default:
log.Log.Error("Main: Sorry I don't understand :(")

View File

@@ -0,0 +1 @@
package api

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils"
@@ -17,7 +18,7 @@ import (
"github.com/kerberos-io/joy4/av"
)
func CleanupRecordingDirectory(configuration *models.Configuration) {
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
autoClean := configuration.Config.AutoClean
if autoClean == "true" {
maxSize := configuration.Config.MaxDirectorySize
@@ -25,7 +26,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
maxSize = 300
}
// Total size of the recording directory.
recordingsDirectory := "./data/recordings"
recordingsDirectory := configDirectory + "/data/recordings"
size, err := utils.DirSize(recordingsDirectory)
if err == nil {
sizeInMB := size / 1000 / 1000
@@ -51,7 +52,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
}
}
func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
config := configuration.Config
@@ -134,13 +135,13 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
}
// Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
recordingStatus = "idle"
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration)
CleanupRecordingDirectory(configDirectory, configuration)
}
// If not yet started and a keyframe, let's make a recording
@@ -192,7 +193,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769"
name = s + ".mp4"
fullName = "./data/recordings/" + name
fullName = configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("Recording started")
@@ -259,7 +260,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
}
// Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
recordingStatus = "idle"
@@ -315,7 +316,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769"
name := s + ".mp4"
fullName := "./data/recordings/" + name
fullName := configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("HandleRecordStream: Recording started")
@@ -405,12 +406,33 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
// reopen file into memory 'fullName'
contents, err := os.ReadFile(fullName)
if err == nil {
// encrypt
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
if err == nil {
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("HandleRecordStream: error writing file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error encrypting file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error reading file: " + err.Error())
}
}
// Create a symbol linc.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration)
CleanupRecordingDirectory(configDirectory, configuration)
}
}

View File

@@ -2,6 +2,7 @@ package cloud
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
@@ -31,8 +32,8 @@ import (
"github.com/kerberos-io/agent/machinery/src/webrtc"
)
func PendingUpload() {
ff, err := utils.ReadDirectory("./data/cloud/")
func PendingUpload(configDirectory string) {
ff, err := utils.ReadDirectory(configDirectory + "/data/cloud/")
if err == nil {
for _, f := range ff {
log.Log.Info(f.Name())
@@ -40,12 +41,12 @@ func PendingUpload() {
}
}
func HandleUpload(configuration *models.Configuration, communication *models.Communication) {
func HandleUpload(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
log.Log.Debug("HandleUpload: started")
config := configuration.Config
watchDirectory := "./data/cloud/"
watchDirectory := configDirectory + "/data/cloud/"
if config.Offline == "true" {
log.Log.Debug("HandleUpload: stopping as Offline is enabled.")
@@ -120,8 +121,8 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com
// Check if we need to remove the original recording
// removeAfterUpload is set to false by default
if config.RemoveAfterUpload == "true" {
err := os.Remove("./data/recordings/" + fileName)
if config.RemoveAfterUpload != "false" {
err := os.Remove(configDirectory + "/data/recordings/" + fileName)
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
}
@@ -278,6 +279,8 @@ loop:
onvifEnabled := "false"
onvifZoom := "false"
onvifPanTilt := "false"
onvifPresets := "false"
var onvifPresetsList []byte
if config.Capture.IPCamera.ONVIFXAddr != "" {
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
@@ -292,8 +295,34 @@ loop:
if canPanTilt {
onvifPanTilt = "true"
}
// Try to read out presets
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil && len(presets) > 0 {
onvifPresets = "true"
onvifPresetsList, err = json.Marshal(presets)
if err != nil {
log.Log.Error("HandleHeartBeat: error while marshalling presets: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
if err != nil {
log.Log.Error("HandleHeartBeat: error while getting presets: " + err.Error())
} else {
log.Log.Debug("HandleHeartBeat: no presets found.")
}
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Error("HandleHeartBeat: error while getting PTZ configurations: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Error("HandleHeartBeat: error while connecting to ONVIF device: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Debug("HandleHeartBeat: ONVIF is not enabled.")
onvifPresetsList = []byte("[]")
}
// Check if the agent is running inside a cluster (Kerberos Factory) or as
@@ -338,6 +367,8 @@ loop:
"onvif" : "%s",
"onvif_zoom" : "%s",
"onvif_pantilt" : "%s",
"onvif_presets": "%s",
"onvif_presets_list": %s,
"cameraConnected": "%s",
"numberoffiles" : "33",
"timestamp" : 1564747908,
@@ -345,14 +376,23 @@ loop:
"docker" : true,
"kios" : false,
"raspberrypi" : false
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, cameraConnected)
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)
req, _ := http.NewRequest("POST", url, buffy)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {
resp.Body.Close()
@@ -374,8 +414,6 @@ loop:
buffy = bytes.NewBuffer(jsonStr)
req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
req.Header.Set("Content-Type", "application/json")
client = &http.Client{}
resp, err = client.Do(req)
if resp != nil {
resp.Body.Close()
@@ -420,19 +458,17 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
// Allocate frame
frame := ffmpeg.AllocVideoFrame()
key := ""
hubKey := ""
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
key = config.S3.Publickey
hubKey = config.S3.Publickey
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
key = config.KStorage.CloudKey
hubKey = config.KStorage.CloudKey
}
// This is the new way ;)
if config.HubKey != "" {
key = config.HubKey
hubKey = config.HubKey
}
topic := "kerberos/" + key + "/device/" + config.Key + "/live"
lastLivestreamRequest := int64(0)
var cursorError error
@@ -453,7 +489,27 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
continue
}
log.Log.Info("HandleLiveStreamSD: Sending base64 encoded images to MQTT.")
sendImage(frame, topic, mqttClient, pkt, decoder, decoderMutex)
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
if err == nil {
bytes, _ := computervision.ImageToBytes(&frame.Image)
encoded := base64.StdEncoding.EncodeToString(bytes)
valueMap := make(map[string]interface{})
valueMap["image"] = encoded
message := models.Message{
Payload: models.Payload{
Action: "receive-sd-stream",
DeviceId: configuration.Config.Key,
Value: valueMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
}
}
// Cleanup the frame.
@@ -467,15 +523,6 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
log.Log.Debug("HandleLiveStreamSD: finished")
}
func sendImage(frame *ffmpeg.VideoFrame, topic string, mqttClient mqtt.Client, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
if err == nil {
bytes, _ := computervision.ImageToBytes(&frame.Image)
encoded := base64.StdEncoding.EncodeToString(bytes)
mqttClient.Publish(topic, 0, false, encoded)
}
}
func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
config := configuration.Config
@@ -494,25 +541,23 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
if config.Capture.ForwardWebRTC == "true" {
// We get a request with an offer, but we'll forward it.
for m := range communication.HandleLiveHDHandshake {
/*for m := range communication.HandleLiveHDHandshake {
// Forward SDP
m.CloudKey = config.Key
request, err := json.Marshal(m)
if err == nil {
mqttClient.Publish("kerberos/webrtc/request", 2, false, request)
}
}
}*/
} else {
log.Log.Info("HandleLiveStreamHD: Waiting for peer connections.")
for handshake := range communication.HandleLiveHDHandshake {
log.Log.Info("HandleLiveStreamHD: setting up a peer connection.")
key := config.Key + "/" + handshake.Cuuid
webrtc.CandidatesMutex.Lock()
key := config.Key + "/" + handshake.SessionID
_, ok := webrtc.CandidateArrays[key]
if !ok {
webrtc.CandidateArrays[key] = make(chan string, 30)
webrtc.CandidateArrays[key] = make(chan string)
}
webrtc.CandidatesMutex.Unlock()
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
}
@@ -550,7 +595,15 @@ func VerifyHub(c *gin.Context) {
if err == nil {
req.Header.Set("X-Kerberos-Hub-PublicKey", publicKey)
req.Header.Set("X-Kerberos-Hub-PrivateKey", privateKey)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if err == nil {
@@ -598,7 +651,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) {
func VerifyPersistence(c *gin.Context, configDirectory string) {
var config models.Config
err := c.BindJSON(&config)
@@ -620,7 +673,7 @@ func VerifyPersistence(c *gin.Context) {
} else {
// Open test-480p.mp4
file, err := os.Open("./data/test-480p.mp4")
file, err := os.Open(configDirectory + "/data/test-480p.mp4")
if err != nil {
msg := "VerifyPersistence: error reading test-480p.mp4: " + err.Error()
log.Log.Error(msg)
@@ -649,7 +702,15 @@ func VerifyPersistence(c *gin.Context) {
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {
@@ -689,7 +750,16 @@ func VerifyPersistence(c *gin.Context) {
if err == nil && uri != "" && accessKey != "" && secretAccessKey != "" {
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
req, err := http.NewRequest("POST", uri+"/ping", nil)
req.Header.Add("X-Kerberos-Storage-AccessKey", accessKey)
req.Header.Add("X-Kerberos-Storage-SecretAccessKey", secretAccessKey)
@@ -708,7 +778,7 @@ func VerifyPersistence(c *gin.Context) {
"_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
// Open test-480p.mp4
file, err := os.Open("./data/test-480p.mp4")
file, err := os.Open(configDirectory + "/test-480p.mp4")
if err != nil {
msg := "VerifyPersistence: error reading test-480p.mp4: " + err.Error()
log.Log.Error(msg)
@@ -731,7 +801,15 @@ func VerifyPersistence(c *gin.Context) {
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
req.Header.Set("X-Kerberos-Storage-Directory", directory)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)

View File

@@ -1,6 +1,7 @@
package cloud
import (
"crypto/tls"
"errors"
"io/ioutil"
"net/http"
@@ -43,7 +44,7 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
if err != nil {
err := "UploadKerberosHub: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, true, errors.New(err)
return false, false, errors.New(err)
}
// Check if we are allowed to upload to the hub with these credentials.
@@ -62,7 +63,15 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {
@@ -96,9 +105,6 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
req.Header.Set("X-Kerberos-Hub-PublicKey", config.HubKey)
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
client = &http.Client{}
resp, err = client.Do(req)
if resp != nil {
defer resp.Body.Close()

View File

@@ -1,6 +1,7 @@
package cloud
import (
"crypto/tls"
"errors"
"io/ioutil"
"net/http"
@@ -43,7 +44,7 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string) (
if err != nil {
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, true, errors.New(err)
return false, false, errors.New(err)
}
publicKey := config.KStorage.CloudKey
@@ -67,7 +68,16 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string) (
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
req.Header.Set("X-Kerberos-Storage-Directory", config.KStorage.Directory)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {

View File

@@ -14,6 +14,7 @@ import (
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/computervision"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
@@ -23,7 +24,7 @@ import (
"github.com/tevino/abool"
)
func Bootstrap(configuration *models.Configuration, communication *models.Communication) {
func Bootstrap(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
log.Log.Debug("Bootstrap: started")
// We will keep track of the Kerberos Agent up time
@@ -72,14 +73,14 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
// We'll create a MQTT handler, which will be used to communicate with Kerberos Hub.
// Configure a MQTT client which helps for a bi-directional communication
mqttClient := routers.ConfigureMQTT(configuration, communication)
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
// Run the agent and fire up all the other
// goroutines which do image capture, motion detection, onvif, etc.
for {
// This will blocking until receiving a signal to be restarted, reconfigured, stopped, etc.
status := RunAgent(configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
status := RunAgent(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
if status == "stop" {
break
@@ -87,15 +88,15 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
if status == "not started" {
// We will re open the configuration, might have changed :O!
OpenConfig(configuration)
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
configService.OverrideWithEnvironmentVariables(configuration)
}
// Reset the MQTT client, might have provided new information, so we need to reconnect.
if routers.HasMQTTClientModified(configuration) {
routers.DisconnectMQTT(mqttClient, &configuration.Config)
mqttClient = routers.ConfigureMQTT(configuration, communication)
mqttClient = routers.ConfigureMQTT(configDirectory, configuration, communication)
}
// We will create a new cancelable context, which will be used to cancel and restart.
@@ -107,7 +108,7 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
log.Log.Debug("Bootstrap: finished")
}
func RunAgent(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
func RunAgent(configDirectory string, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
log.Log.Debug("RunAgent: bootstrapping agent")
config := configuration.Config
@@ -134,6 +135,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
var queue *pubsub.Queue
var subQueue *pubsub.Queue
@@ -162,10 +167,18 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
time.Sleep(time.Second * 3)
return status
}
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
}
if cameraSettings.RTSP != rtspUrl || cameraSettings.SubRTSP != subRtspUrl || cameraSettings.Width != width || cameraSettings.Height != height || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
if cameraSettings.Initialized {
if cameraSettings.RTSP != "" && cameraSettings.SubRTSP != "" && cameraSettings.Initialized {
decoder.Close()
if subStreamEnabled {
subDecoder.Close()
@@ -189,6 +202,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
cameraSettings.Denum = denum
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
cameraSettings.Initialized = true
} else {
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
}
@@ -250,7 +264,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
}
// Handle livestream HD (high resolution over WEBRTC)
communication.HandleLiveHDHandshake = make(chan models.SDPPayload, 1)
communication.HandleLiveHDHandshake = make(chan models.RequestHDStreamPayload, 1)
if subStreamEnabled {
livestreamHDCursor := subQueue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, subStreams, subDecoder, &decoderMutex)
@@ -260,10 +274,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
}
// Handle recording, will write an mp4 to disk.
go capture.HandleRecordStream(queue, configuration, communication, streams)
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, streams)
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
go cloud.HandleUpload(configuration, communication)
go cloud.HandleUpload(configDirectory, configuration, communication)
// Handle ONVIF actions
go onvif.HandleONVIFActions(configuration, communication)
@@ -284,10 +298,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
(*communication.CancelContext)()
// We will re open the configuration, might have changed :O!
OpenConfig(configuration)
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
configService.OverrideWithEnvironmentVariables(configuration)
// Here we are cleaning up everything!
if configuration.Config.Offline != "true" {

View File

@@ -41,7 +41,8 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
log.Log.Info("ProcessMotion: Motion detection enabled.")
key := config.HubKey
hubKey := config.HubKey
deviceKey := config.Key
// Allocate a VideoFrame
frame := ffmpeg.AllocVideoFrame()
@@ -167,10 +168,24 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
// If offline mode is disabled, send a message to the hub
if config.Offline != "true" {
if mqttClient != nil {
if key != "" {
mqttClient.Publish("kerberos/"+key+"/device/"+config.Key+"/motion", 2, false, "motion")
if hubKey != "" {
message := models.Message{
Payload: models.Payload{
Action: "motion",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
"timestamp": time.Now().Unix(),
},
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("ProcessMotion: failed to package MQTT message: " + err.Error())
}
} else {
mqttClient.Publish("kerberos/device/"+config.Key+"/motion", 2, false, "motion")
mqttClient.Publish("kerberos/agent/"+deviceKey, 2, false, "motion")
}
}
}

View File

@@ -1,4 +1,4 @@
package components
package config
import (
"context"
@@ -20,14 +20,14 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
func GetImageFromFilePath() (image.Image, error) {
snapshotDirectory := "./data/snapshots"
func GetImageFromFilePath(configDirectory string) (image.Image, error) {
snapshotDirectory := configDirectory + "/data/snapshots"
files, err := ioutil.ReadDir(snapshotDirectory)
if err == nil && len(files) > 1 {
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().Before(files[j].ModTime())
})
filePath := "./data/snapshots/" + files[1].Name()
filePath := configDirectory + "/data/snapshots/" + files[1].Name()
f, err := os.Open(filePath)
if err != nil {
return nil, err
@@ -42,11 +42,11 @@ func GetImageFromFilePath() (image.Image, error) {
// ReadUserConfig Reads the user configuration of the Kerberos Open Source instance.
// This will return a models.User struct including the username, password,
// selected language, and if the installation was completed or not.
func ReadUserConfig() (userConfig models.User) {
func ReadUserConfig(configDirectory string) (userConfig models.User) {
for {
jsonFile, err := os.Open("./data/config/user.json")
jsonFile, err := os.Open(configDirectory + "/data/config/user.json")
if err != nil {
log.Log.Error("Config file is not found " + "./data/config/user.json, trying again in 5s: " + err.Error())
log.Log.Error("Config file is not found " + configDirectory + "/data/config/user.json, trying again in 5s: " + err.Error())
time.Sleep(5 * time.Second)
} else {
log.Log.Info("Successfully Opened user.json")
@@ -66,7 +66,7 @@ func ReadUserConfig() (userConfig models.User) {
return
}
func OpenConfig(configuration *models.Configuration) {
func OpenConfig(configDirectory string, configuration *models.Configuration) {
// We are checking which deployment this is running, so we can load
// into the configuration as expected.
@@ -84,23 +84,44 @@ func OpenConfig(configuration *models.Configuration) {
collection := db.Collection("configuration")
var globalConfig models.Config
err := collection.FindOne(context.Background(), bson.M{
res := collection.FindOne(context.Background(), bson.M{
"type": "global",
}).Decode(&globalConfig)
})
if res.Err() != nil {
log.Log.Error("Could not find global configuration, using default configuration.")
panic("Could not find global configuration, using default configuration.")
}
err := res.Decode(&globalConfig)
if err != nil {
log.Log.Error("Could not find global configuration, using default configuration.")
panic("Could not find global configuration, using default configuration.")
}
if globalConfig.Type != "global" {
log.Log.Error("Could not find global configuration, might missed the mongodb connection.")
panic("Could not find global configuration, might missed the mongodb connection.")
}
configuration.GlobalConfig = globalConfig
var customConfig models.Config
deploymentName := os.Getenv("DEPLOYMENT_NAME")
err = collection.FindOne(context.Background(), bson.M{
res = collection.FindOne(context.Background(), bson.M{
"type": "config",
"name": deploymentName,
}).Decode(&customConfig)
})
if res.Err() != nil {
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
}
err = res.Decode(&customConfig)
if err != nil {
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
}
if customConfig.Type != "config" {
log.Log.Error("Could not find custom configuration, might missed the mongodb connection.")
panic("Could not find custom configuration, might missed the mongodb connection.")
}
configuration.CustomConfig = customConfig
// We will merge both configs in a single config file.
@@ -120,8 +141,13 @@ func OpenConfig(configuration *models.Configuration) {
},
)
// Merge Config toplevel
// Reset main configuration Config.
configuration.Config = models.Config{}
// Merge the global settings in the main config
conjungo.Merge(&configuration.Config, configuration.GlobalConfig, opts)
// Now we might override some settings with the custom config
conjungo.Merge(&configuration.Config, configuration.CustomConfig, opts)
// Merge Kerberos Vault settings
@@ -136,6 +162,9 @@ func OpenConfig(configuration *models.Configuration) {
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
configuration.Config.S3 = &s3
// Merge timetable manually because it's an array
configuration.Config.Timetable = configuration.CustomConfig.Timetable
// Cleanup
opts = nil
@@ -146,9 +175,9 @@ func OpenConfig(configuration *models.Configuration) {
// Open device config
for {
jsonFile, err := os.Open("./data/config/config.json")
jsonFile, err := os.Open(configDirectory + "/data/config/config.json")
if err != nil {
log.Log.Error("Config file is not found " + "./data/config/config.json" + ", trying again in 5s.")
log.Log.Error("Config file is not found " + configDirectory + "/data/config/config.json" + ", trying again in 5s.")
time.Sleep(5 * time.Second)
} else {
log.Log.Info("Successfully Opened config.json from " + configuration.Name)
@@ -189,7 +218,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Key = value
break
case "AGENT_NAME":
configuration.Config.Name = value
configuration.Config.FriendlyName = value
break
case "AGENT_TIMEZONE":
configuration.Config.Timezone = value
@@ -432,16 +461,33 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
case "AGENT_DROPBOX_DIRECTORY":
configuration.Config.Dropbox.Directory = value
break
/* When encryption is enabled */
case "AGENT_ENCRYPTION":
configuration.Config.Encryption.Enabled = value
break
case "AGENT_ENCRYPTION_RECORDINGS":
configuration.Config.Encryption.Recordings = value
break
case "AGENT_ENCRYPTION_FINGERPRINT":
configuration.Config.Encryption.Fingerprint = value
break
case "AGENT_ENCRYPTION_PRIVATE_KEY":
configuration.Config.Encryption.PrivateKey = value
break
case "AGENT_ENCRYPTION_SYMMETRIC_KEY":
configuration.Config.Encryption.SymmetricKey = value
break
}
}
}
}
func SaveConfig(config models.Config, configuration *models.Configuration, communication *models.Communication) error {
func SaveConfig(configDirectory string, config models.Config, configuration *models.Configuration, communication *models.Communication) error {
if !communication.IsConfiguring.IsSet() {
communication.IsConfiguring.Set()
err := StoreConfig(config)
err := StoreConfig(configDirectory, config)
if err != nil {
communication.IsConfiguring.UnSet()
return err
@@ -462,7 +508,14 @@ func SaveConfig(config models.Config, configuration *models.Configuration, commu
}
}
func StoreConfig(config models.Config) error {
func StoreConfig(configDirectory string, config models.Config) error {
// Encryption key can be set wrong.
encryptionPrivateKey := config.Encryption.PrivateKey
// Replace \\n by \n
encryptionPrivateKey = strings.ReplaceAll(encryptionPrivateKey, "\\n", "\n")
config.Encryption.PrivateKey = encryptionPrivateKey
// Save into database
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
// Write to mongodb
@@ -484,7 +537,7 @@ func StoreConfig(config models.Config) error {
// Save into file
} else if os.Getenv("DEPLOYMENT") == "" || os.Getenv("DEPLOYMENT") == "agent" {
res, _ := json.MarshalIndent(config, "", "\t")
err := ioutil.WriteFile("./data/config/config.json", res, 0644)
err := ioutil.WriteFile(configDirectory+"/data/config/config.json", res, 0644)
return err
}

View File

@@ -28,10 +28,10 @@ func New() *mongo.Client {
password := os.Getenv("MONGODB_PASSWORD")
authentication := "SCRAM-SHA-256"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_init_ctx.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_instance = new(DB)
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
if replicaset != "" {

View File

@@ -0,0 +1,148 @@
package encryption
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"errors"
"hash"
)
// DecryptWithPrivateKey decrypts data with private key
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
// decode our encrypted string into cipher bytes
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
// decrypt the data
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
return out, err
}
// SignWithPrivateKey signs data with private key
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
// hash the data with sha256
hashed := sha256.Sum256(data)
// sign the data
signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
return signature, err
}
func AesEncrypt(content []byte, password string) ([]byte, error) {
salt := make([]byte, 8)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
key, iv, err := DefaultEvpKDF([]byte(password), salt)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCEncrypter(block, iv)
cipherBytes := PKCS5Padding(content, aes.BlockSize)
mode.CryptBlocks(cipherBytes, cipherBytes)
cipherText := make([]byte, 16+len(cipherBytes))
copy(cipherText[:8], []byte("Salted__"))
copy(cipherText[8:16], salt)
copy(cipherText[16:], cipherBytes)
//cipherText := base64.StdEncoding.EncodeToString(data)
return cipherText, nil
}
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
//data, err := base64.StdEncoding.DecodeString(cipherText)
//if err != nil {
// return nil, err
//}
if string(cipherText[:8]) != "Salted__" {
return nil, errors.New("invalid crypto js aes encryption")
}
salt := cipherText[8:16]
cipherBytes := cipherText[16:]
key, iv, err := DefaultEvpKDF([]byte(password), salt)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherBytes, cipherBytes)
result := PKCS5UnPadding(cipherBytes)
return result, nil
}
// https://stackoverflow.com/questions/27677236/encryption-in-javascript-and-decryption-with-php/27678978#27678978
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/evpkdf.js#L55
func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgorithm string) ([]byte, error) {
var block []byte
var hasher hash.Hash
derivedKeyBytes := make([]byte, 0)
switch hashAlgorithm {
case "md5":
hasher = md5.New()
default:
return []byte{}, errors.New("not implement hasher algorithm")
}
for len(derivedKeyBytes) < keySize*4 {
if len(block) > 0 {
hasher.Write(block)
}
hasher.Write(password)
hasher.Write(salt)
block = hasher.Sum([]byte{})
hasher.Reset()
for i := 1; i < iterations; i++ {
hasher.Write(block)
block = hasher.Sum([]byte{})
hasher.Reset()
}
derivedKeyBytes = append(derivedKeyBytes, block...)
}
return derivedKeyBytes[:keySize*4], nil
}
func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err error) {
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/cipher-core.js#L775
keySize := 256 / 32
ivSize := 128 / 32
derivedKeyBytes, err := EvpKDF(password, salt, keySize+ivSize, 1, "md5")
if err != nil {
return []byte{}, []byte{}, err
}
return derivedKeyBytes[:keySize*4], derivedKeyBytes[keySize*4:], nil
}
// https://stackoverflow.com/questions/41579325/golang-how-do-i-decrypt-with-des-cbc-and-pkcs7
func PKCS5UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
func PKCS5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}

View File

@@ -21,7 +21,7 @@ var Log = Logging{
var gologging = logging.MustGetLogger("gologger")
func ConfigureGoLogging(timezone *time.Location) {
func ConfigureGoLogging(configDirectory string, timezone *time.Location) {
// Logging
var format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
@@ -32,7 +32,7 @@ func ConfigureGoLogging(timezone *time.Location) {
stdBackend := logging.NewLogBackend(os.Stderr, "", 0)
stdBackendLeveled := logging.NewBackendFormatter(stdBackend, format)
fileBackend := logging.NewLogBackend(&lumberjack.Logger{
Filename: "./data/log/machinery.txt",
Filename: configDirectory + "/data/log/machinery.txt",
MaxSize: 2, // megabytes
Compress: true, // disabled by default
}, "", 0)
@@ -75,10 +75,10 @@ type Logging struct {
Level string
}
func (self *Logging) Init(timezone *time.Location) {
func (self *Logging) Init(configDirectory string, timezone *time.Location) {
switch self.Logger {
case "go-logging":
ConfigureGoLogging(timezone)
ConfigureGoLogging(configDirectory, timezone)
case "logrus":
ConfigureLogrus(timezone)
default:

View File

@@ -29,3 +29,8 @@ type OnvifZoom struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Zoom float64 `json:"zoom,omitempty" bson:"zoom"`
}
type OnvifPreset struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Preset string `json:"preset,omitempty" bson:"preset"`
}

View File

@@ -26,7 +26,7 @@ type Communication struct {
HandleHeartBeat chan string
HandleLiveSD chan int64
HandleLiveHDKeepalive chan string
HandleLiveHDHandshake chan SDPPayload
HandleLiveHDHandshake chan RequestHDStreamPayload
HandleLiveHDPeers chan string
HandleONVIF chan OnvifAction
IsConfiguring *abool.AtomicBool

View File

@@ -21,7 +21,7 @@ type Config struct {
AutoClean string `json:"auto_clean"`
RemoveAfterUpload string `json:"remove_after_upload"`
MaxDirectorySize int64 `json:"max_directory_size"`
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
Timezone string `json:"timezone"`
Capture Capture `json:"capture"`
Timetable []*Timetable `json:"timetable"`
Region *Region `json:"region"`
@@ -42,6 +42,7 @@ type Config struct {
HubPrivateKey string `json:"hub_private_key" bson:"hub_private_key"`
HubSite string `json:"hub_site" bson:"hub_site"`
ConditionURI string `json:"condition_uri" bson:"condition_uri"`
Encryption *Encryption `json:"encryption,omitempty" bson:"encryption,omitempty"`
}
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
@@ -70,13 +71,15 @@ type Capture struct {
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
// Also includes ONVIF integration
type IPCamera struct {
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
RTSP string `json:"rtsp"`
SubRTSP string `json:"sub_rtsp"`
FPS string `json:"fps"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
}
// USBCamera configuration, such as the device path (/dev/video*)
@@ -155,3 +158,12 @@ type Dropbox struct {
AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
}
// Encryption
type Encryption struct {
Enabled string `json:"enabled" bson:"enabled"`
Recordings string `json:"recordings" bson:"recordings"`
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
PrivateKey string `json:"private_key" bson:"private_key"`
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
}

View File

@@ -0,0 +1,155 @@
package models
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io/ioutil"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
)
func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, error) {
// Create a Version 4 UUID.
u2, err := uuid.NewV4()
if err != nil {
log.Log.Error("failed to generate UUID: " + err.Error())
}
// We'll generate an unique id, and encrypt / decrypt it using the private key if available.
msg.Mid = u2.String()
msg.DeviceId = msg.Payload.DeviceId
msg.Timestamp = time.Now().Unix()
// At the moment we don't do the encryption part, but we'll implement it
// once the legacy methods (subscriptions are moved).
msg.Encrypted = false
if configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
msg.Encrypted = true
}
msg.PublicKey = ""
msg.Fingerprint = ""
if msg.Encrypted {
pload := msg.Payload
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
log.Log.Error("failed to marshal payload: " + err.Error())
}
// Encrypt the value
privateKey := configuration.Config.Encryption.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := ioutil.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
} else {
// Parse private key
b := block.Bytes
key, err := x509.ParsePKCS8PrivateKey(b)
if err != nil {
log.Log.Error("MQTTListenerHandler: error parsing private key: " + err.Error())
}
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
// Create a 16bit key random
k := configuration.Config.Encryption.SymmetricKey
encryptedValue, err := encryption.AesEncrypt(data, k)
if err == nil {
data := base64.StdEncoding.EncodeToString(encryptedValue)
// Sign the encrypted value
signature, err := encryption.SignWithPrivateKey([]byte(data), rsaKey)
if err == nil {
base64Signature := base64.StdEncoding.EncodeToString(signature)
msg.Payload.EncryptedValue = data
msg.Payload.Signature = base64Signature
msg.Payload.Value = make(map[string]interface{})
}
}
}
}
payload, err := json.Marshal(msg)
return payload, err
}
// The message structure which is used to send over
// and receive messages from the MQTT broker
type Message struct {
Mid string `json:"mid"`
DeviceId string `json:"device_id"`
Timestamp int64 `json:"timestamp"`
Encrypted bool `json:"encrypted"`
PublicKey string `json:"public_key"`
Fingerprint string `json:"fingerprint"`
Payload Payload `json:"payload"`
}
// The payload structure which is used to send over
// and receive messages from the MQTT broker
type Payload struct {
Action string `json:"action"`
DeviceId string `json:"device_id"`
Signature string `json:"signature"`
EncryptedValue string `json:"encrypted_value"`
Value map[string]interface{} `json:"value"`
}
// We received a recording request, we'll send it to the motion handler.
type RecordPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
}
// We received a preset position request, we'll request it through onvif and send it back.
type PTZPositionPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
}
// We received a request config request, we'll fetch the current config and send it back.
type RequestConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
}
// We received a update config request, we'll update the current config and send a confirmation back.
type UpdateConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
Config Config `json:"config"`
}
// We received a request SD stream request
type RequestSDStreamPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
}
// We received a request HD stream request
type RequestHDStreamPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
HubKey string `json:"hub_key"` // hub key
SessionID string `json:"session_id"` // session id
SessionDescription string `json:"session_description"` // session description
}
// We received a receive HD candidates request
type ReceiveHDCandidatesPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
SessionID string `json:"session_id"` // session id
Candidate string `json:"candidate"` // candidate
}
type NavigatePTZPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
DeviceId string `json:"device_id"` // device id
Action string `json:"action"` // action
}

View File

@@ -12,4 +12,13 @@ type OnvifActionPTZ struct {
Down int `json:"down" bson:"down"`
Center int `json:"center" bson:"center"`
Zoom float64 `json:"zoom" bson:"zoom"`
X float64 `json:"x" bson:"x"`
Y float64 `json:"y" bson:"y"`
Z float64 `json:"z" bson:"z"`
Preset string `json:"preset" bson:"preset"`
}
type OnvifActionPreset struct {
Name string `json:"name" bson:"name"`
Token string `json:"token" bson:"token"`
}

View File

@@ -45,14 +45,74 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
if err == nil {
if onvifAction.Action == "ptz" {
if onvifAction.Action == "absolute-move" {
// We will move the camera to zero position.
x := ptzAction.X
y := ptzAction.Y
z := ptzAction.Z
// Check which PTZ Space we need to use
functions, _, _ := GetPTZFunctionsFromDevice(configurations)
// Log functions
log.Log.Info("HandleONVIFActions: functions: " + strings.Join(functions, ", "))
// Check if we need to use absolute or continuous move
/*canAbsoluteMove := false
canContinuousMove := false
if len(functions) > 0 {
for _, function := range functions {
if function == "AbsolutePanTiltMove" || function == "AbsoluteZoomMove" {
canAbsoluteMove = true
} else if function == "ContinuousPanTiltMove" || function == "ContinuousZoomMove" {
canContinuousMove = true
}
}
}*/
// Ideally we should be able to use the AbsolutePanTiltMove function, but it looks like
// the current detection through GetPTZFuntionsFromDevice is not working properly. Therefore we will fallback
// on the ContinuousPanTiltMove function which is more compatible with more cameras.
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
} else {
log.Log.Info("HandleONVIFActions (AbsolutePanTitleMoveFake): successfully moved camera")
}
/*if canAbsoluteMove {
err = AbsolutePanTiltMove(device, configurations, token, x, y, z)
if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
}
} else if canContinuousMove {
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
}
}*/
} else if onvifAction.Action == "preset" {
// Execute the preset
preset := ptzAction.Preset
err := GoToPresetFromDevice(device, preset)
if err != nil {
log.Log.Error("HandleONVIFActions (GotoPreset): " + err.Error())
} else {
log.Log.Info("HandleONVIFActions (GotoPreset): successfully moved camera")
}
} else if onvifAction.Action == "ptz" {
if err == nil {
if ptzAction.Center == 1 {
// We will move the camera to zero position.
err := AbsolutePanTiltMove(device, configurations, token, 0, 0)
err := AbsolutePanTiltMove(device, configurations, token, 0, 0, 0)
if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
}
@@ -179,18 +239,83 @@ func GetPTZConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurations
return configurations, err
}
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float32, tilt float32) error {
func GetPositionFromDevice(configuration models.Configuration) (xsd.PTZVector, error) {
var position xsd.PTZVector
// Connect to Onvif device
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
absoluteVector := xsd.Vector2D{
X: float64(pan),
Y: float64(tilt),
// Get token from the first profile
token, err := GetTokenFromProfile(device, 0)
if err == nil {
// Get the PTZ configurations from the device
position, err := GetPosition(device, token)
if err == nil {
return position, err
} else {
log.Log.Error("GetPositionFromDevice: " + err.Error())
return position, err
}
} else {
log.Log.Error("GetPositionFromDevice: " + err.Error())
return position, err
}
} else {
log.Log.Error("GetPositionFromDevice: " + err.Error())
return position, err
}
}
func GetPosition(device *onvif.Device, token xsd.ReferenceToken) (xsd.PTZVector, error) {
// We'll try to receive the PTZ configurations from the server
var status ptz.GetStatusResponse
var position xsd.PTZVector
// Get the PTZ configurations from the device
resp, err := device.CallMethod(ptz.GetStatus{
ProfileToken: token,
})
if err == nil {
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err == nil {
stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GetStatusResponse")
if err != nil {
log.Log.Error("GetPositionFromDevice: " + err.Error())
return position, err
} else {
if err := decodedXML.DecodeElement(&status, et); err != nil {
log.Log.Error("GetPositionFromDevice: " + err.Error())
return position, err
}
}
}
}
position = status.PTZStatus.Position
return position, err
}
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
absolutePantiltVector := xsd.Vector2D{
X: pan,
Y: tilt,
Space: configuration.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace,
}
absoluteZoomVector := xsd.Vector1D{
X: zoom,
Space: configuration.PTZConfiguration.DefaultAbsoluteZoomPositionSpace,
}
res, err := device.CallMethod(ptz.AbsoluteMove{
ProfileToken: token,
Position: xsd.PTZVector{
PanTilt: absoluteVector,
PanTilt: absolutePantiltVector,
Zoom: absoluteZoomVector,
},
})
@@ -199,11 +324,255 @@ func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfiguratio
}
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("AbsoluteMove: " + string(bs))
log.Log.Info("AbsoluteMove: " + string(bs))
return err
}
// This function will simulate the AbsolutePanTiltMove function.
// However the AboslutePanTiltMove function is not working on all cameras.
// So we'll use the ContinuousMove function to simulate the AbsolutePanTiltMove function using the position polling.
func AbsolutePanTiltMoveFake(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
position, err := GetPosition(device, token)
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 && position.PanTilt.Y >= tilt-0.01 && position.PanTilt.Y <= tilt+0.01 && position.Zoom.X >= zoom-0.01 && position.Zoom.X <= zoom+0.01 {
log.Log.Debug("AbsolutePanTiltMoveFake: already at position")
} else {
// The speed of panning, the higher the faster we'll pan the camera
// value is a range between 0 and 1.
speed := 0.6
wait := 100 * time.Millisecond
// We'll move quickly to the position (might be inaccurate)
err = ZoomOutCompletely(device, configuration, token)
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
// Now we'll move a bit slower to make sure we are ok (will be more accurate)
speed = 0.1
wait = 200 * time.Millisecond
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
err = ZoomUntilPosition(device, configuration, token, zoom, speed, wait)
return err
}
return err
}
func ZoomOutCompletely(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken) error {
// Zoom out completely!!!
zoomOut := xsd.Vector1D{
X: -1,
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
}
_, err := device.CallMethod(ptz.ContinuousMove{
ProfileToken: token,
Velocity: xsd.PTZSpeedZoom{
Zoom: zoomOut,
},
})
for {
position, _ := GetPosition(device, token)
if position.Zoom.X == 0 {
break
}
time.Sleep(250 * time.Millisecond)
}
device.CallMethod(ptz.Stop{
ProfileToken: token,
Zoom: true,
})
return err
}
func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 {
} else {
// We'll need to determine if we need to move CW or CCW.
// Check the current position and compare it with the desired position.
directionX := speed
if position.PanTilt.X > pan {
directionX = speed * -1
}
panTiltVector := xsd.Vector2D{
X: directionX,
Y: 0,
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
}
res, err := device.CallMethod(ptz.ContinuousMove{
ProfileToken: token,
Velocity: xsd.PTZSpeedPanTilt{
PanTilt: panTiltVector,
},
})
if err != nil {
log.Log.Error("ContinuousPanTiltMove (Pan): " + err.Error())
}
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("ContinuousPanTiltMove (Pan): " + string(bs))
// While moving we'll check if we reached the desired position.
// or if we overshot the desired position.
// Break after 3seconds
now := time.Now()
for {
position, _ := GetPosition(device, token)
if position.PanTilt.X == -1 || position.PanTilt.X == 1 || (directionX > 0 && position.PanTilt.X >= pan) || (directionX < 0 && position.PanTilt.X <= pan) || (position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01) {
break
}
if time.Since(now) > 3*time.Second {
break
}
time.Sleep(wait)
}
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
if errStop != nil {
log.Log.Error("ContinuousPanTiltMove (Pan): " + errStop.Error())
}
}
return err
}
func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, tilt float64, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005 {
} else {
// We'll need to determine if we need to move CW or CCW.
// Check the current position and compare it with the desired position.
directionY := speed
if position.PanTilt.Y > tilt {
directionY = speed * -1
}
panTiltVector := xsd.Vector2D{
X: 0,
Y: directionY,
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
}
res, err := device.CallMethod(ptz.ContinuousMove{
ProfileToken: token,
Velocity: xsd.PTZSpeedPanTilt{
PanTilt: panTiltVector,
},
})
if err != nil {
log.Log.Error("ContinuousPanTiltMove (Tilt): " + err.Error())
}
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("ContinuousPanTiltMove (Tilt) " + string(bs))
// While moving we'll check if we reached the desired position.
// or if we overshot the desired position.
// Break after 3seconds
now := time.Now()
for {
position, _ := GetPosition(device, token)
if position.PanTilt.Y == -1 || position.PanTilt.Y == 1 || (directionY > 0 && position.PanTilt.Y >= tilt) || (directionY < 0 && position.PanTilt.Y <= tilt) || (position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005) {
break
}
if time.Since(now) > 3*time.Second {
break
}
time.Sleep(wait)
}
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
if errStop != nil {
log.Log.Error("ContinuousPanTiltMove (Tilt): " + errStop.Error())
}
}
return err
}
func ZoomUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005 {
} else {
// We'll need to determine if we need to move CW or CCW.
// Check the current position and compare it with the desired position.
directionZ := speed
if position.Zoom.X > zoom {
directionZ = speed * -1
}
zoomVector := xsd.Vector1D{
X: directionZ,
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
}
res, err := device.CallMethod(ptz.ContinuousMove{
ProfileToken: token,
Velocity: xsd.PTZSpeedZoom{
Zoom: zoomVector,
},
})
if err != nil {
log.Log.Error("ContinuousPanTiltMove (Zoom): " + err.Error())
}
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("ContinuousPanTiltMove (Zoom) " + string(bs))
// While moving we'll check if we reached the desired position.
// or if we overshot the desired position.
// Break after 3seconds
now := time.Now()
for {
position, _ := GetPosition(device, token)
if position.Zoom.X == -1 || position.Zoom.X == 1 || (directionZ > 0 && position.Zoom.X >= zoom) || (directionZ < 0 && position.Zoom.X <= zoom) || (position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005) {
break
}
if time.Since(now) > 3*time.Second {
break
}
time.Sleep(wait)
}
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
if errStop != nil {
log.Log.Error("ContinuousPanTiltMove (Zoom): " + errStop.Error())
}
}
return err
}
func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64) error {
panTiltVector := xsd.Vector2D{
@@ -226,7 +595,7 @@ func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurations
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("ContinuousPanTiltMove: " + string(bs))
time.Sleep(500 * time.Millisecond)
time.Sleep(200 * time.Millisecond)
res, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
@@ -299,6 +668,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
return capabilities
}
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
var presets []models.OnvifActionPreset
var presetsResponse ptz.GetPresetsResponse
// Get token from the first profile
token, err := GetTokenFromProfile(device, 0)
if err == nil {
resp, err := device.CallMethod(ptz.GetPresets{
ProfileToken: token,
})
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err == nil {
stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse")
if err != nil {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
return presets, err
} else {
if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
return presets, err
}
for _, preset := range presetsResponse.Preset {
p := models.OnvifActionPreset{
Name: string(preset.Name),
Token: string(preset.Token),
}
presets = append(presets, p)
}
return presets, err
}
} else {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
}
} else {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
}
return presets, err
}
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
var goToPresetResponse ptz.GotoPresetResponse
// Get token from the first profile
token, err := GetTokenFromProfile(device, 0)
if err == nil {
resp, err := device.CallMethod(ptz.GotoPreset{
ProfileToken: token,
PresetToken: xsd.ReferenceToken(presetName),
})
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err == nil {
stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses")
if err != nil {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
return err
} else {
if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
return err
}
return err
}
} else {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
}
} else {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
}
return err
}
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
xmlBytes := bytes.NewBufferString(xmlBody)
decodedXML := xml.NewDecoder(xmlBytes)

View File

@@ -250,3 +250,105 @@ func DoOnvifZoom(c *gin.Context) {
})
}
}
// GetOnvifPresets godoc
// @Router /api/camera/onvif/presets [post]
// @ID camera-onvif-presets
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF presets for the specific camera.
// @Description Will return the ONVIF presets for the specific camera.
// @Success 200 {object} models.APIResponse
func GetOnvifPresets(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil {
c.JSON(200, gin.H{
"presets": presets,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GoToOnvifPReset godoc
// @Router /api/camera/onvif/gotopreset [post]
// @ID camera-onvif-gotopreset
// @Tags camera
// @Param config body models.OnvifPreset true "OnvifPreset"
// @Summary Will activate the desired ONVIF preset.
// @Description Will activate the desired ONVIF preset.
// @Success 200 {object} models.APIResponse
func GoToOnvifPreset(c *gin.Context) {
var onvifPreset models.OnvifPreset
err := c.BindJSON(&onvifPreset)
if err == nil && onvifPreset.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifPreset.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifPreset.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifPreset.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
if err == nil {
c.JSON(200, gin.H{
"data": "Camera preset activated: " + onvifPreset.Preset,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}

View File

@@ -12,12 +12,13 @@ import (
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/components"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils"
)
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirectory string, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, communication)
@@ -40,7 +41,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(config, configuration, communication)
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
@@ -78,7 +79,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
}
// The total number of recordings stored in the directory.
recordingDirectory := "./data/recordings"
recordingDirectory := configDirectory + "/data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent.
@@ -115,7 +116,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10
}
recordingDirectory := "./data/recordings"
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
@@ -137,7 +138,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
})
api.GET("/days", func(c *gin.Context) {
recordingDirectory := "./data/recordings"
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
@@ -165,7 +166,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(config, configuration, communication)
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
@@ -205,7 +206,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
})
api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c)
cloud.VerifyPersistence(c, configDirectory)
})
// Streaming handler
@@ -215,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
// We will only send an image once per second.
time.Sleep(time.Second * 1)
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
img, err := components.GetImageFromFilePath()
img, err := configService.GetImageFromFilePath(configDirectory)
return img, err
}
h := components.StartMotionJPEG(imageFunction, 80)
@@ -227,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
// the camera.
api.POST("/camera/onvif/login", LoginToOnvif)
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
api.POST("/camera/onvif/presets", GetOnvifPresets)
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
api.POST("/camera/onvif/zoom", DoOnvifZoom)
api.POST("/camera/verify/:streamType", capture.VerifyCamera)

View File

@@ -1,7 +1,9 @@
package http
import (
"io"
"os"
"strconv"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-contrib/pprof"
@@ -12,6 +14,7 @@ import (
"log"
_ "github.com/kerberos-io/agent/machinery/docs"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/models"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
@@ -35,7 +38,7 @@ import (
// @in header
// @name Authorization
func StartServer(configuration *models.Configuration, communication *models.Communication) {
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
// Initialize REST API
r := gin.Default()
@@ -57,12 +60,12 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
// Add all routes
AddRoutes(r, authMiddleware, configuration, communication)
AddRoutes(r, authMiddleware, configDirectory, configuration, communication)
// Update environment variables
environmentVariables := "./www/env.js"
environmentVariables := configDirectory + "/www/env.js"
if os.Getenv("AGENT_MODE") == "demo" {
demoEnvironmentVariables := "./www/env.demo.js"
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
// Move demo environment variables to environment variables
err := os.Rename(demoEnvironmentVariables, environmentVariables)
if err != nil {
@@ -71,12 +74,14 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
// Add static routes to UI
r.Use(static.Serve("/", static.LocalFile("./www", true)))
r.Use(static.Serve("/dashboard", static.LocalFile("./www", true)))
r.Use(static.Serve("/media", static.LocalFile("./www", true)))
r.Use(static.Serve("/settings", static.LocalFile("./www", true)))
r.Use(static.Serve("/login", static.LocalFile("./www", true)))
r.Handle("GET", "/file/*filepath", Files)
r.Use(static.Serve("/", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/dashboard", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/media", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/settings", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/login", static.LocalFile(configDirectory+"/www", true)))
r.Handle("GET", "/file/*filepath", func(c *gin.Context) {
Files(c, configDirectory, configuration)
})
// Run the api on port
err = r.Run(":" + configuration.Port)
@@ -85,8 +90,50 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
}
func Files(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Content-Type", "video/mp4")
c.File("./data/recordings" + c.Param("filepath"))
func Files(c *gin.Context, configDirectory string, configuration *models.Configuration) {
// Get File
filePath := configDirectory + "/data/recordings" + c.Param("filepath")
_, err := os.Open(filePath)
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
contents, err := os.ReadFile(filePath)
if err == nil {
// Get symmetric key
symmetricKey := configuration.Config.Encryption.SymmetricKey
// Decrypt file
if symmetricKey != "" {
// Read file
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
// Decrypt file
contents, err = encryption.AesDecrypt(contents, symmetricKey)
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
}
// Get fileSize from contents
fileSize := len(contents)
// Send file to gin
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Content-Disposition", "attachment; filename="+filePath)
c.Header("Content-Type", "video/mp4")
c.Header("Content-Length", strconv.Itoa(fileSize))
// Send contents to gin
io.WriteString(c.Writer, string(contents))
} else {
c.JSON(404, gin.H{"error": "File not found"})
return
}
}

View File

@@ -5,6 +5,6 @@ import (
"github.com/kerberos-io/agent/machinery/src/routers/http"
)
func StartWebserver(configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configuration, communication)
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configDirectory, configuration, communication)
}

View File

@@ -1,15 +1,24 @@
package mqtt
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"math/rand"
"strconv"
"strings"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/webrtc"
)
@@ -34,7 +43,18 @@ func HasMQTTClientModified(configuration *models.Configuration) bool {
return false
}
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client {
// Configuring MQTT to subscribe for various bi-directional messaging
// Listen and reply (a generic method to share and retrieve information)
//
// - [SUBSCRIPTION] kerberos/agent/{hubkey} (hub -> agent)
// - [PUBLISH] kerberos/hub/{hubkey} (agent -> hub)
//
// !!! LEGACY METHODS BELOW, WE SHOULD LEVERAGE THE ABOVE METHOD!
// [PUBlISH]
// Next to subscribing to various topics, we'll also publish messages to various topics, find a list of available Publish methods.
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
config := configuration.Config
@@ -109,23 +129,8 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
// We managed to connect to the MQTT broker, hurray!
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
// Create a subscription to know if send out a livestream or not.
MQTTListenerHandleLiveSD(c, hubKey, configuration, communication)
// Create a subscription for the WEBRTC livestream.
MQTTListenerHandleLiveHDHandshake(c, hubKey, configuration, communication)
// Create a subscription for keeping alive the WEBRTC livestream.
MQTTListenerHandleLiveHDKeepalive(c, hubKey, configuration, communication)
// Create a subscription to listen to the number of WEBRTC peers.
MQTTListenerHandleLiveHDPeers(c, hubKey, configuration, communication)
// Create a subscription to listen for WEBRTC candidates.
MQTTListenerHandleLiveHDCandidates(c, hubKey, configuration, communication)
// Create a susbcription to listen for ONVIF actions: e.g. PTZ, Zoom, etc.
MQTTListenerHandleONVIF(c, hubKey, configuration, communication)
// Create a susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
}
}
mqc := mqtt.NewClient(opts)
@@ -140,119 +145,352 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
return nil
}
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
select {
case communication.HandleLiveSD <- time.Now().Unix():
default:
}
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
} else {
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.")
}
msg.Ack()
})
}
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequestWebRtc := config.Key + "/register"
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var sdp models.SDPPayload
json.Unmarshal(msg.Payload(), &sdp)
select {
case communication.HandleLiveHDHandshake <- sdp:
default:
}
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
} else {
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.")
}
msg.Ack()
})
}
// Decode the message, we are expecting following format.
// {
// mid: string, "unique id for the message"
// timestamp: int64, "unix timestamp when the message was generated"
// encrypted: boolean,
// fingerprint: string, "fingerprint of the message to validate authenticity"
// payload: Payload, "a json object which might be encrypted"
// }
func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
alive := string(msg.Payload())
communication.HandleLiveHDKeepalive <- alive
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
} else {
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
}
})
}
var message models.Message
json.Unmarshal(msg.Payload(), &message)
func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
peerCount := string(msg.Payload())
communication.HandleLiveHDPeers <- peerCount
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
} else {
log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.")
}
})
}
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
if message.Mid != "" && message.Timestamp != 0 && message.DeviceId == configuration.Config.Key {
// Messages might be encrypted, if so we'll
// need to decrypt them.
var payload models.Payload
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
encryptedValue := message.Payload.EncryptedValue
if len(encryptedValue) > 0 {
symmetricKey := configuration.Config.Encryption.SymmetricKey
privateKey := configuration.Config.Encryption.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := ioutil.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
return
} else {
// Parse private key
b := block.Bytes
key, err := x509.ParsePKCS8PrivateKey(b)
if err != nil {
log.Log.Error("MQTTListenerHandler: error parsing private key: " + err.Error())
return
} else {
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicCandidates := "candidate/cloud"
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var candidate models.Candidate
json.Unmarshal(msg.Payload(), &candidate)
if candidate.CloudKey == config.Key {
key := candidate.CloudKey + "/" + candidate.Cuuid
candidatesExists := false
var channel chan string
for !candidatesExists {
webrtc.CandidatesMutex.Lock()
channel, candidatesExists = webrtc.CandidateArrays[key]
webrtc.CandidatesMutex.Unlock()
// Get encrypted key from message, delimited by :::
encryptedKey := strings.Split(encryptedValue, ":::")[0] // encrypted with RSA
encryptedValue := strings.Split(encryptedValue, ":::")[1] // encrypted with AES
// Convert encrypted value to []byte
decryptedKey, err := encryption.DecryptWithPrivateKey(encryptedKey, rsaKey)
if decryptedKey != nil {
if string(decryptedKey) == symmetricKey {
// Decrypt value with decryptedKey
data, err := base64.StdEncoding.DecodeString(encryptedValue)
if err != nil {
return
}
decryptedValue, err := encryption.AesDecrypt(data, string(decryptedKey))
if err != nil {
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
return
}
json.Unmarshal(decryptedValue, &payload)
} else {
log.Log.Error("MQTTListenerHandler: error decrypting message, assymetric keys do not match.")
return
}
} else if err != nil {
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
return
}
}
}
}
} else {
payload = message.Payload
}
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
channel <- string(msg.Payload())
// We'll find out which message we received, and act accordingly.
log.Log.Info("MQTTListenerHandler: received message with action: " + payload.Action)
switch payload.Action {
case "record":
go HandleRecording(mqttClient, hubKey, payload, configuration, communication)
case "get-ptz-position":
go HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "update-ptz-position":
go HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "navigate-ptz":
go HandleNavigatePTZ(mqttClient, hubKey, payload, configuration, communication)
case "request-config":
go HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
case "update-config":
go HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
case "request-sd-stream":
go HandleRequestSDStream(mqttClient, hubKey, payload, configuration, communication)
case "request-hd-stream":
go HandleRequestHDStream(mqttClient, hubKey, payload, configuration, communication)
case "receive-hd-candidates":
go HandleReceiveHDCandidates(mqttClient, hubKey, payload, configuration, communication)
}
}
} else {
log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
}
})
})
}
}
func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RecordPayload
jsonData, _ := json.Marshal(value)
var recordPayload models.RecordPayload
json.Unmarshal(jsonData, &recordPayload)
if recordPayload.Timestamp != 0 {
motionDataPartial := models.MotionDataPartial{
Timestamp: recordPayload.Timestamp,
}
communication.HandleMotion <- motionDataPartial
}
}
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var positionPayload models.PTZPositionPayload
json.Unmarshal(jsonData, &positionPayload)
if positionPayload.Timestamp != 0 {
// Get Position from device
pos, err := onvif.GetPositionFromDevice(*configuration)
if err != nil {
log.Log.Error("HandlePTZPosition: error getting position from device: " + err.Error())
} else {
// Needs to wrapped!
posString := fmt.Sprintf("%f,%f,%f", pos.PanTilt.X, pos.PanTilt.Y, pos.Zoom.X)
message := models.Message{
Payload: models.Payload{
Action: "ptz-position",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
"timestamp": positionPayload.Timestamp,
"position": posString,
},
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandlePTZPosition: something went wrong while sending position to hub: " + string(payload))
}
}
}
}
func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var onvifAction models.OnvifAction
json.Unmarshal(jsonData, &onvifAction)
if onvifAction.Action != "" {
if communication.CameraConnected {
var onvifAction models.OnvifAction
json.Unmarshal(msg.Payload(), &onvifAction)
communication.HandleONVIF <- onvifAction
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
} else {
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
}
})
}
}
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload models.RequestConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
// Get Config from the device
key := configuration.Config.Key
name := configuration.Config.Name
if key != "" && name != "" {
// Copy the config, as we don't want to share the encryption part.
deepCopy := configuration.Config
var configMap map[string]interface{}
inrec, _ := json.Marshal(deepCopy)
json.Unmarshal(inrec, &configMap)
// Unset encryption part.
delete(configMap, "encryption")
message := models.Message{
Payload: models.Payload{
Action: "receive-config",
DeviceId: configuration.Config.Key,
Value: configMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleRequestConfig: no config available")
}
log.Log.Info("HandleRequestConfig: Received a request for the config")
}
}
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to UpdateConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload models.UpdateConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
config := configPayload.Config
// Make sure to remove Encryption part, as we don't want to save it.
config.Encryption = configuration.Config.Encryption
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
log.Log.Info("HandleUpdateConfig: Config updated")
message := models.Message{
Payload: models.Payload{
Action: "acknowledge-update-config",
DeviceId: configuration.Config.Key,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleUpdateConfig: Config update failed")
}
}
}
func HandleRequestSDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestSDStreamPayload
jsonData, _ := json.Marshal(value)
var requestSDStreamPayload models.RequestSDStreamPayload
json.Unmarshal(jsonData, &requestSDStreamPayload)
if requestSDStreamPayload.Timestamp != 0 {
if communication.CameraConnected {
select {
case communication.HandleLiveSD <- time.Now().Unix():
default:
}
log.Log.Info("HandleRequestSDStream: received request to livestream.")
} else {
log.Log.Info("HandleRequestSDStream: received request to livestream, but camera is not connected.")
}
}
}
func HandleRequestHDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestHDStreamPayload
jsonData, _ := json.Marshal(value)
var requestHDStreamPayload models.RequestHDStreamPayload
json.Unmarshal(jsonData, &requestHDStreamPayload)
if requestHDStreamPayload.Timestamp != 0 {
if communication.CameraConnected {
// Set the Hub key, so we can send back the answer.
requestHDStreamPayload.HubKey = hubKey
select {
case communication.HandleLiveHDHandshake <- requestHDStreamPayload:
default:
}
log.Log.Info("HandleRequestHDStream: received request to setup webrtc.")
} else {
log.Log.Info("HandleRequestHDStream: received request to setup webrtc, but camera is not connected.")
}
}
}
func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to ReceiveHDCandidatesPayload
jsonData, _ := json.Marshal(value)
var receiveHDCandidatesPayload models.ReceiveHDCandidatesPayload
json.Unmarshal(jsonData, &receiveHDCandidatesPayload)
if receiveHDCandidatesPayload.Timestamp != 0 {
if communication.CameraConnected {
channel := webrtc.CandidateArrays[receiveHDCandidatesPayload.SessionID]
log.Log.Info("HandleReceiveHDCandidates: " + receiveHDCandidatesPayload.Candidate)
channel <- receiveHDCandidatesPayload.Candidate
} else {
log.Log.Info("HandleReceiveHDCandidates: received candidate, but camera is not connected.")
}
}
}
func HandleNavigatePTZ(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
jsonData, _ := json.Marshal(value)
var navigatePTZPayload models.NavigatePTZPayload
json.Unmarshal(jsonData, &navigatePTZPayload)
if navigatePTZPayload.Timestamp != 0 {
if communication.CameraConnected {
action := navigatePTZPayload.Action
var onvifAction models.OnvifAction
json.Unmarshal([]byte(action), &onvifAction)
communication.HandleONVIF <- onvifAction
log.Log.Info("HandleNavigatePTZ: Received an action - " + onvifAction.Action)
} else {
log.Log.Info("HandleNavigatePTZ: received action, but camera is not connected.")
}
}
}
func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
if mqttClient != nil {
// Cleanup all subscriptions
mqttClient.Unsubscribe("kerberos/" + PREV_HubKey + "/device/" + PREV_AgentKey + "/request-live")
mqttClient.Unsubscribe(PREV_AgentKey + "/register")
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + PREV_AgentKey)
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + PREV_AgentKey)
mqttClient.Unsubscribe("candidate/cloud")
mqttClient.Unsubscribe("kerberos/onvif/" + PREV_AgentKey)
// New methods
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
mqttClient.Disconnect(1000)
mqttClient = nil
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")

View File

@@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
@@ -110,15 +111,15 @@ func CountDigits(i int64) (count int) {
return count
}
func CheckDataDirectoryPermissions() error {
recordingsDirectory := "./data/recordings"
configDirectory := "./data/config"
snapshotsDirectory := "./data/snapshots"
cloudDirectory := "./data/cloud"
func CheckDataDirectoryPermissions(configDirectory string) error {
recordingsDirectory := configDirectory + "/data/recordings"
configurationDirectory := configDirectory + "/data/config"
snapshotsDirectory := configDirectory + "/data/snapshots"
cloudDirectory := configDirectory + "/data/cloud"
err := CheckDirectoryPermissions(recordingsDirectory)
if err == nil {
err = CheckDirectoryPermissions(configDirectory)
err = CheckDirectoryPermissions(configurationDirectory)
if err == nil {
err = CheckDirectoryPermissions(snapshotsDirectory)
if err == nil {
@@ -330,3 +331,67 @@ func PrintConfiguration(configuration *models.Configuration) {
}
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
}
func Decrypt(directoryOrFile string, symmetricKey []byte) {
// Check if file or directory
fileInfo, err := os.Stat(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
var files []string
if fileInfo.IsDir() {
// Create decrypted directory
err = os.MkdirAll(directoryOrFile+"/decrypted", 0755)
if err != nil {
log.Log.Fatal(err.Error())
return
}
dir, err := os.ReadDir(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
for _, file := range dir {
// Check if file is not a directory
if !file.IsDir() {
// Check if an mp4 file
if strings.HasSuffix(file.Name(), ".mp4") {
files = append(files, directoryOrFile+"/"+file.Name())
}
}
}
} else {
files = append(files, directoryOrFile)
}
// We'll loop over all files and decrypt them one by one.
for _, file := range files {
// Read file
content, err := os.ReadFile(file)
if err != nil {
log.Log.Fatal(err.Error())
return
}
// Decrypt using AES key
decrypted, err := encryption.AesDecrypt(content, string(symmetricKey))
if err != nil {
log.Log.Fatal("Something went wrong while decrypting: " + err.Error())
return
}
// Write decrypted content to file with appended .decrypted
// Get filename split by / and get last element.
fileParts := strings.Split(file, "/")
fileName := fileParts[len(fileParts)-1]
pathToFile := strings.Join(fileParts[:len(fileParts)-1], "/")
err = os.WriteFile(pathToFile+"/decrypted/"+fileName, []byte(decrypted), 0644)
if err != nil {
log.Log.Fatal(err.Error())
return
}
}
}

View File

@@ -87,19 +87,22 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
return offer
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload, candidates chan string) {
config := configuration.Config
name := config.Key
deviceKey := config.Key
stunServers := []string{config.STUNURI}
turnServers := []string{config.TURNURI}
turnServersUsername := config.TURNUsername
turnServersCredential := config.TURNPassword
// Set variables
hubKey := handshake.HubKey
sessionDescription := handshake.SessionDescription
// Create WebRTC object
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(handshake.Sdp)
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(sessionDescription)
if err == nil {
@@ -122,7 +125,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
Credential: w.TurnServersCredential,
},
},
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
},
)
@@ -143,7 +145,7 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
atomic.AddInt64(&peerConnectionCount, -1)
peerConnections[handshake.Cuuid] = nil
peerConnections[handshake.SessionID] = nil
close(candidates)
close(w.PacketsCount)
if err := peerConnection.Close(); err != nil {
@@ -152,9 +154,12 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
atomic.AddInt64(&peerConnectionCount, 1)
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
// Iterate over the candidates and send them to the remote client
// Non blocking channel
for candidate := range candidates {
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
log.Log.Error("InitializeWebRTCConnection: something went wrong while adding candidate: " + candidateErr.Error())
}
}
}
@@ -167,7 +172,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
panic(err)
}
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
@@ -175,37 +179,64 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
panic(err)
}
// When an ICE candidate is available send to the other Pion instance
// the other Pion instance will add this candidate by calling AddICECandidate
var candidatesMux sync.Mutex
// When an ICE candidate is available send to the other peer using the signaling server (MQTT).
// The other peer will add this candidate by calling AddICECandidate
peerConnection.OnICECandidate(func(candidate *pionWebRTC.ICECandidate) {
if candidate == nil {
return
}
candidatesMux.Lock()
defer candidatesMux.Unlock()
topic := fmt.Sprintf("%s/%s/candidate/edge", name, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
candiInit := candidate.ToJSON()
// Create a config map
valueMap := make(map[string]interface{})
candateJSON := candidate.ToJSON()
sdpmid := "0"
candiInit.SDPMid = &sdpmid
candi, err := json.Marshal(candiInit)
candateJSON.SDPMid = &sdpmid
candateBinary, err := json.Marshal(candateJSON)
if err == nil {
log.Log.Info("InitializeWebRTCConnection:" + string(candi))
token := mqttClient.Publish(topic, 2, false, candi)
token.Wait()
valueMap["candidate"] = string(candateBinary)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while marshalling candidate: " + err.Error())
}
// We'll send the candidate to the hub
message := models.Message{
Payload: models.Payload{
Action: "receive-hd-candidates",
DeviceId: configuration.Config.Key,
Value: valueMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
})
peerConnections[handshake.Cuuid] = peerConnection
// Create a channel which will be used to send candidates to the other peer
peerConnections[handshake.SessionID] = peerConnection
if err == nil {
topic := fmt.Sprintf("%s/%s/answer", name, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
// Create a config map
valueMap := make(map[string]interface{})
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
log.Log.Info("InitializeWebRTCConnection: Send SDP answer")
// We'll send the candidate to the hub
message := models.Message{
Payload: models.Payload{
Action: "receive-hd-answer",
DeviceId: configuration.Config.Key,
Value: valueMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
}
}
} else {
@@ -358,16 +389,9 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
pkt.Data = append(codecData.(h264parser.CodecData).SPS(), pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
log.Log.Info("WriteToTrack: Sending keyframe")
if config.Capture.ForwardWebRTC == "true" {
log.Log.Info("WriteToTrack: Sending keep a live to remote broker.")
topic := fmt.Sprintf("kerberos/webrtc/keepalive/%s", config.Key)
mqttClient.Publish(topic, 2, false, "1")
}
}
if start {
sample := pionMedia.Sample{Data: pkt.Data, Duration: bufferDuration}
if config.Capture.ForwardWebRTC == "true" {
samplePacket, err := json.Marshal(sample)

6
snap/hooks/configure vendored Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh -e
cp -R $SNAP/data $SNAP_COMMON/
cp -R $SNAP/www $SNAP_COMMON/
cp -R $SNAP/version $SNAP_COMMON/
cp -R $SNAP/mp4fragment $SNAP_COMMON/

23
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: kerberosio # you probably want to 'snapcraft register <name>'
base: core22 # the base snap is the execution environment for this snap
version: '3.0.0' # just for humans, typically '1.2+git' or '1.3.2'
summary: A stand-alone open source video surveillance system # 79 char long summary
description: |
Kerberos Agent is an isolated and scalable video (surveillance) management
agent made available as Open Source under the MIT License. This means that
all the source code is available for you or your company, and you can use,
transform and distribute the source code; as long you keep a reference of
the original license. Kerberos Agent can be used for commercial usage.
grade: stable # stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
environment:
GIN_MODE: release
apps:
agent:
command: main -config /var/snap/kerberosio/common
plugs: [ network, network-bind ]
parts:
agent:
source: . #https://github.com/kerberos-io/agent/releases/download/21c0e01/agent-amd64.tar
plugin: dump

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Erweiterte Konfiguration",
"description_advanced_configuration": "Erweiterte Einstellungen um Funktionen des Kerberos Agent zu aktivieren oder deaktivieren",
"offline_mode": "Offline Modus",
"description_offline_mode": "Ausgehende Verbindungen deaktivieren"
"description_offline_mode": "Ausgehende Verbindungen deaktivieren",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Kamera",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",

View File

@@ -84,7 +84,16 @@
"advanced_configuration": "Configuration avancée",
"description_advanced_configuration": "Les options de configuration détaillées pour activer ou désactiver des composants spécifiques de l'Agent Kerberos",
"offline_mode": "Mode hors-ligne",
"description_offline_mode": "Désactiver tout le trafic sortant"
"description_offline_mode": "Désactiver tout le trafic sortant",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Caméra",

View File

@@ -0,0 +1,224 @@
{
"breadcrumb": {
"watch_recordings": "रिकॉर्डिंग देखें",
"configure": "कॉन्फ़िगर"
},
"buttons": {
"save": "सेव्ह",
"verify_connection": "कनेक्शन चेक करें"
},
"navigation": {
"profile": "प्रोफ़ाइल",
"admin": "व्यवस्थापक",
"management": "प्रबंध",
"dashboard": "डैशबोर्ड",
"recordings": "रिकॉर्डिंग",
"settings": "सेटिंग",
"help_support": "मदद",
"swagger": "स्वैगर एपीआई",
"documentation": "प्रलेखन",
"ui_library": "यूआई लाइब्रेरी",
"layout": "भाषा और लेआऊट",
"choose_language": "भाषा चुनें"
},
"dashboard": {
"title": "डैशबोर्ड",
"heading": "आपके वीडियो निगरानी का अवलोकन",
"number_of_days": "दिनों की संख्या",
"total_recordings": "कुल रिकॉर्डिंग",
"connected": "जुड़े है",
"not_connected": "जुड़े नहीं हैं",
"offline_mode": "ऑफ़लाइन मोड",
"latest_events": "नवीनतम घटनाए",
"configure_connection": "कनेक्शन कॉन्फ़िगर करें",
"no_events": "कोई घटनाए नहीं",
"no_events_description": "कोई रिकॉर्डिंग नहीं मिली, सुनिश्चित करें कि आपका Kerberos एजेंट ठीक से कॉन्फ़िगर किया गया है।",
"motion_detected": "मोशन का पता चला",
"live_view": "लाइव देखें",
"loading_live_view": "लाइव दृश्य लोड हो रहा है",
"loading_live_view_description": "रुकिए हम आपका लाइव व्यू यहां लोड कर रहे हैं। ",
"time": "समय",
"description": "विवरण",
"name": "नाम"
},
"recordings": {
"title": "रिकॉर्डिंग",
"heading": "आपकी सभी रिकॉर्डिंग एक ही स्थान पर",
"search_media": "मीडिया खोजें"
},
"settings": {
"title": "सेटिंग",
"heading": "अपना कैमरा ऑनबोर्ड करें",
"submenu": {
"all": "सभी",
"overview": "अवलोकन",
"camera": "कैमरा",
"recording": "रिकॉर्डिंग",
"streaming": "स्ट्रीमिंग",
"conditions": "कंडीशन",
"persistence": "परसीस्टेन्स"
},
"info": {
"kerberos_hub_demo": "Kerberos हब को क्रियाशील देखने के लिए हमारे Kerberos हब डेमो पर एक नज़र डालें!",
"configuration_updated_success": "आपका कॉन्फ़िगरेशन सफलतापूर्वक अपडेट कर दिया गया है.",
"configuration_updated_error": "सहेजते समय कुछ ग़लत हो गया.",
"verify_hub": "अपनी Kerberos हब सेटिंग सत्यापित की जा रही है।",
"verify_hub_success": "कर्बेरोस हब सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_hub_error": "कर्बरोस हब का सत्यापन करते समय कुछ गलत हो गया",
"verify_persistence": "आपकी दृढ़ता सेटिंग सत्यापित की जा रही है.",
"verify_persistence_success": "दृढ़ता सेटिंग्स सफलतापूर्वक सत्यापित की गई हैं।",
"verify_persistence_error": "दृढ़ता की पुष्टि करते समय कुछ गलत हो गया",
"verify_camera": "अपनी कैमरा सेटिंग सत्यापित कर रहा है।",
"verify_camera_success": "कैमरा सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_camera_error": "कैमरा सेटिंग्स सत्यापित करते समय कुछ गलत हो गया",
"verify_onvif": "अपनी ONVIF सेटिंग्स सत्यापित कर रहा हूँ।",
"verify_onvif_success": "ONVIF सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_onvif_error": "ONVIF सेटिंग्स सत्यापित करते समय कुछ गलत हो गया"
},
"overview": {
"general": "सामान्य",
"description_general": "आपके Kerberos एजेंट के लिए सामान्य सेटिंग्स",
"key": "की",
"camera_name": "कैमरे का नाम",
"timezone": "समय क्षेत्र",
"select_timezone": "समयक्षेत्र चुनें",
"advanced_configuration": "एडवांस कॉन्फ़िगरेशन",
"description_advanced_configuration": "Kerberos एजेंट के विशिष्ट भागों को सक्षम या अक्षम करने के लिए विस्तृत कॉन्फ़िगरेशन विकल्प",
"offline_mode": "ऑफ़लाइन मोड",
"description_offline_mode": "सभी आउटगोइंग ट्रैफ़िक अक्षम करें",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "कैमरा",
"description_camera": "आपकी पसंद के कैमरे से कनेक्शन बनाने के लिए कैमरा सेटिंग्स की आवश्यकता होती है।",
"only_h264": "वर्तमान में केवल H264 RTSP स्ट्रीम समर्थित हैं।",
"rtsp_url": "RTSP URL",
"rtsp_h264": "आपके कैमरे से H264 RTSP कनेक्शन।",
"sub_rtsp_url": "दुसरी RTSP URL (लाइवस्ट्रीमिंग के लिए प्रयुक्त)",
"sub_rtsp_h264": "आपके कैमरे के कम रिज़ॉल्यूशन के लिए एक दुसरी RTSP कनेक्शन।",
"onvif": "ONVIF",
"description_onvif": "ONVIF क्षमताओं के साथ संचार करने के लिए क्रेडेन्शियल। ",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF उपयोक्तानाम",
"onvif_password": "ओएनवीआईएफ पासवर्ड",
"verify_connection": "कनेक्शन सत्यापित करें",
"verify_sub_connection": "उप कनेक्शन सत्यापित करें"
},
"recording": {
"recording": "रिकॉर्डिंग",
"description_recording": "निर्दिष्ट करें कि आप रिकॉर्डिंग कैसे करना चाहेंगे. ",
"continuous_recording": "लगातार रिकॉर्डिंग",
"description_continuous_recording": "24/7 या गति आधारित रिकॉर्डिंग करें।",
"max_duration": "अधिकतम वीडियो अवधि (सेकंड)",
"description_max_duration": "रिकॉर्डिंग की अधिकतम अवधि.",
"pre_recording": "पूर्व रिकॉर्डिंग (key frames buffered)",
"description_pre_recording": "किसी घटना के घटित होने से सेकंड पहले.",
"post_recording": "पोस्ट रिकॉर्डिंग (सेकंड)",
"description_post_recording": "किसी घटना के घटित होने के सेकंड बाद.",
"threshold": "रिकॉर्डिंग सीमा (पिक्सेल)",
"description_threshold": "रिकॉर्ड करने के लिए पिक्सेल की संख्या बदल दी गई",
"autoclean": "अपने आप क्लीन करे",
"description_autoclean": "निर्दिष्ट करें कि क्या Kerberos एजेंट एक विशिष्ट क्षमता (एमबी) तक पहुंचने पर रिकॉर्डिंग को क्लीन कर सकता है। ",
"autoclean_enable": "स्वतः क्लीन सक्षम करें",
"autoclean_description_enable": "क्षमता पूरी होने पर सबसे पुरानी रिकॉर्डिंग हटा दें।",
"autoclean_max_directory_size": "अधिकतम डिरेक्टरी आकार (एमबी)",
"autoclean_description_max_directory_size": "संग्रहीत रिकॉर्डिंग की अधिकतम एमबी।",
"fragmentedrecordings": "खंडित रिकॉर्डिंग",
"description_fragmentedrecordings": "जब रिकॉर्डिंग खंडित हो जाती हैं तो वे HLS स्ट्रीम के लिए उपयुक्त होती हैं। ",
"fragmentedrecordings_enable": "विखंडन सक्षम करें",
"fragmentedrecordings_description_enable": "HLS के लिए खंडित रिकॉर्डिंग आवश्यक हैं।",
"fragmentedrecordings_duration": "खंड अवधि",
"fragmentedrecordings_description_duration": "एक टुकड़े की अवधि."
},
"streaming": {
"stun_turn": "WebRTC के लिए STUN/TURN",
"description_stun_turn": "पूर्ण-रिज़ॉल्यूशन लाइवस्ट्रीमिंग के लिए हम WebRTC की अवधारणा का उपयोग करते हैं। ",
"stun_server": "STUN server",
"turn_server": "TURN server",
"turn_username": "उपयोगकर्ता नाम",
"turn_password": "पासवर्ड",
"stun_turn_forward": "फोरवर्डींग और ट्रांसकोडिंग",
"stun_turn_description_forward": "TURN/STUN संचार के लिए अनुकूलन और संवर्द्धन।",
"stun_turn_webrtc": "WebRTC ब्रोकर को फोरवर्डींग किया जा रहा है",
"stun_turn_description_webrtc": "MQTT के माध्यम से h264 स्ट्रीम को फोरवर्डींग करें",
"stun_turn_transcode": "ट्रांसकोड स्ट्रीम",
"stun_turn_description_transcode": "स्ट्रीम को कम रिज़ॉल्यूशन में बदलें",
"stun_turn_downscale": "डाउनस्केल रिज़ॉल्यूशन (% या मूल रिज़ॉल्यूशन में)",
"mqtt": "MQTT",
"description_mqtt": "एक MQTT ब्रोकर का उपयोग काम्युनिकेट करने के लिए किया जाता है",
"description2_mqtt": "उदाहरण के लिए लाइवस्ट्रीमिंग या ONVIF (PTZ) क्षमताओं को प्राप्त करने के लिए Kerberos एजेंट को।",
"mqtt_brokeruri": "Broker Uri",
"mqtt_username": "उपयोगकर्ता नाम",
"mqtt_password": "पासवर्ड"
},
"conditions": {
"timeofinterest": "रुचि का समय",
"description_timeofinterest": "रिकॉर्डिंग केवल विशिष्ट समय अंतराल (समय क्षेत्र के आधार पर) के बीच करें।",
"timeofinterest_enabled": "सक्रिय",
"timeofinterest_description_enabled": "सक्षम होने पर आप समय विंडो निर्दिष्ट कर सकते हैं",
"sunday": "रविवार",
"monday": "सोमवार",
"tuesday": "मंगलवार",
"wednesday": "बुधवार",
"thursday": "गुरुवार",
"friday": "शुक्रवार",
"saturday": "शनिवार",
"externalcondition": "बाह्य स्थिति",
"description_externalcondition": "बाहरी वेबसेवा के आधार पर रिकॉर्डिंग को सक्षम या अक्षम किया जा सकता है।",
"regionofinterest": "दिलचस्पी के क्षेत्र",
"description_regionofinterest": "एक या अधिक क्षेत्रों को परिभाषित करने से, गति को केवल आपके द्वारा परिभाषित क्षेत्रों में ही ट्रैक किया जाएगा।"
},
"persistence": {
"kerberoshub": "Kerberos हब",
"description_kerberoshub": "Kerberos एजेंट दिल की धड़कनों को सेंट्रल में भेज सकते हैं",
"description2_kerberoshub": "आपके वीडियो परिदृश्य के बारे में वास्तविक समय की जानकारी दिखाने के लिए दिल की धड़कन और अन्य प्रासंगिक जानकारी को केर्बरोस हब से समन्वयित किया जाता है।",
"persistence": "अटलता",
"saasoffering": "Kerberos हब (SAAS offering)",
"description_persistence": "अपनी रिकॉर्डिंग संग्रहीत करने की क्षमता होना हर चीज़ की शुरुआत है। ",
"description2_persistence": ", या कोई तृतीय पक्ष प्रदाता",
"select_persistence": "एक दृढ़ता का चयन करें",
"kerberoshub_proxyurl": "Kerberos हब प्रॉक्सी URL",
"kerberoshub_description_proxyurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए प्रॉक्सी एंडपॉइंट।",
"kerberoshub_apiurl": "Kerberos हब API URL",
"kerberoshub_description_apiurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए API एंडपॉइंट।",
"kerberoshub_publickey": "सार्वजनिक की",
"kerberoshub_description_publickey": "आपके Kerberos हब खाते को दी गई सार्वजनिक की।",
"kerberoshub_privatekey": "निजी चाबी",
"kerberoshub_description_privatekey": "आपके Kerberos हब खाते को दी गई निजी की।",
"kerberoshub_site": "साइट",
"kerberoshub_description_site": "साइट आईडी Kerberos एजेंट Kerberos हब से संबंधित हैं।",
"kerberoshub_region": "क्षेत्र",
"kerberoshub_description_region": "जिस क्षेत्र में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
"kerberoshub_bucket": "बकेट",
"kerberoshub_description_bucket": "जिस बकेट में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
"kerberoshub_username": "उपयोगकर्ता नाम/निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
"kerberoshub_description_username": "आपके Kerberos हब खाते का उपयोगकर्ता नाम।",
"kerberosvault_apiurl": "Kerberos वॉल्ट API URL",
"kerberosvault_description_apiurl": "कर्बरोस वॉल्ट एपीआई",
"kerberosvault_provider": "प्रदाता",
"kerberosvault_description_provider": "वह प्रदाता जिसे आपकी रिकॉर्डिंग भेजी जाएगी।",
"kerberosvault_directory": "निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
"kerberosvault_description_directory": "उप निर्देशिका रिकॉर्डिंग आपके प्रदाता में संग्रहीत की जाएगी।",
"kerberosvault_accesskey": "प्रवेश की चाबी",
"kerberosvault_description_accesskey": "आपके Kerberos वॉल्ट खाते की एक्सेस की।",
"kerberosvault_secretkey": "गुप्त की",
"kerberosvault_description_secretkey": "आपके कर्बेरोस वॉल्ट खाते की गुप्त की।",
"dropbox_directory": "निर्देशिका",
"dropbox_description_directory": "वह उप निर्देशिका जहां रिकॉर्डिंग आपके ड्रॉपबॉक्स खाते में संग्रहीत की जाएगी।",
"dropbox_accesstoken": "एक्सेस टोकन",
"dropbox_description_accesstoken": "आपके ड्रॉपबॉक्स खाते/ऐप का एक्सेस टोकन।",
"verify_connection": "कनेक्शन सत्यापित करें",
"remove_after_upload": "एक बार जब रिकॉर्डिंग कुछ दृढ़ता पर अपलोड हो जाती है, तो हो सकता है कि आप उन्हें स्थानीय Kerberos एजेंट से हटाना चाहें।",
"remove_after_upload_description": "सफलतापूर्वक अपलोड होने के बाद रिकॉर्डिंग हटा दें।",
"remove_after_upload_enabled": "अपलोड पर डिलीट सक्षम"
}
}
}

View File

@@ -0,0 +1,224 @@
{
"breadcrumb": {
"watch_recordings": "Guarda registrazioni",
"configure": "Configura"
},
"buttons": {
"save": "Salva",
"verify_connection": "Verifica connessione"
},
"navigation": {
"profile": "Profilo",
"admin": "admin",
"management": "Gestione",
"dashboard": "Dashboard",
"recordings": "Registrazioni",
"settings": "Impostazioni",
"help_support": "Aiuto e supporto",
"swagger": "Swagger API",
"documentation": "Documentazione",
"ui_library": "Biblioteca UI",
"layout": "Lingua e layout",
"choose_language": "Scegli lingua"
},
"dashboard": {
"title": "Dashboard",
"heading": "Panoramica della videosorveglianza",
"number_of_days": "Numero di giorni",
"total_recordings": "Registrazioni totali",
"connected": "Connesso",
"not_connected": "Non connesso",
"offline_mode": "Modalità offline",
"latest_events": "Ultimi eventi",
"configure_connection": "Configura connessione",
"no_events": "Nessun evento",
"no_events_description": "Non sono state trovate registrazioni, assicurati che il Kerberos Agent sia configurato correttamente.",
"motion_detected": "Movimento rilevato",
"live_view": "Vista in diretta",
"loading_live_view": "Caricamento vista in diretta",
"loading_live_view_description": "Attendi mentre viene caricata la vista in diretta. Se non l'hai ancora fatto, configura la connessione con la videocamera nelle pagine di impostazione.",
"time": "Ora",
"description": "Descrizione",
"name": "Nome"
},
"recordings": {
"title": "Registrazioni",
"heading": "Tutte le tue registrazioni in un posto solo",
"search_media": "Cerca video"
},
"settings": {
"title": "Impostazioni",
"heading": "Panoramica impostazioni videocamera e Agent",
"submenu": {
"all": "All",
"overview": "Panoramica",
"camera": "Videocamera",
"recording": "Registrazione",
"streaming": "Streaming",
"conditions": "Criteri",
"persistence": "Persistenza"
},
"info": {
"kerberos_hub_demo": "Dai un'occhiata al nostro ambiente demo di Kerberos Hub per vederlo in azione!",
"configuration_updated_success": "La configurazione è stata aggiornata con successo.",
"configuration_updated_error": "Si è verificato un problema durante il salvataggio.",
"verify_hub": "Controllo delle impostazioni di Kerberos Hub.",
"verify_hub_success": "Impostazioni di Kerberos Hub verificate correttamente.",
"verify_hub_error": "Si è verificato un problema durante la verifica delle impostazioni di Kerberos Hub",
"verify_persistence": "Controlla le impostazioni della persistenza.",
"verify_persistence_success": "Impostazioni della persistenza verificate correttamente.",
"verify_persistence_error": "Si è verificato un problema durante la verifica delle impostazioni della persistenza",
"verify_camera": "Controlla le impostazioni della videocamera.",
"verify_camera_success": "Impostazioni videocamera verificate correttamente.",
"verify_camera_error": "Si è verificato un problema durante la verifica delle impostazioni della videocamera",
"verify_onvif": "Controlla le impostazioni ONVIF.",
"verify_onvif_success": "Impostazioni ONVIF verificate correttamente.",
"verify_onvif_error": "Si è verificato un problema durante la verifica delle impostazioni ONVIF"
},
"overview": {
"general": "Generale",
"description_general": "Impostazioni generali del Kerberos Agent",
"key": "Chiave",
"camera_name": "Nome videocamera",
"timezone": "Fuso orario",
"select_timezone": "Seleziona un fuso orario",
"advanced_configuration": "Configurazione avanzata",
"description_advanced_configuration": "Opzioni di configurazione dettagliate per abilitare o disabilitare parti specifiche del Kerberos Agent",
"offline_mode": "Modalità offline",
"description_offline_mode": "Disabilita traffico in uscita",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Videocamera",
"description_camera": "Le impostazioni della fotocamera sono necessarie per stabilire una connessione con la videocamera scelta.",
"only_h264": "Al momento sono supportati solo streams RTSP H264.",
"rtsp_url": "Url RTSP",
"rtsp_h264": "Connessione RTSP H264 alla videocamera.",
"sub_rtsp_url": "Sub-url RTSP (per lo streaming in diretta)",
"sub_rtsp_h264": "URL RTSP supplementare della videocamera con risoluzione inferiore per lo streaming in diretta.",
"onvif": "ONVIF",
"description_onvif": "Credenziali per interagire con le funzionalità ONVIF come PTZ o altre funzioni fornite dalla videocamera.",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF username",
"onvif_password": "ONVIF password",
"verify_connection": "Verifica connessione",
"verify_sub_connection": "Verifica sub-connessione"
},
"recording": {
"recording": "Registrazione",
"description_recording": "Specificare se effettuare le registrazioni con un'impostazione continua 24/7 oppure basata sulla rilevazione di movimento.",
"continuous_recording": "Registrazione continua",
"description_continuous_recording": "Effettuare registrazioni 24/7 o basate sul movimento.",
"max_duration": "massima durata video (in secondi)",
"description_max_duration": "Durata massima della registrazione.",
"pre_recording": "pre registrazione (buffering dei key frames)",
"description_pre_recording": "Secondi prima del verificarsi di un evento.",
"post_recording": "post registrazione (in)",
"description_post_recording": "Secondi dopo il verificarsi di un evento.",
"threshold": "Soglia di registrazione (in pixel)",
"description_threshold": "Numero di pixel modificati per avviare la registrazione",
"autoclean": "Cancellazione automatica",
"description_autoclean": "Specificare se l'Agente Kerberos può cancellare le registrazioni quando viene raggiunta una specifica capacità di archiviazione (in MB). Questo rimuoverà le registrazioni più vecchie quando la capacità viene raggiunta.",
"autoclean_enable": "Abilita cancellazione automatica",
"autoclean_description_enable": "Rimuovere la registrazione più vecchia al raggiungimento della capacità.",
"autoclean_max_directory_size": "Dimensione massima della cartella (in MB)",
"autoclean_description_max_directory_size": "Dimensione massima in MB delle registrazioni salvate.",
"fragmentedrecordings": "Registrazioni frammentate",
"description_fragmentedrecordings": "Quando le registrazioni sono frammentate, sono adatte ad uno stream HLS. Se attivato, il contenitore MP4 avrà un aspetto leggermente diverso.",
"fragmentedrecordings_enable": "Abilita frammentazione",
"fragmentedrecordings_description_enable": "Per utilizzare gli stream HLS sono necessarie registrazioni frammentate.",
"fragmentedrecordings_duration": "durata frammento",
"fragmentedrecordings_description_duration": "Durata del singolo frammento."
},
"streaming": {
"stun_turn": "STUN/TURN per WebRTC",
"description_stun_turn": "Per lo streaming in diretta a massima risoluzione viene impiegato WebRTC. Una delle sue funzionalità chiave è la ICE-candidate, che consente di attraversare il NAT utilizzando i concetti di STUN/TURN.",
"stun_server": "STUN server",
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"stun_turn_forward": "Inoltro e transcodifica",
"stun_turn_description_forward": "Ottimizzazioni e miglioramenti per la comunicazione TURN/STUN.",
"stun_turn_webrtc": "Inoltro al broker WebRTC",
"stun_turn_description_webrtc": "Inoltro dello stream h264 via MQTT",
"stun_turn_transcode": "Transcodifica stream",
"stun_turn_description_transcode": "Conversione dello stream in una risoluzione inferiore",
"stun_turn_downscale": "Riduzione della risoluzione (in % o risoluzione originale)",
"mqtt": "MQTT",
"description_mqtt": "Un broker MQTT è usato per comunicare da",
"description2_mqtt": "al Kerberos Agent, per ottenere, ad esempio, funzionalità di livestreaming o ONVIF (PTZ).",
"mqtt_brokeruri": "Uri Broker",
"mqtt_username": "Username",
"mqtt_password": "Password"
},
"conditions": {
"timeofinterest": "Periodo di interesse",
"description_timeofinterest": "Effettua registrazioni solamente all'interno di specifici intervalli orari (basato sul fuso orario).",
"timeofinterest_enabled": "Abilitato",
"timeofinterest_description_enabled": "Se abilitato, è possibile specificare una finestra temporale",
"sunday": "Domenica",
"monday": "Lunedì",
"tuesday": "Martedì",
"wednesday": "Mercoledì",
"thursday": "Giovedì",
"friday": "Venerdì",
"saturday": "Sabato",
"externalcondition": "Condizione esterna",
"description_externalcondition": "È possibile attivare o disattivare la dipendenza da un servizio esterno di registrazione.",
"regionofinterest": "Regione di interesse",
"description_regionofinterest": "Definendo una o più regioni, il movimento verrà tracciato solo al loro interno."
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Kerberos Agents can send heartbeats to a central",
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Kerberos Hub to show realtime information about your video landscape.",
"persistence": "Persistenza",
"saasoffering": "Kerberos Hub (soluzione SAAS)",
"description_persistence": "La possibilità di poter salvare le tue registrazioni rappresenta l'inizio di tutto. Puoi scegliere tra il nostro",
"description2_persistence": ", oppure un provider di terze parti",
"select_persistence": "Seleziona una persistenza",
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Endpoint del Proxy per l'upload delle registrazioni.",
"kerberoshub_apiurl": "API URL Kerberos Hub",
"kerberoshub_description_apiurl": "Endpoint API per l'upload delle registrazioni.",
"kerberoshub_publickey": "Chiave pubblica",
"kerberoshub_description_publickey": "Chiave pubblica dell'account Kerberos Hub.",
"kerberoshub_privatekey": "Chiave privata",
"kerberoshub_description_privatekey": "Chiave privata dell'account Kerberos Hub.",
"kerberoshub_site": "Sito",
"kerberoshub_description_site": "ID del sito a cui appartengono i Kerberos Agents in Kerberos Hub.",
"kerberoshub_region": "Regione",
"kerberoshub_description_region": "La regione in cui memorizziamo le registrazioni.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "Bucket in cui memorizziamo le registrazioni.",
"kerberoshub_username": "Username/Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
"kerberoshub_description_username": "Username del tuo account Kerberos Hub.",
"kerberosvault_apiurl": "API URL Kerberos Vault",
"kerberosvault_description_apiurl": "API di Kerberos Vault",
"kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "Provider al quale saranno inviate le registrazioni.",
"kerberosvault_directory": "Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
"kerberosvault_description_directory": "Sotto cartella in cui saranno memorizzate le tue registrazioni nel provider.",
"kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "Access key del tuo account Kerberos Vault.",
"kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "Secret key del tuo account Kerberos Vault.",
"dropbox_directory": "Cartella",
"dropbox_description_directory": "Sottcartella dell'account Dropbox in cui saranno salvate le registrazioni.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "Access token del tuo account/app Dropbox.",
"verify_connection": "Verifica connessione",
"remove_after_upload": "Una volta che le registrazioni sono state caricate su una certa persistenza, si potrebbe volerle rimuovere dal Kerberos Agent locale.",
"remove_after_upload_description": "Cancella le registrazioni dopo che sono state caricate correttamente.",
"remove_after_upload_enabled": "Abilita cancellazione al caricamento"
}
}
}

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "詳細設定",
"description_advanced_configuration": "Kerberos エージェントの特定の部分を有効または無効にするための詳細な構成オプション",
"offline_mode": "オフラインモード",
"description_offline_mode": "すべての送信トラフィックを無効にする"
"description_offline_mode": "すべての送信トラフィックを無効にする",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "カメラ",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Geavanceerde instellingen",
"description_advanced_configuration": "Detail instellingen om bepaalde functionaliteiten van je Kerberos Agent aan en uit te zetten",
"offline_mode": "Offline modus",
"description_offline_mode": "Uitzetten van uitgaande connectiviteit"
"description_offline_mode": "Uitzetten van uitgaande connectiviteit",
"encryption": "Encrypteer",
"description_encryption": "Activeer encryptie voor alle uitgaande verkeer. MQTT berichten en/of opnames worden geencrypteerd met AES-256. Een private sleutel wordt gebruikt voor het ondertekenen.",
"encryption_enabled": "Activeer MQTT encryptie",
"description_encryption_enabled": "Activeer encryptie voor alle MQTT berichten.",
"encryption_recordings_enabled": "Activeer opname encryptie",
"description_encryption_recordings_enabled": "Activeer encryptie voor alle opnames.",
"encryption_fingerprint": "Vingerafdruk",
"encryption_privatekey": "Private sleutel",
"encryption_symmetrickey": "Symmetrische sleutel"
},
"camera": {
"camera": "Camera",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "Configurações avançadas",
"description_advanced_configuration": "Opções de configuração detalhadas para habilitar ou desabilitar partes específicas do Kerberos Agent",
"offline_mode": "Modo Offline",
"description_offline_mode": "Desative todo o tráfego de saída"
"description_offline_mode": "Desative todo o tráfego de saída",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Câmera",

View File

@@ -85,7 +85,16 @@
"advanced_configuration": "高级配置",
"description_advanced_configuration": "启用或禁用 Kerberos Agent 特定部分详细配置选项",
"offline_mode": "离线模式",
"description_offline_mode": "禁用所有传出流量"
"description_offline_mode": "禁用所有传出流量",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "相机",

View File

@@ -20,9 +20,11 @@ const LanguageSelect = () => {
fr: { label: 'Francais', dir: 'ltr', active: false },
pl: { label: 'Polski', dir: 'ltr', active: false },
de: { label: 'Deutsch', dir: 'ltr', active: false },
it: { label: 'Italiano', dir: 'ltr', active: false },
pt: { label: 'Português', dir: 'ltr', active: false },
es: { label: 'Español', dir: 'ltr', active: false },
ja: { label: '日本', dir: 'rlt', active: false },
hi: { label: 'हिंदी', dir: 'ltr', active: false },
};
if (!languageMap[selected]) {

View File

@@ -9,9 +9,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}:80/api`,
URL: `${protocol}//${hostname}:80`,
WS_URL: `${websocketprotocol}//${hostname}:80/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,

View File

@@ -729,7 +729,7 @@ class Settings extends React.Component {
/>
)}
{verifyOnvifError && (
<InfoBar type="alert" message={`${verifyOnvifErrorMessage}`} />
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
)}
{loadingHub && (
@@ -810,6 +810,24 @@ class Settings extends React.Component {
this.onUpdateDropdown('', 'timezone', value[0], config)
}
/>
<br />
<hr />
<p>
{t('settings.overview.description_advanced_configuration')}
</p>
<div className="toggle-wrapper">
<Toggle
on={config.offline === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle('', 'offline', event, config)
}
/>
<div>
<span>{t('settings.overview.offline_mode')}</span>
<p>{t('settings.overview.description_offline_mode')}</p>
</div>
</div>
</BlockBody>
<BlockFooter>
<Button
@@ -1239,25 +1257,95 @@ class Settings extends React.Component {
{showOverviewSection && (
<Block>
<BlockHeader>
<h4>{t('settings.overview.advanced_configuration')}</h4>
<h4>{t('settings.overview.encryption')}</h4>
</BlockHeader>
<BlockBody>
<p>
{t('settings.overview.description_advanced_configuration')}
</p>
<p>{t('settings.overview.description_encryption')}</p>
<div className="toggle-wrapper">
<Toggle
on={config.offline === 'true'}
on={config.encryption.enabled === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle('', 'offline', event, config)
this.onUpdateToggle(
'encryption',
'enabled',
event,
config.encryption
)
}
/>
<div>
<span>{t('settings.overview.offline_mode')}</span>
<p>{t('settings.overview.description_offline_mode')}</p>
<span>{t('settings.overview.encryption_enabled')}</span>
<p>
{t('settings.overview.description_encryption_enabled')}
</p>
</div>
</div>
<div className="toggle-wrapper">
<Toggle
on={config.encryption.recordings === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle(
'encryption',
'recordings',
event,
config.encryption
)
}
/>
<div>
<span>
{t('settings.overview.encryption_recordings_enabled')}
</span>
<p>
{t(
'settings.overview.description_encryption_recordings_enabled'
)}
</p>
</div>
</div>
<Input
noPadding
label={t('settings.overview.encryption_fingerprint')}
value={config.encryption.fingerprint}
onChange={(value) =>
this.onUpdateField(
'encryption',
'fingerprint',
value,
config.encryption
)
}
/>
<Input
noPadding
label={t('settings.overview.encryption_privatekey')}
value={config.encryption.private_key}
onChange={(value) =>
this.onUpdateField(
'encryption',
'private_key',
value,
config.encryption
)
}
/>
<Input
noPadding
label={t('settings.overview.encryption_symmetrickey')}
value={config.encryption.symmetric_key}
onChange={(value) =>
this.onUpdateField(
'encryption',
'symmetric_key',
value,
config.encryption
)
}
/>
</BlockBody>
<BlockFooter>
<Button