Compare commits

...
112 Commits
Author SHA1 Message Date
Cedric Verstraeten 47f4c19617 Update Config.go 2023-09-13 08:14:25 +02:00
Cedric Verstraeten 280a81809a Update Config.go 2023-09-12 22:38:26 +02:00
Cedric Verstraeten 59358acb30 add logging + empty friendly name 2023-09-12 15:17:56 +02:00
Cedric Verstraeten ebd655ac73 Allow remote configuration through MQTT + restructure config method 2023-09-12 10:50:36 +02:00
Cedric Verstraeten 6325e37aae empty presets caused hub connection failing 2023-09-07 08:16:46 +02:00
Cedric Verstraeten ecabc47847 integrate ondevice configurated presets 2023-08-30 14:12:07 +02:00
Cedric Verstraeten 31cc3d8939 Rely on continuous move will fix the PTZFunctions later 2023-08-29 14:53:48 +02:00
Cedric Verstraeten c71cb71d08 We should reenable debugging, modifying to Info for now 2023-08-29 14:43:14 +02:00
Cedric Verstraeten 65a739ea75 logging PTZ functions 2023-08-29 14:30:25 +02:00
Cedric Verstraeten 410a62e9ef Some cameras do not support AbsoluteMovement, therefore we'll simulate it with ContinuousMove and a polling mechanism 2023-08-28 09:30:08 +02:00
Cedric Verstraeten aa76dd1ec8 enable PTZ preset + introduce new MQTT messaging between Hub and Agent (introduction e2e encryption) 2023-08-25 09:05:53 +02:00
Cedric Verstraeten 384448d123 panic when no mongodb + remove files when no longer available + do not cleanup recordings by default, however cleanup when recordings have been uploaded 2023-07-31 08:49:34 +02:00
Cedric Verstraeten 414f74758c remove curly brackets 2023-07-26 19:22:19 +02:00
Cedric Verstraeten 25403ccdab dont restart if previously was not set! https://github.com/kerberos-io/agent/issues/110 2023-07-12 17:48:43 +02:00
Cedric Verstraeten 4c03132b83 Fail agent when no mongodb can be reached in Kerberos Factory deployment 2023-07-12 09:58:31 +02:00
Cedric Verstraeten 470f8f1cb6 some deployments might miss the variable, such as Kerberos Factory, we'll default these values to "true" 2023-07-11 21:57:07 +02:00
Cedric Verstraeten 5308376a67 add terraform deployment example 2023-07-04 21:04:16 +02:00
Cedric Verstraeten 2b112d29cf further detail snap deployment 2023-07-01 11:52:13 +02:00
Cedric Verstraeten 20d2517e74 add snapcraft 2023-07-01 07:42:12 +02:00
Cedric Verstraeten 12902e2482 disable snapcraft for the time being 2023-06-29 23:15:34 +02:00
Cedric Verstraeten baca44beef Update docker.yml 2023-06-29 21:07:12 +02:00
Cedric Verstraeten d7580744e2 Update docker.yml 2023-06-29 20:52:58 +02:00
Cedric Verstraeten 04f4bc9bf2 Update docker.yml 2023-06-29 20:47:56 +02:00
Cedric Verstraeten d879174f4c add user to lxd group for snapcraft build 2023-06-29 20:40:28 +02:00
Cedric Verstraeten 5a1a62a723 Update docker.yml 2023-06-29 20:31:18 +02:00
Cedric Verstraeten c519b01092 add snapcraft and try to snap the build 2023-06-29 20:23:11 +02:00
Cedric Verstraeten c2ff7ff785 add missing custom config directory references 2023-06-29 20:10:16 +02:00
Cedric Verstraeten 44ec8c0534 try upgrading the dockerfile 2023-06-29 14:06:41 +02:00
Cedric Verstraeten 21c0e01137 add additional environment variables to tweak the internal agent "disable motion, disable liveview" 2023-06-29 12:28:44 +02:00
Cedric Verstraeten f7ced6056d update to port 80 + allow frontend to take into account a custom config directory 2023-06-28 20:24:41 +02:00
Cedric Verstraeten 00917e3f88 add flag arguments instead of absolute arguments (we now support names)
added option to define the config location, can be different than the relative location of the agent binary
2023-06-28 19:28:07 +02:00
Cedric Verstraeten bcfed04a07 add AGENT_TLS_INSECURE to enable Insecure TLS mode 2023-06-28 17:09:29 +02:00
Cedric Verstraeten bf97bd72f1 add osusergo 2023-06-28 08:41:51 +02:00
Cedric Verstraeten 4b8b6bf66a fix balena links 2023-06-26 08:22:12 +02:00
Cedric Verstraeten 4b6c25bb85 documentation for balena apps 2023-06-25 23:25:42 +02:00
Cedric Verstraeten 729b38999e add deploy with balena 2023-06-25 22:14:23 +02:00
Cedric Verstraeten 4cbf0323f1 Reference separate balena repositories 2023-06-25 20:21:44 +02:00
Cedric Verstraeten 1f5cb8ca88 merge directories 2023-06-25 20:05:03 +02:00
Cedric Verstraeten 8be0a04502 add balena deployment (app + block) 2023-06-25 20:03:37 +02:00
Cedric Verstraeten bdc0039a24 fix: might be empty if not set, so will never fire motion alert 2023-06-24 20:22:51 +02:00
Cedric Verstraeten 756b893ecd reference latest tags 2023-06-24 12:58:16 +02:00
Cedric Verstraeten 36323b076f Fix for Kerberos Vault persistence check 2023-06-23 21:13:13 +02:00
Cedric Verstraeten 95f43b6444 Fix for empty vault settings, throw error 2023-06-23 20:20:38 +02:00
Cedric Verstraeten 5c23a62ac3 New function to validate Kerberos Hub connectivity and subscription 2023-06-23 19:01:04 +02:00
Cedric Verstraeten 2b425a2ddd add test video for verification 2023-06-23 17:16:13 +02:00
Cedric Verstraeten abeeb95204 make region editable through ENV + add new upload function to upload to Kerberos Hub 2023-06-23 16:14:01 +02:00
Cédric Verstraeten 6aed20c466 Align to correct region 2023-06-22 15:53:46 +02:00
Cedric Verstraeten 6672535544 fix glitch with loading livestream when hitting dashboard page first 2023-06-14 17:14:40 +02:00
Cedric Verstraeten ed397b6ecc change environment box sizes 2023-06-14 16:59:26 +02:00
Cedric Verstraeten 530e4c654e set permissions to modify the env.js file on runtime 2023-06-14 16:49:50 +02:00
Cedric Verstraeten 913bd1ba12 add demo environment mode 2023-06-14 16:29:13 +02:00
Cedric Verstraeten 84e532be47 reloading of configuration right after updating config + optimise loading stream + fix for ip camera online and cloud online + onvif ptz checks for hub 2023-06-13 23:12:58 +02:00
Cedric Verstraeten 3341e99af1 timetable might be empty 2023-06-13 09:22:51 +02:00
Cedric Verstraeten ced6e678ec Update Settings.jsx 2023-06-12 21:08:05 +02:00
Cedric Verstraeten 340a5d7ef6 Update Settings.jsx 2023-06-12 21:06:21 +02:00
Cedric Verstraeten 60e8edc876 add onvif verify option + improve streaming logic + reconnect websocket 2023-06-12 20:33:41 +02:00
Cedric Verstraeten 9cf9babd73 introduce camera connected variable + allow MQTT stream to be connected even when no camera attached 2023-06-09 14:44:09 +02:00
Cedric Verstraeten 229c246e1c adding ctx (support) + unblock when unsupported codec 2023-06-08 21:50:10 +02:00
Cedric Verstraeten 15d9bcda4f decoding issue caused new mongodb adapter to fail 2023-06-07 22:01:44 +02:00
Cedric Verstraeten 068063695e time table might be empty 2023-06-07 20:24:09 +02:00
Cedric Verstraeten b1722844f3 fix config overriding 2023-06-07 20:19:50 +02:00
Cedric Verstraeten eb5ab48d6c timetable might be empty 2023-06-07 18:47:48 +02:00
Cedric Verstraeten b64f1039d7 hotfix: fix missing polygon from start 2023-06-07 18:22:18 +02:00
Cedric Verstraeten 6fcd6e53a1 hotfix: fix openconfig 2023-06-07 17:07:39 +02:00
Cedric Verstraeten 25537b5f02 add new mongodb adapter 2023-06-07 16:44:54 +02:00
Cédric Verstraeten 2fad541e06 Update README.md 2023-06-03 08:49:58 +02:00
Cédric Verstraeten afefd32a1f Merge pull request #102 from lubikx/master
Alpine+go combination doesn't resolve DNS correctly under docker comp…
2023-05-30 09:07:52 +02:00
Lubor Nosek 89e01e065c Alpine+go combination doesn't resolve DNS correctly under docker compose environment 2023-05-30 08:30:24 +02:00
Cedric Verstraeten 02f3e6a1e2 Update README.md 2023-05-16 12:01:09 +02:00
Cedric Verstraeten ec5a00f3df get rid of print! 2023-05-10 14:41:32 +02:00
Cedric Verstraeten 2860775954 unused var 2023-05-10 11:43:23 +02:00
Cedric Verstraeten d2e8e04833 upgrade joy4, allow OPUS 2023-05-10 11:33:38 +02:00
Cedric Verstraeten fad90390a6 disable transcoder, will free up some memory + add support for audio 2023-05-10 11:33:03 +02:00
Cedric Verstraeten d6ba875473 add mods 2023-05-09 13:16:32 +02:00
Cedric Verstraeten e9ea34c20f connect to Kerberos Hub if required, even if camera is not working 2023-05-09 13:14:03 +02:00
Cedric Verstraeten 99cc7d419f add .vscode launch file 2023-05-06 08:46:31 +02:00
Cedric Verstraeten 1a6dc27535 add vault uri example 2023-05-03 20:03:22 +02:00
Cedric Verstraeten 93f40a8d34 get rid of dependency 2023-04-20 15:28:27 +02:00
Cedric Verstraeten d7f7de97b4 Merge branch 'develop' 2023-04-20 13:37:07 +02:00
Cedric Verstraeten 50babedcbf remove trailing comma 2023-04-20 10:51:35 +02:00
Cédric Verstraeten 4352d993ed Merge pull request #93 from AFLDT/patch-1
Adding Chinese support
2023-04-10 14:59:56 +02:00
xingZhe 1e144e1c60 Change the path of the translation file 2023-04-09 06:41:59 +00:00
Cedric Verstraeten d4e37e0bae update readme + few fixes + add env for dropbox 2023-04-07 23:27:05 +02:00
Cédric Verstraeten 026bf93980 Merge pull request #96 from cedricve/develop
Persistence: adding dropbox support
2023-04-07 22:26:22 +02:00
Cedric Verstraeten 43c166666c fix codacy errors 2023-04-07 22:16:47 +02:00
Cedric Verstraeten f3bda88f3e add comments 2023-04-07 22:06:43 +02:00
Cedric Verstraeten b0af6b2e2b Add testfile for uploading + fixes ui + added translations 2023-04-07 21:42:35 +02:00
Cedric Verstraeten 2925e19b90 complete dropbox integration (add directory + env variables) 2023-04-07 20:21:16 +02:00
Cedric Verstraeten e67b6a1800 initial work for adding dropbox support 2023-04-07 14:49:12 +02:00
Cédric Verstraeten bbbed49887 Add capabilities + fix typo 2023-04-07 08:41:47 +02:00
xingZhe 65fd400d4d Adding Chinese support 2023-04-02 14:25:20 +08:00
Cedric Verstraeten 87f681cfe1 fix show language selector 2023-03-23 18:08:20 +01:00
Cedric Verstraeten f935360fda add delay when failed uploading, send to different mqtt topic when no hub + fixes responsiveness 2023-03-23 16:19:01 +01:00
Cedric Verstraeten 71cd315142 disable MQTT message on motion + rework cloud upload (new option RemoveAfterUpload) + sidebar 2023-03-21 13:31:04 +01:00
Cedric Verstraeten d9694ac1a3 Merge branch 'heads/develop' 2023-03-19 21:36:31 +01:00
Cedric Verstraeten 08f589586d allow timetable and region to be set through environment variables 2023-03-19 20:39:48 +01:00
Cedric Verstraeten 192f78ae78 renabled arm-v6 2023-03-17 15:00:16 +01:00
Cedric Verstraeten ef20d4c0b1 fix arch 2023-03-17 14:59:58 +01:00
Cedric Verstraeten af95c0f798 add japanese, disable armv6 build 2023-03-17 12:50:50 +01:00
Cedric Verstraeten 0e32a10ff5 Merge branch 'master' into develop 2023-03-17 12:43:03 +01:00
Cédric Verstraeten e59c2b179d Merge pull request #83 from jeffersonGlemos/master
feat add pt full translation
2023-03-17 12:42:44 +01:00
Cedric Verstraeten e5d03f19de Merge branch 'master' into develop 2023-03-16 23:01:03 +01:00
Cedric Verstraeten 58c3e73f6f stop uploading if no credentials 2023-03-16 22:42:37 +01:00
Jefferson Gonçalves Lemos 8ca2c44422 feat add pt full translation 2023-03-10 13:14:28 -04:00
Cedric Verstraeten a2e584a225 default values for recording, snapshots, .. 2023-03-09 10:48:26 +01:00
Cedric Verstraeten c4cda0afb0 fix build 2023-03-07 22:47:47 +01:00
Cédric Verstraeten adbb923e92 align variables config.json 2023-03-07 21:30:37 +00:00
Cedric Verstraeten f444ae4ad6 add img to readme codespance make public 2023-03-07 22:25:26 +01:00
Cédric Verstraeten 9fa9538320 Update README.md 2023-03-07 22:15:40 +01:00
Cédric Verstraeten 943e81000e Update Dockerfile 2023-03-07 20:15:00 +01:00
Cédric Verstraeten b16d028293 Merge pull request #79 from kododake/develop
Added translation for Japanese.
2023-02-27 21:10:14 +01:00
かいりゅか 07646e483d Add files via upload 2023-02-24 19:35:06 +09:00
90 changed files with 5754 additions and 1326 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
FROM kerberos/devcontainer:9da0ee3 FROM kerberos/devcontainer:b2bc659
LABEL AUTHOR=Kerberos.io LABEL AUTHOR=Kerberos.io
+1 -1
View File
@@ -14,7 +14,7 @@
// Uncomment the next line to run commands after the container is created - for example installing curl. // Uncomment the next line to run commands after the container is created - for example installing curl.
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download", "postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download",
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/ansible:1": {}, "ghcr.io/devcontainers-contrib/features/ansible:1": {}
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {
+2 -1
View File
@@ -34,7 +34,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
architecture: [arm64, arm/v7, arm/v6] #architecture: [arm64, arm/v7, arm/v6]
architecture: [arm64, arm/v7]
steps: steps:
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
+13
View File
@@ -43,6 +43,7 @@ jobs:
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{steps.short-sha.outputs.sha}} --output type=tar,dest=output-${{matrix.architecture}}.tar . 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 - 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 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 - uses: rickstaa/action-create-tag@v1
with: with:
tag: ${{ steps.short-sha.outputs.sha }} tag: ${{ steps.short-sha.outputs.sha }}
@@ -54,6 +55,17 @@ jobs:
name: ${{ steps.short-sha.outputs.sha }} name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }} tag: ${{ steps.short-sha.outputs.sha }}
artifacts: "agent-${{matrix.architecture}}.tar" 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: build-other:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -62,6 +74,7 @@ jobs:
strategy: strategy:
matrix: matrix:
architecture: [arm64, arm-v7, arm-v6] architecture: [arm64, arm-v7, arm-v6]
#architecture: [arm64, arm-v7]
steps: steps:
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
+1
View File
@@ -10,4 +10,5 @@ machinery/data/recordings
machinery/data/snapshots machinery/data/snapshots
machinery/test* machinery/test*
machinery/init-dev.sh machinery/init-dev.sh
machinery/.env
deployments/docker/private-docker-compose.yaml deployments/docker/private-docker-compose.yaml
+4 -3
View File
@@ -10,7 +10,7 @@ ENV GOSUMDB=off
########################################## ##########################################
# Installing some additional dependencies. # 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 \ git build-essential cmake pkg-config unzip libgtk2.0-dev \
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \ curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
rm -rf /var/lib/apt/lists/* 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 && \ RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
go mod download && \ go mod download && \
go build -tags timetzdata --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 && \ mkdir -p /agent && \
mv main /agent && \ mv main /agent && \
mv version /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 # Set permissions correctly
RUN chown -R agent:kerberosio /home/agent/data 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 # 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!!! # Leeeeettttt'ssss goooooo!!!
# Run the shizzle from the right working directory. # Run the shizzle from the right working directory.
WORKDIR /home/agent WORKDIR /home/agent
CMD ["./main", "run", "opensource", "80"] CMD ["./main", "-action", "run", "-port", "80"]
+120 -61
View File
@@ -18,6 +18,7 @@
[![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE) [![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE)
<a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a> <a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a>
[![Discord Shield](https://discordapp.com/api/guilds/1039619181731135499/widget.png?style=shield)](https://discord.gg/Bj77Vqfp2G) [![Discord Shield](https://discordapp.com/api/guilds/1039619181731135499/widget.png?style=shield)](https://discord.gg/Bj77Vqfp2G)
[![kerberosio](https://snapcraft.io/kerberosio/badge.svg)](https://snapcraft.io/kerberosio)
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io) [**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
@@ -28,8 +29,8 @@ Kerberos Agent is an isolated and scalable video (surveillance) management agent
## :thinking: Prerequisites ## :thinking: Prerequisites
- An IP camera which supports a RTSP H264 encoded stream, - An IP camera which supports a RTSP H264 encoded stream,
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP stream](https://github.com/kerberos-io/camera-to-rtsp). - (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp).
- Any hardware that can run a container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster. - Any hardware (ARMv6, ARMv7, ARM64, AMD) that can run a binary or container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster.
## :video_camera: Is my camera working? ## :video_camera: Is my camera working?
@@ -41,6 +42,7 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
1. [Quickstart - Docker](#quickstart---docker) 1. [Quickstart - Docker](#quickstart---docker)
2. [Quickstart - Balena](#quickstart---balena) 2. [Quickstart - Balena](#quickstart---balena)
3. [Quickstart - Snap](#quickstart---snap)
### Introduction ### 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 ## Quickstart - Balena
Run Kerberos Agent with Balena super powers. Monitor your agent with seamless remote access, and an encrypted https endpoint. 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).
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.
[![balena deploy button](https://www.balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent) [![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
**_Work In Progress_** - Currently we only support IP and USB Cameras, we have [an approach for leveraging the Raspberry Pi camera](https://github.com/kerberos-io/camera-to-rtsp), but this isn't working as expected with Balena. If you require this, you'll need to use the traditional Docker deployment with sidecar as mentioned above. ## Quickstart - Snap
Run Kerberos Agent with our [Snapcraft package](https://snapcraft.io/kerberosio).
snap install kerberosio
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following
sudo kerberosio.agent -action=run -port=80
## A world of Kerberos Agents ## A world of Kerberos Agents
@@ -97,11 +106,16 @@ This repository contains everything you'll need to know about our core product,
- Simplified and modern user interface. - Simplified and modern user interface.
- Multi architecture (ARMv7, ARMv8, amd64, etc). - Multi architecture (ARMv7, ARMv8, amd64, etc).
- Multi camera support: IP Cameras (H264), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp). - Multi camera support: IP Cameras (H264), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
- Single camera per instance (e.g. oneå container per camera). - Single camera per instance (e.g. one container per camera).
- Ability to specifiy conditions: motion region, time table, continuous recording, etc. - Primary and secondary stream setup (record full-res, stream low-res).
- Low resolution streaming through MQTT and full resolution streaming through WebRTC.
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
- Post- and pre-recording on motion detection.
- Ability to create fragmented recordings, and streaming though HLS fMP4.
- [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc. - [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc.
- Cloud storage (Kerberos Hub, Kerberos Vault). WIP: Minio, Storj, etc. - Cloud storage/persistance: Kerberos Hub, Kerberos Vault and Dropbox. [(WIP: Minio, Storj, Google Drive, FTP etc.)](https://github.com/kerberos-io/agent/issues/95)
- WIP: Integrations (Webhooks, MQTT, Script, etc). - WIP: Integrations (Webhooks, MQTT, Script, etc).
- REST API access and documentation through Swagger (trigger recording, update configuration, etc).
- MIT License - MIT License
## How to run and deploy a Kerberos Agent ## How to run and deploy a Kerberos Agent
@@ -117,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) - [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) - [Terraform](https://github.com/kerberos-io/agent/tree/master/deployments#5-terraform)
- [Salt](https://github.com/kerberos-io/agent/tree/master/deployments#6-salt) - [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. 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.
@@ -142,7 +158,10 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
-v $(pwd)/agent/recordings:/home/agent/data/recordings \ -v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest -d --restart=always kerberos/agent:latest
More example [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments) for each deployment and automation tool. More example [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments) for each deployment and automation tool. Please note to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
chmod -R 755 kerberos-agent/
chown 100:101 kerberos-agent/ -R
## Configure with environment variables ## Configure with environment variables
@@ -155,48 +174,59 @@ Next to attaching the configuration file, it is also possible to override the co
-e AGENT_CAPTURE_CONTINUOUS=true \ -e AGENT_CAPTURE_CONTINUOUS=true \
-d --restart=always kerberos/agent:latest -d --restart=always kerberos/agent:latest
| Name | Description | Default Value | | Name | Description | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------- | | --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" | | `AGENT_MODE` | You can choose to run this in 'release' for production, and or 'demo' for showcasing. | "release" |
| `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" | | `AGENT_TLS_INSECURE` | Specify if you want to use `InsecureSkipVerify` for the internal HTTP client. | "false" |
| `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" | | `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_NAME` | The agent friendly-name. | "agent" | | `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" | | `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" |
| `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" | | `AGENT_NAME` | The agent friendly-name. | "agent" |
| `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" | | `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" |
| `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory in (MB). | "100" | | `AGENT_REMOVE_AFTER_UPLOAD` | When enabled, recordings uploaded successfully to a storage will be removed from disk. | "true" |
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" | | `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" |
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" | | `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" | | `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory in (MB). | "100" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" | | `AGENT_TIME` | Enable the timetable for Kerberos Agent | "false" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" | | `AGENT_TIMETABLE` | A (weekly) time table to specify when to make recordings "start1,end1,start2,end2;start1.. | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" | | `AGENT_REGION_POLYGON` | A single polygon set for motion detection: "x1,y1;x2,y2;x3,y3;... | "" |
| `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" | | `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous or motion based recording. | "false" | | `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
| `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" | | `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
| `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" | | `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
| `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" | | `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
| `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" | | `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
| `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" | | `AGENT_CAPTURE_MOTION` | Toggle for enabling or disabling motion. | "true" |
| `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" | | `AGENT_CAPTURE_LIVEVIEW` | Toggle for enabling or disabling liveview. | "true" |
| `AGENT_CLOUD` | Store recordings in a Kerberos Hub (s3) or your Kerberos Vault (kstorage). | "s3" | | `AGENT_CAPTURE_SNAPSHOTS` | Toggle for enabling or disabling snapshot generation. | "true" |
| `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.cloud.kerberos.io" | | `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" |
| `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" | | `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous "true" or motion "false". | "false" |
| `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" | | `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" |
| `AGENT_HUB_USERNAME` | Your Kerberos Hub username, which owns the above access and secret keys. | "" | | `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" |
| `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" | | `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" |
| `AGENT_MQTT_URI` | A MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" | | `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" |
| `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" | | `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" |
| `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" | | `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" |
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" | | `AGENT_MQTT_URI` | A MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" |
| `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" | | `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" |
| `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" | | `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" |
| `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" | | `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
| `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "" . | | `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" |
| `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" | | `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" |
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" | | `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" |
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" | | `AGENT_CLOUD` | Store recordings in Kerberos Hub (s3), Kerberos Vault (kstorage) or Dropbox (dropbox). | "s3" |
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" | | `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_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. | "" |
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" |
| `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. | "" |
## Contribute with Codespaces ## Contribute with Codespaces
@@ -210,6 +240,43 @@ After a few minutes, you will see a beautiful `Visual Studio Code` shown in your
![Kerberos Agent VSCode](assets/img/codespace-vscode.png) ![Kerberos Agent VSCode](assets/img/codespace-vscode.png)
On opening of the GitHub Codespace, some dependencies will be installed. Once this is done go ahead to the `ui/src/config.json` file, and (un)comment following section. Make sure to replace the `externalHost` variable with the DNS name you will retrieve from the next step.
// Uncomment this when using codespaces or other special DNS names (which you can't control)
// replace this with the DNS name of the kerberos agent server (the codespace url)
const externalHost = 'cedricve-automatic-computing-machine-v647rxvj4whx9qp-80.preview.app.github.dev';
const dev = {
ENV: 'dev',
HOSTNAME: externalHost,
//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`,
URL: `${protocol}//${externalHost}`,
WS_URL: `${websocketprotocol}//${externalHost}/ws`,
};
Go and open two terminals one for the `ui` project and one for the `machinery` project.
1. Terminal A:
cd machinery/
go run main.go -action run -port 80
2. Terminal B:
cd ui/
yarn start
Once executed, a popup will show up mentioning `portforwarding`. You should see two ports being opened, one for the ui `3000` and one for the machinery `80`. `Right-click` on the port `80` and change visibility from `private` to `public`, this is required to avoid `CORS` errors.
![Codespace make public](./assets/img/codespace-make-public.png)
As mentioned above, copy the hostname of the `machinery` DNS name, and past it in the `ui/src/config.json` file. Once done reload, the `ui` page in your browser, and you should be able to access the login page with the default credentials `root` and `root`.
## Develop and build ## Develop and build
Kerberos Agent is divided in two parts a `machinery` and `web`. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production). Kerberos Agent is divided in two parts a `machinery` and `web`. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production).
@@ -238,7 +305,7 @@ You can simply run the `machinery` using following commands.
git clone https://github.com/kerberos-io/agent git clone https://github.com/kerberos-io/agent
cd machinery 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. 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.
@@ -273,14 +340,6 @@ By running the `docker build` command, you will create the Kerberos Agent Docker
docker build -t kerberos/agent . docker build -t kerberos/agent .
## Support our project
If you like our product please feel free to execute an Ethereum donation. All donations will flow back and split to our Open Source contributors, as they are the heart of this community.
<img width="272" alt="Ethereum donation linke" src="https://user-images.githubusercontent.com/1546779/173443671-3d773068-ae10-4862-a990-dc7c89f3d9c2.png">
Ethereum Address: `0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE`
## What is new? ## What is new?
This repository contains the next generation of Kerberos.io, **Kerberos Agent (v3)**, and is the successor of the machinery and web repositories. A switch in technologies and architecture has been made. This version is still under active development and can be followed on the [develop branch](https://github.com/kerberos-io/agent/tree/develop) and [project overview](https://github.com/kerberos-io/agent/projects/1). This repository contains the next generation of Kerberos.io, **Kerberos Agent (v3)**, and is the successor of the machinery and web repositories. A switch in technologies and architecture has been made. This version is still under active development and can be followed on the [develop branch](https://github.com/kerberos-io/agent/tree/develop) and [project overview](https://github.com/kerberos-io/agent/projects/1).
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+20 -1
View File
@@ -14,6 +14,7 @@ We will discuss following deployment models.
- [5. Kerberos Factory](#5-kerberos-factory) - [5. Kerberos Factory](#5-kerberos-factory)
- [6. Terraform](#6-terraform) - [6. Terraform](#6-terraform)
- [7. Salt](#7-salt) - [7. Salt](#7-salt)
- [8. Balena](#8-balena)
## 0. Static binary ## 0. Static binary
@@ -53,8 +54,26 @@ All of the previously deployments, `docker`, `kubernetes` and `openshift` are gr
## 6. Terraform ## 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 ## 7. Salt
To be written 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).
+2 -2
View File
@@ -82,7 +82,7 @@
initContainers: initContainers:
- name: download-config - name: download-config
image: kerberos/agent:1b96d01 image: kerberos/agent:latest
volumeMounts: volumeMounts:
- name: kerberos-data - name: kerberos-data
mountPath: /home/agent/data/config mountPath: /home/agent/data/config
@@ -96,7 +96,7 @@
containers: containers:
- name: agent - name: agent
image: kerberos/agent:1b96d01 image: kerberos/agent:latest
volumeMounts: volumeMounts:
- name: kerberos-data - name: kerberos-data
mountPath: /home/agent/data/config mountPath: /home/agent/data/config
+31
View 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).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
You can find the source code, `balena.yaml` and `docker-compose.yaml` files in the [`balena-agent` repository](https://github.com/kerberos-io/balena-agent).
@@ -21,7 +21,7 @@ spec:
initContainers: initContainers:
- name: download-config - name: download-config
image: kerberos/agent:1b96d01 image: kerberos/agent:latest
volumeMounts: volumeMounts:
- name: kerberos-data - name: kerberos-data
mountPath: /home/agent/data/config mountPath: /home/agent/data/config
+15
View File
@@ -0,0 +1,15 @@
# Deployment with Snap Store
By browsing to the Snap Store, you'll be able [to find our own snap `Kerberos Agent`](https://snapcraft.io/kerberosio). You can either install the `Kerberos Agent` through the command line.
snap install kerberosio
Or use the Desktop client to have a visual interface.
![Kerberos Agent on Snap Store](./snapstore.png)
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following.
sudo kerberosio.agent -action=run -port=80
If successfull you'll be able to browse to port `80` or if you defined a different port. This will open the Kerberos Agent interface.
Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

+41
View File
@@ -0,0 +1,41 @@
# Deployment with Terraform
If you are using Terraform as part of your DevOps stack, you might utilise it to deploy your Kerberos Agents. Within this deployment folder we have added an example Terraform file `docker.tf`, which installs the Kerberos Agent `docker` container on a remote system over `SSH`. We might create our own provider in the future, or add additional examples for example `snap`, `kubernetes`, etc.
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
terraform init
Once initialised you should see similar output:
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of telkomindonesia/linux from the dependency lock file
- Using previously-installed telkomindonesia/linux v0.7.0
Go and open the `docker.tf` file and locate the `linux` provider, modify following credentials accordingly. Make sure they match for creating an `SSH` connection.
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
Apply the `docker.tf` file, to install `docker` and the `kerberos/agent` docker container.
terraform apply
Once done you should see following output, and you should be able to reach the remote machine on port `80` or if configured differently the specified port you've defined.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
linux_script.install_docker_kerberos_agent: Modifying... [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
linux_script.install_docker_kerberos_agent: Modifications complete after 3s [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
+47
View File
@@ -0,0 +1,47 @@
terraform {
required_providers {
linux = {
source = "TelkomIndonesia/linux"
version = "0.7.0"
}
}
}
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
locals {
image = "kerberos/agent"
version = "latest"
port = 80
}
resource "linux_script" "install_docker" {
lifecycle_commands {
create = "apt update && apt install -y $PACKAGE_NAME"
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
update = "apt update && apt install -y $PACKAGE_NAME"
delete = "apt remove -y $PACKAGE_NAME"
}
environment = {
PACKAGE_NAME = "docker"
}
}
resource "linux_script" "install_docker_kerberos_agent" {
lifecycle_commands {
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
read = "docker inspect agent"
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
delete = "docker rm agent --force"
}
environment = {
IMAGE = local.image
VERSION = local.version
PORT = local.port
}
}
+5 -2
View File
@@ -9,7 +9,10 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${fileDirname}" "program": "main.go",
} "args": ["-action", "run"],
"envFile": "${workspaceFolder}/.env",
"buildFlags": "--tags dynamic",
},
] ]
} }
+8 -2
View File
@@ -6,6 +6,7 @@
"time": "false", "time": "false",
"offline": "false", "offline": "false",
"auto_clean": "true", "auto_clean": "true",
"remove_after_upload": "true",
"max_directory_size": 100, "max_directory_size": 100,
"timezone": "Africa/Ceuta", "timezone": "Africa/Ceuta",
"capture": { "capture": {
@@ -22,6 +23,10 @@
"device": "" "device": ""
}, },
"continuous": "false", "continuous": "false",
"recording": "true",
"snapshots": "true",
"liveview": "true",
"motion": "true",
"postrecording": 20, "postrecording": 20,
"prerecording": 10, "prerecording": 10,
"maxlengthrecording": 30, "maxlengthrecording": 30,
@@ -90,9 +95,10 @@
"s3": { "s3": {
"proxyuri": "http://proxy.kerberos.io", "proxyuri": "http://proxy.kerberos.io",
"bucket": "kerberosaccept", "bucket": "kerberosaccept",
"region": "eu-west1" "region": "eu-west-1"
}, },
"kstorage": {}, "kstorage": {},
"dropbox": {},
"mqtturi": "tcp://mqtt.kerberos.io:1883", "mqtturi": "tcp://mqtt.kerberos.io:1883",
"mqtt_username": "", "mqtt_username": "",
"mqtt_password": "", "mqtt_password": "",
@@ -106,4 +112,4 @@
"hub_private_key": "", "hub_private_key": "",
"hub_site": "", "hub_site": "",
"condition_uri": "" "condition_uri": ""
} }
Binary file not shown.
+144 -2
View File
@@ -54,6 +54,35 @@ const docTemplate = `{
} }
} }
}, },
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": { "/api/camera/onvif/login": {
"post": { "post": {
"description": "Try to login into ONVIF supported camera.", "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": { "/api/camera/onvif/zoom": {
"post": { "post": {
"description": "Zooming in or out the camera.", "description": "Zooming in or out the camera.",
@@ -244,6 +302,40 @@ const docTemplate = `{
} }
} }
}, },
"/api/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"config"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "Camera Config",
"name": "cameraConfig",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.IPCamera"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/persistence/verify": { "/api/persistence/verify": {
"post": { "post": {
"security": [ "security": [
@@ -283,8 +375,15 @@ const docTemplate = `{
"models.APIResponse": { "models.APIResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {}, "data": {},
"message": {} "message": {},
"ptz_functions": {}
} }
}, },
"models.Authentication": { "models.Authentication": {
@@ -347,9 +446,15 @@ const docTemplate = `{
"ipcamera": { "ipcamera": {
"$ref": "#/definitions/models.IPCamera" "$ref": "#/definitions/models.IPCamera"
}, },
"liveview": {
"type": "string"
},
"maxlengthrecording": { "maxlengthrecording": {
"type": "integer" "type": "integer"
}, },
"motion": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@@ -365,6 +470,12 @@ const docTemplate = `{
"raspicamera": { "raspicamera": {
"$ref": "#/definitions/models.RaspiCamera" "$ref": "#/definitions/models.RaspiCamera"
}, },
"recording": {
"type": "string"
},
"snapshots": {
"type": "string"
},
"transcodingresolution": { "transcodingresolution": {
"type": "integer" "type": "integer"
}, },
@@ -391,6 +502,12 @@ const docTemplate = `{
"condition_uri": { "condition_uri": {
"type": "string" "type": "string"
}, },
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"friendly_name": {
"type": "string"
},
"heartbeaturi": { "heartbeaturi": {
"description": "obsolete", "description": "obsolete",
"type": "string" "type": "string"
@@ -434,6 +551,9 @@ const docTemplate = `{
"region": { "region": {
"$ref": "#/definitions/models.Region" "$ref": "#/definitions/models.Region"
}, },
"remove_after_upload": {
"type": "string"
},
"s3": { "s3": {
"$ref": "#/definitions/models.S3" "$ref": "#/definitions/models.S3"
}, },
@@ -477,6 +597,17 @@ const docTemplate = `{
} }
} }
}, },
"models.Dropbox": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"directory": {
"type": "string"
}
}
},
"models.IPCamera": { "models.IPCamera": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -484,7 +615,7 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"onvif": { "onvif": {
"type": "boolean" "type": "string"
}, },
"onvif_password": { "onvif_password": {
"type": "string" "type": "string"
@@ -555,6 +686,17 @@ const docTemplate = `{
} }
} }
}, },
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": { "models.OnvifZoom": {
"type": "object", "type": "object",
"properties": { "properties": {
+144 -2
View File
@@ -46,6 +46,35 @@
} }
} }
}, },
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": { "/api/camera/onvif/login": {
"post": { "post": {
"description": "Try to login into ONVIF supported camera.", "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": { "/api/camera/onvif/zoom": {
"post": { "post": {
"description": "Zooming in or out the camera.", "description": "Zooming in or out the camera.",
@@ -236,6 +294,40 @@
} }
} }
}, },
"/api/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"config"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "Camera Config",
"name": "cameraConfig",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.IPCamera"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/persistence/verify": { "/api/persistence/verify": {
"post": { "post": {
"security": [ "security": [
@@ -275,8 +367,15 @@
"models.APIResponse": { "models.APIResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {}, "data": {},
"message": {} "message": {},
"ptz_functions": {}
} }
}, },
"models.Authentication": { "models.Authentication": {
@@ -339,9 +438,15 @@
"ipcamera": { "ipcamera": {
"$ref": "#/definitions/models.IPCamera" "$ref": "#/definitions/models.IPCamera"
}, },
"liveview": {
"type": "string"
},
"maxlengthrecording": { "maxlengthrecording": {
"type": "integer" "type": "integer"
}, },
"motion": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@@ -357,6 +462,12 @@
"raspicamera": { "raspicamera": {
"$ref": "#/definitions/models.RaspiCamera" "$ref": "#/definitions/models.RaspiCamera"
}, },
"recording": {
"type": "string"
},
"snapshots": {
"type": "string"
},
"transcodingresolution": { "transcodingresolution": {
"type": "integer" "type": "integer"
}, },
@@ -383,6 +494,12 @@
"condition_uri": { "condition_uri": {
"type": "string" "type": "string"
}, },
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"friendly_name": {
"type": "string"
},
"heartbeaturi": { "heartbeaturi": {
"description": "obsolete", "description": "obsolete",
"type": "string" "type": "string"
@@ -426,6 +543,9 @@
"region": { "region": {
"$ref": "#/definitions/models.Region" "$ref": "#/definitions/models.Region"
}, },
"remove_after_upload": {
"type": "string"
},
"s3": { "s3": {
"$ref": "#/definitions/models.S3" "$ref": "#/definitions/models.S3"
}, },
@@ -469,6 +589,17 @@
} }
} }
}, },
"models.Dropbox": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"directory": {
"type": "string"
}
}
},
"models.IPCamera": { "models.IPCamera": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -476,7 +607,7 @@
"type": "string" "type": "string"
}, },
"onvif": { "onvif": {
"type": "boolean" "type": "string"
}, },
"onvif_password": { "onvif_password": {
"type": "string" "type": "string"
@@ -547,6 +678,17 @@
} }
} }
}, },
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": { "models.OnvifZoom": {
"type": "object", "type": "object",
"properties": { "properties": {
+93 -1
View File
@@ -2,8 +2,13 @@ basePath: /
definitions: definitions:
models.APIResponse: models.APIResponse:
properties: properties:
can_pan_tilt:
type: boolean
can_zoom:
type: boolean
data: {} data: {}
message: {} message: {}
ptz_functions: {}
type: object type: object
models.Authentication: models.Authentication:
properties: properties:
@@ -44,8 +49,12 @@ definitions:
type: integer type: integer
ipcamera: ipcamera:
$ref: '#/definitions/models.IPCamera' $ref: '#/definitions/models.IPCamera'
liveview:
type: string
maxlengthrecording: maxlengthrecording:
type: integer type: integer
motion:
type: string
name: name:
type: string type: string
pixelChangeThreshold: pixelChangeThreshold:
@@ -56,6 +65,10 @@ definitions:
type: integer type: integer
raspicamera: raspicamera:
$ref: '#/definitions/models.RaspiCamera' $ref: '#/definitions/models.RaspiCamera'
recording:
type: string
snapshots:
type: string
transcodingresolution: transcodingresolution:
type: integer type: integer
transcodingwebrtc: transcodingwebrtc:
@@ -73,6 +86,10 @@ definitions:
type: string type: string
condition_uri: condition_uri:
type: string type: string
dropbox:
$ref: '#/definitions/models.Dropbox'
friendly_name:
type: string
heartbeaturi: heartbeaturi:
description: obsolete description: obsolete
type: string type: string
@@ -102,6 +119,8 @@ definitions:
type: string type: string
region: region:
$ref: '#/definitions/models.Region' $ref: '#/definitions/models.Region'
remove_after_upload:
type: string
s3: s3:
$ref: '#/definitions/models.S3' $ref: '#/definitions/models.S3'
stunuri: stunuri:
@@ -130,12 +149,19 @@ definitions:
"y": "y":
type: number type: number
type: object type: object
models.Dropbox:
properties:
access_token:
type: string
directory:
type: string
type: object
models.IPCamera: models.IPCamera:
properties: properties:
fps: fps:
type: string type: string
onvif: onvif:
type: boolean type: string
onvif_password: onvif_password:
type: string type: string
onvif_username: onvif_username:
@@ -181,6 +207,13 @@ definitions:
tilt: tilt:
type: number type: number
type: object type: object
models.OnvifPreset:
properties:
onvif_credentials:
$ref: '#/definitions/models.OnvifCredentials'
preset:
type: string
type: object
models.OnvifZoom: models.OnvifZoom:
properties: properties:
onvif_credentials: onvif_credentials:
@@ -289,6 +322,25 @@ paths:
summary: Will return the ONVIF capabilities for the specific camera. summary: Will return the ONVIF capabilities for the specific camera.
tags: tags:
- camera - 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: /api/camera/onvif/login:
post: post:
description: Try to login into ONVIF supported camera. description: Try to login into ONVIF supported camera.
@@ -327,6 +379,25 @@ paths:
summary: Panning or/and tilting the camera. summary: Panning or/and tilting the camera.
tags: tags:
- camera - 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: /api/camera/onvif/zoom:
post: post:
description: Zooming in or out the camera. description: Zooming in or out the camera.
@@ -414,6 +485,27 @@ paths:
summary: Get Authorization token. summary: Get Authorization token.
tags: tags:
- authentication - authentication
/api/onvif/verify:
post:
description: Will verify the ONVIF connectivity.
operationId: verify-onvif
parameters:
- description: Camera Config
in: body
name: cameraConfig
required: true
schema:
$ref: '#/definitions/models.IPCamera'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will verify the ONVIF connectivity.
tags:
- config
/api/persistence/verify: /api/persistence/verify:
post: post:
description: Will verify the persistence. description: Will verify the persistence.
+28 -12
View File
@@ -2,15 +2,18 @@ module github.com/kerberos-io/agent/machinery
go 1.19 go 1.19
//replace github.com/kerberos-io/joy4 v1.0.54 => ../../../../github.com/kerberos-io/joy4 // replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
//replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
require ( require (
github.com/InVisionApp/conjungo v1.1.0 github.com/InVisionApp/conjungo v1.1.0
github.com/appleboy/gin-jwt/v2 v2.9.1 github.com/appleboy/gin-jwt/v2 v2.9.1
github.com/asticode/go-astits v1.11.0
github.com/bluenviron/gortsplib/v3 v3.6.1
github.com/bluenviron/mediacommon v0.5.0
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6
github.com/deepch/vdk v0.0.19 github.com/deepch/vdk v0.0.19
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
github.com/eclipse/paho.mqtt.golang v1.4.2 github.com/eclipse/paho.mqtt.golang v1.4.2
github.com/elastic/go-sysinfo v1.9.0 github.com/elastic/go-sysinfo v1.9.0
github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/cors v1.4.0
@@ -21,19 +24,20 @@ require (
github.com/golang-module/carbon/v2 v2.2.3 github.com/golang-module/carbon/v2 v2.2.3
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/kellydunn/golang-geo v0.7.0 github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.55 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/minio/minio-go/v6 v6.0.57
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pion/rtp v1.7.13
github.com/pion/webrtc/v3 v3.1.50 github.com/pion/webrtc/v3 v3.1.50
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/swaggo/files v1.0.0 github.com/swaggo/files v1.0.0
github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.9 github.com/swaggo/swag v1.8.9
github.com/tevino/abool v1.2.0 github.com/tevino/abool v1.2.0
go.mongodb.org/mongo-driver v1.7.5
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0 gopkg.in/DataDog/dd-trace-go.v1 v1.46.0
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )
@@ -49,6 +53,7 @@ require (
github.com/Microsoft/go-winio v0.5.1 // indirect github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/beevik/etree v1.1.0 // indirect github.com/beevik/etree v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/clbanning/mxj v1.8.4 // indirect github.com/clbanning/mxj v1.8.4 // indirect
@@ -65,14 +70,19 @@ require (
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect github.com/goccy/go-json v0.10.0 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210423192551-a2663126120b // indirect github.com/google/pprof v0.0.0-20210423192551-a2663126120b // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/klauspost/cpuid v1.2.3 // indirect github.com/klauspost/cpuid v1.2.3 // indirect
github.com/kylelemons/go-gypsy v1.0.0 // indirect github.com/kylelemons/go-gypsy v1.0.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
@@ -84,6 +94,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/gomega v1.27.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/philhofer/fwd v1.1.1 // indirect github.com/philhofer/fwd v1.1.1 // indirect
github.com/pion/datachannel v1.5.5 // indirect github.com/pion/datachannel v1.5.5 // indirect
@@ -94,7 +105,6 @@ require (
github.com/pion/mdns v0.0.5 // indirect github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.10 // indirect github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/rtp v1.7.13 // indirect
github.com/pion/sctp v1.8.5 // indirect github.com/pion/sctp v1.8.5 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect github.com/pion/srtp/v2 v2.0.10 // indirect
@@ -109,17 +119,23 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect github.com/tinylib/msgp v1.1.6 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/ziutek/mymysql v1.5.4 // indirect github.com/ziutek/mymysql v1.5.4 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
golang.org/x/crypto v0.4.0 // indirect golang.org/x/crypto v0.4.0 // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/grpc v1.32.0 // indirect google.golang.org/grpc v1.32.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/ini.v1 v1.42.0 // indirect
+325 -17
View File
@@ -1,6 +1,39 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw= github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw= github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 h1:Rmz52Xlc5k3WzAHzD0SCH4USCzyti7EbK4HtrHys3ME= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 h1:Rmz52Xlc5k3WzAHzD0SCH4USCzyti7EbK4HtrHys3ME=
@@ -31,8 +64,16 @@ github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxG
github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk= github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/bluenviron/gortsplib/v3 v3.6.1 h1:+/kPiwmdRwUasU5thOBATJQ4/yD+vrIEutJyRTB/f+0=
github.com/bluenviron/gortsplib/v3 v3.6.1/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A=
github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY=
github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4PWaa2GjOh90HG88uKi2a+B6VnQcDlCA= github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4PWaa2GjOh90HG88uKi2a+B6VnQcDlCA=
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0= github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -58,6 +99,8 @@ github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/Lu
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU=
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
@@ -70,6 +113,7 @@ github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae h1:3KvK2DmA7TxQ6PZ2f0rWbdqjgJhRcqgbY70bBeE4clI= github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae h1:3KvK2DmA7TxQ6PZ2f0rWbdqjgJhRcqgbY70bBeE4clI=
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g= github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
@@ -93,6 +137,9 @@ github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -115,6 +162,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
@@ -127,41 +176,79 @@ github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2
github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI= github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc= github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc=
github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
@@ -171,14 +258,20 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg= github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg=
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU= github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
github.com/kerberos-io/joy4 v1.0.55 h1:P5RISBp8kUowgb/bvqLPVKPJL9n9jI/wXBCLs+XFMWg= github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
github.com/kerberos-io/joy4 v1.0.55/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU= 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.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
github.com/kerberos-io/onvif v0.0.5/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0= 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=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -220,6 +313,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e h1:bQo/jQ9qvcw7zqnovm8IbLsaOq3F+ELUQcxtxvalQvA= github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e h1:bQo/jQ9qvcw7zqnovm8IbLsaOq3F+ELUQcxtxvalQvA=
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e/go.mod h1:PW9xCZScEClMBP22n37i0SnN/8B9YzNXTNvOaIkLjv0= github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e/go.mod h1:PW9xCZScEClMBP22n37i0SnN/8B9YzNXTNvOaIkLjv0=
@@ -236,8 +330,9 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
@@ -293,6 +388,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -300,6 +396,7 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -332,8 +429,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
@@ -349,6 +446,7 @@ github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
@@ -359,19 +457,40 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -382,24 +501,69 @@ golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -416,29 +580,62 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -460,49 +657,150 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d h1:HJaAqDnKreMkv+AQyf1Mcw0jEmL9kKBNL07RDJu1N/k= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -510,7 +808,11 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
@@ -529,8 +831,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -550,8 +850,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU= inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+45 -26
View File
@@ -1,13 +1,16 @@
package main package main
import ( import (
"fmt" "context"
"flag"
"os" "os"
"time" "time"
"github.com/kerberos-io/agent/machinery/src/components" "github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models" "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/routers"
"github.com/kerberos-io/agent/machinery/src/utils" "github.com/kerberos-io/agent/machinery/src/utils"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
@@ -17,7 +20,6 @@ import (
var VERSION = "3.0.0" var VERSION = "3.0.0"
func main() { func main() {
// You might be interested in debugging the agent. // You might be interested in debugging the agent.
if os.Getenv("DATADOG_AGENT_ENABLED") == "true" { if os.Getenv("DATADOG_AGENT_ENABLED") == "true" {
if os.Getenv("DATADOG_AGENT_K8S_ENABLED") == "true" { if os.Getenv("DATADOG_AGENT_K8S_ENABLED") == "true" {
@@ -26,7 +28,7 @@ func main() {
} else { } else {
service := os.Getenv("DATADOG_AGENT_SERVICE") service := os.Getenv("DATADOG_AGENT_SERVICE")
environment := os.Getenv("DATADOG_AGENT_ENVIRONMENT") environment := os.Getenv("DATADOG_AGENT_ENVIRONMENT")
fmt.Println("Starting Datadog Agent with service: " + service + " and environment: " + environment) log.Log.Info("Starting Datadog Agent with service: " + service + " and environment: " + environment)
rules := []tracer.SamplingRule{tracer.RateRule(1)} rules := []tracer.SamplingRule{tracer.RateRule(1)}
tracer.Start( tracer.Start(
tracer.WithSamplingRules(rules), tracer.WithSamplingRules(rules),
@@ -50,35 +52,39 @@ func main() {
} }
// Start the show ;) // 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") timezone, _ := time.LoadLocation("CET")
log.Log.Init(timezone) log.Log.Init(configDirectory, timezone)
switch action { switch action {
case "version": case "version":
log.Log.Info("You are currrently running Kerberos Agent " + VERSION) log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
case "pending-upload":
name := os.Args[2]
fmt.Println(name)
case "discover": case "discover":
timeout := os.Args[2] log.Log.Info(timeout)
fmt.Println(timeout)
case "run": case "run":
{ {
name := os.Args[2] // Print Kerberos.io ASCII art
port := os.Args[3] utils.PrintASCIIArt()
// Check the folder permissions, it might be that we do not have permissions to write // Print the environment variables which include "AGENT_" as prefix.
// recordings, update the configuration or save snapshots. utils.PrintEnvironmentVariables()
err := utils.CheckDataDirectoryPermissions()
if err != nil {
log.Log.Fatal(err.Error())
}
// Read the config on start, and pass it to the other // Read the config on start, and pass it to the other
// function and features. Please note that this might be changed // function and features. Please note that this might be changed
@@ -88,21 +94,28 @@ func main() {
configuration.Port = port configuration.Port = port
// Open this configuration either from Kerberos Agent or Kerberos Factory. // 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 // 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(configDirectory)
// Set timezone // Set timezone
timezone, _ := time.LoadLocation(configuration.Config.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 // Check if we have a device Key or not, if not
// we will generate one. // we will generate one.
if configuration.Config.Key == "" { if configuration.Config.Key == "" {
key := utils.RandStringBytesMaskImpr(30) key := utils.RandStringBytesMaskImpr(30)
configuration.Config.Key = key configuration.Config.Key = key
err := components.StoreConfig(configuration.Config) err := configService.StoreConfig(configDirectory, configuration.Config)
if err == nil { if err == nil {
log.Log.Info("Main: updated unique key for agent to: " + key) log.Log.Info("Main: updated unique key for agent to: " + key)
} else { } else {
@@ -110,16 +123,22 @@ func main() {
} }
} }
// Create a cancelable context, which will be used to cancel and restart.
// This is used to restart the agent when the configuration is updated.
ctx, cancel := context.WithCancel(context.Background())
// Bootstrapping the agent // Bootstrapping the agent
communication := models.Communication{ communication := models.Communication{
Context: &ctx,
CancelContext: &cancel,
HandleBootstrap: make(chan string, 1), HandleBootstrap: make(chan string, 1),
} }
go components.Bootstrap(&configuration, &communication) go components.Bootstrap(configDirectory, &configuration, &communication)
// Start the REST API. // Start the REST API.
routers.StartWebserver(&configuration, &communication) routers.StartWebserver(configDirectory, &configuration, &communication)
} }
default: default:
fmt.Println("Sorry I don't understand :(") log.Log.Error("Main: Sorry I don't understand :(")
} }
} }
+1
View File
@@ -0,0 +1 @@
package api
+3 -56
View File
@@ -1,6 +1,7 @@
package capture package capture
import ( import (
"context"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@@ -15,9 +16,9 @@ import (
"github.com/kerberos-io/joy4/format" "github.com/kerberos-io/joy4/format"
) )
func OpenRTSP(url string) (av.DemuxCloser, []av.CodecData, error) { func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) {
format.RegisterAll() format.RegisterAll()
infile, err := avutil.Open(url) infile, err := avutil.Open(ctx, url)
if err == nil { if err == nil {
streams, errstreams := infile.Streams() streams, errstreams := infile.Streams()
return infile, streams, errstreams return infile, streams, errstreams
@@ -60,60 +61,6 @@ func DecodeImage(frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoD
return img, err return img, err
} }
func GetStreamInsights(infile av.DemuxCloser, streams []av.CodecData) (int, int, int, int) {
var width, height, fps, gopsize int
for _, stream := range streams {
if stream.Type().IsAudio() {
//astream := stream.(av.AudioCodecData)
} else if stream.Type().IsVideo() {
vstream := stream.(av.VideoCodecData)
width = vstream.Width()
height = vstream.Height()
}
}
loop:
for timeout := time.After(1 * time.Second); ; {
var err error
if _, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleStream: " + err.Error())
}
fps++
select {
case <-timeout:
break loop
default:
}
}
gopCounter := 0
start := false
for {
var pkt av.Packet
var err error
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleStream: " + err.Error())
}
// Could be that a decode is throwing errors.
if len(pkt.Data) > 0 {
if start {
gopCounter = gopCounter + 1
}
if pkt.IsKeyFrame {
if start == false {
start = true
} else {
gopsize = gopCounter
break
}
}
}
}
return width, height, fps, gopsize
}
func HandleStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) { func HandleStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
log.Log.Debug("HandleStream: started") log.Log.Debug("HandleStream: started")
+16 -11
View File
@@ -2,6 +2,7 @@
package capture package capture
import ( import (
"context"
"os" "os"
"strconv" "strconv"
"time" "time"
@@ -16,7 +17,7 @@ import (
"github.com/kerberos-io/joy4/av" "github.com/kerberos-io/joy4/av"
) )
func CleanupRecordingDirectory(configuration *models.Configuration) { func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
autoClean := configuration.Config.AutoClean autoClean := configuration.Config.AutoClean
if autoClean == "true" { if autoClean == "true" {
maxSize := configuration.Config.MaxDirectorySize maxSize := configuration.Config.MaxDirectorySize
@@ -24,7 +25,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
maxSize = 300 maxSize = 300
} }
// Total size of the recording directory. // Total size of the recording directory.
recordingsDirectory := "./data/recordings" recordingsDirectory := configDirectory + "/data/recordings"
size, err := utils.DirSize(recordingsDirectory) size, err := utils.DirSize(recordingsDirectory)
if err == nil { if err == nil {
sizeInMB := size / 1000 / 1000 sizeInMB := size / 1000 / 1000
@@ -50,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 config := configuration.Config
@@ -133,13 +134,13 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
} }
// Create a symbol link. // Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name) fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close() fc.Close()
recordingStatus = "idle" recordingStatus = "idle"
// Clean up the recording directory if necessary. // Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration) CleanupRecordingDirectory(configDirectory, configuration)
} }
// If not yet started and a keyframe, let's make a recording // If not yet started and a keyframe, let's make a recording
@@ -191,7 +192,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769" "769"
name = s + ".mp4" name = s + ".mp4"
fullName = "./data/recordings/" + name fullName = configDirectory + "/data/recordings/" + name
// Running... // Running...
log.Log.Info("Recording started") log.Log.Info("Recording started")
@@ -258,7 +259,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
} }
// Create a symbol link. // Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name) fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close() fc.Close()
recordingStatus = "idle" recordingStatus = "idle"
@@ -314,7 +315,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769" "769"
name := s + ".mp4" name := s + ".mp4"
fullName := "./data/recordings/" + name fullName := configDirectory + "/data/recordings/" + name
// Running... // Running...
log.Log.Info("HandleRecordStream: Recording started") log.Log.Info("HandleRecordStream: Recording started")
@@ -405,11 +406,11 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
} }
// Create a symbol linc. // Create a symbol linc.
fc, _ := os.Create("./data/cloud/" + name) fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close() fc.Close()
// Clean up the recording directory if necessary. // Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration) CleanupRecordingDirectory(configDirectory, configuration)
} }
} }
@@ -431,6 +432,10 @@ func VerifyCamera(c *gin.Context) {
var cameraStreams models.CameraStreams var cameraStreams models.CameraStreams
err := c.BindJSON(&cameraStreams) err := c.BindJSON(&cameraStreams)
// Should return in 5 seconds.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err == nil { if err == nil {
streamType := c.Param("streamType") streamType := c.Param("streamType")
@@ -442,7 +447,7 @@ func VerifyCamera(c *gin.Context) {
if streamType == "secondary" { if streamType == "secondary" {
rtspUrl = cameraStreams.SubRTSP rtspUrl = cameraStreams.SubRTSP
} }
_, codecs, err := OpenRTSP(rtspUrl) _, codecs, err := OpenRTSP(ctx, rtspUrl)
if err == nil { if err == nil {
videoIdx := -1 videoIdx := -1
+395 -247
View File
@@ -15,14 +15,12 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-module/carbon/v2" "github.com/golang-module/carbon/v2"
"github.com/kerberos-io/joy4/av/pubsub" "github.com/kerberos-io/joy4/av/pubsub"
"github.com/minio/minio-go/v6"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
av "github.com/kerberos-io/joy4/av" av "github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/cgo/ffmpeg" "github.com/kerberos-io/joy4/cgo/ffmpeg"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
@@ -34,8 +32,8 @@ import (
"github.com/kerberos-io/agent/machinery/src/webrtc" "github.com/kerberos-io/agent/machinery/src/webrtc"
) )
func PendingUpload() { func PendingUpload(configDirectory string) {
ff, err := utils.ReadDirectory("./data/cloud/") ff, err := utils.ReadDirectory(configDirectory + "/data/cloud/")
if err == nil { if err == nil {
for _, f := range ff { for _, f := range ff {
log.Log.Info(f.Name()) log.Log.Info(f.Name())
@@ -43,21 +41,22 @@ 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") log.Log.Debug("HandleUpload: started")
config := configuration.Config config := configuration.Config
watchDirectory := "./data/cloud/" watchDirectory := configDirectory + "/data/cloud/"
if config.Offline == "true" { if config.Offline == "true" {
log.Log.Debug("HandleUpload: stopping as Offline is enabled.") log.Log.Debug("HandleUpload: stopping as Offline is enabled.")
} else { } else {
// Half a second delay between two uploads
delay := 500 * time.Millisecond
loop: loop:
for { for {
ff, err := utils.ReadDirectory(watchDirectory)
// This will check if we need to stop the thread, // This will check if we need to stop the thread,
// because of a reconfiguration. // because of a reconfiguration.
select { select {
@@ -66,7 +65,10 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
} }
if err == nil { ff, err := utils.ReadDirectory(watchDirectory)
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
} else {
for _, f := range ff { for _, f := range ff {
// This will check if we need to stop the thread, // This will check if we need to stop the thread,
@@ -78,11 +80,66 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com
} }
fileName := f.Name() fileName := f.Name()
if config.Cloud == "s3" { uploaded := false
UploadS3(configuration, fileName, watchDirectory) configured := false
} else if config.Cloud == "kstorage" { err = nil
UploadKerberosVault(configuration, fileName, watchDirectory) 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)
} else if config.Cloud == "gdrive" {
// Todo: implement gdrive upload
} else if config.Cloud == "onedrive" {
// Todo: implement onedrive upload
} else if config.Cloud == "minio" {
// Todo: implement minio upload
} else if config.Cloud == "webdav" {
// Todo: implement webdav upload
} else if config.Cloud == "ftp" {
// 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)
// Check if the file is uploaded, if so, remove it.
if uploaded {
delay = 500 * time.Millisecond // reset
err := os.Remove(watchDirectory + fileName)
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
}
// Check if we need to remove the original recording
// removeAfterUpload is set to false by default
if config.RemoveAfterUpload != "false" {
err := os.Remove(configDirectory + "/data/recordings/" + fileName)
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
}
}
} else if !configured {
err := os.Remove(watchDirectory + fileName)
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
}
} else {
delay = 20 * time.Second // slow down
if err != nil {
log.Log.Error("HandleUpload: " + err.Error())
}
}
time.Sleep(delay)
} }
} }
} }
@@ -165,161 +222,220 @@ func GetSystemInfo() (models.System, error) {
func HandleHeartBeat(configuration *models.Configuration, communication *models.Communication, uptimeStart time.Time) { func HandleHeartBeat(configuration *models.Configuration, communication *models.Communication, uptimeStart time.Time) {
log.Log.Debug("HandleHeartBeat: started") log.Log.Debug("HandleHeartBeat: started")
config := configuration.Config loop:
for {
if config.Offline == "true" { config := configuration.Config
log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.")
} else {
url := config.HeartbeatURI if config.Offline == "true" {
key := "" log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.")
username := "" } else {
vaultURI := ""
url := config.HeartbeatURI
key := ""
username := ""
vaultURI := ""
username = config.S3.Username
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
username = config.S3.Username username = config.S3.Username
key = config.S3.Publickey if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" { username = config.S3.Username
key = config.KStorage.CloudKey key = config.S3.Publickey
username = config.KStorage.Directory } else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
} key = config.KStorage.CloudKey
username = config.KStorage.Directory
// This is the new way ;)
if config.HubURI != "" {
url = config.HubURI + "/devices/heartbeat"
}
if config.HubKey != "" {
key = config.HubKey
}
loop:
for {
// Check if we have a friendly name or not.
name := config.Name
if config.FriendlyName != "" {
name = config.FriendlyName
} }
// Get some system information // This is the new way ;)
// like the uptime, hostname, memory usage, etc. if config.HubURI != "" {
system, _ := GetSystemInfo() url = config.HubURI + "/devices/heartbeat"
}
if config.HubKey != "" {
key = config.HubKey
}
// We will formated the uptime to a human readable format if key != "" {
// this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours. // Check if we have a friendly name or not.
uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05") name := config.Name
uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans() if config.FriendlyName != "" {
uptimeString = strings.ReplaceAll(uptimeString, "ago", "") name = config.FriendlyName
// Do the same for boottime
bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05")
boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans()
boottimeString = strings.ReplaceAll(boottimeString, "ago", "")
// We'll check which mode is enabled for the camera.
onvifEnabled := "false"
if config.Capture.IPCamera.ONVIFXAddr != "" {
device, err := onvif.ConnectToOnvifDevice(configuration)
if err == nil {
capabilities := onvif.GetCapabilitiesFromDevice(device)
for _, v := range capabilities {
if v == "PTZ" || v == "ptz" {
onvifEnabled = "true"
}
}
} }
}
// Check if the agent is running inside a cluster (Kerberos Factory) or as // Get some system information
// an open source agent // like the uptime, hostname, memory usage, etc.
isEnterprise := false system, _ := GetSystemInfo()
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
isEnterprise = true
}
// Congert to string // We will formated the uptime to a human readable format
macs, _ := json.Marshal(system.MACs) // this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours.
ips, _ := json.Marshal(system.IPs) uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05")
uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans()
uptimeString = strings.ReplaceAll(uptimeString, "ago", "")
var object = fmt.Sprintf(`{ // Do the same for boottime
"key" : "%s", bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05")
"version" : "3.0.0", boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans()
"release" : "%s", boottimeString = strings.ReplaceAll(boottimeString, "ago", "")
"cpuid" : "%s",
"clouduser" : "%s",
"cloudpublickey" : "%s",
"cameraname" : "%s",
"enterprise" : %t,
"hostname" : "%s",
"architecture" : "%s",
"totalMemory" : "%d",
"usedMemory" : "%d",
"freeMemory" : "%d",
"processMemory" : "%d",
"mac_list" : %s,
"ip_list" : %s,
"board" : "",
"disk1size" : "%s",
"disk3size" : "%s",
"diskvdasize" : "%s",
"uptime" : "%s",
"boot_time" : "%s",
"siteID" : "%s",
"onvif" : "%s",
"numberoffiles" : "33",
"timestamp" : 1564747908,
"cameratype" : "IPCamera",
"docker" : true,
"kios" : false,
"raspberrypi" : false
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled)
var jsonStr = []byte(object) // We'll check which mode is enabled for the camera.
buffy := bytes.NewBuffer(jsonStr) onvifEnabled := "false"
req, _ := http.NewRequest("POST", url, buffy) onvifZoom := "false"
req.Header.Set("Content-Type", "application/json") onvifPanTilt := "false"
onvifPresets := "false"
var onvifPresetsList []byte
if config.Capture.IPCamera.ONVIFXAddr != "" {
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
configurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil {
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("[]")
}
client := &http.Client{} // Check if the agent is running inside a cluster (Kerberos Factory) or as
resp, err := client.Do(req) // an open source agent
if resp != nil { isEnterprise := false
resp.Body.Close() if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
} isEnterprise = true
if err == nil && resp.StatusCode == 200 { }
communication.CloudTimestamp.Store(time.Now().Unix())
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.")
} else {
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Hub.")
}
// If we have a Kerberos Vault connected, we will also send some analytics // Congert to string
// to that service. macs, _ := json.Marshal(system.MACs)
vaultURI = config.KStorage.URI ips, _ := json.Marshal(system.IPs)
if vaultURI != "" { cameraConnected := "true"
buffy = bytes.NewBuffer(jsonStr) if communication.CameraConnected == false {
req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy) cameraConnected = "false"
}
var object = fmt.Sprintf(`{
"key" : "%s",
"version" : "3.0.0",
"release" : "%s",
"cpuid" : "%s",
"clouduser" : "%s",
"cloudpublickey" : "%s",
"cameraname" : "%s",
"enterprise" : %t,
"hostname" : "%s",
"architecture" : "%s",
"totalMemory" : "%d",
"usedMemory" : "%d",
"freeMemory" : "%d",
"processMemory" : "%d",
"mac_list" : %s,
"ip_list" : %s,
"board" : "",
"disk1size" : "%s",
"disk3size" : "%s",
"diskvdasize" : "%s",
"uptime" : "%s",
"boot_time" : "%s",
"siteID" : "%s",
"onvif" : "%s",
"onvif_zoom" : "%s",
"onvif_pantilt" : "%s",
"onvif_presets": "%s",
"onvif_presets_list": %s,
"cameraConnected": "%s",
"numberoffiles" : "33",
"timestamp" : 1564747908,
"cameratype" : "IPCamera",
"docker" : true,
"kios" : false,
"raspberrypi" : false
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)
req, _ := http.NewRequest("POST", url, buffy)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client = &http.Client{} var client *http.Client
resp, err = client.Do(req) 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 { if resp != nil {
resp.Body.Close() resp.Body.Close()
} }
if err == nil && resp.StatusCode == 200 { if err == nil && resp.StatusCode == 200 {
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.") communication.CloudTimestamp.Store(time.Now().Unix())
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.")
} else { } else {
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.") 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.")
} }
}
// This will check if we need to stop the thread, // If we have a Kerberos Vault connected, we will also send some analytics
// because of a reconfiguration. // to that service.
select { vaultURI = config.KStorage.URI
case <-communication.HandleHeartBeat: if vaultURI != "" {
break loop buffy = bytes.NewBuffer(jsonStr)
case <-time.After(15 * time.Second): req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
req.Header.Set("Content-Type", "application/json")
resp, err = client.Do(req)
if resp != nil {
resp.Body.Close()
}
if err == nil && resp.StatusCode == 200 {
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.")
} else {
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.")
}
}
} else {
log.Log.Error("HandleHeartBeat: Disabled as we do not have a public key defined.")
} }
} }
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleHeartBeat:
break loop
case <-time.After(15 * time.Second):
}
} }
log.Log.Debug("HandleHeartBeat: finished") log.Log.Debug("HandleHeartBeat: finished")
@@ -410,8 +526,9 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
if config.Capture.Liveview != "false" { if config.Capture.Liveview != "false" {
// Should create a track here. // Should create a track here.
track := webrtc.NewVideoTrack() videoTrack := webrtc.NewVideoTrack(codecs)
go webrtc.WriteToTrack(livestreamCursor, configuration, communication, mqttClient, track, codecs, decoder, decoderMutex) audioTrack := webrtc.NewAudioTrack(codecs)
go webrtc.WriteToTrack(livestreamCursor, configuration, communication, mqttClient, videoTrack, audioTrack, codecs, decoder, decoderMutex)
if config.Capture.ForwardWebRTC == "true" { if config.Capture.ForwardWebRTC == "true" {
// We get a request with an offer, but we'll forward it. // We get a request with an offer, but we'll forward it.
@@ -434,7 +551,7 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
webrtc.CandidateArrays[key] = make(chan string, 30) webrtc.CandidateArrays[key] = make(chan string, 30)
} }
webrtc.CandidatesMutex.Unlock() webrtc.CandidatesMutex.Unlock()
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, track, handshake, webrtc.CandidateArrays[key]) webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
} }
} }
@@ -463,15 +580,23 @@ func VerifyHub(c *gin.Context) {
err := c.BindJSON(&config) err := c.BindJSON(&config)
if err == nil { if err == nil {
hubKey := config.HubKey
hubURI := config.HubURI hubURI := config.HubURI
publicKey := config.HubKey
privateKey := config.HubPrivateKey
content := []byte(`{"message": "fake-message"}`) req, err := http.NewRequest("POST", hubURI+"/subscription/verify", nil)
body := bytes.NewReader(content)
req, err := http.NewRequest("POST", hubURI+"/queue/test", body)
if err == nil { if err == nil {
req.Header.Set("X-Kerberos-Cloud-Key", hubKey) req.Header.Set("X-Kerberos-Hub-PublicKey", publicKey)
client := &http.Client{} 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) resp, err := client.Do(req)
if err == nil { if err == nil {
@@ -519,96 +644,96 @@ func VerifyHub(c *gin.Context) {
// @Summary Will verify the persistence. // @Summary Will verify the persistence.
// @Description Will verify the persistence. // @Description Will verify the persistence.
// @Success 200 {object} models.APIResponse // @Success 200 {object} models.APIResponse
func VerifyPersistence(c *gin.Context) { func VerifyPersistence(c *gin.Context, configDirectory string) {
var config models.Config var config models.Config
err := c.BindJSON(&config) err := c.BindJSON(&config)
if err != nil || config.Cloud != "" { if err != nil || config.Cloud != "" {
if config.Cloud == "s3" { if config.Cloud == "dropbox" {
VerifyDropbox(config, c)
} else if config.Cloud == "s3" || config.Cloud == "kerberoshub" {
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token if config.HubURI == "" ||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4 config.HubKey == "" ||
// - Timestamp config.HubPrivateKey == "" ||
// - Size + - + microseconds config.S3.Region == "" {
// - device msg := "VerifyPersistence: Kerberos Hub not properly configured."
// - Region log.Log.Info(msg)
// - 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 {
c.JSON(400, models.APIResponse{ c.JSON(400, models.APIResponse{
Data: "Creation of Kerberos Hub connection failed: " + err.Error(), Data: msg,
}) })
} else { } else {
// Check if we need to use the proxy. // Open test-480p.mp4
if config.S3.ProxyURI != "" { file, err := os.Open(configDirectory + "/data/test-480p.mp4")
var transport http.RoundTripper = &http.Transport{ if err != nil {
Proxy: func(*http.Request) (*url.URL, error) { msg := "VerifyPersistence: error reading test-480p.mp4: " + err.Error()
return url.Parse(config.S3.ProxyURI) log.Log.Error(msg)
}, c.JSON(400, models.APIResponse{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, Data: msg,
} })
s3Client.SetCustomTransport(transport) }
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() timestamp := time.Now().Unix()
fileName := strconv.FormatInt(timestamp, 10) + "_6-967003_justatest_200-200-400-400_24_769.mp4" fileName := strconv.FormatInt(timestamp, 10) +
content := []byte("test-file") "_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
body := bytes.NewReader(content) 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, var client *http.Client
config.S3.Username+"/"+fileName, if os.Getenv("AGENT_TLS_INSECURE") == "true" {
body, tr := &http.Transport{
body.Size(), TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
minio.PutObjectOptions{ }
ContentType: "video/mp4", client = &http.Client{Transport: tr}
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(),
})
} else { } else {
c.JSON(200, models.APIResponse{ client = &http.Client{}
Data: "Upload Finished: file has been uploaded to bucket: " + strconv.FormatInt(n, 10), }
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,
}) })
} }
} }
}
if config.Cloud == "kstorage" { } else if config.Cloud == "kstorage" || config.Cloud == "kerberosvault" {
uri := config.KStorage.URI uri := config.KStorage.URI
accessKey := config.KStorage.AccessKey accessKey := config.KStorage.AccessKey
@@ -617,10 +742,18 @@ func VerifyPersistence(c *gin.Context) {
provider := config.KStorage.Provider provider := config.KStorage.Provider
if err == nil && uri != "" && accessKey != "" && secretAccessKey != "" { 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-AccessKey", accessKey)
req.Header.Add("X-Kerberos-Storage-SecretAccessKey", secretAccessKey) req.Header.Add("X-Kerberos-Storage-SecretAccessKey", secretAccessKey)
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -632,33 +765,44 @@ func VerifyPersistence(c *gin.Context) {
if provider != "" || directory != "" { if provider != "" || directory != "" {
hubKey := config.KStorage.CloudKey
// This is the new way ;)
if config.HubKey != "" {
hubKey = config.HubKey
}
// Generate a random name. // Generate a random name.
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
fileName := strconv.FormatInt(timestamp, 10) + fileName := strconv.FormatInt(timestamp, 10) +
"_6-967003_justatest_200-200-400-400_24_769.mp4" "_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
content := []byte("test-file")
body := bytes.NewReader(content)
//fileSize := int64(len(content))
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 { if err == nil {
req.Header.Set("Content-Type", "video/mp4") 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-AccessKey", accessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", secretAccessKey) req.Header.Set("X-Kerberos-Storage-SecretAccessKey", secretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", provider) req.Header.Set("X-Kerberos-Storage-Provider", provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName) 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-Capture", "IPCamera")
req.Header.Set("X-Kerberos-Storage-Directory", directory) 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) resp, err := client.Do(req)
@@ -671,41 +815,45 @@ func VerifyPersistence(c *gin.Context) {
c.JSON(200, body) c.JSON(200, body)
} else { } else {
c.JSON(400, models.APIResponse{ 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 { } else {
c.JSON(400, models.APIResponse{ c.JSON(400, models.APIResponse{
Data: "Upload of fake recording failed: " + err.Error(), Data: "VerifyPersistence: Upload of fake recording failed: " + err.Error(),
}) })
} }
} else { } else {
c.JSON(400, models.APIResponse{ 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 { } else {
c.JSON(400, models.APIResponse{ 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 { } else {
c.JSON(400, models.APIResponse{ 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 { } else {
c.JSON(400, models.APIResponse{ 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 { } else {
c.JSON(400, models.APIResponse{ 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(),
}) })
} }
} }
+135
View File
@@ -0,0 +1,135 @@
// Package cloud contains the Dropbox implementation of the Cloud interface.
// It uses the Dropbox SDK to upload files to Dropbox.
package cloud
import (
"bytes"
"errors"
"os"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/users"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
// UploadDropbox uploads the file to your Dropbox account using the access token and directory.
func UploadDropbox(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config
token := config.Dropbox.AccessToken
directory := config.Dropbox.Directory
if directory != "" {
// Check if trailing slash if not we'll add one.
if directory[len(directory)-1:] != "/" {
directory = directory + "/"
}
}
if token == "" {
err := "UploadDropbox: Dropbox not properly configured"
log.Log.Info(err)
return false, true, errors.New(err)
}
// Upload to Dropbox
log.Log.Info("UploadDropbox: Uploading to Dropbox")
log.Log.Info("UploadDropbox: Upload started for " + fileName)
fullname := "data/recordings/" + fileName
dConfig := dropbox.Config{
Token: token,
LogLevel: dropbox.LogInfo, // if needed, set the desired logging level. Default is off
}
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
if file != nil {
defer file.Close()
}
if err == nil {
// Upload the file
dbf := files.New(dConfig)
res, err := dbf.Upload(&files.UploadArg{
CommitInfo: files.CommitInfo{
Path: "/" + directory + fileName,
Mode: &files.WriteMode{
Tagged: dropbox.Tagged{
Tag: "overwrite",
},
},
},
}, file)
if err != nil {
log.Log.Error("UploadDropbox: Error uploading file: " + err.Error())
return false, false, err
}
log.Log.Info("UploadDropbox: File uploaded successfully, " + res.Name)
return true, true, nil
}
log.Log.Error("UploadDropbox: Error opening file: " + err.Error())
return false, true, err
}
// VerifyDropbox verifies if the Dropbox token is valid and it is able to upload a file.
func VerifyDropbox(config models.Config, c *gin.Context) {
token := config.Dropbox.AccessToken
directory := config.Dropbox.Directory
if directory != "" {
// Check if trailing slash if not we'll add one.
if directory[len(directory)-1:] != "/" {
directory = directory + "/"
}
}
if token != "" {
dConfig := dropbox.Config{
Token: token,
LogLevel: dropbox.LogInfo, // if needed, set the desired logging level. Default is off
}
dbx := users.New(dConfig)
_, err := dbx.GetCurrentAccount()
if err != nil {
c.JSON(400, models.APIResponse{
Data: "Something went wrong while reaching the Dropbox API: " + err.Error(),
})
} else {
// Upload the file
content := TestFile
file := bytes.NewReader(content)
dbf := files.New(dConfig)
_, err := dbf.Upload(&files.UploadArg{
CommitInfo: files.CommitInfo{
Path: "/" + directory + "kerbers-agent-test.mp4",
Mode: &files.WriteMode{
Tagged: dropbox.Tagged{
Tag: "overwrite",
},
},
},
}, file)
if err != nil {
c.JSON(400, models.APIResponse{
Data: "Something went wrong while reaching the Dropbox API: " + err.Error(),
})
} else {
c.JSON(200, models.APIResponse{
Data: "Dropbox is working fine.",
})
}
}
} else {
c.JSON(400, models.APIResponse{
Data: "Dropbox token is not set.",
})
}
}
+131
View 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)
}
+32 -23
View File
@@ -1,6 +1,8 @@
package cloud package cloud
import ( import (
"crypto/tls"
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
@@ -9,19 +11,19 @@ import (
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
) )
func UploadKerberosVault(configuration *models.Configuration, fileName string, directory string) bool { func UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config config := configuration.Config
if config.KStorage.AccessKey == "" || if config.KStorage.AccessKey == "" ||
config.KStorage.SecretAccessKey == "" || config.KStorage.SecretAccessKey == "" ||
config.KStorage.Provider == "" ||
config.KStorage.Directory == "" || config.KStorage.Directory == "" ||
config.KStorage.URI == "" { config.KStorage.URI == "" {
log.Log.Info("UploadKerberosVault: Kerberos Vault not properly configured.") err := "UploadKerberosVault: Kerberos Vault not properly configured."
log.Log.Info(err)
return false, false, errors.New(err)
} }
//fmt.Println("Uploading...")
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token // timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4 // 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
// - Timestamp // - Timestamp
@@ -30,21 +32,20 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
// - Region // - Region
// - Number of changes // - Number of changes
// - Token // - Token
// KerberosCloud, this means storage is disabled and proxy enabled. // KerberosCloud, this means storage is disabled and proxy enabled.
log.Log.Info("UploadKerberosVault: Uploading to Kerberos Vault (" + config.KStorage.URI + ")") log.Log.Info("UploadKerberosVault: Uploading to Kerberos Vault (" + config.KStorage.URI + ")")
log.Log.Info("UploadKerberosVault: Upload started for " + fileName) log.Log.Info("UploadKerberosVault: Upload started for " + fileName)
fullname := "data/recordings/" + fileName fullname := "data/recordings/" + fileName
file, err := os.OpenFile(fullname, os.O_RDWR, 0755) file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
if err != nil { if file != nil {
log.Log.Info("UploadKerberosVault: Upload Failed, file doesn't exists anymore.") defer file.Close()
os.Remove(directory + "/" + fileName) }
return false if err != nil {
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, false, errors.New(err)
} }
defer file.Close()
publicKey := config.KStorage.CloudKey publicKey := config.KStorage.CloudKey
// This is the new way ;) // This is the new way ;)
@@ -54,7 +55,9 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file) req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
if err != nil { if err != nil {
log.Log.Error("Error reading request. " + err.Error()) errorMessage := "UploadKerberosVault: error reading request, " + config.KStorage.URI + "/storage: " + err.Error()
log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
} }
req.Header.Set("Content-Type", "video/mp4") req.Header.Set("Content-Type", "video/mp4")
req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey) req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey)
@@ -65,11 +68,18 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
req.Header.Set("X-Kerberos-Storage-Device", config.Key) req.Header.Set("X-Kerberos-Storage-Device", config.Key)
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera") req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
req.Header.Set("X-Kerberos-Storage-Directory", config.KStorage.Directory) req.Header.Set("X-Kerberos-Storage-Directory", config.KStorage.Directory)
//client := &http.Client{Timeout: time.Second * 30}
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) resp, err := client.Do(req)
if resp != nil { if resp != nil {
defer resp.Body.Close() defer resp.Body.Close()
} }
@@ -80,17 +90,16 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
if err == nil { if err == nil {
if resp.StatusCode == 200 { if resp.StatusCode == 200 {
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body)) log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
// We will remove the file from disk as well return true, true, nil
os.Remove(fullname)
os.Remove(directory + "/" + fileName)
} else { } else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body)) log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
return false, true, nil
} }
resp.Body.Close()
} }
} }
} else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + err.Error())
} }
return true
errorMessage := "UploadKerberosVault: Upload Failed, " + err.Error()
log.Log.Info(errorMessage)
return false, true, errors.New(errorMessage)
} }
+33 -19
View File
@@ -2,6 +2,7 @@ package cloud
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -13,11 +14,10 @@ import (
"github.com/minio/minio-go/v6" "github.com/minio/minio-go/v6"
) )
func UploadS3(configuration *models.Configuration, fileName string, directory string) bool { func UploadS3(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config config := configuration.Config
//fmt.Println("Uploading...")
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token // timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4 // 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
// - Timestamp // - Timestamp
@@ -28,10 +28,12 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
// - Token // - Token
if config.S3 == nil { if config.S3 == nil {
log.Log.Error("UploadS3: Uploading Failed, as no settings found") errorMessage := "UploadS3: Uploading Failed, as no settings found"
return false log.Log.Error(errorMessage)
return false, false, errors.New(errorMessage)
} }
// Legacy support, should get rid of it!
aws_access_key_id := config.S3.Publickey aws_access_key_id := config.S3.Publickey
aws_secret_access_key := config.S3.Secretkey aws_secret_access_key := config.S3.Secretkey
aws_region := config.S3.Region aws_region := config.S3.Region
@@ -44,9 +46,18 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
aws_secret_access_key = config.HubPrivateKey aws_secret_access_key = config.HubPrivateKey
} }
// Check if we have some credentials otherwise we abort the request.
if aws_access_key_id == "" || aws_secret_access_key == "" {
errorMessage := "UploadS3: Uploading Failed, as no credentials found"
log.Log.Error(errorMessage)
return false, false, errors.New(errorMessage)
}
s3Client, err := minio.NewWithRegion("s3.amazonaws.com", aws_access_key_id, aws_secret_access_key, true, aws_region) s3Client, err := minio.NewWithRegion("s3.amazonaws.com", aws_access_key_id, aws_secret_access_key, true, aws_region)
if err != nil { if err != nil {
log.Log.Error(err.Error()) errorMessage := "UploadS3: " + err.Error()
log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
} }
// Check if we need to use the proxy. // Check if we need to use the proxy.
@@ -62,9 +73,9 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
fileParts := strings.Split(fileName, "_") fileParts := strings.Split(fileName, "_")
if len(fileParts) == 1 { if len(fileParts) == 1 {
log.Log.Error("ERROR: " + fileName + " is not a valid name.") errorMessage := "UploadS3: " + fileName + " is not a valid name."
os.Remove(directory + "/" + fileName) log.Log.Error(errorMessage)
return false return false, true, errors.New(errorMessage)
} }
deviceKey := config.Key deviceKey := config.Key
@@ -78,18 +89,21 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
fullname := "data/recordings/" + fileName fullname := "data/recordings/" + fileName
file, err := os.OpenFile(fullname, os.O_RDWR, 0755) file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
defer file.Close() if file != nil {
defer file.Close()
}
if err != nil { if err != nil {
log.Log.Error("UploadS3: " + err.Error()) errorMessage := "UploadS3: " + err.Error()
os.Remove(directory + "/" + fileName) log.Log.Error(errorMessage)
return false return false, true, errors.New(errorMessage)
} }
fileInfo, err := file.Stat() fileInfo, err := file.Stat()
if err != nil { if err != nil {
log.Log.Error("UploadS3: " + err.Error()) errorMessage := "UploadS3: " + err.Error()
os.Remove(directory + "/" + fileName) log.Log.Error(errorMessage)
return false return false, true, errors.New(errorMessage)
} }
n, err := s3Client.PutObject(config.S3.Bucket, n, err := s3Client.PutObject(config.S3.Bucket,
@@ -113,11 +127,11 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
}) })
if err != nil { if err != nil {
log.Log.Error("UploadS3: Uploading Failed, " + err.Error()) errorMessage := "UploadS3: Uploading Failed, " + err.Error()
return false log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
} else { } else {
log.Log.Info("UploadS3: Upload Finished, file has been uploaded to bucket: " + strconv.FormatInt(n, 10)) log.Log.Info("UploadS3: Upload Finished, file has been uploaded to bucket: " + strconv.FormatInt(n, 10))
os.Remove(directory + "/" + fileName) return true, true, nil
return true
} }
} }
File diff suppressed because one or more lines are too long
+120 -63
View File
@@ -1,17 +1,20 @@
package components package components
import ( import (
"context"
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/kerberos-io/joy4/cgo/ffmpeg" "github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/agent/machinery/src/capture" "github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud" "github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/computervision" "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/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif" "github.com/kerberos-io/agent/machinery/src/onvif"
@@ -21,7 +24,7 @@ import (
"github.com/tevino/abool" "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") log.Log.Debug("Bootstrap: started")
// We will keep track of the Kerberos Agent up time // We will keep track of the Kerberos Agent up time
@@ -53,6 +56,7 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
communication.HandleLiveSD = make(chan int64, 1) communication.HandleLiveSD = make(chan int64, 1)
communication.HandleLiveHDKeepalive = make(chan string, 1) communication.HandleLiveHDKeepalive = make(chan string, 1)
communication.HandleLiveHDPeers = make(chan string, 1) communication.HandleLiveHDPeers = make(chan string, 1)
communication.HandleONVIF = make(chan models.OnvifAction, 1)
communication.IsConfiguring = abool.New() communication.IsConfiguring = abool.New()
// Before starting the agent, we have a control goroutine, that might // Before starting the agent, we have a control goroutine, that might
@@ -64,31 +68,76 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
subDecoder := &ffmpeg.VideoDecoder{} subDecoder := &ffmpeg.VideoDecoder{}
cameraSettings := &models.Camera{} cameraSettings := &models.Camera{}
// Handle heartbeats
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
// 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(configDirectory, configuration, communication)
// Run the agent and fire up all the other // Run the agent and fire up all the other
// goroutines which do image capture, motion detection, onvif, etc. // goroutines which do image capture, motion detection, onvif, etc.
for { for {
// This will blocking until receiving a signal to be restarted, reconfigured, stopped, etc. // This will blocking until receiving a signal to be restarted, reconfigured, stopped, etc.
status := RunAgent(configuration, communication, uptimeStart, cameraSettings, decoder, subDecoder) status := RunAgent(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
if status == "stop" { if status == "stop" {
break break
} }
// We will re open the configuration, might have changed :O!
OpenConfig(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(configDirectory, configuration, communication)
}
// We will create a new cancelable context, which will be used to cancel and restart.
// This is used to restart the agent when the configuration is updated.
ctx, cancel := context.WithCancel(context.Background())
communication.Context = &ctx
communication.CancelContext = &cancel
} }
log.Log.Debug("Bootstrap: finished") log.Log.Debug("Bootstrap: finished")
} }
func RunAgent(configuration *models.Configuration, communication *models.Communication, 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: started")
log.Log.Debug("RunAgent: bootstrapping agent")
config := configuration.Config config := configuration.Config
status := "not started"
// Currently only support H264 encoded cameras, this will change. // Currently only support H264 encoded cameras, this will change.
// Establishing the camera connection // Establishing the camera connection
log.Log.Info("RunAgent: opening RTSP stream")
rtspUrl := config.Capture.IPCamera.RTSP rtspUrl := config.Capture.IPCamera.RTSP
infile, streams, err := capture.OpenRTSP(rtspUrl) infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl)
// We will initialise the camera settings object
// so we can check if the camera settings have changed, and we need
// to reload the decoders.
videoStream, _ := capture.GetVideoStream(streams)
if videoStream == nil {
log.Log.Error("RunAgent: no video stream found, might be the wrong codec (we only support H264 for the moment)")
time.Sleep(time.Second * 3)
return status
}
num, denum := videoStream.(av.VideoCodecData).Framerate()
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 queue *pubsub.Queue
var subQueue *pubsub.Queue var subQueue *pubsub.Queue
@@ -96,11 +145,9 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
var decoderMutex sync.Mutex var decoderMutex sync.Mutex
var subDecoderMutex sync.Mutex var subDecoderMutex sync.Mutex
status := "not started"
if err == nil { if err == nil {
log.Log.Info("RunAgent: opened RTSP stream") log.Log.Info("RunAgent: opened RTSP stream: " + rtspUrl)
// We might have a secondary rtsp url, so we might need to use that. // We might have a secondary rtsp url, so we might need to use that.
var subInfile av.DemuxCloser var subInfile av.DemuxCloser
@@ -108,24 +155,30 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
subStreamEnabled := false subStreamEnabled := false
subRtspUrl := config.Capture.IPCamera.SubRTSP subRtspUrl := config.Capture.IPCamera.SubRTSP
if subRtspUrl != "" && subRtspUrl != rtspUrl { if subRtspUrl != "" && subRtspUrl != rtspUrl {
subInfile, subStreams, err = capture.OpenRTSP(subRtspUrl) subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl)
if err == nil { if err == nil {
log.Log.Info("RunAgent: opened RTSP sub stream") log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl)
subStreamEnabled = true subStreamEnabled = true
} }
}
// We will initialise the camera settings object videoStream, _ := capture.GetVideoStream(subStreams)
// so we can check if the camera settings have changed, and we need if videoStream == nil {
// to reload the decoders. log.Log.Error("RunAgent: no video substream found, might be the wrong codec (we only support H264 for the moment)")
videoStream, _ := capture.GetVideoStream(streams) time.Sleep(time.Second * 3)
num, denum := videoStream.(av.VideoCodecData).Framerate() return status
width := videoStream.(av.VideoCodecData).Width() }
height := videoStream.(av.VideoCodecData).Height()
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.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() decoder.Close()
if subStreamEnabled { if subStreamEnabled {
subDecoder.Close() subDecoder.Close()
@@ -149,6 +202,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
cameraSettings.Denum = denum cameraSettings.Denum = denum
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type() cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
cameraSettings.Initialized = true cameraSettings.Initialized = true
} else { } else {
log.Log.Info("RunAgent: camera settings did not change, keeping decoder") log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
} }
@@ -182,13 +236,6 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
subQueue.WriteHeader(subStreams) subQueue.WriteHeader(subStreams)
} }
// Configure a MQTT client which helps for a bi-directional communication
communication.HandleONVIF = make(chan models.OnvifAction, 1)
mqttClient := routers.ConfigureMQTT(configuration, communication)
// Handle heartbeats
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
// Handle the camera stream // Handle the camera stream
go capture.HandleStream(infile, queue, communication) go capture.HandleStream(infile, queue, communication)
@@ -227,29 +274,45 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
} }
// Handle recording, will write an mp4 to disk. // 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) // 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 // Handle ONVIF actions
go onvif.HandleONVIFActions(configuration, communication) go onvif.HandleONVIFActions(configuration, communication)
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This will go into a blocking state, once this channel is triggered // This will go into a blocking state, once this channel is triggered
// the agent will cleanup and restart. // the agent will cleanup and restart.
status = <-communication.HandleBootstrap status = <-communication.HandleBootstrap
// If we reach this point, we are stopping the stream.
communication.CameraConnected = false
// 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! // Here we are cleaning up everything!
if configuration.Config.Offline != "true" { if configuration.Config.Offline != "true" {
communication.HandleHeartBeat <- "stop"
communication.HandleUpload <- "stop" communication.HandleUpload <- "stop"
} }
communication.HandleStream <- "stop" communication.HandleStream <- "stop"
if subStreamEnabled { if subStreamEnabled {
communication.HandleSubStream <- "stop" communication.HandleSubStream <- "stop"
} }
time.Sleep(time.Second * 1)
time.Sleep(time.Second * 3)
infile.Close() infile.Close()
infile = nil infile = nil
@@ -263,22 +326,13 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
subQueue = nil subQueue = nil
communication.SubQueue = nil communication.SubQueue = nil
} }
close(communication.HandleONVIF)
communication.HandleONVIF = nil
close(communication.HandleLiveHDHandshake)
communication.HandleLiveHDHandshake = nil
close(communication.HandleMotion) close(communication.HandleMotion)
communication.HandleMotion = nil communication.HandleMotion = nil
// Disconnect MQTT
routers.DisconnectMQTT(mqttClient, &configuration.Config)
// Wait a few seconds to stop the decoder.
time.Sleep(time.Second * 3)
// Waiting for some seconds to make sure everything is properly closed. // 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.") log.Log.Info("RunAgent: waiting 3 seconds to make sure everything is properly closed.")
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
} else { } else {
log.Log.Error("Something went wrong while opening RTSP: " + err.Error()) log.Log.Error("Something went wrong while opening RTSP: " + err.Error())
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
@@ -300,29 +354,32 @@ func ControlAgent(communication *models.Communication) {
var previousPacket int64 = 0 var previousPacket int64 = 0
var occurence = 0 var occurence = 0
for { for {
packetsR := packageCounter.Load().(int64)
if packetsR == previousPacket { // If camera is connected, we'll check if we are still receiving packets.
// If we are already reconfiguring, if communication.CameraConnected {
// we dont need to check if the stream is blocking. packetsR := packageCounter.Load().(int64)
if !communication.IsConfiguring.IsSet() { if packetsR == previousPacket {
occurence = occurence + 1 // If we are already reconfiguring,
// we dont need to check if the stream is blocking.
if !communication.IsConfiguring.IsSet() {
occurence = occurence + 1
}
} else {
occurence = 0
} }
} else {
occurence = 0 log.Log.Info("ControlAgent: Number of packets read " + strconv.FormatInt(packetsR, 10))
// After 15 seconds without activity this is thrown..
if occurence == 3 {
log.Log.Info("Main: Restarting machinery.")
communication.HandleBootstrap <- "restart"
time.Sleep(2 * time.Second)
occurence = 0
}
previousPacket = packageCounter.Load().(int64)
} }
log.Log.Info("ControlAgent: Number of packets read " + strconv.FormatInt(packetsR, 10))
// After 15 seconds without activity this is thrown..
if occurence == 3 {
log.Log.Info("Main: Restarting machinery.")
communication.HandleBootstrap <- "restart"
time.Sleep(2 * time.Second)
occurence = 0
}
previousPacket = packageCounter.Load().(int64)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} }
}() }()
-1
View File
@@ -66,7 +66,6 @@ func (s Stream) GetCodecs() []av.CodecData {
func (s Stream) ReadPackets(packetChannel chan av.Packet) { func (s Stream) ReadPackets(packetChannel chan av.Packet) {
session := s.Open() session := s.Open()
fmt.Println("Start reading H264 packages from stream")
for { for {
packet, err := session.ReadPacket() packet, err := session.ReadPacket()
if err != nil { if err != nil {
+48 -35
View File
@@ -10,15 +10,12 @@ import (
"time" "time"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
geo "github.com/kellydunn/golang-geo"
"github.com/kerberos-io/agent/machinery/src/capture" "github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/av/pubsub"
//"github.com/whorfin/go-libjpeg/jpeg"
geo "github.com/kellydunn/golang-geo"
"github.com/kerberos-io/joy4/av" "github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/cgo/ffmpeg" "github.com/kerberos-io/joy4/cgo/ffmpeg"
) )
@@ -44,7 +41,8 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
log.Log.Info("ProcessMotion: Motion detection enabled.") log.Log.Info("ProcessMotion: Motion detection enabled.")
key := config.HubKey hubKey := config.HubKey
deviceKey := config.Key
// Allocate a VideoFrame // Allocate a VideoFrame
frame := ffmpeg.AllocVideoFrame() frame := ffmpeg.AllocVideoFrame()
@@ -77,18 +75,21 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
// Calculate mask // Calculate mask
var polyObjects []geo.Polygon var polyObjects []geo.Polygon
for _, polygon := range config.Region.Polygon {
coords := polygon.Coordinates if config.Region != nil {
poly := geo.Polygon{} for _, polygon := range config.Region.Polygon {
for _, c := range coords { coords := polygon.Coordinates
x := c.X poly := geo.Polygon{}
y := c.Y for _, c := range coords {
p := geo.NewPoint(x, y) x := c.X
if !poly.Contains(p) { y := c.Y
poly.Add(p) p := geo.NewPoint(x, y)
if !poly.Contains(p) {
poly.Add(p)
}
} }
polyObjects = append(polyObjects, poly)
} }
polyObjects = append(polyObjects, poly)
} }
bounds := img.Bounds() bounds := img.Bounds()
@@ -132,24 +133,29 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
// Check if within time interval // Check if within time interval
detectMotion := true detectMotion := true
now := time.Now().In(loc) timeEnabled := config.Time
weekday := now.Weekday() if timeEnabled != "false" {
hour := now.Hour() now := time.Now().In(loc)
minute := now.Minute() weekday := now.Weekday()
second := now.Second() hour := now.Hour()
timeInterval := config.Timetable[int(weekday)] minute := now.Minute()
if timeInterval != nil { second := now.Second()
start1 := timeInterval.Start1 if config.Timetable != nil && len(config.Timetable) > 0 {
end1 := timeInterval.End1 timeInterval := config.Timetable[int(weekday)]
start2 := timeInterval.Start2 if timeInterval != nil {
end2 := timeInterval.End2 start1 := timeInterval.Start1
currentTimeInSeconds := hour*60*60 + minute*60 + second end1 := timeInterval.End1
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) || start2 := timeInterval.Start2
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) { end2 := timeInterval.End2
currentTimeInSeconds := hour*60*60 + minute*60 + second
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
} else { } else {
detectMotion = false detectMotion = false
log.Log.Debug("ProcessMotion: Time interval not valid, disabling motion detection.") log.Log.Info("ProcessMotion: Time interval not valid, disabling motion detection.")
}
}
} }
} }
@@ -159,8 +165,15 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold) isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
if detectMotion && isPixelChangeThresholdReached { if detectMotion && isPixelChangeThresholdReached {
if mqttClient != nil { // If offline mode is disabled, send a message to the hub
mqttClient.Publish("kerberos/"+key+"/device/"+config.Key+"/motion", 2, false, "motion") if config.Offline != "true" {
if mqttClient != nil {
if hubKey != "" {
mqttClient.Publish("kerberos/"+hubKey+"/device/"+deviceKey+"/motion", 2, false, "motion")
} else {
mqttClient.Publish("kerberos/device/"+deviceKey+"/motion", 2, false, "motion")
}
}
} }
if config.Capture.Recording != "false" { if config.Capture.Recording != "false" {
@@ -1,11 +1,10 @@
package components package config
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"image" "image"
_ "image/png"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
@@ -18,17 +17,17 @@ import (
"github.com/kerberos-io/agent/machinery/src/database" "github.com/kerberos-io/agent/machinery/src/database"
"github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"gopkg.in/mgo.v2/bson" "go.mongodb.org/mongo-driver/bson"
) )
func GetImageFromFilePath() (image.Image, error) { func GetImageFromFilePath(configDirectory string) (image.Image, error) {
snapshotDirectory := "./data/snapshots" snapshotDirectory := configDirectory + "/data/snapshots"
files, err := ioutil.ReadDir(snapshotDirectory) files, err := ioutil.ReadDir(snapshotDirectory)
if err == nil && len(files) > 1 { if err == nil && len(files) > 1 {
sort.Slice(files, func(i, j int) bool { sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().Before(files[j].ModTime()) 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) f, err := os.Open(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -43,19 +42,18 @@ func GetImageFromFilePath() (image.Image, error) {
// ReadUserConfig Reads the user configuration of the Kerberos Open Source instance. // ReadUserConfig Reads the user configuration of the Kerberos Open Source instance.
// This will return a models.User struct including the username, password, // This will return a models.User struct including the username, password,
// selected language, and if the installation was completed or not. // selected language, and if the installation was completed or not.
func ReadUserConfig() (userConfig models.User) { func ReadUserConfig(configDirectory string) (userConfig models.User) {
for { for {
jsonFile, err := os.Open("./data/config/user.json") jsonFile, err := os.Open(configDirectory + "/data/config/user.json")
if err != nil { if err != nil {
fmt.Println(err) log.Log.Error("Config file is not found " + configDirectory + "/data/config/user.json, trying again in 5s: " + err.Error())
fmt.Println("Config file is not found " + "./data/config/user.json" + ", trying again in 5s.")
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} else { } else {
fmt.Println("Successfully Opened user.json") log.Log.Info("Successfully Opened user.json")
byteValue, _ := ioutil.ReadAll(jsonFile) byteValue, _ := ioutil.ReadAll(jsonFile)
err = json.Unmarshal(byteValue, &userConfig) err = json.Unmarshal(byteValue, &userConfig)
if err != nil { if err != nil {
fmt.Println("JSON file not valid: " + err.Error()) log.Log.Error("JSON file not valid: " + err.Error())
} else { } else {
jsonFile.Close() jsonFile.Close()
break break
@@ -68,7 +66,7 @@ func ReadUserConfig() (userConfig models.User) {
return 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 // We are checking which deployment this is running, so we can load
// into the configuration as expected. // into the configuration as expected.
@@ -79,19 +77,52 @@ func OpenConfig(configuration *models.Configuration) {
// Multiple agents have there configuration stored, and can benefit from // Multiple agents have there configuration stored, and can benefit from
// the concept of a global concept. // the concept of a global concept.
session := database.New().Copy() // Write to mongodb
defer session.Close() client := database.New()
db := session.DB(database.DatabaseName)
collection := db.C("configuration")
collection.Find(bson.M{ db := client.Database(database.DatabaseName)
collection := db.Collection("configuration")
var globalConfig models.Config
res := collection.FindOne(context.Background(), bson.M{
"type": "global", "type": "global",
}).One(&configuration.GlobalConfig) })
collection.Find(bson.M{ 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")
res = collection.FindOne(context.Background(), bson.M{
"type": "config", "type": "config",
"name": os.Getenv("DEPLOYMENT_NAME"), "name": deploymentName,
}).One(&configuration.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. // We will merge both configs in a single config file.
// Read again from database but this store overwrite the same object. // Read again from database but this store overwrite the same object.
@@ -136,9 +167,9 @@ func OpenConfig(configuration *models.Configuration) {
// Open device config // Open device config
for { for {
jsonFile, err := os.Open("./data/config/config.json") jsonFile, err := os.Open(configDirectory + "/data/config/config.json")
if err != nil { 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) time.Sleep(5 * time.Second)
} else { } else {
log.Log.Info("Successfully Opened config.json from " + configuration.Name) log.Log.Info("Successfully Opened config.json from " + configuration.Name)
@@ -146,11 +177,11 @@ func OpenConfig(configuration *models.Configuration) {
err = json.Unmarshal(byteValue, &configuration.Config) err = json.Unmarshal(byteValue, &configuration.Config)
jsonFile.Close() jsonFile.Close()
if err != nil { if err != nil {
fmt.Println("JSON file not valid: " + err.Error()) log.Log.Error("JSON file not valid: " + err.Error())
} else { } else {
err = json.Unmarshal(byteValue, &configuration.CustomConfig) err = json.Unmarshal(byteValue, &configuration.CustomConfig)
if err != nil { if err != nil {
fmt.Println("JSON file not valid: " + err.Error()) log.Log.Error("JSON file not valid: " + err.Error())
} else { } else {
break break
} }
@@ -179,7 +210,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Key = value configuration.Config.Key = value
break break
case "AGENT_NAME": case "AGENT_NAME":
configuration.Config.Name = value configuration.Config.FriendlyName = value
break break
case "AGENT_TIMEZONE": case "AGENT_TIMEZONE":
configuration.Config.Timezone = value configuration.Config.Timezone = value
@@ -207,8 +238,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
/* ONVIF connnection settings */ /* ONVIF connnection settings */
case "AGENT_CAPTURE_IPCAMERA_ONVIF": case "AGENT_CAPTURE_IPCAMERA_ONVIF":
isEnabled := value == " true" configuration.Config.Capture.IPCamera.ONVIF = value
configuration.Config.Capture.IPCamera.ONVIF = isEnabled
break break
case "AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR": case "AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR":
configuration.Config.Capture.IPCamera.ONVIFXAddr = value configuration.Config.Capture.IPCamera.ONVIFXAddr = value
@@ -270,26 +300,82 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
} }
break break
/* Cloud settings for persisting recordings */ /* Conditions */
case "AGENT_CLOUD":
configuration.Config.Cloud = value case "AGENT_TIME":
configuration.Config.Time = value
break
case "AGENT_TIMETABLE":
var timetable []*models.Timetable
// Convert value to timetable array with (start1, end1, start2, end2)
// Where days are limited by ; and time by ,
// su;mo;tu;we;th;fr;sa
// 0,43199,43200,86400;0,43199,43200,86400
// Split days
daysString := strings.Split(value, ";")
for _, dayString := range daysString {
// Split time
timeString := strings.Split(dayString, ",")
if len(timeString) == 4 {
start1, err := strconv.ParseInt(timeString[0], 10, 64)
if err != nil {
continue
}
end1, err := strconv.ParseInt(timeString[1], 10, 64)
if err != nil {
continue
}
start2, err := strconv.ParseInt(timeString[2], 10, 64)
if err != nil {
continue
}
end2, err := strconv.ParseInt(timeString[3], 10, 64)
if err != nil {
continue
}
timetable = append(timetable, &models.Timetable{
Start1: int(start1),
End1: int(end1),
Start2: int(start2),
End2: int(end2),
})
}
}
configuration.Config.Timetable = timetable
break break
/* When connected and storing in Kerberos Hub (SAAS) */ case "AGENT_REGION_POLYGON":
case "AGENT_HUB_URI": var coordinates []models.Coordinate
configuration.Config.HubURI = value
break // Convert value to coordinates array
case "AGENT_HUB_KEY": // 0,0;1,1;2,2;3,3
configuration.Config.HubKey = value coordinatesString := strings.Split(value, ";")
break for _, coordinateString := range coordinatesString {
case "AGENT_HUB_PRIVATE_KEY": coordinate := strings.Split(coordinateString, ",")
configuration.Config.HubPrivateKey = value if len(coordinate) == 2 {
break x, err := strconv.ParseFloat(coordinate[0], 64)
case "AGENT_HUB_USERNAME": if err != nil {
configuration.Config.S3.Username = value continue
break }
case "AGENT_HUB_SITE": y, err := strconv.ParseFloat(coordinate[1], 64)
configuration.Config.HubSite = value if err != nil {
continue
}
coordinates = append(coordinates, models.Coordinate{
X: x,
Y: y,
})
}
}
configuration.Config.Region.Polygon = []models.Polygon{
{
Coordinates: coordinates,
ID: "0",
},
}
break break
/* MQTT settings for bi-directional communication */ /* MQTT settings for bi-directional communication */
@@ -317,6 +403,32 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.TURNPassword = value configuration.Config.TURNPassword = value
break break
/* Cloud settings for persisting recordings */
case "AGENT_CLOUD":
configuration.Config.Cloud = value
break
case "AGENT_REMOVE_AFTER_UPLOAD":
configuration.Config.RemoveAfterUpload = value
break
/* When connected and storing in Kerberos Hub (SAAS) */
case "AGENT_HUB_URI":
configuration.Config.HubURI = value
break
case "AGENT_HUB_KEY":
configuration.Config.HubKey = value
break
case "AGENT_HUB_PRIVATE_KEY":
configuration.Config.HubPrivateKey = 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 */ /* When storing in a Kerberos Vault */
case "AGENT_KERBEROSVAULT_URI": case "AGENT_KERBEROSVAULT_URI":
configuration.Config.KStorage.URI = value configuration.Config.KStorage.URI = value
@@ -334,24 +446,33 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.KStorage.Directory = value configuration.Config.KStorage.Directory = value
break break
/* When storing in dropbox */
case "AGENT_DROPBOX_ACCESS_TOKEN":
configuration.Config.Dropbox.AccessToken = value
break
case "AGENT_DROPBOX_DIRECTORY":
configuration.Config.Dropbox.Directory = 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() { if !communication.IsConfiguring.IsSet() {
communication.IsConfiguring.Set() communication.IsConfiguring.Set()
err := StoreConfig(config) err := StoreConfig(configDirectory, config)
if err != nil { if err != nil {
communication.IsConfiguring.UnSet() communication.IsConfiguring.UnSet()
return err return err
} }
select { if communication.CameraConnected {
case communication.HandleBootstrap <- "restart": select {
default: case communication.HandleBootstrap <- "restart":
default:
}
} }
communication.IsConfiguring.UnSet() communication.IsConfiguring.UnSet()
@@ -362,25 +483,29 @@ 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 // Save into database
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" { if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
// Write to mongodb // Write to mongodb
session := database.New().Copy() client := database.New()
defer session.Close()
db := session.DB(database.DatabaseName)
collection := db.C("configuration")
err := collection.Update(bson.M{ db := client.Database(database.DatabaseName)
collection := db.Collection("configuration")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err := collection.UpdateOne(ctx, bson.M{
"type": "config", "type": "config",
"name": os.Getenv("DEPLOYMENT_NAME"), "name": os.Getenv("DEPLOYMENT_NAME"),
}, &config) }, bson.M{"$set": config})
return err return err
// Save into file // Save into file
} else if os.Getenv("DEPLOYMENT") == "" || os.Getenv("DEPLOYMENT") == "agent" { } else if os.Getenv("DEPLOYMENT") == "" || os.Getenv("DEPLOYMENT") == "agent" {
res, _ := json.MarshalIndent(config, "", "\t") 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 return err
} }
+25 -15
View File
@@ -1,45 +1,55 @@
package database package database
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
"gopkg.in/mgo.v2" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
) )
type DB struct { type DB struct {
Session *mgo.Session Client *mongo.Client
} }
var _init_ctx sync.Once var _init_ctx sync.Once
var _instance *DB var _instance *DB
var DatabaseName = "KerberosFactory" var DatabaseName = "KerberosFactory"
func New() *mgo.Session { func New() *mongo.Client {
host := os.Getenv("MONGODB_HOST") host := os.Getenv("MONGODB_HOST")
database := os.Getenv("MONGODB_DATABASE_CREDENTIALS") databaseCredentials := os.Getenv("MONGODB_DATABASE_CREDENTIALS")
replicaset := os.Getenv("MONGODB_REPLICASET")
username := os.Getenv("MONGODB_USERNAME") username := os.Getenv("MONGODB_USERNAME")
password := os.Getenv("MONGODB_PASSWORD") password := os.Getenv("MONGODB_PASSWORD")
authentication := "SCRAM-SHA-256"
_init_ctx.Do(func() { _init_ctx.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_instance = new(DB) _instance = new(DB)
mongoDBDialInfo := &mgo.DialInfo{ mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
Addrs: strings.Split(host, ","), if replicaset != "" {
Timeout: 3 * time.Second, mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
Database: database,
Username: username,
Password: password,
} }
session, err := mgo.DialWithInfo(mongoDBDialInfo)
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongodbURI).SetAuth(options.Credential{
AuthMechanism: authentication,
AuthSource: databaseCredentials,
Username: username,
Password: password,
}))
if err != nil { if err != nil {
fmt.Printf("Error en mongo: %+v\n", err) fmt.Printf("Error setting up mongodb connection: %+v\n", err)
os.Exit(1) os.Exit(1)
} }
_instance.Session = session _instance.Client = client
}) })
return _instance.Session return _instance.Client
} }
+4 -4
View File
@@ -21,7 +21,7 @@ var Log = Logging{
var gologging = logging.MustGetLogger("gologger") var gologging = logging.MustGetLogger("gologger")
func ConfigureGoLogging(timezone *time.Location) { func ConfigureGoLogging(configDirectory string, timezone *time.Location) {
// Logging // Logging
var format = logging.MustStringFormatter( var format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, `%{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) stdBackend := logging.NewLogBackend(os.Stderr, "", 0)
stdBackendLeveled := logging.NewBackendFormatter(stdBackend, format) stdBackendLeveled := logging.NewBackendFormatter(stdBackend, format)
fileBackend := logging.NewLogBackend(&lumberjack.Logger{ fileBackend := logging.NewLogBackend(&lumberjack.Logger{
Filename: "./data/log/machinery.txt", Filename: configDirectory + "/data/log/machinery.txt",
MaxSize: 2, // megabytes MaxSize: 2, // megabytes
Compress: true, // disabled by default Compress: true, // disabled by default
}, "", 0) }, "", 0)
@@ -75,10 +75,10 @@ type Logging struct {
Level string Level string
} }
func (self *Logging) Init(timezone *time.Location) { func (self *Logging) Init(configDirectory string, timezone *time.Location) {
switch self.Logger { switch self.Logger {
case "go-logging": case "go-logging":
ConfigureGoLogging(timezone) ConfigureGoLogging(configDirectory, timezone)
case "logrus": case "logrus":
ConfigureLogrus(timezone) ConfigureLogrus(timezone)
default: default:
+10 -2
View File
@@ -1,8 +1,11 @@
package models package models
type APIResponse struct { type APIResponse struct {
Data interface{} `json:"data" bson:"data"` Data interface{} `json:"data" bson:"data"`
Message interface{} `json:"message" bson:"message"` Message interface{} `json:"message" bson:"message"`
PTZFunctions interface{} `json:"ptz_functions" bson:"ptz_functions"`
CanZoom bool `json:"can_zoom" bson:"can_zoom"`
CanPanTilt bool `json:"can_pan_tilt" bson:"can_pan_tilt"`
} }
type OnvifCredentials struct { type OnvifCredentials struct {
@@ -26,3 +29,8 @@ type OnvifZoom struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"` OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Zoom float64 `json:"zoom,omitempty" bson:"zoom"` 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"`
}
+4
View File
@@ -1,6 +1,7 @@
package models package models
import ( import (
"context"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -12,6 +13,8 @@ import (
// The communication struct that is managing // The communication struct that is managing
// all the communication between the different goroutines. // all the communication between the different goroutines.
type Communication struct { type Communication struct {
Context *context.Context
CancelContext *context.CancelFunc
PackageCounter *atomic.Value PackageCounter *atomic.Value
LastPacketTimer *atomic.Value LastPacketTimer *atomic.Value
CloudTimestamp *atomic.Value CloudTimestamp *atomic.Value
@@ -34,4 +37,5 @@ type Communication struct {
Decoder *ffmpeg.VideoDecoder Decoder *ffmpeg.VideoDecoder
SubDecoder *ffmpeg.VideoDecoder SubDecoder *ffmpeg.VideoDecoder
Image string Image string
CameraConnected bool
} }
+44 -34
View File
@@ -12,34 +12,36 @@ type Configuration struct {
// Config is the highlevel struct which contains all the configuration of // Config is the highlevel struct which contains all the configuration of
// your Kerberos Open Source instance. // your Kerberos Open Source instance.
type Config struct { type Config struct {
Type string `json:"type"` Type string `json:"type"`
Key string `json:"key"` Key string `json:"key"`
Name string `json:"name"` Name string `json:"name"`
FriendlyName string `json:"friendly_name"` FriendlyName string `json:"friendly_name"`
Time string `json:"time" bson:"time"` Time string `json:"time" bson:"time"`
Offline string `json:"offline"` Offline string `json:"offline"`
AutoClean string `json:"auto_clean"` AutoClean string `json:"auto_clean"`
MaxDirectorySize int64 `json:"max_directory_size"` RemoveAfterUpload string `json:"remove_after_upload"`
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"` MaxDirectorySize int64 `json:"max_directory_size"`
Capture Capture `json:"capture"` Timezone string `json:"timezone"`
Timetable []*Timetable `json:"timetable"` Capture Capture `json:"capture"`
Region *Region `json:"region"` Timetable []*Timetable `json:"timetable"`
Cloud string `json:"cloud" bson:"cloud"` Region *Region `json:"region"`
S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"` Cloud string `json:"cloud" bson:"cloud"`
KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"` S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"`
MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"` KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"`
MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"` Dropbox *Dropbox `json:"dropbox,omitempty" bson:"dropbox,omitempty"`
MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"` MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"`
STUNURI string `json:"stunuri" bson:"stunuri"` MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"`
TURNURI string `json:"turnuri" bson:"turnuri"` MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"`
TURNUsername string `json:"turn_username" bson:"turn_username"` STUNURI string `json:"stunuri" bson:"stunuri"`
TURNPassword string `json:"turn_password" bson:"turn_password"` TURNURI string `json:"turnuri" bson:"turnuri"`
HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/ TURNUsername string `json:"turn_username" bson:"turn_username"`
HubURI string `json:"hub_uri" bson:"hub_uri"` TURNPassword string `json:"turn_password" bson:"turn_password"`
HubKey string `json:"hub_key" bson:"hub_key"` HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/
HubPrivateKey string `json:"hub_private_key" bson:"hub_private_key"` HubURI string `json:"hub_uri" bson:"hub_uri"`
HubSite string `json:"hub_site" bson:"hub_site"` HubKey string `json:"hub_key" bson:"hub_key"`
ConditionURI string `json:"condition_uri" bson:"condition_uri"` 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"`
} }
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera), // Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
@@ -50,10 +52,10 @@ type Capture struct {
USBCamera USBCamera `json:"usbcamera"` USBCamera USBCamera `json:"usbcamera"`
RaspiCamera RaspiCamera `json:"raspicamera"` RaspiCamera RaspiCamera `json:"raspicamera"`
Recording string `json:"recording,omitempty"` Recording string `json:"recording,omitempty"`
Continuous string `json:"continuous,omitempty"`
Snapshots string `json:"snapshots,omitempty"` Snapshots string `json:"snapshots,omitempty"`
Motion string `json:"motion,omitempty"` Motion string `json:"motion,omitempty"`
Liveview string `json:"liveview,omitempty"` Liveview string `json:"liveview,omitempty"`
Continuous string `json:"continuous,omitempty"`
PostRecording int64 `json:"postrecording"` PostRecording int64 `json:"postrecording"`
PreRecording int64 `json:"prerecording"` PreRecording int64 `json:"prerecording"`
MaxLengthRecording int64 `json:"maxlengthrecording"` MaxLengthRecording int64 `json:"maxlengthrecording"`
@@ -68,13 +70,15 @@ type Capture struct {
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS. // IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
// Also includes ONVIF integration // Also includes ONVIF integration
type IPCamera struct { type IPCamera struct {
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
RTSP string `json:"rtsp"` RTSP string `json:"rtsp"`
SubRTSP string `json:"sub_rtsp"` SubRTSP string `json:"sub_rtsp"`
FPS string `json:"fps"` ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIF bool `json:"onvif,omitempty" bson:"onvif"` ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"` ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"` ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
} }
// USBCamera configuration, such as the device path (/dev/video*) // USBCamera configuration, such as the device path (/dev/video*)
@@ -147,3 +151,9 @@ type KStorage struct {
Provider string `json:"provider,omitempty" bson:"provider,omitempty"` Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"` Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
} }
// Dropbox integration
type Dropbox struct {
AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
}
+9
View File
@@ -12,4 +12,13 @@ type OnvifActionPTZ struct {
Down int `json:"down" bson:"down"` Down int `json:"down" bson:"down"`
Center int `json:"center" bson:"center"` Center int `json:"center" bson:"center"`
Zoom float64 `json:"zoom" bson:"zoom"` 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"`
} }
+541 -22
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/onvif/media" "github.com/kerberos-io/onvif/media"
@@ -31,7 +32,8 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
json.Unmarshal(b, &ptzAction) json.Unmarshal(b, &ptzAction)
// Connect to Onvif device // Connect to Onvif device
device, err := ConnectToOnvifDevice(configuration) cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := ConnectToOnvifDevice(&cameraConfiguration)
if err == nil { if err == nil {
// Get token from the first profile // Get token from the first profile
@@ -39,18 +41,78 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
if err == nil { if err == nil {
// Get the configurations from the device // Get the configurations from the device
configurations, err := GetConfigurationsFromDevice(device) configurations, err := GetPTZConfigurationsFromDevice(device)
if err == nil { 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 err == nil {
if ptzAction.Center == 1 { if ptzAction.Center == 1 {
// We will move the camera to zero position. // 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 { if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error()) log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
} }
@@ -100,16 +162,13 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
log.Log.Debug("HandleONVIFActions: finished") log.Log.Debug("HandleONVIFActions: finished")
} }
func ConnectToOnvifDevice(configuration *models.Configuration) (*onvif.Device, error) { func ConnectToOnvifDevice(cameraConfiguration *models.IPCamera) (*onvif.Device, error) {
log.Log.Debug("ConnectToOnvifDevice: started") log.Log.Debug("ConnectToOnvifDevice: started")
config := configuration.Config
// Get the capabilities of the ONVIF device
device, err := onvif.NewDevice(onvif.DeviceParams{ device, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: config.Capture.IPCamera.ONVIFXAddr, Xaddr: cameraConfiguration.ONVIFXAddr,
Username: config.Capture.IPCamera.ONVIFUsername, Username: cameraConfiguration.ONVIFUsername,
Password: config.Capture.IPCamera.ONVIFPassword, Password: cameraConfiguration.ONVIFPassword,
}) })
if err != nil { if err != nil {
@@ -154,11 +213,11 @@ func GetTokenFromProfile(device *onvif.Device, profileId int) (xsd.ReferenceToke
return profileToken, err return profileToken, err
} }
func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsResponse, error) { func GetPTZConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsResponse, error) {
// We'll try to receive the PTZ configurations from the server // We'll try to receive the PTZ configurations from the server
var configurations ptz.GetConfigurationsResponse var configurations ptz.GetConfigurationsResponse
// Get the configurations from the device // Get the PTZ configurations from the device
resp, err := device.CallMethod(ptz.GetConfigurations{}) resp, err := device.CallMethod(ptz.GetConfigurations{})
if err == nil { if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
@@ -167,11 +226,11 @@ func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsRes
stringBody := string(b) stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GetConfigurationsResponse") decodedXML, et, err := getXMLNode(stringBody, "GetConfigurationsResponse")
if err != nil { if err != nil {
log.Log.Error("GetConfigurationsFromDevice: " + err.Error()) log.Log.Error("GetPTZConfigurationsFromDevice: " + err.Error())
return configurations, err return configurations, err
} else { } else {
if err := decodedXML.DecodeElement(&configurations, et); err != nil { if err := decodedXML.DecodeElement(&configurations, et); err != nil {
log.Log.Error("GetConfigurationsFromDevice: " + err.Error()) log.Log.Error("GetPTZConfigurationsFromDevice: " + err.Error())
return configurations, err return configurations, err
} }
} }
@@ -180,18 +239,83 @@ func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsRes
return configurations, err 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{ // Get token from the first profile
X: float64(pan), token, err := GetTokenFromProfile(device, 0)
Y: float64(tilt), 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, Space: configuration.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace,
} }
absoluteZoomVector := xsd.Vector1D{
X: zoom,
Space: configuration.PTZConfiguration.DefaultAbsoluteZoomPositionSpace,
}
res, err := device.CallMethod(ptz.AbsoluteMove{ res, err := device.CallMethod(ptz.AbsoluteMove{
ProfileToken: token, ProfileToken: token,
Position: xsd.PTZVector{ Position: xsd.PTZVector{
PanTilt: absoluteVector, PanTilt: absolutePantiltVector,
Zoom: absoluteZoomVector,
}, },
}) })
@@ -200,11 +324,237 @@ func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfiguratio
} }
bs, _ := ioutil.ReadAll(res.Body) bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("AbsoluteMove: " + string(bs)) log.Log.Info("AbsoluteMove: " + string(bs))
return err 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.005 && position.PanTilt.X <= pan+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.
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.
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.005 && position.PanTilt.X <= pan+0.005) {
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.
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
}
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.
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
}
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 { func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64) error {
panTiltVector := xsd.Vector2D{ panTiltVector := xsd.Vector2D{
@@ -227,7 +577,7 @@ func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurations
bs, _ := ioutil.ReadAll(res.Body) bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("ContinuousPanTiltMove: " + string(bs)) log.Log.Debug("ContinuousPanTiltMove: " + string(bs))
time.Sleep(500 * time.Millisecond) time.Sleep(200 * time.Millisecond)
res, errStop := device.CallMethod(ptz.Stop{ res, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token, ProfileToken: token,
@@ -300,6 +650,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
return capabilities 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) { func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
xmlBytes := bytes.NewBufferString(xmlBody) xmlBytes := bytes.NewBufferString(xmlBody)
decodedXML := xml.NewDecoder(xmlBytes) decodedXML := xml.NewDecoder(xmlBytes)
@@ -317,3 +750,89 @@ func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElemen
} }
return nil, nil, errors.New("error in NodeName - username and password might be wrong") return nil, nil, errors.New("error in NodeName - username and password might be wrong")
} }
func GetPTZFunctionsFromDevice(configurations ptz.GetConfigurationsResponse) ([]string, bool, bool) {
var functions []string
canZoom := false
canPanTilt := false
if configurations.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace != "" {
functions = append(functions, "AbsolutePanTiltMove")
canPanTilt = true
}
if configurations.PTZConfiguration.DefaultAbsoluteZoomPositionSpace != "" {
functions = append(functions, "AbsoluteZoomMove")
canZoom = true
}
if configurations.PTZConfiguration.DefaultRelativePanTiltTranslationSpace != "" {
functions = append(functions, "RelativePanTiltMove")
canPanTilt = true
}
if configurations.PTZConfiguration.DefaultRelativeZoomTranslationSpace != "" {
functions = append(functions, "RelativeZoomMove")
canZoom = true
}
if configurations.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace != "" {
functions = append(functions, "ContinuousPanTiltMove")
canPanTilt = true
}
if configurations.PTZConfiguration.DefaultContinuousZoomVelocitySpace != "" {
functions = append(functions, "ContinuousZoomMove")
canZoom = true
}
if configurations.PTZConfiguration.DefaultPTZSpeed != nil {
functions = append(functions, "PTZSpeed")
}
if configurations.PTZConfiguration.DefaultPTZTimeout != "" {
functions = append(functions, "PTZTimeout")
}
return functions, canZoom, canPanTilt
}
// VerifyOnvifConnection godoc
// @Router /api/onvif/verify [post]
// @ID verify-onvif
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags config
// @Param cameraConfig body models.IPCamera true "Camera Config"
// @Summary Will verify the ONVIF connectivity.
// @Description Will verify the ONVIF connectivity.
// @Success 200 {object} models.APIResponse
func VerifyOnvifConnection(c *gin.Context) {
var cameraConfig models.IPCamera
err := c.BindJSON(&cameraConfig)
if err == nil {
device, err := ConnectToOnvifDevice(&cameraConfig)
if err == nil {
// Get the list of configurations
configurations, err := GetPTZConfigurationsFromDevice(device)
if err == nil {
// Check if can zoom and/or pan/tilt is supported
ptzFunctions, canZoom, canPanTilt := GetPTZFunctionsFromDevice(configurations)
c.JSON(200, models.APIResponse{
Data: device,
PTZFunctions: ptzFunctions,
CanZoom: canZoom,
CanPanTilt: canPanTilt,
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong while getting the configurations " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong while verifying the ONVIF connection " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong while receiving the config " + err.Error(),
})
}
}
+115 -9
View File
@@ -42,7 +42,8 @@ func LoginToOnvif(c *gin.Context) {
}, },
} }
device, err := onvif.ConnectToOnvifDevice(configuration) cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil { if err == nil {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"device": device, "device": device,
@@ -85,7 +86,8 @@ func GetOnvifCapabilities(c *gin.Context) {
}, },
} }
device, err := onvif.ConnectToOnvifDevice(configuration) cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil { if err == nil {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"capabilities": onvif.GetCapabilitiesFromDevice(device), "capabilities": onvif.GetCapabilitiesFromDevice(device),
@@ -128,7 +130,8 @@ func DoOnvifPanTilt(c *gin.Context) {
}, },
} }
device, err := onvif.ConnectToOnvifDevice(configuration) cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil { if err == nil {
// Get token from the first profile // Get token from the first profile
@@ -137,13 +140,13 @@ func DoOnvifPanTilt(c *gin.Context) {
if err == nil { if err == nil {
// Get the configurations from the device // Get the configurations from the device
configurations, err := onvif.GetConfigurationsFromDevice(device) ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil { if err == nil {
pan := onvifPanTilt.Pan pan := onvifPanTilt.Pan
tilt := onvifPanTilt.Tilt tilt := onvifPanTilt.Tilt
err := onvif.ContinuousPanTilt(device, configurations, token, pan, tilt) err := onvif.ContinuousPanTilt(device, ptzConfigurations, token, pan, tilt)
if err == nil { if err == nil {
c.JSON(200, models.APIResponse{ c.JSON(200, models.APIResponse{
Message: "Successfully pan/tilted the camera", Message: "Successfully pan/tilted the camera",
@@ -201,7 +204,8 @@ func DoOnvifZoom(c *gin.Context) {
}, },
} }
device, err := onvif.ConnectToOnvifDevice(configuration) cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil { if err == nil {
// Get token from the first profile // Get token from the first profile
@@ -209,13 +213,13 @@ func DoOnvifZoom(c *gin.Context) {
if err == nil { if err == nil {
// Get the configurations from the device // Get the PTZ configurations from the device
configurations, err := onvif.GetConfigurationsFromDevice(device) ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil { if err == nil {
zoom := onvifZoom.Zoom zoom := onvifZoom.Zoom
err := onvif.ContinuousZoom(device, configurations, token, zoom) err := onvif.ContinuousZoom(device, ptzConfigurations, token, zoom)
if err == nil { if err == nil {
c.JSON(200, models.APIResponse{ c.JSON(200, models.APIResponse{
Message: "Successfully zoomed the camera", Message: "Successfully zoomed the camera",
@@ -246,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(),
})
}
}
+25 -18
View File
@@ -7,16 +7,18 @@ import (
jwt "github.com/appleboy/gin-jwt/v2" jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/capture" "github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/routers/websocket" "github.com/kerberos-io/agent/machinery/src/routers/websocket"
"github.com/kerberos-io/agent/machinery/src/cloud" "github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/components" "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/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils" "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) { r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, communication) websocket.WebsocketHandler(c, communication)
@@ -39,7 +41,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
var config models.Config var config models.Config
err := c.BindJSON(&config) err := c.BindJSON(&config)
if err == nil { if err == nil {
err := components.SaveConfig(config, configuration, communication) err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil { if err == nil {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"data": "☄ Reconfiguring", "data": "☄ Reconfiguring",
@@ -62,23 +64,22 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
api.GET("/dashboard", func(c *gin.Context) { api.GET("/dashboard", func(c *gin.Context) {
// This will return the timestamp when the last packet was correctyl received // Check if camera is online.
// this is to calculate if the camera connection is still working. cameraIsOnline := communication.CameraConnected
lastPacketReceived := int64(0)
if communication.LastPacketTimer != nil && communication.LastPacketTimer.Load() != nil {
lastPacketReceived = communication.LastPacketTimer.Load().(int64)
}
// If an agent is properly setup with Kerberos Hub, we will send // If an agent is properly setup with Kerberos Hub, we will send
// a ping to Kerberos Hub every 15seconds. On receiving a positive response // a ping to Kerberos Hub every 15seconds. On receiving a positive response
// it will update the CloudTimestamp value. // it will update the CloudTimestamp value.
cloudTimestamp := int64(0) cloudIsOnline := false
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil { 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. // The total number of recordings stored in the directory.
recordingDirectory := "./data/recordings" recordingDirectory := configDirectory + "/data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory) numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent. // All days stored in this agent.
@@ -99,8 +100,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"offlineMode": configuration.Config.Offline, "offlineMode": configuration.Config.Offline,
"cameraOnline": lastPacketReceived, "cameraOnline": cameraIsOnline,
"cloudOnline": cloudTimestamp, "cloudOnline": cloudIsOnline,
"numberOfRecordings": numberOfRecordings, "numberOfRecordings": numberOfRecordings,
"days": days, "days": days,
"latestEvents": latestEvents, "latestEvents": latestEvents,
@@ -115,7 +116,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
if eventFilter.NumberOfElements == 0 { if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10 eventFilter.NumberOfElements = 10
} }
recordingDirectory := "./data/recordings" recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory) files, err := utils.ReadDirectory(recordingDirectory)
if err == nil { if err == nil {
events := utils.GetSortedDirectory(files) events := utils.GetSortedDirectory(files)
@@ -137,7 +138,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
}) })
api.GET("/days", func(c *gin.Context) { api.GET("/days", func(c *gin.Context) {
recordingDirectory := "./data/recordings" recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory) files, err := utils.ReadDirectory(recordingDirectory)
if err == nil { if err == nil {
events := utils.GetSortedDirectory(files) events := utils.GetSortedDirectory(files)
@@ -165,7 +166,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
var config models.Config var config models.Config
err := c.BindJSON(&config) err := c.BindJSON(&config)
if err == nil { if err == nil {
err := components.SaveConfig(config, configuration, communication) err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil { if err == nil {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"data": "☄ Reconfiguring", "data": "☄ Reconfiguring",
@@ -196,12 +197,16 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
}) })
}) })
api.POST("/onvif/verify", func(c *gin.Context) {
onvif.VerifyOnvifConnection(c)
})
api.POST("/hub/verify", func(c *gin.Context) { api.POST("/hub/verify", func(c *gin.Context) {
cloud.VerifyHub(c) cloud.VerifyHub(c)
}) })
api.POST("/persistence/verify", func(c *gin.Context) { api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c) cloud.VerifyPersistence(c, configDirectory)
}) })
// Streaming handler // Streaming handler
@@ -211,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
// We will only send an image once per second. // We will only send an image once per second.
time.Sleep(time.Second * 1) time.Sleep(time.Second * 1)
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream") log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
img, err := components.GetImageFromFilePath() img, err := configService.GetImageFromFilePath(configDirectory)
return img, err return img, err
} }
h := components.StartMotionJPEG(imageFunction, 80) h := components.StartMotionJPEG(imageFunction, 80)
@@ -223,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
// the camera. // the camera.
api.POST("/camera/onvif/login", LoginToOnvif) api.POST("/camera/onvif/login", LoginToOnvif)
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities) 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/pantilt", DoOnvifPanTilt)
api.POST("/camera/onvif/zoom", DoOnvifZoom) api.POST("/camera/onvif/zoom", DoOnvifZoom)
api.POST("/camera/verify/:streamType", capture.VerifyCamera) api.POST("/camera/verify/:streamType", capture.VerifyCamera)
+25 -10
View File
@@ -1,6 +1,8 @@
package http package http
import ( import (
"os"
jwt "github.com/appleboy/gin-jwt/v2" jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-contrib/pprof" "github.com/gin-contrib/pprof"
"github.com/gin-gonic/contrib/static" "github.com/gin-gonic/contrib/static"
@@ -33,7 +35,7 @@ import (
// @in header // @in header
// @name Authorization // @name Authorization
func StartServer(configuration *models.Configuration, communication *models.Communication) { func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
// Initialize REST API // Initialize REST API
r := gin.Default() r := gin.Default()
@@ -55,15 +57,28 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
} }
// Add all routes // 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 // Add static routes to UI
r.Use(static.Serve("/", static.LocalFile("./www", true))) r.Use(static.Serve("/", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/dashboard", static.LocalFile("./www", true))) r.Use(static.Serve("/dashboard", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/media", static.LocalFile("./www", true))) r.Use(static.Serve("/media", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/settings", static.LocalFile("./www", true))) r.Use(static.Serve("/settings", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/login", static.LocalFile("./www", true))) r.Use(static.Serve("/login", static.LocalFile(configDirectory+"/www", true)))
r.Handle("GET", "/file/*filepath", Files) r.Handle("GET", "/file/*filepath", func(c *gin.Context) {
Files(c, configDirectory)
})
// Run the api on port // Run the api on port
err = r.Run(":" + configuration.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("Access-Control-Allow-Origin", "*")
c.Header("Content-Type", "video/mp4") c.Header("Content-Type", "video/mp4")
c.File("./data/recordings" + c.Param("filepath")) c.File(configDirectory + "/data/recordings" + c.Param("filepath"))
} }
+2 -2
View File
@@ -5,6 +5,6 @@ import (
"github.com/kerberos-io/agent/machinery/src/routers/http" "github.com/kerberos-io/agent/machinery/src/routers/http"
) )
func StartWebserver(configuration *models.Configuration, communication *models.Communication) { func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configuration, communication) http.StartServer(configDirectory, configuration, communication)
} }
+410 -48
View File
@@ -8,15 +8,119 @@ import (
"time" "time"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gofrs/uuid"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models" "github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/webrtc" "github.com/kerberos-io/agent/machinery/src/webrtc"
) )
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client { // The message structure which is used to send over
// and receive messages from the MQTT broker
type Message struct {
Mid string `json:"mid"`
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"`
Value map[string]interface{} `json:"value"`
}
// We'll cache the MQTT settings to know if we need to reinitialize the MQTT client connection.
// If we update the configuration but no new MQTT settings are provided, we don't need to restart it.
var PREV_MQTTURI string
var PREV_MQTTUsername string
var PREV_MQTTPassword string
var PREV_HubKey string
var PREV_AgentKey string
func HasMQTTClientModified(configuration *models.Configuration) bool {
MTTURI := configuration.Config.MQTTURI
MTTUsername := configuration.Config.MQTTUsername
MQTTPassword := configuration.Config.MQTTPassword
HubKey := configuration.Config.HubKey
AgentKey := configuration.Config.Key
if PREV_MQTTURI != MTTURI || PREV_MQTTUsername != MTTUsername || PREV_MQTTPassword != MQTTPassword || PREV_HubKey != HubKey || PREV_AgentKey != AgentKey {
log.Log.Info("HasMQTTClientModified: MQTT settings have been modified, restarting MQTT client.")
return true
}
return false
}
func PackageMQTTMessage(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.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
msg.PublicKey = ""
msg.Fingerprint = ""
payload, err := json.Marshal(msg)
return payload, err
}
// Configuring MQTT to subscribe for various bi-directional messaging
// Listen and reply (a generic method to share and retrieve information)
//
// !!! NEW METHOD TO COMMUNICATE: only create a single subscription for all communication.
// and an additional publish messages back
//
// - [SUBSCRIPTION] kerberos/agent/{hubkey} (hub -> agent)
// - [PUBLISH] kerberos/hub/{hubkey} (agent -> hub)
//
// !!! LEGACY METHODS BELOW, WE SHOULD LEVERAGE THE ABOVE METHOD!
//
// [SUBSCRIPTIONS]
//
// SD Streaming (Base64 JPEGs)
// - kerberos/{hubkey}/device/{devicekey}/request-live: use for polling of SD live streaming (as long the user requests stream, we'll send JPEGs over).
//
// HD Streaming (WebRTC)
// - kerberos/register: use for receiving HD live streaming requests.
// - candidate/cloud: remote ICE candidates are shared over this line.
// - kerberos/webrtc/keepalivehub/{devicekey}: use for polling of HD streaming (as long the user requests stream, we'll send it over).
// - kerberos/webrtc/peers/{devicekey}: we'll keep track of the number of peers (we can have more than 1 concurrent listeners).
//
// ONVIF capabilities
// - kerberos/onvif/{devicekey}: endpoint to execute ONVIF commands such as (PTZ, Zoom, IO, etc)
//
// [PUBlISH]
// Next to subscribing to various topics, we'll also publish messages to various topics, find a list of available Publish methods.
//
// - kerberos/webrtc/packets/{devicekey}: use for forwarding WebRTC (RTP Packets) over MQTT -> Complex firewall.
// - kerberos/webrtc/keepalive/{devicekey}: use for keeping alive forwarded WebRTC stream
// - {devicekey}/{sessionid}/answer: once a WebRTC request is received through (kerberos/register), we'll draft an answer and send it back to the remote WebRTC client.
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
config := configuration.Config config := configuration.Config
// Set the MQTT settings.
PREV_MQTTURI = configuration.Config.MQTTURI
PREV_MQTTUsername = configuration.Config.MQTTUsername
PREV_MQTTPassword = configuration.Config.MQTTPassword
PREV_HubKey = configuration.Config.HubKey
PREV_AgentKey = configuration.Config.Key
if config.Offline == "true" { if config.Offline == "true" {
log.Log.Info("ConfigureMQTT: not starting as running in Offline mode.") log.Log.Info("ConfigureMQTT: not starting as running in Offline mode.")
} else { } else {
@@ -78,10 +182,13 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
webrtc.CandidateArrays = make(map[string](chan string)) webrtc.CandidateArrays = make(map[string](chan string))
opts.OnConnect = func(c mqtt.Client) { opts.OnConnect = func(c mqtt.Client) {
// We managed to connect to the MQTT broker, hurray! // We managed to connect to the MQTT broker, hurray!
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL) log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
// Create a susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
// Legacy methods below -> should be converted to the above method.
// Create a subscription to know if send out a livestream or not. // Create a subscription to know if send out a livestream or not.
MQTTListenerHandleLiveSD(c, hubKey, configuration, communication) MQTTListenerHandleLiveSD(c, hubKey, configuration, communication)
@@ -113,15 +220,264 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
return nil return nil
} }
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
// Decode the message, we are expecting following format.
// {
// mid: string, "unique id for the message"
// timestamp: int64, "unix timestamp when the message was generated"
// encrypted: boolean,
// fingerprint: string, "fingerprint of the message to validate authenticity"
// payload: Payload, "a json object which might be encrypted"
// }
var message Message
json.Unmarshal(msg.Payload(), &message)
if message.Mid != "" && message.Timestamp != 0 {
// Messages might be encrypted, if so we'll
// need to decrypt them.
var payload Payload
if message.Encrypted {
// We'll find out the key we use to decrypt the message.
// TODO -> still needs to be implemented.
// Use to fingerprint to act accordingly.
} else {
payload = message.Payload
}
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
if payload.DeviceId != configuration.Config.Key {
// Not relevant for this device, so we'll ignore it.
} else {
// 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":
HandleRecording(mqttClient, hubKey, payload, configuration, communication)
case "get-ptz-position":
HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "update-ptz-position":
HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "request-config":
HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
case "update-config":
HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
}
}
}
})
}
}
// 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.
}
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RecordPayload
jsonData, _ := json.Marshal(value)
var recordPayload RecordPayload
json.Unmarshal(jsonData, &recordPayload)
if recordPayload.Timestamp != 0 {
motionDataPartial := models.MotionDataPartial{
Timestamp: recordPayload.Timestamp,
}
communication.HandleMotion <- motionDataPartial
}
}
// 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.
}
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var positionPayload 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 := Message{
Payload: Payload{
Action: "ptz-position",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
"timestamp": positionPayload.Timestamp,
"position": posString,
},
},
}
payload, err := PackageMQTTMessage(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 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 {
communication.HandleONVIF <- onvifAction
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
} else {
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
}
}
}
// 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.
}
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload 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 := Message{
Payload: Payload{
Action: "receive-config",
DeviceId: configuration.Config.Key,
Value: configMap,
},
}
payload, err := PackageMQTTMessage(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")
}
}
// 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 models.Config `json:"config"`
}
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to UpdateConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload 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 := Message{
Payload: Payload{
Action: "acknowledge-update-config",
DeviceId: configuration.Config.Key,
},
}
payload, err := PackageMQTTMessage(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 DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
if mqttClient != nil {
// Cleanup all subscriptions
// New methods
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
// Legacy methods
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)
mqttClient.Disconnect(1000)
mqttClient = nil
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
}
}
// #################################################################################################
// Below you'll find legacy methods, as of now we'll have a single subscription, which scales better
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) { func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config config := configuration.Config
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live" topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
select { if communication.CameraConnected {
case communication.HandleLiveSD <- time.Now().Unix(): select {
default: 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.")
} }
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
msg.Ack() msg.Ack()
}) })
} }
@@ -130,12 +486,16 @@ func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, co
config := configuration.Config config := configuration.Config
topicRequestWebRtc := config.Key + "/register" topicRequestWebRtc := config.Key + "/register"
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.") if communication.CameraConnected {
var sdp models.SDPPayload var sdp models.SDPPayload
json.Unmarshal(msg.Payload(), &sdp) json.Unmarshal(msg.Payload(), &sdp)
select { select {
case communication.HandleLiveHDHandshake <- sdp: case communication.HandleLiveHDHandshake <- sdp:
default: 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() msg.Ack()
}) })
@@ -145,9 +505,13 @@ func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, co
config := configuration.Config config := configuration.Config
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key) topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
alive := string(msg.Payload()) if communication.CameraConnected {
communication.HandleLiveHDKeepalive <- alive alive := string(msg.Payload())
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive) communication.HandleLiveHDKeepalive <- alive
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
} else {
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
}
}) })
} }
@@ -155,9 +519,13 @@ func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, config
config := configuration.Config config := configuration.Config
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key) topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
peerCount := string(msg.Payload()) if communication.CameraConnected {
communication.HandleLiveHDPeers <- peerCount peerCount := string(msg.Payload())
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount) 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.")
}
}) })
} }
@@ -165,19 +533,23 @@ func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, c
config := configuration.Config config := configuration.Config
topicCandidates := "candidate/cloud" topicCandidates := "candidate/cloud"
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
var candidate models.Candidate if communication.CameraConnected {
json.Unmarshal(msg.Payload(), &candidate) var candidate models.Candidate
if candidate.CloudKey == config.Key { json.Unmarshal(msg.Payload(), &candidate)
key := candidate.CloudKey + "/" + candidate.Cuuid if candidate.CloudKey == config.Key {
candidatesExists := false key := candidate.CloudKey + "/" + candidate.Cuuid
var channel chan string candidatesExists := false
for !candidatesExists { var channel chan string
webrtc.CandidatesMutex.Lock() for !candidatesExists {
channel, candidatesExists = webrtc.CandidateArrays[key] webrtc.CandidatesMutex.Lock()
webrtc.CandidatesMutex.Unlock() channel, candidatesExists = webrtc.CandidateArrays[key]
webrtc.CandidatesMutex.Unlock()
}
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
channel <- string(msg.Payload())
} }
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload())) } else {
channel <- string(msg.Payload()) log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
} }
}) })
} }
@@ -186,23 +558,13 @@ func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuratio
config := configuration.Config config := configuration.Config
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key) topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) { mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
var onvifAction models.OnvifAction if communication.CameraConnected {
json.Unmarshal(msg.Payload(), &onvifAction) var onvifAction models.OnvifAction
communication.HandleONVIF <- onvifAction json.Unmarshal(msg.Payload(), &onvifAction)
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action) 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 DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
if mqttClient != nil {
// Cleanup all subscriptions.
mqttClient.Unsubscribe("kerberos/" + config.HubKey + "/device/" + config.Key + "/request-live")
mqttClient.Unsubscribe(config.Key + "/register")
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + config.Key)
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + config.Key)
mqttClient.Unsubscribe("candidate/cloud")
mqttClient.Unsubscribe("kerberos/onvif/" + config.Key)
mqttClient.Disconnect(1000)
mqttClient = nil
}
}
+28 -18
View File
@@ -3,7 +3,6 @@ package websocket
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt"
"net/http" "net/http"
"sync" "sync"
@@ -52,6 +51,7 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
w := c.Writer w := c.Writer
r := c.Request r := c.Request
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
// error handling here // error handling here
if err == nil { if err == nil {
defer conn.Close() defer conn.Close()
@@ -84,28 +84,29 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
_, exists := sockets[clientID].Cancels["stream-sd"] _, exists := sockets[clientID].Cancels["stream-sd"]
if exists { if exists {
sockets[clientID].Cancels["stream-sd"]() sockets[clientID].Cancels["stream-sd"]()
delete(sockets[clientID].Cancels, "stream-sd")
} else { } else {
fmt.Println("Streaming sd does not exists for " + clientID) log.Log.Error("Streaming sd does not exists for " + clientID)
} }
case "stream-sd": case "stream-sd":
startStrean := Message{ if communication.CameraConnected {
ClientID: clientID, _, exists := sockets[clientID].Cancels["stream-sd"]
MessageType: "stream-sd", if exists {
Message: map[string]string{ log.Log.Info("Already streaming sd for " + clientID)
"message": "Start streaming low resolution", } else {
}, startStream := Message{
} ClientID: clientID,
sockets[clientID].WriteJson(startStrean) MessageType: "stream-sd",
Message: map[string]string{
"message": "Start streaming low resolution",
},
}
sockets[clientID].WriteJson(startStream)
_, exists := sockets[clientID].Cancels["stream-sd"] ctx, cancel := context.WithCancel(context.Background())
if exists { sockets[clientID].Cancels["stream-sd"] = cancel
fmt.Println("Already streaming sd for " + clientID) go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
} else { }
ctx, cancel := context.WithCancel(context.Background())
sockets[clientID].Cancels["stream-sd"] = cancel
go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
} }
} }
@@ -174,5 +175,14 @@ logreader:
frame.Free() frame.Free()
// Close socket for streaming
_, exists := connection.Cancels["stream-sd"]
if exists {
delete(connection.Cancels, "stream-sd")
} else {
log.Log.Error("Streaming sd does not exists for " + clientID)
}
// Send stop streaming message
log.Log.Info("ForwardSDStream: stop sending streaming over websocket") log.Log.Info("ForwardSDStream: stop sending streaming over websocket")
} }
+247
View File
@@ -0,0 +1,247 @@
package rtsp
import (
"fmt"
"image"
"image/jpeg"
"log"
"os"
"strconv"
"time"
"github.com/bluenviron/gortsplib/v3"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/formats"
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtph265"
"github.com/bluenviron/gortsplib/v3/pkg/url"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/pion/rtp"
)
func CreateClient() {
c := &gortsplib.Client{
OnRequest: func(req *base.Request) {
//log.Log.Info(logger.Debug, "c->s %v", req)
},
OnResponse: func(res *base.Response) {
//s.Log(logger.Debug, "s->c %v", res)
},
OnTransportSwitch: func(err error) {
//s.Log(logger.Warn, err.Error())
},
OnPacketLost: func(err error) {
//s.Log(logger.Warn, err.Error())
},
OnDecodeError: func(err error) {
//s.Log(logger.Warn, err.Error())
},
}
u, err := url.Parse("rtsp://admin:admin@192.168.1.111") //"rtsp://seing:bud-edPTQc@109.159.199.103:554/rtsp/defaultPrimary?mtu=1440&streamType=m") //
if err != nil {
panic(err)
}
err = c.Start(u.Scheme, u.Host)
if err != nil {
//return err
}
defer c.Close()
medias, baseURL, _, err := c.Describe(u)
if err != nil {
//return err
}
fmt.Println(medias)
// find the H264 media and format
var forma *formats.H265
medi := medias.FindFormat(&forma)
if medi == nil {
panic("media not found")
}
// setup RTP/H264 -> H264 decoder
rtpDec := forma.CreateDecoder()
// setup H264 -> MPEG-TS muxer
//pegtsMuxer, err := newMPEGTSMuxer(forma.SPS, forma.PPS)
if err != nil {
panic(err)
}
// setup H264 -> raw frames decoder
/*h264RawDec, err := newH264Decoder()
if err != nil {
panic(err)
}
defer h264RawDec.close()
// if SPS and PPS are present into the SDP, send them to the decoder
if forma.SPS != nil {
h264RawDec.decode(forma.SPS)
}
if forma.PPS != nil {
h264RawDec.decode(forma.PPS)
}*/
readErr := make(chan error)
go func() {
readErr <- func() error {
// Get codecs
for _, medi := range medias {
for _, forma := range medi.Formats {
fmt.Println(forma)
}
}
err = c.SetupAll(medias, baseURL)
if err != nil {
return err
}
for _, medi := range medias {
for _, forma := range medi.Formats {
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
au, pts, err := rtpDec.Decode(pkt)
if err != nil {
if err != rtph265.ErrNonStartingPacketAndNoPrevious && err != rtph265.ErrMorePacketsNeeded {
log.Printf("ERR: %v", err)
}
return
}
for _, nalu := range au {
log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu))
}
/*// extract access unit from RTP packets
// DecodeUntilMarker is necessary for the DTS extractor to work
if pkt.PayloadType == 96 {
au, pts, err := rtpDec.DecodeUntilMarker(pkt)
if err != nil {
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
log.Printf("ERR: %v", err)
}
return
}
// encode the access unit into MPEG-TS
mpegtsMuxer.encode(au, pts)
for _, nalu := range au {
// convert NALUs into RGBA frames
img, err := h264RawDec.decode(nalu)
if err != nil {
panic(err)
}
// wait for a frame
if img == nil {
continue
}
// convert frame to JPEG and save to file
err = saveToFile(img)
if err != nil {
panic(err)
}
}
}*/
})
}
}
_, err = c.Play(nil)
if err != nil {
return err
}
return c.Wait()
}()
}()
for {
select {
case err := <-readErr:
fmt.Println(err)
}
}
}
func saveToFile(img image.Image) error {
// create file
fname := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + ".jpg"
f, err := os.Create(fname)
if err != nil {
panic(err)
}
defer f.Close()
log.Println("saving", fname)
// convert to jpeg
return jpeg.Encode(f, img, &jpeg.Options{
Quality: 60,
})
}
// extract SPS and PPS without decoding RTP packets
func rtpH264ExtractSPSPPS(pkt *rtp.Packet) ([]byte, []byte) {
if len(pkt.Payload) < 1 {
return nil, nil
}
typ := h264.NALUType(pkt.Payload[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
return pkt.Payload, nil
case h264.NALUTypePPS:
return nil, pkt.Payload
case h264.NALUTypeSTAPA:
payload := pkt.Payload[1:]
var sps []byte
var pps []byte
for len(payload) > 0 {
if len(payload) < 2 {
break
}
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 {
break
}
if int(size) > len(payload) {
return nil, nil
}
nalu := payload[:size]
payload = payload[size:]
typ = h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
sps = nalu
case h264.NALUTypePPS:
pps = nalu
}
}
return sps, pps
default:
return nil, nil
}
}
+140
View File
@@ -0,0 +1,140 @@
package rtsp
import (
"fmt"
"image"
"unsafe"
)
// #cgo pkg-config: libavcodec libavutil libswscale
// #include <libavcodec/avcodec.h>
// #include <libavutil/imgutils.h>
// #include <libswscale/swscale.h>
import "C"
func frameData(frame *C.AVFrame) **C.uint8_t {
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
}
func frameLineSize(frame *C.AVFrame) *C.int {
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
}
// h264Decoder is a wrapper around ffmpeg's H264 decoder.
type h264Decoder struct {
codecCtx *C.AVCodecContext
srcFrame *C.AVFrame
swsCtx *C.struct_SwsContext
dstFrame *C.AVFrame
dstFramePtr []uint8
}
// newH264Decoder allocates a new h264Decoder.
func newH264Decoder() (*h264Decoder, error) {
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
if codec == nil {
return nil, fmt.Errorf("avcodec_find_decoder() failed")
}
codecCtx := C.avcodec_alloc_context3(codec)
if codecCtx == nil {
return nil, fmt.Errorf("avcodec_alloc_context3() failed")
}
res := C.avcodec_open2(codecCtx, codec, nil)
if res < 0 {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("avcodec_open2() failed")
}
srcFrame := C.av_frame_alloc()
if srcFrame == nil {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("av_frame_alloc() failed")
}
return &h264Decoder{
codecCtx: codecCtx,
srcFrame: srcFrame,
}, nil
}
// close closes the decoder.
func (d *h264Decoder) close() {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}
if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}
C.av_frame_free(&d.srcFrame)
C.avcodec_close(d.codecCtx)
}
func (d *h264Decoder) decode(nalu []byte) (image.Image, error) {
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)
// send frame to decoder
var avPacket C.AVPacket
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
defer C.free(unsafe.Pointer(avPacket.data))
avPacket.size = C.int(len(nalu))
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
if res < 0 {
return nil, nil
}
// receive frame if available
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
if res < 0 {
return nil, nil
}
// if frame size has changed, allocate needed objects
if d.dstFrame == nil || d.dstFrame.width != d.srcFrame.width || d.dstFrame.height != d.srcFrame.height {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}
if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}
d.dstFrame = C.av_frame_alloc()
d.dstFrame.format = C.AV_PIX_FMT_RGBA
d.dstFrame.width = d.srcFrame.width
d.dstFrame.height = d.srcFrame.height
d.dstFrame.color_range = C.AVCOL_RANGE_JPEG
res = C.av_frame_get_buffer(d.dstFrame, 1)
if res < 0 {
return nil, fmt.Errorf("av_frame_get_buffer() err")
}
d.swsCtx = C.sws_getContext(d.srcFrame.width, d.srcFrame.height, C.AV_PIX_FMT_YUV420P,
d.dstFrame.width, d.dstFrame.height, (int32)(d.dstFrame.format), C.SWS_BILINEAR, nil, nil, nil)
if d.swsCtx == nil {
return nil, fmt.Errorf("sws_getContext() err")
}
dstFrameSize := C.av_image_get_buffer_size((int32)(d.dstFrame.format), d.dstFrame.width, d.dstFrame.height, 1)
d.dstFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.dstFrame.data[0]))[:dstFrameSize:dstFrameSize]
}
// convert frame from YUV420 to RGB
res = C.sws_scale(d.swsCtx, frameData(d.srcFrame), frameLineSize(d.srcFrame),
0, d.srcFrame.height, frameData(d.dstFrame), frameLineSize(d.dstFrame))
if res < 0 {
return nil, fmt.Errorf("sws_scale() err")
}
// embed frame into an image.Image
return &image.RGBA{
Pix: d.dstFramePtr,
Stride: 4 * (int)(d.dstFrame.width),
Rect: image.Rectangle{
Max: image.Point{(int)(d.dstFrame.width), (int)(d.dstFrame.height)},
},
}, nil
}
+15
View File
@@ -0,0 +1,15 @@
package rtsp
// mp4Muxer allows to save a H264 stream into a Mp4 file.
type mp4Muxer struct {
sps []byte
pps []byte
}
// newMp4Muxer allocates a mp4Muxer.
func newMp4Muxer(sps []byte, pps []byte) (*mp4Muxer, error) {
return &mp4Muxer{
sps: sps,
pps: pps,
}, nil
}
+173
View File
@@ -0,0 +1,173 @@
package rtsp
import (
"bufio"
"context"
"log"
"os"
"time"
"github.com/asticode/go-astits"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
)
// mpegtsMuxer allows to save a H264 stream into a MPEG-TS file.
type mpegtsMuxer struct {
sps []byte
pps []byte
f *os.File
b *bufio.Writer
mux *astits.Muxer
dtsExtractor *h264.DTSExtractor
firstIDRReceived bool
startDTS time.Duration
}
// newMPEGTSMuxer allocates a mpegtsMuxer.
func newMPEGTSMuxer(sps []byte, pps []byte) (*mpegtsMuxer, error) {
f, err := os.Create("mystream.ts")
if err != nil {
return nil, err
}
b := bufio.NewWriter(f)
mux := astits.NewMuxer(context.Background(), b)
mux.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
mux.SetPCRPID(256)
return &mpegtsMuxer{
sps: sps,
pps: pps,
f: f,
b: b,
mux: mux,
}, nil
}
// close closes all the mpegtsMuxer resources.
func (e *mpegtsMuxer) close() {
e.b.Flush()
e.f.Close()
}
// encode encodes a H264 access unit into MPEG-TS.
func (e *mpegtsMuxer) encode(au [][]byte, pts time.Duration) error {
// prepend an AUD. This is required by some players
filteredNALUs := [][]byte{
{byte(h264.NALUTypeAccessUnitDelimiter), 240},
}
nonIDRPresent := false
idrPresent := false
for _, nalu := range au {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
e.sps = append([]byte(nil), nalu...)
continue
case h264.NALUTypePPS:
e.pps = append([]byte(nil), nalu...)
continue
case h264.NALUTypeAccessUnitDelimiter:
continue
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
filteredNALUs = append(filteredNALUs, nalu)
}
au = filteredNALUs
if !nonIDRPresent && !idrPresent {
return nil
}
// add SPS and PPS before every group that contains an IDR
if idrPresent {
au = append([][]byte{e.sps, e.pps}, au...)
}
var dts time.Duration
if !e.firstIDRReceived {
// skip samples silently until we find one with a IDR
if !idrPresent {
return nil
}
e.firstIDRReceived = true
e.dtsExtractor = h264.NewDTSExtractor()
var err error
dts, err = e.dtsExtractor.Extract(au, pts)
if err != nil {
return err
}
e.startDTS = dts
dts = 0
pts -= e.startDTS
} else {
var err error
dts, err = e.dtsExtractor.Extract(au, pts)
if err != nil {
return err
}
dts -= e.startDTS
pts -= e.startDTS
}
oh := &astits.PESOptionalHeader{
MarkerBits: 2,
}
if dts == pts {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
} else {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent
oh.DTS = &astits.ClockReference{Base: int64(dts.Seconds() * 90000)}
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
}
// encode into Annex-B
annexb, err := h264.AnnexBMarshal(au)
if err != nil {
return err
}
// write TS packet
_, err = e.mux.WriteData(&astits.MuxerData{
PID: 256,
AdaptationField: &astits.PacketAdaptationField{
RandomAccessIndicator: idrPresent,
},
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: oh,
StreamID: 224, // video
},
Data: annexb,
},
})
if err != nil {
return err
}
log.Println("wrote TS packet")
return nil
}
+73 -14
View File
@@ -9,6 +9,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -38,6 +39,17 @@ const (
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
) )
func PrintASCIIArt() {
asciiArt := ` _ __ _ _
| |/ /___ _ __| |__ ___ _ __ ___ ___ (_) ___
| ' // _ \ '__| '_ \ / _ \ '__/ _ \/ __| | |/ _ \
| . \ __/ | | |_) | __/ | | (_) \__ \_| | (_) |
|_|\_\___|_| |_.__/ \___|_| \___/|___(_)_|\___/
`
fmt.Println(asciiArt)
}
func DirSize(path string) (int64, error) { func DirSize(path string) (int64, error) {
var size int64 var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
@@ -98,30 +110,46 @@ func CountDigits(i int64) (count int) {
return count return count
} }
func CheckDataDirectoryPermissions() error { func CheckDataDirectoryPermissions(configDirectory string) error {
recordingsDirectory := "./data/recordings" recordingsDirectory := configDirectory + "/data/recordings"
configDirectory := "./data/config" configurationDirectory := configDirectory + "/data/config"
snapshotsDirectory := "./data/snapshots" snapshotsDirectory := configDirectory + "/data/snapshots"
cloudDirectory := configDirectory + "/data/cloud"
err := CheckDirectoryPermissions(recordingsDirectory) err := CheckDirectoryPermissions(recordingsDirectory)
if err == nil {
err = CheckDirectoryPermissions(configurationDirectory)
if err == nil {
err = CheckDirectoryPermissions(snapshotsDirectory)
if err == nil {
err = CheckDirectoryPermissions(cloudDirectory)
}
}
}
if err != nil { if err != nil {
log.Log.Error("Checking data directory permissions: " + err.Error())
return err return err
} }
err = CheckDirectoryPermissions(configDirectory)
if err != nil { log.Log.Info("Checking data directory permissions: OK")
return err
}
err = CheckDirectoryPermissions(snapshotsDirectory)
if err != nil {
return err
}
return nil return nil
} }
func CheckDirectoryPermissions(directory string) error { func CheckDirectoryPermissions(directory string) error {
// Check if the directory exists
if _, err := os.Stat(directory); os.IsNotExist(err) {
return errors.New("Directory does not exist, " + directory)
}
// Try to create a file
file := directory + "/.test" file := directory + "/.test"
f, err := os.Create(file) f, err := os.Create(file)
defer f.Close() if f != nil {
defer f.Close()
}
// We will remove the file if it was created
if err == nil { if err == nil {
err := os.Remove(file) err := os.Remove(file)
if err == nil { if err == nil {
@@ -136,7 +164,6 @@ func CheckDirectoryPermissions(directory string) error {
func ReadDirectory(directory string) ([]os.FileInfo, error) { func ReadDirectory(directory string) ([]os.FileInfo, error) {
ff, err := ioutil.ReadDir(directory) ff, err := ioutil.ReadDir(directory)
if err != nil { if err != nil {
log.Log.Error(err.Error())
return []os.FileInfo{}, nil return []os.FileInfo{}, nil
} }
return ff, err return ff, err
@@ -271,3 +298,35 @@ func CreateFragmentedMP4(fullName string, fragmentedDuration int64) {
os.Remove(fullName) os.Remove(fullName)
os.Rename(fullName+"f.mp4", fullName) os.Rename(fullName+"f.mp4", fullName)
} }
func PrintEnvironmentVariables() {
// Print environment variables that include "AGENT_" as a prefix.
environmentVariables := ""
for _, e := range os.Environ() {
if strings.Contains(e, "AGENT_") {
pair := strings.Split(e, "=")
environmentVariables = environmentVariables + pair[0] + "=" + pair[1] + " "
}
}
log.Log.Info("Printing out environmentVariables (AGENT_...): " + environmentVariables)
}
func PrintConfiguration(configuration *models.Configuration) {
// We will print out the struct.
if configuration == nil {
log.Log.Info("Configuration is nil")
return
}
config := configuration.Config
// Iterate over the struct and printout the values.
v := reflect.ValueOf(config)
typeOfS := v.Type()
configurationVariables := ""
for i := 0; i < v.NumField(); i++ {
key := typeOfS.Field(i).Name
value := v.Field(i).Interface()
// Convert to string.
configurationVariables = configurationVariables + key + ": " + fmt.Sprintf("%v", value) + " "
}
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
}
+42 -24
View File
@@ -27,7 +27,7 @@ var (
CandidateArrays map[string](chan string) CandidateArrays map[string](chan string)
peerConnectionCount int64 peerConnectionCount int64
peerConnections map[string]*pionWebRTC.PeerConnection peerConnections map[string]*pionWebRTC.PeerConnection
encoder *ffmpeg.VideoEncoder //encoder *ffmpeg.VideoEncoder
) )
type WebRTC struct { type WebRTC struct {
@@ -40,7 +40,8 @@ type WebRTC struct {
PacketsCount chan int PacketsCount chan int
} }
func init() { // No longer used, is for transcoding, might comeback on this!
/*func init() {
// Encoder is created for once and for all. // Encoder is created for once and for all.
var err error var err error
encoder, err = ffmpeg.NewVideoEncoderByCodecType(av.H264) encoder, err = ffmpeg.NewVideoEncoderByCodecType(av.H264)
@@ -55,7 +56,7 @@ func init() {
encoder.SetPixelFormat(av.I420) encoder.SetPixelFormat(av.I420)
encoder.SetBitrate(1000000) // 1MB encoder.SetBitrate(1000000) // 1MB
encoder.SetGopSize(30 / 1) // 1s encoder.SetGopSize(30 / 1) // 1s
} }*/
func CreateWebRTC(name string, stunServers []string, turnServers []string, turnServersUsername string, turnServersCredential string) *WebRTC { func CreateWebRTC(name string, stunServers []string, turnServers []string, turnServersUsername string, turnServersCredential string) *WebRTC {
return &WebRTC{ return &WebRTC{
@@ -86,18 +87,18 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
return offer return offer
} }
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, track *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.SDPPayload, candidates chan string) {
config := configuration.Config config := configuration.Config
name := config.Key deviceKey := config.Key
stunServers := []string{config.STUNURI} stunServers := []string{config.STUNURI}
turnServers := []string{config.TURNURI} turnServers := []string{config.TURNURI}
turnServersUsername := config.TURNUsername turnServersUsername := config.TURNUsername
turnServersCredential := config.TURNPassword turnServersCredential := config.TURNPassword
// Create WebRTC object // Create WebRTC object
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential) w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(handshake.Sdp) sd, err := w.DecodeSessionDescription(handshake.Sdp)
if err == nil { if err == nil {
@@ -127,15 +128,13 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
if err == nil && peerConnection != nil { if err == nil && peerConnection != nil {
if _, err = peerConnection.AddTrack(track); err != nil { if _, err = peerConnection.AddTrack(videoTrack); err != nil {
panic(err) panic(err)
} }
_, err = peerConnection.AddTransceiverFromTrack(track, if _, err = peerConnection.AddTrack(audioTrack); err != nil {
pionWebRTC.RtpTransceiverInit{ panic(err)
Direction: pionWebRTC.RTPTransceiverDirectionSendonly, }
},
)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -188,7 +187,7 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
candidatesMux.Lock() candidatesMux.Lock()
defer candidatesMux.Unlock() defer candidatesMux.Unlock()
topic := fmt.Sprintf("%s/%s/candidate/edge", name, handshake.Cuuid) topic := fmt.Sprintf("%s/%s/candidate/edge", deviceKey, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic) log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
candiInit := candidate.ToJSON() candiInit := candidate.ToJSON()
sdpmid := "0" sdpmid := "0"
@@ -204,7 +203,7 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
peerConnections[handshake.Cuuid] = peerConnection peerConnections[handshake.Cuuid] = peerConnection
if err == nil { if err == nil {
topic := fmt.Sprintf("%s/%s/answer", name, handshake.Cuuid) topic := fmt.Sprintf("%s/%s/answer", deviceKey, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic) log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))) mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
} }
@@ -214,12 +213,29 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
} }
} }
func NewVideoTrack() *pionWebRTC.TrackLocalStaticSample { func NewVideoTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
outboundVideoTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: "video/h264"}, "video", "pion124") var mimeType string
mimeType = pionWebRTC.MimeTypeH264
outboundVideoTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "video", "pion124")
return outboundVideoTrack return outboundVideoTrack
} }
func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, track *pionWebRTC.TrackLocalStaticSample, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) { func NewAudioTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
var mimeType string
for _, codec := range codecs {
if codec.Type().String() == "OPUS" {
mimeType = pionWebRTC.MimeTypeOpus
} else if codec.Type().String() == "PCM_MULAW" {
mimeType = pionWebRTC.MimeTypePCMU
} else if codec.Type().String() == "PCM_ALAW" {
mimeType = pionWebRTC.MimeTypePCMA
}
}
outboundAudioTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "audio", "pion124")
return outboundAudioTrack
}
func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
config := configuration.Config config := configuration.Config
@@ -230,13 +246,10 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
// Later when we read a packet we need to figure out which track to send it to. // Later when we read a packet we need to figure out which track to send it to.
videoIdx := -1 videoIdx := -1
audioIdx := -1 audioIdx := -1
log.Log.Info("WriteToTrack: listing codecs.")
for i, codec := range codecs { for i, codec := range codecs {
log.Log.Info("WriteToTrack: codec - " + codec.Type().String() + " found.")
log.Log.Info(codec.Type().String())
if codec.Type().String() == "H264" && videoIdx < 0 { if codec.Type().String() == "H264" && videoIdx < 0 {
videoIdx = i videoIdx = i
} else if codec.Type().String() == "PCM_MULAW" && audioIdx < 0 { } else if (codec.Type().String() == "OPUS" || codec.Type().String() == "PCM_MULAW" || codec.Type().String() == "PCM_ALAW") && audioIdx < 0 {
audioIdx = i audioIdx = i
} }
} }
@@ -366,13 +379,17 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
log.Log.Info("WriteToTrack: Error marshalling frame, " + err.Error()) log.Log.Info("WriteToTrack: Error marshalling frame, " + err.Error())
} }
} else { } else {
if err := track.WriteSample(sample); err != nil && err != io.ErrClosedPipe { if err := videoTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
fmt.Println("WriteToTrack: something went wrong while writing sample: " + err.Error()) log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
} }
} }
} }
case audioIdx: case audioIdx:
//log.Log.Info("WriteToTrack: not writing audio for the moment.") // We will send the audio
sample := pionMedia.Sample{Data: pkt.Data, Duration: pkt.Time}
if err := audioTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
}
} }
} }
} }
@@ -381,6 +398,7 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
p.Close() p.Close()
} }
} }
peerConnectionCount = 0 peerConnectionCount = 0
log.Log.Info("WriteToTrack: stop writing to track.") log.Log.Info("WriteToTrack: stop writing to track.")
} }
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh -e
cp -R $SNAP/data $SNAP_COMMON/
cp -R $SNAP/www $SNAP_COMMON/
cp -R $SNAP/version $SNAP_COMMON/
cp -R $SNAP/mp4fragment $SNAP_COMMON/
+23
View File
@@ -0,0 +1,23 @@
name: kerberosio # you probably want to 'snapcraft register <name>'
base: core22 # the base snap is the execution environment for this snap
version: '3.0.0' # just for humans, typically '1.2+git' or '1.3.2'
summary: A stand-alone open source video surveillance system # 79 char long summary
description: |
Kerberos Agent is an isolated and scalable video (surveillance) management
agent made available as Open Source under the MIT License. This means that
all the source code is available for you or your company, and you can use,
transform and distribute the source code; as long you keep a reference of
the original license. Kerberos Agent can be used for commercial usage.
grade: stable # stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
environment:
GIN_MODE: release
apps:
agent:
command: main -config /var/snap/kerberosio/common
plugs: [ network, network-bind ]
parts:
agent:
source: . #https://github.com/kerberos-io/agent/releases/download/21c0e01/agent-amd64.tar
plugin: dump
+1
View File
@@ -21,6 +21,7 @@
"react/forbid-prop-types": "off", "react/forbid-prop-types": "off",
"no-case-declarations": "off", "no-case-declarations": "off",
"camelcase": "off", "camelcase": "off",
"dot-notation": "off",
"jsx-a11y/media-has-caption": "off", "jsx-a11y/media-has-caption": "off",
"jsx-a11y/anchor-is-valid": "off", "jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/click-events-have-key-events": "off",
+1 -1
View File
@@ -4,7 +4,7 @@
"private": false, "private": false,
"dependencies": { "dependencies": {
"@giantmachines/redux-websocket": "^1.5.1", "@giantmachines/redux-websocket": "^1.5.1",
"@kerberos-io/ui": "1.70.0", "@kerberos-io/ui": "^1.76.0",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
@@ -1,5 +1,5 @@
(function (window) { (function (window) {
window['env'] = window['env'] || {}; window['env'] = window['env'] || {};
// Environment variables // Environment variables
window['env']['apiUrl'] = '${FACTORY_API_URL}'; window['env']['mode'] = 'demo';
})(this); })(this);
+1 -1
View File
@@ -1,5 +1,5 @@
(function (window) { (function (window) {
window['env'] = window['env'] || {}; window['env'] = window['env'] || {};
// Environment variables // Environment variables
window['env']['apiUrl'] = 'http://api.factory.kerberos.io'; window['env']['mode'] = 'release';
})(this); })(this);
+1 -1
View File
@@ -19,7 +19,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <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. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
+16 -5
View File
@@ -4,7 +4,8 @@
"configure": "Konfigurieren" "configure": "Konfigurieren"
}, },
"buttons": { "buttons": {
"save": "Speichern" "save": "Speichern",
"verify_connection": "Verify Connection"
}, },
"navigation": { "navigation": {
"profile": "Profil", "profile": "Profil",
@@ -69,7 +70,10 @@
"verify_persistence_error": "Beim überprüfen der Speichereinstellungen ist ein Fehler aufgetreten", "verify_persistence_error": "Beim überprüfen der Speichereinstellungen ist ein Fehler aufgetreten",
"verify_camera": "Überprüfen der Kameraeinstellungen.", "verify_camera": "Überprüfen der Kameraeinstellungen.",
"verify_camera_success": "Die Kameraeinstellungen wurden erfolgreich überprüft.", "verify_camera_success": "Die Kameraeinstellungen wurden erfolgreich überprüft.",
"verify_camera_error": "Beim überprüfen der Kameraeinstellungen ist ein Fehler aufgetreten" "verify_camera_error": "Beim überprüfen der Kameraeinstellungen ist ein Fehler aufgetreten",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "Allgemein", "general": "Allgemein",
@@ -186,19 +190,26 @@
"kerberoshub_description_region": "Die Region in der die Aufnahmen gespeichert werden sollen.", "kerberoshub_description_region": "Die Region in der die Aufnahmen gespeichert werden sollen.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "Der Bucket in dem die Aufnahmen gespeichert werden.", "kerberoshub_description_bucket": "Der Bucket in dem die Aufnahmen gespeichert werden.",
"kerberoshub_username": "Benutzername/Verzeichnis", "kerberoshub_username": "Benutzername/Verzeichnis (should match Kerberos Hub username)",
"kerberoshub_description_username": "Der Benutzername des Kerberos Hub Accounts.", "kerberoshub_description_username": "Der Benutzername des Kerberos Hub Accounts.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "Die Kerberos Vault API URL.", "kerberosvault_description_apiurl": "Die Kerberos Vault API URL.",
"kerberosvault_provider": "Anbieter", "kerberosvault_provider": "Anbieter",
"kerberosvault_description_provider": "Der Anbieter zu dem die Aufnahmen gesendet werden.", "kerberosvault_description_provider": "Der Anbieter zu dem die Aufnahmen gesendet werden.",
"kerberosvault_directory": "Verzeichnis", "kerberosvault_directory": "Verzeichnis (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Das Uneterverzeichnis in dem die Aufnahmen gespeichert werden sollen.", "kerberosvault_description_directory": "Das Uneterverzeichnis in dem die Aufnahmen gespeichert werden sollen.",
"kerberosvault_accesskey": "Zugriffsschlüssel", "kerberosvault_accesskey": "Zugriffsschlüssel",
"kerberosvault_description_accesskey": "Der Zugriffsschlüssel des Kerberos Vault Accounts..", "kerberosvault_description_accesskey": "Der Zugriffsschlüssel des Kerberos Vault Accounts..",
"kerberosvault_secretkey": "Geheimer Schlüssel", "kerberosvault_secretkey": "Geheimer Schlüssel",
"kerberosvault_description_secretkey": "Der geheime Schlüssel des Kerberos Vault Accounts.", "kerberosvault_description_secretkey": "Der geheime Schlüssel des Kerberos Vault Accounts.",
"verify_connection": "Verbindung prüfen" "verify_connection": "Verbindung prüfen",
"dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+16 -5
View File
@@ -4,7 +4,8 @@
"configure": "Configure" "configure": "Configure"
}, },
"buttons": { "buttons": {
"save": "Save" "save": "Save",
"verify_connection": "Verify Connection"
}, },
"navigation": { "navigation": {
"profile": "Profile", "profile": "Profile",
@@ -69,7 +70,10 @@
"verify_persistence_error": "Something went wrong while verifying the persistence", "verify_persistence_error": "Something went wrong while verifying the persistence",
"verify_camera": "Verifying your camera settings.", "verify_camera": "Verifying your camera settings.",
"verify_camera_success": "Camera settings are successfully verified.", "verify_camera_success": "Camera settings are successfully verified.",
"verify_camera_error": "Something went wrong while verifying the camera settings" "verify_camera_error": "Something went wrong while verifying the camera settings",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "General", "general": "General",
@@ -186,19 +190,26 @@
"kerberoshub_description_region": "The region we are storing our recordings in.", "kerberoshub_description_region": "The region we are storing our recordings in.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.", "kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
"kerberoshub_username": "Username/Directory", "kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
"kerberoshub_description_username": "The username of your Kerberos Hub account.", "kerberoshub_description_username": "The username of your Kerberos Hub account.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "The Kerberos Vault API", "kerberosvault_description_apiurl": "The Kerberos Vault API",
"kerberosvault_provider": "Provider", "kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "The provider to which your recordings will be send.", "kerberosvault_description_provider": "The provider to which your recordings will be send.",
"kerberosvault_directory": "Directory", "kerberosvault_directory": "Directory (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.", "kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
"kerberosvault_accesskey": "Access key", "kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.", "kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
"kerberosvault_secretkey": "Secret key", "kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.", "kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
"verify_connection": "Verify Connection" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verify Connection",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+16 -5
View File
@@ -4,7 +4,8 @@
"configure": "Configure" "configure": "Configure"
}, },
"buttons": { "buttons": {
"save": "Save" "save": "Save",
"verify_connection": "Verify Connection"
}, },
"navigation": { "navigation": {
"profile": "Perfil", "profile": "Perfil",
@@ -69,7 +70,10 @@
"verify_persistence_error": "Something went wrong while verifying the persistence", "verify_persistence_error": "Something went wrong while verifying the persistence",
"verify_camera": "Verifying your camera settings.", "verify_camera": "Verifying your camera settings.",
"verify_camera_success": "Camera settings are successfully verified.", "verify_camera_success": "Camera settings are successfully verified.",
"verify_camera_error": "Something went wrong while verifying the camera settings" "verify_camera_error": "Something went wrong while verifying the camera settings",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "General", "general": "General",
@@ -186,19 +190,26 @@
"kerberoshub_description_region": "The region we are storing our recordings in.", "kerberoshub_description_region": "The region we are storing our recordings in.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.", "kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
"kerberoshub_username": "Username/Directory", "kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
"kerberoshub_description_username": "The username of your Kerberos Hub account.", "kerberoshub_description_username": "The username of your Kerberos Hub account.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "The Kerberos Vault API", "kerberosvault_description_apiurl": "The Kerberos Vault API",
"kerberosvault_provider": "Provider", "kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "The provider to which your recordings will be send.", "kerberosvault_description_provider": "The provider to which your recordings will be send.",
"kerberosvault_directory": "Directory", "kerberosvault_directory": "Directory (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.", "kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
"kerberosvault_accesskey": "Access key", "kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.", "kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
"kerberosvault_secretkey": "Secret key", "kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.", "kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
"verify_connection": "Verify Connection" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verify Connection",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+14 -4
View File
@@ -69,7 +69,10 @@
"verify_persistence_error": "Quelque chose s'est mal déroulé lors de la vérification de la persistance", "verify_persistence_error": "Quelque chose s'est mal déroulé lors de la vérification de la persistance",
"verify_camera": "Vérifier les paramètres de votre caméra.", "verify_camera": "Vérifier les paramètres de votre caméra.",
"verify_camera_success": "Les paramètres de la caméra sont vérifiés avec succès.", "verify_camera_success": "Les paramètres de la caméra sont vérifiés avec succès.",
"verify_camera_error": "Quelque chose s'est mal déroulé lors de la vérification des paramètres de la caméra" "verify_camera_error": "Quelque chose s'est mal déroulé lors de la vérification des paramètres de la caméra",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "Général", "general": "Général",
@@ -186,19 +189,26 @@
"kerberoshub_description_region": "La région dans laquelle nous stockons vos enregistrements.", "kerberoshub_description_region": "La région dans laquelle nous stockons vos enregistrements.",
"kerberoshub_bucket": "Compartiment", "kerberoshub_bucket": "Compartiment",
"kerberoshub_description_bucket": "Le compartiment dans lequel nous stockons vos enregistrements.", "kerberoshub_description_bucket": "Le compartiment dans lequel nous stockons vos enregistrements.",
"kerberoshub_username": "Utilisateur/Répertoire", "kerberoshub_username": "Utilisateur/Répertoire (should match Kerberos Hub username)",
"kerberoshub_description_username": "Le nom d'utilisateur de votre compte Kerberos Hub.", "kerberoshub_description_username": "Le nom d'utilisateur de votre compte Kerberos Hub.",
"kerberosvault_apiurl": "URL de l'API Kerberos Vault", "kerberosvault_apiurl": "URL de l'API Kerberos Vault",
"kerberosvault_description_apiurl": "L'API Kerberos Vault", "kerberosvault_description_apiurl": "L'API Kerberos Vault",
"kerberosvault_provider": "Fournisseur", "kerberosvault_provider": "Fournisseur",
"kerberosvault_description_provider": "Le fournisseur auquel vos enregistrements seront envoyés.", "kerberosvault_description_provider": "Le fournisseur auquel vos enregistrements seront envoyés.",
"kerberosvault_directory": "Répertoire", "kerberosvault_directory": "Répertoire (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Le sous-répertoire dans lequel les enregistrements seront stockés chez votre fournisseur.", "kerberosvault_description_directory": "Le sous-répertoire dans lequel les enregistrements seront stockés chez votre fournisseur.",
"kerberosvault_accesskey": "Clé d'accès", "kerberosvault_accesskey": "Clé d'accès",
"kerberosvault_description_accesskey": "La clé d'accès de votre compte Kerberos Vault.", "kerberosvault_description_accesskey": "La clé d'accès de votre compte Kerberos Vault.",
"kerberosvault_secretkey": "Clé secrète", "kerberosvault_secretkey": "Clé secrète",
"kerberosvault_description_secretkey": "La clé secrète de votre compte Kerberos Vault.", "kerberosvault_description_secretkey": "La clé secrète de votre compte Kerberos Vault.",
"verify_connection": "Vérifier la connexion" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Vérifier la connexion",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+215
View File
@@ -0,0 +1,215 @@
{
"breadcrumb": {
"watch_recordings": "録画を見る",
"configure": "設定"
},
"buttons": {
"save": "保存",
"verify_connection": "Verify Connection"
},
"navigation": {
"profile": "プロフィール",
"admin": "管理者",
"management": "管理",
"dashboard": "ダッシュボード",
"recordings": "録画",
"settings": "設定",
"help_support": "ヘルプ",
"swagger": "Swagger API",
"documentation": "ドキュメンテーション",
"ui_library": "UI ライブラリ",
"layout": "言語",
"choose_language": "言語を選択"
},
"dashboard": {
"title": "ダッシュボード",
"heading": "ビデオ監視の概要",
"number_of_days": "日数",
"total_recordings": "録画一覧",
"connected": "接続済み",
"not_connected": "接続されていません",
"offline_mode": "オフラインモード",
"latest_events": "最新のイベント",
"configure_connection": "接続の構成",
"no_events": "イベントなし",
"no_events_description": "記録が見つかりません。Kerberos エージェントが正しく構成されていることを確認してください。",
"motion_detected": "モーションが検出されました",
"live_view": "ライブビュー",
"loading_live_view": "ライブビューを読み込んでいます",
"loading_live_view_description": "ライブ ビューをロードしています。お待ちください。",
"time": "時間",
"description": "説明",
"name": "名前"
},
"recordings": {
"title": "録画",
"heading": "すべての録音を 1 か所に",
"search_media": "メディアを検索"
},
"settings": {
"title": "設定",
"heading": "搭載カメラ",
"submenu": {
"all": "全て",
"overview": "概要",
"camera": "カメラ",
"recording": "録音",
"streaming": "ストリーミング",
"conditions": "条件",
"persistence": "持続性"
},
"info": {
"kerberos_hub_demo": "Kerberos Hub のデモ環境を見て、Kerberos Hub の動作を確認してください。",
"configuration_updated_success": "構成が正常に更新されました。",
"configuration_updated_error": "保存中にエラーが発生しました。",
"verify_hub": "Kerberos Hub の設定を確認しています。",
"verify_hub_success": "Kerberos Hub 設定が正常に検証されました。",
"verify_hub_error": "Kerberos Hub の検証中に問題が発生しました",
"verify_persistence": "持続性設定を確認しています。",
"verify_persistence_success": "持続性設定が正常に検証されました。",
"verify_persistence_error": "持続性の検証中に問題が発生しました",
"verify_camera": "カメラの設定を確認しています。",
"verify_camera_success": "カメラの設定が正常に検証されました。",
"verify_camera_error": "カメラ設定の確認中に問題が発生しました",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
},
"overview": {
"general": "全般的",
"description_general": "Kerberos エージェントの一般設定",
"key": "鍵",
"camera_name": "カメラ名",
"timezone": "タイムゾーン",
"select_timezone": "タイムゾーンを選択",
"advanced_configuration": "詳細設定",
"description_advanced_configuration": "Kerberos エージェントの特定の部分を有効または無効にするための詳細な構成オプション",
"offline_mode": "オフラインモード",
"description_offline_mode": "すべての送信トラフィックを無効にする"
},
"camera": {
"camera": "カメラ",
"description_camera": "選択したカメラに接続するには、カメラの設定が必要です。",
"only_h264": "現在、H264 RTSP ストリームのみがサポートされています。",
"rtsp_url": "RTSP URL",
"rtsp_h264": "カメラへの H264 RTSP 接続。",
"sub_rtsp_url": "Sub RTSP url (ライブストリーミングに使用)",
"sub_rtsp_h264": "カメラの低解像度へのセカンダリ RTSP 接続。",
"onvif": "ONVIF",
"description_onvif": "ONVIF 機能と通信するための資格情報",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF ユーザー名",
"onvif_password": "ONVIF パスワード",
"verify_connection": "接続の確認",
"verify_sub_connection": "サブ接続の確認"
},
"recording": {
"recording": "録画",
"description_recording": "録画方法を指定します。",
"continuous_recording": "連続記録",
"description_continuous_recording": "24 時間またはモーション ベースの録画を行います。",
"max_duration": "動画の最大再生時間 (秒)",
"description_max_duration": "録音の最大継続時間。",
"pre_recording": "事前録画 (キー フレームのバッファリング)",
"description_pre_recording": "イベントが発生する数秒前。",
"post_recording": "ポストレコーディング (秒)",
"description_post_recording": "イベントが発生してからの秒数。",
"threshold": "記録閾値(ピクセル)",
"description_threshold": "記録するために変更されたピクセル数",
"autoclean": "オートクリーン",
"description_autoclean": "特定のストレージ容量 (MB) に達したときに、Kerberos エージェントが記録をクリーンアップできるかどうかを指定します。",
"autoclean_enable": "自動クリーニングを有効にする",
"autoclean_description_enable": "容量に達したら、最も古い記録を削除します。",
"autoclean_max_directory_size": "最大ディレクトリ サイズ (MB)",
"autoclean_description_max_directory_size": "保存された録音の最大 MB。",
"fragmentedrecordings": "断片化された録音",
"description_fragmentedrecordings": "録音が断片化されている場合、HLS ストリームに適しています。",
"fragmentedrecordings_enable": "断片化を有効にする",
"fragmentedrecordings_description_enable": "HLS には断片化された録音が必要です。",
"fragmentedrecordings_duration": "フラグメント期間",
"fragmentedrecordings_description_duration": "1 つのフラグメントの持続時間。"
},
"streaming": {
"stun_turn": "WebRTCのSTUN/TURN",
"description_stun_turn": "フル解像度のライブ ストリーミングには、WebRTC の概念を使用します。",
"stun_server": "STUNサーバー",
"turn_server": "TURNサーバー",
"turn_username": "ユーザー名",
"turn_password": "パスワード",
"stun_turn_forward": "転送とトランスコーディング",
"stun_turn_description_forward": "TURN/STUN 通信の最適化と機能強化。",
"stun_turn_webrtc": "WebRTC ブローカーへの転送",
"stun_turn_description_webrtc": "MQTT を介して h264 ストリームを転送する",
"stun_turn_transcode": "トランスコード ストリーム",
"stun_turn_description_transcode": "ストリームを低解像度に変換する",
"stun_turn_downscale": "解像度のダウンスケール (% または元の解像度)",
"mqtt": "MQTT",
"description_mqtt": "それらの通信にはMQTT ブローカーが使用されます。",
"description2_mqtt": "たとえば、ライブストリーミングや ONVIF (PTZ) 機能を実現するために、Kerberos エージェントに送信します。",
"mqtt_brokeruri": "ブローカー URI",
"mqtt_username": "ユーザー名",
"mqtt_password": "パスワード"
},
"conditions": {
"timeofinterest": "特定の時間",
"description_timeofinterest": "特定の時間間隔 (タイムゾーンに基づく) の間のみ録画を行います。",
"timeofinterest_enabled": "有効",
"timeofinterest_description_enabled": "有効にすると、時間枠を指定できます",
"sunday": "日曜日",
"monday": "月曜日",
"tuesday": "火曜日",
"wednesday": "水曜日",
"thursday": "木曜日",
"friday": "金曜日",
"saturday": "土曜日",
"externalcondition": "外部条件",
"description_externalcondition": "外部 Web サービスに応じて、記録を有効または無効にすることができます。",
"regionofinterest": "検出領域",
"description_regionofinterest": "1 つまたは複数の領域を定義すると、定義した領域でのみモーションが追跡されます。"
},
"persistence": {
"kerberoshub": "ケルベロス ハブ",
"description_kerberoshub": "Kerberos エージェントはハートビートを中央に送信できます。",
"description2_kerberoshub": "インストール。",
"persistence": "持続性",
"saasoffering": "Kerberos ハブ (SAAS オファリング)",
"description_persistence": "録音を保存する機能を持つことは、すべての始まりです。",
"description2_persistence": "、またはサードパーティのプロバイダ",
"select_persistence": "永続性を選択",
"kerberoshub_proxyurl": "Kerberos ハブ プロキシ URL",
"kerberoshub_description_proxyurl": "記録をアップロードするためのプロキシ エンドポイント。",
"kerberoshub_apiurl": "ケルベロス ハブ API URL",
"kerberoshub_description_apiurl": "録音をアップロードするための API エンドポイント。",
"kerberoshub_publickey": "公開鍵",
"kerberoshub_description_publickey": "Kerberos Hub アカウントに付与された公開鍵。",
"kerberoshub_privatekey": "秘密鍵",
"kerberoshub_description_privatekey": "Kerberos Hub アカウントに付与された秘密鍵。",
"kerberoshub_site": "サイト",
"kerberoshub_description_site": "Kerberos Hub で Kerberos エージェントが属しているサイト ID。",
"kerberoshub_region": "領域",
"kerberoshub_description_region": "録音を保存しているリージョン。",
"kerberoshub_bucket": "bucket",
"kerberoshub_description_bucket": "録音を保存しているbucket",
"kerberoshub_username": "ユーザー名/ディレクトリ (should match Kerberos Hub username)",
"kerberoshub_description_username": "Kerberos Hub アカウントのユーザー名。",
"kerberosvault_apiurl": "Kerberos ボールト API URL",
"kerberosvault_description_apiurl": "Kerberos ボールト API",
"kerberosvault_provider": "プロバイダ",
"kerberosvault_description_provider": "録音の送信先のプロバイダー。",
"kerberosvault_directory": "ディレクトリ (should match Kerberos Hub username)",
"kerberosvault_description_directory": "録音がプロバイダーに保存されるサブディレクトリ。",
"kerberosvault_accesskey": "アクセスキー",
"kerberosvault_description_accesskey": "Kerberos Vault アカウントのアクセス キー。",
"kerberosvault_secretkey": "秘密鍵",
"kerberosvault_description_secretkey": "Kerberos Vault アカウントの秘密鍵。",
"dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "接続の確認",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
}
}
}
+16 -5
View File
@@ -4,7 +4,8 @@
"configure": "Instellingen" "configure": "Instellingen"
}, },
"buttons": { "buttons": {
"save": "Opslaan" "save": "Opslaan",
"verify_connection": "Verifieer Connectie"
}, },
"navigation": { "navigation": {
"profile": "Profiel", "profile": "Profiel",
@@ -69,7 +70,10 @@
"verify_persistence_error": "Er ging iets fout tijdens het controleren van de opslag instellingen", "verify_persistence_error": "Er ging iets fout tijdens het controleren van de opslag instellingen",
"verify_camera": "We controleren de camera instellingen.", "verify_camera": "We controleren de camera instellingen.",
"verify_camera_success": "De camera instellingen zijn gevalideerd.", "verify_camera_success": "De camera instellingen zijn gevalideerd.",
"verify_camera_error": "Er ging iets mis met de camera instellingen." "verify_camera_error": "Er ging iets mis met de camera instellingen.",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "Algemeen", "general": "Algemeen",
@@ -187,19 +191,26 @@
"kerberoshub_description_region": "De regio waar jouw opnames worden opgeslagen.", "kerberoshub_description_region": "De regio waar jouw opnames worden opgeslagen.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "De bucket opslag locatie", "kerberoshub_description_bucket": "De bucket opslag locatie",
"kerberoshub_username": "Gebruikersnaam/Map", "kerberoshub_username": "Gebruikersnaam/Map (moet overeenkomen met de Kerberos Hub username)",
"kerberoshub_description_username": "De gebruikersnaam van jouw Kerberos Hub account.", "kerberoshub_description_username": "De gebruikersnaam van jouw Kerberos Hub account.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "De Kerberos Vault API", "kerberosvault_description_apiurl": "De Kerberos Vault API",
"kerberosvault_provider": "Provider", "kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "De provider verantwoordelijk voor de opnames op te slaan.", "kerberosvault_description_provider": "De provider verantwoordelijk voor de opnames op te slaan.",
"kerberosvault_directory": "Map", "kerberosvault_directory": "Map (moet overeenkomen met de Kerberos Hub username)",
"kerberosvault_description_directory": "Sub map waarin de opnames worden opgeslagen.", "kerberosvault_description_directory": "Sub map waarin de opnames worden opgeslagen.",
"kerberosvault_accesskey": "Access key", "kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "De access key van jouw Kerberos Vault account.", "kerberosvault_description_accesskey": "De access key van jouw Kerberos Vault account.",
"kerberosvault_secretkey": "Secret key", "kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "De secret key van jouw Kerberos Vault account.", "kerberosvault_description_secretkey": "De secret key van jouw Kerberos Vault account.",
"verify_connection": "Verifieer verbinding" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verifieer verbinding",
"remove_after_upload": "Wanneer opnames opgeslagen zijn, kan je ze verwijderen in de lokale Kerberos Agent.",
"remove_after_upload_description": "Verwijder opnames wanneer ze zijn geupload naar een opslag locatie.",
"remove_after_upload_enabled": "Activeer verwijderen na uploaden"
} }
} }
} }
+16 -5
View File
@@ -4,7 +4,8 @@
"configure": "Configure" "configure": "Configure"
}, },
"buttons": { "buttons": {
"save": "Save" "save": "Save",
"verify_connection": "Verify Connection"
}, },
"navigation": { "navigation": {
"profile": "Profil", "profile": "Profil",
@@ -69,7 +70,10 @@
"verify_persistence_error": "Something went wrong while verifying the persistence", "verify_persistence_error": "Something went wrong while verifying the persistence",
"verify_camera": "Verifying your camera settings.", "verify_camera": "Verifying your camera settings.",
"verify_camera_success": "Camera settings are successfully verified.", "verify_camera_success": "Camera settings are successfully verified.",
"verify_camera_error": "Something went wrong while verifying the camera settings" "verify_camera_error": "Something went wrong while verifying the camera settings",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "General", "general": "General",
@@ -186,19 +190,26 @@
"kerberoshub_description_region": "The region we are storing our recordings in.", "kerberoshub_description_region": "The region we are storing our recordings in.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.", "kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
"kerberoshub_username": "Username/Directory", "kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
"kerberoshub_description_username": "The username of your Kerberos Hub account.", "kerberoshub_description_username": "The username of your Kerberos Hub account.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "The Kerberos Vault API", "kerberosvault_description_apiurl": "The Kerberos Vault API",
"kerberosvault_provider": "Provider", "kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "The provider to which your recordings will be send.", "kerberosvault_description_provider": "The provider to which your recordings will be send.",
"kerberosvault_directory": "Directory", "kerberosvault_directory": "Directory (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.", "kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
"kerberosvault_accesskey": "Access key", "kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.", "kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
"kerberosvault_secretkey": "Secret key", "kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.", "kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
"verify_connection": "Verify Connection" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verify Connection",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+168 -157
View File
@@ -1,10 +1,11 @@
{ {
"breadcrumb": { "breadcrumb": {
"watch_recordings": "Watch recordings", "watch_recordings": "Assistir gravações",
"configure": "Configure" "configure": "Configurar"
}, },
"buttons": { "buttons": {
"save": "Save" "save": "Salvar",
"verify_connection": "Verify Connection"
}, },
"navigation": { "navigation": {
"profile": "Perfil", "profile": "Perfil",
@@ -12,7 +13,7 @@
"management": "Gestão", "management": "Gestão",
"dashboard": "Painel", "dashboard": "Painel",
"recordings": "Gravações", "recordings": "Gravações",
"settings": "Definições", "settings": "Configurações",
"help_support": "Ajuda e suporte", "help_support": "Ajuda e suporte",
"swagger": "Swagger API", "swagger": "Swagger API",
"documentation": "Documentação", "documentation": "Documentação",
@@ -21,184 +22,194 @@
"choose_language": "Escolha o seu idioma" "choose_language": "Escolha o seu idioma"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Painel",
"heading": "Overview of your video surveilance", "heading": "Visão geral de sua videovigilância",
"number_of_days": "Number of days", "number_of_days": "Número de dias",
"total_recordings": "Total recordings", "total_recordings": "Total de gravações",
"connected": "Connected", "connected": "Conectado",
"not_connected": "Not connected", "not_connected": "Desconectado",
"offline_mode": "Offline mode", "offline_mode": "Modo Offline",
"latest_events": "Latest events", "latest_events": "Ultimos eventos",
"configure_connection": "Configure connection", "configure_connection": "Configurar conexão",
"no_events": "No events", "no_events": "Nenhum evento",
"no_events_description": "No recordings where found, make sure your Kerberos Agent is properly configured.", "no_events_description": "Nenhuma gravação foi encontrada, certifique-se de que seu Kerberos Agent esteja configurado corretamente.",
"motion_detected": "Motion was detected", "motion_detected": "Movimento detectado",
"live_view": "Live view", "live_view": "Visualização ao vivo",
"loading_live_view": "Loading live view", "loading_live_view": "Carregando visualização ao vivo",
"loading_live_view_description": "Hold on we are loading your live view here. If you didn't configure your camera connection, update it on the settings pages.", "loading_live_view_description": "Aguarde, estamos carregando sua visualização ao vivo. Se você não configurou a conexão de sua câmera, atualize-a nas páginas de configurações.",
"time": "Time", "time": "Time",
"description": "Description", "description": "Descrição",
"name": "Name" "name": "Nome"
}, },
"recordings": { "recordings": {
"title": "Recordings", "title": "Gravações",
"heading": "All your recordings in a single place", "heading": "Todas as suas gravações em um único lugar",
"search_media": "Search media" "search_media": "Pesquisar mídia"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Configurações",
"heading": "Onboard your camera", "heading": "Integre sua câmera",
"submenu": { "submenu": {
"all": "All", "all": "Todas",
"overview": "Overview", "overview": "Visão geral",
"camera": "Camera", "camera": "Câmera",
"recording": "Recording", "recording": "Gravação",
"streaming": "Streaming", "streaming": "Transmissão",
"conditions": "Conditions", "conditions": "Condições",
"persistence": "Persistence" "persistence": "Armazenamento"
}, },
"info": { "info": {
"kerberos_hub_demo": "Have a look at our Kerberos Hub demo environment, to see Kerberos Hub in action!", "kerberos_hub_demo": "Dê uma olhada em nosso ambiente de demonstração do Kerberos Hub para ver o Kerberos Hub em ação!",
"configuration_updated_success": "Your configuration have been updated successfully.", "configuration_updated_success": "Sua configuração foi atualizada com sucesso.",
"configuration_updated_error": "Something went wrong while saving.", "configuration_updated_error": "Ocorreu um erro durante o salvamento.",
"verify_hub": "Verifying your Kerberos Hub settings.", "verify_hub": "Verificando as configurações do Kerberos Hub.",
"verify_hub_success": "Kerberos Hub settings are successfully verified.", "verify_hub_success": "As configurações do Kerberos Hub foram verificadas com sucesso.",
"verify_hub_error": "Something went wrong while verifying Kerberos Hub", "verify_hub_error": "Algo deu errado ao verificar o Kerberos Hub",
"verify_persistence": "Verifying your persistence settings.", "verify_persistence": "Verificando suas configurações de armazenamento.",
"verify_persistence_success": "Persistence settings are successfully verified.", "verify_persistence_success": "As configurações de armazenamento foram verificadas com sucesso.",
"verify_persistence_error": "Something went wrong while verifying the persistence", "verify_persistence_error": "Algo deu errado ao verificar o armazenamento",
"verify_camera": "Verifying your camera settings.", "verify_camera": "Verificando as configurações de sua câmera.",
"verify_camera_success": "Camera settings are successfully verified.", "verify_camera_success": "As configurações da câmera foram verificadas com sucesso.",
"verify_camera_error": "Something went wrong while verifying the camera settings" "verify_camera_error": "Algo deu errado ao verificar as configurações da câmera.",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
}, },
"overview": { "overview": {
"general": "General", "general": "Geral",
"description_general": "General settings for your Kerberos Agent", "description_general": "Configurações gerais para seu agente Kerberos",
"key": "Key", "key": "Chave",
"camera_name": "Camera name", "camera_name": "Nome da câmera",
"timezone": "Timezone", "timezone": "Fuso horário",
"select_timezone": "Select a timezone", "select_timezone": "Selecione a timezone",
"advanced_configuration": "Advanced configuration", "advanced_configuration": "Configurações avançadas",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent", "description_advanced_configuration": "Opções de configuração detalhadas para habilitar ou desabilitar partes específicas do Kerberos Agent",
"offline_mode": "Offline mode", "offline_mode": "Modo Offline",
"description_offline_mode": "Disable all outgoing traffic" "description_offline_mode": "Desative todo o tráfego de saída"
}, },
"camera": { "camera": {
"camera": "Camera", "camera": "Câmera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.", "description_camera": "As configurações da câmera são necessárias para fazer uma conexão com a câmera de sua escolha.",
"only_h264": "Currently only H264 RTSP streams are supported.", "only_h264": "Atualmente, apenas streams H264 RTSP são suportados.",
"rtsp_url": "RTSP url", "rtsp_url": "Url RTSP",
"rtsp_h264": "A H264 RTSP connection to your camera.", "rtsp_h264": "Uma conexão H264 RTSP para sua câmera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)", "sub_rtsp_url": "Sub RTSP URL(usado para transmissão ao vivo)",
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.", "sub_rtsp_h264": "Uma conexão RTSP secundária para a baixa resolução de sua câmera.",
"onvif": "ONVIF", "onvif": "ONVIF",
"description_onvif": "Credentials to communicate with ONVIF capabilities. These are used for PTZ or other capabilities provided by the camera.", "description_onvif": "Credenciais para se comunicar com recursos ONVIF. Eles são usados para PTZ ou outros recursos fornecidos pela câmera.",
"onvif_xaddr": "ONVIF xaddr", "onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF username", "onvif_username": "ONVIF usuario (username)",
"onvif_password": "ONVIF password", "onvif_password": "ONVIF senha (password)",
"verify_connection": "Verify Connection", "verify_connection": "Verificar conexão",
"verify_sub_connection": "Verify Sub Connection" "verify_sub_connection": "Verificar subconexão"
}, },
"recording": { "recording": {
"recording": "Recording", "recording": "Gravação",
"description_recording": "Specify how you would like to make recordings. Having a continuous 24/7 setup or a motion based recording.", "description_recording": "Especifique como você gostaria de fazer gravações. Gravar o tempo todo (24/7) ou baseada na detecção de movimento.",
"continuous_recording": "Continuous recording", "continuous_recording": "Gravação contínua",
"description_continuous_recording": "Make 24/7 or motion based recordings.", "description_continuous_recording": "Faça gravações 24/7 ou baseadas na detecção de movimento.",
"max_duration": "max video duration (seconds)", "max_duration": "duração máxima do vídeo (segundos)",
"description_max_duration": "The maximum duration of a recording.", "description_max_duration": "A duração máxima de uma gravação.",
"pre_recording": "pre recording (key frames buffered)", "pre_recording": "pré-gravação (key frames em buffer)",
"description_pre_recording": "Seconds before an event occurred.", "description_pre_recording": "Segundos antes de um evento ocorrer.",
"post_recording": "post recording (seconds)", "post_recording": "pós gravação (segundos)",
"description_post_recording": "Seconds after an event occurred.", "description_post_recording": "Segundos após a ocorrência de um evento.",
"threshold": "Recording threshold (pixels)", "threshold": "Limite de gravação (pixels)",
"description_threshold": "The number of pixels changed to record", "description_threshold": "O número de pixels alterados para gravar",
"autoclean": "Auto clean", "autoclean": "Limpeza automática",
"description_autoclean": "Specify if the Kerberos Agent can cleanup recordings when a specific storage capacity (MB) is reached. This will remove the oldest recordings when the capacity is reached.", "description_autoclean": "Especifique se o Agente Kerberos pode limpar gravações quando uma capacidade de armazenamento específica (MB) é atingida. Isso removerá as gravações mais antigas quando a capacidade for atingida.",
"autoclean_enable": "Enable auto clean", "autoclean_enable": "Ativar limpeza automática",
"autoclean_description_enable": "Remove oldest recording when capacity reached.", "autoclean_description_enable": "Remova a gravação mais antiga quando a capacidade for atingida.",
"autoclean_max_directory_size": "Maximum directory size (MB)", "autoclean_max_directory_size": "Tamanho máximo do diretório (MB)",
"autoclean_description_max_directory_size": "The maximum MB's of recordings stored.", "autoclean_description_max_directory_size": "O máximo de MB de gravações armazenadas.",
"fragmentedrecordings": "Fragmented recordings", "fragmentedrecordings": "Gravações Fragmentadas",
"description_fragmentedrecordings": "When recordings are fragmented they are suitable for an HLS stream. When turned on the MP4 container will look a bit different.", "description_fragmentedrecordings": "Quando as gravações são fragmentadas, elas são adequadas para um fluxo HLS. Quando ativado, o contêiner MP4 parecerá um pouco diferente.",
"fragmentedrecordings_enable": "Enable fragmentation", "fragmentedrecordings_enable": "Ativar fragmentação",
"fragmentedrecordings_description_enable": "Fragmented recordings are required for HLS.", "fragmentedrecordings_description_enable": "Gravações fragmentadas são necessárias para HLS.",
"fragmentedrecordings_duration": "fragment duration", "fragmentedrecordings_duration": "Duração do fragmento",
"fragmentedrecordings_description_duration": "Duration of a single fragment." "fragmentedrecordings_description_duration": "Duração de um único fragmento."
}, },
"streaming": { "streaming": {
"stun_turn": "STUN/TURN for WebRTC", "stun_turn": "STUN/TURN para WebRTC",
"description_stun_turn": "For full-resolution livestreaming we use the concept of WebRTC. One of the key capabilities is the ICE-candidate feature, which allows NAT traversal using the concepts of STUN/TURN.", "description_stun_turn": "Para transmissão ao vivo de resolução total, usamos o conceito de WebRTC. Um dos principais recursos é o recurso ICE-candidate, que permite a travessia de NAT usando os conceitos de STUN/TURN.",
"stun_server": "STUN server", "stun_server": "Servidor STUN",
"turn_server": "TURN server", "turn_server": "Servidor TURN",
"turn_username": "Username", "turn_username": "Usuario",
"turn_password": "Password", "turn_password": "Senha",
"stun_turn_forward": "Forwarding and transcoding", "stun_turn_forward": "Encaminhamento e transcodificação",
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.", "stun_turn_description_forward": "Otimizações e melhorias para a comunicação TURN/STUN.",
"stun_turn_webrtc": "Forwarding to WebRTC broker", "stun_turn_webrtc": "Encaminhamento para broker WebRTC",
"stun_turn_description_webrtc": "Forward h264 stream through MQTT", "stun_turn_description_webrtc": "Encaminhar stream h264 através do MQTT",
"stun_turn_transcode": "Transcode stream", "stun_turn_transcode": "Transcodificar stream",
"stun_turn_description_transcode": "Convert stream to a lower resolution", "stun_turn_description_transcode": "Converter stream para uma resolução mais baixa",
"stun_turn_downscale": "Downscale resolution (in % or original resolution)", "stun_turn_downscale": "Resolução reduzida (em % ou resolução original)",
"mqtt": "MQTT", "mqtt": "MQTT",
"description_mqtt": "A MQTT broker is used to communicate from", "description_mqtt": "Um broker MQTT é usado para se comunicar de",
"description2_mqtt": "to the Kerberos Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.", "description2_mqtt": "para o Kerberos Agent, para obter, por exemplo, recursos de transmissão ao vivo ou ONVIF (PTZ).",
"mqtt_brokeruri": "Broker Uri", "mqtt_brokeruri": "Url Broker",
"mqtt_username": "Username", "mqtt_username": "Usuario",
"mqtt_password": "Password" "mqtt_password": "Senha"
}, },
"conditions": { "conditions": {
"timeofinterest": "Time Of Interest", "timeofinterest": "Tempo de interesse",
"description_timeofinterest": "Only make recordings between specific time intervals (based on timezone).", "description_timeofinterest": "Apenas faça gravações entre intervalos de tempo específicos (com base no fuso horário).",
"timeofinterest_enabled": "Enabled", "timeofinterest_enabled": "Habilitado",
"timeofinterest_description_enabled": "If enabled you can specify time windows", "timeofinterest_description_enabled": "Se ativado, você pode especificar os intervalos de tempo",
"sunday": "Sunday", "sunday": "Domingo",
"monday": "Monday", "monday": "Segunda",
"tuesday": "Tuesday", "tuesday": "Terça",
"wednesday": "Wednesday", "wednesday": "Quarta",
"thursday": "Thursday", "thursday": "Quinta",
"friday": "Friday", "friday": "Sexta",
"saturday": "Saturday", "saturday": "Sabado",
"externalcondition": "External Condition", "externalcondition": "Condição Externa",
"description_externalcondition": "Depending on an external webservice recording can be enabled or disabled.", "description_externalcondition": "Dependendo de um serviço de web(API) externo, a gravação pode ser habilitada ou desabilitada.",
"regionofinterest": "Region Of Interest", "regionofinterest": "Região de interesse",
"description_regionofinterest": "By defining one or more regions, motion will be tracked only in the regions you have defined." "description_regionofinterest": "Ao definir uma ou mais regiões, o movimento será rastreado apenas nas regiões definidas por você."
}, },
"persistence": { "persistence": {
"kerberoshub": "Kerberos Hub", "kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Kerberos Agents can send heartbeats to a central", "description_kerberoshub": "Agentes Kerberos podem enviar sinais constantes para a central",
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Kerberos Hub to show realtime information about your video landscape.", "description2_kerberoshub": "instalação. Os sinais e outras informações relevantes são sincronizadas com o Kerberos Hub para mostrar informações em tempo real sobre seu cenário de vídeo.",
"persistence": "Persistence", "persistence": "Armazenamento",
"saasoffering": "Kerberos Hub (SAAS offering)", "saasoffering": "Kerberos Hub (oferta SAAS)",
"description_persistence": "Having the ability to store your recordings is the beginning of everything. You can choose between our", "description_persistence": "Ter a capacidade de armazenar suas gravações é o começo de tudo. Você pode escolher entre nossos",
"description2_persistence": ", or a 3rd party provider", "description2_persistence": ", ou um provedor terceirizado",
"select_persistence": "Select a persistence", "select_persistence": "Selecione um provedor de armazenamento",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL", "kerberoshub_proxyurl": "Url proxy para Kerberos Hub",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.", "kerberoshub_description_proxyurl": "O endpoint Proxy para enviar suas gravações.",
"kerberoshub_apiurl": "Kerberos Hub API URL", "kerberoshub_apiurl": "Url de API do Kerberos Hub",
"kerberoshub_description_apiurl": "The API endpoint for uploading your recordings.", "kerberoshub_description_apiurl": "O endpoint da API para enviar suas gravações.",
"kerberoshub_publickey": "Public key", "kerberoshub_publickey": "Chave pública (Public key)",
"kerberoshub_description_publickey": "The public key granted to your Kerberos Hub account.", "kerberoshub_description_publickey": "A chave pública concedida à sua conta do Kerberos Hub.",
"kerberoshub_privatekey": "Private key", "kerberoshub_privatekey": "Chave privada (Private key)",
"kerberoshub_description_privatekey": "The private key granted to your Kerberos Hub account.", "kerberoshub_description_privatekey": "A chave privada concedida à sua conta do Kerberos Hub.",
"kerberoshub_site": "Site", "kerberoshub_site": "Local",
"kerberoshub_description_site": "The site ID the Kerberos Agents are belonging to in Kerberos Hub.", "kerberoshub_description_site": "O ID do local ao qual os Agentes Kerberos pertencem no Hub Kerberos.",
"kerberoshub_region": "Region", "kerberoshub_region": "Região",
"kerberoshub_description_region": "The region we are storing our recordings in.", "kerberoshub_description_region": "A região em que estamos armazenando nossas gravações.",
"kerberoshub_bucket": "Bucket", "kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.", "kerberoshub_description_bucket": "O bucket no qual estamos armazenando nossas gravações.",
"kerberoshub_username": "Username/Directory", "kerberoshub_username": "Nome de usuário/diretório (should match Kerberos Hub username)",
"kerberoshub_description_username": "The username of your Kerberos Hub account.", "kerberoshub_description_username": "O nome de usuário da sua conta do Kerberos Hub.",
"kerberosvault_apiurl": "Kerberos Vault API URL", "kerberosvault_apiurl": "Url da API do Kerberos Vault",
"kerberosvault_description_apiurl": "The Kerberos Vault API", "kerberosvault_description_apiurl": "a API Kerberos Vault",
"kerberosvault_provider": "Provider", "kerberosvault_provider": "Provedor",
"kerberosvault_description_provider": "The provider to which your recordings will be send.", "kerberosvault_description_provider": "O provedor para o qual suas gravações serão enviadas.",
"kerberosvault_directory": "Directory", "kerberosvault_directory": "Diretório (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.", "kerberosvault_description_directory": "Subdiretório as gravações serão armazenadas em seu provedor.",
"kerberosvault_accesskey": "Access key", "kerberosvault_accesskey": "Chave de acesso(Access key)",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.", "kerberosvault_description_accesskey": "A chave de acesso da sua conta do Kerberos Vault.",
"kerberosvault_secretkey": "Secret key", "kerberosvault_secretkey": "Chave secreta(Secret key)",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.", "kerberosvault_description_secretkey": "A chave secreta da sua conta do Kerberos Vault.",
"verify_connection": "Verify Connection" "dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verificar conexão",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
} }
} }
} }
+208
View File
@@ -0,0 +1,208 @@
{
"breadcrumb": {
"watch_recordings": "观看录像",
"configure": "配置"
},
"buttons": {
"save": "保存",
"verify_connection": "Verify Connection"
},
"navigation": {
"profile": "配置文件",
"admin": "admin",
"management": "管理",
"dashboard": "仪表盘",
"recordings": "录像",
"settings": "设置",
"help_support": "帮助与支持",
"swagger": "Swagger API",
"documentation": "文档",
"ui_library": "UI 库",
"layout": "语言与布局",
"choose_language": "选择语言"
},
"dashboard": {
"title": "指示板",
"heading": "视频监控概述",
"number_of_days": "天数",
"total_recordings": "总录像数",
"connected": "连接",
"not_connected": "未连接",
"offline_mode": "离线模式",
"latest_events": "最新事件",
"configure_connection": "配置连接",
"no_events": "无事件",
"no_events_description": "没有发现录像,请确保正确配置了 Kerberos Agent。",
"motion_detected": "检测到运动",
"live_view": "实时视图",
"loading_live_view": "加载实时视图",
"loading_live_view_description": "请稍等,我们正在为您加载实时视图。如果您没有配置相机连接,请在设置页面进行更新。",
"time": "时间",
"description": "描述",
"name": "名称"
},
"recordings": {
"title": "录像",
"heading": "您所有的录像都存放在这",
"search_media": "搜索媒体"
},
"settings": {
"title": "设置",
"heading": "载入相机",
"submenu": {
"all": "所有",
"overview": "概述",
"camera": "相机",
"recording": "录像",
"streaming": "流",
"conditions": "条件",
"persistence": "持久化存储"
},
"info": {
"kerberos_hub_demo": "查看我们的 Kerberos Hub 演示环境,了解 Kerberos Hub 的实际运行情况!",
"configuration_updated_success": "您的配置已成功更新",
"configuration_updated_error": "保存时出错",
"verify_hub": "验证您的 Kerberos Hub 设置",
"verify_hub_success": "Kerberos Hub 设置验证成功",
"verify_hub_error": "验证 Kerberos Hub 时出错",
"verify_persistence": "验证持久化存储设置",
"verify_persistence_success": "持久化存储设置验证成功",
"verify_persistence_error": "验证持久化存储时出错",
"verify_camera": "验证您的相机设置",
"verify_camera_success": "相机设置验证成功",
"verify_camera_error": "验证相机设置时出错",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
},
"overview": {
"general": "常规",
"description_general": "Kerberos Agent 常规设置",
"key": "Key",
"camera_name": "相机名称",
"timezone": "时区",
"select_timezone": "选择时区",
"advanced_configuration": "高级配置",
"description_advanced_configuration": "启用或禁用 Kerberos Agent 特定部分详细配置选项",
"offline_mode": "离线模式",
"description_offline_mode": "禁用所有传出流量"
},
"camera": {
"camera": "相机",
"description_camera": "需要相机设置才能连接到您选择的相机。",
"only_h264": "目前仅支持 H264 RTSP 流。",
"rtsp_url": "RTSP 网址",
"rtsp_h264": "与摄像机的 H264 RTSP 连接。",
"sub_rtsp_url": "子 RTSP 网址(用于直播)",
"sub_rtsp_h264": "与低分辨率相机的辅助 RTSP 连接。",
"onvif": "ONVIF",
"description_onvif": "验证连接凭证以与 ONVIF 功能进行通信。这些用于 PTZ 或相机提供的其他功能。",
"onvif_xaddr": "ONVIF XADDR",
"onvif_username": "ONVIF 账户",
"onvif_password": "ONVIF 密码",
"verify_connection": "验证连接",
"verify_sub_connection": "验证子连接"
},
"recording": {
"recording": "录像",
"description_recording": "指定录制方式。具有连续的 24/7 全天候设置 或 基于运动的录制。",
"continuous_recording": "连续录制",
"description_continuous_recording": "进行 24/7 全天候 或 基于运动 的录制。",
"max_duration": "最长视频时长(秒)",
"description_max_duration": "录制的最长持续时间",
"pre_recording": "预录制(缓冲关键帧)",
"description_pre_recording": "事件发生前的秒数",
"post_recording": "录制后(秒)",
"description_post_recording": "事件发生后的秒数",
"threshold": "录制阈值(像素)",
"description_threshold": "录制改变的像素数",
"autoclean": "自动清理",
"description_autoclean": "指定 Kerberos Agent 代理是否可以在达到特定存储容量(MB)时清理录制文件。这将在达到容量时删除最旧的录制文件。",
"autoclean_enable": "启用自动清理",
"autoclean_description_enable": "启用自动清理",
"autoclean_max_directory_size": "最大目录大小(MB)",
"autoclean_description_max_directory_size": "存储录像最大的 MB 数",
"fragmentedrecordings": "碎片录制",
"description_fragmentedrecordings": "当录制文件碎片化时,它们适用于 HLS 流。打开 MP4 容器时,看起来会有点不同。",
"fragmentedrecordings_enable": "启用碎片",
"fragmentedrecordings_description_enable": "HLS 需要分段录制。",
"fragmentedrecordings_duration": "片段持续时间",
"fragmentedrecordings_description_duration": "单个片段的持续时间"
},
"streaming": {
"stun_turn": "WebRTC 的 STUN/TURN",
"description_stun_turn": "对于全分辨率直播,我们使用 WebRTC 的概念。其中一个关键功能是 ICE 候选功能,它允许使用 STUN/TURN 的概念进行 NAT 遍历。",
"stun_server": "STUN 服务",
"turn_server": "TURN 服务",
"turn_username": "账户",
"turn_password": "密码",
"stun_turn_forward": "转发和转码",
"stun_turn_description_forward": "TURN/STUN 通信的优化和增强。",
"stun_turn_webrtc": "转发到 WebRTC 代理",
"stun_turn_description_webrtc": "通过 MQTT 转发 h264 流",
"stun_turn_transcode": "转码流",
"stun_turn_description_transcode": "将流转换为较低分辨率",
"stun_turn_downscale": "缩小分辨率(以%或原始分辨率为单位)",
"mqtt": "MQTT",
"description_mqtt": "MQTT 代理用于从",
"description2_mqtt": "到 Kerberos Agent 进行通信, 以实现例如实时流式传输或 ONVIF (PTZ) 功能。",
"mqtt_brokeruri": "代理 URI",
"mqtt_username": "账户",
"mqtt_password": "密码"
},
"conditions": {
"timeofinterest": "兴趣时间",
"description_timeofinterest": "仅在特定时间间隔(基于时区)之间进行录制。",
"timeofinterest_enabled": "启用",
"timeofinterest_description_enabled": "如果启用,您可以指定时间窗口",
"sunday": "周日",
"monday": "周一",
"tuesday": "周二",
"wednesday": "周三",
"thursday": "周四",
"friday": "周五",
"saturday": "周六",
"externalcondition": "外界条件",
"description_externalcondition": "根据外部 Web 服务,可以启用或禁用录制。",
"regionofinterest": "兴趣地区",
"description_regionofinterest": "通过定义一个或多个区域,将仅在您定义的区域中跟踪运动。"
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Kerberos Agents 可以将检测信号发送到中央",
"description2_kerberoshub": "进行安装。检测信号和其他相关信息将同步到 Kerberos Hub,以显示有关视频环境的实时信息。",
"persistence": "持久化存储",
"saasoffering": "Kerberos Hub (SAAS 产品)",
"description_persistence": "能够存储您的录像是一切的开始。您可以在我们的",
"description2_persistence": ", 或第三方提供商之间进行选择。",
"select_persistence": "选择持久化存储",
"kerberoshub_proxyurl": "Kerberos Hub 代理 URL",
"kerberoshub_description_proxyurl": "用于上传您录像的代理端点",
"kerberoshub_apiurl": "Kerberos Hub API URL",
"kerberoshub_description_apiurl": "用于上传您录像的 API 端点",
"kerberoshub_publickey": "公钥",
"kerberoshub_description_publickey": "授予 Kerberos Hub 帐户的公钥",
"kerberoshub_privatekey": "私钥",
"kerberoshub_description_privatekey": "授予 Kerberos Hub 帐户的私钥",
"kerberoshub_site": "站点",
"kerberoshub_description_site": "Kerberos Agents 在 Kerberos Hub 中所属的站点 ID",
"kerberoshub_region": "区域",
"kerberoshub_description_region": "存储录像的区域",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "存储录像的桶",
"kerberoshub_username": "账户/目录 (should match Kerberos Hub username)",
"kerberoshub_description_username": "您的 Kerberos Hub 帐户的用户名",
"kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "Kerberos Vault API",
"kerberosvault_provider": "供应商",
"kerberosvault_description_provider": "您的录像将会被发送到的提供商",
"kerberosvault_directory": "目录 (should match Kerberos Hub username)",
"kerberosvault_description_directory": "录像将存储在提供商中的子目录",
"kerberosvault_accesskey": "访问密钥",
"kerberosvault_description_accesskey": "Kerberos Vault 帐户的访问密钥",
"kerberosvault_secretkey": "密钥",
"kerberosvault_description_secretkey": "Kerberos Vault 帐户的密钥",
"verify_connection": "验证连接"
}
}
}
+121 -99
View File
@@ -38,6 +38,16 @@ class App extends React.Component {
dispatchGetDashboardInformation(); dispatchGetDashboardInformation();
dispatchConnect(); dispatchConnect();
const connectInterval = interval(1000);
this.connectionSubscription = connectInterval.subscribe(() => {
const { connected } = this.props;
if (connected) {
// Already connected
} else {
dispatchConnect();
}
});
const interval$ = interval(5000); const interval$ = interval(5000);
this.subscription = interval$.subscribe(() => { this.subscription = interval$.subscribe(() => {
dispatchGetDashboardInformation(); dispatchGetDashboardInformation();
@@ -64,6 +74,7 @@ class App extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.connectionSubscription.unsubscribe();
const message = { const message = {
client_id: uuid(), client_id: uuid(),
message_type: 'goodbye', message_type: 'goodbye',
@@ -82,111 +93,122 @@ class App extends React.Component {
const { children, username, dashboard, dispatchLogout } = this.props; const { children, username, dashboard, dispatchLogout } = this.props;
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30; const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30;
return ( return (
<div id="page-root"> <>
<Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile> {config.MODE !== 'release' && (
<Profilebar <div className={`environment ${config.MODE}`}>
username={username} Environment: {config.MODE}
email="support@kerberos.io" </div>
userrole={t('navigation.admin')} )}
logout={dispatchLogout} <div id="page-root">
/> <Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile>
<Navigation> <Profilebar
<NavigationSection title={t('navigation.management')} /> username={username}
<NavigationGroup> email="support@kerberos.io"
<NavigationItem userrole={t('navigation.admin')}
title={t('navigation.dashboard')} logout={dispatchLogout}
icon="dashboard" />
link="dashboard" <Navigation>
/> <NavigationSection title={t('navigation.management')} />
<NavigationItem <NavigationGroup>
title={t('navigation.recordings')} <NavigationItem
icon="media" title={t('navigation.dashboard')}
link="media" icon="dashboard"
/> link="dashboard"
<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'}
/> />
</div> <NavigationItem
</NavigationGroup> title={t('navigation.recordings')}
</Navigation> icon="media"
</Sidebar> link="media"
<Main> />
<Gradient /> <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 && ( <NavigationSection title="Websocket" />
<a href="https://app.kerberos.io" target="_blank" rel="noreferrer"> <NavigationGroup>
<div className="cloud-not-installed"> <div className="websocket-badge">
<div> <Badge
<Icon label="cloud" /> title={connected ? 'connected' : 'disconnected'}
Activate Kerberos Hub, and make your cameras and recordings status={connected ? 'success' : 'warning'}
available through a secured cloud! />
</div> </div>
</div> </NavigationGroup>
</a> </Navigation>
)} </Sidebar>
<Main>
<Gradient />
{dashboard.offlineMode === 'true' && ( {!cloudOnline && (
<Link to="/settings"> <a
<div className="offline-mode"> href="https://app.kerberos.io"
<div> target="_blank"
<Icon label="info" /> rel="noreferrer"
Attention! Kerberos is currently running in Offline mode. >
<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>
</div> </a>
</Link> )}
)}
<MainBody>{children}</MainBody> {dashboard.offlineMode === 'true' && (
</Main> <Link to="/settings">
</div> <div className="offline-mode">
<div>
<Icon label="info" />
Attention! Kerberos is currently running in Offline mode.
</div>
</div>
</Link>
)}
<MainBody>{children}</MainBody>
</Main>
</div>
</>
); );
} }
} }
+15
View File
@@ -1,5 +1,20 @@
@import "./app.variables"; @import "./app.variables";
.grid-container {
row-gap: size(2);
}
.main {
padding-top: size(7);
@media (min-width: 800px) {
padding-top: 0;
}
}
.MuiPopover-root {
z-index: 99999999 !important;
}
.offline-mode, .cloud-not-installed { .offline-mode, .cloud-not-installed {
background: var(--upper-gradient); background: var(--upper-gradient);
+23
View File
@@ -1,6 +1,7 @@
import { import {
doGetConfig, doGetConfig,
doSaveConfig, doSaveConfig,
doVerifyOnvif,
doVerifyHub, doVerifyHub,
doVerifyPersistence, doVerifyPersistence,
doGetKerberosAgentTags, doGetKerberosAgentTags,
@@ -39,6 +40,28 @@ export const updateRegion = (id, polygon) => {
}; };
}; };
export const verifyOnvif = (config, onSuccess, onError) => {
return (dispatch) => {
doVerifyOnvif(
config,
(data) => {
dispatch({
type: 'VERIFY_ONVIF',
});
if (onSuccess) {
onSuccess(data);
}
},
(error) => {
const { message } = error;
if (onError) {
onError(message);
}
}
);
};
};
export const verifyCamera = (streamType, config, onSuccess, onError) => { export const verifyCamera = (streamType, config, onSuccess, onError) => {
return (dispatch) => { return (dispatch) => {
doVerifyCamera( doVerifyCamera(
+20
View File
@@ -91,6 +91,26 @@ export function doVerifyHub(config, onSuccess, onError) {
}); });
} }
export function doVerifyOnvif(config, onSuccess, onError) {
const endpoint = API.post(`onvif/verify`, {
...config,
});
endpoint
.then((res) => {
if (res.status !== 200) {
throw new Error(res.data);
}
return res.data;
})
.then((data) => {
onSuccess(data);
})
.catch((e) => {
const { data } = e.response;
onError(data);
});
}
export function doVerifyCamera(streamType, config, onSuccess, onError) { export function doVerifyCamera(streamType, config, onSuccess, onError) {
const cameraStreams = { const cameraStreams = {
rtsp: '', rtsp: '',
-8
View File
@@ -46,11 +46,3 @@ export function doCheckIfInstalled(onSuccess, onError) {
} }
}); });
} }
/* export function doAuth(onSuccess, onError) {
}
export function doRefreshToken(onSuccess, onError) {
} */
@@ -22,6 +22,7 @@ const LanguageSelect = () => {
de: { label: 'Deutsch', dir: 'ltr', active: false }, de: { label: 'Deutsch', dir: 'ltr', active: false },
pt: { label: 'Português', dir: 'ltr', active: false }, pt: { label: 'Português', dir: 'ltr', active: false },
es: { label: 'Español', dir: 'ltr', active: false }, es: { label: 'Español', dir: 'ltr', active: false },
ja: { label: '日本', dir: 'rlt', active: false },
}; };
if (!languageMap[selected]) { if (!languageMap[selected]) {
+8 -4
View File
@@ -7,14 +7,17 @@ const websocketprotocol = protocol === 'http:' ? 'ws:' : 'wss:';
const dev = { const dev = {
ENV: 'dev', ENV: 'dev',
// Comment the below lines, when using codespaces or other special DNS names (which you can't control)
HOSTNAME: hostname, HOSTNAME: hostname,
API_URL: `${protocol}//${hostname}:8080/api`, API_URL: `${protocol}//${hostname}:80/api`,
URL: `${protocol}//${hostname}:8080`, URL: `${protocol}//${hostname}:80`,
WS_URL: `${websocketprotocol}//${hostname}:8080/ws`, 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) // 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`, // API_URL: `${protocol}//${externalHost}/api`,
// URL: `${protocol}//${externalHost}`, // URL: `${protocol}//${externalHost}`,
// WS_URL: `${websocketprotocol}//${externalHost}/ws`,
}; };
const prod = { const prod = {
@@ -23,6 +26,7 @@ const prod = {
API_URL: `${protocol}//${host}/api`, API_URL: `${protocol}//${host}/api`,
URL: `${protocol}//${host}`, URL: `${protocol}//${host}`,
WS_URL: `${websocketprotocol}//${host}/ws`, WS_URL: `${websocketprotocol}//${host}/ws`,
MODE: window['env']['mode'],
}; };
const config = process.env.REACT_APP_ENVIRONMENT === 'production' ? prod : dev; const config = process.env.REACT_APP_ENVIRONMENT === 'production' ? prod : dev;
+1 -1
View File
@@ -14,7 +14,7 @@ i18n
escapeValue: false, escapeValue: false,
}, },
load: 'languageOnly', load: 'languageOnly',
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt'], whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja'],
}); });
export default i18n; export default i18n;
+3 -1
View File
@@ -50,7 +50,9 @@ function getAuthState() {
} }
} }
const reduxWebsocketMiddleware = reduxWebsocket(); const reduxWebsocketMiddleware = reduxWebsocket({
reconnectOnClose: true,
});
const store = createStore( const store = createStore(
rootReducer(history), rootReducer(history),
+52
View File
@@ -144,3 +144,55 @@ body {
padding: 0; padding: 0;
font-family: 'Inter', sans-serif; // use regular Inter font by default.. 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;
}
+53 -28
View File
@@ -4,6 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { send } from '@giantmachines/redux-websocket'; import { send } from '@giantmachines/redux-websocket';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { interval } from 'rxjs';
import { import {
Breadcrumb, Breadcrumb,
KPI, KPI,
@@ -34,7 +35,9 @@ class Dashboard extends React.Component {
liveviewLoaded: false, liveviewLoaded: false,
open: false, open: false,
currentRecording: '', currentRecording: '',
initialised: false,
}; };
this.initialiseLiveview = this.initialiseLiveview.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -46,27 +49,11 @@ class Dashboard extends React.Component {
}); });
}); });
} }
this.initialiseLiveview();
const { connected } = this.props;
if (connected === true) {
const { dispatchSend } = this.props;
const message = {
message_type: 'stream-sd',
};
dispatchSend(message);
}
} }
componentDidUpdate(prevProps) { componentDidUpdate() {
const { connected: connectedPrev } = prevProps; this.initialiseLiveview();
const { connected } = this.props;
if (connectedPrev === false && connected === true) {
const { dispatchSend } = this.props;
const message = {
message_type: 'stream-sd',
};
dispatchSend(message);
}
} }
componentWillUnmount() { componentWillUnmount() {
@@ -75,6 +62,9 @@ class Dashboard extends React.Component {
liveview[0].remove(); liveview[0].remove();
} }
if (this.requestStreamSubscription) {
this.requestStreamSubscription.unsubscribe();
}
const { dispatchSend } = this.props; const { dispatchSend } = this.props;
const message = { const message = {
message_type: 'stop-sd', message_type: 'stop-sd',
@@ -93,6 +83,30 @@ class Dashboard extends React.Component {
return Math.round(Date.now() / 1000); 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) { openModal(file) {
this.setState({ this.setState({
open: true, open: true,
@@ -106,17 +120,16 @@ class Dashboard extends React.Component {
// We check if the camera was getting a valid frame // We check if the camera was getting a valid frame
// during the last 5 seconds, otherwise we assume the camera is offline. // during the last 5 seconds, otherwise we assume the camera is offline.
const isCameraOnline = const isCameraOnline = dashboard.cameraOnline;
this.getCurrentTimestamp() - dashboard.cameraOnline < 15;
// We check if a connection is made to Kerberos Hub, or if Offline mode // We check if a connection is made to Kerberos Hub, or if Offline mode
// has been turned on. // has been turned on.
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30; const isCloudOnline = dashboard.cloudOnline;
let cloudConnection = t('dashboard.not_connected'); let cloudConnection = t('dashboard.not_connected');
if (dashboard.offlineMode === 'true') { if (dashboard.offlineMode === 'true') {
cloudConnection = t('dashboard.offline_mode'); cloudConnection = t('dashboard.offline_mode');
} else { } else {
cloudConnection = cloudOnline cloudConnection = isCloudOnline
? t('dashboard.connected') ? t('dashboard.connected')
: t('dashboard.not_connected'); : t('dashboard.not_connected');
} }
@@ -176,7 +189,7 @@ class Dashboard extends React.Component {
subtitle={cloudConnection} subtitle={cloudConnection}
footer="Cloud" footer="Cloud"
icon={ icon={
cloudOnline && dashboard.offlineMode !== 'true' isCloudOnline && dashboard.offlineMode !== 'true'
? 'circle-check-big' ? 'circle-check-big'
: 'circle-cross-big' : 'circle-cross-big'
} }
@@ -220,14 +233,21 @@ class Dashboard extends React.Component {
id="cells1" id="cells1"
bodycells={[ bodycells={[
<> <>
<div className="time"> <div
className="time"
onClick={() =>
this.openModal(
`${config.URL}/file/${event.key}`
)
}
>
<Ellipse status="success" />{' '} <Ellipse status="success" />{' '}
<p data-tip="10m and 5s ago">{event.time}</p> <p data-tip="10m and 5s ago">{event.time}</p>
</div> </div>
</>, </>,
<> <>
<p <p
className="pointer" className="pointer event-description"
onClick={() => onClick={() =>
this.openModal( this.openModal(
`${config.URL}/file/${event.key}` `${config.URL}/file/${event.key}`
@@ -290,7 +310,7 @@ class Dashboard extends React.Component {
</div> </div>
<div> <div>
<h2>{t('dashboard.live_view')}</h2> <h2>{t('dashboard.live_view')}</h2>
{!liveviewLoaded && ( {(!liveviewLoaded || !isCameraOnline) && (
<SetupBox <SetupBox
btnicon="preferences" btnicon="preferences"
btnlabel={t('dashboard.configure_connection')} btnlabel={t('dashboard.configure_connection')}
@@ -300,7 +320,12 @@ class Dashboard extends React.Component {
text={t('dashboard.loading_live_view_description')} text={t('dashboard.loading_live_view_description')}
/> />
)} )}
<div style={{ visibility: liveviewLoaded ? 'visible' : 'hidden' }}> <div
style={{
visibility:
liveviewLoaded && isCameraOnline ? 'visible' : 'hidden',
}}
>
<ImageCard <ImageCard
imageSrc={`data:image/png;base64, ${ imageSrc={`data:image/png;base64, ${
images.length ? images[0] : '' images.length ? images[0] : ''
+9
View File
@@ -1,3 +1,5 @@
@import "../../app.variables";
#dashboard { #dashboard {
hr { hr {
@@ -15,6 +17,13 @@
video { video {
width: 100%; width: 100%;
} }
p.event-description {
display: none;
@media (min-width: 800px) {
display: block;
}
}
a { a {
display: flex; display: flex;
+92 -54
View File
@@ -49,60 +49,98 @@ class Login extends React.Component {
const { loginError, error } = this.props; const { loginError, error } = this.props;
return ( return (
<LandingLayout <>
title="Kerberos Agent" {config.MODE !== 'release' && (
version={config.VERSION} <div className={`environment ${config.MODE}`}>
description="Video surveillance for everyone" Environment: {config.MODE}
> </div>
<section className="login-body"> )}
<Block> <LandingLayout
<form onSubmit={this.handleSubmit} noValidate> title="Kerberos Agent"
<BlockHeader> version={config.VERSION}
<div> description="Video surveillance for everyone"
<Icon label="login" /> <h4>Login</h4> >
</div> <section className="login-body">
</BlockHeader> <Block>
{loginError && ( <form onSubmit={this.handleSubmit} noValidate>
<AlertMessage <BlockHeader>
message={error} <div>
onClick={() => this.hideMessage()} <Icon label="login" /> <h4>Login</h4>
/> </div>
)} </BlockHeader>
<BlockBody> {loginError && (
<Input <AlertMessage
label="username or email" message={error}
placeholder="Your username/email" onClick={() => this.hideMessage()}
readonly={false} />
disabled={false} )}
type="text" <BlockBody>
name="username" {config.MODE === 'demo' && (
iconleft="accounts" <>
/> <Input
<Input label="username or email"
label="password" placeholder="Your username/email"
placeholder="Your password" readonly
readonly={false} disabled={false}
disabled={false} value="root"
type="password" type="text"
name="password" name="username"
iconleft="locked" iconleft="accounts"
iconright="activity" />
seperate <Input
/> label="password"
</BlockBody> placeholder="Your password"
<BlockFooter> readonly
<p /> disabled={false}
<Button value="root"
buttonType="submit" type="password"
type="submit" name="password"
icon="logout" iconleft="locked"
label="Login" iconright="activity"
/> seperate
</BlockFooter> />
</form> </>
</Block> )}
</section>
</LandingLayout> {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>
</>
); );
} }
} }
+2 -15
View File
@@ -3,11 +3,8 @@ import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { import {
Breadcrumb, Breadcrumb,
VideoContainer,
VideoCard, VideoCard,
ControlBar,
Button, Button,
Input,
Modal, Modal,
ModalHeader, ModalHeader,
ModalBody, ModalBody,
@@ -105,17 +102,7 @@ class Media extends React.Component {
</Link> </Link>
</Breadcrumb> </Breadcrumb>
<ControlBar> <div className="stats grid-container --four-columns">
<Input
iconleft="search"
onChange={() => {}}
placeholder={t('recordings.search_media')}
layout="controlbar"
type="text"
/>
</ControlBar>
<VideoContainer cols={4} isVideoWall={false}>
{events.map((event) => ( {events.map((event) => (
<div <div
key={event.key} key={event.key}
@@ -135,7 +122,7 @@ class Media extends React.Component {
/> />
</div> </div>
))} ))}
</VideoContainer> </div>
{open && ( {open && (
<Modal> <Modal>
<ModalHeader <ModalHeader
+168 -43
View File
@@ -27,6 +27,7 @@ import {
updateRegion, updateRegion,
removeRegion, removeRegion,
saveConfig, saveConfig,
verifyOnvif,
verifyCamera, verifyCamera,
verifyHub, verifyHub,
verifyPersistence, verifyPersistence,
@@ -40,6 +41,8 @@ class Settings extends React.Component {
KERBEROS_HUB = 's3'; // @TODO needs to change KERBEROS_HUB = 's3'; // @TODO needs to change
DROPBOX = 'dropbox';
constructor() { constructor() {
super(); super();
this.state = { this.state = {
@@ -61,6 +64,9 @@ class Settings extends React.Component {
verifyCameraSuccess: false, verifyCameraSuccess: false,
verifyCameraError: false, verifyCameraError: false,
verifyCameraMessage: '', verifyCameraMessage: '',
verifyOnvifSuccess: false,
verifyOnvifError: false,
verifyOnvifErrorMessage: '',
loading: false, loading: false,
loadingHub: false, loadingHub: false,
loadingCamera: false, loadingCamera: false,
@@ -74,6 +80,10 @@ class Settings extends React.Component {
label: 'Kerberos Vault', label: 'Kerberos Vault',
value: this.KERBEROS_VAULT, value: this.KERBEROS_VAULT,
}, },
{
label: 'Dropbox',
value: this.DROPBOX,
},
]; ];
this.tags = { this.tags = {
@@ -95,6 +105,7 @@ class Settings extends React.Component {
'gcp', 'gcp',
'aws', 'aws',
'minio', 'minio',
'dropbox',
], ],
}; };
this.timezones = []; this.timezones = [];
@@ -120,6 +131,7 @@ class Settings extends React.Component {
this.onAddRegion = this.onAddRegion.bind(this); this.onAddRegion = this.onAddRegion.bind(this);
this.onUpdateRegion = this.onUpdateRegion.bind(this); this.onUpdateRegion = this.onUpdateRegion.bind(this);
this.onDeleteRegion = this.onDeleteRegion.bind(this); this.onDeleteRegion = this.onDeleteRegion.bind(this);
this.verifyONVIF = this.verifyONVIF.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -217,13 +229,15 @@ class Settings extends React.Component {
calculateTimetable(timetable) { calculateTimetable(timetable) {
this.timetable = timetable; this.timetable = timetable;
for (let i = 0; i < timetable.length; i += 1) { if (this.timetable) {
const time = timetable[i]; for (let i = 0; i < timetable.length; i += 1) {
const { start1, start2, end1, end2 } = time; const time = timetable[i];
this.timetable[i].start1Full = this.convertSecondsToHourMinute(start1); const { start1, start2, end1, end2 } = time;
this.timetable[i].start2Full = this.convertSecondsToHourMinute(start2); this.timetable[i].start1Full = this.convertSecondsToHourMinute(start1);
this.timetable[i].end1Full = this.convertSecondsToHourMinute(end1); this.timetable[i].start2Full = this.convertSecondsToHourMinute(start2);
this.timetable[i].end2Full = this.convertSecondsToHourMinute(end2); this.timetable[i].end1Full = this.convertSecondsToHourMinute(end1);
this.timetable[i].end2Full = this.convertSecondsToHourMinute(end2);
}
} }
} }
@@ -265,6 +279,8 @@ class Settings extends React.Component {
verifyHubError: false, verifyHubError: false,
configSuccess: false, configSuccess: false,
configError: false, configError: false,
verifyOnvifSuccess: false,
verifyOnvifError: false,
}); });
if (config) { if (config) {
@@ -286,6 +302,52 @@ class Settings extends React.Component {
} }
} }
verifyONVIF() {
const { config, dispatchVerifyOnvif } = this.props;
// Get camera configuration (subset of config).
const cameraConfig = {
onvif_xaddr: config.config.capture.ipcamera.onvif_xaddr,
onvif_username: config.config.capture.ipcamera.onvif_username,
onvif_password: config.config.capture.ipcamera.onvif_password,
};
this.setState({
verifyOnvifSuccess: false,
verifyOnvifError: false,
verifyOnvifErrorMessage: '',
verifyCameraSuccess: false,
verifyCameraError: false,
verifyCameraErrorMessage: '',
configSuccess: false,
configError: false,
loadingCamera: false,
loadingOnvif: true,
});
if (config) {
dispatchVerifyOnvif(
cameraConfig,
() => {
this.setState({
verifyOnvifSuccess: true,
verifyOnvifError: false,
verifyOnvifErrorMessage: '',
loadingOnvif: false,
});
},
(error) => {
this.setState({
verifyOnvifSuccess: false,
verifyOnvifError: true,
verifyOnvifErrorMessage: error,
loadingOnvif: false,
});
}
);
}
}
verifyHubSettings() { verifyHubSettings() {
const { config, dispatchVerifyHub } = this.props; const { config, dispatchVerifyHub } = this.props;
if (config) { if (config) {
@@ -308,6 +370,8 @@ class Settings extends React.Component {
verifyCameraSuccess: false, verifyCameraSuccess: false,
verifyCameraError: false, verifyCameraError: false,
verifyCameraErrorMessage: '', verifyCameraErrorMessage: '',
verifyOnvifSuccess: false,
verifyOnvifError: false,
loadingHub: true, loadingHub: true,
}); });
@@ -353,6 +417,8 @@ class Settings extends React.Component {
persistenceError: false, persistenceError: false,
verifyCameraSuccess: false, verifyCameraSuccess: false,
verifyCameraError: false, verifyCameraError: false,
verifyOnvifSuccess: false,
verifyOnvifError: false,
verifyCameraErrorMessage: '', verifyCameraErrorMessage: '',
loading: true, loading: true,
}); });
@@ -393,6 +459,7 @@ class Settings extends React.Component {
this.setState({ this.setState({
configSuccess: false, configSuccess: false,
configError: false, configError: false,
loadingCamera: true,
verifyPersistenceSuccess: false, verifyPersistenceSuccess: false,
verifyPersistenceError: false, verifyPersistenceError: false,
verifyHubSuccess: false, verifyHubSuccess: false,
@@ -401,9 +468,10 @@ class Settings extends React.Component {
verifyCameraSuccess: false, verifyCameraSuccess: false,
verifyCameraError: false, verifyCameraError: false,
verifyCameraErrorMessage: '', verifyCameraErrorMessage: '',
verifyOnvifSuccess: false,
verifyOnvifError: false,
hubSuccess: false, hubSuccess: false,
hubError: false, hubError: false,
loadingCamera: true,
}); });
dispatchVerifyCamera( dispatchVerifyCamera(
@@ -444,6 +512,10 @@ class Settings extends React.Component {
verifyCameraSuccess, verifyCameraSuccess,
verifyCameraError, verifyCameraError,
verifyCameraErrorMessage, verifyCameraErrorMessage,
loadingOnvif,
verifyOnvifSuccess,
verifyOnvifError,
verifyOnvifErrorMessage,
loadingCamera, loadingCamera,
loading, loading,
loadingHub, loadingHub,
@@ -643,10 +715,23 @@ class Settings extends React.Component {
type="alert" type="alert"
message={`${t( message={`${t(
'settings.info.verify_camera_error' 'settings.info.verify_camera_error'
)} :${verifyCameraErrorMessage}`} )}: ${verifyCameraErrorMessage}`}
/> />
)} )}
{loadingOnvif && (
<InfoBar type="loading" message={t('settings.info.verify_onvif')} />
)}
{verifyOnvifSuccess && (
<InfoBar
type="success"
message={t('settings.info.verify_onvif_success')}
/>
)}
{verifyOnvifError && (
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
)}
{loadingHub && ( {loadingHub && (
<InfoBar type="loading" message={t('settings.info.verify_hub')} /> <InfoBar type="loading" message={t('settings.info.verify_hub')} />
)} )}
@@ -1094,7 +1179,7 @@ class Settings extends React.Component {
noPadding noPadding
label={t('settings.camera.onvif_xaddr')} label={t('settings.camera.onvif_xaddr')}
value={config.capture.ipcamera.onvif_xaddr} value={config.capture.ipcamera.onvif_xaddr}
placeholder="http://x.x.x.x/onvif/device_service" placeholder="x.x.x.x:yyyy"
onChange={(value) => onChange={(value) =>
this.onUpdateField( this.onUpdateField(
'capture.ipcamera', 'capture.ipcamera',
@@ -1134,6 +1219,12 @@ class Settings extends React.Component {
/> />
</BlockBody> </BlockBody>
<BlockFooter> <BlockFooter>
<Button
label={t('buttons.verify_connection')}
type="default"
icon="verify"
onClick={this.verifyONVIF}
/>
<Button <Button
label={t('buttons.save')} label={t('buttons.save')}
type="default" type="default"
@@ -1966,6 +2057,32 @@ class Settings extends React.Component {
<h4>{t('settings.persistence.persistence')}</h4> <h4>{t('settings.persistence.persistence')}</h4>
</BlockHeader> </BlockHeader>
<BlockBody> <BlockBody>
<p>{t('settings.persistence.remove_after_upload')}</p>
<div className="toggle-wrapper">
<Toggle
on={config.remove_after_upload === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle(
'',
'remove_after_upload',
event,
config
)
}
/>
<div>
<span>
{t('settings.persistence.remove_after_upload_enabled')}
</span>
<p>
{t(
'settings.persistence.remove_after_upload_description'
)}
</p>
</div>
</div>
<p> <p>
{t('settings.persistence.description_persistence')}{' '} {t('settings.persistence.description_persistence')}{' '}
<a <a
@@ -1997,17 +2114,6 @@ class Settings extends React.Component {
/> />
{config.cloud === this.KERBEROS_HUB && ( {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 <Input
noPadding noPadding
label={t('settings.persistence.kerberoshub_region')} label={t('settings.persistence.kerberoshub_region')}
@@ -2019,28 +2125,6 @@ class Settings extends React.Component {
this.onUpdateField('s3', 'region', value, config.s3) 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 && ( {config.cloud === this.KERBEROS_VAULT && (
@@ -2139,6 +2223,44 @@ class Settings extends React.Component {
/> />
</> </>
)} )}
{config.cloud === this.DROPBOX && (
<>
<Input
noPadding
label={t('settings.persistence.dropbox_directory')}
placeholder={t(
'settings.persistence.dropbox_description_directory'
)}
value={config.dropbox ? config.dropbox.directory : ''}
onChange={(value) =>
this.onUpdateField(
'dropbox',
'directory',
value,
config.dropbox
)
}
/>
<Input
noPadding
label={t('settings.persistence.dropbox_accesstoken')}
placeholder={t(
'settings.persistence.dropbox_description_accesstoken'
)}
value={
config.dropbox ? config.dropbox.access_token : ''
}
onChange={(value) =>
this.onUpdateField(
'dropbox',
'access_token',
value,
config.dropbox
)
}
/>
</>
)}
</BlockBody> </BlockBody>
<BlockFooter> <BlockFooter>
<Button <Button
@@ -2172,6 +2294,8 @@ const mapStateToProps = (state /* , ownProps */) => ({
}); });
const mapDispatchToProps = (dispatch /* , ownProps */) => ({ const mapDispatchToProps = (dispatch /* , ownProps */) => ({
dispatchVerifyOnvif: (config, success, error) =>
dispatch(verifyOnvif(config, success, error)),
dispatchVerifyCamera: (streamType, config, success, error) => dispatchVerifyCamera: (streamType, config, success, error) =>
dispatch(verifyCamera(streamType, config, success, error)), dispatch(verifyCamera(streamType, config, success, error)),
dispatchVerifyHub: (config, success, error) => dispatchVerifyHub: (config, success, error) =>
@@ -2199,6 +2323,7 @@ Settings.propTypes = {
dispatchUpdateRegion: PropTypes.func.isRequired, dispatchUpdateRegion: PropTypes.func.isRequired,
dispatchRemoveRegion: PropTypes.func.isRequired, dispatchRemoveRegion: PropTypes.func.isRequired,
dispatchVerifyCamera: PropTypes.func.isRequired, dispatchVerifyCamera: PropTypes.func.isRequired,
dispatchVerifyOnvif: PropTypes.func.isRequired,
}; };
export default withTranslation()( export default withTranslation()(
-1
View File
@@ -12,7 +12,6 @@
} }
} }
.table-container table { .table-container table {
border-spacing: 0 12px; border-spacing: 0 12px;
width: 100%; width: 100%;
+340 -11
View File
@@ -174,7 +174,7 @@
dependencies: dependencies:
"@babel/types" "^7.18.9" "@babel/types" "^7.18.9"
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6": "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
@@ -1056,6 +1056,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.11" regenerator-runtime "^0.13.11"
"@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3":
version "7.18.10" version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -1206,11 +1213,151 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
"@date-io/core@^2.15.0", "@date-io/core@^2.16.0":
version "2.16.0"
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.16.0.tgz#7871bfc1d9bca9aa35ad444a239505589d0f22f6"
integrity sha512-DYmSzkr+jToahwWrsiRA2/pzMEtz9Bq1euJwoOuYwuwIYXnZFtHajY2E6a1VNVDc9jP8YUXK1BvnZH9mmT19Zg==
"@date-io/date-fns@^2.15.0":
version "2.16.0"
resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.16.0.tgz#bd5e09b6ecb47ee55e593fc3a87e7b2caaa3da40"
integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/dayjs@^2.15.0":
version "2.16.0"
resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.16.0.tgz#0d2c254ad8db1306fdc4b8eda197cb53c9af89dc"
integrity sha512-y5qKyX2j/HG3zMvIxTobYZRGnd1FUW2olZLS0vTj7bEkBQkjd2RO7/FEwDY03Z1geVGlXKnzIATEVBVaGzV4Iw==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/luxon@^2.15.0":
version "2.16.1"
resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.16.1.tgz#b08786614cb58831c729a15807753011e4acb966"
integrity sha512-aeYp5K9PSHV28946pC+9UKUi/xMMYoaGelrpDibZSgHu2VWHXrr7zWLEr+pMPThSs5vt8Ei365PO+84pCm37WQ==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/moment@^2.15.0":
version "2.16.1"
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.16.1.tgz#ec6e0daa486871e0e6412036c6f806842a0eeed4"
integrity sha512-JkxldQxUqZBfZtsaCcCMkm/dmytdyq5pS1RxshCQ4fHhsvP5A7gSqPD22QbVXMcJydi3d3v1Y8BQdUKEuGACZQ==
dependencies:
"@date-io/core" "^2.16.0"
"@emotion/babel-plugin@^11.10.6":
version "11.10.6"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz#a68ee4b019d661d6f37dec4b8903255766925ead"
integrity sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==
dependencies:
"@babel/helper-module-imports" "^7.16.7"
"@babel/runtime" "^7.18.3"
"@emotion/hash" "^0.9.0"
"@emotion/memoize" "^0.8.0"
"@emotion/serialize" "^1.1.1"
babel-plugin-macros "^3.1.0"
convert-source-map "^1.5.0"
escape-string-regexp "^4.0.0"
find-root "^1.1.0"
source-map "^0.5.7"
stylis "4.1.3"
"@emotion/cache@^11.10.5":
version "11.10.5"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12"
integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==
dependencies:
"@emotion/memoize" "^0.8.0"
"@emotion/sheet" "^1.2.1"
"@emotion/utils" "^1.2.0"
"@emotion/weak-memoize" "^0.3.0"
stylis "4.1.3"
"@emotion/hash@^0.8.0": "@emotion/hash@^0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/hash@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7"
integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==
"@emotion/is-prop-valid@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83"
integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==
dependencies:
"@emotion/memoize" "^0.8.0"
"@emotion/memoize@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/react@^11.10.4":
version "11.10.6"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.6.tgz#dbe5e650ab0f3b1d2e592e6ab1e006e75fd9ac11"
integrity sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.10.6"
"@emotion/cache" "^11.10.5"
"@emotion/serialize" "^1.1.1"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
"@emotion/utils" "^1.2.0"
"@emotion/weak-memoize" "^0.3.0"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0"
integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==
dependencies:
"@emotion/hash" "^0.9.0"
"@emotion/memoize" "^0.8.0"
"@emotion/unitless" "^0.8.0"
"@emotion/utils" "^1.2.0"
csstype "^3.0.2"
"@emotion/sheet@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
"@emotion/styled@^11.10.4":
version "11.10.6"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.6.tgz#d886afdc51ef4d66c787ebde848f3cc8b117ebba"
integrity sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.10.6"
"@emotion/is-prop-valid" "^1.2.0"
"@emotion/serialize" "^1.1.1"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
"@emotion/utils" "^1.2.0"
"@emotion/unitless@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
"@emotion/use-insertion-effect-with-fallbacks@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
"@emotion/utils@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
"@emotion/weak-memoize@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
"@eslint/eslintrc@^0.4.3": "@eslint/eslintrc@^0.4.3":
version "0.4.3" version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@@ -1568,14 +1715,20 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@kerberos-io/ui@1.70.0": "@kerberos-io/ui@^1.71.0":
version "1.70.0" version "1.71.0"
resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.70.0.tgz#377f42725cc98d02af43e133cf66f3c10852caf0" resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.71.0.tgz#06914c94e8b0982068d2099acf8158917a511bfc"
integrity sha512-Sq7gxNAXUFtjUNUpKBKogbvnJ03l/oxkf7943I0GpAzI0n+YdWNxlGpShB5HxsiarRsb+HphwZjAZZY+WupUwA== integrity sha512-pHCTn/iQTcQEPoCK82eJHGRn6BgzW3wgV4C+mNqdKOtLTquxL+vh7molEgC66tl3DGf7HyjSNa8LuoxYbt9TEg==
dependencies: dependencies:
"@emotion/react" "^11.10.4"
"@emotion/styled" "^11.10.4"
"@mui/icons-material" "^5.10.3"
"@mui/material" "^5.10.3"
"@mui/x-date-pickers" "^5.0.0-beta.7"
"@svgr/webpack" "^5.5.0" "@svgr/webpack" "^5.5.0"
"@types/react-router-dom" "^5.1.7" "@types/react-router-dom" "^5.1.7"
md5 "^2.3.0" md5 "^2.3.0"
moment "^2.29.4"
react "^17.0.2" react "^17.0.2"
react-dom "^17.0.2" react-dom "^17.0.2"
react-router-dom "^5.2.0" react-router-dom "^5.2.0"
@@ -1664,6 +1817,117 @@
prop-types "^15.7.2" prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0" react-is "^16.8.0 || ^17.0.0"
"@mui/base@5.0.0-alpha.121":
version "5.0.0-alpha.121"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.121.tgz#24a238a32fc0386efcfefaf88d80712a0e00560c"
integrity sha512-8nJRY76UqlJV+q/Yzo0tgGfPWEOa+4N9rjO81fMmcJqP0I6m54hLDXsjvMg4tvelY5eKHXUK6Tb7en+GHfTqZA==
dependencies:
"@babel/runtime" "^7.21.0"
"@emotion/is-prop-valid" "^1.2.0"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.13"
"@popperjs/core" "^2.11.6"
clsx "^1.2.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/core-downloads-tracker@^5.11.13":
version "5.11.13"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.13.tgz#0419a88ae98b3eaff2a633229d23ee04c8868be1"
integrity sha512-lx+GXBR9h/ApZsEP728tl0pyZyuajto+VnBgsoAzw1d5+CbmOo8ZWieKwVUGxZlPT1wMYNUYS5NtKzCli0xYjw==
"@mui/icons-material@^5.10.3":
version "5.11.11"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.11.tgz#d4e01bd405b0dac779f5e060586277f91f3acb6e"
integrity sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==
dependencies:
"@babel/runtime" "^7.21.0"
"@mui/material@^5.10.3":
version "5.11.13"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.11.13.tgz#bbbdc74a78d4a0e6257a7bca3b8fa38ac8ad3915"
integrity sha512-2CnSj43F+159LbGmTLLQs5xbGYMiYlpTByQhP7c7cMX6opbScctBFE1PuyElpAmwW8Ag9ysfZH1d1MFAmJQkjg==
dependencies:
"@babel/runtime" "^7.21.0"
"@mui/base" "5.0.0-alpha.121"
"@mui/core-downloads-tracker" "^5.11.13"
"@mui/system" "^5.11.13"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.13"
"@types/react-transition-group" "^4.4.5"
clsx "^1.2.1"
csstype "^3.1.1"
prop-types "^15.8.1"
react-is "^18.2.0"
react-transition-group "^4.4.5"
"@mui/private-theming@^5.11.13":
version "5.11.13"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.13.tgz#7841acc7e0d85e3aad223b1a0fad11be9349ef01"
integrity sha512-PJnYNKzW5LIx3R+Zsp6WZVPs6w5sEKJ7mgLNnUXuYB1zo5aX71FVLtV7geyPXRcaN2tsoRNK7h444ED0t7cIjA==
dependencies:
"@babel/runtime" "^7.21.0"
"@mui/utils" "^5.11.13"
prop-types "^15.8.1"
"@mui/styled-engine@^5.11.11":
version "5.11.11"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.11.tgz#9084c331fdcff2210ec33adf37f34e94d67202e4"
integrity sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==
dependencies:
"@babel/runtime" "^7.21.0"
"@emotion/cache" "^11.10.5"
csstype "^3.1.1"
prop-types "^15.8.1"
"@mui/system@^5.11.13":
version "5.11.13"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.13.tgz#67941e401d2dde8ac6c078738dbecd193ed9074f"
integrity sha512-OWP0Alp6C8ufnGm9+CZcl3d+OoRXL2PnrRT5ohaMLxvGL9OfNcL2t4JOjMmA0k1UAGd6E/Ygbu5lEPrZSDlvCg==
dependencies:
"@babel/runtime" "^7.21.0"
"@mui/private-theming" "^5.11.13"
"@mui/styled-engine" "^5.11.11"
"@mui/types" "^7.2.3"
"@mui/utils" "^5.11.13"
clsx "^1.2.1"
csstype "^3.1.1"
prop-types "^15.8.1"
"@mui/types@^7.2.3":
version "7.2.3"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
"@mui/utils@^5.10.3", "@mui/utils@^5.11.13":
version "5.11.13"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.13.tgz#8d7317f221e8973200764820fa7f2a622dbc7150"
integrity sha512-5ltA58MM9euOuUcnvwFJqpLdEugc9XFsRR8Gt4zZNb31XzMfSKJPR4eumulyhsOTK1rWf7K4D63NKFPfX0AxqA==
dependencies:
"@babel/runtime" "^7.21.0"
"@types/prop-types" "^15.7.5"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-date-pickers@^5.0.0-beta.7":
version "5.0.20"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.20.tgz#7b4e5b5a214a8095937ba7d82bb82acd6f270d72"
integrity sha512-ERukSeHIoNLbI1C2XRhF9wRhqfsr+Q4B1SAw2ZlU7CWgcG8UBOxgqRKDEOVAIoSWL+DWT6GRuQjOKvj6UXZceA==
dependencies:
"@babel/runtime" "^7.18.9"
"@date-io/core" "^2.15.0"
"@date-io/date-fns" "^2.15.0"
"@date-io/dayjs" "^2.15.0"
"@date-io/luxon" "^2.15.0"
"@date-io/moment" "^2.15.0"
"@mui/utils" "^5.10.3"
"@types/react-transition-group" "^4.4.5"
clsx "^1.2.1"
prop-types "^15.7.2"
react-transition-group "^4.4.5"
rifm "^0.12.1"
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1700,6 +1964,11 @@
schema-utils "^3.0.0" schema-utils "^3.0.0"
source-map "^0.7.3" source-map "^0.7.3"
"@popperjs/core@^2.11.6":
version "2.11.6"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@rollup/plugin-babel@^5.2.0": "@rollup/plugin-babel@^5.2.0":
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@@ -2135,7 +2404,7 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc"
integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==
"@types/prop-types@*": "@types/prop-types@*", "@types/prop-types@^15.7.5":
version "15.7.5" version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
@@ -2155,6 +2424,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-is@^16.7.1 || ^17.0.0":
version "17.0.3"
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20": "@types/react-redux@^7.1.20":
version "7.1.24" version "7.1.24"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
@@ -2182,7 +2458,7 @@
"@types/history" "^4.7.11" "@types/history" "^4.7.11"
"@types/react" "*" "@types/react" "*"
"@types/react-transition-group@^4.2.0": "@types/react-transition-group@^4.2.0", "@types/react-transition-group@^4.4.5":
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
@@ -3256,7 +3532,7 @@ cliui@^7.0.2:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
clsx@^1.0.4: clsx@^1.0.4, clsx@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@@ -3416,6 +3692,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
dependencies: dependencies:
safe-buffer "~5.1.1" safe-buffer "~5.1.1"
convert-source-map@^1.5.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
cookie-signature@1.0.6: cookie-signature@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -3715,6 +3996,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
csstype@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
damerau-levenshtein@^1.0.8: damerau-levenshtein@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -4784,6 +5070,11 @@ find-cache-dir@^3.3.1:
make-dir "^3.0.2" make-dir "^3.0.2"
pkg-dir "^4.1.0" pkg-dir "^4.1.0"
find-root@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
find-up@^3.0.0: find-up@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
@@ -5132,7 +5423,7 @@ history@4.10.1, history@^4.9.0:
tiny-warning "^1.0.0" tiny-warning "^1.0.0"
value-equal "^1.0.1" value-equal "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6755,6 +7046,11 @@ mkdirp@~0.5.1:
dependencies: dependencies:
minimist "^1.2.6" minimist "^1.2.6"
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -8001,7 +8297,7 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^18.0.0: react-is@^18.0.0, react-is@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
@@ -8128,7 +8424,7 @@ react-tooltip@^4.2.21:
prop-types "^15.7.2" prop-types "^15.7.2"
uuid "^7.0.3" uuid "^7.0.3"
react-transition-group@^4.4.0: react-transition-group@^4.4.0, react-transition-group@^4.4.5:
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
@@ -8388,6 +8684,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rifm@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4"
integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -8657,6 +8958,11 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
sha-1@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/sha-1/-/sha-1-0.1.1.tgz#2a39304bf41bbab11dd9efb7474ec25b1a92c257"
integrity sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g==
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -8783,6 +9089,11 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.7.3: source-map@^0.7.3:
version "0.7.4" version "0.7.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
@@ -9006,6 +9317,11 @@ stylehacks@^5.1.0:
browserslist "^4.16.6" browserslist "^4.16.6"
postcss-selector-parser "^6.0.4" postcss-selector-parser "^6.0.4"
stylis@4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
supports-color@^5.3.0: supports-color@^5.3.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -9486,6 +9802,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^7.0.3: uuid@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
@@ -9496,6 +9817,14 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuidv4@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-2.0.0.tgz#3ec764288f9e9c4e40f8027ad309c2c528be2976"
integrity sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q==
dependencies:
sha-1 "0.1.1"
uuid "3.3.2"
v8-compile-cache@^2.0.3: v8-compile-cache@^2.0.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"