我正试图发送一条ONVIF PTZ soap消息,以获取相机的状态,作为一个简单的测试。我也在努力保持这种纯JavaScript。我不能使用Node.js,因为应用程序的其余部分是用不同的语言编写的,我需要它作为客户端。我尝试做的一个测试是复制ONVIF TM应用程序程序员指南中的结果。我可以发送soap消息从SoapUI获取状态,但SoapUI不使用WS-UsernameToken。
这是一个简单的HTML文件:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<!-- This folder is for asking the question of how to access a module from JQuery -->
<head>
<title>My Test Page</title>
<!-- sha.js is from jsSHA library (https://github.com/Caligatio/jsSHA) -->
<script src="./crypto/sha1.js"></script>
<script src="./soap.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
</head>
<body>
My page.
<h1>Camera Status:</h1>
<textarea class="statusArea" rows="20" cols="40" style="border:none;">
</textarea>
<script>
$(document).ready(function() {
testSoap();
});
</script>
</body>
</html>
这是一个JavaScript文件:
const testPW = "testPassword";
const textHash = new jsSHA( "SHA-1", "TEXT");
const PasswordType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest";
const WSSE = 'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"';
const WSU = 'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"';
const testData = {
nonce: 'LKqI6G/AikKCQrN0zqZFlg==',
date: '2010-09-16T07:50:45Z',
password: 'userpassword',
result: 'tuOSpGlFlIXsozq4HFNeeGeFLEI='
};
const pwDigestFormula = (nonce_, date_, pw_) => {
let temp = nonce_ + date_ + pw_;
textHash.update(temp);
return textHash.getHash("B64");
}
const getNonce = (length = 24) => {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for(var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
const getIsoTimestamp = () => {
let d = (new Date()).toISOString();
console.log(d);
return d;
};
const getPasswordDigest = (password_) => {
let result = {
passwordType: PasswordType,
nonce: getNonce(),
created: getIsoTimestamp(),
digestPassword: null
};
result.digestPassword = pwDigestFormula(atob(result['nonce']), result['created'], password_);
return result;
}
const TEST_ONVIF_PTZ_SERVICE_URL = "http://###.###.###.###/onvif/ptz";
const getObjectTypeName = (object_) => {
return (object_?.constructor?.name ?? null);
};
/*
Parts of this class were from https://stackoverflow.com/questions/42642924/onvif-soap-message-request-using-jquery
*/
class SoapMessageObj {
#mediaProfile = 'test!';
commands = {
SECURE_HEADER: (username_, password_, nonce_, isoTimestamp_) =>
`<soap:Header>
<wsse:Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>${username_}</wsse:Username>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">${nonce_}</wsse:Nonce>
<wsu:Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">${isoTimestamp_}</wsu:Created>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">${password_}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>`,
STATUS: (profileToken_ = 'media_profile1', header_ = '<soap:Header/>', attributes_ = null) => {
if (null!== attributes_) {
attributes_ = ` ${attributes_.join(' ')}`;
} else {
attributes_ = '';
}
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsdl="http://www.onvif.org/ver20/ptz/wsdl" ${attributes_}>
${header_}
<soap:Body>
<wsdl:GetStatus>
<wsdl:ProfileToken>${profileToken_}</wsdl:ProfileToken>
</wsdl:GetStatus>
</soap:Body>
</soap:Envelope>`
}
};
xmlSerializer = new XMLSerializer();
// String containing the soap message
#soapMessage = null;
// URL object
#url = null;
constructor(soapUrl_) {
let objectType = getObjectTypeName(soapUrl_);
switch(objectType) {
case 'String':
this.#url = new URL(soapUrl_);
break;
case 'URL':
this.#url = soapUrl_;
break;
default:
throw new Error(`Error: unknown object in SoapMessageObj call: ${objectType}`);
}
};
/*
* Getters and Setters
*/
get soapMessage() {
return this.#soapMessage;
};
set soapMessage(value) {
this.#soapMessage = value;
};
get url() {
return this.#url;
};
set url(url_) {
this.#url = url_;
};
get mediaProfile() {
return this.#mediaProfile;
}
set mediaProfile(mediaProfile_) {
this.#mediaProfile = mediaProfile_;
}
/*
Default processing for Success
*/
async processSuccess(data_, status_, req_) {
let dataType = getObjectTypeName(data_);
console.log('Successfully Sent command');
console.debug( `SUCCESS. Status: ${status_}` );
console.debug('Data object: ' + dataType);
console.debug('req object: ' + getObjectTypeName(req_));
this.response = data_;
if (dataType === "XMLDocument") {
console.debug(this.xmlSerializer.serializeToString(data_));
} else {
for (let o in data_) {
console.debug(`${o}: ${data_[o]}`);
}
}
};
/*
Default processing for failure.
*/
async processError(data_, status_, req_) {
console.debug( `ERROR. Status: ${status_}` );
let dataType = getObjectTypeName(data_);
console.debug('Data object: ' + dataType);
if (dataType === "XMLDocument") {
console.debug(this.xmlSerializer.serializeToString(data_));
} else {
dataType = getObjectTypeName(data_.responseXML);
if (dataType === "XMLDocument" ) {
this.response = data_.responseXML;
console.clear();
console.debug('responseXML property object: ' + dataType);
console.debug(this.xmlSerializer.serializeToString(data_.responseXML));
} else {
this.response = data_;
for (let o in data_) {
console.debug(`${o}: ${data_[o]}`);
}
}
}
};
/*
Pass in JavaScript SoapMessageObj object
The bind is needed to insure the right class/object for the "this" variable.
*/
async sendSoapMessage(soapMessage_, success_ = this.processSuccess.bind(this), failure_ = this.processError.bind(this), context_ = this) {
jQuery.support.cors = true;
$.ajax({
type: "POST",
url: context_.url.href,
crossDomain: true,
processData: false,
data: soapMessage_,
success: success_,
error: failure_
});
};
};
/*
Test function
*/
function testSoap() {
//try to replicate the example from ONVIF_WG-APG-Application_Programmers_Guide-1.pdf
let test = btoa(pwDigestFormula( atob(testData.nonce), testData.date, testData.password ) )
console.debug(`atob(btoa): ${test} testData equal: ${test==testData.result}`);
test = atob(pwDigestFormula( btoa(testData.nonce), testData.date, testData.password ) );
console.debug(`atob(btoa): ${test} testData equal: ${test==testData.result}`);
};
2022年9月3日更新从testSoap中删除了额外的代码。
我在这里发布了这篇文章,这样任何其他想要答案的人都会得到它。我通过谷歌搜索、同事的链接和试错找到了答案。我能够使用两个JavaScript代码文件复制该示例。为了方便起见,我把它们组合成了下面的一个。
/**
* base64.js
* Original author: Chris Veness
* Repository: https://gist.github.com/chrisveness/e121cffb51481bd1c142
* License: MIT (According to a comment).
*
* 03/09/2022 JLM Updated to ES6 and use strict.
*/
/**
* Encode string into Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648].
* As per RFC 4648, no newlines are added.
*
* Characters in str must be within ISO-8859-1 with Unicode code point <= 256.
*
* Can be achieved JavaScript with btoa(), but this approach may be useful in other languages.
*
* @param {string} str_ ASCII/ISO-8859-1 string to be encoded as base-64.
* @returns {string} Base64-encoded string.
*/
"use strict";
const B64_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
//function base64Encode(str) {
const base64Encode = (str_) => {
if (/([^u0000-u00ff])/.test(str_)) throw Error("String must be ASCII");
let b64 = B64_CHARS;
let o1,
o2,
o3,
bits,
h1,
h2,
h3,
h4,
e = [],
pad = "",
c;
c = str_.length % 3; // pad string to length of multiple of 3
if (c > 0) {
while (c++ < 3) {
pad += "=";
str_ += "