diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 09596396..45fb18b3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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: diff --git a/Dockerfile b/Dockerfile index 168de294..8bb517e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 --fix-missing --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"] diff --git a/README.md b/README.md index fde82b34..b20ac8be 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ [![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE) Twitter Widget [![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) @@ -28,7 +29,7 @@ Kerberos Agent is an isolated and scalable video (surveillance) management agent ## :thinking: Prerequisites - An IP camera which supports a RTSP H264 encoded stream, - - (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp). + - (or) a USB camera, Raspberry Pi camera or other camera, that [you can transform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp). - Any hardware (ARMv6, ARMv7, ARM64, AMD) that can run a binary or container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster. ## :video_camera: Is my camera working? @@ -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). -[![balena deploy button](https://www.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" | @@ -203,7 +234,7 @@ Next to attaching the configuration file, it is also possible to override the co | `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.hub.domain.com" | | `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" | | `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" | -| `AGENT_HUB_USERNAME` | Your Kerberos Hub username, which owns the above access and secret keys. | "" | +| `AGENT_HUB_REGION` | The Kerberos Hub region, to which you want to upload. | "" | | `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" | | `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" | | `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" | @@ -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. diff --git a/deployments/README.md b/deployments/README.md index c6b52508..44323dcf 100644 --- a/deployments/README.md +++ b/deployments/README.md @@ -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). diff --git a/deployments/ansible-openshift/playbook.yml b/deployments/ansible-openshift/playbook.yml index 7ec508d6..14d47370 100644 --- a/deployments/ansible-openshift/playbook.yml +++ b/deployments/ansible-openshift/playbook.yml @@ -82,7 +82,7 @@ initContainers: - name: download-config - image: kerberos/agent:1b96d01 + image: kerberos/agent:latest volumeMounts: - name: kerberos-data mountPath: /home/agent/data/config @@ -96,7 +96,7 @@ containers: - name: agent - image: kerberos/agent:1b96d01 + image: kerberos/agent:latest volumeMounts: - name: kerberos-data mountPath: /home/agent/data/config diff --git a/deployments/balena/README.md b/deployments/balena/README.md new file mode 100644 index 00000000..900d0dc3 --- /dev/null +++ b/deployments/balena/README.md @@ -0,0 +1,31 @@ +# Deployment with 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. + +We provide two mechanisms to deploy Kerberos Agent to a Balena Cloud fleet: + +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). diff --git a/deployments/kubernetes/deployment-agent-volume.yml b/deployments/kubernetes/deployment-agent-volume.yml index f58e9fd8..ed1ab57a 100644 --- a/deployments/kubernetes/deployment-agent-volume.yml +++ b/deployments/kubernetes/deployment-agent-volume.yml @@ -21,7 +21,7 @@ spec: initContainers: - name: download-config - image: kerberos/agent:1b96d01 + image: kerberos/agent:latest volumeMounts: - name: kerberos-data mountPath: /home/agent/data/config diff --git a/deployments/snap/README.md b/deployments/snap/README.md new file mode 100644 index 00000000..ac00b662 --- /dev/null +++ b/deployments/snap/README.md @@ -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. diff --git a/deployments/snap/snapstore.png b/deployments/snap/snapstore.png new file mode 100644 index 00000000..1c5aa3ca Binary files /dev/null and b/deployments/snap/snapstore.png differ diff --git a/deployments/terraform/README.md b/deployments/terraform/README.md new file mode 100644 index 00000000..25330ed5 --- /dev/null +++ b/deployments/terraform/README.md @@ -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. diff --git a/deployments/terraform/docker.tf b/deployments/terraform/docker.tf new file mode 100644 index 00000000..5a46b72a --- /dev/null +++ b/deployments/terraform/docker.tf @@ -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 + } +} \ No newline at end of file diff --git a/machinery/.vscode/launch.json b/machinery/.vscode/launch.json index 93aef948..505a4a5f 100644 --- a/machinery/.vscode/launch.json +++ b/machinery/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "mode": "auto", "program": "main.go", - "args": ["run", "cameraname", "8080"], + "args": ["-action", "run"], "envFile": "${workspaceFolder}/.env", "buildFlags": "--tags dynamic", }, diff --git a/machinery/data/config/config.json b/machinery/data/config/config.json index 326c88ab..27bc12c8 100644 --- a/machinery/data/config/config.json +++ b/machinery/data/config/config.json @@ -95,7 +95,7 @@ "s3": { "proxyuri": "http://proxy.kerberos.io", "bucket": "kerberosaccept", - "region": "eu-west1" + "region": "eu-west-1" }, "kstorage": {}, "dropbox": {}, @@ -111,5 +111,6 @@ "hub_key": "", "hub_private_key": "", "hub_site": "", - "condition_uri": "" -} \ No newline at end of file + "condition_uri": "", + "encryption": {} +} diff --git a/machinery/data/test-480p.mp4 b/machinery/data/test-480p.mp4 new file mode 100644 index 00000000..982c9c07 Binary files /dev/null and b/machinery/data/test-480p.mp4 differ diff --git a/machinery/docs/docs.go b/machinery/docs/docs.go index d2b2515f..18ec24c0 100644 --- a/machinery/docs/docs.go +++ b/machinery/docs/docs.go @@ -25,27 +25,22 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/camera/onvif/audio-backchannel": { + "/api/camera/onvif/capabilities": { "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get the audio decoders for the audio backchannel.", + "description": "Will return the ONVIF capabilities for the specific camera.", "tags": [ "camera" ], - "summary": "Get the audio decoders for the audio backchannel.", - "operationId": "audio-output-onvif", + "summary": "Will return the ONVIF capabilities for the specific camera.", + "operationId": "camera-onvif-capabilities", "parameters": [ { - "description": "Camera Config", - "name": "cameraConfig", + "description": "OnvifCredentials", + "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.IPCamera" + "$ref": "#/definitions/models.OnvifCredentials" } } ], @@ -59,22 +54,22 @@ const docTemplate = `{ } } }, - "/api/camera/onvif/capabilities": { + "/api/camera/onvif/gotopreset": { "post": { - "description": "Will return the ONVIF capabilities for the specific camera.", + "description": "Will activate the desired ONVIF preset.", "tags": [ "camera" ], - "summary": "Will return the ONVIF capabilities for the specific camera.", - "operationId": "camera-onvif-capabilities", + "summary": "Will activate the desired ONVIF preset.", + "operationId": "camera-onvif-gotopreset", "parameters": [ { - "description": "OnvifCredentials", + "description": "OnvifPreset", "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.OnvifCredentials" + "$ref": "#/definitions/models.OnvifPreset" } } ], @@ -146,61 +141,22 @@ const docTemplate = `{ } } }, - "/api/camera/onvif/verify": { + "/api/camera/onvif/presets": { "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Will verify the ONVIF connectivity.", + "description": "Will return the ONVIF presets for the specific camera.", "tags": [ "camera" ], - "summary": "Will verify the ONVIF connectivity.", - "operationId": "verify-onvif", + "summary": "Will return the ONVIF presets for the specific camera.", + "operationId": "camera-onvif-presets", "parameters": [ { - "description": "Camera Config", - "name": "cameraConfig", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.IPCamera" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.APIResponse" - } - } - } - } - }, - "/api/camera/onvif/version": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get the ONVIF version installed on the camera.", - "tags": [ - "camera" - ], - "summary": "Get the ONVIF version installed on the camera.", - "operationId": "version-onvif", - "parameters": [ - { - "description": "Camera Config", - "name": "cameraConfig", + "description": "OnvifCredentials", + "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.IPCamera" + "$ref": "#/definitions/models.OnvifCredentials" } } ], @@ -346,6 +302,40 @@ const docTemplate = `{ } } }, + "/api/onvif/verify": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Will verify the ONVIF connectivity.", + "tags": [ + "config" + ], + "summary": "Will verify the ONVIF connectivity.", + "operationId": "verify-onvif", + "parameters": [ + { + "description": "Camera Config", + "name": "cameraConfig", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.IPCamera" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + } + } + } + }, "/api/persistence/verify": { "post": { "security": [ @@ -696,6 +686,17 @@ const docTemplate = `{ } } }, + "models.OnvifPreset": { + "type": "object", + "properties": { + "onvif_credentials": { + "$ref": "#/definitions/models.OnvifCredentials" + }, + "preset": { + "type": "string" + } + } + }, "models.OnvifZoom": { "type": "object", "properties": { diff --git a/machinery/docs/swagger.json b/machinery/docs/swagger.json index 525e7017..796543bc 100644 --- a/machinery/docs/swagger.json +++ b/machinery/docs/swagger.json @@ -17,27 +17,22 @@ }, "basePath": "/", "paths": { - "/api/camera/onvif/audio-backchannel": { + "/api/camera/onvif/capabilities": { "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get the audio decoders for the audio backchannel.", + "description": "Will return the ONVIF capabilities for the specific camera.", "tags": [ "camera" ], - "summary": "Get the audio decoders for the audio backchannel.", - "operationId": "audio-output-onvif", + "summary": "Will return the ONVIF capabilities for the specific camera.", + "operationId": "camera-onvif-capabilities", "parameters": [ { - "description": "Camera Config", - "name": "cameraConfig", + "description": "OnvifCredentials", + "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.IPCamera" + "$ref": "#/definitions/models.OnvifCredentials" } } ], @@ -51,22 +46,22 @@ } } }, - "/api/camera/onvif/capabilities": { + "/api/camera/onvif/gotopreset": { "post": { - "description": "Will return the ONVIF capabilities for the specific camera.", + "description": "Will activate the desired ONVIF preset.", "tags": [ "camera" ], - "summary": "Will return the ONVIF capabilities for the specific camera.", - "operationId": "camera-onvif-capabilities", + "summary": "Will activate the desired ONVIF preset.", + "operationId": "camera-onvif-gotopreset", "parameters": [ { - "description": "OnvifCredentials", + "description": "OnvifPreset", "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.OnvifCredentials" + "$ref": "#/definitions/models.OnvifPreset" } } ], @@ -138,61 +133,22 @@ } } }, - "/api/camera/onvif/verify": { + "/api/camera/onvif/presets": { "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Will verify the ONVIF connectivity.", + "description": "Will return the ONVIF presets for the specific camera.", "tags": [ "camera" ], - "summary": "Will verify the ONVIF connectivity.", - "operationId": "verify-onvif", + "summary": "Will return the ONVIF presets for the specific camera.", + "operationId": "camera-onvif-presets", "parameters": [ { - "description": "Camera Config", - "name": "cameraConfig", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.IPCamera" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.APIResponse" - } - } - } - } - }, - "/api/camera/onvif/version": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get the ONVIF version installed on the camera.", - "tags": [ - "camera" - ], - "summary": "Get the ONVIF version installed on the camera.", - "operationId": "version-onvif", - "parameters": [ - { - "description": "Camera Config", - "name": "cameraConfig", + "description": "OnvifCredentials", + "name": "config", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.IPCamera" + "$ref": "#/definitions/models.OnvifCredentials" } } ], @@ -338,6 +294,40 @@ } } }, + "/api/onvif/verify": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Will verify the ONVIF connectivity.", + "tags": [ + "config" + ], + "summary": "Will verify the ONVIF connectivity.", + "operationId": "verify-onvif", + "parameters": [ + { + "description": "Camera Config", + "name": "cameraConfig", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.IPCamera" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + } + } + } + }, "/api/persistence/verify": { "post": { "security": [ @@ -688,6 +678,17 @@ } } }, + "models.OnvifPreset": { + "type": "object", + "properties": { + "onvif_credentials": { + "$ref": "#/definitions/models.OnvifCredentials" + }, + "preset": { + "type": "string" + } + } + }, "models.OnvifZoom": { "type": "object", "properties": { diff --git a/machinery/docs/swagger.yaml b/machinery/docs/swagger.yaml index 8ef7dde7..2d50c8d2 100644 --- a/machinery/docs/swagger.yaml +++ b/machinery/docs/swagger.yaml @@ -207,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: @@ -296,44 +303,42 @@ info: title: Swagger Kerberos Agent API version: "1.0" paths: - /api/camera/onvif/audio-backchannel: + /api/camera/onvif/capabilities: post: - description: Get the audio decoders for the audio backchannel. - operationId: audio-output-onvif + description: Will return the ONVIF capabilities for the specific camera. + operationId: camera-onvif-capabilities parameters: - - description: Camera Config + - description: OnvifCredentials in: body - name: cameraConfig + name: config required: true schema: - $ref: '#/definitions/models.IPCamera' + $ref: '#/definitions/models.OnvifCredentials' responses: "200": description: OK schema: $ref: '#/definitions/models.APIResponse' - security: - - Bearer: [] - summary: Get the audio decoders for the audio backchannel. + summary: Will return the ONVIF capabilities for the specific camera. tags: - camera - /api/camera/onvif/capabilities: + /api/camera/onvif/gotopreset: post: - description: Will return the ONVIF capabilities for the specific camera. - operationId: camera-onvif-capabilities + description: Will activate the desired ONVIF preset. + operationId: camera-onvif-gotopreset parameters: - - description: OnvifCredentials + - description: OnvifPreset in: body name: config required: true schema: - $ref: '#/definitions/models.OnvifCredentials' + $ref: '#/definitions/models.OnvifPreset' responses: "200": description: OK schema: $ref: '#/definitions/models.APIResponse' - summary: Will return the ONVIF capabilities for the specific camera. + summary: Will activate the desired ONVIF preset. tags: - camera /api/camera/onvif/login: @@ -374,46 +379,23 @@ paths: summary: Panning or/and tilting the camera. tags: - camera - /api/camera/onvif/verify: + /api/camera/onvif/presets: post: - description: Will verify the ONVIF connectivity. - operationId: verify-onvif + description: Will return the ONVIF presets for the specific camera. + operationId: camera-onvif-presets parameters: - - description: Camera Config - in: body - name: cameraConfig - required: true - schema: - $ref: '#/definitions/models.IPCamera' - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.APIResponse' - security: - - Bearer: [] - summary: Will verify the ONVIF connectivity. - tags: - - camera - /api/camera/onvif/version: - post: - description: Get the ONVIF version installed on the camera. - operationId: version-onvif - parameters: - - description: Camera Config + - description: OnvifCredentials in: body - name: cameraConfig + name: config required: true schema: - $ref: '#/definitions/models.IPCamera' + $ref: '#/definitions/models.OnvifCredentials' responses: "200": description: OK schema: $ref: '#/definitions/models.APIResponse' - security: - - Bearer: [] - summary: Get the ONVIF version installed on the camera. + summary: Will return the ONVIF presets for the specific camera. tags: - camera /api/camera/onvif/zoom: @@ -503,6 +485,27 @@ paths: summary: Get Authorization token. tags: - authentication + /api/onvif/verify: + post: + description: Will verify the ONVIF connectivity. + operationId: verify-onvif + parameters: + - description: Camera Config + in: body + name: cameraConfig + required: true + schema: + $ref: '#/definitions/models.IPCamera' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.APIResponse' + security: + - Bearer: [] + summary: Will verify the ONVIF connectivity. + tags: + - config /api/persistence/verify: post: description: Will verify the persistence. diff --git a/machinery/go.mod b/machinery/go.mod index 8bd0e751..5bd543ed 100644 --- a/machinery/go.mod +++ b/machinery/go.mod @@ -2,8 +2,9 @@ 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/joy4 v1.0.63 => ../../../../github.com/kerberos-io/joy4 + +// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif require ( github.com/InVisionApp/conjungo v1.1.0 @@ -20,12 +21,13 @@ require ( github.com/gin-contrib/pprof v1.4.0 github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 github.com/gin-gonic/gin v1.8.2 + github.com/gofrs/uuid v3.2.0+incompatible github.com/golang-jwt/jwt/v4 v4.4.3 github.com/golang-module/carbon/v2 v2.2.3 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/joy4 v1.0.64 + 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 @@ -36,6 +38,7 @@ require ( github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.8.9 github.com/tevino/abool v1.2.0 + github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 go.mongodb.org/mongo-driver v1.7.5 gopkg.in/DataDog/dd-trace-go.v1 v1.46.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 @@ -72,7 +75,6 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/goccy/go-json v0.10.0 // indirect - github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/machinery/go.sum b/machinery/go.sum index e96e66ef..d40a558d 100644 --- a/machinery/go.sum +++ b/machinery/go.sum @@ -264,10 +264,10 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg= 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/joy4 v1.0.64 h1:gTUSotHSOhp9mNqEecgq88tQHvpj7TjmrvPUsPm0idg= +github.com/kerberos-io/joy4 v1.0.64/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU= +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= @@ -471,6 +471,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM= +github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= diff --git a/machinery/main.go b/machinery/main.go index 7a44250c..eb88d396 100644 --- a/machinery/main.go +++ b/machinery/main.go @@ -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 :(") diff --git a/machinery/src/api/onvif.go b/machinery/src/api/onvif.go new file mode 100644 index 00000000..778f64ec --- /dev/null +++ b/machinery/src/api/onvif.go @@ -0,0 +1 @@ +package api diff --git a/machinery/src/capture/IPCamera.go b/machinery/src/capture/IPCamera.go index b109bcc7..8108d183 100644 --- a/machinery/src/capture/IPCamera.go +++ b/machinery/src/capture/IPCamera.go @@ -16,12 +16,27 @@ import ( "github.com/kerberos-io/joy4/format" ) -func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) { +func OpenRTSP(ctx context.Context, url string, withBackChannel bool) (av.DemuxCloser, []av.CodecData, error) { format.RegisterAll() - infile, err := avutil.Open(ctx, url) + + // Try with backchannel first (if variable is set to true) + // If set to true, it will try to open the stream with a backchannel + // If fails we will try again (see below). + infile, err := avutil.Open(ctx, url, withBackChannel) if err == nil { streams, errstreams := infile.Streams() - return infile, streams, errstreams + if len(streams) > 0 { + return infile, streams, errstreams + } else { + // Try again without backchannel + log.Log.Info("OpenRTSP: trying without backchannel") + withBackChannel = false + infile, err := avutil.Open(ctx, url, withBackChannel) + if err == nil { + streams, errstreams := infile.Streams() + return infile, streams, errstreams + } + } } return nil, []av.CodecData{}, err } diff --git a/machinery/src/capture/main.go b/machinery/src/capture/main.go index 2b52144b..31de56d7 100644 --- a/machinery/src/capture/main.go +++ b/machinery/src/capture/main.go @@ -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) } } @@ -447,7 +469,7 @@ func VerifyCamera(c *gin.Context) { if streamType == "secondary" { rtspUrl = cameraStreams.SubRTSP } - _, codecs, err := OpenRTSP(ctx, rtspUrl) + _, codecs, err := OpenRTSP(ctx, rtspUrl, true) if err == nil { videoIdx := -1 diff --git a/machinery/src/cloud/Cloud.go b/machinery/src/cloud/Cloud.go index 0de85964..ca4372b5 100644 --- a/machinery/src/cloud/Cloud.go +++ b/machinery/src/cloud/Cloud.go @@ -15,14 +15,12 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-module/carbon/v2" "github.com/kerberos-io/joy4/av/pubsub" - "github.com/minio/minio-go/v6" mqtt "github.com/eclipse/paho.mqtt.golang" av "github.com/kerberos-io/joy4/av" "github.com/kerberos-io/joy4/cgo/ffmpeg" "net/http" - "net/url" "strconv" "time" @@ -34,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()) @@ -43,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.") @@ -85,9 +83,9 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com uploaded := false configured := false err = nil - if config.Cloud == "s3" { - uploaded, configured, err = UploadS3(configuration, fileName) - } else if config.Cloud == "kstorage" { + if config.Cloud == "s3" || config.Cloud == "kerberoshub" { + uploaded, configured, err = UploadKerberosHub(configuration, fileName) + } else if config.Cloud == "kstorage" || config.Cloud == "kerberosvault" { uploaded, configured, err = UploadKerberosVault(configuration, fileName) } else if config.Cloud == "dropbox" { uploaded, configured, err = UploadDropbox(configuration, fileName) @@ -103,6 +101,13 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com // Todo: implement ftp upload } else if config.Cloud == "sftp" { // Todo: implement sftp upload + } else if config.Cloud == "aws" { + // Todo: need to be updated, was previously used for hub. + uploaded, configured, err = UploadS3(configuration, fileName) + } else if config.Cloud == "azure" { + // Todo: implement azure upload + } else if config.Cloud == "google" { + // Todo: implement google upload } // And so on... (have a look here -> https://github.com/kerberos-io/agent/issues/95) @@ -116,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()) } @@ -226,7 +231,7 @@ loop: log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.") } else { - url := config.HeartbeatURI + hubURI := config.HeartbeatURI key := "" username := "" vaultURI := "" @@ -242,74 +247,115 @@ loop: // This is the new way ;) if config.HubURI != "" { - url = config.HubURI + "/devices/heartbeat" + hubURI = config.HubURI + "/devices/heartbeat" } if config.HubKey != "" { key = config.HubKey } - if key != "" { - // Check if we have a friendly name or not. - name := config.Name - if config.FriendlyName != "" { - name = config.FriendlyName - } + // Check if we have a friendly name or not. + name := config.Name + if config.FriendlyName != "" { + name = config.FriendlyName + } + + // Get some system information + // like the uptime, hostname, memory usage, etc. + system, _ := GetSystemInfo() + + // Check if the agent is running inside a cluster (Kerberos Factory) or as + // an open source agent + isEnterprise := false + if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" { + isEnterprise = true + } + + // Congert to string + macs, _ := json.Marshal(system.MACs) + ips, _ := json.Marshal(system.IPs) + cameraConnected := "true" + if !communication.CameraConnected { + cameraConnected = "false" + } - // Get some system information - // like the uptime, hostname, memory usage, etc. - system, _ := GetSystemInfo() - - // We will formated the uptime to a human readable format - // this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours. - uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05") - uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans() - uptimeString = strings.ReplaceAll(uptimeString, "ago", "") - - // Do the same for boottime - bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05") - boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans() - boottimeString = strings.ReplaceAll(boottimeString, "ago", "") - - // We'll check which mode is enabled for the camera. - onvifEnabled := "false" - onvifZoom := "false" - onvifPanTilt := "false" - onvifVersion := "unknown" - - if config.Capture.IPCamera.ONVIFXAddr != "" { - cameraConfiguration := configuration.Config.Capture.IPCamera - device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration) + hasBackChannel := "false" + if communication.HasBackChannel { + hasBackChannel = "true" + } + + // We will formated the uptime to a human readable format + // this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours. + uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05") + uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans() + uptimeString = strings.ReplaceAll(uptimeString, "ago", "") + + // Do the same for boottime + bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05") + boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans() + boottimeString = strings.ReplaceAll(boottimeString, "ago", "") + + // We'll check which mode is enabled for the camera. + 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) + if err == nil { + configurations, err := onvif.GetPTZConfigurationsFromDevice(device) if err == nil { - configurations, err := onvif.GetPTZConfigurationsFromDevice(device) - if err == nil { - onvifEnabled = "true" - _, canZoom, canPanTilt := onvif.GetPTZFunctionsFromDevice(configurations) - if canZoom { - onvifZoom = "true" + onvifEnabled = "true" + _, canZoom, canPanTilt := onvif.GetPTZFunctionsFromDevice(configurations) + if canZoom { + onvifZoom = "true" + } + 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("[]") } - if canPanTilt { - onvifPanTilt = "true" + } else { + if err != nil { + log.Log.Error("HandleHeartBeat: error while getting presets: " + err.Error()) + } else { + log.Log.Debug("HandleHeartBeat: no presets found.") } + onvifPresetsList = []byte("[]") } - // Get the ONVIF version from the device. - onvifVersion, err = onvif.GetONVIFVersionFromDevice(device) + } 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 - // an open source agent - isEnterprise := false - if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" { - isEnterprise = true + 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{} + } - // Congert to string - macs, _ := json.Marshal(system.MACs) - ips, _ := json.Marshal(system.IPs) - cameraConnected := "true" - if communication.CameraConnected == false { - cameraConnected = "false" - } + // We need a hub URI and hub public key before we will send a heartbeat + if hubURI != "" && key != "" { var object = fmt.Sprintf(`{ "key" : "%s", @@ -338,22 +384,22 @@ loop: "onvif" : "%s", "onvif_zoom" : "%s", "onvif_pantilt" : "%s", - "onvif_version" : "%s", + "onvif_presets": "%s", + "onvif_presets_list": %s, "cameraConnected": "%s", + "hasBackChannel": "%s", "numberoffiles" : "33", "timestamp" : 1564747908, "cameratype" : "IPCamera", "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, onvifVersion, 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, hasBackChannel) var jsonStr = []byte(object) buffy := bytes.NewBuffer(jsonStr) - req, _ := http.NewRequest("POST", url, buffy) + req, _ := http.NewRequest("POST", hubURI, buffy) req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} resp, err := client.Do(req) if resp != nil { resp.Body.Close() @@ -362,31 +408,72 @@ loop: communication.CloudTimestamp.Store(time.Now().Unix()) log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.") } else { - communication.CloudTimestamp.Store(0) + if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil { + communication.CloudTimestamp.Store(int64(0)) + } log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Hub.") } + } else { + log.Log.Error("HandleHeartBeat: Disabled as we do not have a public key defined.") + } - // If we have a Kerberos Vault connected, we will also send some analytics - // to that service. - vaultURI = config.KStorage.URI - if vaultURI != "" { - buffy = bytes.NewBuffer(jsonStr) - req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy) - req.Header.Set("Content-Type", "application/json") + // If we have a Kerberos Vault connected, we will also send some analytics + // to that service. + vaultURI = config.KStorage.URI + if vaultURI != "" { - client = &http.Client{} - resp, err = client.Do(req) - if resp != nil { - resp.Body.Close() - } - if err == nil && resp.StatusCode == 200 { - log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.") - } else { - log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.") - } + var object = fmt.Sprintf(`{ + "key" : "%s", + "version" : "3.0.0", + "release" : "%s", + "cpuid" : "%s", + "clouduser" : "%s", + "cloudpublickey" : "%s", + "cameraname" : "%s", + "enterprise" : %t, + "hostname" : "%s", + "architecture" : "%s", + "totalMemory" : "%d", + "usedMemory" : "%d", + "freeMemory" : "%d", + "processMemory" : "%d", + "mac_list" : %s, + "ip_list" : %s, + "board" : "", + "disk1size" : "%s", + "disk3size" : "%s", + "diskvdasize" : "%s", + "uptime" : "%s", + "boot_time" : "%s", + "siteID" : "%s", + "onvif" : "%s", + "onvif_zoom" : "%s", + "onvif_pantilt" : "%s", + "onvif_presets": "%s", + "onvif_presets_list": %s, + "cameraConnected": "%s", + "numberoffiles" : "33", + "timestamp" : 1564747908, + "cameratype" : "IPCamera", + "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, onvifPresets, onvifPresetsList, cameraConnected) + + var jsonStr = []byte(object) + buffy := bytes.NewBuffer(jsonStr) + req, _ := http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if resp != nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == 200 { + log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.") + } else { + log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.") } - } else { - log.Log.Error("HandleHeartBeat: Disabled as we do not have a public key defined.") } } @@ -419,19 +506,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 @@ -452,7 +537,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. @@ -466,15 +571,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 @@ -493,26 +589,19 @@ 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() - _, ok := webrtc.CandidateArrays[key] - if !ok { - webrtc.CandidateArrays[key] = make(chan string, 30) - } - webrtc.CandidatesMutex.Unlock() - webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key]) + go webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake) } } @@ -541,15 +630,23 @@ func VerifyHub(c *gin.Context) { err := c.BindJSON(&config) if err == nil { - hubKey := config.HubKey hubURI := config.HubURI + publicKey := config.HubKey + privateKey := config.HubPrivateKey - content := []byte(`{"message": "fake-message"}`) - body := bytes.NewReader(content) - req, err := http.NewRequest("POST", hubURI+"/queue/test", body) + req, err := http.NewRequest("POST", hubURI+"/subscription/verify", nil) if err == nil { - req.Header.Set("X-Kerberos-Cloud-Key", hubKey) - client := &http.Client{} + req.Header.Set("X-Kerberos-Hub-PublicKey", publicKey) + req.Header.Set("X-Kerberos-Hub-PrivateKey", privateKey) + 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 { @@ -597,7 +694,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) @@ -605,88 +702,88 @@ func VerifyPersistence(c *gin.Context) { if config.Cloud == "dropbox" { VerifyDropbox(config, c) - } else if config.Cloud == "s3" { - - // timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token - // 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4 - // - Timestamp - // - Size + - + microseconds - // - device - // - Region - // - Number of changes - // - Token - - aws_access_key_id := config.S3.Publickey - aws_secret_access_key := config.S3.Secretkey - aws_region := config.S3.Region - - // This is the new way ;) - if config.HubKey != "" { - aws_access_key_id = config.HubKey - } - if config.HubPrivateKey != "" { - aws_secret_access_key = config.HubPrivateKey - } - - s3Client, err := minio.NewWithRegion("s3.amazonaws.com", aws_access_key_id, aws_secret_access_key, true, aws_region) - if err != nil { + } else if config.Cloud == "s3" || config.Cloud == "kerberoshub" { + + if config.HubURI == "" || + config.HubKey == "" || + config.HubPrivateKey == "" || + config.S3.Region == "" { + msg := "VerifyPersistence: Kerberos Hub not properly configured." + log.Log.Info(msg) c.JSON(400, models.APIResponse{ - Data: "Creation of Kerberos Hub connection failed: " + err.Error(), + Data: msg, }) } else { - // Check if we need to use the proxy. - if config.S3.ProxyURI != "" { - var transport http.RoundTripper = &http.Transport{ - Proxy: func(*http.Request) (*url.URL, error) { - return url.Parse(config.S3.ProxyURI) - }, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - s3Client.SetCustomTransport(transport) - } - - deviceKey := "fake-key" - devicename := "justatest" - coordinates := "200-200-400-400" - eventToken := "769" - - timestamp := time.Now().Unix() - fileName := strconv.FormatInt(timestamp, 10) + "_6-967003_justatest_200-200-400-400_24_769.mp4" - content := []byte("test-file") - body := bytes.NewReader(content) - - n, err := s3Client.PutObject(config.S3.Bucket, - config.S3.Username+"/"+fileName, - body, - body.Size(), - minio.PutObjectOptions{ - ContentType: "video/mp4", - StorageClass: "ONEZONE_IA", - UserMetadata: map[string]string{ - "event-timestamp": strconv.FormatInt(timestamp, 10), - "event-microseconds": deviceKey, - "event-instancename": devicename, - "event-regioncoordinates": coordinates, - "event-numberofchanges": deviceKey, - "event-token": eventToken, - "productid": deviceKey, - "publickey": aws_access_key_id, - "uploadtime": "now", - }, + // Open 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) + c.JSON(400, models.APIResponse{ + Data: msg, }) + } + defer file.Close() + req, err := http.NewRequest("POST", config.HubURI+"/storage/upload", file) if err != nil { + msg := "VerifyPersistence: error reading Kerberos Hub HEAD request, " + config.HubURI + "/storage: " + err.Error() + log.Log.Error(msg) c.JSON(400, models.APIResponse{ - Data: "Upload of fake recording failed: " + err.Error(), + Data: msg, }) + } + + timestamp := time.Now().Unix() + fileName := strconv.FormatInt(timestamp, 10) + + "_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4" + req.Header.Set("X-Kerberos-Storage-FileName", fileName) + req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera") + req.Header.Set("X-Kerberos-Storage-Device", config.Key) + 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) + + 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 { - c.JSON(200, models.APIResponse{ - Data: "Upload Finished: file has been uploaded to bucket: " + strconv.FormatInt(n, 10), + client = &http.Client{} + } + + resp, err := client.Do(req) + if resp != nil { + defer resp.Body.Close() + } + + if err == nil && resp != nil { + if resp.StatusCode == 200 { + msg := "VerifyPersistence: Upload allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")" + log.Log.Info(msg) + c.JSON(200, models.APIResponse{ + Data: msg, + }) + } else { + msg := "VerifyPersistence: Upload NOT allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")" + log.Log.Info(msg) + c.JSON(400, models.APIResponse{ + Data: msg, + }) + } + } else { + msg := "VerifyPersistence: Error creating Kerberos Hub request" + log.Log.Info(msg) + c.JSON(400, models.APIResponse{ + Data: msg, }) } } - } else if config.Cloud == "kstorage" { + + } else if config.Cloud == "kstorage" || config.Cloud == "kerberosvault" { uri := config.KStorage.URI accessKey := config.KStorage.AccessKey @@ -695,10 +792,18 @@ func VerifyPersistence(c *gin.Context) { provider := config.KStorage.Provider if err == nil && uri != "" && accessKey != "" && secretAccessKey != "" { - var postData = []byte(`{"title":"Buy cheese and bread for breakfast."}`) - client := &http.Client{} - req, err := http.NewRequest("POST", uri+"/ping", bytes.NewReader(postData)) + 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) resp, err := client.Do(req) @@ -710,33 +815,44 @@ func VerifyPersistence(c *gin.Context) { if provider != "" || directory != "" { - hubKey := config.KStorage.CloudKey - // This is the new way ;) - if config.HubKey != "" { - hubKey = config.HubKey - } - // Generate a random name. timestamp := time.Now().Unix() fileName := strconv.FormatInt(timestamp, 10) + - "_6-967003_justatest_200-200-400-400_24_769.mp4" - content := []byte("test-file") - body := bytes.NewReader(content) - //fileSize := int64(len(content)) + "_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4" + + // Open 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) + c.JSON(400, models.APIResponse{ + Data: msg, + }) + } + defer file.Close() - req, err := http.NewRequest("POST", uri+"/storage", body) + req, err := http.NewRequest("POST", uri+"/storage", file) if err == nil { req.Header.Set("Content-Type", "video/mp4") - req.Header.Set("X-Kerberos-Storage-CloudKey", hubKey) + req.Header.Set("X-Kerberos-Storage-CloudKey", config.HubKey) req.Header.Set("X-Kerberos-Storage-AccessKey", accessKey) req.Header.Set("X-Kerberos-Storage-SecretAccessKey", secretAccessKey) req.Header.Set("X-Kerberos-Storage-Provider", provider) req.Header.Set("X-Kerberos-Storage-FileName", fileName) - req.Header.Set("X-Kerberos-Storage-Device", "test") + req.Header.Set("X-Kerberos-Storage-Device", config.Key) 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) @@ -749,41 +865,45 @@ func VerifyPersistence(c *gin.Context) { c.JSON(200, body) } else { c.JSON(400, models.APIResponse{ - Data: "Something went wrong while verifying your persistence settings. Make sure your provider is the same as the storage provider in your Kerberos Vault, and the relevant storage provider is configured properly.", + Data: "VerifyPersistence: Something went wrong while verifying your persistence settings. Make sure your provider is the same as the storage provider in your Kerberos Vault, and the relevant storage provider is configured properly.", }) } } } } else { c.JSON(400, models.APIResponse{ - Data: "Upload of fake recording failed: " + err.Error(), + Data: "VerifyPersistence: Upload of fake recording failed: " + err.Error(), }) } } else { c.JSON(400, models.APIResponse{ - Data: "Something went wrong while creating /storage POST request." + err.Error(), + Data: "VerifyPersistence: Something went wrong while creating /storage POST request." + err.Error(), }) } } else { c.JSON(400, models.APIResponse{ - Data: "Provider and/or directory is missing from the request.", + Data: "VerifyPersistence: Provider and/or directory is missing from the request.", }) } } else { c.JSON(400, models.APIResponse{ - Data: "Something went wrong while verifying storage credentials: " + string(body), + Data: "VerifyPersistence: Something went wrong while verifying storage credentials: " + string(body), }) } } else { c.JSON(400, models.APIResponse{ - Data: "Something went wrong while verifying storage credentials:" + err.Error(), + Data: "VerifyPersistence: Something went wrong while verifying storage credentials:" + err.Error(), }) } + } else { + c.JSON(400, models.APIResponse{ + Data: "VerifyPersistence: please fill-in the required Kerberos Vault credentials.", + }) } } } else { c.JSON(400, models.APIResponse{ - Data: "No persistence was specified, so do not know what to verify:" + err.Error(), + Data: "VerifyPersistence: No persistence was specified, so do not know what to verify:" + err.Error(), }) } } diff --git a/machinery/src/cloud/KerberosHub.go b/machinery/src/cloud/KerberosHub.go new file mode 100644 index 00000000..59aaea9d --- /dev/null +++ b/machinery/src/cloud/KerberosHub.go @@ -0,0 +1,131 @@ +package cloud + +import ( + "crypto/tls" + "errors" + "io/ioutil" + "net/http" + "os" + + "github.com/kerberos-io/agent/machinery/src/log" + "github.com/kerberos-io/agent/machinery/src/models" +) + +func UploadKerberosHub(configuration *models.Configuration, fileName string) (bool, bool, error) { + config := configuration.Config + + if config.HubURI == "" || + config.HubKey == "" || + config.HubPrivateKey == "" || + config.S3.Region == "" { + err := "UploadKerberosHub: Kerberos Hub not properly configured." + log.Log.Info(err) + return false, false, errors.New(err) + } + + // timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token + // 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4 + // - Timestamp + // - Size + - + microseconds + // - device + // - Region + // - Number of changes + // - Token + + log.Log.Info("UploadKerberosHub: Uploading to Kerberos Hub (" + config.HubURI + ")") + log.Log.Info("UploadKerberosHub: Upload started for " + fileName) + fullname := "data/recordings/" + fileName + + // Check if we still have the file otherwise we abort the request. + file, err := os.OpenFile(fullname, os.O_RDWR, 0755) + if file != nil { + defer file.Close() + } + if err != nil { + err := "UploadKerberosHub: Upload Failed, file doesn't exists anymore." + log.Log.Info(err) + return false, false, errors.New(err) + } + + // Check if we are allowed to upload to the hub with these credentials. + // There might be different reasons like (muted, read-only..) + req, err := http.NewRequest("HEAD", config.HubURI+"/storage/upload", nil) + if err != nil { + errorMessage := "UploadKerberosHub: error reading HEAD request, " + config.HubURI + "/storage: " + err.Error() + log.Log.Error(errorMessage) + return false, true, errors.New(errorMessage) + } + + req.Header.Set("X-Kerberos-Storage-FileName", fileName) + req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera") + req.Header.Set("X-Kerberos-Storage-Device", config.Key) + 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) + + 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 { + defer resp.Body.Close() + } + + if err == nil { + if resp != nil { + if err == nil { + if resp.StatusCode == 200 { + log.Log.Info("UploadKerberosHub: Upload allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")") + } else { + log.Log.Info("UploadKerberosHub: Upload NOT allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")") + return false, true, nil + } + } + } + } + + // Now we know we are allowed to upload to the hub, we can start uploading. + req, err = http.NewRequest("POST", config.HubURI+"/storage/upload", file) + if err != nil { + errorMessage := "UploadKerberosHub: error reading POST request, " + config.KStorage.URI + "/storage/upload: " + err.Error() + log.Log.Error(errorMessage) + return false, true, errors.New(errorMessage) + } + req.Header.Set("Content-Type", "video/mp4") + req.Header.Set("X-Kerberos-Storage-FileName", fileName) + req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera") + req.Header.Set("X-Kerberos-Storage-Device", config.Key) + 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) + resp, err = client.Do(req) + if resp != nil { + defer resp.Body.Close() + } + + if err == nil { + if resp != nil { + body, err := ioutil.ReadAll(resp.Body) + if err == nil { + if resp.StatusCode == 200 { + log.Log.Info("UploadKerberosHub: Upload Finished, " + resp.Status + ".") + return true, true, nil + } else { + log.Log.Info("UploadKerberosHub: Upload Failed, " + resp.Status + ", " + string(body)) + return false, true, nil + } + } + } + } + + errorMessage := "UploadKerberosHub: Upload Failed, " + err.Error() + log.Log.Info(errorMessage) + return false, true, errors.New(errorMessage) +} diff --git a/machinery/src/cloud/KerberosVault.go b/machinery/src/cloud/KerberosVault.go index 81df0470..9abdf1fe 100644 --- a/machinery/src/cloud/KerberosVault.go +++ b/machinery/src/cloud/KerberosVault.go @@ -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 { diff --git a/machinery/src/components/Audio.go b/machinery/src/components/Audio.go new file mode 100644 index 00000000..4e24d2f3 --- /dev/null +++ b/machinery/src/components/Audio.go @@ -0,0 +1,80 @@ +package components + +import ( + "bufio" + "fmt" + "os" + "time" + + "github.com/kerberos-io/agent/machinery/src/log" + "github.com/kerberos-io/agent/machinery/src/models" + "github.com/kerberos-io/joy4/av" + "github.com/zaf/g711" +) + +func GetBackChannelAudioCodec(streams []av.CodecData, communication *models.Communication) av.AudioCodecData { + for _, stream := range streams { + if stream.Type().IsAudio() { + if stream.Type().String() == "PCM_MULAW" { + pcmuCodec := stream.(av.AudioCodecData) + if pcmuCodec.IsBackChannel() { + communication.HasBackChannel = true + return pcmuCodec + } + } + } + } + return nil +} + +func WriteAudioToBackchannel(infile av.DemuxCloser, streams []av.CodecData, communication *models.Communication) { + log.Log.Info("WriteAudioToBackchannel: looking for backchannel audio codec") + + pcmuCodec := GetBackChannelAudioCodec(streams, communication) + if pcmuCodec != nil { + log.Log.Info("WriteAudioToBackchannel: found backchannel audio codec") + + length := 0 + channel := pcmuCodec.GetIndex() * 2 // This is the same calculation as Interleaved property in the SDP file. + for audio := range communication.HandleAudio { + // Encode PCM to MULAW + var bufferUlaw []byte + for _, v := range audio.Data { + b := g711.EncodeUlawFrame(v) + bufferUlaw = append(bufferUlaw, b) + } + infile.Write(bufferUlaw, channel, uint32(length)) + length = (length + len(bufferUlaw)) % 65536 + time.Sleep(128 * time.Millisecond) + } + } + log.Log.Info("WriteAudioToBackchannel: finished") + +} + +func WriteFileToBackChannel(infile av.DemuxCloser) { + // Do the warmup! + file, err := os.Open("./audiofile.bye") + if err != nil { + fmt.Println("WriteFileToBackChannel: error opening audiofile.bye file") + } + defer file.Close() + + // Read file into buffer + reader := bufio.NewReader(file) + buffer := make([]byte, 1024) + + count := 0 + for { + _, err := reader.Read(buffer) + if err != nil { + break + } + // Send to backchannel + fmt.Println(buffer) + infile.Write(buffer, 2, uint32(count)) + + count = count + 1024 + time.Sleep(128 * time.Millisecond) + } +} diff --git a/machinery/src/components/Kerberos.go b/machinery/src/components/Kerberos.go index d5c406c8..2b2c7949 100644 --- a/machinery/src/components/Kerberos.go +++ b/machinery/src/components/Kerberos.go @@ -11,9 +11,12 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/kerberos-io/joy4/cgo/ffmpeg" + //"github.com/youpy/go-wav" + "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 +26,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 +75,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 +90,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 +110,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 @@ -115,9 +118,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi status := "not started" // Currently only support H264 encoded cameras, this will change. - // Establishing the camera connection + // Establishing the camera connection without backchannel if no substream rtspUrl := config.Capture.IPCamera.RTSP - infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl) + withBackChannel := true + infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl, withBackChannel) // We will initialise the camera settings object // so we can check if the camera settings have changed, and we need @@ -134,6 +138,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 @@ -150,7 +158,8 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi subStreamEnabled := false subRtspUrl := config.Capture.IPCamera.SubRTSP if subRtspUrl != "" && subRtspUrl != rtspUrl { - subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl) + withBackChannel := false + subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl, withBackChannel) // We'll try to enable backchannel for the substream. if err == nil { log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl) subStreamEnabled = true @@ -162,10 +171,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 +206,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") } @@ -230,6 +248,9 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi go capture.HandleSubStream(subInfile, subQueue, communication) } + // Handle processing of audio + communication.HandleAudio = make(chan models.AudioDataPartial) + // Handle processing of motion communication.HandleMotion = make(chan models.MotionDataPartial, 1) if subStreamEnabled { @@ -250,7 +271,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 +281,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) @@ -271,6 +292,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi // If we reach this point, we have a working RTSP connection. communication.CameraConnected = true + // We might have a camera with audio backchannel enabled. + // Check if we have a stream with a backchannel and is PCMU encoded. + go WriteAudioToBackchannel(infile, streams, communication) + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // This will go into a blocking state, once this channel is triggered // the agent will cleanup and restart. @@ -284,10 +309,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" { @@ -314,6 +339,8 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi } close(communication.HandleMotion) communication.HandleMotion = nil + close(communication.HandleAudio) + communication.HandleAudio = nil // Waiting for some seconds to make sure everything is properly closed. log.Log.Info("RunAgent: waiting 3 seconds to make sure everything is properly closed.") diff --git a/machinery/src/computervision/main.go b/machinery/src/computervision/main.go index 23c2f03c..664a8a15 100644 --- a/machinery/src/computervision/main.go +++ b/machinery/src/computervision/main.go @@ -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() @@ -165,12 +166,26 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi if detectMotion && isPixelChangeThresholdReached { // If offline mode is disabled, send a message to the hub - if config.Offline == "false" { + 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") } } } diff --git a/machinery/src/components/Config.go b/machinery/src/config/main.go similarity index 77% rename from machinery/src/components/Config.go rename to machinery/src/config/main.go index b86a4387..107582b8 100644 --- a/machinery/src/components/Config.go +++ b/machinery/src/config/main.go @@ -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,15 @@ func OpenConfig(configuration *models.Configuration) { conjungo.Merge(&s3, configuration.CustomConfig.S3, opts) configuration.Config.S3 = &s3 + // Merge Encryption settings + var encryption models.Encryption + conjungo.Merge(&encryption, configuration.GlobalConfig.Encryption, opts) + conjungo.Merge(&encryption, configuration.CustomConfig.Encryption, opts) + configuration.Config.Encryption = &encryption + + // Merge timetable manually because it's an array + configuration.Config.Timetable = configuration.CustomConfig.Timetable + // Cleanup opts = nil @@ -146,9 +181,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 +224,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 @@ -401,12 +436,12 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) { case "AGENT_HUB_PRIVATE_KEY": configuration.Config.HubPrivateKey = value break - case "AGENT_HUB_USERNAME": - configuration.Config.S3.Username = value - break case "AGENT_HUB_SITE": configuration.Config.HubSite = value break + case "AGENT_HUB_REGION": + configuration.Config.S3.Region = value + break /* When storing in a Kerberos Vault */ case "AGENT_KERBEROSVAULT_URI": @@ -432,16 +467,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 +514,16 @@ 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. + if config.Encryption != nil { + 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 +545,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 } diff --git a/machinery/src/database/main.go b/machinery/src/database/main.go index 63be08ab..23b68d5a 100644 --- a/machinery/src/database/main.go +++ b/machinery/src/database/main.go @@ -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 != "" { diff --git a/machinery/src/encryption/main.go b/machinery/src/encryption/main.go new file mode 100644 index 00000000..3d4e8a15 --- /dev/null +++ b/machinery/src/encryption/main.go @@ -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...) +} diff --git a/machinery/src/log/main.go b/machinery/src/log/main.go index d34b7fc1..11c562ef 100644 --- a/machinery/src/log/main.go +++ b/machinery/src/log/main.go @@ -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: diff --git a/machinery/src/models/ApiResponse.go b/machinery/src/models/ApiResponse.go index bc2edfe5..1b4fab25 100644 --- a/machinery/src/models/ApiResponse.go +++ b/machinery/src/models/ApiResponse.go @@ -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"` +} diff --git a/machinery/src/models/AudioData.go b/machinery/src/models/AudioData.go new file mode 100644 index 00000000..8e18a8d4 --- /dev/null +++ b/machinery/src/models/AudioData.go @@ -0,0 +1,6 @@ +package models + +type AudioDataPartial struct { + Timestamp int64 `json:"timestamp" bson:"timestamp"` + Data []int16 `json:"data" bson:"data"` +} diff --git a/machinery/src/models/Communication.go b/machinery/src/models/Communication.go index 93c526d2..c9723b86 100644 --- a/machinery/src/models/Communication.go +++ b/machinery/src/models/Communication.go @@ -22,11 +22,12 @@ type Communication struct { HandleStream chan string HandleSubStream chan string HandleMotion chan MotionDataPartial + HandleAudio chan AudioDataPartial HandleUpload chan string HandleHeartBeat chan string HandleLiveSD chan int64 HandleLiveHDKeepalive chan string - HandleLiveHDHandshake chan SDPPayload + HandleLiveHDHandshake chan RequestHDStreamPayload HandleLiveHDPeers chan string HandleONVIF chan OnvifAction IsConfiguring *abool.AtomicBool @@ -38,4 +39,5 @@ type Communication struct { SubDecoder *ffmpeg.VideoDecoder Image string CameraConnected bool + HasBackChannel bool } diff --git a/machinery/src/models/Config.go b/machinery/src/models/Config.go index 5d1ec2e5..d83c94da 100644 --- a/machinery/src/models/Config.go +++ b/machinery/src/models/Config.go @@ -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"` +} diff --git a/machinery/src/models/MQTT.go b/machinery/src/models/MQTT.go new file mode 100644 index 00000000..a8aff43c --- /dev/null +++ b/machinery/src/models/MQTT.go @@ -0,0 +1,161 @@ +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 audio input +type AudioPayload struct { + Timestamp int64 `json:"timestamp"` // timestamp of the recording request. + Data []int16 `json:"data"` +} + +// 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 +} diff --git a/machinery/src/models/Onvif.go b/machinery/src/models/Onvif.go index c1952aad..5ecb1172 100644 --- a/machinery/src/models/Onvif.go +++ b/machinery/src/models/Onvif.go @@ -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"` } diff --git a/machinery/src/onvif/main.go b/machinery/src/onvif/main.go index 591f7ae8..fab04175 100644 --- a/machinery/src/onvif/main.go +++ b/machinery/src/onvif/main.go @@ -5,10 +5,8 @@ import ( "encoding/json" "encoding/xml" "errors" - "fmt" "io" "io/ioutil" - "strconv" "strings" "time" @@ -18,7 +16,6 @@ import ( "github.com/kerberos-io/onvif/media" "github.com/kerberos-io/onvif" - dev "github.com/kerberos-io/onvif/device" "github.com/kerberos-io/onvif/ptz" xsd "github.com/kerberos-io/onvif/xsd/onvif" ) @@ -48,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()) } @@ -182,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, }, }) @@ -202,8 +324,252 @@ 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 } @@ -229,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, @@ -302,84 +668,87 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string { return capabilities } -func GetONVIFVersionFromDevice(device *onvif.Device) (string, error) { - // Get the ONVIF version from the device - resp, err := device.CallMethod(dev.GetServices{IncludeCapability: false}) +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, "GetServicesResponse") + decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse") if err != nil { - log.Log.Error("GetServicesResponse: " + err.Error()) - return "", err + log.Log.Error("GetPresetsFromDevice: " + err.Error()) + return presets, err } else { - // Decode the profiles from the server - var mServiceResp dev.GetServicesResponse - if err := decodedXML.DecodeElement(&mServiceResp, et); err != nil { - log.Log.Error("GetServicesResponse: " + err.Error()) + if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil { + log.Log.Error("GetPresetsFromDevice: " + err.Error()) + return presets, err } - // We'll try to get the version of the ONVIF server - version := mServiceResp.Service.Version + for _, preset := range presetsResponse.Preset { + p := models.OnvifActionPreset{ + Name: string(preset.Name), + Token: string(preset.Token), + } - // Convert version int to string - major := strconv.Itoa(version.Major) - minor := strconv.Itoa(version.Minor) - return major + "." + minor, nil + presets = append(presets, p) + } + return presets, err } + } else { + log.Log.Error("GetPresetsFromDevice: " + err.Error()) } + } else { + log.Log.Error("GetPresetsFromDevice: " + err.Error()) } - return "", err + + return presets, err } -func GetAudioOutputConfiguration(device *onvif.Device) (media.GetAudioDecoderConfigurationOptionsResponse, error) { - // Get the ONVIF version from the device - resp, err := device.CallMethod(media.GetAudioDecoderConfigurationOptions{}) - var mAudioEncoderConfigurationOptionsResponse media.GetAudioDecoderConfigurationOptionsResponse +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, "GetAudioDecoderConfigurationOptionsResponse") + decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses") if err != nil { - log.Log.Error("GetAudioDecoderConfigurationOptionsResponse: " + err.Error()) - return mAudioEncoderConfigurationOptionsResponse, err - + log.Log.Error("GoToPresetFromDevice: " + err.Error()) + return err } else { - // Decode the profiles from the server - if err := decodedXML.DecodeElement(&mAudioEncoderConfigurationOptionsResponse, et); err != nil { - log.Log.Error("GetAudioDecoderConfigurationOptionsResponse: " + err.Error()) + if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil { + log.Log.Error("GoToPresetFromDevice: " + err.Error()) + return err } - - // We'll try to get the version of the ONVIF server - audioDecoders := mAudioEncoderConfigurationOptionsResponse - return audioDecoders, nil + return err } + } else { + log.Log.Error("GoToPresetFromDevice: " + err.Error()) } + } else { + log.Log.Error("GoToPresetFromDevice: " + err.Error()) } - return mAudioEncoderConfigurationOptionsResponse, err -} -func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) { - xmlBytes := bytes.NewBufferString(xmlBody) - decodedXML := xml.NewDecoder(xmlBytes) - for { - token, err := decodedXML.Token() - if err != nil { - break - } - switch et := token.(type) { - case xml.StartElement: - if et.Name.Local == nodeName { - return decodedXML, &et, nil - } - } - } - return nil, nil, errors.New("error in NodeName - username and password might be wrong") + return err } func GetPTZFunctionsFromDevice(configurations ptz.GetConfigurationsResponse) ([]string, bool, bool) { @@ -422,74 +791,38 @@ func GetPTZFunctionsFromDevice(configurations ptz.GetConfigurationsResponse) ([] } // VerifyOnvifConnection godoc -// @Router /api/camera/onvif/verify [post] +// @Router /api/onvif/verify [post] // @ID verify-onvif // @Security Bearer // @securityDefinitions.apikey Bearer // @in header // @name Authorization -// @Tags camera +// @Tags config // @Param cameraConfig body models.IPCamera true "Camera Config" // @Summary Will verify the ONVIF connectivity. // @Description Will verify the ONVIF connectivity. // @Success 200 {object} models.APIResponse func VerifyOnvifConnection(c *gin.Context) { - var cameraConfig models.IPCamera - err := c.BindJSON(&cameraConfig) - if err == nil { - device, err := ConnectToOnvifDevice(&cameraConfig) - if err == nil { - version, err := GetONVIFVersionFromDevice(device) - if err == nil { - // Check if can zoom and/or pan/tilt is supported - c.JSON(200, models.APIResponse{ - Data: version, - }) - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while getting the ONVIF version " + err.Error(), - }) - } - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while verifying the ONVIF connection " + err.Error(), - }) - } - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while receiving the config " + err.Error(), - }) - } -} - -// VerifyOnvifConnection godoc -// @Router /api/camera/onvif/version [post] -// @ID version-onvif -// @Security Bearer -// @securityDefinitions.apikey Bearer -// @in header -// @name Authorization -// @Tags camera -// @Param cameraConfig body models.IPCamera true "Camera Config" -// @Summary Get the ONVIF version installed on the camera. -// @Description Get the ONVIF version installed on the camera. -// @Success 200 {object} models.APIResponse -func GetVersionONVIF(c *gin.Context) { var cameraConfig models.IPCamera err := c.BindJSON(&cameraConfig) if err == nil { device, err := ConnectToOnvifDevice(&cameraConfig) if err == nil { // Get the list of configurations - version, err := GetONVIFVersionFromDevice(device) + configurations, err := GetPTZConfigurationsFromDevice(device) if err == nil { + // Check if can zoom and/or pan/tilt is supported + ptzFunctions, canZoom, canPanTilt := GetPTZFunctionsFromDevice(configurations) c.JSON(200, models.APIResponse{ - Data: version, + Data: device, + PTZFunctions: ptzFunctions, + CanZoom: canZoom, + CanPanTilt: canPanTilt, }) } else { c.JSON(400, models.APIResponse{ - Message: "Something went wrong while getting the ONVIF version " + err.Error(), + Message: "Something went wrong while getting the configurations " + err.Error(), }) } } else { @@ -504,59 +837,20 @@ func GetVersionONVIF(c *gin.Context) { } } -// GetAudioOutputConfigurationONVIF godoc -// @Router /api/camera/onvif/audio-backchannel [post] -// @ID audio-output-onvif -// @Security Bearer -// @securityDefinitions.apikey Bearer -// @in header -// @name Authorization -// @Tags camera -// @Param cameraConfig body models.IPCamera true "Camera Config" -// @Summary Get the audio decoders for the audio backchannel. -// @Description Get the audio decoders for the audio backchannel. -// @Success 200 {object} models.APIResponse -func GetAudioOutputConfigurationONVIF(c *gin.Context) { - var cameraConfig models.IPCamera - err := c.BindJSON(&cameraConfig) - if err == nil { - device, err := ConnectToOnvifDevice(&cameraConfig) - - // Get token from the first profile - token, err := GetTokenFromProfile(device, 0) - fmt.Println(token) - if err == nil { - // Get the list of configurations - decoders, err := GetAudioOutputConfiguration(device) - if err == nil { - - // Filter the available decoders - var availableDecoders []string - options := decoders.Options - - // Check if G711 is supported - if options.G711DecOptions.SampleRateRange.Items != nil && len(options.G711DecOptions.SampleRateRange.Items) > 0 && - options.G711DecOptions.Bitrate.Items != nil && len(options.G711DecOptions.Bitrate.Items) > 0 { - availableDecoders = append(availableDecoders, "G711") - } - - // Check if can zoom and/or pan/tilt is supported - c.JSON(200, models.APIResponse{ - Data: availableDecoders, - }) - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while getting the audio output configuration " + err.Error(), - }) +func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) { + xmlBytes := bytes.NewBufferString(xmlBody) + decodedXML := xml.NewDecoder(xmlBytes) + for { + token, err := decodedXML.Token() + if err != nil { + break + } + switch et := token.(type) { + case xml.StartElement: + if et.Name.Local == nodeName { + return decodedXML, &et, nil } - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while verifying the ONVIF connection " + err.Error(), - }) } - } else { - c.JSON(400, models.APIResponse{ - Message: "Something went wrong while receiving the config " + err.Error(), - }) } + return nil, nil, errors.New("error in NodeName - username and password might be wrong") } diff --git a/machinery/src/routers/http/Methods.go b/machinery/src/routers/http/Methods.go index 79fa4258..fa0aaa7a 100644 --- a/machinery/src/routers/http/Methods.go +++ b/machinery/src/routers/http/Methods.go @@ -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(), + }) + } +} diff --git a/machinery/src/routers/http/Routes.go b/machinery/src/routers/http/Routes.go index cc6569b3..8254a0ef 100644 --- a/machinery/src/routers/http/Routes.go +++ b/machinery/src/routers/http/Routes.go @@ -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", @@ -201,7 +202,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 @@ -211,7 +212,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) @@ -223,6 +224,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/onvif/version", onvif.GetVersionONVIF) diff --git a/machinery/src/routers/http/Server.go b/machinery/src/routers/http/Server.go index d00a3c4f..31b0772d 100644 --- a/machinery/src/routers/http/Server.go +++ b/machinery/src/routers/http/Server.go @@ -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 + } } diff --git a/machinery/src/routers/main.go b/machinery/src/routers/main.go index 0e6ff11d..509dc1b6 100644 --- a/machinery/src/routers/main.go +++ b/machinery/src/routers/main.go @@ -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) } diff --git a/machinery/src/routers/mqtt/main.go b/machinery/src/routers/mqtt/main.go index 3b382e8a..60e4e298 100644 --- a/machinery/src/routers/mqtt/main.go +++ b/machinery/src/routers/mqtt/main.go @@ -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,371 @@ 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: +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) { + + // 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" + // } + + var message models.Message + json.Unmarshal(msg.Payload(), &message) + + // 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) + + // 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 + } + + // 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-audio-backchannel": + go HandleAudio(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) + } + } - log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.") + }) + } +} + +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 HandleAudio(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) { + value := payload.Value + + // Convert map[string]interface{} to AudioPayload + jsonData, _ := json.Marshal(value) + var audioPayload models.AudioPayload + json.Unmarshal(jsonData, &audioPayload) + + if audioPayload.Timestamp != 0 { + audioDataPartial := models.AudioDataPartial{ + Timestamp: audioPayload.Timestamp, + Data: audioPayload.Data, + } + communication.HandleAudio <- audioDataPartial + } +} + +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 { - log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.") + // 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)) + } } - msg.Ack() - }) + } } -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) { +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 sdp models.SDPPayload - json.Unmarshal(msg.Payload(), &sdp) - select { - case communication.HandleLiveHDHandshake <- sdp: - default: + 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)) } - log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.") + } else { - log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.") + log.Log.Info("HandleRequestConfig: no config available") } - msg.Ack() - }) + + log.Log.Info("HandleRequestConfig: Received a request for the config") + } } -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) +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("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.") + log.Log.Info("HandleUpdateConfig: Config update failed") } - }) + } } -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) { +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 { - peerCount := string(msg.Payload()) - communication.HandleLiveHDPeers <- peerCount - log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount) + select { + case communication.HandleLiveSD <- time.Now().Unix(): + default: + } + log.Log.Info("HandleRequestSDStream: received request to livestream.") } else { - log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.") + log.Log.Info("HandleRequestSDStream: received request to livestream, but camera is not connected.") } - }) + } } -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) { +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 { - 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() - } - log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload())) - channel <- string(msg.Payload()) + // 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("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.") + log.Log.Info("HandleRequestHDStream: received request to setup webrtc, 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 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 { + // Register candidate channel + key := configuration.Config.Key + "/" + receiveHDCandidatesPayload.SessionID + go webrtc.RegisterCandidates(key, receiveHDCandidatesPayload) + } 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(msg.Payload(), &onvifAction) + json.Unmarshal([]byte(action), &onvifAction) communication.HandleONVIF <- onvifAction - log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action) + log.Log.Info("HandleNavigatePTZ: Received an action - " + onvifAction.Action) + } else { - log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.") + 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.") diff --git a/machinery/src/utils/main.go b/machinery/src/utils/main.go index 8fa4f448..045d0d0b 100644 --- a/machinery/src/utils/main.go +++ b/machinery/src/utils/main.go @@ -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 + } + } +} diff --git a/machinery/src/webrtc/main.go b/machinery/src/webrtc/main.go index 4c52efd2..67ee4dcd 100644 --- a/machinery/src/webrtc/main.go +++ b/machinery/src/webrtc/main.go @@ -87,19 +87,47 @@ 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 RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) { + // Set lock + CandidatesMutex.Lock() + _, ok := CandidateArrays[key] + if !ok { + CandidateArrays[key] = make(chan string) + } + log.Log.Info("HandleReceiveHDCandidates: " + candidate.Candidate) + select { + case CandidateArrays[key] <- candidate.Candidate: + default: + log.Log.Info("HandleReceiveHDCandidates: channel is full.") + } + CandidatesMutex.Unlock() +} - config := configuration.Config +func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload) { - name := config.Key + config := configuration.Config + deviceKey := config.Key stunServers := []string{config.STUNURI} turnServers := []string{config.TURNURI} turnServersUsername := config.TURNUsername turnServersCredential := config.TURNPassword + // We create a channel which will hold the candidates for this session. + sessionKey := config.Key + "/" + handshake.SessionID + CandidatesMutex.Lock() + _, ok := CandidateArrays[sessionKey] + if !ok { + CandidateArrays[sessionKey] = make(chan string) + } + CandidatesMutex.Unlock() + + // 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 { @@ -129,34 +157,47 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati if err == nil && peerConnection != nil { if _, err = peerConnection.AddTrack(videoTrack); err != nil { - panic(err) + //panic(err) } if _, err = peerConnection.AddTrack(audioTrack); err != nil { - panic(err) + //panic(err) } if err != nil { - panic(err) + //panic(err) } peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) { if connectionState == pionWebRTC.ICEConnectionStateDisconnected { atomic.AddInt64(&peerConnectionCount, -1) - peerConnections[handshake.Cuuid] = nil - close(candidates) + + // Set lock + CandidatesMutex.Lock() + peerConnections[handshake.SessionID] = nil + _, ok := CandidateArrays[sessionKey] + if ok { + close(CandidateArrays[sessionKey]) + } + CandidatesMutex.Unlock() + close(w.PacketsCount) if err := peerConnection.Close(); err != nil { - panic(err) + //panic(err) } } else if connectionState == pionWebRTC.ICEConnectionStateConnected { atomic.AddInt64(&peerConnectionCount, 1) } else if connectionState == pionWebRTC.ICEConnectionStateChecking { - for candidate := range candidates { + // Iterate over the candidates and send them to the remote client + // Non blocking channel + for candidate := range CandidateArrays[sessionKey] { 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()) } } + } else if connectionState == pionWebRTC.ICEConnectionStateFailed { + } log.Log.Info("InitializeWebRTCConnection: connection state changed to: " + connectionState.String()) log.Log.Info("InitializeWebRTCConnection: Number of peers connected (" + strconv.FormatInt(peerConnectionCount, 10) + ")") @@ -164,48 +205,76 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati offer := w.CreateOffer(sd) if err = peerConnection.SetRemoteDescription(offer); err != nil { - panic(err) + //panic(err) } - //gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { - panic(err) + //panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { - panic(err) + //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 { + 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 { - log.Log.Info("InitializeWebRTCConnection:" + string(candi)) - token := mqttClient.Publish(topic, 2, false, candi) + log.Log.Info("InitializeWebRTCConnection:" + string(candateBinary)) + token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload) token.Wait() + } 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 { + token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload) + token.Wait() + } else { + log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload)) + } } } } else { @@ -358,16 +427,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) diff --git a/snap/hooks/configure b/snap/hooks/configure new file mode 100644 index 00000000..d85976c1 --- /dev/null +++ b/snap/hooks/configure @@ -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/ \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000..63971d3a --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,23 @@ +name: kerberosio # you probably want to 'snapcraft register ' +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 \ No newline at end of file diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index c44e0b09..18edea6f 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -25,6 +25,7 @@ "jsx-a11y/media-has-caption": "off", "jsx-a11y/anchor-is-valid": "off", "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/control-has-associated-label": "off", "jsx-a11y/no-noninteractive-element-interactions": "off", "jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/label-has-associated-control": [ diff --git a/ui/public/locales/de/translation.json b/ui/public/locales/de/translation.json index 1ca9b87c..4b154b4b 100644 --- a/ui/public/locales/de/translation.json +++ b/ui/public/locales/de/translation.json @@ -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", diff --git a/ui/public/locales/en/translation.json b/ui/public/locales/en/translation.json index 5b6b1a09..13c6f375 100644 --- a/ui/public/locales/en/translation.json +++ b/ui/public/locales/en/translation.json @@ -23,7 +23,7 @@ }, "dashboard": { "title": "Dashboard", - "heading": "Overview of your video surveilance", + "heading": "Overview of your video surveillance", "number_of_days": "Number of days", "total_recordings": "Total recordings", "connected": "Connected", @@ -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", @@ -142,7 +151,7 @@ "stun_turn_description_webrtc": "Forward h264 stream through MQTT", "stun_turn_transcode": "Transcode stream", "stun_turn_description_transcode": "Convert stream to a lower resolution", - "stun_turn_downscale": "Downscale resolution (in % or original resolution)", + "stun_turn_downscale": "Downscale resolution (in % of original resolution)", "mqtt": "MQTT", "description_mqtt": "A MQTT broker is used to communicate from", "description2_mqtt": "to the Kerberos Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.", diff --git a/ui/public/locales/es/translation.json b/ui/public/locales/es/translation.json index 8c64faa5..eb492d62 100644 --- a/ui/public/locales/es/translation.json +++ b/ui/public/locales/es/translation.json @@ -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", diff --git a/ui/public/locales/fr/translation.json b/ui/public/locales/fr/translation.json index 392b83a8..8070c6c7 100644 --- a/ui/public/locales/fr/translation.json +++ b/ui/public/locales/fr/translation.json @@ -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", diff --git a/ui/public/locales/hi/translation.json b/ui/public/locales/hi/translation.json new file mode 100644 index 00000000..8452f5d3 --- /dev/null +++ b/ui/public/locales/hi/translation.json @@ -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": "अपलोड पर डिलीट सक्षम" + } + } +} diff --git a/ui/public/locales/it/translation.json b/ui/public/locales/it/translation.json new file mode 100644 index 00000000..6e04d1c7 --- /dev/null +++ b/ui/public/locales/it/translation.json @@ -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" + } + } +} \ No newline at end of file diff --git a/ui/public/locales/ja/translation.json b/ui/public/locales/ja/translation.json index b1c2f371..8307d55f 100644 --- a/ui/public/locales/ja/translation.json +++ b/ui/public/locales/ja/translation.json @@ -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": "カメラ", diff --git a/ui/public/locales/nl/translation.json b/ui/public/locales/nl/translation.json index 534c0bc3..3f89b2e9 100644 --- a/ui/public/locales/nl/translation.json +++ b/ui/public/locales/nl/translation.json @@ -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", diff --git a/ui/public/locales/pl/translation.json b/ui/public/locales/pl/translation.json index 8db876cb..5690c145 100644 --- a/ui/public/locales/pl/translation.json +++ b/ui/public/locales/pl/translation.json @@ -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", diff --git a/ui/public/locales/pt/translation.json b/ui/public/locales/pt/translation.json index 0b4bd2ca..7b4e0040 100644 --- a/ui/public/locales/pt/translation.json +++ b/ui/public/locales/pt/translation.json @@ -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", diff --git a/ui/public/locales/ru/translation.json b/ui/public/locales/ru/translation.json new file mode 100644 index 00000000..3b2f3b1a --- /dev/null +++ b/ui/public/locales/ru/translation.json @@ -0,0 +1,224 @@ +{ + "breadcrumb": { + "watch_recordings": "Смотреть записи", + "configure": "Настроить" + }, + "buttons": { + "save": "Сохранить", + "verify_connection": "Проверить подключение" + }, + "navigation": { + "profile": "Профиль", + "admin": "admin", + "management": "Управление", + "dashboard": "Панель", + "recordings": "Записи", + "settings": "Настройки", + "help_support": "Помощь & Поддержка", + "swagger": "Swagger API", + "documentation": "Документация", + "ui_library": "UI Библиотека", + "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 Agent правильно настроен.", + "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 Hub в действии!", + "configuration_updated_success": "Настройки успешно обновлены.", + "configuration_updated_error": "При сохранении что-то пошло не так.", + "verify_hub": "Проверка настроек Kerberos Hub.", + "verify_hub_success": "Настройки Kerberos Hub успешно проверены.", + "verify_hub_error": "Что-то пошло не так при проверке концентратора Kerberos Hub", + "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 Agent", + "key": "Ключ", + "camera_name": "Название камеры", + "timezone": "Часовой пояс", + "select_timezone": "Выберите часовой пояс", + "advanced_configuration": "Расширенные настройки", + "description_advanced_configuration": "Расширенные настройки для включения или отключения определенных частей Kerberos Agent", + "offline_mode": "Автономный режим", + "description_offline_mode": "Отключить весь исходящий трафик", + "encryption": "Шифрование", + "description_encryption": "Включите шифрование для всего исходящего трафика. MQTT-сообщения и/или записи будут зашифрованы с использованием AES-256. Для подписи используется закрытый ключ.", + "encryption_enabled": "Включить шифрование MQTT", + "description_encryption_enabled": "Включает шифрование для всех сообщений MQTT.", + "encryption_recordings_enabled": "Включить шифрование записей", + "description_encryption_recordings_enabled": "Включает шифрование для всех записей.", + "encryption_fingerprint": "Отпечаток", + "encryption_privatekey": "Закрытый ключ", + "encryption_symmetrickey": "Симметричный ключ" + }, + "camera": { + "camera": "Камера", + "description_camera": "Настройки камеры необходимы для установки соединения с выбранной камерой.", + "only_h264": "В настоящее время поддерживаются только потоки H264 RTSP.", + "rtsp_url": "Адрес основного потока RTSP", + "rtsp_h264": "Подключение к камере по протоколу H264 RTSP.", + "sub_rtsp_url": "Адрес дополнительного потока RTSP (используется для прямой трансляции)", + "sub_rtsp_h264": "Дополнительное RTSP-соединение с низким разрешением камеры.", + "onvif": "ONVIF", + "description_onvif": "Учетные данные для связи по протоколу ONVIF. Они используются для PTZ или других возможностей, предоставляемых камерой.", + "onvif_xaddr": "ONVIF xaddr", + "onvif_username": "ONVIF пользователь", + "onvif_password": "ONVIF пароль", + "verify_connection": "Проверка основного соединения", + "verify_sub_connection": "Проверка дополнительного подключения" + }, + "recording": { + "recording": "Запись", + "description_recording": "Укажите, как вы хотите вести запись. Непрерывная круглосуточная запись или запись по движению.", + "continuous_recording": "Непрерывная запись", + "description_continuous_recording": "Осуществлять 24/7 запись или запись по движению.", + "max_duration": "максимальная продолжительность видео (секунд)", + "description_max_duration": "Максимальная продолжительность записи.", + "pre_recording": "Предзапись (секунд)", + "description_pre_recording": "Секунд до наступления события.", + "post_recording": "Записывать после (секунд)", + "description_post_recording": "Секунд после наступления события.", + "threshold": "Уровень срабатывания записи (пикселей)", + "description_threshold": "Количество пикселей, измененных для записи", + "autoclean": "Автоочистка", + "description_autoclean": "Укажите, может ли Kerberos Agent очищать записи при достижении определенного объема памяти (МБ). При этом по достижении указанной емкости будут удаляться самые старые записи.", + "autoclean_enable": "Включить автоматическую очистку", + "autoclean_description_enable": "При достижении емкости удаляется самая старая запись.", + "autoclean_max_directory_size": "Максимальный размер каталога (МБ)", + "autoclean_description_max_directory_size": "Максимальное количество хранимых мегабайт записей.", + "fragmentedrecordings": "Фрагментированные записи", + "description_fragmentedrecordings": "Когда записи фрагментированы, они подходят для HLS-потока. При включении контейнер MP4 будет выглядеть несколько иначе.", + "fragmentedrecordings_enable": "Включить фрагментацию", + "fragmentedrecordings_description_enable": "Фрагментированные записи необходимы для HLS.", + "fragmentedrecordings_duration": "продолжительность фрагмента", + "fragmentedrecordings_description_duration": "Продолжительность одного фрагмента." + }, + "streaming": { + "stun_turn": "STUN/TURN для WebRTC", + "description_stun_turn": "Для организации трансляций в полном разрешении мы используем технологию WebRTC. Одной из ключевых возможностей является функция ICE-candidate, которая позволяет обходить NAT, используя концепции STUN/TURN.", + "stun_server": "STUN сервер", + "turn_server": "TURN сервер", + "turn_username": "Имя пользователя", + "turn_password": "Пароль", + "stun_turn_forward": "Переадресация и транскодирование", + "stun_turn_description_forward": "Оптимизация и усовершенствование связи TURN/STUN.", + "stun_turn_webrtc": "Переадресация на WebRTC-брокера", + "stun_turn_description_webrtc": "Передача потока h264 через MQTT", + "stun_turn_transcode": "Транскодирование потока", + "stun_turn_description_transcode": "Преобразование потока в меньшее разрешение", + "stun_turn_downscale": "Уменьшение разрешения (в % от исходного разрешения)", + "mqtt": "MQTT", + "description_mqtt": "Брокер MQTT используется для обмена данными с", + "description2_mqtt": "к Kerberos Agent, чтобы, например, получить возможность трансляции видео или ONVIF (PTZ).", + "mqtt_brokeruri": "Адрес брокера", + "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 Hub", + "description_kerberoshub": "Kerberos Agent'ы могут отправлять heartbeat сообщения в центральный", + "description2_kerberoshub": "узел. Heartbeat и другая необходимая информация синхронизируются с Kerberos Hub для отображения информации о видеоландшафте в реальном времени.", + "persistence": "Хранилище", + "saasoffering": "Kerberos Hub (SAAS предложение)", + "description_persistence": "Возможность хранения записей - это начало всего. Вы можете выбрать один из наших вариантов", + "description2_persistence": ", или стороннего провайдера", + "select_persistence": "Выберите хранилище", + "kerberoshub_proxyurl": "Kerberos Hub Proxy URL", + "kerberoshub_description_proxyurl": "Конечная точка Proxy для загрузки записей.", + "kerberoshub_apiurl": "Kerberos Hub API URL", + "kerberoshub_description_apiurl": "Конечная точка API для загрузки записей.", + "kerberoshub_publickey": "Открытый ключ", + "kerberoshub_description_publickey": "Открытый ключ, присвоенный вашей учетной записи Kerberos Hub.", + "kerberoshub_privatekey": "Закрытый ключ", + "kerberoshub_description_privatekey": "Закрытый ключ, присвоенный вашей учетной записи Kerberos Hub.", + "kerberoshub_site": "Сайт", + "kerberoshub_description_site": "Идентификатор сайта, к которому принадлежат агенты Kerberos (Agent) в Kerberos Hub.", + "kerberoshub_region": "Регион", + "kerberoshub_description_region": "Регион, в котором хранятся наши записи.", + "kerberoshub_bucket": "Bucket", + "kerberoshub_description_bucket": "Bucket, в котором мы храним наши записи.", + "kerberoshub_username": "Имя пользователя/каталог (должно соответствовать имени пользователя в Kerberos Hub)", + "kerberoshub_description_username": "Имя пользователя вашей учетной записи Kerberos Hub.", + "kerberosvault_apiurl": "Kerberos Vault API URL", + "kerberosvault_description_apiurl": "The Kerberos Vault API", + "kerberosvault_provider": "Провайдер", + "kerberosvault_description_provider": "Провайдер, которому будут отправляться ваши записи.", + "kerberosvault_directory": "Каталог (должен совпадать с именем пользователя в Kerberos Hub)", + "kerberosvault_description_directory": "Подкаталог, в котором будут храниться записи у вашего провайдера.", + "kerberosvault_accesskey": "Ключ доступа", + "kerberosvault_description_accesskey": "Ключ доступа вашей учетной записи Kerberos Vault.", + "kerberosvault_secretkey": "Секретный ключ", + "kerberosvault_description_secretkey": "Секретный ключ учетной записи Kerberos Vault.", + "dropbox_directory": "Каталог", + "dropbox_description_directory": "Подкаталог, в котором будут храниться записи в вашем аккаунте Dropbox.", + "dropbox_accesstoken": "Токен доступа", + "dropbox_description_accesstoken": "Токен доступа вашего аккаунта/приложения Dropbox.", + "verify_connection": "Проверка соединения", + "remove_after_upload": "Как только записи будут загружены на какой-либо сервер, вы, возможно, захотите удалить их из локального агента Kerberos.", + "remove_after_upload_description": "Удаление записей после их успешной загрузки.", + "remove_after_upload_enabled": "Включено удаление при выгрузке" + } + } +} diff --git a/ui/public/locales/zh/translation.json b/ui/public/locales/zh/translation.json index 54a15bb2..8856be75 100644 --- a/ui/public/locales/zh/translation.json +++ b/ui/public/locales/zh/translation.json @@ -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": "相机", diff --git a/ui/src/components/LanguageSelect/LanguageSelect.jsx b/ui/src/components/LanguageSelect/LanguageSelect.jsx index f05a5c38..92ebed82 100644 --- a/ui/src/components/LanguageSelect/LanguageSelect.jsx +++ b/ui/src/components/LanguageSelect/LanguageSelect.jsx @@ -20,9 +20,12 @@ 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 }, + ru: { label: 'Русский', dir: 'ltr', active: false }, }; if (!languageMap[selected]) { diff --git a/ui/src/config.js b/ui/src/config.js index a587eed1..9462e62b 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -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, diff --git a/ui/src/i18n.js b/ui/src/i18n.js index df60712e..029487c3 100644 --- a/ui/src/i18n.js +++ b/ui/src/i18n.js @@ -14,7 +14,7 @@ i18n escapeValue: false, }, load: 'languageOnly', - whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja'], + whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja', 'ru'], }); export default i18n; diff --git a/ui/src/pages/Settings/Settings.jsx b/ui/src/pages/Settings/Settings.jsx index e87a3099..af723ecd 100644 --- a/ui/src/pages/Settings/Settings.jsx +++ b/ui/src/pages/Settings/Settings.jsx @@ -729,7 +729,7 @@ class Settings extends React.Component { /> )} {verifyOnvifError && ( - + )} {loadingHub && ( @@ -810,6 +810,24 @@ class Settings extends React.Component { this.onUpdateDropdown('', 'timezone', value[0], config) } /> +
+
+

+ {t('settings.overview.description_advanced_configuration')} +

+
+ + this.onUpdateToggle('', 'offline', event, config) + } + /> +
+ {t('settings.overview.offline_mode')} +

{t('settings.overview.description_offline_mode')}

+
+