Skip to content

Commit

Permalink
Merge pull request #267 from richard-austin/stage_6_handle_motion_ser…
Browse files Browse the repository at this point in the history
…vice_triggered_recordings_with_camera_recordings_service

Stage 6 handle motion service triggered recordings with camera recordings service
  • Loading branch information
richard-austin committed Mar 13, 2024
2 parents 8fa3c65 + 412b92f commit 085c2e9
Show file tree
Hide file tree
Showing 85 changed files with 1,717 additions and 1,305 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/xtrn-scripts-and-config/productKeyGen/generateProductKey.jar
/.profileconfig.json
3 changes: 3 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified README.images/config-editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added README.images/config-editor2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
144 changes: 64 additions & 80 deletions README.md

Large diffs are not rendered by default.

Binary file modified README.pdf
Binary file not shown.
102 changes: 102 additions & 0 deletions README_CLOUD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
## Accessing the NVR through the Cloud Service

### Introduction

The Cloud Service gives an alternative method of access to NVRs. The NVR makes a client
connection to the Clouds ActiveMQ server, which obviates the requirement
for port forwarding when the NVR is behind NAT, when the Cloud Server is on a public IP.
The Cloud Service can host one or more NVRs, each accessible separately
using the appropriate account credentials. These credentials are set up on the Cloud
Service and are not necessarily the same as the NVRs direct access
credentials. It is possible to access NVRs which have no direct
access credentials on the Cloud server. To set up a cloud account for an NVR
you must have the product ID for the NVR. This ID is generated on the initial
installation of the NVR, and is shown near the end of the text that comes
up on the terminal during installation. It is not changed on subsequent upgrades.

There is no live implementation of the Cloud Service, the source code
is <a href="https://github.com/richard-austin/cloud-server">here</a>.

The Cloud Service and <a href="https://github.com/richard-austin/activemq-for-cloud-service">ActiveMQ</a> should be set
up
(with necessary configuration adjustments) on (preferably) a public server to enable the
use of NVRs through this means.
### Setting up the NVR for the Cloud Service

* The NVR must have the correct cloudActiveMQUrl set up in application.yml which should contain the domain/IP and port
used by ActiveMQ
for the Cloud Service. If this needs to be changed, the tomcat service should be restarted to apply the update
(***sudo systemctl restart tomcat9.service***)
* If there is a local account for direct access on the NVR, you should to ensure that
the link to the Cloud Service is enabled: -
* **General** menu
* **Set CloudProxy Status**
* Ensure the checkbox is checked.
* NVR connection to Cloud ActiveMQ
* If the connection is successful, you should see a green dialogue box
with the message "**Success: Connected to the Cloud successfully**".
You should then be able to use the Cloud Service.
* If the connection to ActiveMQ fails, the message <span style="color: darkred; font-weight: bold">Not Connected To
Transport</span> will be shown.
* This can mean that the ActiveMQ url (in the NVR application.yml file) is not set correctly or that
ActiveMQ is not running.
* The error message <span style="color: darkred">Product key was not accepted by the Cloud server</span>
indicates that the NVR connected successfully to ActiveMQ, but it was not authenticated on the Cloud Server. Check that the Cloud Server is running, and it is configured correctly to connect to ActiveMQ.

* If there is no local NVR account, connection to the Cloud Service will be automatically enabled.
In this case the NVR cannot indicate if it has failed to connect to Active MQ.
You can check the cloudproxy.log at /var/log/security-cam
<div style="margin-left: 2rem">

* If connection was successful, the log should have a line like: -
<div style="color: green; margin-left: 2rem">2024-02-22 16:44:20.341 INFO CLOUDPROXY [pool-248-thread-1] - loginToCloud:191 - Connected successfully to the Cloud</div>
<div style="margin-left: 2rem;margin-top:0.25rem">If this line follows previous errors, it would mean that the connection was successful, provided no further errors follow this message.</div>

* If the NVR failed to connect to ActiveMQ, the log will have the following line.

<div style="color: darkred; margin-left: 2rem">2024-02-22 16:42:00.747 ERROR CLOUDPROXY [pool-230-thread-1] - showExceptionDetails:539 - javax.jms.JMSException exception in Clo
udAMQProxy.start: Could not connect to broker URL: ssl://192.168.1.82:61617?socket.verifyHostName=false. Reason: java.net.NoRoute
ToHostException: No route to host</div>
<div style="margin-left: 2rem; margin-top: 0.25rem">This can mean that the ActiveMQ url (in the NVR application.yml file) is not set correctly or that
ActiveMQ is not running.</div>

