Compare commits

..

86 Commits

Author SHA1 Message Date
Brendan LE GLAUNEC 6bb0e0cabe Merge pull request #81 from EtixLabs/fix-entrypoint
v2.0.0: Fix entrypoint ignoring parameters & fix default dict values
2017-09-19 07:53:52 +02:00
Brendan LE GLAUNEC 83f90a9e64 v2.0.0: Fix entrypoint ignoring parameters & fix default dictionary values 2017-09-19 07:49:22 +02:00
Brendan LE GLAUNEC 93b853c130 v2.0.0: Fixes in readme and contributing.md (#77) (#79)
v2.0.0: Remove codacy badge
2017-09-18 22:54:35 +02:00
Brendan LE GLAUNEC 9b2d38450c Fixes in readme and contributing.md (#77) 2017-09-18 22:43:30 +02:00
Brendan LE GLAUNEC 40512f5287 v2.0.0: Cameradar becomes a golang library and cameraccess replaces the old cameradar (#75)
* Better performance
* Better UX
* Lighter docker image
* More control over the features
* Suited for devs
* Better documentation

* No tests yet
2017-09-18 22:07:19 +02:00
Brendan LE GLAUNEC e27dfc8fba Update README.md 2017-06-07 15:12:22 +02:00
Brendan LE GLAUNEC 0e7577ed7c v2.0.0: Rename bruteforce to dictionary attack to avoid confusion (#71) 2017-05-12 10:51:49 +02:00
Brendan LE GLAUNEC b61fe52161 v2.0.0: Rename subnet to target to avoid confusion (#70) 2017-05-09 08:15:50 +02:00
Brendan LE GLAUNEC 5df1776f51 v2.0.0: Update README.md (#69)
Add more precision to the subnetwork (-s) argument to cameradar to improve user experience
2017-05-08 08:25:59 +02:00
Brendan LE GLAUNEC 7f417cdd1a v2.0.0: Rename CONTRIBUTION.md to CONTRIBUTING.md (#66) 2017-05-04 09:48:19 +02:00
Brendan LE GLAUNEC bbe9705cc3 v2.0.0: Add new RTSP routes (#67) 2017-05-04 09:48:05 +02:00
Brendan LE GLAUNEC 375080f960 v2.0.0 : Fixed golang style issues (#65)
In writeResult.go, two error strings began with an uppercase letter and ended with a dot.
See https://github.com/golang/go/wiki/Errors for more information
2017-03-23 15:28:54 +01:00
Brendan LE GLAUNEC 8c2c7123aa v2.0.0 : Updated Codacy badge URL 2017-03-20 10:30:18 +01:00
Brendan LE GLAUNEC 7791c45304 v2.0.0 : Usage of the develop branch unrestricted
I decided that putting a `2.0.0` branch in place would require too much work as it would mean maintaining two versions of Cameradar at the same time. Thus, we will just keep the normal workflow as we used to.
2017-02-01 09:16:45 +01:00
Brendan LE GLAUNEC c92eef6cdd v2.0.0 : Fixed automatic Docker Hub deployment
The previous travis script was only tagging latest and not the
tag of the branch, which resulted in outdated tags on the DockerHub
2017-02-01 09:13:59 +01:00
Brendan LE GLAUNEC c3fcc7a39c v1.1.4 : Release 2017-01-18 13:45:01 +01:00
Brendan LE GLAUNEC fd88e761e2 v1.1.4 : Renamed MySQL table & Updated CONTRIBUTION.md for 2.0.0 2017-01-18 11:10:30 +01:00
Brendan LE GLAUNEC 1af533a1d3 v1.1.4 : Docker image usage made simpler 2016-12-22 09:28:47 +01:00
Brendan LE GLAUNEC 3510b98797 v1.1.4 : Fixed a case in which func tests didnt detect errors 2016-12-21 11:22:53 +01:00
Brendan LE GLAUNEC 1e25be7ca5 v1.1.4 : New contribution doc & updated readme & dictionaries 2016-12-15 12:28:06 +01:00
Brendan LE GLAUNEC 6e06346685 v1.1.4 : Added code quality check & fixed result.json fmt 2016-12-13 14:58:11 +01:00
Brendan LE GLAUNEC 1fc21f0906 v1.1.4 : Output more human readable and no more useless alias for namespace 2016-12-13 14:58:10 +01:00
Brendan LE GLAUNEC 369728f6c3 Put Gitflow back in place 2016-12-02 15:49:58 +01:00
Brendan LE GLAUNEC 6f09f99eb8 Update contribution format guidelines 2016-12-02 11:49:38 +01:00
Brendan LE GLAUNEC cb7531f93f Updated contribution info 2016-12-01 17:13:45 +01:00
Brendan LE GLAUNEC 78c7e17816 Enhanced the latest release badge
Replaced the raw version with one that will get the latest tag automatically and update the badge.
2016-12-01 15:44:26 +01:00
Brendan LE GLAUNEC d98d78cd73 v1.1.3 : Travis functional testing & minor changes 2016-11-23 08:49:19 +01:00
Brendan LE GLAUNEC e9ffb44b45 v1.1.2 : Travis build test integration & changes to docker deployment 2016-11-23 08:31:35 +01:00
Brendan LE GLAUNEC 9f85415d89 Added docker pulls badge to README.md 2016-11-14 13:40:54 +01:00
Brendan LE GLAUNEC c0d890acad Merge pull request #20 from EtixLabs/bugfix/version-1.1.1
v1.1.1 : Fixed functional tests & Multiple bugfixes
2016-11-12 13:13:14 +01:00
Brendan LE GLAUNEC 553524ae43 v1.1.1 : Fixed functional tests & Multiple bugfixes 2016-11-12 12:55:32 +01:00
Brendan LE GLAUNEC f80af6bd58 Merge pull request #18 from EtixLabs/bugfix/remove-unnecessary-null-pointer-checks
v1.1.0 : Removed unnecessary null pointer checks
2016-11-03 16:36:46 +01:00
Brendan LE GLAUNEC 6c127e4cbe v1.1.0 : Removed unnecessary null pointer checks 2016-11-03 16:34:54 +01:00
Brendan LE GLAUNEC c16c8c0aaa v1.1.0 : Docker Hub Readme changes 2016-11-03 08:45:20 +01:00
Brendan LE GLAUNEC be74c3c814 v1.1.0 : Deployment updated & Docker Hub 2016-11-02 14:42:33 +01:00
Brendan LE GLAUNEC 2f93ddd7e5 v1.1.0 : Updated deployment 2016-11-02 14:42:22 +01:00
Brendan LE GLAUNEC 108f869a43 v1.1.0 : Updated package 2016-11-02 08:42:45 +01:00
Brendan LE GLAUNEC 6685f74a90 v1.1.0 : Fixed multithreading & added timeout to ffmpeg 2016-11-02 07:56:03 +01:00
Brendan LE GLAUNEC 37176292d0 Merge branch 'master' of github.com:EtixLabs/cameradar 2016-10-31 10:13:52 +01:00
Brendan LE GLAUNEC a49b8ef481 v1.1.0 : Updated package 2016-10-31 10:02:53 +01:00
Brendan LE GLAUNEC 8e26751247 v1.1.0 : Added GST RTSP standard in cmd line 2016-10-31 10:01:13 +01:00
Brendan LE GLAUNEC 2564943ae7 Merge pull request #15 from EtixLabs/feature/improve-cameradar-ux
Feature/improve cameradar ux last details
2016-10-29 08:59:48 +02:00
Brendan LE GLAUNEC 74b4590758 v1.1.0 : Added package generation to README 2016-10-29 08:57:51 +02:00
Brendan LE GLAUNEC 9e9c1ba5b6 v1.1.0 : Updated package name & added packge generation script 2016-10-29 08:55:14 +02:00
Brendan LE GLAUNEC 9d78e84dc0 Merge pull request #14 from EtixLabs/feature/improve-cameradar-ux
v1.1.0 : Multithreading & UX update
2016-10-28 11:15:36 +02:00
Brendan LE GLAUNEC ecd318d0c2 v1.1.0 : Update Readme & Removed debug logs 2016-10-28 10:53:41 +02:00
Brendan LE GLAUNEC 58b101ed60 v1.1.0 : Multithreading & UX update 2016-10-28 09:50:37 +02:00
Brendan LE GLAUNEC 7e6c501582 Merge branch 'master' of github.com:EtixLabs/cameradar 2016-10-24 14:02:59 +02:00
Brendan LE GLAUNEC d9a221f9c6 Added standard Comelit RTSP URL to dictionary 2016-10-24 14:02:51 +02:00
Brendan LE GLAUNEC c10525b50e Added Cameradar logo to README.md 2016-10-13 15:51:30 +02:00
Brendan LE GLAUNEC 67b118a82e Added Cameradar logo 2016-10-13 15:43:48 +02:00
Brendan LE GLAUNEC 1f5db9baa0 Updated testing binary 2016-09-12 14:28:28 +02:00
Brendan LE GLAUNEC 4ef463d8a9 Merge branch 'master' of github.com:EtixLabs/cameradar 2016-08-31 13:34:01 +02:00
Brendan LE GLAUNEC ae3329bd25 v1.0.5 : Fixed a potential failure in MySQL CM and fixed code 2016-08-31 12:46:21 +02:00
Brendan LE GLAUNEC 46e17bb0ee Create CHANGELOG.md 2016-08-31 10:36:39 +02:00
Brendan LE GLAUNEC 27b296c9d2 v1.0.4 : Fixed nmap package detection 2016-08-31 09:39:08 +02:00
Brendan LE GLAUNEC 006c0139be Merge branch 'master' of github.com:EtixLabs/cameradar 2016-08-30 16:59:08 +02:00
Brendan LE GLAUNEC 63119d3ff3 v1.0.3 : Corrected GStreamer check 2016-08-30 16:58:46 +02:00
Brendan LE GLAUNEC 5859e9c595 Removed forgotten logs 2016-08-26 12:59:46 +02:00
Brendan LE GLAUNEC d0220ceb7f v1.0.2 - Fix issues with MySQL CM 2016-08-24 12:11:54 +02:00
Brendan LE GLAUNEC 064a6ff588 v1.0.1 : Removed useless text from the Readme 2016-07-08 15:05:17 +02:00
Brendan LE GLAUNEC 9a269bfe0e v1.0.1 : Updated to 16.04 & removed boost dependency 2016-07-07 17:47:08 +02:00
Brendan LE GLAUNEC c44b933a83 v 1.0.0 - Changed tag - Updated deployment version 2016-06-21 11:00:35 +02:00
Brendan LE GLAUNEC 1f5e9fc502 v 1.0.0 - Added functionnal testing - Needs Travis integration 2016-06-21 10:53:24 +02:00
Brendan LE GLAUNEC 08231074b9 Update README.md 2016-06-07 12:38:17 +02:00
Brendan LE GLAUNEC c6d801750e Cameradar now waits for MySQL before being deployed 2016-06-03 09:07:07 +02:00
Brendan LE GLAUNEC 2cf49a8db4 Update CMakeLists.txt 2016-06-03 08:49:55 +02:00
Brendan LE GLAUNEC 4fba8a8594 Update README.md 2016-06-02 10:13:48 +02:00
Brendan LE GLAUNEC 76365e3a07 v0.2.2 : Cameradar now supports badly configured cameras 2016-05-27 14:36:52 +02:00
Brendan LE GLAUNEC e6a38af241 Update README.md 2016-05-26 10:52:29 +02:00
Brendan LE GLAUNEC a4ad49c1a7 Quick MySQL docker deployment & code cleaning 2016-05-24 08:56:11 +02:00
Brendan LE GLAUNEC 0ac1046138 MySQL Cache Manager & code cleanup 2016-05-23 21:22:12 +02:00
Brendan LE GLAUNEC eef9c6f562 Deployment / CPack / Docker / Boost / Versionning 2016-05-23 21:20:56 +02:00
Brendan LE GLAUNEC cf18d869e0 JsonCPP should now be downloaded and included properly 2016-05-23 21:19:40 +02:00
Brendan LE GLAUNEC 4017429835 Initial commit 2016-05-23 21:14:59 +02:00
Brendan LE GLAUNEC 615f14d614 Update README.md 2016-05-23 17:14:41 +02:00
Brendan LE GLAUNEC d09b7abea9 Cloning method updated to HTTPS / TODO updated 2016-05-21 00:58:22 +02:00
Brendan LE GLAUNEC fdb146f019 Merge pull request #7 from EtixLabs/feature/docker-deployment
Deployment / CPack / Docker / Boost / Versionning
2016-05-21 00:43:11 +02:00
Brendan LE GLAUNEC 77446189dd Deployment / CPack / Docker / Boost / Versionning 2016-05-21 00:39:14 +02:00
Brendan LE GLAUNEC e4ba477b06 Merge pull request #6 from EtixLabs/feature/docker-deployment
Updated README to add future improvement
2016-05-20 21:37:58 +02:00
Brendan LE GLAUNEC 6ae2608f8e Updated README to add future improvement 2016-05-20 21:36:48 +02:00
Brendan LE GLAUNEC 6908c7bcac Merge pull request #5 from EtixLabs/develop
JsonCPP should now be downloaded and included properly
2016-05-20 21:33:33 +02:00
Brendan LE GLAUNEC adbbe244b0 JsonCPP should now be downloaded and included properly 2016-05-20 21:30:36 +02:00
Brendan LE GLAUNEC 8d6de630a5 Merge pull request #4 from EtixLabs/develop
Dependencies added & README updated
2016-05-20 17:03:43 +02:00
Brendan LE GLAUNEC 9aa86a5c2d Dependencies added & README updated 2016-05-20 17:02:33 +02:00
Brendan LE GLAUNEC 201d7e31c6 Initial commit 2016-05-20 16:13:22 +02:00
58 changed files with 1386 additions and 4470 deletions
-12
View File
@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [ullaakut] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-52
View File
@@ -1,52 +0,0 @@
First, make sure that none of the open and closed issues is about the same issue as you are describing, and make sure to check the frequently asked questions in the README file.
Then, replace the parts of this template that are between <angle brackets> with the data relative to your issue.
**If you're reporting a bug, use the template below. Otherwise, delete this template and write your issue normally.**
## Context
Please select one:
- [ ] I use the docker image `ullaakut/cameradar`
- [ ] I use my own build of the docker image
- [ ] I use the pre-compiled binary
- [ ] I use my own build of the binary
- [ ] None of the above / I don't know
Please select one:
- [ ] I use a specific version: <version tag>
- [ ] I use the latest commit of the master branch
- [ ] I use the latest commit of the develop branch
- [ ] I use a forked version of the repository: <fork URL>
- [ ] I use a specific commit: <commit hash>
## Environment
My operating system:
- [ ] Windows
- [ ] OSX
- [ ] Linux
- [ ] Other
OS version: <version>
OS architecture: <architecture>
## Issue
### What was expected
<expected behavior>
### What happened
<observed behavior>
### Logs
If your issue is with Cameradar's binary or docker image, please run it with `-v` to print verbose logs, and paste them here:
```
<cameradar logs>
```
+11 -5
View File
@@ -1,10 +1,16 @@
# Results
result.json
*.xml
# IDE config
.idea/
.vscode/
# Golang
/bin/*
/pkg/*
# Deps
cpp/deps/jsoncpp/
cpp/deps/mysql-connector/
cpp/deployment/cameradar_*_Release_Linux.tar.gz
# Builds
dist/
# Test
test/cameradartest.conf.json
test/cameradar_*_Debug_Linux.tar.gz
-7
View File
@@ -1,7 +0,0 @@
# https://github.com/golangci/golangci/wiki/Configuration
service:
project-path: github.com/Ullaakut/cameradar
prepare:
- apt-get update && apt-get install -y libcurl4-gnutls-dev
- dep ensure
-43
View File
@@ -1,43 +0,0 @@
project_name: cameradar
dist: dist/cameradar
env:
- GO111MODULE=on
before:
hooks:
- go mod download
builds:
- binary: cameradar
main: ./cmd/cameradar
goos:
- windows
- darwin
- linux
goarch:
- amd64
- 386
- arm
- arm64
goarm:
- 6
- 7
ignore:
- goos: darwin
goarch: 386
changelog:
skip: true
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
archives:
- name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm}}v{{ .Arm }}{{ end }}"
format: tar.gz
format_overrides:
- goos: windows
format: zip
files:
- CHANGELOG.md
+7 -54
View File
@@ -1,65 +1,18 @@
dist: trusty
language: generic
sudo: required
language: go
addons:
apt:
packages:
# needed for the nfpm pipe:
- rpm
# needed for the snap pipe:
- snapd
env:
- GO111MODULE=on
# needed for the snap pipe:
- PATH=/snap/bin:$PATH
services:
- docker
dist: trusty
before_install:
- echo "Testing Docker Hub credentials"
- if [[ "$DOCKER_PASSOWRD" != "" ]]; then docker login -u=$DOCKER_USERNAME -p=$DOCKER_PASSWORD; fi
- docker login -u=$DOCKER_USERNAME -p=$DOCKER_PASSWORD
- echo "Docker Hub credentials are working"
# If I see one day that Travis CI updates their default docker version
# I can remove the lines below. That's why I leave this here :-)
- docker version
- sudo apt-get remove docker docker-engine docker.io
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
- sudo apt-get update
- sudo apt-get install -y docker-ce nmap libcurl4-openssl-dev
- go get github.com/mattn/goveralls
- docker version
install:
- docker build -t cameradar .
script:
# Run unit tests
- GO111MODULE=on go test -v -covermode=count -coverprofile=coverage.out
- GO111MODULE=on $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
# Launch fake cameras to check if cameradar is able to access them
- docker run -d --name="fake_camera_digest" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="admin" -e RTSP_PASSWORD="12345" -e RTSP_AUTHENTICATION_METHOD="digest" -p 8554:8554 ullaakut/rtspatt
- docker run -d --name="fake_camera_basic" -e RTSP_ROUTE="/live.sdp" -e RTSP_USERNAME="root" -e RTSP_PASSWORD="root" -e RTSP_AUTHENTICATION_METHOD="digest" -p 5554:5554 ullaakut/rtspatt
# Launch cameradar on the local machine
- docker run --net=host -t cameradar -t 0.0.0.0 -p 8554,5554 -v > logs.txt
# Gather the logs from the cameras
- docker logs fake_camera_digest > camera_digest_logs.txt
- docker logs fake_camera_basic > camera_basic_logs.txt
# Stop the fake cameras
- docker stop fake_camera_basic
- docker stop fake_camera_digest
# Print logs
- cat camera_digest_logs.txt
- cat camera_basic_logs.txt
- cat logs.txt
- grep "Successful attack" logs.txt || exit 1
- git clean -fd
- docker run cameradar
notifications:
email:
recipients:
- brendan.le-glaunec@epitech.eu
on_success: never
on_failure: always
after_success:
- echo "Test Success - Branch($TRAVIS_BRANCH) Pull Request($TRAVIS_PULL_REQUEST) Tag($TRAVIS_TAG)"
- if [[ "$TRAVIS_BRANCH" == "master" ]]; then echo -e "Push Container to Docker Hub" && docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD && docker tag cameradar $DOCKER_REPO:latest && docker push $DOCKER_REPO; fi
+222
View File
@@ -0,0 +1,222 @@
# Cameradar Changelog
This file lists all versions of the repository and precises all changes.
## v2.0.0
#### Major changes:
* Cameradar is no longer a C++ application but a Golang library
* Cameraccess is a Golang application replacing the former C++ one (the C++ Cameradar image can still be used with the tag `1.1.4`)
* The docker image for Cameraccess is lighter than the one for Cameradar
* The Cameradar golang library enables users to build their own application around camera discovery and attack. Example of applications could be an automatic camera discovery daemon with scheduled scans, a security audit tool to check if CCTV cameras are protected from attacks by being isolated and having strong passwords, etc.
## v1.1.4
#### Minor changes:
* Simplified use of Docker image
* Renamed MySQL table name to be more explicit
* Refactoring of the Golang functional tester done
* The output was made more human readable
* Added automatic code quality checks for pull requests
* Added contribution documentation
* Updated dictionaries to add user suggestions for Chinese cameras
* Enhanced `result.json` file's format
#### Bugfixes:
* Fixed a bug in the functional testing in which if the `result.json` file was not formatted correctly, the test failed but was still considered a success.
## v1.1.3
#### Minor changes:
* Added automatic pushes to DockerHub to the travis integration
* Made travis configuration file better
* Changed the package generation scripts to make them report errors
* Removed old etix_rtsp_server binary from the test folder
#### Bugfixes:
* Fixed an issue that made it mandatory to launch tests at least once so that they can work the second time
* Fixed an issue that made the golang testing tool not compile in the testing script
* Fixed an issue that made the golang testing tool sometimes ignore some tests
* The previous known issue has been investigated and we don't know where it came from. However after a night of testing I have been unable to reproduce it, so I will consider it closed
## v1.1.2
#### Minor changes:
* Added travis integration
* Added default environment value for Docker deployment
* Updated docker image description with new easy usage
* Updated README badges style (replaced flat with square-flat)
* Build last package can now also generate a debug package if given the `Debug` command-line argument
#### Known issues
* There is still the issue with Camera Emulation Server, see the [previous version's patchnote](#v1.1.1) for more information.
## v1.1.1
#### Minor changes:
* Removed unnecessary null pointer checks (thanks to https://github.com/elfring)
* Updated package description
* Removed debug message in CMake build
* Added `/ch01.264` to the URL dictionary in the deployment (Comelit default RTSP URL)
* Updated tests partially (still needs work to make the code cleaner)
* Variable names are now compliant with Golang best practices
* JSON variable names are back to normal
* Functions have been moved in more appropriate source files
* Structure definitions have been moved in more appropriate source files
* Source files have been renamed to be more relevant
* JUnit output now considers each camera as a test case
* JUnit output now contains errors which makes debugging much easier
* Added header files where it was forgotten
#### Bugfixes:
* Fixed an issue where if you loose your internet connection during thumbnail generation, FFMpeg would get stuck forever and thus Cameradar would never finish
* Fixed an issue where multithreading could cause crashes
* Fixed an issue where the routes dictionary was mistaken for the credentials dictionary
* Fixed issues with the golang testing tool
* Fixed automated camera generation
* Fixed docker IP address resolution
#### Known issues:
* There is an issue with Camera Emulation Server that makes it impossible for Cameradar to generate thumbnails, which is why right now the verification of the thumbnails presence is commented and it is assumed correct. It is probably an issue with GST-RTSP-Server but requires investigation.
## v1.1.0
#### Major changes:
* There are more command line options
* Port can now be overridden in the command line
* Target can now be overridden in the command line
* Bruteforce is now multithreaded and will use as many threads as there are discovered cameras
* Thumbnail generation is now multithreaded and will use as many threads as there are discovered cameras
* There are now default configuration values in order to make cameradar easier to use
#### Minor changes:
* The algorithms take external input into account (so that a 3rd party can change the DB to help Cameradar in real-time) and thus check the persistent data at each iteration
* The default log level is now DEBUG instead of INFO
* The attack logs are now INFO instead of DEBUG
* The thumbnail generation logs are now INFO instead of DEBUG
#### Bugs fixed
* Fixed a bug in which the MySQL cache manager would consider a camera with known ids as having a valid path even if it weren't
* Fixed a bug in which TCP RTSP streams would not generate thumbnails
## v1.0.5
* Fixed error in MySQL Cache Manager in which thumbnail generation on valid streams could not be done
* Fixed potential crash in the case the machine running cameradar has no memory left to allocate space for the dynamic cache manager
## v1.0.4
#### Bugs fixed:
* Fixed nmap package detection
## v1.0.3
#### Bugs fixed:
* Corrected GStreamer check
## v1.0.2
#### Bugs fixed:
* Fixed issues in MySQL Cache Manager
#### Minor changes:
* Added useful debug logs
## v1.0.1
### Ubuntu 16.04 Release
#### Major changes:
* The Docker deployment is now done using Ubuntu 16.04 instead of Ubuntu 15.10, so that it uses more recent packages.
#### Minor changes:
* Removed useless dependencies
## v1.0.0
### First production-ready release
#### Major changes:
* Added functional testing
## v0.2.2
After doing some testing on a weirdly configured camera network in a far away Datacenter, I discovered that some Cameras needed a few tweaks to the Cameradar attack method in order to be accessed.
#### Major changes:
* Cameradar can access Cameras that are configured to always send 400 Bad Requests responses
#### Minor changes:
* Changed iterator name from `it` to `stream` in dumb cache manager to improve code readability
#### Bugfixes:
* Cameradar no longer considers a timing out Camera as an accessible stream
## v0.2.1
This package adds fixes the Docker deployment package.
#### Minor changes
* Fixed the Docker deployment package
* Updated README
## v0.2.0
### MySQL Cache Manager Release
This package adds a new cache manager using a MySQL database, that can store the results between mutiple uses.
#### Major changes
* Added a MySQL Cache Manager
#### Minor changes
* Removed legacy code
* Removed boost dependency
* Improved debugging logs
## v0.1.1
### Docker release
This package adds a way to deploy Cameradar using Docker.
#### Major changes
* Added a quick Docker deployment process
* Added automatic dependencies downloading through CMake for the manual installation
* Added CPack packaging for the Docker deployment
#### Minor changes
* Changed recommended cloning method to HTTPS
* Added lots of informations to README.md
## v0.1.0
This package was the first OpenSource version of Cameradar. It contained only a simple cache manager and had some bugs.
+65
View File
@@ -0,0 +1,65 @@
# Cameradar Contribution
This file will give you guidelines on how to contribute if you want to, and will list known contributors to this repo.
If you're not into software development or not into Golang, you can still help. Updating the dictionaries for example, would be a really cool contribution! Just make sure the credentials and routes you add are **default constructor credentials** and not custom credentials.
If you have other cool ideas, feel free to share them with me at [brendan.leglaunec@etixgroup.com](mailto:brendan.leglaunec@etixgroup.com) !
## Version 2.0.0
- *Cameradar* is the name of the Golang library.
- *Cameraccess* is the name of the binary that uses Cameradar to discover and access the cameras.
This quite big refactoring comes from the fact that most users who want to access cameras either want to launch it with the basic cache manager, mostly using the docker image already provided in this repository, or will not use it because it does not integrate into their software solution without sharing their database with Cameradar, which would cause issues with database migrations for example.
Transforming it into a library allows developers to use it directly in their own code exactly as they want, allowing for a greater flexibility. The Cameraccess binary also provides a simple use example as well as maintains the old simple way of using Cameradar for non-developers.
## Workflow
### Branches & issues
When an issue is opened, a branch will be automatically created. If you want to work on this issue, this is the branch you **have** to work on and create your pull request from.
**Always make sure you're not working on the same issue as someone else, by asking on the issue to be assigned to it.**
### Commit names
The name of the commits should always be `v[next version] : [name of the fixed issue]` (ex: `v1.1.4 : Removed unnecessary null pointer checks`), and each PR should only contain one single commit.
When working on your local branch, you can do as many commits as you want, obviously. The most important is that you **squash** your commits before creating your pull request.
In case you're not familiar with squashing, here is a simple way to do it :
+ On your branch, when everything is clean and working, launch `git log` and count the number of commits your branch is ahead from compared to the `develop` branch.
+ Then launch `git rebase -i HEAD~X`, X being the number of commits you want to squash. For example if I had 12 commits on my branch, I will squash all of them by writing `git rebase -i HEAD~12`.
+ This will open a file letting you decide what to do with the commits. You want to keep the first `pick` and write `s` instead of the other ones, s meaning squash.
+ If there are conflicts, you will fix them step by step by following what git tells you, it's pretty straight-forward.
+ If there are no conflicts or if they are resolved, git will let you edit the commit names. Don't forget to comment the commit names of the commits you squashed by adding a `#` character in front of the commit message.
+ Now launch `git log`, you should see only one commit by the name you chose during the rebase.
### Pull Requests
When your pull request is created, GitHub will first check for conflicts, Codacy will check the shell and C++ code's quality and then Travis CI will try to build and launch functional tests of your versions of Cameradar.
If GitHub reports conflicts with the develop branch, you should resolve them by yourself using your git command-line interface. The easiest and cleanest way is to use `git rebase -i origin/develop` and follow git's instructions.
If Codacy reports new issues, they will be added in the comments of the PR to let you know what you should fix.
If Travis CI reports errors, you should be able to view the logs [by clicking here](https://travis-ci.org/EtixLabs/cameradar/builds) and you should fix it. No PR will be merged before all tests are passing correctly.
### Coding guidelines
This part will tell you about what are the general coding guidelines I want to keep on this project.
#### Golang
+ All Golang code has to be formated using `gofmt`
+ Make sure you follow the Golang [best practices](https://golang.org/doc/effective_go.html)
#### Shell scripting
+ Just make sure Codacy does not trigger warnings on your code.
## Contributors
+ **Brendan Le Glaunec** - [@Ullaakut](https://github.com/Ullaakut) - brendan.leglaunec@etixgroup.com : *Original developer & Maintainer*
+ **Jeremy Letang** - [@jeremyletang](https://github.com/jeremyletang) - letang.jeremy@gmail.com : *Idea of the project & Mentorship*

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+15 -30
View File
@@ -1,38 +1,23 @@
# Build stage
FROM golang:alpine AS build-env
FROM golang:alpine
WORKDIR /go/src/github.com/EtixLabs/cameradar/cameraccess
COPY . /go/src/github.com/Ullaakut/cameradar
WORKDIR /go/src/github.com/Ullaakut/cameradar/cmd/cameradar
COPY . /go/src/github.com/EtixLabs/cameradar
RUN apk update && \
apk upgrade && \
apk add nmap nmap-nselibs nmap-scripts \
curl curl-dev \
gcc \
libc-dev \
git \
pkgconfig
ENV GO111MODULE=on
RUN go version
RUN go build -o cameradar
curl-dev \
gcc \
libc-dev \
git \
pkgconfig
# Final stage
FROM alpine
RUN go get github.com/andelf/go-curl
RUN go get github.com/pkg/errors
RUN go get gopkg.in/go-playground/validator.v9
RUN go get github.com/jessevdk/go-flags
RUN go get github.com/fatih/color
# Necessary to install curl v7.64.0-r3.
# Fix for https://github.com/Ullaakut/cameradar/issues/247
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.9/main' >> /etc/apk/repositories
RUN go install
RUN apk --update add --no-cache nmap \
nmap-nselibs \
nmap-scripts \
curl-dev==7.64.0-r5
WORKDIR /app/cameradar
COPY --from=build-env /go/src/github.com/Ullaakut/cameradar/dictionaries/ /app/dictionaries/
COPY --from=build-env /go/src/github.com/Ullaakut/cameradar/cmd/cameradar/ /app/cameradar/
ENV CAMERADAR_CUSTOM_ROUTES="/app/dictionaries/routes"
ENV CAMERADAR_CUSTOM_CREDENTIALS="/app/dictionaries/credentials.json"
ENTRYPOINT ["/app/cameradar/cameradar"]
ENTRYPOINT ["/go/bin/cameraccess"]
+199 -15
View File
@@ -1,17 +1,201 @@
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+127 -211
View File
@@ -1,39 +1,14 @@
# Cameradar
<p align="center">
<img src="images/Cameradar.gif" width="100%"/>
</p>
<p align="center">
<a href="#license">
<img src="https://img.shields.io/badge/license-Apache-blue.svg?style=flat" />
</a>
<a href="https://hub.docker.com/r/ullaakut/cameradar/">
<img src="https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://travis-ci.org/Ullaakut/cameradar">
<img src="https://travis-ci.org/Ullaakut/cameradar.svg?branch=master" />
</a>
<a href='https://coveralls.io/github/Ullaakut/cameradar?branch=master'>
<img src='https://coveralls.io/repos/github/Ullaakut/cameradar/badge.svg?branch=master' alt='Coverage Status' />
</a>
<a href="https://golangci.com/r/github.com/ullaakut/cameradar">
<img src="https://golangci.com/badges/github.com/ullaakut/cameradar.svg" />
</a>
<a href="https://goreportcard.com/report/github.com/ullaakut/cameradar">
<img src="https://goreportcard.com/badge/github.com/ullaakut/cameradar" />
</a>
<a href="https://github.com/ullaakut/cameradar/releases/latest">
<img src="https://img.shields.io/github/release/Ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://godoc.org/github.com/ullaakut/cameradar">
<img src="https://godoc.org/github.com/ullaakut/cameradar?status.svg" />
</a>
</p>
## An RTSP stream access tool that comes with its library
### Cameradar allows you to
[![cameradar License](https://img.shields.io/badge/license-Apache-blue.svg?style=flat)](#license)
[![Docker Pulls](https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat)](https://hub.docker.com/r/ullaakut/cameradar/)
[![Build](https://img.shields.io/travis/EtixLabs/cameradar/master.svg?style=flat)](https://travis-ci.org/EtixLabs/cameradar)
[![Go Report Card](https://goreportcard.com/badge/github.com/EtixLabs/cameradar)](https://goreportcard.com/report/github.com/EtixLabs/cameradar)
[![Latest release](https://img.shields.io/github/release/EtixLabs/cameradar.svg?style=flat)](https://github.com/EtixLabs/cameradar/releases/latest)
#### Cameradar allows you to:
* **Detect open RTSP hosts** on any accessible target host
* Detect which device model is streaming
@@ -41,65 +16,120 @@
* Launch automated dictionary attacks to get the **username and password** of the cameras
* Retrieve a complete and user-friendly report of the results
<p align="center"><img src="images/Cameradar.png" width="250"/></p>
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/Cameradar.png" width="350"/></p>
## Table of content
* [Docker Image](#docker-image)
* [Configuration](#configuration)
* [Output](#output)
* [Check camera access](#check-camera-access)
* [Command-line options](#command-line-options)
* [Contribution](#contribution)
* [Frequently Asked Questions](#frequently-asked-questions)
* [License](#license)
- [Docker Image](#docker-image)
- [Configuration](#configuration)
- [Output](#output)
- [Check camera access](#check-camera-access)
- [Command line options](#command-line-options)
- [Contribution](#contribution)
- [Frequently Asked Questions](#frequently-asked-questions)
- [License](#license)
## Docker Image for Cameradar
<p align="center"><img src="images/CameradarV4.png" width="70%"/></p>
## Docker Image for Cameraccess
Install [docker](https://docs.docker.com/engine/installation/) on your machine, and run the following command:
```bash
docker run -t ullaakut/cameradar -t <target> <other command-line options>
docker run ullaakut/cameradar <command-line options>
```
[See command-line options](#command-line-options).
e.g.: `docker run -t ullaakut/cameradar -t 192.168.100.0/24` will scan the ports 554, 5554 and 8554 of hosts on the 192.168.100.0/24 subnetwork and attack the discovered RTSP streams and will output debug logs.
e.g.: `docker run ullaakut/cameradar -t 192.168.100.0/24 -l` will scan the ports 554 and 8554 of hosts on the 192.168.100.0/24 subnetwork and attack the discovered RTSP streams and will output lots of logs.
* `YOUR_TARGET` can be a subnet (e.g.: `172.16.100.0/24`), an IP (e.g.: `172.16.100.10`), or a range of IPs (e.g.: `172.16.100.10-20`).
* `YOUR_TARGET` can be a subnet (e.g.: `172.16.100.0/24`) or even an IP (e.g.: `172.16.100.10`), a range of IPs (e.g.: `172.16.100.10-172.16.100.20`) or a mix of all those separated by commas (e.g.: `172.17.100.0/24,172.16.100.10-172.16.100.20,0.0.0.0`).
* If you want to get the precise results of the nmap scan in the form of an XML file, you can add `-v /your/path:/tmp/cameradar_scan.xml` to the docker run command, before `ullaakut/cameradar`.
* If you use the `-r` and `-c` options to specify your custom dictionaries, make sure to also use a volume to add them to the docker container. Example: `docker run -t -v /path/to/dictionaries/:/tmp/ ullaakut/cameradar -r /tmp/myroutes -c /tmp/mycredentials.json -t mytarget`
* If you use the `-r` and `-c` options to specify your
## Installing the binary on your machine
### Library
Only use this solution if for some reason using docker is not an option for you or if you want to locally build Cameradar on your machine.
### Dependencies of the library
**WARNING**: Manually building the binary will **NOT WORK** for any camera that uses **DIGEST AUTHENTICATION** [if your version of `curl` is over `7.64.0`](https://github.com/Ullaakut/cameradar/pull/252), which is most likely the case. For more information, see [this response on the subject from the author of curl](https://stackoverflow.com/a/59778142/4145098).
- `curl-dev` / `libcurl` (depending on your OS)
- `nmap`
- `github.com/pkg/errors`
- `gopkg.in/go-playground/validator.v9`
- `github.com/andelf/go-curl`
### Dependencies
#### Installing the library
* `go` (> `1.10`)
* `libcurl` development library (**[version has to be <7.66.0](https://github.com/Ullaakut/cameradar/issues/247)**)
* For apt users: `apt install libcurl4-openssl-dev`
`go get github.com/EtixLabs/cameradar`
### Steps to install
After this command, the *cameradar* library is ready to use. Its source will be in:
1. `go install github.com/Ullaakut/cameradar/v5/cmd/cameradar@latest`
$GOPATH/src/pkg/github.com/EtixLabs/cameradar
The `cameradar` binary is now in your `$GOPATH/bin` ready to be used. See command line options [here](#command-line-options).
You can use `go get -u` to update the package.
## Configuration
Here is an overview of the exposed functions of this library:
The **RTSP port used for most cameras is 554**, so you should probably specify 554 as one of the ports you scan. Not specifying any ports to the cameradar application will scan the 554, 5554 and 8554 ports.
#### Discovery
`docker run -t --net=host ullaakut/cameradar -p "18554,19000-19010" -t localhost` will scan the ports `18554`, and the range of ports between `19000` and `19010` on `localhost`.
You can use the cameradar library for simple discovery purposes if you don't need to access the cameras but just to be aware of their existence.
You **can use your own files for the credentials and routes dictionaries** used to attack the cameras, but the Cameradar repository already gives you a good base that works with most cameras, in the `/dictionaries` folder.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/Discover.png"/></p>
The Discover function calls the RunNmap function as well as the ParseNmapResults function and returns the discovered streams without attempting any attack.
It will use default values for its calls to RunNmap:
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/NmapPresets.png"/></p>
This describes the nmap time presets. You can pass a value between 1 and 5 as described in this table, to the RunNmap function.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/RunNmap.png"/></p>
The RunNmap function will execute nmap and generate an XML file containing the results of the scan.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/ParseNmapResults.png"/></p>
The ParseNmapResult function will open the specified XML file and return all open RTSP streams found within it.
#### Attack
If you already know which hosts and ports you want to attack, you can also skip the discovery part and use directly the attack functions. The attack functions also take a timeout value as a parameter.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/AttackCredentials.png"/></p>
The AttackCredentials function takes valid streams as an input (with IP addresses and ports) and will attempt to guess their credentials using the provided dictionary.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/AttackRoute.png"/></p>
The AttackRoute function takes valid streams as an input (with IP addresses and ports) and will attempt to guess their routes using the provided dictionary.
#### Data models
Here are the different data models useful to use the exposed functions of the cameradar library.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/Models.png"/></p>
#### Dictionary loaders
The cameradar library also provides two functions that take file paths as inputs and return the appropriate data models filled.
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/LoadCredentials.png"/></p>
LoadCredentials takes a JSON file that has the same format as [this one](dictionary/credentials.json).
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/LoadRoutes.png"/></p>
LoadRoutes takes a file that has the same format as [this one](dictionary/routes). Warning: This file is not JSON.
#### Miscellaneous
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/RTSPURL.png"/></p>
RTSPURL allows you to generate the full URL of a stream.
### Configuration
The **RTSP port used for most cameras is 554**, so you should probably specify 554 as one of the ports you scan. Not specifying any ports to the cameraccess application will scan the 554 and 8554 ports.
e.g.: `docker run ullaakut/cameradar -p "18554,19000-19010" -t localhost` will scan the ports 18554, and the range of ports between 19000 and 19010 on localhost.
You **can use your own files for the ids and routes dictionaries** used to attack the cameras, but the Cameradar repository already gives you a good base that works with most cameras, in the `/dictionaries` folder.
e.g.:
```bash
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
docker run -v /my/folder/with/dictionaries:/tmp/dictionaries \
ullaakut/cameradar \
-r "/tmp/dictionaries/my_routes" \
-c "/tmp/dictionaries/my_credentials.json" \
@@ -108,183 +138,69 @@ docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
This will put the contents of your folder containing dictionaries in the docker image and will use it for the dictionary attack instead of the default dictionaries provided in the cameradar repo.
## Output
For each camera, Cameraccess will output this:
<p align="center"><img src="https://raw.githubusercontent.com/EtixLabs/cameradar/master/images/Output.png"/></p>
## Check camera access
If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format: `rtsp://username:password@address:port/route`
If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI or the command-line to connect to the RTSP stream using this format : `rtsp://username:password@address:port/route`
## Command-line options
With the above result, the RTSP URL would be `rtsp://admin:12345@173.16.100.45:554/live.sdp`
* **"-t, --targets"**: Set target. Required. Target can be a file (see [instructions on how to format the file](#format-input-file)), an IP, an IP range, a subnetwork, or a combination of those. Example: `--targets="192.168.1.72,192.168.1.74"`
* **"-p, --ports"**: (Default: `554,5554,8554`) Set custom ports.
* **"-s, --scan-speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. You might also want to keep it low to keep your discovery stealthy. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
* **"-I, --attack-interval"**: (Default: `0ms`) Set custom interval after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
* **"-T, --timeout"**: (Default: `2000ms`) Set custom timeout value after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
* **"-r, --custom-routes"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/routes`) Set custom dictionary path for routes
* **"-c, --custom-credentials"**: (Default: `<CAMERADAR_GOPATH>/dictionaries/credentials.json`) Set custom dictionary path for credentials
## Command line options
* **"-t, --target"**: Set custom target. Required.
* **"-p, --ports"**: (Default: `554,8554`) Set custom ports.
* **"-s, --speed"**: (Default: `4`) Set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a very performant and reliable network. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
* **"-T, --timeout"**: (Default: `1000`) Set custom timeout value in miliseconds after which an attack attempt without an answer should give up.
* **"-r, --custom-routes"**: (Default: `dictionaries/routes`) Set custom dictionary path for routes
* **"-c, --custom-credentials"**: (Default: `dictionaries/credentials.json`) Set custom dictionary path for credentials
* **"-o, --nmap-output"**: (Default: `/tmp/cameradar_scan.xml`) Set custom nmap output path
* **"-d, --debug"**: Enable debug logs
* **"-v, --verbose"**: Enable verbose curl logs (not recommended for most use)
* **"-h"**: Display the usage information
* **"-l, --log"**: Enable debug logs (nmap requests, curl describe requests, etc.)
* **"-h"** : Display the usage information
## Format input file
## Environment variables
The file can contain IPs, hostnames, IP ranges and subnetwork, separated by newlines. Example:
```text
0.0.0.0
localhost
192.17.0.0/16
192.168.1.140-255
192.168.2-3.0-255
```
## Environment Variables
### `CAMERADAR_TARGET`
This variable is mandatory and specifies the target that cameradar should scan and attempt to access RTSP streams on.
Examples:
* `172.16.100.0/24`
* `192.168.1.1`
* `localhost`
* `192.168.1.140-255`
* `192.168.2-3.0-255`
### `CAMERADAR_PORTS`
This variable is optional and allows you to specify the ports on which to run the scans.
Default value: `554,5554,8554`
It is recommended not to change these except if you are certain that cameras have been configured to stream RTSP over a different port. 99.9% of cameras are streaming on these ports.
### `CAMERADAR_NMAP_OUTPUT_FILE`
This variable is optional and allows you to specify on which file nmap will write its output.
Default value: `/tmp/cameradar_scan.xml`
This can be useful only if you want to read the files yourself, if you don't want it to write in your `/tmp` folder, or if you want to use only the RunNmap function in cameradar, and do its parsing manually.
### `CAMERADAR_CUSTOM_ROUTES`, `CAMERADAR_CUSTOM_CREDENTIALS`
These variables are optional, allowing to replace the default dictionaries with custom ones, for the dictionary attack.
Default values: `<CAMERADAR_GOPATH>/dictionaries/routes` and `<CAMERADAR_GOPATH>/dictionaries/credentials.json`
### `CAMERADAR_SCAN_SPEED`
This optional variable allows you to set custom nmap discovery presets to improve speed or accuracy. It's recommended to lower it if you are attempting to scan an unstable and slow network, or to increase it if on a fast and reliable network. See [this for more info on the nmap timing templates](https://nmap.org/book/man-performance.html).
Default value: `4`
### `CAMERADAR_ATTACK_INTERVAL`
This optional variable allows you to set `custom interval` to wait between each attack in order to stay stealthy. It's recommended to increase it when attempting to scan a network that might be protected against bruteforce attacks. By default, there is no interval, in order to make attacks as fast as possible
Default value: `0ms`
### `CAMERADAR_TIMEOUT`
This optional variable allows you to set custom timeout value after which an attack attempt without an answer should give up. It's recommended to increase it when attempting to scan unstable and slow networks or to decrease it on fast and reliable networks.
Default value: `2000ms`
### `CAMERADAR_LOGGING`
This optional variable allows you to enable a more verbose output to have more information about what is going on.
It will output nmap results, cURL requests, etc.
Default: `false`
Not yet implemented.
## Contribution
### Build
#### Docker build
To build the docker image, simply run `docker build . -t cameradar` in the root of the project.
Your image will be called `cameradar` and NOT `ullaakut/cameradar`.
#### Go build
1. `go get github.com/Ullaakut/cameradar`
2. `cd $GOPATH/src/github.com/Ullaakut/cameradar`
3. `cd cmd/cameradar`
4. `go install`
The cameradar binary is now in `$GOPATH/bin/cameradar`.
See [the contribution document](/CONTRIBUTION.md) to get started.
## Frequently Asked Questions
> Cameradar does not detect any camera!
That means that either your cameras are not streaming in RTSP or that they are not on the target you are scanning. In most cases, CCTV cameras will be on a private subnetwork, isolated from the internet. Use the `-t` option to specify your target. If you are sure you did everything right but it still does not work, please open an issue with details on the device you are trying to access 🙏
That means that either your cameras are not streaming in RTSP or that they are not on the target you are scanning. In most cases, CCTV cameras will be on a private subnetwork, isolated from the internet. Use the `-t` option to specify your target.
> Cameradar detects my cameras, but does not manage to access them at all!
Maybe your cameras have been configured, and the credentials / URL have been changed. Cameradar only guesses using default constructor values if a custom dictionary is not provided. You can use your own dictionaries in which you just have to add your credentials and RTSP routes. To do that, see how the [configuration](#configuration) works. Also, maybe your camera's credentials are not yet known, in which case if you find them it would be very nice to add them to the Cameradar dictionaries to help other people in the future.
Maybe your cameras have been configured and the credentials / URL have been changed. Cameradar only guesses using default constructor values if a custom dictionary is not provided. You can use your own dictionaries in which you just have to add your credentials and RTSP routes. To do that, see how the [configuration](#configuration) works. Also, maybe your camera's credentials are not yet known, in which case if you find them it would be very nice to add them to the Cameradar dictionaries to help other people in the future.
> What happened to the C++ version?
You can still find it under the 1.1.4 tag on this repo, however it was slower and less stable than the current version written in Golang. It is not recommended using it.
You can still find it under the 1.1.4 tag on this repo, however it was less performant and stable than the current version written in Golang.
> How to use the Cameradar library for my own project?
See the example in `/cmd/cameradar`. You just need to run `go get github.com/Ullaakut/cameradar` and to use the `cameradar` package in your code. You can find the documentation on [godoc](https://godoc.org/github.com/ullaakut/cameradar).
See the cameraccess example. You just need to run `go get github.com/EtixLabs/cameradar/cameradar` and to use the `cmrdr` package in your code.
> I want to scan my own localhost for some reason, and it does not work! What's going on?
> I want to scan my own localhost for some reason and it does not work! What's going on?
Use the `--net=host` flag when launching the cameradar image, or use the binary by running `go run cameradar/cameradar.go` or [installing it](#go-build).
> I don't see a colored output:(
You forgot the `-t` flag before `ullaakut/cameradar` in your command-line. This tells docker to allocate a pseudo-tty for cameradar, which makes it able to use colors.
> I don't have a camera, but I'd like to try Cameradar!
Simply run `docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt` and then run cameradar, and it should guess that the username is admin and that the password is 12345. You can try this with any default constructor credentials (they can be found [here](dictionaries/credentials.json)).
> What authentication types does Cameradar support?
Cameradar supports both basic and digest authentication.
## Examples
> Running cameradar on your own machine to scan for default ports
`docker run --net=host -t ullaakut/cameradar -t localhost`
> Running cameradar with an input file, logs enabled on port 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t /tmp/test.txt -p 8554`
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554 and 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t 192.168.0.0/24 --custom-credentials="/tmp/dictionaries/credentials.json" --custom-routes="/tmp/dictionaries/routes" -p 554,5554,8554`
Use the `--net=host` flag when launching the cameradar image, or use the binary by running `go run cameraccess/main.go`.
## License
Copyright 2023 Ullaakut
Copyright 2017 Etix Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
See the License for the specific language governing permissions and limitations under the License.
-289
View File
@@ -1,289 +0,0 @@
# Cameradar
<p align="center">
<img src="images/Cameradar.gif" width="100%"/>
</p>
<p align="center">
<a href="#license">
<img src="https://img.shields.io/badge/license-Apache-blue.svg?style=flat" />
</a>
<a href="https://hub.docker.com/r/ullaakut/cameradar/">
<img src="https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://travis-ci.org/Ullaakut/cameradar">
<img src="https://travis-ci.org/Ullaakut/cameradar.svg?branch=master" />
</a>
<a href='https://coveralls.io/github/Ullaakut/cameradar?branch=master'>
<img src='https://coveralls.io/repos/github/Ullaakut/cameradar/badge.svg?branch=master' alt='Coverage Status' />
</a>
<a href="https://golangci.com/r/github.com/ullaakut/cameradar">
<img src="https://golangci.com/badges/github.com/ullaakut/cameradar.svg" />
</a>
<a href="https://goreportcard.com/report/github.com/ullaakut/cameradar">
<img src="https://goreportcard.com/badge/github.com/ullaakut/cameradar" />
</a>
<a href="https://github.com/ullaakut/cameradar/releases/latest">
<img src="https://img.shields.io/github/release/Ullaakut/cameradar.svg?style=flat" />
</a>
<a href="https://godoc.org/github.com/ullaakut/cameradar">
<img src="https://godoc.org/github.com/ullaakut/cameradar?status.svg" />
</a>
</p>
## RTSP stream потоковы протокол со своей библиотекой
### Возможности Cameradar
* **Обнаружить открытые RTSP-хосты** на любом доступном целевом узле.
* Определить, какая модель устройства осуществляет потоковое вещание
* Запускать автоматический перебор по словарю для получения **маршрута потока** (например: `/live.sdp`)
* Запустить автоматические перебор по словарю для получения **имени пользователя и пароля** камер.
* Получить полный и удобный отчет о результатах.
<p align="center"><img src="images/Cameradar.png" width="250"/></p>
## Оглавление
* [Docker Image](#docker-image)
* [Зависмости](#configuration)
* [Вывод](#output)
* [Параметры командной строки](#command-line-options)
* [Вклад](#contribution)
* [Часто задаваемые вопросы](#frequently-asked-questions)
* [Лицензия](#license)
## Docker для Cameradar
<p align="center"><img src="images/CameradarV4.png" width="70%"/></p>
* У
Устоновка [docker](https://docs.docker.com/engine/installation/) на свою машину и запустить команду:
```bash
docker run -t ullaakut/cameradar -t <target> <other command-line options>
```
[Параметры командной строки](#command-line-options).
Пример: `docker run -t ullaakut/cameradar -t 192.168.100.0/24` будет сканировать порты 554, 5554 и 8554 хостов в подсети 192.168.100.0/24 и перебирать обнаруженные RTSP-stream и выводить журнал отладки.
*YOUR_TARGET может быть подсетью (например: 172.16.100.0/24), IP (например: 172.16.100.10) или диапазоном IPs (например: 172.16.100.10-20).
* Если вы хотите получить точные результаты сканирования nmap в виде XML файла, вы можете добавить `-v /your/path:/tmp/cameradar_scan.xml` в команду docker run, перед `ullaakut/cameradar`.
* Если вы используете `-r` и `-c` для указания ваших пользовательских словарей, убедитесь, что вы также используете для добавления их в контейнер docker. Пример: `docker run -t -v /path/to/dictionaries/:/tmp/ ullaakut/cameradar -r /tmp/myroutes -c /tmp/mycredentials.json -t mytarget`.
## Бинарная устоновка
Решение локального Camader без использование docker.
**WARNING**: Бинарная устновка **НЕ БУДЕТ РАБОТАТЬ** для камер, которые используют **DIGEST AUTHENTICATION** [если ваша версия `curl` старше `7.64.0`](https://github.com/Ullaakut/cameradar/pull/252), то скорее всего, так и есть. Дополнильная информация [this response on the subject from the author of curl](https://stackoverflow.com/a/59778142/4145098).
### Зависимости
* `go` (> `1.10`)
* `libcurl` библиотека (**[version has to be <7.66.0](https://github.com/Ullaakut/cameradar/issues/247)**)
* Для apt: `apt install libcurl4-openssl-dev`
### Этапы устоновки
1. `go install github.com/Ullaakut/cameradar/v5/cmd/cameradar@latest`
Бинарный файл `cameradar` находится здесь `$GOPATH/bin` и готов к использованию. Опции командной строки [здесь](#command-line-options).
## Конфигурация
Порт **RTSP, используемый для большинства камер, 554**, поэтому указываем 554 в качестве одного из сканируемых портов. Если не прописать порты в приложении cameradar, оно будет искать порты 554, 5554 и 8554.
`docker run -t --net=host ullaakut/cameradar -p "18554,19000-19010" -t localhost` будет сканировать порты `18554`, и диапазон портов между `19000` и `19010` на `localhost`.
Вы **можете использовать свои собственные файлы для словарей учетных данных и маршрутов**, используемых для атаки на камеры, но репозиторий Cameradar уже предоставляет вам хорошую базу, которая работает с большинством камер, в папке `/dictionaries`.
```bash
docker run -t -v /my/folder/with/dictionaries:/tmp/dictionaries \
ullaakut/cameradar \
-r "/tmp/dictionaries/my_routes" \
-c "/tmp/dictionaries/my_credentials.json" \
-t 172.19.124.0/24
```
Это поместит содержимое вашей папки со словарями в образ докера и будет использовать его для перебора словаря, вместо словарей по умолчанию, предоставленных в репозитории cameradar.
## Доступ камеры
Если у вас есть [VLC Media Playe](http://www.videolan.org/vlc/), у вас есть возможность использовать графический интерфейс или командную строку для подключения к потоку RTSP, для этого используется этот формат `rtsp://username:password@address:port/route`.
## Опции командной строки
* ** "-t, --targets "**: Устанавливаем цель.Целью может быть файл (см. [инструкции по форматированию файла](#format-input-file)), IP, диапазон IP, подсеть или их комбинация. Пример: `--targets="192.168.1.72,192.168.1.74"`
* ** "-p, --ports "**: (По умолчанию: `554,5554,8554`) Устанавливаем пользовательские порты.
* ** "-s, --scan-speed "**: (По умолчанию: `4`) Установка пользовательских настроек обнаружения nmap для повышения скорости или точности. Рекомендуется уменьшить это значение, если вы пытаетесь сканировать нестабильную и медленную сеть, или увеличить его, если вы работаете в очень производительной и надежной сети. Вы также можете оставить значение низким, чтобы уменшить вероятность обнуружения. Смотрите [здесь более подробная информация о шаблонах синхронизации nmap] (https://nmap.org/book/man-performance.html).
* ** "-I, --attack-interval "**: (По умолчанию: `0ms`) Задает пользовательский интервал, после которого попытка атаки без ответа должна прекратиться. Рекомендуется увеличить его при попытке сканирования нестабильных и медленных сетей или уменьшить в быстрых и надежных сетях.
* ** "-T, --timeout "**: (По умолчанию: `2000 мс`) Задает пользовательское значение тайм-аута, после которого попытка атаки без ответа должна прекратиться. Рекомендуется увеличить это значение при попытке сканирования нестабильных и медленных сетей или уменьшить его в быстрых и надежных сетях.
* ** "-r, --custom-routes "**: (По умолчанию: `<CAMERADAR_GOPATH>/dictionaries/routes`) Устанавливает путь к пользовательскому словарю для маршрутов.
* ** "-c, --custom-credentials "**: (По умолчанию: `<CAMERADAR_GOPATH>/dictionaries/credentials.json`) Устанавливает путь к пользовательскому словарю для учетных данных
* ** "-o, --nmap-output "**: (По умолчанию: `/tmp/cameradar_scan.xml`) устанавливаем путь вывода nmap.
* **"-d, --debug "**: Включаем журналы отладки
* **"-v, --verbose "**: Включаем журналы curl (не рекомендуется для большинства пользователей)
* **"-h "**: Отображаем использованную информацию
## Форматирование входного файла
Файл может содержать IP-адреса, имена хостов, диапазоны IP-адресов и подсети, разделенные на строки. Например:
```text
0.0.0.0
localhost
192.17.0.0/16
192.168.1.140-255
192.168.2-3.0-255
```
## Переменные среды
### `CAMERADAR_TARGET`
Эта переменная обязательная и указывает цель, которую cameradar должен сканировать и пытаться получить доступ к потокам RTSP.
Например:
* `172.16.100.0/24`
* `192.168.1.1`
* `localhost`
* `192.168.1.140-255`
* `192.168.2-3.0-255`
### `CAMERADAR_PORTS`
Эта переменная не обязательная,нужна для того, чтоб указать сканируемые порты
Значение по умолчанию `554,5554,8554`
Рекомендуется не изменять их, кроме случаев, когда вы уверены, что камеры были настроены на передачу RTSP через другой порт. 99,9% камер передают поток через эти порты.
### `CAMERADAR_NMAP_OUTPUT_FILE`
Эта переменная является необязательной и позволяет указать, в какой файл nmap будет записывать свой вывод.
Значение по умолчанию: `/tmp/cameradar_scan.xml`
Это может быть полезно, только для самотятельного чтения файлов, если вы не хотите, чтобы он записывал данные в вашу папку `/tmp`, или если вы хотите использовать только функцию RunNmap в cameradar, и делать ее разбор вручную.
### `CAMERADAR_CUSTOM_ROUTES`, `CAMERADAR_CUSTOM_CREDENTIALS`
Эти переменные являются необязательными и позволяют заменить словари по умолчанию на пользовательские, Это нужно для перебора по словарю.
Значение по умолчанию: `<CAMERADAR_GOPATH>/dictionaries/routes` and `<CAMERADAR_GOPATH>/dictionaries/credentials.json`
### `CAMERADAR_SCAN_SPEED`
Эта опцианальная переменная позволяет установить пользовательские настройки обнаружения nmap для повышения скорости и точности. Рекомендуется уменьшить значение, если вы пытаетесь сканировать нестабильную и медленную сеть, или увеличить, если сеть быстрая и надежная. Дополнительные сведения о временных шаблонах nmap смотреть [здесь](https://nmap.org/book/man-performance.html).
Значение по умолчанию: `4`
### `CAMERADAR_ATTACK_INTERVAL`
Эта не обязательная переменная позволяет установить `custom interval` между каждой атакой, чтобы оставаться незаметным. Рекомендуется увеличивать его при попытке сканирования сети, которая может быть защищена от атак методом перебора. По умолчанию интервал отсутствует, чтобы сделать атаки как можно более быстрыми.
Значение по умолчанию: `0ms`
### `CAMERADAR_TIMEOUT`
Эта необязательная переменная дает возмоджность установить пользовательское значение тайм-аута, по истечении которого попытка атаки без ответа должна прекратиться. Рекомендуется увеличить это значение при попытке сканирования нестабильных и медленных сетей или уменьшить его в быстрых и надежных сетях.
Значение по умолчанию: `2000ms`
### `CAMERADAR_LOGGING`
Эта необязательная переменная позволяет вам включить более расширенный вывод, чтобы иметь больше информации о процессах.
Она будет выводить результаты nmap, запросы cURL и т.д.
Значение по умолчанию: `false`
## Вклад
### Сборка
#### Docker build
Чтобы собрать образ докера, просто выполните команду `docker build . -t cameradar` в корне проекта.
Ваше изображение будет называться `cameradar`, а НЕ `ullaakut/cameradar`.
#### Приступим к сборке
1. `go get github.com/Ullaakut/cameradar`
2. `cd $GOPATH/src/github.com/Ullaakut/cameradar`
3. `cd cmd/cameradar`
4. `go install`
Бинарный файл cameradar теперь находится в `$GOPATH/bin/cameradar`.
## Часто задаваемые вопросы
> Cameradar не находит не одной камеры!
Это означает, что либо ваши камеры не передают поток в RTSP, либо их нет на объекте, который вы сканируете. В большинстве случаев камеры видеонаблюдения находятся в частной подсети, изолированной от Интернета. Используйте опцию `-t` для указания цели. Если вы уверены, что все сделали правильно, но это все равно не работает, пожалуйста, откройте проблему с подробной информацией об устройстве, к которому вы пытаетесь получить доступ.
> Cameradar нашел мои камеры, но не может получить доступ к ним
Возможно, ваши камеры были настроены, и учетные данные / URL были изменены. Cameradar только угадывает, используя значения конструктора по умолчанию, если не предоставлен пользовательский словарь. Вы можете использовать свои собственные словари, в которые нужно просто добавить ваши учетные данные и маршруты RTSP. Для этого посмотрите, как работает [configuration](#configuration). Также, возможно, учетные данные вашей камеры еще не известны, в таком случае, если вы их найдете, было бы очень хорошо добавить их в словари Cameradar, чтобы помочь другим людям в будущем.
> Что случилось с версией на C++?
Вы все еще можете найти его под тегом 1.1.4 в этом репозитории, однако он был медленнее и менее стабилен, чем текущая версия, написанная на Golang. Использовать ее не рекомендуется.
> Как использовать библиотеку Cameradar для моего собственного проекта?
Смотрите пример в `/cmd/cameradar`. Вам просто нужно запустить `go get github.com/Ullaakut/cameradar` и использовать пакет `cameradar` в своем коде. Документацию можно найти на [godoc](https://godoc.org/github.com/ullaakut/cameradar).
> Я почему-то хочу просканировать свой собственный localhost, а он не работает!
Используйте флаг `--net=host` при запуске образа cameradar, или используйте бинарный файл, выполнив команду `go run cameradar/cameradar.go` или [установив его](#go-build).
> Я не вижу цветного вывода :(
Вероятнее вы забыли использовать флаг `-t` перед `ullaakut/cameradar` в вашей командной строке. Это указывается -tty для cameradar, что позволит ему использовать цвета.
> У меня нет камеры, но я хотел бы попробовать Cameradar!
Просто воспользуйтесь командой `docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt` и запустите cameradar, и он должен определить, что имя пользователя - admin, а пароль - 12345. Вы можете попробовать это с любыми учетными данными конструктора по умолчанию (их можно найти [здесь](dictionaries/credentials.json)).
> Какие типы аутентификации поддерживает Cameradar?
Cameradar поддерживает как базовую, так и дайджест-аутентификацию.
## Примеры
>> Запуск cameradar на вашей собственной машине для сканирования портов по умолчанию
`docker run --net=host -t ullaakut/cameradar -t localhost`
> Запуск cameradar с входным файлом, включение журналов на порту 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t /tmp/test.txt -p 8554`
> Запуск cameradar в подсети с пользовательскими словарями, на портах 554, 5554 и 8554
`docker run -v /tmp:/tmp --net=host -t ullaakut/cameradar -t 192.168.0.0/24 --custom-credentials="/tmp/dictionaries/credentials.json" --custom-routes="/tmp/dictionaries/routes" -p 554,5554,8554`.
## Лицензия
Copyright 2023 Ullaakut
Настоящим предоставляется бесплатное разрешение любому лицу, получившему копию
данного программного обеспечения и сопутствующих файлов документации ("Программное обеспечение"), совершать сделки с Программным обеспечением без ограничений, включая, без ограничения, права
использовать, копировать, изменять, объединять, публиковать, распространять, выдавать сублицензии и/или продавать
копии программного обеспечения, а также разрешать лицам, которым предоставляется Программное обеспечение
делать это, при соблюдении следующих условий:
Вышеуказанное уведомление об авторском праве и данное уведомление о разрешении должны быть включены во все
копиях или существенных частях Программного обеспечения.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ
ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОГО СОСТОЯНИЯ,
ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ
АВТОРЫ ИЛИ ВЛАДЕЛЬЦЫ АВТОРСКИХ ПРАВ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПРЕТЕНЗИИ, УБЫТКИ ИЛИ ДРУГУЮ
ОТВЕТСТВЕННОСТЬ, БУДЬ ТО В РАМКАХ ДОГОВОРНОГО, ДЕЛИКТНОГО ИЛИ ИНОГО ИСКА, ВОЗНИКАЮЩЕГО
ИЗ, В РЕЗУЛЬТАТЕ ИЛИ В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ИНЫМИ
ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
-400
View File
@@ -1,400 +0,0 @@
package cameradar
import (
"fmt"
"time"
"github.com/Ullaakut/go-curl"
)
// HTTP responses.
const (
httpOK = 200
httpUnauthorized = 401
httpForbidden = 403
httpNotFound = 404
)
// CURL RTSP request types.
const (
rtspDescribe = 2
rtspSetup = 4
)
// Authentication types.
const (
authNone = 0
authBasic = 1
authDigest = 2
)
// Route that should never be a constructor default.
const dummyRoute = "/0x8b6c42"
// Attack attacks the given targets and returns the accessed streams.
func (s *Scanner) Attack(targets []Stream) ([]Stream, error) {
if len(targets) == 0 {
return nil, fmt.Errorf("no stream found")
}
// Most cameras will be accessed successfully with these two attacks.
s.term.StartStepf("Attacking routes of %d streams", len(targets))
streams := s.AttackRoute(targets)
s.term.StartStepf("Attempting to detect authentication methods of %d streams", len(targets))
streams = s.DetectAuthMethods(streams)
s.term.StartStepf("Attacking credentials of %d streams", len(targets))
streams = s.AttackCredentials(streams)
s.term.StartStep("Validating that streams are accessible")
streams = s.ValidateStreams(streams)
// But some cameras run GST RTSP Server which prioritizes 401 over 404 contrary to most cameras.
// For these cameras, running another route attack will solve the problem.
for _, stream := range streams {
if !stream.RouteFound || !stream.CredentialsFound || !stream.Available {
s.term.StartStepf("Second round of attacks")
streams = s.AttackRoute(streams)
s.term.StartStep("Validating that streams are accessible")
streams = s.ValidateStreams(streams)
break
}
}
s.term.EndStep()
return streams, nil
}
// ValidateStreams tries to setup the stream to validate whether or not it is available.
func (s *Scanner) ValidateStreams(targets []Stream) []Stream {
for i := range targets {
targets[i].Available = s.validateStream(targets[i])
time.Sleep(s.attackInterval)
}
return targets
}
// AttackCredentials attempts to guess the provided targets' credentials using the given
// dictionary or the default dictionary if none was provided by the user.
func (s *Scanner) AttackCredentials(targets []Stream) []Stream {
resChan := make(chan Stream)
defer close(resChan)
for i := range targets {
go s.attackCameraCredentials(targets[i], resChan)
}
for range targets {
attackResult := <-resChan
if attackResult.CredentialsFound {
targets = replace(targets, attackResult)
}
}
return targets
}
// AttackRoute attempts to guess the provided targets' streaming routes using the given
// dictionary or the default dictionary if none was provided by the user.
func (s *Scanner) AttackRoute(targets []Stream) []Stream {
resChan := make(chan Stream)
defer close(resChan)
for i := range targets {
go s.attackCameraRoute(targets[i], resChan)
}
for range targets {
attackResult := <-resChan
if attackResult.RouteFound {
targets = replace(targets, attackResult)
}
}
return targets
}
// DetectAuthMethods attempts to guess the provided targets' authentication types, between
// digest, basic auth or none at all.
func (s *Scanner) DetectAuthMethods(targets []Stream) []Stream {
for i := range targets {
targets[i].AuthenticationType = s.detectAuthMethod(targets[i])
time.Sleep(s.attackInterval)
var authMethod string
switch targets[i].AuthenticationType {
case authNone:
authMethod = "no"
case authBasic:
authMethod = "basic"
case authDigest:
authMethod = "digest"
default:
authMethod = "unknown:" + string(targets[i].AuthenticationType)
}
s.term.Debugf("Stream %s uses %s authentication method\n", GetCameraRTSPURL(targets[i]), authMethod)
}
return targets
}
func (s *Scanner) attackCameraCredentials(target Stream, resChan chan<- Stream) {
for _, username := range s.credentials.Usernames {
for _, password := range s.credentials.Passwords {
ok := s.credAttack(target, username, password)
if ok {
target.CredentialsFound = true
target.Username = username
target.Password = password
resChan <- target
return
}
time.Sleep(s.attackInterval)
}
}
target.CredentialsFound = false
resChan <- target
}
func (s *Scanner) attackCameraRoute(target Stream, resChan chan<- Stream) {
// If the stream responds positively to the dummy route, it means
// it doesn't require (or respect the RFC) a route and the attack
// can be skipped.
ok := s.routeAttack(target, dummyRoute)
if ok {
target.RouteFound = true
target.Routes = append(target.Routes, "/")
resChan <- target
return
}
// Otherwise, bruteforce the routes.
for _, route := range s.routes {
ok := s.routeAttack(target, route)
if ok {
target.RouteFound = true
target.Routes = append(target.Routes, route)
}
time.Sleep(s.attackInterval)
}
resChan <- target
}
func (s *Scanner) detectAuthMethod(stream Stream) int {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf(
"rtsp://%s:%d/%s",
stream.Address,
stream.Port,
stream.Route(),
)
s.setCurlOptions(c)
// Send a request to the URL of the stream we want to attack.
_ = c.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the stream URL.
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
// Perform the request.
err := c.Perform()
if err != nil {
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
return -1
}
authType, err := c.Getinfo(curl.INFO_HTTPAUTH_AVAIL)
if err != nil {
s.term.Errorf("Getinfo failed: %v", err)
return -1
}
if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", authType)
}
return authType.(int)
}
func (s *Scanner) routeAttack(stream Stream, route string) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
stream.Username,
stream.Password,
stream.Address,
stream.Port,
route,
)
s.setCurlOptions(c)
// Set proper authentication type.
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
// Send a request to the URL of the stream we want to attack.
_ = c.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the stream URL.
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
// Perform the request.
err := c.Perform()
if err != nil {
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
return false
}
// Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
s.term.Errorf("Getinfo failed: %v", err)
return false
}
if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
}
// If it's a 401 or 403, it means that the credentials are wrong but the route might be okay.
// If it's a 200, the stream is accessed successfully.
if rc == httpOK || rc == httpUnauthorized || rc == httpForbidden {
return true
}
return false
}
func (s *Scanner) credAttack(stream Stream, username string, password string) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
username,
password,
stream.Address,
stream.Port,
stream.Route(),
)
s.setCurlOptions(c)
// Set proper authentication type.
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(username, ":", password))
// Send a request to the URL of the stream we want to attack.
_ = c.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the stream URL.
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspDescribe)
// Perform the request.
err := c.Perform()
if err != nil {
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
return false
}
// Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
s.term.Errorf("Getinfo failed: %v", err)
return false
}
if s.debug {
s.term.Debugln("DESCRIBE", attackURL, "RTSP/1.0 >", rc)
}
// If it's a 404, it means that the route is incorrect but the credentials might be okay.
// If it's a 200, the stream is accessed successfully.
if rc == httpOK || rc == httpNotFound {
return true
}
return false
}
func (s *Scanner) validateStream(stream Stream) bool {
c := s.curl.Duphandle()
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
stream.Username,
stream.Password,
stream.Address,
stream.Port,
stream.Route(),
)
s.setCurlOptions(c)
// Set proper authentication type.
_ = c.Setopt(curl.OPT_HTTPAUTH, stream.AuthenticationType)
_ = c.Setopt(curl.OPT_USERPWD, fmt.Sprint(stream.Username, ":", stream.Password))
// Send a request to the URL of the stream we want to attack.
_ = c.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the stream URL.
_ = c.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
_ = c.Setopt(curl.OPT_RTSP_REQUEST, rtspSetup)
_ = c.Setopt(curl.OPT_RTSP_TRANSPORT, "RTP/AVP;unicast;client_port=33332-33333")
// Perform the request.
err := c.Perform()
if err != nil {
s.term.Errorf("Perform failed for %q (auth %d): %v", attackURL, stream.AuthenticationType, err)
return false
}
// Get return code for the request.
rc, err := c.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
s.term.Errorf("Getinfo failed: %v", err)
return false
}
if s.debug {
s.term.Debugln("SETUP", attackURL, "RTSP/1.0 >", rc)
}
// If it's a 200, the stream is accessed successfully.
if rc == httpOK {
return true
}
return false
}
func (s *Scanner) setCurlOptions(c Curler) {
// Do not write sdp in stdout
_ = c.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
// Do not use signals (would break multithreading).
_ = c.Setopt(curl.OPT_NOSIGNAL, 1)
// Do not send a body in the describe request.
_ = c.Setopt(curl.OPT_NOBODY, 1)
// Set custom timeout.
_ = c.Setopt(curl.OPT_TIMEOUT_MS, int(s.timeout/time.Millisecond))
// Enable verbose logs if verbose mode is on.
if s.verbose {
_ = c.Setopt(curl.OPT_VERBOSE, 1)
} else {
_ = c.Setopt(curl.OPT_VERBOSE, 0)
}
}
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
func doNotWrite([]uint8, interface{}) bool {
return true
}
-786
View File
@@ -1,786 +0,0 @@
package cameradar
import (
"errors"
"io/ioutil"
"testing"
"time"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/go-curl"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type CurlerMock struct {
mock.Mock
}
func (m *CurlerMock) Setopt(opt int, param interface{}) error {
args := m.Called(opt, param)
return args.Error(0)
}
func (m *CurlerMock) Perform() error {
args := m.Called()
return args.Error(0)
}
func (m *CurlerMock) Getinfo(info curl.CurlInfo) (interface{}, error) {
args := m.Called(info)
return args.Int(0), args.Error(1)
}
func (m *CurlerMock) Duphandle() Curler {
return m
}
func TestAttack(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
fakeTargets = []Stream{stream1, stream2}
fakeRoutes = Routes{"live.sdp", "media.amp"}
fakeCredentials = Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
)
tests := []struct {
description string
targets []Stream
performErr error
expectedStreams []Stream
expectedErr error
}{
{
description: "inverted RTSP RFC",
targets: fakeTargets,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "attack works",
targets: fakeTargets,
expectedStreams: fakeTargets,
},
{
description: "no targets",
targets: nil,
expectedStreams: nil,
expectedErr: errors.New("no stream found"),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
if len(test.targets) != 0 {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(200, nil)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: time.Millisecond,
verbose: true,
debug: true,
credentials: fakeCredentials,
routes: fakeRoutes,
}
results, err := scanner.Attack(test.targets)
assert.Equal(t, test.expectedErr, err)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
}
}
func TestAttackCredentials(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
fakeTargets = []Stream{stream1, stream2}
fakeCredentials = Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
)
tests := []struct {
description string
targets []Stream
credentials Credentials
timeout time.Duration
verbose bool
status int
performErr error
getInfoErr error
invalidTargets bool
expectedStreams []Stream
}{
{
description: "Credentials found",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
status: 404,
expectedStreams: fakeTargets,
},
{
description: "Camera accessed",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
{
description: "curl perform fails",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "curl getinfo fails",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "Verbose mode disabled",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
verbose: false,
status: 403,
expectedStreams: fakeTargets,
},
{
description: "Verbose mode enabled",
targets: fakeTargets,
credentials: fakeCredentials,
timeout: 1 * time.Millisecond,
verbose: true,
status: 403,
expectedStreams: fakeTargets,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
if !test.invalidTargets {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
debug: test.verbose,
credentials: test.credentials,
}
results := scanner.AttackCredentials(test.targets)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
}
}
func TestAttackRoute(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
fakeTargets = []Stream{stream1, stream2}
fakeRoutes = Routes{"live.sdp", "media.amp"}
)
tests := []struct {
description string
targets []Stream
routes Routes
timeout time.Duration
verbose bool
status int
performErr error
getInfoErr error
invalidTargets bool
expectedStreams []Stream
expectedErr error
}{
{
description: "Route found",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 403,
expectedStreams: fakeTargets,
},
{
description: "Route found",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 401,
expectedStreams: fakeTargets,
},
{
description: "Camera accessed",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
{
description: "curl perform fails",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "curl getinfo fails",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "verbose mode disabled",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
verbose: false,
expectedStreams: fakeTargets,
},
{
description: "verbose mode enabled",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
verbose: true,
expectedStreams: fakeTargets,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
if !test.invalidTargets {
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
debug: test.verbose,
routes: test.routes,
}
results := scanner.AttackRoute(test.targets)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
}
}
func TestAttackRoute_NoDummyRoute(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
fakeTargets = []Stream{stream1, stream2}
fakeRoutes = Routes{"live.sdp", "media.amp"}
)
tests := []struct {
description string
targets []Stream
routes Routes
timeout time.Duration
verbose bool
status int
expectedStreams []Stream
expectedErr error
}{
{
description: "Route found",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 403,
expectedStreams: fakeTargets,
},
{
description: "Route found",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 401,
expectedStreams: fakeTargets,
},
{
description: "Camera accessed",
targets: fakeTargets,
routes: fakeRoutes,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(nil)
// 404 on first call to the dummy route.
curlerMock.On("Getinfo", mock.Anything).Return(404, nil).Once()
curlerMock.On("Getinfo", mock.Anything).Return(test.status, nil)
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
routes: test.routes,
}
results := scanner.AttackRoute(test.targets)
assert.Len(t, results, len(test.expectedStreams))
curlerMock.AssertExpectations(t)
})
}
}
func TestValidateStreams(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
fakeTargets = []Stream{stream1, stream2}
)
tests := []struct {
description string
targets []Stream
timeout time.Duration
verbose bool
status int
performErr error
getInfoErr error
expectedStreams []Stream
}{
{
description: "route found",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 403,
expectedStreams: fakeTargets,
},
{
description: "route found",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 401,
expectedStreams: fakeTargets,
},
{
description: "camera accessed",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 200,
expectedStreams: fakeTargets,
},
{
description: "unavailable stream",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 400,
expectedStreams: fakeTargets,
},
{
description: "curl perform fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "curl getinfo fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "verbose disabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
verbose: false,
expectedStreams: fakeTargets,
},
{
description: "verbose enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
verbose: true,
expectedStreams: fakeTargets,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
debug: test.verbose,
}
results := scanner.ValidateStreams(test.targets)
assert.Equal(t, len(test.expectedStreams), len(results))
for _, expectedStream := range test.expectedStreams {
assert.Contains(t, results, expectedStream)
}
curlerMock.AssertExpectations(t)
})
}
}
func TestDetectAuthenticationType(t *testing.T) {
var (
stream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
Available: true,
}
stream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
Available: true,
}
fakeTargets = []Stream{stream1, stream2}
)
tests := []struct {
description string
targets []Stream
timeout time.Duration
verbose bool
status int
performErr error
getInfoErr error
expectedStreams []Stream
}{
{
description: "no auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 0,
expectedStreams: fakeTargets,
},
{
description: "basic auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 1,
expectedStreams: fakeTargets,
},
{
description: "digest auth enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
status: 2,
expectedStreams: fakeTargets,
},
{
description: "curl getinfo fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
getInfoErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "curl perform fails",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
performErr: errors.New("dummy error"),
expectedStreams: fakeTargets,
},
{
description: "verbose disabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
verbose: false,
expectedStreams: fakeTargets,
},
{
description: "verbose enabled",
targets: fakeTargets,
timeout: 1 * time.Millisecond,
verbose: true,
expectedStreams: fakeTargets,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
curlerMock := &CurlerMock{}
curlerMock.On("Setopt", mock.Anything, mock.Anything).Return(nil)
curlerMock.On("Perform").Return(test.performErr)
if test.performErr == nil {
curlerMock.On("Getinfo", mock.Anything).Return(test.status, test.getInfoErr)
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
curl: curlerMock,
timeout: test.timeout,
verbose: test.verbose,
}
results := scanner.DetectAuthMethods(test.targets)
assert.Equal(t, len(test.expectedStreams), len(results))
for _, expectedStream := range test.expectedStreams {
assert.Contains(t, results, expectedStream)
}
curlerMock.AssertExpectations(t)
})
}
}
func TestDoNotWrite(t *testing.T) {
assert.Equal(t, true, doNotWrite(nil, nil))
}
+106
View File
@@ -0,0 +1,106 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"os"
"time"
"github.com/EtixLabs/cameradar/cameradar"
"github.com/fatih/color"
"github.com/jessevdk/go-flags"
)
type options struct {
Target string `short:"t" long:"target" description:"The target on which to scan for open RTSP streams - required" required:"true"`
Ports string `short:"p" long:"ports" description:"The ports on which to search for RTSP streams" default:"554,8554"`
OutputFile string `short:"o" long:"nmap-output" description:"The path where nmap will create its XML result file" default:"/tmp/cameradar_scan.xml"`
Routes string `short:"r" long:"custom-routes" description:"The path on which to load a custom routes dictionary" default:"../dictionaries/routes"`
Credentials string `short:"c" long:"custom-credentials" description:"The path on which to load a custom credentials JSON dictionary" default:"../dictionaries/credentials.json"`
Speed int `short:"s" long:"speed" description:"The nmap speed preset to use" default:"4"`
Timeout int `short:"T" long:"timeout" description:"The timeout in miliseconds to use for attack attempts" default:"1000"`
EnableLogs bool `short:"l" long:"log" description:"Enable the logs for nmap's output to stdout"`
}
func main() {
var options options
_, err := flags.ParseArgs(&options, os.Args[1:])
if err != nil {
os.Exit(0)
}
credentials, err := cmrdr.LoadCredentials(options.Credentials)
if err != nil {
color.Red("Invalid credentials dictionary: %s", err.Error())
return
}
routes, err := cmrdr.LoadRoutes(options.Routes)
if err != nil {
color.Red("Invalid routes dictionary: %s", err.Error())
return
}
streams, _ := cmrdr.Discover(options.Target, options.Ports, options.OutputFile, options.Speed, options.EnableLogs)
streams, _ = cmrdr.AttackRoute(streams, routes, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
streams, _ = cmrdr.AttackCredentials(streams, credentials, time.Duration(options.Timeout)*time.Millisecond, options.EnableLogs)
prettyPrint(streams)
}
func prettyPrint(streams []cmrdr.Stream) {
yellow := color.New(color.FgYellow, color.Bold, color.Underline).SprintFunc()
blue := color.New(color.FgBlue, color.Underline).SprintFunc()
green := color.New(color.FgGreen, color.Bold).SprintFunc()
red := color.New(color.FgRed, color.Bold).SprintFunc()
white := color.New(color.Italic).SprintFunc()
success := 0
if len(streams) > 0 {
for _, stream := range streams {
if stream.CredentialsFound && stream.RouteFound {
fmt.Printf("%s\tDevice RTSP URL:\t%s\n", green("\xE2\x96\xB6"), blue(cmrdr.RTSPURL(stream)))
success++
} else {
fmt.Printf("%s\tAdmin panel URL:\t%s %s\n", red("\xE2\x96\xB6"), yellow(cmrdr.AdminPanelURL(stream)), white("You can use this URL to try attacking the camera's admin panel instead."))
}
fmt.Printf("\tDevice model:\t\t%s\n\n", stream.Device)
fmt.Printf("\tIP address:\t\t%s\n", stream.Address)
fmt.Printf("\tRTSP port:\t\t%d\n", stream.Port)
if stream.CredentialsFound {
fmt.Printf("\tUsername:\t\t%s\n", green(stream.Username))
fmt.Printf("\tPassword:\t\t%s\n", green(stream.Password))
} else {
fmt.Printf("\tUsername:\t\t%s\n", red("not found"))
fmt.Printf("\tPassword:\t\t%s\n", red("not found"))
}
if stream.RouteFound {
fmt.Printf("\tRTSP route:\t\t%s\n\n\n", green("/"+stream.Route))
} else {
fmt.Printf("\tRTSP route:\t\t%s\n\n\n", red("not found"))
}
}
if success > 1 {
fmt.Printf("%s Successful attack: %s devices were accessed", green("\xE2\x9C\x94"), green(len(streams)))
} else if success == 1 {
fmt.Printf("%s Successful attack: %s device was accessed", green("\xE2\x9C\x94"), green(len(streams)))
} else {
fmt.Printf("%s Streams were found but none were accessed. They are most likely configured with secure credentials and routes. You can try adding entries to the dictionary or generating your own in order to attempt a bruteforce attack on the cameras.", red("\xE2\x9C\x96"))
}
} else {
fmt.Printf("%s No streams were found. Please make sure that your target is on an accessible network.", red("\xE2\x9C\x96"))
}
}
-14
View File
@@ -1,14 +0,0 @@
// Package cameradar provides methods to be able to discover and
// attack RTSP streams easily. RTSP streams are used by most
// IP Cameras, often for surveillance.
//
// A simple example usage of the library can be found in
// https://github.com/Ullaakut/cameradar/tree/master/cameradar
//
// The example usage is complete enough for most users to
// ignore the library, but for users with specific needs
// such as creating their own bruteforcing dictionary to
// access cameras, or running their own network scan, this
// library allows to use simple and performant methods to
// attack streams.
package cameradar
+232
View File
@@ -0,0 +1,232 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
import (
"fmt"
"time"
curl "github.com/andelf/go-curl"
"github.com/pkg/errors"
v "gopkg.in/go-playground/validator.v9"
)
// HACK: See https://stackoverflow.com/questions/3572397/lib-curl-in-c-disable-printing
func doNotWrite([]uint8, interface{}) bool {
return true
}
func routeAttack(camera Stream, route string, timeout time.Duration, enableLogs bool) bool {
easy := curl.EasyInit()
defer easy.Cleanup()
if easy != nil {
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
camera.Username,
camera.Password,
camera.Address,
camera.Port,
route,
)
if enableLogs {
// Debug logs when logs are enabled
easy.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
easy.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Do not send a body in the describe request
easy.Setopt(curl.OPT_NOBODY, 1)
// Send a request to the URL of the camera we want to attack
easy.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the camera URL
easy.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_DESCRIBE
easy.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set custom timeout
easy.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
// Perform the request
easy.Perform()
// Get return code for the request
rc, err := easy.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
return false
}
// If it's a 404, it means that the route was not valid
if rc == 404 {
return false
}
return true
}
return false
}
func credAttack(camera Stream, username string, password string, timeout time.Duration, enableLogs bool) bool {
easy := curl.EasyInit()
defer easy.Cleanup()
if easy != nil {
attackURL := fmt.Sprintf(
"rtsp://%s:%s@%s:%d/%s",
username,
password,
camera.Address,
camera.Port,
camera.Route,
)
if enableLogs {
// Debug logs when logs are enabled
easy.Setopt(curl.OPT_VERBOSE, 1)
} else {
// Do not write sdp in stdout
easy.Setopt(curl.OPT_WRITEFUNCTION, doNotWrite)
}
// Do not send a body in the describe request
easy.Setopt(curl.OPT_NOBODY, 1)
// Send a request to the URL of the camera we want to attack
easy.Setopt(curl.OPT_URL, attackURL)
// Set the RTSP STREAM URI as the camera URL
easy.Setopt(curl.OPT_RTSP_STREAM_URI, attackURL)
// 2 is CURL_RTSPREQ_DESCRIBE
easy.Setopt(curl.OPT_RTSP_REQUEST, 2)
// Set custom timeout
easy.Setopt(curl.OPT_TIMEOUT_MS, int(timeout/time.Millisecond))
// Perform the request
easy.Perform()
// Get return code for the request
rc, err := easy.Getinfo(curl.INFO_RESPONSE_CODE)
if err != nil {
return false
}
// If it's a 403 or a 401, it means that the credentials are not correct
if rc == 403 || rc == 401 {
return false
}
return true
}
return false
}
func attackCameraCredentials(target Stream, credentials Credentials, resultsChan chan<- Stream, timeout time.Duration, log bool) {
for _, username := range credentials.Usernames {
for _, password := range credentials.Passwords {
ok := credAttack(target, username, password, timeout, log)
if ok {
target.CredentialsFound = true
target.Username = username
target.Password = password
resultsChan <- target
return
}
}
}
target.CredentialsFound = false
resultsChan <- target
}
func attackCameraRoute(target Stream, routes Routes, resultsChan chan<- Stream, timeout time.Duration, log bool) {
for _, route := range routes {
ok := routeAttack(target, route, timeout, log)
if ok {
target.RouteFound = true
target.Route = route
resultsChan <- target
return
}
}
target.RouteFound = false
resultsChan <- target
}
// AttackCredentials attempts to guess the provided targets' credentials using the given
// dictionary or the default dictionary if none was provided by the user
func AttackCredentials(targets []Stream, credentials Credentials, timeout time.Duration, log bool) (results []Stream, err error) {
attacks := make(chan Stream)
defer close(attacks)
validate := v.New()
for _, target := range targets {
err := validate.Struct(target)
if err != nil {
return targets, errors.Wrap(err, "invalid streams")
}
go attackCameraCredentials(target, credentials, attacks, timeout, log)
}
attackResults := []Stream{}
for _ = range targets {
attackResults = append(attackResults, <-attacks)
}
found := 0
for _, result := range attackResults {
if result.CredentialsFound == true {
targets = replace(targets, result)
found++
}
}
if found == 0 {
return targets, errors.New("No credentials found")
}
return targets, nil
}
// AttackRoute attempts to guess the provided targets' streaming routes using the given
// dictionary or the default dictionary if none was provided by the user
func AttackRoute(targets []Stream, routes Routes, timeout time.Duration, log bool) (results []Stream, err error) {
attacks := make(chan Stream)
defer close(attacks)
validate := v.New()
for _, target := range targets {
err := validate.Struct(target)
if err != nil {
return targets, errors.Wrap(err, "invalid streams")
}
go attackCameraRoute(target, routes, attacks, timeout, log)
}
attackResults := []Stream{}
for _ = range targets {
attackResults = append(attackResults, <-attacks)
}
found := 0
for _, result := range attackResults {
if result.RouteFound == true {
targets = replace(targets, result)
found++
}
}
if found == 0 {
return targets, errors.New("No routes found")
}
return targets, nil
}
+152
View File
@@ -0,0 +1,152 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
import (
"bufio"
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"os/exec"
"github.com/pkg/errors"
v "gopkg.in/go-playground/validator.v9"
)
// These constants detail the different level of nmap speed presets
// that determine the timeout values and wether or not nmap makes use of parallelism
const (
// PARANOID NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay
PARANOIAC = 0
// SNEAKY NO PARALLELISM | 15sec timeout | 100ms to 10s round-trip time timeout | 15s scan delay
SNEAKY = 1
// POLITE NO PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 400ms scan delay
POLITE = 2
// NORMAL PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 0s scan delay
NORMAL = 3
// AGGRESSIVE PARALLELISM | 500ms timeout | 100ms to 1250ms round-trip time timeout | 0s scan delay
AGGRESSIVE = 4
// INSANE PARALLELISM | 250ms timeout | 50ms to 300ms round-trip time timeout | 0s scan delay
INSANE = 5
)
// RunNmap runs nmap on the specified targets's specified ports, using the given nmap speed
func RunNmap(targets, ports string, resultFilePath string, nmapSpeed int, enableLogs bool) error {
// Prepare nmap command
cmd := exec.Command(
"nmap",
fmt.Sprintf("-T%d", nmapSpeed),
"-A",
targets,
"-p",
ports,
"-oX",
resultFilePath,
)
// Pipe stdout to be able to write the logs in realtime
stdout, err := cmd.StdoutPipe()
if err != nil {
return errors.Wrap(err, "Couldn't get stdout pipe")
}
// Execute the nmap command
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "Coudln't run nmap command")
}
// Scan the pipe until an end of file or an error occurs
in := bufio.NewScanner(stdout)
for in.Scan() {
if enableLogs {
log.Printf(in.Text())
}
}
if err := in.Err(); err != nil {
if enableLogs {
log.Printf("error: %s", err)
}
}
return nil
}
// ParseNmapResult returns a slice of streams from an NMap XML result file
// To generate one yourself, use the -X option when running NMap
func ParseNmapResult(nmapResultFilePath string) ([]Stream, error) {
var streams []Stream
// Open & Read XML file
content, err := ioutil.ReadFile(nmapResultFilePath)
if err != nil {
return streams, errors.Wrap(err, "Could not read nmap result file at "+nmapResultFilePath+":")
}
// Unmarshal content of XML file into data structure
result := &NmapResult{}
err = xml.Unmarshal(content, &result)
if err != nil {
return streams, err
}
// Iterate on hosts to try to find hosts with ports that
// - serve RTSP
// - are open
validate := v.New()
for _, host := range result.Hosts {
if host.Ports.Ports == nil {
continue
}
for _, port := range host.Ports.Ports {
err = validate.Struct(port)
if err != nil {
continue
}
streams = append(streams, Stream{
Device: port.Service.Product,
Address: host.Address.Addr,
Port: port.PortID,
})
}
}
return streams, nil
}
// Discover scans the target networks and tries to find RTSP streams within them
// targets - string: The addresses
// - a subnet (e.g.: 172.16.100.0/24)
// - an IP (e.g.: 172.16.100.10)
// - a hostname (e.g.: localhost)
// - a range of IPs (e.g.: 172.16.100.10-172.16.100.20)
// - a mix of all those separated by commas (e.g.: localhost,172.17.100.0/24,172.16.100.10-172.16.100.20,0.0.0.0).
// ports - string :
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
func Discover(targets string, ports string, nmapResultPath string, speed int, log bool) ([]Stream, error) {
var streams []Stream
// Run nmap command to discover open ports on the specified targets & ports
err := RunNmap(targets, ports, nmapResultPath, speed, log)
if err != nil {
return streams, err
}
// Get found streams from nmap results
streams, err = ParseNmapResult(nmapResultPath)
if err != nil {
return streams, err
}
return streams, nil
}
+38
View File
@@ -0,0 +1,38 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
import "fmt"
func replace(streams []Stream, new Stream) []Stream {
updatedSlice := streams[:0]
for _, old := range streams {
if old.Address == new.Address && old.Port == new.Port {
updatedSlice = append(updatedSlice, new)
} else {
updatedSlice = append(updatedSlice, old)
}
}
return updatedSlice
}
// RTSPURL generates a stream's RTSP URL
func RTSPURL(stream Stream) string {
return "rtsp://" + stream.Username + ":" + stream.Password + "@" + stream.Address + ":" + fmt.Sprint(stream.Port) + "/" + stream.Route
}
// AdminPanelURL returns the URL to the camera's admin panel
func AdminPanelURL(stream Stream) string {
return "http://" + stream.Address + "/"
}
+58
View File
@@ -0,0 +1,58 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
import (
"bufio"
"encoding/json"
"io/ioutil"
"os"
"github.com/pkg/errors"
)
// LoadCredentials opens a dictionary file and returns its contents as a Credentials structure
func LoadCredentials(path string) (Credentials, error) {
var creds Credentials
// Open & Read XML file
content, err := ioutil.ReadFile(path)
if err != nil {
return creds, errors.Wrap(err, "Could not read credentials dictionary file at "+path+":")
}
// Unmarshal content of JSON file into data structure
err = json.Unmarshal(content, &creds)
if err != nil {
return creds, err
}
return creds, nil
}
// LoadRoutes opens a dictionary file and returns its contents as a Routes structure
func LoadRoutes(path string) (Routes, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var routes Routes
scanner := bufio.NewScanner(file)
for scanner.Scan() {
routes = append(routes, scanner.Text())
}
return routes, scanner.Err()
}
+38
View File
@@ -0,0 +1,38 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
// Stream represents a camera's RTSP stream
type Stream struct {
Device string
Username string
Password string
Route string
Address string `validate:"required"`
Port uint `validate:"required"`
CredentialsFound bool
RouteFound bool
}
// Credentials is a map of credentials
// usernames are keys and passwords are values
// creds['admin'] -> 'secure_password'
type Credentials struct {
Usernames []string `json:"usernames"`
Passwords []string `json:"passwords"`
}
// Routes is a slice of Routes
// ['/live.sdp', '/media.amp', ...]
type Routes []string
+62
View File
@@ -0,0 +1,62 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmrdr
import "encoding/xml"
// NmapResult is the structure that holds all the information from an NMap scan
type NmapResult struct {
XMLName xml.Name `xml:"nmaprun"`
Hosts []Host `xml:"host" validate:"required"`
}
// Host represents a host discovered during a scan
type Host struct {
XMLName xml.Name `xml:"host"`
Address Address `xml:"address"`
Ports Ports `xml:"ports"`
}
// Address is a host's address discovered during a scan
type Address struct {
XMLName xml.Name `xml:"address"`
Addr string `xml:"addr,attr"`
AddrType string `xml:"addrType,attr"`
}
// Ports is the list of openned ports on a host
type Ports struct {
XMLName xml.Name `xml:"ports"`
Ports []Port `xml:"port"`
}
// Port is a port found on a host during a scan
type Port struct {
XMLName xml.Name `xml:"port"`
PortID uint `xml:"portid,attr"`
State State `xml:"state"`
Service Service `xml:"service"`
}
// State is the state of a port
type State struct {
XMLName xml.Name `xml:"state"`
State string `xml:"state,attr" validate:"required,eq=open"`
}
// Service represents the service that a port provides
type Service struct {
XMLName xml.Name `xml:"service"`
Name string `xml:"name,attr" validate:"required,eq=rtsp"`
Product string `xml:"product,attr"`
}
-96
View File
@@ -1,96 +0,0 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/Ullaakut/cameradar/v5"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/disgo/style"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
func parseArguments() error {
viper.SetEnvPrefix("cameradar")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
pflag.StringSliceP("targets", "t", []string{}, "The targets on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
pflag.StringSliceP("ports", "p", []string{"554", "5554", "8554"}, "The ports on which to search for RTSP streams")
pflag.StringP("custom-routes", "r", "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/routes", "The path on which to load a custom routes dictionary")
pflag.StringP("custom-credentials", "c", "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/credentials.json", "The path on which to load a custom credentials JSON dictionary")
pflag.IntP("scan-speed", "s", 4, "The nmap speed preset to use for scanning (lower is stealthier)")
pflag.DurationP("attack-interval", "I", 0, "The interval between each attack (i.e: 2000ms, higher is stealthier)")
pflag.DurationP("timeout", "T", 2000*time.Millisecond, "The timeout to use for attack attempts (i.e: 2000ms)")
pflag.BoolP("debug", "d", false, "Enable the debug logs")
pflag.BoolP("verbose", "v", false, "Enable the verbose logs")
pflag.BoolP("help", "h", false, "displays this help message")
viper.AutomaticEnv()
pflag.Parse()
err := viper.BindPFlags(pflag.CommandLine)
if err != nil {
return err
}
if viper.GetBool("help") {
pflag.Usage()
fmt.Println("\nExamples of usage:")
fmt.Println("\tScanning your home network for RTSP streams:\tcameradar -t 192.168.0.0/24")
fmt.Println("\tScanning a remote camera on a specific port:\tcameradar -t 172.178.10.14 -p 18554 -s 2")
fmt.Println("\tScanning an unstable remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 --timeout 10000 -l")
fmt.Println("\tStealthily scanning a remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 -I 5000")
os.Exit(0)
}
if len(viper.GetStringSlice("targets")) == 0 {
pflag.Usage()
return errors.New("targets (-t, --targets) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
}
return nil
}
func main() {
err := parseArguments()
if err != nil {
printErr(err)
}
c, err := cameradar.New(
cameradar.WithTargets(viper.GetStringSlice("targets")),
cameradar.WithPorts(viper.GetStringSlice("ports")),
cameradar.WithDebug(viper.GetBool("debug")),
cameradar.WithVerbose(viper.GetBool("verbose")),
cameradar.WithCustomCredentials(viper.GetString("custom-credentials")),
cameradar.WithCustomRoutes(viper.GetString("custom-routes")),
cameradar.WithScanSpeed(viper.GetInt("scan-speed")),
cameradar.WithAttackInterval(viper.GetDuration("attack-interval")),
cameradar.WithTimeout(viper.GetDuration("timeout")),
)
if err != nil {
printErr(err)
}
scanResult, err := c.Scan()
if err != nil {
printErr(err)
}
streams, err := c.Attack(scanResult)
if err != nil {
printErr(err)
}
c.PrintStreams(streams)
}
func printErr(err error) {
disgo.Errorln(style.Failure(style.SymbolCross), err)
os.Exit(1)
}
-25
View File
@@ -1,25 +0,0 @@
package cameradar
import (
curl "github.com/Ullaakut/go-curl"
)
// Curler is an interface that implements the CURL interface of the go-curl library
// Used for mocking
type Curler interface {
Setopt(opt int, param interface{}) error
Perform() error
Getinfo(info curl.CurlInfo) (interface{}, error)
Duphandle() Curler
}
// Curl is a libcurl wrapper used to make the Curler interface work even though
// golang currently does not support covariance (see https://github.com/golang/go/issues/7512)
type Curl struct {
*curl.CURL
}
// Duphandle wraps curl.Duphandle
func (c *Curl) Duphandle() Curler {
return &Curl{c.CURL.Duphandle()}
}
-20
View File
@@ -1,20 +0,0 @@
package cameradar
import (
"reflect"
"testing"
curl "github.com/Ullaakut/go-curl"
)
func TestCurl(t *testing.T) {
handle := Curl{
CURL: curl.EasyInit(),
}
handle2 := handle.Duphandle()
if reflect.DeepEqual(handle, handle2) {
t.Errorf("unexpected identical handle from duphandle: expected %+v got %+v", handle, handle2)
}
}
+17 -67
View File
@@ -1,81 +1,31 @@
{
"usernames": [
"",
"666666",
"888888",
"Admin",
"admin",
"admin1",
"administrator",
"Administrator",
"aiphone",
"Dinion",
"none",
"Admin",
"root",
"Root",
"service",
"supervisor",
"ubnt"
],
"passwords": [
"passwords" : [
"",
"0000",
"00000",
"1111",
"111111",
"1111111",
"123",
"admin",
"9999",
"123456",
"pass",
"camera",
"1234",
"12345",
"123456",
"1234567",
"12345678",
"123456789",
"12345678910",
"4321",
"666666",
"6fJjMKYx",
"888888",
"9999",
"admin",
"admin123456",
"admin pass",
"Admin",
"admin123",
"administrator",
"Administrator",
"aiphone",
"camera",
"Camera",
"fliradmin",
"GRwvcj8j",
"hikvision",
"hikadmin",
"HuaWei123",
"ikwd",
"jvc",
"kj3TqCWv",
"meinsm",
"pass",
"Pass",
"password",
"password123",
"qwerty",
"qwerty123",
"Recorder",
"reolink",
"root",
"service",
"supervisor",
"support",
"system",
"tlJwpbo6",
"toor",
"tp-link",
"ubnt",
"user",
"wbox",
"wbox123",
"Y5eIMz3C"
"jvc",
"meinsm",
"root",
"4321",
"1111111",
"password",
"ikwd",
"supervisor",
"ubnt"
]
}
}
+37 -121
View File
@@ -1,127 +1,50 @@
/live/ch01_0
0/1:1/main
0/usrnm:pwd/main
0/video1
1
1.AMP
1/h264major
1/stream1
11
12
125
1080p
1440p
480p
4K
666
720p
AVStream1_1
CAM_ID.password.mp2
CH001.sdp
GetData.cgi
HD
HighResolutionVideo
LowResolutionVideo
MediaInput/h264
MediaInput/mpeg4
ONVIF/MediaInput
ONVIF/MediaInput?profile=4_def_profile6
StdCh1
Streaming/Channels/1
Streaming/Unicast/channels/101
StreamingSetting?version=1.0&action=getRTSPStream&ChannelID=1&ChannelName=Channel1
VideoInput/1/h264/1
VideoInput/1/mpeg4/1
access_code
access_name_for_stream_1_to_5
api/mjpegvideo.cgi
av0_0
av2
avc
avn=2
axis-media/media.amp
axis-media/media.amp?camera=1
axis-media/media.amp?videocodec=h264
cam
cam/realmonitor
cam/realmonitor?channel=0&subtype=0
cam/realmonitor?channel=1&subtype=0
cam/realmonitor?channel=1&subtype=1
cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
cam0
cam0_0
cam0_1
cam1
cam1/h264
cam1/h264/multicast
cam1/mjpeg
cam1/mpeg4
cam1/mpeg4?user='username'&pwd='password'
cam1/onvif-h264
camera.stm
ch0
ch00/0
ch001.sdp
ch01.264
ch01.264?
ch01.264?ptype=tcp
ch1_0
ch2_0
ch3_0
ch4_0
ch1/0
ch2/0
ch3/0
ch4/0
ch0_0.h264
ch0_unicast_firststream
ch0_unicast_secondstream
ch1-s1
channel1
gnz_media/main
h264
h264.sdp
h264/ch1/sub/av_stream
h264/media.amp
h264Preview_01_main
h264Preview_01_sub
h264_vga.sdp
h264_stream
image.mpg
img/media.sav
img/media.sav?channel=1
img/video.asf
img/video.sav
ioImage/1
ipcam.sdp
ipcam_h264.sdp
ipcam_mjpeg.sdp
live
live.sdp
live/av0
live/ch0
live/ch00_0
live/ch01_0
live/h264
live/main
live/main0
live/mpeg4
live1.sdp
live3.sdp
live_mpeg4.sdp
live_st1
livestream
main
media
media.amp
media.amp?streamprofile=Profile1
livestream/
media/media.amp
media/video1
medias2
mjpeg/media.smp
mp4
mpeg/media.amp
mpeg4
mpeg4/1/media.amp
mpeg4/media.amp
@@ -132,66 +55,59 @@ multicaststream
now.mp4
nph-h264.cgi
nphMpeg4/g726-640x
nphMpeg4/g726-640x48
nphMpeg4/g726-640x480
nphMpeg4/nil-320x240
onvif-media/media.amp
onvif1
pass@10.0.0.5:6667/blinkhd
play1.sdp
play2.sdp
profile0
profile1
profile2
profile2/media.smp
profile5/media.smp
rtpvideo1.sdp
rtsp_live0
rtsp_live1
rtsp_live2
rtsp_tunnel
rtsph264
rtsph2641080p
snap.jpg
stream
stream/0
stream/1
stream/live.sdp
stream.sdp
stream1
streaming/channels/0
streaming/channels/1
streaming/channels/101
tcp/av0_0
test
tmpfs/auto.jpg
trackID=1
ucast/11
udp/av0_0
udp/unicast/aiphone_H264
udpstream
user.pin.mp2
user=admin&password=&channel=1&stream=0.sdp?
user=admin&password=&channel=1&stream=0.sdp?real_stream
user=admin_password=?????_channel=1_stream=0.sdp?real_stream
user=admin_password=R5XFY888_channel=1_stream=0.sdp?real_stream
user_defined
v2
video
video.3gp
video.h264
video.mjpg
video.mp4
video.pro1
video.pro2
video.pro3
video0
video0.sdp
video1
video1.sdp
video1+audio1
videoMain
videoinput_1/h264_1/media.stm
videostream.asf
vis
wfov
video.h264
11
12
ch1-s1
live3.sdp
onvif-media/media.amp
axis-media/media.amp
axis-media/media.amp?videocodec=h264
mpeg4/media.amp
stream
cam/realmonitor
live
video.pro2
videoMain
VideoInput/1/mpeg4/1
VideoInput/1/h264/1
video.pro3
video.pro1
video.mjpg
h264_vga.sdp
media.amp
media
ONVIF/MediaInput
nphMpeg4/g726-640x48
MediaInput/mpeg4
MediaInput/h264
Streaming/Channels/1
ch0_0.h264
rtsph2641080p
live/av0
cam1/onvif-h264
ucast/11
LowResolutionVideo
1
live/ch00_0
medias2
-5
View File
@@ -1,5 +0,0 @@
0.0.0.0
localhost
192.17.0.0/16
192.168.1.140-255
192.168.2-3.0-255
-17
View File
@@ -1,17 +0,0 @@
module github.com/Ullaakut/cameradar/v5
go 1.14
require (
github.com/PuerkitoBio/goquery v1.5.0
github.com/Ullaakut/disgo v0.3.1
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd
github.com/Ullaakut/nmap v2.0.0+incompatible
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.2.2
github.com/vbauerster/mpb v3.4.0+incompatible
)
-163
View File
@@ -1,163 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/Ullaakut/disgo v0.3.1 h1:BGGVHynji41KGuGI02ztTCnILRvyzlvmiCRl5bBpjKk=
github.com/Ullaakut/disgo v0.3.1/go.mod h1:/CSvpnYVSKOeh2dvUvx9cXshzz2t7T1/lRO/MrFj3fI=
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd h1:CMe+dX1CL4pCXNytxIB2U1qp0xZObGMZosJhaQdUlUo=
github.com/Ullaakut/go-curl v0.0.0-20190525093431-597e157bbffd/go.mod h1:u8mVgpDT88IPIt1B+Tu8vkrcFfBKGcfGwS9I7wmvMh0=
github.com/Ullaakut/nmap v2.0.0+incompatible h1:tNXub052dsnG8+yrgpph9nhVixIBdpRRgzvmQoc8eBA=
github.com/Ullaakut/nmap v2.0.0+incompatible/go.mod h1:fkC066hwfcoKwlI7DS2ARTggSVtBTZYCjVH1TzuTMaQ=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/vbauerster/mpb v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw=
github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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-20181221193216-37e7f081c4d4/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-27
View File
@@ -1,27 +0,0 @@
package cameradar
import "fmt"
func replace(streams []Stream, new Stream) []Stream {
var updatedSlice []Stream
for _, old := range streams {
if old.Address == new.Address && old.Port == new.Port {
updatedSlice = append(updatedSlice, new)
} else {
updatedSlice = append(updatedSlice, old)
}
}
return updatedSlice
}
// GetCameraRTSPURL generates a stream's RTSP URL.
func GetCameraRTSPURL(stream Stream) string {
return "rtsp://" + stream.Username + ":" + stream.Password + "@" + stream.Address + ":" + fmt.Sprint(stream.Port) + "/" + stream.Route()
}
// GetCameraAdminPanelURL returns the URL to the camera's admin panel.
func GetCameraAdminPanelURL(stream Stream) string {
return "http://" + stream.Address + "/"
}
-105
View File
@@ -1,105 +0,0 @@
package cameradar
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReplace(t *testing.T) {
validStream1 := Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1,
}
validStream2 := Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 2,
}
invalidStream := Stream{
Device: "invalidDevice",
Address: "anotherFakeAddress",
Port: 3,
}
invalidStreamModified := Stream{
Device: "updatedDevice",
Address: "anotherFakeAddress",
Port: 3,
}
testCases := []struct {
streams []Stream
newStream Stream
expectedStreams []Stream
}{
{
streams: []Stream{validStream1, validStream2, invalidStream},
newStream: invalidStreamModified,
expectedStreams: []Stream{validStream1, validStream2, invalidStreamModified},
},
}
for _, test := range testCases {
streams := replace(test.streams, test.newStream)
assert.Equal(t, len(test.expectedStreams), len(streams))
for _, expectedStream := range test.expectedStreams {
assert.Contains(t, streams, expectedStream)
}
}
}
func TestGetCameraRTSPURL(t *testing.T) {
validStream := Stream{
Address: "1.2.3.4",
Username: "ullaakut",
Password: "ba69897483886f0d2b0afb6345b76c0c",
Routes: []string{"cameradar.sdp"},
Port: 1337,
}
testCases := []struct {
stream Stream
expectedRTSPURL string
}{
{
stream: validStream,
expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp",
},
}
for _, test := range testCases {
assert.Equal(t, test.expectedRTSPURL, GetCameraRTSPURL(test.stream))
}
}
func TestGetCameraAdminPanelURL(t *testing.T) {
validStream := Stream{
Address: "1.2.3.4",
}
testCases := []struct {
stream Stream
expectedRTSPURL string
}{
{
stream: validStream,
expectedRTSPURL: "http://1.2.3.4/",
},
}
for _, test := range testCases {
assert.Equal(t, test.expectedRTSPURL, GetCameraAdminPanelURL(test.stream))
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

-122
View File
@@ -1,122 +0,0 @@
package cameradar
import (
"bufio"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
)
var fs fileSystem = osFS{}
type fileSystem interface {
Open(name string) (file, error)
Stat(name string) (os.FileInfo, error)
}
type file interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
Stat() (os.FileInfo, error)
}
// osFS implements fileSystem using the local disk.
type osFS struct{}
func (osFS) Open(name string) (file, error) { return os.Open(name) }
func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }
// LoadCredentials opens a dictionary file and returns its contents as a Credentials structure.
func (s *Scanner) LoadCredentials() error {
s.term.Debugf("Loading credentials dictionary from path %q\n", s.credentialDictionaryPath)
// Open & Read XML file.
content, err := ioutil.ReadFile(s.credentialDictionaryPath)
if err != nil {
return fmt.Errorf("could not read credentials dictionary file at %q: %v", s.credentialDictionaryPath, err)
}
// Unmarshal content of JSON file into data structure.
err = json.Unmarshal(content, &s.credentials)
if err != nil {
return fmt.Errorf("unable to unmarshal dictionary contents: %v", err)
}
s.term.Debugf("Loaded %d usernames and %d passwords\n", len(s.credentials.Usernames), len(s.credentials.Passwords))
return nil
}
// LoadRoutes opens a dictionary file and returns its contents as a Routes structure.
func (s *Scanner) LoadRoutes() error {
s.term.Debugf("Loading routes dictionary from path %q\n", s.routeDictionaryPath)
file, err := os.Open(s.routeDictionaryPath)
if err != nil {
return fmt.Errorf("unable to open dictionary: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s.routes = append(s.routes, scanner.Text())
}
s.term.Debugf("Loaded %d routes\n", len(s.routes))
return scanner.Err()
}
// ParseCredentialsFromString parses a dictionary string and returns its contents as a Credentials structure.
func ParseCredentialsFromString(content string) (Credentials, error) {
var creds Credentials
// Unmarshal content of JSON file into data structure.
err := json.Unmarshal([]byte(content), &creds)
if err != nil {
return creds, err
}
return creds, nil
}
// ParseRoutesFromString parses a dictionary string and returns its contents as a Routes structure.
func ParseRoutesFromString(content string) Routes {
return strings.Split(content, "\n")
}
// LoadTargets parses the file containing hosts to targets, if the targets are
// just set to a file name.
func (s *Scanner) LoadTargets() error {
if len(s.targets) != 1 {
return nil
}
path := s.targets[0]
_, err := fs.Stat(path)
if err != nil {
return nil
}
file, err := fs.Open(path)
if err != nil {
return fmt.Errorf("unable to open targets file %q: %v", path, err)
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return fmt.Errorf("unable to read targets file %q: %v", path, err)
}
s.targets = strings.Split(string(bytes), "\n")
s.term.Debugf("Successfully parsed targets file with %d entries", len(s.targets))
return nil
}
-440
View File
@@ -1,440 +0,0 @@
package cameradar
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/Ullaakut/disgo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Setup Mock
type mockedFS struct {
osFS
fileExists bool
openError bool
fileMock *fileMock
fileSize int64
}
// fileMock mocks a file
type fileMock struct {
mock.Mock
readError bool
bytes.Buffer
}
type mockedFileInfo struct {
os.FileInfo
}
func (m mockedFileInfo) Size() int64 { return 1 }
func (m mockedFS) Stat(name string) (os.FileInfo, error) {
if !m.fileExists {
return nil, os.ErrNotExist
}
return mockedFileInfo{}, nil
}
func (m mockedFS) Open(name string) (file, error) {
if m.openError {
return nil, os.ErrNotExist
}
return m.fileMock, nil
}
func (m *fileMock) Read(p []byte) (n int, err error) {
if m.readError {
return 0, os.ErrNotExist
}
return m.Buffer.Read(p)
}
func (m *fileMock) ReadAt(p []byte, off int64) (n int, err error) {
return 1, nil
}
func (m *fileMock) Seek(offset int64, whence int) (int64, error) {
return offset, nil
}
func (m *fileMock) Stat() (os.FileInfo, error) {
return mockedFileInfo{}, nil
}
// Close mock
func (m *fileMock) Close() error {
args := m.Called()
return args.Error(0)
}
// Sync mock
func (m *fileMock) Sync() error {
args := m.Called()
return args.Error(0)
}
func TestLoadCredentials(t *testing.T) {
credentialsJSONString := []byte("{\"usernames\":[\"admin\",\"root\"],\"passwords\":[\"12345\",\"root\"]}")
validCredentials := Credentials{
Usernames: []string{"admin", "root"},
Passwords: []string{"12345", "root"},
}
tests := []struct {
description string
input []byte
fileExists bool
expectedCredentials Credentials
expectedErr error
}{
{
description: "Valid baseline",
fileExists: true,
input: credentialsJSONString,
expectedCredentials: validCredentials,
},
{
description: "File does not exist",
fileExists: false,
input: credentialsJSONString,
expectedErr: errors.New("could not read credentials dictionary file at \"/tmp/cameradar_test_load_credentials_1.xml\": open /tmp/cameradar_test_load_credentials_1.xml: no such file or directory"),
},
{
description: "Invalid format",
fileExists: true,
input: []byte("not json"),
expectedErr: errors.New("unable to unmarshal dictionary contents: invalid character 'o' in literal null (expecting 'u')"),
},
{
description: "No streams in dictionary",
fileExists: true,
input: []byte("{\"invalid\":\"json\"}"),
},
}
for i, test := range tests {
t.Run(test.description, func(t *testing.T) {
filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml"
// create file.
if test.fileExists {
_, err := os.Create(filePath)
if err != nil {
t.Fatalf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
}
err = ioutil.WriteFile(filePath, test.input, 0644)
if err != nil {
t.Fatalf("could not write xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
credentialDictionaryPath: filePath,
}
err := scanner.LoadCredentials()
assert.Equal(t, test.expectedErr, err)
assert.Len(t, scanner.credentials.Usernames, len(test.expectedCredentials.Usernames))
for _, expectedUsername := range test.expectedCredentials.Usernames {
assert.Contains(t, scanner.credentials.Usernames, expectedUsername)
}
assert.Len(t, scanner.credentials.Passwords, len(test.expectedCredentials.Passwords))
for _, expectedPassword := range test.expectedCredentials.Passwords {
assert.Contains(t, scanner.credentials.Passwords, expectedPassword)
}
})
}
}
func TestLoadRoutes(t *testing.T) {
routesJSONString := []byte("admin\nroot")
validRoutes := Routes{"admin", "root"}
tests := []struct {
description string
input []byte
fileExists bool
expectedRoutes Routes
expectedErr error
}{
{
description: "Valid baseline",
fileExists: true,
input: routesJSONString,
expectedRoutes: validRoutes,
},
{
description: "File does not exist",
fileExists: false,
input: routesJSONString,
expectedErr: errors.New("unable to open dictionary: open /tmp/cameradar_test_load_routes_1.xml: no such file or directory"),
},
{
description: "No streams in dictionary",
fileExists: true,
input: []byte(""),
},
}
for i, test := range tests {
t.Run(test.description, func(t *testing.T) {
filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml"
// Create file.
if test.fileExists {
_, err := os.Create(filePath)
if err != nil {
fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
err = ioutil.WriteFile(filePath, test.input, 0644)
if err != nil {
fmt.Printf("could not write xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath)
os.Exit(1)
}
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
routeDictionaryPath: filePath,
}
err := scanner.LoadRoutes()
assert.Equal(t, test.expectedErr, err)
assert.Len(t, scanner.routes, len(test.expectedRoutes))
for _, expectedRoute := range test.expectedRoutes {
assert.Contains(t, scanner.routes, expectedRoute)
}
})
}
}
func TestParseCredentialsFromString(t *testing.T) {
defaultCredentials := Credentials{
Usernames: []string{
"",
"admin",
"Admin",
"Administrator",
"root",
"supervisor",
"ubnt",
"service",
"Dinion",
"administrator",
"admin1",
},
Passwords: []string{
"",
"admin",
"9999",
"123456",
"pass",
"camera",
"1234",
"12345",
"fliradmin",
"system",
"jvc",
"meinsm",
"root",
"4321",
"111111",
"1111111",
"password",
"ikwd",
"supervisor",
"ubnt",
"wbox123",
"service",
},
}
tests := []struct {
str string
expectedCredentials Credentials
}{
{
str: "{\"usernames\":[\"\",\"admin\",\"Admin\",\"Administrator\",\"root\",\"supervisor\",\"ubnt\",\"service\",\"Dinion\",\"administrator\",\"admin1\"],\"passwords\":[\"\",\"admin\",\"9999\",\"123456\",\"pass\",\"camera\",\"1234\",\"12345\",\"fliradmin\",\"system\",\"jvc\",\"meinsm\",\"root\",\"4321\",\"111111\",\"1111111\",\"password\",\"ikwd\",\"supervisor\",\"ubnt\",\"wbox123\",\"service\"]}",
expectedCredentials: defaultCredentials,
},
{
str: "{}",
expectedCredentials: Credentials{},
},
{
str: "{\"invalid_field\":42}",
expectedCredentials: Credentials{},
},
{
str: "not json",
expectedCredentials: Credentials{},
},
}
for _, test := range tests {
parsedCredentials, _ := ParseCredentialsFromString(test.str)
assert.Equal(t, test.expectedCredentials, parsedCredentials)
}
}
func TestParseRoutesFromString(t *testing.T) {
tests := []struct {
str string
expectedRoutes Routes
}{
{
str: "a\nb\nc",
expectedRoutes: []string{"a", "b", "c"},
},
{
str: "a",
expectedRoutes: []string{"a"},
},
{
str: "",
expectedRoutes: []string{""},
},
}
for _, test := range tests {
assert.Equal(t, test.expectedRoutes, ParseRoutesFromString(test.str))
}
}
func TestLoadTargets(t *testing.T) {
oldFS := fs
mfs := &mockedFS{}
fs = mfs
defer func() {
fs = oldFS
}()
tests := []struct {
description string
targets []string
fileExists bool
openError bool
readError bool
expectedTargets []string
expectedError error
}{
{
description: "not a file",
targets: []string{"0.0.0.0"},
fileExists: false,
expectedTargets: []string{"0.0.0.0"},
expectedError: nil,
},
{
description: "not file targets",
targets: []string{"0.0.0.0", "1.2.3.4/24"},
expectedTargets: []string{"0.0.0.0", "1.2.3.4/24"},
expectedError: nil,
},
{
description: "file contains targets",
targets: []string{"test_does_not_really_exist"},
fileExists: true,
expectedTargets: []string{"0.0.0.0", "localhost", "192.17.0.0/16", "192.168.1.140-255", "192.168.2-3.0-255"},
expectedError: nil,
},
{
description: "open error",
targets: []string{"test_does_not_really_exist"},
fileExists: true,
openError: true,
expectedTargets: []string{"test_does_not_really_exist"},
expectedError: errors.New("unable to open targets file \"test_does_not_really_exist\": file does not exist"),
},
{
description: "read error",
targets: []string{"test_does_not_really_exist"},
fileExists: true,
readError: true,
expectedTargets: []string{"test_does_not_really_exist"},
expectedError: errors.New("unable to read targets file \"test_does_not_really_exist\": file does not exist"),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
mfs.fileExists = test.fileExists
mfs.openError = test.openError
mfs.fileMock = &fileMock{
readError: test.readError,
}
mfs.fileMock.On("Close").Return(nil)
mfs.fileMock.WriteString("0.0.0.0\nlocalhost\n192.17.0.0/16\n192.168.1.140-255\n192.168.2-3.0-255")
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
targets: test.targets,
}
err := scanner.LoadTargets()
assert.Equal(t, test.expectedTargets, scanner.targets)
assert.Equal(t, test.expectedError, err)
})
}
}
// This is completely useless and just lets me
// not look at these two red lines on the coverage
// any longer.
func TestFS(t *testing.T) {
fs := osFS{}
fs.Open("test")
fs.Stat("test")
}
-49
View File
@@ -1,49 +0,0 @@
package cameradar
import "time"
// Stream represents a camera's RTSP stream
type Stream struct {
Device string `json:"device"`
Username string `json:"username"`
Password string `json:"password"`
Routes []string `json:"route"`
Address string `json:"address" validate:"required"`
Port uint16 `json:"port" validate:"required"`
CredentialsFound bool `json:"credentials_found"`
RouteFound bool `json:"route_found"`
Available bool `json:"available"`
AuthenticationType int `json:"authentication_type"`
}
// Route returns this stream's route if there is one.
func (s Stream) Route() string {
if len(s.Routes) > 0 {
return s.Routes[0]
}
return ""
}
// Credentials is a map of credentials
// usernames are keys and passwords are values
// creds['admin'] -> 'secure_password'
type Credentials struct {
Usernames []string `json:"usernames"`
Passwords []string `json:"passwords"`
}
// Routes is a slice of Routes
// ['/live.sdp', '/media.amp', ...]
type Routes []string
// Options contains all options needed to launch a complete cameradar scan
type Options struct {
Targets []string `json:"target" validate:"required"`
Ports []string `json:"ports"`
Routes Routes `json:"routes"`
Credentials Credentials `json:"credentials"`
Speed int `json:"speed"`
Timeout time.Duration `json:"timeout"`
}
-74
View File
@@ -1,74 +0,0 @@
package cameradar
import (
"strings"
"github.com/Ullaakut/nmap"
)
// Scan scans the target networks and tries to find RTSP streams within them.
//
// targets can be:
//
// - a subnet (e.g.: 172.16.100.0/24)
// - an IP (e.g.: 172.16.100.10)
// - a hostname (e.g.: localhost)
// - a range of IPs (e.g.: 172.16.100.10-20)
//
// ports can be:
//
// - one or multiple ports and port ranges separated by commas (e.g.: 554,8554-8560,18554-28554)
func (s *Scanner) Scan() ([]Stream, error) {
s.term.StartStep("Scanning the network")
// Run nmap command to discover open ports on the specified targets & ports.
nmapScanner, err := nmap.NewScanner(
nmap.WithTargets(s.targets...),
nmap.WithPorts(s.ports...),
nmap.WithServiceInfo(),
nmap.WithTimingTemplate(nmap.Timing(s.scanSpeed)),
)
if err != nil {
return nil, s.term.FailStepf("unable to create network scanner: %v", err)
}
return s.scan(nmapScanner)
}
func (s *Scanner) scan(nmapScanner nmap.ScanRunner) ([]Stream, error) {
results, warnings, err := nmapScanner.Run()
for _, warning := range warnings {
s.term.Infoln("[Nmap Warning]", warning)
}
if err != nil {
return nil, s.term.FailStepf("error while scanning network: %v", err)
}
// Get streams from nmap results.
var streams []Stream
for _, host := range results.Hosts {
for _, port := range host.Ports {
if port.Status() != "open" {
continue
}
if !strings.Contains(port.Service.Name, "rtsp") {
continue
}
for _, address := range host.Addresses {
streams = append(streams, Stream{
Device: port.Service.Product,
Address: address.Addr,
Port: port.ID,
})
}
}
}
s.term.Debugf("Found %d RTSP streams\n", len(streams))
s.term.EndStep()
return streams, nil
}
-322
View File
@@ -1,322 +0,0 @@
package cameradar
import (
"errors"
"io/ioutil"
"os"
"testing"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/nmap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type nmapMock struct {
mock.Mock
}
func (m *nmapMock) Run() (*nmap.Run, []string, error) {
args := m.Called()
if args.Get(0) != nil && args.Get(1) != nil {
return args.Get(0).(*nmap.Run), args.Get(1).([]string), args.Error(2)
}
return nil, nil, args.Error(2)
}
var (
validStream1 = Stream{
Device: "fakeDevice",
Address: "fakeAddress",
Port: 1337,
}
validStream2 = Stream{
Device: "fakeDevice",
Address: "differentFakeAddress",
Port: 1337,
}
invalidStreamNoPort = Stream{
Device: "invalidDevice",
Address: "fakeAddress",
Port: 0,
}
invalidStreamNoAddress = Stream{
Device: "invalidDevice",
Address: "",
Port: 1337,
}
)
func TestScan(t *testing.T) {
tests := []struct {
description string
targets []string
ports []string
speed int
removePath bool
expectedErr error
expectedStreams []Stream
}{
{
description: "create new scanner and call scan, no error",
targets: []string{"localhost"},
ports: []string{"80"},
speed: 5,
},
{
description: "create new scanner with missing nmap installation",
removePath: true,
ports: []string{"80"},
expectedErr: errors.New("unable to create network scanner: nmap binary was not found"),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
if test.removePath {
os.Setenv("PATH", "")
}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
targets: test.targets,
ports: test.ports,
scanSpeed: test.speed,
}
result, err := scanner.Scan()
assert.Equal(t, test.expectedErr, err)
assert.Equal(t, test.expectedStreams, result)
})
}
}
func TestInternalScan(t *testing.T) {
tests := []struct {
description string
nmapResult *nmap.Run
nmapWarnings []string
nmapError error
expectedStreams []Stream
expectedErr error
}{
{
description: "valid streams",
nmapResult: &nmap.Run{
Hosts: []nmap.Host{
{
Addresses: []nmap.Address{
{
Addr: validStream1.Address,
},
},
Ports: []nmap.Port{
{
State: nmap.State{
State: "open",
},
ID: validStream1.Port,
Service: nmap.Service{
Name: "rtsp",
Product: validStream1.Device,
},
},
},
},
{
Addresses: []nmap.Address{
{
Addr: validStream2.Address,
},
},
Ports: []nmap.Port{
{
State: nmap.State{
State: "open",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "rtsp-alt",
Product: validStream2.Device,
},
},
},
},
},
},
expectedStreams: []Stream{validStream1, validStream2},
},
{
description: "two invalid targets, no error",
nmapResult: &nmap.Run{
Hosts: []nmap.Host{
{
Addresses: []nmap.Address{
{
Addr: invalidStreamNoPort.Address,
},
},
},
{
Addresses: []nmap.Address{},
Ports: []nmap.Port{
{
State: nmap.State{
State: "open",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "rtsp-alt",
Product: invalidStreamNoAddress.Device,
},
},
},
},
},
},
expectedStreams: nil,
},
{
description: "different port states, no error",
nmapResult: &nmap.Run{
Hosts: []nmap.Host{
{
Addresses: []nmap.Address{
{
Addr: invalidStreamNoPort.Address,
}},
Ports: []nmap.Port{
{
State: nmap.State{
State: "closed",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "rtsp-alt",
Product: invalidStreamNoAddress.Device,
},
},
},
},
{
Addresses: []nmap.Address{
{
Addr: invalidStreamNoPort.Address,
}},
Ports: []nmap.Port{
{
State: nmap.State{
State: "unfiltered",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "rtsp-alt",
Product: invalidStreamNoAddress.Device,
},
},
},
},
{
Addresses: []nmap.Address{
{
Addr: invalidStreamNoPort.Address,
}},
Ports: []nmap.Port{
{
State: nmap.State{
State: "filtered",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "rtsp-alt",
Product: invalidStreamNoAddress.Device,
},
},
},
},
},
},
expectedStreams: nil,
},
{
description: "not rtsp, no error",
nmapResult: &nmap.Run{
Hosts: []nmap.Host{
{
Addresses: []nmap.Address{
{
Addr: invalidStreamNoPort.Address,
}},
Ports: []nmap.Port{
{
State: nmap.State{
State: "open",
},
ID: validStream2.Port,
Service: nmap.Service{
Name: "tcp",
Product: invalidStreamNoAddress.Device,
},
},
},
},
},
},
expectedStreams: nil,
},
{
description: "no hosts found",
nmapResult: &nmap.Run{},
expectedStreams: nil,
},
{
description: "scan failed",
nmapError: errors.New("scan failed"),
nmapWarnings: []string{"invalid host"},
expectedErr: errors.New("error while scanning network: scan failed"),
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
nmapMock := &nmapMock{}
nmapMock.On("Run").Return(test.nmapResult, test.nmapWarnings, test.nmapError)
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(ioutil.Discard)),
}
results, err := scanner.scan(nmapMock)
assert.Equal(t, test.expectedErr, err)
assert.Equal(t, test.expectedStreams, results, "wrong streams parsed")
assert.Equal(t, len(test.expectedStreams), len(results), "wrong streams parsed")
nmapMock.AssertExpectations(t)
})
}
}
-162
View File
@@ -1,162 +0,0 @@
package cameradar
import (
"fmt"
"os"
"time"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/disgo/style"
curl "github.com/Ullaakut/go-curl"
)
const (
defaultCredentialDictionaryPath = "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/credentials.json"
defaultRouteDictionaryPath = "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/routes"
)
// Scanner represents a cameradar scanner. It scans a network and
// attacks all streams found to get their RTSP credentials.
type Scanner struct {
curl Curler
term *disgo.Terminal
targets []string
ports []string
debug bool
verbose bool
scanSpeed int
attackInterval time.Duration
timeout time.Duration
credentialDictionaryPath string
routeDictionaryPath string
credentials Credentials
routes Routes
}
// New creates a new Cameradar Scanner and applies the given options.
func New(options ...func(*Scanner)) (*Scanner, error) {
err := curl.GlobalInit(curl.GLOBAL_ALL)
if err != nil {
return nil, fmt.Errorf("unable to initialize curl library: %v", err)
}
handle := curl.EasyInit()
if handle == nil {
return nil, fmt.Errorf("unable to initialize curl handle: %v", err)
}
scanner := &Scanner{
curl: &Curl{CURL: handle},
credentialDictionaryPath: defaultCredentialDictionaryPath,
routeDictionaryPath: defaultRouteDictionaryPath,
}
for _, option := range options {
option(scanner)
}
gopath := os.Getenv("GOPATH")
if gopath == "" && scanner.credentialDictionaryPath == defaultCredentialDictionaryPath && scanner.routeDictionaryPath == defaultRouteDictionaryPath {
disgo.Errorln(style.Failure("No $GOPATH was found.\nDictionaries may not be loaded properly, please set your $GOPATH to use the default dictionaries."))
}
scanner.credentialDictionaryPath = os.ExpandEnv(scanner.credentialDictionaryPath)
scanner.routeDictionaryPath = os.ExpandEnv(scanner.routeDictionaryPath)
scanner.term = disgo.NewTerminal(
disgo.WithDebug(scanner.debug),
)
err = scanner.LoadTargets()
if err != nil {
return nil, fmt.Errorf("unable to parse target file: %v", err)
}
scanner.term.StartStepf("Loading credentials")
err = scanner.LoadCredentials()
if err != nil {
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
}
scanner.term.StartStepf("Loading routes")
err = scanner.LoadRoutes()
if err != nil {
return nil, scanner.term.FailStepf("unable to load credentials dictionary: %v", err)
}
disgo.EndStep()
return scanner, nil
}
// WithTargets specifies the targets to scan and attack.
func WithTargets(targets []string) func(s *Scanner) {
return func(s *Scanner) {
s.targets = targets
}
}
// WithPorts specifies the ports to scan and attack.
func WithPorts(ports []string) func(s *Scanner) {
return func(s *Scanner) {
s.ports = ports
}
}
// WithDebug specifies whether or not to enable debug logs.
func WithDebug(debug bool) func(s *Scanner) {
return func(s *Scanner) {
s.debug = debug
}
}
// WithVerbose specifies whether or not to enable verbose logs.
func WithVerbose(verbose bool) func(s *Scanner) {
return func(s *Scanner) {
s.verbose = verbose
}
}
// WithCustomCredentials specifies a custom credential dictionary
// to use for the attacks.
func WithCustomCredentials(dictionaryPath string) func(s *Scanner) {
return func(s *Scanner) {
s.credentialDictionaryPath = dictionaryPath
}
}
// WithCustomRoutes specifies a custom route dictionary
// to use for the attacks.
func WithCustomRoutes(dictionaryPath string) func(s *Scanner) {
return func(s *Scanner) {
s.routeDictionaryPath = dictionaryPath
}
}
// WithScanSpeed specifies the speed at which the scan should be executed. Faster
// means easier to detect, slower has bigger timeout values and is more silent.
func WithScanSpeed(speed int) func(s *Scanner) {
return func(s *Scanner) {
s.scanSpeed = speed
}
}
// WithAttackInterval specifies the interval of time during which Cameradar
// should wait between each attack attempt during bruteforcing.
// Setting a high value for this obviously makes attacks much slower.
func WithAttackInterval(interval time.Duration) func(s *Scanner) {
return func(s *Scanner) {
s.attackInterval = interval
}
}
// WithTimeout specifies the amount of time after which attack requests should
// timeout. This should be high if the network you are attacking has a poor
// connectivity or that you are located far away from it.
func WithTimeout(timeout time.Duration) func(s *Scanner) {
return func(s *Scanner) {
s.timeout = timeout
}
}
-150
View File
@@ -1,150 +0,0 @@
package cameradar
import (
"fmt"
"io/ioutil"
"os"
"testing"
"time"
curl "github.com/Ullaakut/go-curl"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
tests := []struct {
description string
targets []string
ports []string
debug bool
verbose bool
customCredentials string
customRoutes string
speed int
attackInterval time.Duration
timeout time.Duration
loadTargetsFail bool
loadCredsFail bool
loadRoutesFail bool
curlGlobalFail bool
curlEasyFail bool
expectedErr bool
}{
{
description: "no error while loading dictionaries",
targets: []string{"titi", "toto"},
ports: []string{"554"},
debug: true,
verbose: false,
speed: 3,
timeout: time.Millisecond,
},
{
description: "unable to load targets",
loadTargetsFail: true,
expectedErr: true,
},
{
description: "unable to load credentials",
loadCredsFail: true,
expectedErr: true,
},
{
description: "unable to load routes",
loadRoutesFail: true,
expectedErr: true,
},
{
description: "curl fails to init",
curlGlobalFail: true,
expectedErr: true,
},
{
description: "curl fails to create handle",
curlEasyFail: true,
expectedErr: true,
},
{
description: "gopath not set and default dicts",
customCredentials: defaultCredentialDictionaryPath,
customRoutes: defaultRouteDictionaryPath,
expectedErr: true,
},
}
// Temporarily empty the gopath for testing purposes.
defer os.Setenv("GOPATH", os.Getenv("GOPATH"))
for i, test := range tests {
t.Run(test.description, func(t *testing.T) {
os.Setenv("GOPATH", "")
if test.loadTargetsFail {
test.targets = []string{generateTmpFileName(i, "targets")}
ioutil.WriteFile(test.targets[0], []byte(`0.0.0.0`), 0000)
}
if !test.loadCredsFail && test.customCredentials == "" {
test.customCredentials = generateTmpFileName(i, "creds")
ioutil.WriteFile(test.customCredentials, []byte(`{"usernames":["admin"],"passwords":["admin"]}`), 0644)
}
if !test.loadRoutesFail && test.customRoutes == "" {
test.customRoutes = generateTmpFileName(i, "routes")
ioutil.WriteFile(test.customRoutes, []byte(`live.sdp`), 0644)
}
curl.TestGlobalFail = test.curlGlobalFail
curl.TestEasyFail = test.curlEasyFail
scanner, err := New(
WithTargets(test.targets),
WithPorts(test.ports),
WithDebug(test.debug),
WithVerbose(test.verbose),
WithScanSpeed(test.speed),
WithAttackInterval(test.attackInterval),
WithTimeout(test.timeout),
WithCustomCredentials(test.customCredentials),
WithCustomRoutes(test.customRoutes),
)
if test.expectedErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if scanner != nil {
assert.Equal(t, test.targets, scanner.targets)
assert.Equal(t, test.ports, scanner.ports)
assert.Equal(t, test.debug, scanner.debug)
assert.Equal(t, test.verbose, scanner.verbose)
assert.Equal(t, test.speed, scanner.scanSpeed)
assert.Equal(t, test.attackInterval, scanner.attackInterval)
assert.Equal(t, test.timeout, scanner.timeout)
}
})
}
}
func generateTmpFileName(iteration int, purpose string) string {
return fmt.Sprintf("/tmp/cameradar_test_scanner_%s_%d_%d", purpose, time.Now().Unix(), iteration)
}
-68
View File
@@ -1,68 +0,0 @@
package cameradar
import (
"github.com/Ullaakut/disgo/style"
curl "github.com/Ullaakut/go-curl"
)
// PrintStreams prints information on each stream.
func (s *Scanner) PrintStreams(streams []Stream) {
if len(streams) == 0 {
s.term.Infof("%s No streams were found. Please make sure that your target is on an accessible network.\n", style.Failure(style.SymbolCross))
}
success := 0
for _, stream := range streams {
if stream.Available {
s.term.Infof("%s\tDevice RTSP URL:\t%s\n", style.Success(style.SymbolRightTriangle), style.Link(GetCameraRTSPURL(stream)))
s.term.Infof("\tAvailable:\t\t%s\n", style.Success(style.SymbolCheck))
success++
} else {
s.term.Infof("%s\tAdmin panel URL:\t%s You can use this URL to try attacking the camera's admin panel instead.\n", style.Failure(style.SymbolCross), style.Link(GetCameraAdminPanelURL(stream)))
s.term.Infof("\tAvailable:\t\t%s\n", style.Failure(style.SymbolCross))
}
if len(stream.Device) > 0 {
s.term.Infof("\tDevice model:\t\t%s\n\n", stream.Device)
}
s.term.Infof("\tIP address:\t\t%s\n", stream.Address)
s.term.Infof("\tRTSP port:\t\t%d\n", stream.Port)
switch stream.AuthenticationType {
case curl.AUTH_NONE:
s.term.Infoln("\tThis camera does not require authentication")
case curl.AUTH_BASIC:
s.term.Infoln("\tAuth type:\t\tbasic")
case curl.AUTH_DIGEST:
s.term.Infoln("\tAuth type:\t\tdigest")
}
if stream.CredentialsFound {
s.term.Infof("\tUsername:\t\t%s\n", style.Success(stream.Username))
s.term.Infof("\tPassword:\t\t%s\n", style.Success(stream.Password))
} else {
s.term.Infof("\tUsername:\t\t%s\n", style.Failure("not found"))
s.term.Infof("\tPassword:\t\t%s\n", style.Failure("not found"))
}
s.term.Infoln("\tRTSP routes:")
if stream.RouteFound {
for _, route := range stream.Routes {
s.term.Infoln(style.Success("\t\t\t\t/" + route))
}
} else {
s.term.Infoln(style.Failure("not found"))
}
s.term.Info("\n\n")
}
if success > 1 {
s.term.Infof("%s Successful attack: %s devices were accessed", style.Success(style.SymbolCheck), style.Success(len(streams)))
} else if success == 1 {
s.term.Infof("%s Successful attack: %s device was accessed", style.Success(style.SymbolCheck), style.Success("one"))
} else {
s.term.Infof("%s Streams were found but none were accessed. They are most likely configured with secure credentials and routes. You can try adding entries to the dictionary or generating your own in order to attempt a bruteforce attack on the cameras.\n", style.Failure("\xE2\x9C\x96"))
}
}
-186
View File
@@ -1,186 +0,0 @@
package cameradar
import (
"bytes"
"testing"
"github.com/Ullaakut/disgo"
"github.com/stretchr/testify/assert"
)
var (
unavailable = Stream{}
available = Stream{
Available: true,
}
deviceFound = Stream{
Device: "devicename",
}
noAuth = Stream{
AuthenticationType: 0,
}
basic = Stream{
AuthenticationType: 1,
}
digest = Stream{
AuthenticationType: 2,
}
credsFound = Stream{
CredentialsFound: true,
Username: "us3r",
Password: "p4ss",
}
routeFound = Stream{
RouteFound: true,
Routes: []string{"r0ute"},
}
)
func TestPrintStreams(t *testing.T) {
tests := []struct {
description string
streams []Stream
expectedLogs []string
}{
{
description: "displays the proper message when no streams found",
streams: nil,
expectedLogs: []string{"No streams were found"},
},
{
description: "displays the admin panel URL when a stream is not accessible",
streams: []Stream{
unavailable,
},
expectedLogs: []string{"Admin panel URL"},
},
{
description: "displays the device name when it is found",
streams: []Stream{
deviceFound,
},
expectedLogs: []string{"Device model:"},
},
{
description: "displays authentication type (no auth)",
streams: []Stream{
noAuth,
},
expectedLogs: []string{"This camera does not require authentication"},
},
{
description: "displays authentication type (basic)",
streams: []Stream{
basic,
},
expectedLogs: []string{"basic"},
},
{
description: "displays authentication type (digest)",
streams: []Stream{
digest,
},
expectedLogs: []string{"digest"},
},
{
description: "displays credentials properly",
streams: []Stream{
credsFound,
},
expectedLogs: []string{
"Username",
"us3r",
"Password",
"p4ss",
},
},
{
description: "displays route properly",
streams: []Stream{
routeFound,
},
expectedLogs: []string{
"RTSP route",
"/r0ute",
},
},
{
description: "displays successes properly (no success)",
streams: []Stream{
unavailable,
},
expectedLogs: []string{
"Streams were found but none were accessed",
},
},
{
description: "displays successes properly (1 success)",
streams: []Stream{
available,
},
expectedLogs: []string{
"Successful attack",
"device was accessed",
},
},
{
description: "displays successes properly (multiple successes)",
streams: []Stream{
available,
available,
available,
available,
},
expectedLogs: []string{
"Successful attack",
"devices were accessed",
},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
writer := &bytes.Buffer{}
scanner := &Scanner{
term: disgo.NewTerminal(disgo.WithDefaultOutput(writer)),
}
scanner.PrintStreams(test.streams)
for _, expectedLog := range test.expectedLogs {
assert.Contains(t, writer.String(), expectedLog)
}
})
}
}
-191
View File
@@ -1,191 +0,0 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"sort"
"strings"
"sync"
"github.com/Ullaakut/disgo/style"
"github.com/PuerkitoBio/goquery"
"github.com/Ullaakut/disgo"
"github.com/vbauerster/mpb"
"github.com/vbauerster/mpb/decor"
)
const dictionaryURL = "https://community.geniusvision.net/platform/cprndr/manulist"
var rtspURLsFound sync.Map
func main() {
if err := updateDictionary(); err != nil {
log.Fatalf(err.Error())
}
}
func updateDictionary() error {
disgo.SetTerminalOptions(disgo.WithColors(true), disgo.WithDebug(true))
disgo.StartStep("Fetching dictionary list")
resp, err := http.Get(dictionaryURL)
if err != nil {
return disgo.FailStepf("unable to download dictionaries: %v", err)
}
defer resp.Body.Close()
disgo.StartStep("Parsing dictionary list")
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return disgo.FailStepf("unable to read from dictionary list: %v", err)
}
var vendorURLs []string
doc.Find("td.simpletable a").Each(func(i int, s *goquery.Selection) {
url, ok := s.Attr("href")
if !ok {
return
}
if url != "javascript:void(0)" {
vendorURLs = append(vendorURLs, url)
}
})
disgo.StartStep("Loading current cameradar dictionary")
currentDictionary, err := ioutil.ReadFile("dictionaries/routes")
if err != nil {
return disgo.FailStepf("unable to read current dictionary: %v", err)
}
dictionaryEntries := bytes.Split(currentDictionary, []byte("\n"))
for _, rtspURL := range dictionaryEntries {
rtspURLsFound.Store(string(rtspURL), struct{}{})
}
disgo.Debugf("Current dictionary has %d entries\n", len(dictionaryEntries))
disgo.EndStep()
p := mpb.New(mpb.WithWidth(64))
name := fmt.Sprintf("Fetching default routes from %d constructors:", len(vendorURLs))
bar := p.AddBar(int64(len(vendorURLs)),
// set custom bar style, default one is "[=>-]"
mpb.BarStyle("╢▌▌░╟"),
mpb.PrependDecorators(
// display our name with one space on the right
decor.Name(name, decor.WC{W: len(name), C: decor.DidentRight}),
),
mpb.AppendDecorators(decor.Percentage()),
)
for _, url := range vendorURLs {
go loadRoutes(url, bar)
}
p.Wait()
disgo.StartStep("Converting found routes into proper data model")
var rtspURLs []string
rtspURLsFound.Range(func(rtspURL, _ interface{}) bool {
disgo.Infoln("Adding URL", rtspURL.(string))
rtspURLs = append(rtspURLs, rtspURL.(string))
return true
})
sort.Slice(rtspURLs, func(a, b int) bool {
return rtspURLs[a] < rtspURLs[b]
})
disgo.EndStep()
if len(dictionaryEntries) < len(rtspURLs) {
disgo.Infof("%s Saving them in cameradar default dictionary.\n", style.Success("Found ", len(rtspURLs)-len(dictionaryEntries), " new entries!"))
saveRoutes(rtspURLs)
} else {
disgo.Infoln(style.Success("No new entry found, dictionary up-to-date! :)"))
}
return nil
}
func loadRoutes(url string, bar *mpb.Bar) {
defer bar.IncrBy(1)
var (
failureCounter int
resp *http.Response
err error
)
for failureCounter < 5 {
resp, err = http.Get(url)
if err != nil {
failureCounter++
} else {
break
}
}
if failureCounter == 5 {
disgo.Errorln("Request failed 5 times in a row, giving up on this vendor")
return
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
disgo.Errorf("unable to read from dictionary list for URL %q: %v\n", url, err)
return
}
doc.Find("tr.simpletable td.simpletable:nth-child(4) a").Each(func(i int, s *goquery.Selection) {
rtspURL := s.Text()
if strings.HasPrefix(rtspURL, "(") && strings.HasSuffix(rtspURL, ")") {
return
}
if strings.HasPrefix(rtspURL, "[") && strings.HasSuffix(rtspURL, "]") {
return
}
if strings.HasPrefix(rtspURL, "http://") {
return
}
// Skip the port and only get the route.
if strings.HasPrefix(rtspURL, "rtsp://ip-addr:") {
routeAndPort := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr:"))
route := strings.TrimLeft(routeAndPort, "0123456789/")
rtspURLsFound.Store(route, struct{}{})
return
}
switch rtspURL {
case "",
"rtsp://ip-addr/",
"rtsp://ip-addr",
"rtsp://ip-addr:pass@10.0.0.5:6667/blinkhd":
return
default:
route := strings.TrimSpace(strings.TrimPrefix(rtspURL, "rtsp://ip-addr/"))
rtspURLsFound.Store(route, struct{}{})
}
})
}
func saveRoutes(rtspURLs []string) {
contents := strings.Join(rtspURLs, "\n")
disgo.StartStep("Writing new dictionary file")
err := ioutil.WriteFile("dictionaries/routes", []byte(contents), 0644)
if err != nil {
disgo.FailStepf("unable to write dictionary: %v", err)
}
}
-10
View File
@@ -1,10 +0,0 @@
module github.com/Ullaakut/cameradar/magefile
go 1.16
require (
github.com/Ullaakut/disgo v0.3.1
github.com/fatih/color v1.10.0 // indirect
github.com/magefile/mage v1.11.0
github.com/stretchr/testify v1.7.0 // indirect
)
-23
View File
@@ -1,23 +0,0 @@
github.com/Ullaakut/disgo v0.3.1 h1:BGGVHynji41KGuGI02ztTCnILRvyzlvmiCRl5bBpjKk=
github.com/Ullaakut/disgo v0.3.1/go.mod h1:/CSvpnYVSKOeh2dvUvx9cXshzz2t7T1/lRO/MrFj3fI=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-109
View File
@@ -1,109 +0,0 @@
//+build mage
package main
import (
"os"
"github.com/magefile/mage/sh"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/disgo/style"
)
var supportedPlatforms = map[string]string{
"linux/amd64": "ullaakut/cameradar:amd64",
"linux/386": "ullaakut/cameradar:386",
"linux/arm64": "ullaakut/cameradar:arm64",
"linux/arm/v7": "ullaakut/cameradar:armv7",
//"linux/riscv64": "ullaakut/cameradar:riscv64", // UNSUPPORTED.
//"linux/ppc64le": "ullaakut/cameradar:ppc64le", // UNSUPPORTED.
//"linux/s390x": "ullaakut/cameradar:s390x", // UNSUPPORTED.
//"linux/arm/v6": "ullaakut/cameradar:armv6", // UNSUPPORTED.
}
var Default = Build
// Follows https://www.docker.com/blog/multi-platform-docker-builds/.
func Build() error {
term := disgo.NewTerminal(disgo.WithColors(true))
term.StartStep("Building images for all platforms")
term.Infof("Builds planned for %v\n", supportedPlatforms)
for platform, name := range supportedPlatforms {
term.Infoln("Building image for", platform, "at", name)
// docker buildx build --platform linux/arm/v7 -t ullaakut/cameradar:armv7 .
if err := sh.Run("docker", "buildx", "build", "--platform", platform, "-t", name, "../../"); err != nil {
return term.FailStepf("unable to build image: %v", err)
}
}
term.Infoln(style.Success("Cross-platform docker build successful."))
return nil
}
func Publish() error {
term := disgo.NewTerminal(disgo.WithColors(true))
term.StartStep("Pushing images to DockerHub")
term.Infoln("Pushing ullaakut/cameradar:latest")
if err := sh.Run("docker", "push", "ullaakut/cameradar:latest"); err != nil {
return term.FailStepf("unable to push latest docker images to docker hub: %v", err)
}
if version, exists := os.LookupEnv("CAMERADAR_VERSION"); exists {
term.Infoln("Pushing ullaakut/cameradar:"+version)
if err := sh.Run("docker", "push", "ullaakut/cameradar:"+version); err != nil {
return term.FailStepf("unable to push versionned docker images to docker hub: %v", err)
}
}
term.StartStep("Pushing images to GitHub Packages")
term.Infoln("Pushing docker.pkg.github.com/ullaakut/cameradar/cameradar:latest")
if err := sh.Run("docker", "tag", "ullaakut/cameradar:latest", "docker.pkg.github.com/ullaakut/cameradar/cameradar:latest"); err != nil {
return term.FailStepf("unable to push latest docker images to docker hub: %v", err)
}
if err := sh.Run("docker", "push", "docker.pkg.github.com/ullaakut/cameradar/cameradar:latest"); err != nil {
return term.FailStepf("unable to push latest docker images to docker hub: %v", err)
}
if version, exists := os.LookupEnv("CAMERADAR_VERSION"); exists {
term.Infoln("Pushing docker.pkg.github.com/ullaakut/cameradar/cameradar:"+version)
if err := sh.Run("docker", "tag", "ullaakut/cameradar:"+version, "docker.pkg.github.com/ullaakut/cameradar/cameradar:"+version); err != nil {
return term.FailStepf("unable to push latest docker images to docker hub: %v", err)
}
if err := sh.Run("docker", "push", "ullaakut/cameradar:"+version); err != nil {
return term.FailStepf("unable to push versionned docker images to docker hub: %v", err)
}
}
term.StartStep("Creating manifest(s) for cross platform builds")
var manifestImages []string
for _, image := range supportedPlatforms {
manifestImages = append(manifestImages, image)
}
args := []string{"manifest", "create", "--amend", "ullaakut/cameradar:latest"}
args = append(args, manifestImages...)
// docker manifest create ullaakut/cameradar:latest ullaakut/cameradar:amd64 ullaakut/cameradar:armv7 [...]
if err := sh.Run("docker", args...); err != nil {
return term.FailStepf("unable to create manifest: %v", err)
}
if version, exists := os.LookupEnv("CAMERADAR_VERSION"); exists {
args = []string{"manifest", "create", "--amend", "ullaakut/cameradar:"+version}
args = append(args, manifestImages...)
if err := sh.Run("docker", args...); err != nil {
return term.FailStepf("unable to create manifest: %v", err)
}
}
term.EndStep()
term.Infoln(style.Success("Images published successfully."))
return nil
}