mirror of
https://github.com/kerberos-io/agent.git
synced 2026-03-03 08:50:08 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38247ac9f6 | ||
|
|
580f17028a | ||
|
|
48d933a561 | ||
|
|
0c70ab6158 | ||
|
|
ba6cdef9d5 | ||
|
|
bedb3c0d7f | ||
|
|
2539255940 | ||
|
|
24136f8b15 | ||
|
|
910bb3c079 | ||
|
|
47f4c19617 | ||
|
|
280a81809a | ||
|
|
59358acb30 | ||
|
|
ebd655ac73 | ||
|
|
6325e37aae | ||
|
|
ecabc47847 | ||
|
|
31cc3d8939 | ||
|
|
c71cb71d08 | ||
|
|
65a739ea75 | ||
|
|
410a62e9ef | ||
|
|
aa76dd1ec8 | ||
|
|
384448d123 | ||
|
|
414f74758c | ||
|
|
25403ccdab | ||
|
|
4c03132b83 | ||
|
|
470f8f1cb6 | ||
|
|
5308376a67 | ||
|
|
2b112d29cf | ||
|
|
20d2517e74 | ||
|
|
12902e2482 | ||
|
|
baca44beef | ||
|
|
d7580744e2 | ||
|
|
04f4bc9bf2 | ||
|
|
d879174f4c | ||
|
|
5a1a62a723 | ||
|
|
c519b01092 | ||
|
|
c2ff7ff785 | ||
|
|
44ec8c0534 | ||
|
|
21c0e01137 | ||
|
|
f7ced6056d | ||
|
|
00917e3f88 | ||
|
|
bcfed04a07 | ||
|
|
bf97bd72f1 | ||
|
|
4b8b6bf66a | ||
|
|
4b6c25bb85 | ||
|
|
729b38999e | ||
|
|
4cbf0323f1 | ||
|
|
1f5cb8ca88 | ||
|
|
8be0a04502 | ||
|
|
bdc0039a24 | ||
|
|
756b893ecd | ||
|
|
36323b076f | ||
|
|
95f43b6444 | ||
|
|
5c23a62ac3 | ||
|
|
2b425a2ddd | ||
|
|
abeeb95204 | ||
|
|
6aed20c466 | ||
|
|
6672535544 | ||
|
|
ed397b6ecc | ||
|
|
530e4c654e | ||
|
|
913bd1ba12 | ||
|
|
84e532be47 |
12
.github/workflows/docker.yml
vendored
12
.github/workflows/docker.yml
vendored
@@ -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:
|
||||
|
||||
@@ -10,7 +10,7 @@ ENV GOSUMDB=off
|
||||
##########################################
|
||||
# Installing some additional dependencies.
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get upgrade -y && apt-get update && apt-get install -y --no-install-recommends \
|
||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
||||
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
@@ -32,7 +32,7 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
|
||||
|
||||
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
||||
go mod download && \
|
||||
go build -tags timetzdata,netgo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
mkdir -p /agent && \
|
||||
mv main /agent && \
|
||||
mv version /agent && \
|
||||
@@ -122,6 +122,7 @@ RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json
|
||||
# Set permissions correctly
|
||||
|
||||
RUN chown -R agent:kerberosio /home/agent/data
|
||||
RUN chown -R agent:kerberosio /home/agent/www
|
||||
|
||||
###########################
|
||||
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
||||
@@ -146,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"]
|
||||
|
||||
42
README.md
42
README.md
@@ -18,6 +18,7 @@
|
||||
[](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE)
|
||||
<a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a>
|
||||
[](https://discord.gg/Bj77Vqfp2G)
|
||||
[](https://snapcraft.io/kerberosio)
|
||||
|
||||
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
|
||||
|
||||
@@ -41,6 +42,7 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
|
||||
|
||||
1. [Quickstart - Docker](#quickstart---docker)
|
||||
2. [Quickstart - Balena](#quickstart---balena)
|
||||
3. [Quickstart - Snap](#quickstart---snap)
|
||||
|
||||
### Introduction
|
||||
|
||||
@@ -78,12 +80,19 @@ If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our
|
||||
|
||||
## Quickstart - Balena
|
||||
|
||||
Run Kerberos Agent with Balena super powers. Monitor your agent with seamless remote access, and an encrypted https endpoint.
|
||||
Checkout our fleet on [Balena Hub](https://hub.balena.io/fleets?0%5B0%5D%5Bn%5D=any&0%5B0%5D%5Bo%5D=full_text_search&0%5B0%5D%5Bv%5D=agent), and add your agent.
|
||||
Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Monitor your Kerberos Agent with seamless remote access, over the air updates, an encrypted public `https` endpoint and many more. Checkout our application `video-surveillance` on [Balena Hub](https://hub.balena.io/apps/2064752/video-surveillance), and create your first or fleet of Kerberos Agent(s).
|
||||
|
||||
[](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
|
||||
[](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
|
||||
|
||||
@@ -122,6 +131,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.
|
||||
|
||||
@@ -165,6 +176,8 @@ 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. | "" |
|
||||
@@ -183,8 +196,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" |
|
||||
@@ -202,7 +218,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. | "" |
|
||||
@@ -211,6 +227,10 @@ 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 through MQTT (recordings will follow). | "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
|
||||
|
||||
@@ -233,9 +253,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`,
|
||||
@@ -248,7 +268,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:
|
||||
|
||||
@@ -289,7 +309,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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
31
deployments/balena/README.md
Normal file
31
deployments/balena/README.md
Normal file
@@ -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).
|
||||
|
||||
[](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).
|
||||
@@ -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
|
||||
|
||||
15
deployments/snap/README.md
Normal file
15
deployments/snap/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Deployment with Snap Store
|
||||
|
||||
By browsing to the Snap Store, you'll be able [to find our own snap `Kerberos Agent`](https://snapcraft.io/kerberosio). You can either install the `Kerberos Agent` through the command line.
|
||||
|
||||
snap install kerberosio
|
||||
|
||||
Or use the Desktop client to have a visual interface.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
BIN
deployments/snap/snapstore.png
Normal file
BIN
deployments/snap/snapstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 616 KiB |
41
deployments/terraform/README.md
Normal file
41
deployments/terraform/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Deployment with Terraform
|
||||
|
||||
If you are using Terraform as part of your DevOps stack, you might utilise it to deploy your Kerberos Agents. Within this deployment folder we have added an example Terraform file `docker.tf`, which installs the Kerberos Agent `docker` container on a remote system over `SSH`. We might create our own provider in the future, or add additional examples for example `snap`, `kubernetes`, etc.
|
||||
|
||||
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
|
||||
|
||||
terraform init
|
||||
|
||||
Once initialised you should see similar output:
|
||||
|
||||
Initializing the backend...
|
||||
|
||||
Initializing provider plugins...
|
||||
- Reusing previous version of telkomindonesia/linux from the dependency lock file
|
||||
- Using previously-installed telkomindonesia/linux v0.7.0
|
||||
|
||||
Go and open the `docker.tf` file and locate the `linux` provider, modify following credentials accordingly. Make sure they match for creating an `SSH` connection.
|
||||
|
||||
provider "linux" {
|
||||
host = "x.y.z.u"
|
||||
port = 22
|
||||
user = "root"
|
||||
password = "password"
|
||||
}
|
||||
|
||||
Apply the `docker.tf` file, to install `docker` and the `kerberos/agent` docker container.
|
||||
|
||||
terraform apply
|
||||
|
||||
Once done you should see following output, and you should be able to reach the remote machine on port `80` or if configured differently the specified port you've defined.
|
||||
|
||||
Do you want to perform these actions?
|
||||
Terraform will perform the actions described above.
|
||||
Only 'yes' will be accepted to approve.
|
||||
|
||||
Enter a value: yes
|
||||
|
||||
linux_script.install_docker_kerberos_agent: Modifying... [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
|
||||
linux_script.install_docker_kerberos_agent: Modifications complete after 3s [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
|
||||
|
||||
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
|
||||
47
deployments/terraform/docker.tf
Normal file
47
deployments/terraform/docker.tf
Normal file
@@ -0,0 +1,47 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
linux = {
|
||||
source = "TelkomIndonesia/linux"
|
||||
version = "0.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "linux" {
|
||||
host = "x.y.z.u"
|
||||
port = 22
|
||||
user = "root"
|
||||
password = "password"
|
||||
}
|
||||
|
||||
locals {
|
||||
image = "kerberos/agent"
|
||||
version = "latest"
|
||||
port = 80
|
||||
}
|
||||
|
||||
resource "linux_script" "install_docker" {
|
||||
lifecycle_commands {
|
||||
create = "apt update && apt install -y $PACKAGE_NAME"
|
||||
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
|
||||
update = "apt update && apt install -y $PACKAGE_NAME"
|
||||
delete = "apt remove -y $PACKAGE_NAME"
|
||||
}
|
||||
environment = {
|
||||
PACKAGE_NAME = "docker"
|
||||
}
|
||||
}
|
||||
|
||||
resource "linux_script" "install_docker_kerberos_agent" {
|
||||
lifecycle_commands {
|
||||
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
|
||||
read = "docker inspect agent"
|
||||
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
|
||||
delete = "docker rm agent --force"
|
||||
}
|
||||
environment = {
|
||||
IMAGE = local.image
|
||||
VERSION = local.version
|
||||
PORT = local.port
|
||||
}
|
||||
}
|
||||
2
machinery/.vscode/launch.json
vendored
2
machinery/.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["run", "cameraname", "8080"],
|
||||
"args": ["-action", "run"],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"buildFlags": "--tags dynamic",
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
"condition_uri": "",
|
||||
"encryption": {}
|
||||
}
|
||||
|
||||
BIN
machinery/data/test-480p.mp4
Normal file
BIN
machinery/data/test-480p.mp4
Normal file
Binary file not shown.
@@ -54,6 +54,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/gotopreset": {
|
||||
"post": {
|
||||
"description": "Will activate the desired ONVIF preset.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will activate the desired ONVIF preset.",
|
||||
"operationId": "camera-onvif-gotopreset",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifPreset",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifPreset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/login": {
|
||||
"post": {
|
||||
"description": "Try to login into ONVIF supported camera.",
|
||||
@@ -112,6 +141,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/presets": {
|
||||
"post": {
|
||||
"description": "Will return the ONVIF presets for the specific camera.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will return the ONVIF presets for the specific camera.",
|
||||
"operationId": "camera-onvif-presets",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifCredentials",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/zoom": {
|
||||
"post": {
|
||||
"description": "Zooming in or out the camera.",
|
||||
@@ -317,8 +375,15 @@ const docTemplate = `{
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_pan_tilt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_zoom": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {},
|
||||
"message": {}
|
||||
"message": {},
|
||||
"ptz_functions": {}
|
||||
}
|
||||
},
|
||||
"models.Authentication": {
|
||||
@@ -621,6 +686,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onvif_credentials": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
},
|
||||
"preset": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifZoom": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -46,6 +46,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/gotopreset": {
|
||||
"post": {
|
||||
"description": "Will activate the desired ONVIF preset.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will activate the desired ONVIF preset.",
|
||||
"operationId": "camera-onvif-gotopreset",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifPreset",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifPreset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/login": {
|
||||
"post": {
|
||||
"description": "Try to login into ONVIF supported camera.",
|
||||
@@ -104,6 +133,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/presets": {
|
||||
"post": {
|
||||
"description": "Will return the ONVIF presets for the specific camera.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will return the ONVIF presets for the specific camera.",
|
||||
"operationId": "camera-onvif-presets",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifCredentials",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/zoom": {
|
||||
"post": {
|
||||
"description": "Zooming in or out the camera.",
|
||||
@@ -309,8 +367,15 @@
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_pan_tilt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_zoom": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {},
|
||||
"message": {}
|
||||
"message": {},
|
||||
"ptz_functions": {}
|
||||
}
|
||||
},
|
||||
"models.Authentication": {
|
||||
@@ -613,6 +678,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onvif_credentials": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
},
|
||||
"preset": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifZoom": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,8 +2,13 @@ basePath: /
|
||||
definitions:
|
||||
models.APIResponse:
|
||||
properties:
|
||||
can_pan_tilt:
|
||||
type: boolean
|
||||
can_zoom:
|
||||
type: boolean
|
||||
data: {}
|
||||
message: {}
|
||||
ptz_functions: {}
|
||||
type: object
|
||||
models.Authentication:
|
||||
properties:
|
||||
@@ -202,6 +207,13 @@ definitions:
|
||||
tilt:
|
||||
type: number
|
||||
type: object
|
||||
models.OnvifPreset:
|
||||
properties:
|
||||
onvif_credentials:
|
||||
$ref: '#/definitions/models.OnvifCredentials'
|
||||
preset:
|
||||
type: string
|
||||
type: object
|
||||
models.OnvifZoom:
|
||||
properties:
|
||||
onvif_credentials:
|
||||
@@ -310,6 +322,25 @@ paths:
|
||||
summary: Will return the ONVIF capabilities for the specific camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/gotopreset:
|
||||
post:
|
||||
description: Will activate the desired ONVIF preset.
|
||||
operationId: camera-onvif-gotopreset
|
||||
parameters:
|
||||
- description: OnvifPreset
|
||||
in: body
|
||||
name: config
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.OnvifPreset'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
summary: Will activate the desired ONVIF preset.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/login:
|
||||
post:
|
||||
description: Try to login into ONVIF supported camera.
|
||||
@@ -348,6 +379,25 @@ paths:
|
||||
summary: Panning or/and tilting the camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/presets:
|
||||
post:
|
||||
description: Will return the ONVIF presets for the specific camera.
|
||||
operationId: camera-onvif-presets
|
||||
parameters:
|
||||
- description: OnvifCredentials
|
||||
in: body
|
||||
name: config
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.OnvifCredentials'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
summary: Will return the ONVIF presets for the specific camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/zoom:
|
||||
post:
|
||||
description: Zooming in or out the camera.
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/kerberos-io/agent/machinery
|
||||
go 1.19
|
||||
|
||||
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
|
||||
// replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
|
||||
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
|
||||
|
||||
require (
|
||||
github.com/InVisionApp/conjungo v1.1.0
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/kellydunn/golang-geo v0.7.0
|
||||
github.com/kerberos-io/joy4 v1.0.58
|
||||
github.com/kerberos-io/onvif v0.0.5
|
||||
github.com/kerberos-io/onvif v0.0.7
|
||||
github.com/minio/minio-go/v6 v6.0.57
|
||||
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
|
||||
@@ -266,8 +266,8 @@ github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+Mrar
|
||||
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
|
||||
github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
|
||||
github.com/kerberos-io/joy4 v1.0.58/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
|
||||
github.com/kerberos-io/onvif v0.0.5 h1:kq9mnHZkih9Jl4DyIJ4Rzt++Y3DDKy3nI8S2ESEfZ5w=
|
||||
github.com/kerberos-io/onvif v0.0.5/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
|
||||
github.com/kerberos-io/onvif v0.0.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
|
||||
github.com/kerberos-io/onvif v0.0.7/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
|
||||
|
||||
@@ -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,10 @@ func main() {
|
||||
log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
|
||||
|
||||
case "discover":
|
||||
timeout := os.Args[2]
|
||||
log.Log.Info(timeout)
|
||||
|
||||
case "run":
|
||||
{
|
||||
name := os.Args[2]
|
||||
port := os.Args[3]
|
||||
|
||||
// Print Kerberos.io ASCII art
|
||||
utils.PrintASCIIArt()
|
||||
|
||||
@@ -82,28 +94,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 +133,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 :(")
|
||||
|
||||
1
machinery/src/api/onvif.go
Normal file
1
machinery/src/api/onvif.go
Normal file
@@ -0,0 +1 @@
|
||||
package api
|
||||
@@ -17,7 +17,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 +25,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 +51,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 +134,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 +192,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 +259,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 +315,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")
|
||||
@@ -406,11 +406,11 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -272,17 +277,52 @@ loop:
|
||||
|
||||
// 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 {
|
||||
capabilities := onvif.GetCapabilitiesFromDevice(device)
|
||||
for _, v := range capabilities {
|
||||
if v == "PTZ" || v == "ptz" {
|
||||
onvifEnabled = "true"
|
||||
configurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
if err == nil {
|
||||
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("[]")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Log.Error("HandleHeartBeat: error while getting presets: " + err.Error())
|
||||
} else {
|
||||
log.Log.Debug("HandleHeartBeat: no presets found.")
|
||||
}
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: error while getting PTZ configurations: " + err.Error())
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: error while connecting to ONVIF device: " + err.Error())
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Debug("HandleHeartBeat: ONVIF is not enabled.")
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
|
||||
// Check if the agent is running inside a cluster (Kerberos Factory) or as
|
||||
@@ -325,6 +365,10 @@ loop:
|
||||
"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,
|
||||
@@ -332,14 +376,23 @@ loop:
|
||||
"docker" : true,
|
||||
"kios" : false,
|
||||
"raspberrypi" : false
|
||||
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, cameraConnected)
|
||||
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
req, _ := http.NewRequest("POST", url, buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
var client *http.Client
|
||||
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
@@ -348,6 +401,9 @@ loop:
|
||||
communication.CloudTimestamp.Store(time.Now().Unix())
|
||||
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.")
|
||||
} else {
|
||||
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.")
|
||||
}
|
||||
|
||||
@@ -358,8 +414,6 @@ loop:
|
||||
buffy = bytes.NewBuffer(jsonStr)
|
||||
req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client = &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
@@ -404,19 +458,17 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
// Allocate frame
|
||||
frame := ffmpeg.AllocVideoFrame()
|
||||
|
||||
key := ""
|
||||
hubKey := ""
|
||||
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
|
||||
key = config.S3.Publickey
|
||||
hubKey = config.S3.Publickey
|
||||
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
|
||||
key = config.KStorage.CloudKey
|
||||
hubKey = config.KStorage.CloudKey
|
||||
}
|
||||
// This is the new way ;)
|
||||
if config.HubKey != "" {
|
||||
key = config.HubKey
|
||||
hubKey = config.HubKey
|
||||
}
|
||||
|
||||
topic := "kerberos/" + key + "/device/" + config.Key + "/live"
|
||||
|
||||
lastLivestreamRequest := int64(0)
|
||||
|
||||
var cursorError error
|
||||
@@ -437,7 +489,27 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
continue
|
||||
}
|
||||
log.Log.Info("HandleLiveStreamSD: Sending base64 encoded images to MQTT.")
|
||||
sendImage(frame, topic, mqttClient, pkt, decoder, decoderMutex)
|
||||
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
|
||||
if err == nil {
|
||||
bytes, _ := computervision.ImageToBytes(&frame.Image)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["image"] = encoded
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-sd-stream",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the frame.
|
||||
@@ -451,15 +523,6 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
log.Log.Debug("HandleLiveStreamSD: finished")
|
||||
}
|
||||
|
||||
func sendImage(frame *ffmpeg.VideoFrame, topic string, mqttClient mqtt.Client, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
|
||||
if err == nil {
|
||||
bytes, _ := computervision.ImageToBytes(&frame.Image)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
mqttClient.Publish(topic, 0, false, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
|
||||
config := configuration.Config
|
||||
@@ -478,25 +541,23 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
// We get a request with an offer, but we'll forward it.
|
||||
for m := range communication.HandleLiveHDHandshake {
|
||||
/*for m := range communication.HandleLiveHDHandshake {
|
||||
// Forward SDP
|
||||
m.CloudKey = config.Key
|
||||
request, err := json.Marshal(m)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/webrtc/request", 2, false, request)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
log.Log.Info("HandleLiveStreamHD: Waiting for peer connections.")
|
||||
for handshake := range communication.HandleLiveHDHandshake {
|
||||
log.Log.Info("HandleLiveStreamHD: setting up a peer connection.")
|
||||
key := config.Key + "/" + handshake.Cuuid
|
||||
webrtc.CandidatesMutex.Lock()
|
||||
key := config.Key + "/" + handshake.SessionID
|
||||
_, ok := webrtc.CandidateArrays[key]
|
||||
if !ok {
|
||||
webrtc.CandidateArrays[key] = make(chan string, 30)
|
||||
webrtc.CandidateArrays[key] = make(chan string)
|
||||
}
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
|
||||
|
||||
}
|
||||
@@ -526,15 +587,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 {
|
||||
@@ -582,7 +651,7 @@ func VerifyHub(c *gin.Context) {
|
||||
// @Summary Will verify the persistence.
|
||||
// @Description Will verify the persistence.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func VerifyPersistence(c *gin.Context) {
|
||||
func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
@@ -590,88 +659,88 @@ func VerifyPersistence(c *gin.Context) {
|
||||
|
||||
if config.Cloud == "dropbox" {
|
||||
VerifyDropbox(config, c)
|
||||
} else if config.Cloud == "s3" {
|
||||
} else if config.Cloud == "s3" || config.Cloud == "kerberoshub" {
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
// 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: msg,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Upload of fake recording failed: " + err.Error(),
|
||||
})
|
||||
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
|
||||
@@ -680,10 +749,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)
|
||||
@@ -695,33 +772,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"
|
||||
|
||||
req, err := http.NewRequest("POST", uri+"/storage", body)
|
||||
// Open test-480p.mp4
|
||||
file, err := os.Open(configDirectory + "/test-480p.mp4")
|
||||
if err != nil {
|
||||
msg := "VerifyPersistence: error reading test-480p.mp4: " + err.Error()
|
||||
log.Log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
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)
|
||||
|
||||
@@ -734,41 +822,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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
131
machinery/src/cloud/KerberosHub.go
Normal file
131
machinery/src/cloud/KerberosHub.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/capture"
|
||||
"github.com/kerberos-io/agent/machinery/src/cloud"
|
||||
"github.com/kerberos-io/agent/machinery/src/computervision"
|
||||
configService "github.com/kerberos-io/agent/machinery/src/config"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/agent/machinery/src/onvif"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
func Bootstrap(configuration *models.Configuration, communication *models.Communication) {
|
||||
func Bootstrap(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
log.Log.Debug("Bootstrap: started")
|
||||
|
||||
// We will keep track of the Kerberos Agent up time
|
||||
@@ -72,29 +73,30 @@ 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
|
||||
}
|
||||
|
||||
// We will re open the configuration, might have changed :O!
|
||||
OpenConfig(configuration)
|
||||
|
||||
// We will override the configuration with the environment variables
|
||||
OverrideWithEnvironmentVariables(configuration)
|
||||
if status == "not started" {
|
||||
// We will re open the configuration, might have changed :O!
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
// We will override the configuration with the environment variables
|
||||
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.
|
||||
@@ -106,7 +108,7 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
|
||||
log.Log.Debug("Bootstrap: finished")
|
||||
}
|
||||
|
||||
func RunAgent(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
|
||||
func RunAgent(configDirectory string, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
|
||||
|
||||
log.Log.Debug("RunAgent: bootstrapping agent")
|
||||
config := configuration.Config
|
||||
@@ -133,6 +135,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
width := videoStream.(av.VideoCodecData).Width()
|
||||
height := videoStream.(av.VideoCodecData).Height()
|
||||
|
||||
// Set config values as well
|
||||
configuration.Config.Capture.IPCamera.Width = width
|
||||
configuration.Config.Capture.IPCamera.Height = height
|
||||
|
||||
var queue *pubsub.Queue
|
||||
var subQueue *pubsub.Queue
|
||||
|
||||
@@ -161,10 +167,18 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
time.Sleep(time.Second * 3)
|
||||
return status
|
||||
}
|
||||
|
||||
width := videoStream.(av.VideoCodecData).Width()
|
||||
height := videoStream.(av.VideoCodecData).Height()
|
||||
|
||||
// Set config values as well
|
||||
configuration.Config.Capture.IPCamera.Width = width
|
||||
configuration.Config.Capture.IPCamera.Height = height
|
||||
}
|
||||
|
||||
if cameraSettings.RTSP != rtspUrl || cameraSettings.SubRTSP != subRtspUrl || cameraSettings.Width != width || cameraSettings.Height != height || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
|
||||
if cameraSettings.Initialized {
|
||||
|
||||
if cameraSettings.RTSP != "" && cameraSettings.SubRTSP != "" && cameraSettings.Initialized {
|
||||
decoder.Close()
|
||||
if subStreamEnabled {
|
||||
subDecoder.Close()
|
||||
@@ -188,6 +202,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
cameraSettings.Denum = denum
|
||||
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
|
||||
cameraSettings.Initialized = true
|
||||
|
||||
} else {
|
||||
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
|
||||
}
|
||||
@@ -249,7 +264,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
}
|
||||
|
||||
// Handle livestream HD (high resolution over WEBRTC)
|
||||
communication.HandleLiveHDHandshake = make(chan models.SDPPayload, 1)
|
||||
communication.HandleLiveHDHandshake = make(chan models.RequestHDStreamPayload, 1)
|
||||
if subStreamEnabled {
|
||||
livestreamHDCursor := subQueue.Latest()
|
||||
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, subStreams, subDecoder, &decoderMutex)
|
||||
@@ -259,10 +274,10 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
}
|
||||
|
||||
// Handle recording, will write an mp4 to disk.
|
||||
go capture.HandleRecordStream(queue, configuration, communication, streams)
|
||||
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, streams)
|
||||
|
||||
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
|
||||
go cloud.HandleUpload(configuration, communication)
|
||||
go cloud.HandleUpload(configDirectory, configuration, communication)
|
||||
|
||||
// Handle ONVIF actions
|
||||
go onvif.HandleONVIFActions(configuration, communication)
|
||||
@@ -282,6 +297,12 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
// Cancel the main context, this will stop all the other goroutines.
|
||||
(*communication.CancelContext)()
|
||||
|
||||
// We will re open the configuration, might have changed :O!
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
|
||||
// We will override the configuration with the environment variables
|
||||
configService.OverrideWithEnvironmentVariables(configuration)
|
||||
|
||||
// Here we are cleaning up everything!
|
||||
if configuration.Config.Offline != "true" {
|
||||
communication.HandleUpload <- "stop"
|
||||
@@ -308,9 +329,6 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
close(communication.HandleMotion)
|
||||
communication.HandleMotion = nil
|
||||
|
||||
// Wait a few seconds to stop the decoder.
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
// 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.")
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package components
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -20,14 +20,14 @@ import (
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func GetImageFromFilePath() (image.Image, error) {
|
||||
snapshotDirectory := "./data/snapshots"
|
||||
func GetImageFromFilePath(configDirectory string) (image.Image, error) {
|
||||
snapshotDirectory := configDirectory + "/data/snapshots"
|
||||
files, err := ioutil.ReadDir(snapshotDirectory)
|
||||
if err == nil && len(files) > 1 {
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime().Before(files[j].ModTime())
|
||||
})
|
||||
filePath := "./data/snapshots/" + files[1].Name()
|
||||
filePath := configDirectory + "/data/snapshots/" + files[1].Name()
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -42,11 +42,11 @@ func GetImageFromFilePath() (image.Image, error) {
|
||||
// ReadUserConfig Reads the user configuration of the Kerberos Open Source instance.
|
||||
// This will return a models.User struct including the username, password,
|
||||
// selected language, and if the installation was completed or not.
|
||||
func ReadUserConfig() (userConfig models.User) {
|
||||
func ReadUserConfig(configDirectory string) (userConfig models.User) {
|
||||
for {
|
||||
jsonFile, err := os.Open("./data/config/user.json")
|
||||
jsonFile, err := os.Open(configDirectory + "/data/config/user.json")
|
||||
if err != nil {
|
||||
log.Log.Error("Config file is not found " + "./data/config/user.json, trying again in 5s: " + err.Error())
|
||||
log.Log.Error("Config file is not found " + configDirectory + "/data/config/user.json, trying again in 5s: " + err.Error())
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
log.Log.Info("Successfully Opened user.json")
|
||||
@@ -66,7 +66,7 @@ func ReadUserConfig() (userConfig models.User) {
|
||||
return
|
||||
}
|
||||
|
||||
func OpenConfig(configuration *models.Configuration) {
|
||||
func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
|
||||
// We are checking which deployment this is running, so we can load
|
||||
// into the configuration as expected.
|
||||
@@ -84,23 +84,44 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
collection := db.Collection("configuration")
|
||||
|
||||
var globalConfig models.Config
|
||||
err := collection.FindOne(context.Background(), bson.M{
|
||||
res := collection.FindOne(context.Background(), bson.M{
|
||||
"type": "global",
|
||||
}).Decode(&globalConfig)
|
||||
})
|
||||
|
||||
if res.Err() != nil {
|
||||
log.Log.Error("Could not find global configuration, using default configuration.")
|
||||
panic("Could not find global configuration, using default configuration.")
|
||||
}
|
||||
err := res.Decode(&globalConfig)
|
||||
if err != nil {
|
||||
log.Log.Error("Could not find global configuration, using default configuration.")
|
||||
panic("Could not find global configuration, using default configuration.")
|
||||
}
|
||||
if globalConfig.Type != "global" {
|
||||
log.Log.Error("Could not find global configuration, might missed the mongodb connection.")
|
||||
panic("Could not find global configuration, might missed the mongodb connection.")
|
||||
}
|
||||
|
||||
configuration.GlobalConfig = globalConfig
|
||||
|
||||
var customConfig models.Config
|
||||
deploymentName := os.Getenv("DEPLOYMENT_NAME")
|
||||
err = collection.FindOne(context.Background(), bson.M{
|
||||
res = collection.FindOne(context.Background(), bson.M{
|
||||
"type": "config",
|
||||
"name": deploymentName,
|
||||
}).Decode(&customConfig)
|
||||
})
|
||||
if res.Err() != nil {
|
||||
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
|
||||
}
|
||||
err = res.Decode(&customConfig)
|
||||
if err != nil {
|
||||
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
|
||||
}
|
||||
|
||||
if customConfig.Type != "config" {
|
||||
log.Log.Error("Could not find custom configuration, might missed the mongodb connection.")
|
||||
panic("Could not find custom configuration, might missed the mongodb connection.")
|
||||
}
|
||||
configuration.CustomConfig = customConfig
|
||||
|
||||
// We will merge both configs in a single config file.
|
||||
@@ -120,8 +141,13 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
},
|
||||
)
|
||||
|
||||
// Merge Config toplevel
|
||||
// Reset main configuration Config.
|
||||
configuration.Config = models.Config{}
|
||||
|
||||
// Merge the global settings in the main config
|
||||
conjungo.Merge(&configuration.Config, configuration.GlobalConfig, opts)
|
||||
|
||||
// Now we might override some settings with the custom config
|
||||
conjungo.Merge(&configuration.Config, configuration.CustomConfig, opts)
|
||||
|
||||
// Merge Kerberos Vault settings
|
||||
@@ -136,6 +162,9 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
|
||||
configuration.Config.S3 = &s3
|
||||
|
||||
// Merge timetable manually because it's an array
|
||||
configuration.Config.Timetable = configuration.CustomConfig.Timetable
|
||||
|
||||
// Cleanup
|
||||
opts = nil
|
||||
|
||||
@@ -146,9 +175,9 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
|
||||
// Open device config
|
||||
for {
|
||||
jsonFile, err := os.Open("./data/config/config.json")
|
||||
jsonFile, err := os.Open(configDirectory + "/data/config/config.json")
|
||||
if err != nil {
|
||||
log.Log.Error("Config file is not found " + "./data/config/config.json" + ", trying again in 5s.")
|
||||
log.Log.Error("Config file is not found " + configDirectory + "/data/config/config.json" + ", trying again in 5s.")
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
log.Log.Info("Successfully Opened config.json from " + configuration.Name)
|
||||
@@ -189,7 +218,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
configuration.Config.Key = value
|
||||
break
|
||||
case "AGENT_NAME":
|
||||
configuration.Config.Name = value
|
||||
configuration.Config.FriendlyName = value
|
||||
break
|
||||
case "AGENT_TIMEZONE":
|
||||
configuration.Config.Timezone = value
|
||||
@@ -401,12 +430,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 +461,34 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
case "AGENT_DROPBOX_DIRECTORY":
|
||||
configuration.Config.Dropbox.Directory = value
|
||||
break
|
||||
|
||||
/* When encryption is enabled */
|
||||
case "AGENT_ENCRYPTION":
|
||||
if value == "true" {
|
||||
configuration.Config.Encryption.Enabled = true
|
||||
} else {
|
||||
configuration.Config.Encryption.Enabled = false
|
||||
}
|
||||
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 +509,7 @@ func SaveConfig(config models.Config, configuration *models.Configuration, commu
|
||||
}
|
||||
}
|
||||
|
||||
func StoreConfig(config models.Config) error {
|
||||
func StoreConfig(configDirectory string, config models.Config) error {
|
||||
// Save into database
|
||||
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
|
||||
// Write to mongodb
|
||||
@@ -484,7 +531,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
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
148
machinery/src/encryption/main.go
Normal file
148
machinery/src/encryption/main.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package encryption
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// DecryptWithPrivateKey decrypts data with private key
|
||||
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||
|
||||
// decode our encrypted string into cipher bytes
|
||||
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
|
||||
|
||||
// decrypt the data
|
||||
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// SignWithPrivateKey signs data with private key
|
||||
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||
|
||||
// hash the data with sha256
|
||||
hashed := sha256.Sum256(data)
|
||||
|
||||
// sign the data
|
||||
signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
|
||||
|
||||
return signature, err
|
||||
}
|
||||
|
||||
func AesEncrypt(content string, password string) (string, error) {
|
||||
salt := make([]byte, 8)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
cipherBytes := PKCS5Padding([]byte(content), aes.BlockSize)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
data := make([]byte, 16+len(cipherBytes))
|
||||
copy(data[:8], []byte("Salted__"))
|
||||
copy(data[8:16], salt)
|
||||
copy(data[16:], cipherBytes)
|
||||
|
||||
cipherText := base64.StdEncoding.EncodeToString(data)
|
||||
return cipherText, nil
|
||||
}
|
||||
|
||||
func AesDecrypt(cipherText string, password string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if string(data[:8]) != "Salted__" {
|
||||
return "", errors.New("invalid crypto js aes encryption")
|
||||
}
|
||||
|
||||
salt := data[8:16]
|
||||
cipherBytes := data[16:]
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
result := PKCS5UnPadding(cipherBytes)
|
||||
return string(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...)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Communication struct {
|
||||
HandleHeartBeat chan string
|
||||
HandleLiveSD chan int64
|
||||
HandleLiveHDKeepalive chan string
|
||||
HandleLiveHDHandshake chan SDPPayload
|
||||
HandleLiveHDHandshake chan RequestHDStreamPayload
|
||||
HandleLiveHDPeers chan string
|
||||
HandleONVIF chan OnvifAction
|
||||
IsConfiguring *abool.AtomicBool
|
||||
|
||||
@@ -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" bson:"encryption"`
|
||||
}
|
||||
|
||||
// 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,11 @@ 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 bool `json:"enabled" bson:"enabled"`
|
||||
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
|
||||
PrivateKey string `json:"private_key" bson:"private_key"`
|
||||
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
|
||||
}
|
||||
|
||||
151
machinery/src/models/MQTT.go
Normal file
151
machinery/src/models/MQTT.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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 {
|
||||
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(string(data), k)
|
||||
|
||||
// Sign the encrypted value
|
||||
signature, err := encryption.SignWithPrivateKey([]byte(encryptedValue), rsaKey)
|
||||
base64Signature := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
msg.Payload.EncryptedValue = encryptedValue
|
||||
msg.Payload.Signature = base64Signature
|
||||
msg.Payload.Value = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(msg)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// The message structure which is used to send over
|
||||
// and receive messages from the MQTT broker
|
||||
type Message struct {
|
||||
Mid string `json:"mid"`
|
||||
DeviceId string `json:"device_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Payload Payload `json:"payload"`
|
||||
}
|
||||
|
||||
// The payload structure which is used to send over
|
||||
// and receive messages from the MQTT broker
|
||||
type Payload struct {
|
||||
Action string `json:"action"`
|
||||
DeviceId string `json:"device_id"`
|
||||
Signature string `json:"signature"`
|
||||
EncryptedValue string `json:"encrypted_value"`
|
||||
Value map[string]interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// We received a recording request, we'll send it to the motion handler.
|
||||
type RecordPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
|
||||
}
|
||||
|
||||
// We received a preset position request, we'll request it through onvif and send it back.
|
||||
type PTZPositionPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
}
|
||||
|
||||
// We received a request config request, we'll fetch the current config and send it back.
|
||||
type RequestConfigPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
}
|
||||
|
||||
// We received a update config request, we'll update the current config and send a confirmation back.
|
||||
type UpdateConfigPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
Config Config `json:"config"`
|
||||
}
|
||||
|
||||
// We received a request SD stream request
|
||||
type RequestSDStreamPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
}
|
||||
|
||||
// We received a request HD stream request
|
||||
type RequestHDStreamPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
HubKey string `json:"hub_key"` // hub key
|
||||
SessionID string `json:"session_id"` // session id
|
||||
SessionDescription string `json:"session_description"` // session description
|
||||
}
|
||||
|
||||
// We received a receive HD candidates request
|
||||
type ReceiveHDCandidatesPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
SessionID string `json:"session_id"` // session id
|
||||
Candidate string `json:"candidate"` // candidate
|
||||
}
|
||||
|
||||
type NavigatePTZPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
DeviceId string `json:"device_id"` // device id
|
||||
Action string `json:"action"` // action
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -45,14 +45,74 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
|
||||
if err == nil {
|
||||
|
||||
if onvifAction.Action == "ptz" {
|
||||
if onvifAction.Action == "absolute-move" {
|
||||
|
||||
// We will move the camera to zero position.
|
||||
x := ptzAction.X
|
||||
y := ptzAction.Y
|
||||
z := ptzAction.Z
|
||||
|
||||
// Check which PTZ Space we need to use
|
||||
functions, _, _ := GetPTZFunctionsFromDevice(configurations)
|
||||
|
||||
// Log functions
|
||||
log.Log.Info("HandleONVIFActions: functions: " + strings.Join(functions, ", "))
|
||||
|
||||
// Check if we need to use absolute or continuous move
|
||||
/*canAbsoluteMove := false
|
||||
canContinuousMove := false
|
||||
|
||||
if len(functions) > 0 {
|
||||
for _, function := range functions {
|
||||
if function == "AbsolutePanTiltMove" || function == "AbsoluteZoomMove" {
|
||||
canAbsoluteMove = true
|
||||
} else if function == "ContinuousPanTiltMove" || function == "ContinuousZoomMove" {
|
||||
canContinuousMove = true
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Ideally we should be able to use the AbsolutePanTiltMove function, but it looks like
|
||||
// the current detection through GetPTZFuntionsFromDevice is not working properly. Therefore we will fallback
|
||||
// on the ContinuousPanTiltMove function which is more compatible with more cameras.
|
||||
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
|
||||
} else {
|
||||
log.Log.Info("HandleONVIFActions (AbsolutePanTitleMoveFake): successfully moved camera")
|
||||
}
|
||||
|
||||
/*if canAbsoluteMove {
|
||||
err = AbsolutePanTiltMove(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
|
||||
}
|
||||
} else if canContinuousMove {
|
||||
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
|
||||
}
|
||||
}*/
|
||||
|
||||
} else if onvifAction.Action == "preset" {
|
||||
|
||||
// Execute the preset
|
||||
preset := ptzAction.Preset
|
||||
err := GoToPresetFromDevice(device, preset)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (GotoPreset): " + err.Error())
|
||||
} else {
|
||||
log.Log.Info("HandleONVIFActions (GotoPreset): successfully moved camera")
|
||||
}
|
||||
|
||||
} else if onvifAction.Action == "ptz" {
|
||||
|
||||
if err == nil {
|
||||
|
||||
if ptzAction.Center == 1 {
|
||||
|
||||
// We will move the camera to zero position.
|
||||
err := AbsolutePanTiltMove(device, configurations, token, 0, 0)
|
||||
err := AbsolutePanTiltMove(device, configurations, token, 0, 0, 0)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
|
||||
}
|
||||
@@ -179,18 +239,83 @@ func GetPTZConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurations
|
||||
return configurations, err
|
||||
}
|
||||
|
||||
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float32, tilt float32) error {
|
||||
func GetPositionFromDevice(configuration models.Configuration) (xsd.PTZVector, error) {
|
||||
var position xsd.PTZVector
|
||||
// Connect to Onvif device
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
|
||||
absoluteVector := xsd.Vector2D{
|
||||
X: float64(pan),
|
||||
Y: float64(tilt),
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
// Get the PTZ configurations from the device
|
||||
position, err := GetPosition(device, token)
|
||||
if err == nil {
|
||||
return position, err
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
}
|
||||
|
||||
func GetPosition(device *onvif.Device, token xsd.ReferenceToken) (xsd.PTZVector, error) {
|
||||
// We'll try to receive the PTZ configurations from the server
|
||||
var status ptz.GetStatusResponse
|
||||
var position xsd.PTZVector
|
||||
|
||||
// Get the PTZ configurations from the device
|
||||
resp, err := device.CallMethod(ptz.GetStatus{
|
||||
ProfileToken: token,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetStatusResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&status, et); err != nil {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
position = status.PTZStatus.Position
|
||||
return position, err
|
||||
}
|
||||
|
||||
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
|
||||
|
||||
absolutePantiltVector := xsd.Vector2D{
|
||||
X: pan,
|
||||
Y: tilt,
|
||||
Space: configuration.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace,
|
||||
}
|
||||
|
||||
absoluteZoomVector := xsd.Vector1D{
|
||||
X: zoom,
|
||||
Space: configuration.PTZConfiguration.DefaultAbsoluteZoomPositionSpace,
|
||||
}
|
||||
|
||||
res, err := device.CallMethod(ptz.AbsoluteMove{
|
||||
ProfileToken: token,
|
||||
Position: xsd.PTZVector{
|
||||
PanTilt: absoluteVector,
|
||||
PanTilt: absolutePantiltVector,
|
||||
Zoom: absoluteZoomVector,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -199,11 +324,255 @@ func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfiguratio
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("AbsoluteMove: " + string(bs))
|
||||
log.Log.Info("AbsoluteMove: " + string(bs))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// This function will simulate the AbsolutePanTiltMove function.
|
||||
// However the AboslutePanTiltMove function is not working on all cameras.
|
||||
// So we'll use the ContinuousMove function to simulate the AbsolutePanTiltMove function using the position polling.
|
||||
func AbsolutePanTiltMoveFake(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
|
||||
position, err := GetPosition(device, token)
|
||||
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 && position.PanTilt.Y >= tilt-0.01 && position.PanTilt.Y <= tilt+0.01 && position.Zoom.X >= zoom-0.01 && position.Zoom.X <= zoom+0.01 {
|
||||
log.Log.Debug("AbsolutePanTiltMoveFake: already at position")
|
||||
} else {
|
||||
|
||||
// The speed of panning, the higher the faster we'll pan the camera
|
||||
// value is a range between 0 and 1.
|
||||
speed := 0.6
|
||||
wait := 100 * time.Millisecond
|
||||
|
||||
// We'll move quickly to the position (might be inaccurate)
|
||||
err = ZoomOutCompletely(device, configuration, token)
|
||||
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
|
||||
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
|
||||
|
||||
// Now we'll move a bit slower to make sure we are ok (will be more accurate)
|
||||
speed = 0.1
|
||||
wait = 200 * time.Millisecond
|
||||
|
||||
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
|
||||
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
|
||||
err = ZoomUntilPosition(device, configuration, token, zoom, speed, wait)
|
||||
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ZoomOutCompletely(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken) error {
|
||||
// Zoom out completely!!!
|
||||
zoomOut := xsd.Vector1D{
|
||||
X: -1,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
|
||||
}
|
||||
_, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedZoom{
|
||||
Zoom: zoomOut,
|
||||
},
|
||||
})
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.Zoom.X == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
Zoom: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionX := speed
|
||||
if position.PanTilt.X > pan {
|
||||
directionX = speed * -1
|
||||
}
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
X: directionX,
|
||||
Y: 0,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedPanTilt{
|
||||
PanTilt: panTiltVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Pan): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Pan): " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.PanTilt.X == -1 || position.PanTilt.X == 1 || (directionX > 0 && position.PanTilt.X >= pan) || (directionX < 0 && position.PanTilt.X <= pan) || (position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Pan): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, tilt float64, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionY := speed
|
||||
if position.PanTilt.Y > tilt {
|
||||
directionY = speed * -1
|
||||
}
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
X: 0,
|
||||
Y: directionY,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedPanTilt{
|
||||
PanTilt: panTiltVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Tilt): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Tilt) " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.PanTilt.Y == -1 || position.PanTilt.Y == 1 || (directionY > 0 && position.PanTilt.Y >= tilt) || (directionY < 0 && position.PanTilt.Y <= tilt) || (position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Tilt): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ZoomUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionZ := speed
|
||||
if position.Zoom.X > zoom {
|
||||
directionZ = speed * -1
|
||||
}
|
||||
|
||||
zoomVector := xsd.Vector1D{
|
||||
X: directionZ,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedZoom{
|
||||
Zoom: zoomVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Zoom): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Zoom) " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.Zoom.X == -1 || position.Zoom.X == 1 || (directionZ > 0 && position.Zoom.X >= zoom) || (directionZ < 0 && position.Zoom.X <= zoom) || (position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Zoom): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64) error {
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
@@ -226,7 +595,7 @@ func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurations
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove: " + string(bs))
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
res, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
@@ -299,6 +668,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
var presets []models.OnvifActionPreset
|
||||
var presetsResponse ptz.GetPresetsResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
resp, err := device.CallMethod(ptz.GetPresets{
|
||||
ProfileToken: token,
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
}
|
||||
|
||||
for _, preset := range presetsResponse.Preset {
|
||||
p := models.OnvifActionPreset{
|
||||
Name: string(preset.Name),
|
||||
Token: string(preset.Token),
|
||||
}
|
||||
|
||||
presets = append(presets, p)
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
|
||||
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
|
||||
var goToPresetResponse ptz.GotoPresetResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
|
||||
resp, err := device.CallMethod(ptz.GotoPreset{
|
||||
ProfileToken: token,
|
||||
PresetToken: xsd.ReferenceToken(presetName),
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses")
|
||||
if err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
|
||||
xmlBytes := bytes.NewBufferString(xmlBody)
|
||||
decodedXML := xml.NewDecoder(xmlBytes)
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -63,23 +64,22 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
|
||||
api.GET("/dashboard", func(c *gin.Context) {
|
||||
|
||||
// This will return the timestamp when the last packet was correctyl received
|
||||
// this is to calculate if the camera connection is still working.
|
||||
lastPacketReceived := int64(0)
|
||||
if communication.LastPacketTimer != nil && communication.LastPacketTimer.Load() != nil {
|
||||
lastPacketReceived = communication.LastPacketTimer.Load().(int64)
|
||||
}
|
||||
// Check if camera is online.
|
||||
cameraIsOnline := communication.CameraConnected
|
||||
|
||||
// If an agent is properly setup with Kerberos Hub, we will send
|
||||
// a ping to Kerberos Hub every 15seconds. On receiving a positive response
|
||||
// it will update the CloudTimestamp value.
|
||||
cloudTimestamp := int64(0)
|
||||
cloudIsOnline := false
|
||||
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
|
||||
cloudTimestamp = communication.CloudTimestamp.Load().(int64)
|
||||
timestamp := communication.CloudTimestamp.Load().(int64)
|
||||
if timestamp > 0 {
|
||||
cloudIsOnline = true
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -100,8 +100,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"offlineMode": configuration.Config.Offline,
|
||||
"cameraOnline": lastPacketReceived,
|
||||
"cloudOnline": cloudTimestamp,
|
||||
"cameraOnline": cameraIsOnline,
|
||||
"cloudOnline": cloudIsOnline,
|
||||
"numberOfRecordings": numberOfRecordings,
|
||||
"days": days,
|
||||
"latestEvents": latestEvents,
|
||||
@@ -116,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)
|
||||
@@ -138,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)
|
||||
@@ -166,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",
|
||||
@@ -206,7 +206,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
})
|
||||
|
||||
api.POST("/persistence/verify", func(c *gin.Context) {
|
||||
cloud.VerifyPersistence(c)
|
||||
cloud.VerifyPersistence(c, configDirectory)
|
||||
})
|
||||
|
||||
// Streaming handler
|
||||
@@ -216,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
// We will only send an image once per second.
|
||||
time.Sleep(time.Second * 1)
|
||||
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
|
||||
img, err := components.GetImageFromFilePath()
|
||||
img, err := configService.GetImageFromFilePath(configDirectory)
|
||||
return img, err
|
||||
}
|
||||
h := components.StartMotionJPEG(imageFunction, 80)
|
||||
@@ -228,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
// the camera.
|
||||
api.POST("/camera/onvif/login", LoginToOnvif)
|
||||
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
|
||||
api.POST("/camera/onvif/presets", GetOnvifPresets)
|
||||
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
|
||||
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
|
||||
api.POST("/camera/onvif/zoom", DoOnvifZoom)
|
||||
api.POST("/camera/verify/:streamType", capture.VerifyCamera)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
@@ -33,7 +35,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()
|
||||
@@ -55,15 +57,28 @@ 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 := configDirectory + "/www/env.js"
|
||||
if os.Getenv("AGENT_MODE") == "demo" {
|
||||
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
|
||||
// Move demo environment variables to environment variables
|
||||
err := os.Rename(demoEnvironmentVariables, environmentVariables)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
// Run the api on port
|
||||
err = r.Run(":" + configuration.Port)
|
||||
@@ -72,8 +87,8 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
|
||||
}
|
||||
}
|
||||
|
||||
func Files(c *gin.Context) {
|
||||
func Files(c *gin.Context, configDirectory string) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Content-Type", "video/mp4")
|
||||
c.File("./data/recordings" + c.Param("filepath"))
|
||||
c.File(configDirectory + "/data/recordings" + c.Param("filepath"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"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 +42,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 +128,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 +144,338 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
|
||||
return nil
|
||||
}
|
||||
|
||||
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
|
||||
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
select {
|
||||
case communication.HandleLiveSD <- time.Now().Unix():
|
||||
default:
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.")
|
||||
}
|
||||
msg.Ack()
|
||||
})
|
||||
}
|
||||
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
if hubKey == "" {
|
||||
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
|
||||
} else {
|
||||
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
|
||||
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
|
||||
|
||||
func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicRequestWebRtc := config.Key + "/register"
|
||||
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
var sdp models.SDPPayload
|
||||
json.Unmarshal(msg.Payload(), &sdp)
|
||||
select {
|
||||
case communication.HandleLiveHDHandshake <- sdp:
|
||||
default:
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.")
|
||||
}
|
||||
msg.Ack()
|
||||
})
|
||||
}
|
||||
// Decode the message, we are expecting following format.
|
||||
// {
|
||||
// mid: string, "unique id for the message"
|
||||
// timestamp: int64, "unix timestamp when the message was generated"
|
||||
// encrypted: boolean,
|
||||
// fingerprint: string, "fingerprint of the message to validate authenticity"
|
||||
// payload: Payload, "a json object which might be encrypted"
|
||||
// }
|
||||
|
||||
func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
|
||||
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
alive := string(msg.Payload())
|
||||
communication.HandleLiveHDKeepalive <- alive
|
||||
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
var message models.Message
|
||||
json.Unmarshal(msg.Payload(), &message)
|
||||
|
||||
func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
|
||||
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
peerCount := string(msg.Payload())
|
||||
communication.HandleLiveHDPeers <- peerCount
|
||||
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
|
||||
if message.Mid != "" && message.Timestamp != 0 && message.DeviceId == configuration.Config.Key {
|
||||
// Messages might be encrypted, if so we'll
|
||||
// need to decrypt them.
|
||||
var payload models.Payload
|
||||
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled {
|
||||
encryptedValue := message.Payload.EncryptedValue
|
||||
if len(encryptedValue) > 0 {
|
||||
symmetricKey := configuration.Config.Encryption.SymmetricKey
|
||||
privateKey := configuration.Config.Encryption.PrivateKey
|
||||
r := strings.NewReader(privateKey)
|
||||
pemBytes, _ := ioutil.ReadAll(r)
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
|
||||
return
|
||||
} else {
|
||||
// Parse private key
|
||||
b := block.Bytes
|
||||
key, err := x509.ParsePKCS8PrivateKey(b)
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error parsing private key: " + err.Error())
|
||||
return
|
||||
} else {
|
||||
// Conver key to *rsa.PrivateKey
|
||||
rsaKey, _ := key.(*rsa.PrivateKey)
|
||||
|
||||
func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicCandidates := "candidate/cloud"
|
||||
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
var candidate models.Candidate
|
||||
json.Unmarshal(msg.Payload(), &candidate)
|
||||
if candidate.CloudKey == config.Key {
|
||||
key := candidate.CloudKey + "/" + candidate.Cuuid
|
||||
candidatesExists := false
|
||||
var channel chan string
|
||||
for !candidatesExists {
|
||||
webrtc.CandidatesMutex.Lock()
|
||||
channel, candidatesExists = webrtc.CandidateArrays[key]
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
// Get encrypted key from message, delimited by :::
|
||||
encryptedKey := strings.Split(encryptedValue, ":::")[0] // encrypted with RSA
|
||||
encryptedValue := strings.Split(encryptedValue, ":::")[1] // encrypted with AES
|
||||
// Convert encrypted value to []byte
|
||||
decryptedKey, err := encryption.DecryptWithPrivateKey(encryptedKey, rsaKey)
|
||||
if decryptedKey != nil {
|
||||
if string(decryptedKey) == symmetricKey {
|
||||
// Decrypt value with decryptedKey
|
||||
decryptedValue, err := encryption.AesDecrypt(encryptedValue, string(decryptedKey))
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
|
||||
return
|
||||
}
|
||||
json.Unmarshal([]byte(decryptedValue), &payload)
|
||||
} else {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message, assymetric keys do not match.")
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload = message.Payload
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
|
||||
channel <- string(msg.Payload())
|
||||
|
||||
// We'll find out which message we received, and act accordingly.
|
||||
log.Log.Info("MQTTListenerHandler: received message with action: " + payload.Action)
|
||||
switch payload.Action {
|
||||
case "record":
|
||||
go HandleRecording(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "get-ptz-position":
|
||||
go HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "update-ptz-position":
|
||||
go HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "navigate-ptz":
|
||||
go HandleNavigatePTZ(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "request-config":
|
||||
go HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "update-config":
|
||||
go HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
|
||||
case "request-sd-stream":
|
||||
go HandleRequestSDStream(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "request-hd-stream":
|
||||
go HandleRequestHDStream(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "receive-hd-candidates":
|
||||
go HandleReceiveHDCandidates(mqttClient, hubKey, payload, configuration, communication)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
|
||||
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to RecordPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var recordPayload models.RecordPayload
|
||||
json.Unmarshal(jsonData, &recordPayload)
|
||||
|
||||
if recordPayload.Timestamp != 0 {
|
||||
motionDataPartial := models.MotionDataPartial{
|
||||
Timestamp: recordPayload.Timestamp,
|
||||
}
|
||||
communication.HandleMotion <- motionDataPartial
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to PTZPositionPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var positionPayload models.PTZPositionPayload
|
||||
json.Unmarshal(jsonData, &positionPayload)
|
||||
|
||||
if positionPayload.Timestamp != 0 {
|
||||
// Get Position from device
|
||||
pos, err := onvif.GetPositionFromDevice(*configuration)
|
||||
if err != nil {
|
||||
log.Log.Error("HandlePTZPosition: error getting position from device: " + err.Error())
|
||||
} else {
|
||||
// Needs to wrapped!
|
||||
posString := fmt.Sprintf("%f,%f,%f", pos.PanTilt.X, pos.PanTilt.Y, pos.Zoom.X)
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "ptz-position",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: map[string]interface{}{
|
||||
"timestamp": positionPayload.Timestamp,
|
||||
"position": posString,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandlePTZPosition: something went wrong while sending position to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to PTZPositionPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal(jsonData, &onvifAction)
|
||||
|
||||
if onvifAction.Action != "" {
|
||||
if communication.CameraConnected {
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal(msg.Payload(), &onvifAction)
|
||||
communication.HandleONVIF <- onvifAction
|
||||
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to RequestConfigPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var configPayload models.RequestConfigPayload
|
||||
json.Unmarshal(jsonData, &configPayload)
|
||||
|
||||
if configPayload.Timestamp != 0 {
|
||||
// Get Config from the device
|
||||
|
||||
key := configuration.Config.Key
|
||||
name := configuration.Config.Name
|
||||
|
||||
if key != "" && name != "" {
|
||||
|
||||
var configMap map[string]interface{}
|
||||
inrec, _ := json.Marshal(configuration.Config)
|
||||
json.Unmarshal(inrec, &configMap)
|
||||
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-config",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: configMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: no config available")
|
||||
}
|
||||
|
||||
log.Log.Info("HandleRequestConfig: Received a request for the config")
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to UpdateConfigPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var configPayload models.UpdateConfigPayload
|
||||
json.Unmarshal(jsonData, &configPayload)
|
||||
|
||||
if configPayload.Timestamp != 0 {
|
||||
|
||||
config := configPayload.Config
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
log.Log.Info("HandleUpdateConfig: Config updated")
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "acknowledge-update-config",
|
||||
DeviceId: configuration.Config.Key,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("HandleUpdateConfig: Config update failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestSDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to RequestSDStreamPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var requestSDStreamPayload models.RequestSDStreamPayload
|
||||
json.Unmarshal(jsonData, &requestSDStreamPayload)
|
||||
|
||||
if requestSDStreamPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
select {
|
||||
case communication.HandleLiveSD <- time.Now().Unix():
|
||||
default:
|
||||
}
|
||||
log.Log.Info("HandleRequestSDStream: received request to livestream.")
|
||||
} else {
|
||||
log.Log.Info("HandleRequestSDStream: received request to livestream, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestHDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to RequestHDStreamPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var requestHDStreamPayload models.RequestHDStreamPayload
|
||||
json.Unmarshal(jsonData, &requestHDStreamPayload)
|
||||
|
||||
if requestHDStreamPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
// Set the Hub key, so we can send back the answer.
|
||||
requestHDStreamPayload.HubKey = hubKey
|
||||
select {
|
||||
case communication.HandleLiveHDHandshake <- requestHDStreamPayload:
|
||||
default:
|
||||
}
|
||||
log.Log.Info("HandleRequestHDStream: received request to setup webrtc.")
|
||||
} else {
|
||||
log.Log.Info("HandleRequestHDStream: received request to setup webrtc, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to ReceiveHDCandidatesPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var receiveHDCandidatesPayload models.ReceiveHDCandidatesPayload
|
||||
json.Unmarshal(jsonData, &receiveHDCandidatesPayload)
|
||||
|
||||
if receiveHDCandidatesPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
channel := webrtc.CandidateArrays[receiveHDCandidatesPayload.SessionID]
|
||||
log.Log.Info("HandleReceiveHDCandidates: " + receiveHDCandidatesPayload.Candidate)
|
||||
channel <- receiveHDCandidatesPayload.Candidate
|
||||
} else {
|
||||
log.Log.Info("HandleReceiveHDCandidates: received candidate, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleNavigatePTZ(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var navigatePTZPayload models.NavigatePTZPayload
|
||||
json.Unmarshal(jsonData, &navigatePTZPayload)
|
||||
|
||||
if navigatePTZPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
action := navigatePTZPayload.Action
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal([]byte(action), &onvifAction)
|
||||
communication.HandleONVIF <- onvifAction
|
||||
log.Log.Info("HandleNavigatePTZ: Received an action - " + onvifAction.Action)
|
||||
|
||||
} else {
|
||||
log.Log.Info("HandleNavigatePTZ: received action, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
|
||||
if mqttClient != nil {
|
||||
// Cleanup all subscriptions
|
||||
mqttClient.Unsubscribe("kerberos/" + PREV_HubKey + "/device/" + PREV_AgentKey + "/request-live")
|
||||
mqttClient.Unsubscribe(PREV_AgentKey + "/register")
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + PREV_AgentKey)
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + PREV_AgentKey)
|
||||
mqttClient.Unsubscribe("candidate/cloud")
|
||||
mqttClient.Unsubscribe("kerberos/onvif/" + PREV_AgentKey)
|
||||
// New methods
|
||||
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
|
||||
mqttClient.Disconnect(1000)
|
||||
mqttClient = nil
|
||||
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
|
||||
|
||||
@@ -110,15 +110,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 {
|
||||
|
||||
@@ -87,19 +87,22 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
|
||||
return offer
|
||||
}
|
||||
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload, candidates chan string) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
name := config.Key
|
||||
deviceKey := config.Key
|
||||
stunServers := []string{config.STUNURI}
|
||||
turnServers := []string{config.TURNURI}
|
||||
turnServersUsername := config.TURNUsername
|
||||
turnServersCredential := config.TURNPassword
|
||||
|
||||
// Set variables
|
||||
hubKey := handshake.HubKey
|
||||
sessionDescription := handshake.SessionDescription
|
||||
|
||||
// Create WebRTC object
|
||||
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(handshake.Sdp)
|
||||
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(sessionDescription)
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -122,7 +125,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
Credential: w.TurnServersCredential,
|
||||
},
|
||||
},
|
||||
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -143,7 +145,7 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
|
||||
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
|
||||
atomic.AddInt64(&peerConnectionCount, -1)
|
||||
peerConnections[handshake.Cuuid] = nil
|
||||
peerConnections[handshake.SessionID] = nil
|
||||
close(candidates)
|
||||
close(w.PacketsCount)
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
@@ -152,9 +154,12 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
|
||||
atomic.AddInt64(&peerConnectionCount, 1)
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
|
||||
// Iterate over the candidates and send them to the remote client
|
||||
// Non blocking channel
|
||||
for candidate := range candidates {
|
||||
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
|
||||
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
|
||||
log.Log.Error("InitializeWebRTCConnection: something went wrong while adding candidate: " + candidateErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +172,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -175,37 +179,64 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// When an ICE candidate is available send to the other Pion instance
|
||||
// the other Pion instance will add this candidate by calling AddICECandidate
|
||||
var candidatesMux sync.Mutex
|
||||
// When an ICE candidate is available send to the other peer using the signaling server (MQTT).
|
||||
// The other peer will add this candidate by calling AddICECandidate
|
||||
peerConnection.OnICECandidate(func(candidate *pionWebRTC.ICECandidate) {
|
||||
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidatesMux.Lock()
|
||||
defer candidatesMux.Unlock()
|
||||
|
||||
topic := fmt.Sprintf("%s/%s/candidate/edge", name, handshake.Cuuid)
|
||||
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
|
||||
candiInit := candidate.ToJSON()
|
||||
// Create a config map
|
||||
valueMap := make(map[string]interface{})
|
||||
candateJSON := candidate.ToJSON()
|
||||
sdpmid := "0"
|
||||
candiInit.SDPMid = &sdpmid
|
||||
candi, err := json.Marshal(candiInit)
|
||||
candateJSON.SDPMid = &sdpmid
|
||||
candateBinary, err := json.Marshal(candateJSON)
|
||||
if err == nil {
|
||||
log.Log.Info("InitializeWebRTCConnection:" + string(candi))
|
||||
token := mqttClient.Publish(topic, 2, false, candi)
|
||||
token.Wait()
|
||||
valueMap["candidate"] = string(candateBinary)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while marshalling candidate: " + err.Error())
|
||||
}
|
||||
|
||||
// We'll send the candidate to the hub
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-hd-candidates",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
})
|
||||
|
||||
peerConnections[handshake.Cuuid] = peerConnection
|
||||
// Create a channel which will be used to send candidates to the other peer
|
||||
peerConnections[handshake.SessionID] = peerConnection
|
||||
|
||||
if err == nil {
|
||||
topic := fmt.Sprintf("%s/%s/answer", name, handshake.Cuuid)
|
||||
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
|
||||
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
|
||||
// Create a config map
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
|
||||
log.Log.Info("InitializeWebRTCConnection: Send SDP answer")
|
||||
|
||||
// We'll send the candidate to the hub
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-hd-answer",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -358,16 +389,9 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
|
||||
pkt.Data = append(codecData.(h264parser.CodecData).SPS(), pkt.Data...)
|
||||
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
|
||||
log.Log.Info("WriteToTrack: Sending keyframe")
|
||||
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
log.Log.Info("WriteToTrack: Sending keep a live to remote broker.")
|
||||
topic := fmt.Sprintf("kerberos/webrtc/keepalive/%s", config.Key)
|
||||
mqttClient.Publish(topic, 2, false, "1")
|
||||
}
|
||||
}
|
||||
|
||||
if start {
|
||||
|
||||
sample := pionMedia.Sample{Data: pkt.Data, Duration: bufferDuration}
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
samplePacket, err := json.Marshal(sample)
|
||||
|
||||
6
snap/hooks/configure
vendored
Normal file
6
snap/hooks/configure
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
cp -R $SNAP/data $SNAP_COMMON/
|
||||
cp -R $SNAP/www $SNAP_COMMON/
|
||||
cp -R $SNAP/version $SNAP_COMMON/
|
||||
cp -R $SNAP/mp4fragment $SNAP_COMMON/
|
||||
23
snap/snapcraft.yaml
Normal file
23
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: kerberosio # you probably want to 'snapcraft register <name>'
|
||||
base: core22 # the base snap is the execution environment for this snap
|
||||
version: '3.0.0' # just for humans, typically '1.2+git' or '1.3.2'
|
||||
summary: A stand-alone open source video surveillance system # 79 char long summary
|
||||
description: |
|
||||
Kerberos Agent is an isolated and scalable video (surveillance) management
|
||||
agent made available as Open Source under the MIT License. This means that
|
||||
all the source code is available for you or your company, and you can use,
|
||||
transform and distribute the source code; as long you keep a reference of
|
||||
the original license. Kerberos Agent can be used for commercial usage.
|
||||
|
||||
grade: stable # stable # must be 'stable' to release into candidate/stable channels
|
||||
confinement: strict # use 'strict' once you have the right plugs and slots
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
apps:
|
||||
agent:
|
||||
command: main -config /var/snap/kerberosio/common
|
||||
plugs: [ network, network-bind ]
|
||||
parts:
|
||||
agent:
|
||||
source: . #https://github.com/kerberos-io/agent/releases/download/21c0e01/agent-amd64.tar
|
||||
plugin: dump
|
||||
@@ -21,6 +21,7 @@
|
||||
"react/forbid-prop-types": "off",
|
||||
"no-case-declarations": "off",
|
||||
"camelcase": "off",
|
||||
"dot-notation": "off",
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
|
||||
5
ui/public/env.demo.js
Normal file
5
ui/public/env.demo.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(function (window) {
|
||||
window['env'] = window['env'] || {};
|
||||
// Environment variables
|
||||
window['env']['mode'] = 'demo';
|
||||
})(this);
|
||||
5
ui/public/env.js
Normal file
5
ui/public/env.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(function (window) {
|
||||
window['env'] = window['env'] || {};
|
||||
// Environment variables
|
||||
window['env']['mode'] = 'release';
|
||||
})(this);
|
||||
@@ -19,7 +19,7 @@
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<script src="assets/env.js"></script>
|
||||
<script src="%PUBLIC_URL%/env.js"></script>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
||||
215
ui/public/locales/it/translation.json
Normal file
215
ui/public/locales/it/translation.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
209
ui/src/App.jsx
209
ui/src/App.jsx
@@ -93,111 +93,122 @@ class App extends React.Component {
|
||||
const { children, username, dashboard, dispatchLogout } = this.props;
|
||||
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30;
|
||||
return (
|
||||
<div id="page-root">
|
||||
<Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile>
|
||||
<Profilebar
|
||||
username={username}
|
||||
email="support@kerberos.io"
|
||||
userrole={t('navigation.admin')}
|
||||
logout={dispatchLogout}
|
||||
/>
|
||||
<Navigation>
|
||||
<NavigationSection title={t('navigation.management')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.dashboard')}
|
||||
icon="dashboard"
|
||||
link="dashboard"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.recordings')}
|
||||
icon="media"
|
||||
link="media"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.settings')}
|
||||
icon="preferences"
|
||||
link="settings"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.help_support')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.swagger')}
|
||||
icon="api"
|
||||
external
|
||||
link={`${config.URL}/swagger/index.html`}
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.documentation')}
|
||||
icon="book"
|
||||
external
|
||||
link="https://doc.kerberos.io/agent/announcement"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Kerberos Hub"
|
||||
icon="cloud"
|
||||
external
|
||||
link="https://app.kerberos.io"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.ui_library')}
|
||||
icon="paint"
|
||||
external
|
||||
link="https://ui.kerberos.io/"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Github"
|
||||
icon="github-nav"
|
||||
external
|
||||
link="https://github.com/kerberos-io/agent"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.layout')} />
|
||||
<NavigationGroup>
|
||||
<LanguageSelect />
|
||||
</NavigationGroup>
|
||||
|
||||
<NavigationSection title="Websocket" />
|
||||
<NavigationGroup>
|
||||
<div className="websocket-badge">
|
||||
<Badge
|
||||
title={connected ? 'connected' : 'disconnected'}
|
||||
status={connected ? 'success' : 'warning'}
|
||||
<>
|
||||
{config.MODE !== 'release' && (
|
||||
<div className={`environment ${config.MODE}`}>
|
||||
Environment: {config.MODE}
|
||||
</div>
|
||||
)}
|
||||
<div id="page-root">
|
||||
<Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile>
|
||||
<Profilebar
|
||||
username={username}
|
||||
email="support@kerberos.io"
|
||||
userrole={t('navigation.admin')}
|
||||
logout={dispatchLogout}
|
||||
/>
|
||||
<Navigation>
|
||||
<NavigationSection title={t('navigation.management')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.dashboard')}
|
||||
icon="dashboard"
|
||||
link="dashboard"
|
||||
/>
|
||||
</div>
|
||||
</NavigationGroup>
|
||||
</Navigation>
|
||||
</Sidebar>
|
||||
<Main>
|
||||
<Gradient />
|
||||
<NavigationItem
|
||||
title={t('navigation.recordings')}
|
||||
icon="media"
|
||||
link="media"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.settings')}
|
||||
icon="preferences"
|
||||
link="settings"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.help_support')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.swagger')}
|
||||
icon="api"
|
||||
external
|
||||
link={`${config.URL}/swagger/index.html`}
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.documentation')}
|
||||
icon="book"
|
||||
external
|
||||
link="https://doc.kerberos.io/agent/announcement"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Kerberos Hub"
|
||||
icon="cloud"
|
||||
external
|
||||
link="https://app.kerberos.io"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.ui_library')}
|
||||
icon="paint"
|
||||
external
|
||||
link="https://ui.kerberos.io/"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Github"
|
||||
icon="github-nav"
|
||||
external
|
||||
link="https://github.com/kerberos-io/agent"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.layout')} />
|
||||
<NavigationGroup>
|
||||
<LanguageSelect />
|
||||
</NavigationGroup>
|
||||
|
||||
{!cloudOnline && (
|
||||
<a href="https://app.kerberos.io" target="_blank" rel="noreferrer">
|
||||
<div className="cloud-not-installed">
|
||||
<div>
|
||||
<Icon label="cloud" />
|
||||
Activate Kerberos Hub, and make your cameras and recordings
|
||||
available through a secured cloud!
|
||||
<NavigationSection title="Websocket" />
|
||||
<NavigationGroup>
|
||||
<div className="websocket-badge">
|
||||
<Badge
|
||||
title={connected ? 'connected' : 'disconnected'}
|
||||
status={connected ? 'success' : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</NavigationGroup>
|
||||
</Navigation>
|
||||
</Sidebar>
|
||||
<Main>
|
||||
<Gradient />
|
||||
|
||||
{dashboard.offlineMode === 'true' && (
|
||||
<Link to="/settings">
|
||||
<div className="offline-mode">
|
||||
<div>
|
||||
<Icon label="info" />
|
||||
Attention! Kerberos is currently running in Offline mode.
|
||||
{!cloudOnline && (
|
||||
<a
|
||||
href="https://app.kerberos.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="cloud-not-installed">
|
||||
<div>
|
||||
<Icon label="cloud" />
|
||||
Activate Kerberos Hub, and make your cameras and recordings
|
||||
available through a secured cloud!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<MainBody>{children}</MainBody>
|
||||
</Main>
|
||||
</div>
|
||||
{dashboard.offlineMode === 'true' && (
|
||||
<Link to="/settings">
|
||||
<div className="offline-mode">
|
||||
<div>
|
||||
<Icon label="info" />
|
||||
Attention! Kerberos is currently running in Offline mode.
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<MainBody>{children}</MainBody>
|
||||
</Main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 },
|
||||
|
||||
@@ -9,9 +9,10 @@ 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,
|
||||
// API_URL: `${protocol}//${externalHost}/api`,
|
||||
@@ -25,6 +26,7 @@ const prod = {
|
||||
API_URL: `${protocol}//${host}/api`,
|
||||
URL: `${protocol}//${host}`,
|
||||
WS_URL: `${websocketprotocol}//${host}/ws`,
|
||||
MODE: window['env']['mode'],
|
||||
};
|
||||
|
||||
const config = process.env.REACT_APP_ENVIRONMENT === 'production' ? prod : dev;
|
||||
|
||||
@@ -144,3 +144,55 @@ body {
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif; // use regular Inter font by default..
|
||||
}
|
||||
|
||||
.environment.develop {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#035e1f 10px,
|
||||
#035e1f 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: size(1) 0;
|
||||
}
|
||||
|
||||
.environment.demo {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#035e1f 10px,
|
||||
#035e1f 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.environment.staging {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#6d0e0e 10px,
|
||||
#6d0e0e 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.environment.acceptance {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#8b8203 10px,
|
||||
#8b8203 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 12px 0;
|
||||
}
|
||||
@@ -35,7 +35,9 @@ class Dashboard extends React.Component {
|
||||
liveviewLoaded: false,
|
||||
open: false,
|
||||
currentRecording: '',
|
||||
initialised: false,
|
||||
};
|
||||
this.initialiseLiveview = this.initialiseLiveview.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -47,32 +49,11 @@ class Dashboard extends React.Component {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { connected } = this.props;
|
||||
if (connected === true) {
|
||||
const { dispatchSend } = this.props;
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
dispatchSend(message);
|
||||
}
|
||||
this.initialiseLiveview();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { connected: connectedPrev } = prevProps;
|
||||
const { connected } = this.props;
|
||||
if (connectedPrev === false && connected === true) {
|
||||
const { dispatchSend } = this.props;
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
dispatchSend(message);
|
||||
|
||||
const requestStreamInterval = interval(3000);
|
||||
this.requestStreamSubscription = requestStreamInterval.subscribe(() => {
|
||||
dispatchSend(message);
|
||||
});
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.initialiseLiveview();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -102,6 +83,30 @@ class Dashboard extends React.Component {
|
||||
return Math.round(Date.now() / 1000);
|
||||
}
|
||||
|
||||
initialiseLiveview() {
|
||||
const { initialised } = this.state;
|
||||
if (!initialised) {
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
const { connected, dispatchSend } = this.props;
|
||||
if (connected) {
|
||||
dispatchSend(message);
|
||||
}
|
||||
|
||||
const requestStreamInterval = interval(2000);
|
||||
this.requestStreamSubscription = requestStreamInterval.subscribe(() => {
|
||||
const { connected: isConnected } = this.props;
|
||||
if (isConnected) {
|
||||
dispatchSend(message);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
initialised: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openModal(file) {
|
||||
this.setState({
|
||||
open: true,
|
||||
@@ -115,17 +120,16 @@ class Dashboard extends React.Component {
|
||||
|
||||
// We check if the camera was getting a valid frame
|
||||
// during the last 5 seconds, otherwise we assume the camera is offline.
|
||||
const isCameraOnline =
|
||||
this.getCurrentTimestamp() - dashboard.cameraOnline < 15;
|
||||
const isCameraOnline = dashboard.cameraOnline;
|
||||
|
||||
// We check if a connection is made to Kerberos Hub, or if Offline mode
|
||||
// has been turned on.
|
||||
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30;
|
||||
const isCloudOnline = dashboard.cloudOnline;
|
||||
let cloudConnection = t('dashboard.not_connected');
|
||||
if (dashboard.offlineMode === 'true') {
|
||||
cloudConnection = t('dashboard.offline_mode');
|
||||
} else {
|
||||
cloudConnection = cloudOnline
|
||||
cloudConnection = isCloudOnline
|
||||
? t('dashboard.connected')
|
||||
: t('dashboard.not_connected');
|
||||
}
|
||||
@@ -185,7 +189,7 @@ class Dashboard extends React.Component {
|
||||
subtitle={cloudConnection}
|
||||
footer="Cloud"
|
||||
icon={
|
||||
cloudOnline && dashboard.offlineMode !== 'true'
|
||||
isCloudOnline && dashboard.offlineMode !== 'true'
|
||||
? 'circle-check-big'
|
||||
: 'circle-cross-big'
|
||||
}
|
||||
@@ -306,7 +310,7 @@ class Dashboard extends React.Component {
|
||||
</div>
|
||||
<div>
|
||||
<h2>{t('dashboard.live_view')}</h2>
|
||||
{!liveviewLoaded && (
|
||||
{(!liveviewLoaded || !isCameraOnline) && (
|
||||
<SetupBox
|
||||
btnicon="preferences"
|
||||
btnlabel={t('dashboard.configure_connection')}
|
||||
@@ -316,7 +320,12 @@ class Dashboard extends React.Component {
|
||||
text={t('dashboard.loading_live_view_description')}
|
||||
/>
|
||||
)}
|
||||
<div style={{ visibility: liveviewLoaded ? 'visible' : 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
visibility:
|
||||
liveviewLoaded && isCameraOnline ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<ImageCard
|
||||
imageSrc={`data:image/png;base64, ${
|
||||
images.length ? images[0] : ''
|
||||
|
||||
@@ -49,60 +49,98 @@ class Login extends React.Component {
|
||||
const { loginError, error } = this.props;
|
||||
|
||||
return (
|
||||
<LandingLayout
|
||||
title="Kerberos Agent"
|
||||
version={config.VERSION}
|
||||
description="Video surveillance for everyone"
|
||||
>
|
||||
<section className="login-body">
|
||||
<Block>
|
||||
<form onSubmit={this.handleSubmit} noValidate>
|
||||
<BlockHeader>
|
||||
<div>
|
||||
<Icon label="login" /> <h4>Login</h4>
|
||||
</div>
|
||||
</BlockHeader>
|
||||
{loginError && (
|
||||
<AlertMessage
|
||||
message={error}
|
||||
onClick={() => this.hideMessage()}
|
||||
/>
|
||||
)}
|
||||
<BlockBody>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<p />
|
||||
<Button
|
||||
buttonType="submit"
|
||||
type="submit"
|
||||
icon="logout"
|
||||
label="Login"
|
||||
/>
|
||||
</BlockFooter>
|
||||
</form>
|
||||
</Block>
|
||||
</section>
|
||||
</LandingLayout>
|
||||
<>
|
||||
{config.MODE !== 'release' && (
|
||||
<div className={`environment ${config.MODE}`}>
|
||||
Environment: {config.MODE}
|
||||
</div>
|
||||
)}
|
||||
<LandingLayout
|
||||
title="Kerberos Agent"
|
||||
version={config.VERSION}
|
||||
description="Video surveillance for everyone"
|
||||
>
|
||||
<section className="login-body">
|
||||
<Block>
|
||||
<form onSubmit={this.handleSubmit} noValidate>
|
||||
<BlockHeader>
|
||||
<div>
|
||||
<Icon label="login" /> <h4>Login</h4>
|
||||
</div>
|
||||
</BlockHeader>
|
||||
{loginError && (
|
||||
<AlertMessage
|
||||
message={error}
|
||||
onClick={() => this.hideMessage()}
|
||||
/>
|
||||
)}
|
||||
<BlockBody>
|
||||
{config.MODE === 'demo' && (
|
||||
<>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly
|
||||
disabled={false}
|
||||
value="root"
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly
|
||||
disabled={false}
|
||||
value="root"
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{config.MODE !== 'demo' && (
|
||||
<>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<p />
|
||||
<Button
|
||||
buttonType="submit"
|
||||
type="submit"
|
||||
icon="logout"
|
||||
label="Login"
|
||||
/>
|
||||
</BlockFooter>
|
||||
</form>
|
||||
</Block>
|
||||
</section>
|
||||
</LandingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,7 +729,7 @@ class Settings extends React.Component {
|
||||
/>
|
||||
)}
|
||||
{verifyOnvifError && (
|
||||
<InfoBar type="alert" message={`${verifyOnvifErrorMessage}`} />
|
||||
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
|
||||
)}
|
||||
|
||||
{loadingHub && (
|
||||
@@ -2114,17 +2114,6 @@ class Settings extends React.Component {
|
||||
/>
|
||||
{config.cloud === this.KERBEROS_HUB && (
|
||||
<>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_proxyurl')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_proxyurl'
|
||||
)}
|
||||
value={config.s3 ? config.s3.proxyuri : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'proxyuri', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_region')}
|
||||
@@ -2136,28 +2125,6 @@ class Settings extends React.Component {
|
||||
this.onUpdateField('s3', 'region', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_bucket')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_bucket'
|
||||
)}
|
||||
value={config.s3 ? config.s3.bucket : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'bucket', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_username')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_username'
|
||||
)}
|
||||
value={config.s3 ? config.s3.username : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'username', value, config.s3)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{config.cloud === this.KERBEROS_VAULT && (
|
||||
|
||||
Reference in New Issue
Block a user