* If the log has the following line: -
<div style="color: darkred; margin-left: 2rem">2024-02-22 16:43:57.623 ERROR CLOUDPROXY [pool-245-thread-1] - loginToCloud:200 - Product key was not accepted by the Cloud server</div>
<div style="margin-left: 2rem; margin-top: 0.25rem">This indicates that the NVR connected successfully to ActiveMQ, but it was not authenticated on the Cloud Server.
Check that the Cloud Server is running, and it is configured correctly to connect to ActiveMQ.</div>
</div>

* **Hosting of camera admin page**, This feature is not supported through the Cloud Service.

When the NVR is accessed through the Cloud service, port forwarding is not required
as all communication is through a client connection that the NVR makes to the
Cloud service. Camera web admin pages are not accessible through the Cloud Service.

### Cloudproxy parameters in application.yml

#### These are under the cloudProxy section :-

| *Parameter* | Description |
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| enabled | CloudProxy will run if true (and CloudProxy status is enabled on the NVR General menu) |
| mqTrustStorePath ** | Path to the trust store which contains the ActiveMQ servers certificate |
| mqKeyStorePath ** | Path the the ActiveMQ client key store |
| mqTrustStorePassword * | Password for the trust store |
| mqKeyStorePassword * | Password for the keystore |
| mqUser * | ActiveMQ user name |
| mqPassword * | ActiveMQ password |
| productKeyPath | Path to the file containing the encrypted NVR Product key |
| cloudActiveMQUrl | Url to the ActiveMQ service that the NVRs and the Cloud server connect to. This should begin with failover://ssl: |
| activeMQInitQueue | The name of the queue in ActiveMQ through which connections are initiated. This must be the same on all NVRs and the Cloud server. |
| webServerForCloudProxyHost | The host name for the NVRs cloud web server (normally localhost) |
| webServerForCloudProxyPort | The port for the NVRs web server (Normally 8088) This service is set up on nginx to provide special access for Cloud connections. |
| logLevel | The log level for cloudproxy.log (normally located at /var/log/security-cam) |

&ast; You may want to change these from their defaults. The ActiveMQ user name and password must obviously be changed
in the ActiveMQ and Cloud server settings as well as in this config.

&ast;&ast; You may want to create your own keys and certs for the Cloud and
NVRs <a href="https://activemq.apache.org/how-do-i-use-ssl">see here</a>

Please see the README.md for the Cloud Service for details on setting up a cloud account and using the Cloud service.

4 changes: 2 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@stomp/stompjs": "^7.0.0",
"engine.io": "^6.5.4",
"file-saver": "^2.0.5",
"hls.js": "^1.0.7",
"hls.js": "<=1.4.12",
"moment": "^2.29.1",
"object-hash": "^3.0.0",
"rx": "^4.1.0",
Expand All @@ -45,7 +45,7 @@
"codelyzer": "^6.0.0",
"jasmine-core": "~3.8.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "^6.3.0",
"karma": "^6.4.3",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
Expand Down
10 changes: 7 additions & 3 deletions client/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {MatSortModule} from "@angular/material/sort";
import {MatTooltipModule} from "@angular/material/tooltip";
import { ExcludeOwnStreamPipe } from './config-setup/exclude-own-stream.pipe';
import { DisableControlDirective } from './shared/disable-control.directive';
import { CredentialsForCameraAccessComponent } from './credentials-for-camera-access/credentials-for-camera-access.component';
import { OnvifCredentialsComponent } from './config-setup/camera-credentials/onvif-credentials.component';
import { CloudProxyComponent } from './cloud-proxy/cloud-proxy.component';
import { ChangeEmailComponent } from './change-email/change-email.component';
import { SetUpGuestAccountComponent } from './set-up-guest-account/set-up-guest-account.component';
Expand All @@ -59,6 +59,8 @@ import {GetActiveIPAddressesComponent} from './get-active-ipaddresses/get-active
import { CreateUserAccountContainerComponent } from './create-user-account-container/create-user-account-container.component';
import { AudioInputPipe } from './video/audio-input.pipe';
import { AddAsOnvifDeviceComponent } from './config-setup/add-as-onvif-device/add-as-onvif-device.component';
import { SafeHtmlPipe } from './shared/safe-html.pipe';
import { OnvifFailuresComponent } from './config-setup/onvif-failures/onvif-failures.component';

@NgModule({
declarations: [
Expand All @@ -79,7 +81,7 @@ import { AddAsOnvifDeviceComponent } from './config-setup/add-as-onvif-device/ad
ConfigSetupComponent,
ExcludeOwnStreamPipe,
DisableControlDirective,
CredentialsForCameraAccessComponent,
OnvifCredentialsComponent,
CloudProxyComponent,
ChangeEmailComponent,
ChangeEmailComponent,
Expand All @@ -93,7 +95,9 @@ import { AddAsOnvifDeviceComponent } from './config-setup/add-as-onvif-device/ad
WifiSettingsComponent,
CreateUserAccountContainerComponent,
AudioInputPipe,
AddAsOnvifDeviceComponent
AddAsOnvifDeviceComponent,
SafeHtmlPipe,
OnvifFailuresComponent
],
imports: [
BrowserModule,
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/cameras/Camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,5 @@ export class Camera
rtspTransport: string = "tcp";
useRtspAuth: boolean = false;
retriggerWindow: number = 30;

cred: string = "";
}
70 changes: 53 additions & 17 deletions client/src/app/cameras/camera.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {BaseUrl} from "../shared/BaseUrl/BaseUrl";
import {Observable, throwError} from "rxjs";
import {catchError, map, tap} from "rxjs/operators";
import {AudioEncoding, Camera, CameraParamSpec, Stream} from "./Camera";
import {CameraAdminCredentials} from "../credentials-for-camera-access/credentials-for-camera-access.component";
import {NativeDateAdapter} from '@angular/material/core';
import {KeyValue} from "@angular/common";

Expand Down Expand Up @@ -48,6 +47,13 @@ export class CustomDateAdapter extends NativeDateAdapter {
}
}

export class OnvifCredentials
{
userName: string='';
password: string='';
}


export enum cameraType {none, sv3c, zxtechMCW5B10X}

@Injectable({
Expand All @@ -70,6 +76,7 @@ export class CameraService {
private cameras: Map<string, Camera> = new Map();

errorEmitter: EventEmitter<HttpErrorResponse> = new EventEmitter<HttpErrorResponse>();
private _publicKey!: Uint8Array;

public readonly _cameraParamSpecs: CameraParamSpec[] =
[new CameraParamSpec(
Expand Down Expand Up @@ -126,11 +133,15 @@ export class CameraService {
get preambleFrameValues() {
return this._preambleFrameValues;
}

get publicKey() : Uint8Array {
return this._publicKey;
}
constructor(private http: HttpClient, private _baseUrl: BaseUrl) {
this.loadCameras().subscribe((cams) => {
this.cameras = cams;

})
});
this.getPublicKey();
}

/**
Expand All @@ -141,7 +152,7 @@ export class CameraService {
}

private static convertCamsObjectToMap(cams: Object): Map<string, Camera> {
let cameras: Map<string, Camera> = new Map<string, Camera>();
const cameras: Map<string, Camera> = new Map<string, Camera>();

for (let key in cams) {
// @ts-ignore
Expand All @@ -159,6 +170,15 @@ export class CameraService {
return cameras;
}

private static convertFailureReasonsToMap(failed: Object): Map<string, string> {
const failures: Map<string, string> = new Map<string, string>();
for(let address in failed) {
// @ts-ignore
const failure = failed[address]
failures.set(address, failure)
}
return failures;
}
/**
* compareFn: Compare function for use with the keyvalue pipe. This compares string with numbers (such as stream9, stream10)
* and sorts the numeric parts numerically rather than alphabetically. This was added to fix a bug which
Expand All @@ -185,8 +205,8 @@ export class CameraService {
catchError((err: HttpErrorResponse) => throwError(err)));
}

haveCameraCredentials(): Observable<string> {
return this.http.post(this._baseUrl.getLink("cam", "haveCameraCredentials"), '', {responseType: 'text'}).pipe(
haveOnvifCredentials(): Observable<string> {
return this.http.post(this._baseUrl.getLink("cam", "haveOnvifCredentials"), '', {responseType: 'text'}).pipe(
catchError((err: HttpErrorResponse) => throwError(err)));
}

Expand All @@ -206,22 +226,26 @@ export class CameraService {
);
}

discover(): Observable<Map<string, Camera>> {
discover(): Observable<{cams: Map<string, Camera>, failed: Map<string, string>}> {
return this.http.post<any>(this._baseUrl.getLink("onvif", "discover"), '', this.httpJSONOptions).pipe(
map(cams => {
return CameraService.convertCamsObjectToMap(cams);
map(result => {
return {cams: CameraService.convertCamsObjectToMap(result.cams), failed: CameraService.convertFailureReasonsToMap(result.failed)};
})
);
}

discoverCameraDetails(onvifUrl: string): Observable<Camera> {
discoverCameraDetails(onvifUrl: string, onvifUserName: string = "", onvifPassword: string = ""): Observable<{cam: Camera, failed: Map<string, string>}> {
const formData: FormData = new FormData();
formData.append('onvifUrl', onvifUrl)
formData.append("onvifUserName", onvifUserName);
formData.append("onvifPassword", onvifPassword);
return this.http.post<any>(this._baseUrl.getLink("onvif", "discoverCameraDetails"), formData, this.httpUploadOptions).pipe(
map(cams => {
let map: Map<string, Camera> = CameraService.convertCamsObjectToMap(cams);
map(result => {
let map: Map<string, Camera> = CameraService.convertCamsObjectToMap(result.cams);
if (map.size == 1)
return map.entries().next().value[1];
return {cam: map.entries().next().value[1], failed: CameraService.convertFailureReasonsToMap(result.failed)};
else
return {cam: result.cam, failed: CameraService.convertFailureReasonsToMap(result.failed)}
})
);
}
Expand All @@ -234,16 +258,28 @@ export class CameraService {
catchError((err: HttpErrorResponse) => throwError(err)));
}

getSnapshot(url: string): Observable<Array<any>> {
getSnapshot(cam: Camera): Observable<Array<any>> {
const formData: FormData = new FormData();
formData.append('url', url);
formData.append('url', cam.snapshotUri);
formData.append('cred', cam.cred)
return this.http.post<Array<any>>(this._baseUrl.getLink("onvif", "getSnapshot"), formData, this.httpUploadOptions).pipe(
tap(),
catchError((err: HttpErrorResponse) => throwError(err)));
}

setCameraAdminCredentials(creds: CameraAdminCredentials): Observable<any> {
return this.http.post<any>(this._baseUrl.getLink("cam", "setAccessCredentials"), creds, this.httpUploadOptions).pipe(
getPublicKey():void {
if(this._publicKey === undefined) {
this.http.post<Uint8Array>(this._baseUrl.getLink("cam", "getPublicKey"), "", this.httpUploadOptions).pipe(
tap((pk) => {
this._publicKey = new Uint8Array(pk);
}),
catchError((err: HttpErrorResponse) => throwError(err)))
.subscribe();
}
}
setOnvifCredentials(creds: OnvifCredentials): Observable<any> {
const msg = {onvifUserName: creds.userName, onvifPassword: creds.password};
return this.http.post<any>(this._baseUrl.getLink("cam", "setOnvifCredentials"), JSON.stringify(msg), this.httpJSONOptions).pipe(
tap(),
catchError((err: HttpErrorResponse) => throwError(err)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Encryption } from './encryption';

describe('Encryption', () => {
it('should create an instance', () => {
expect(new Encryption()).toBeTruthy();
});
});
35 changes: 35 additions & 0 deletions client/src/app/config-setup/camera-credentials/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export class Encryption {
readonly publicKey: Uint8Array;
constructor(publicKey: Uint8Array) {
this.publicKey = publicKey;
}
async encrypt(strToEncrypt: string) {
//import key to encrypt with RSA-OAEP
const key: CryptoKey = await crypto.subtle.importKey(
"spki",
this.publicKey,
{name: "RSA-OAEP", hash: {name: "SHA-256"}},
false,
["encrypt"]);
return await this.encryptMessage(key, strToEncrypt)
}

private async encryptMessage(publicKey: CryptoKey, strToEncrypt: string) {
const enc = new TextEncoder();
const encoded = enc.encode(strToEncrypt);
const result = await window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
encoded
);

let binary = '';
const bytes = new Uint8Array(result);
let len = bytes.byteLength;
for (let i = 0; i < len; i++)
binary += String.fromCharCode(bytes[i]);
return window.btoa(binary);
}
}
Loading

0 comments on commit 085c2e9

Please sign in to comment.