第一节 Gsoap使用
SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。或者更简单地说:SOAP 是用于访问网络服务的协议。
1.1简介
ü gSOAP是一个跨平台的,用于开发Web Service服务端和客户端的工具。
ü 在Windows、Linux、MAC OS和UNIX下使用C和C++语言编码,集成了SSL功能。
ü 下载地址:http://sourceforge.net/projects/gsoap2
ü 官方网站:http://genivia.com/Products/gsoap/index.html
1.2 window下使用gsoap开发webservice
对于Windows平台下开发客户端:
首先下载最新的gsoap_win32_2.7.6c.zip包。
下载地址:http://optusnet.dl.sourceforge.net/sourceforge/gsoap2/gsoap_win32_2.7.6c.zip
查看gsoap的User's Guide,基本就能对gsoap有个全面的了解。
gSOAP简单多线程服务器程序http://blog.chinaunix.net/u1/55091/showart_430965.html
纯C gSoap实现WebService :http://hi.baidu.com/2sky2sea/blog/item/40ec5555680279c1b745ae9b.html
VC下用gsoap编写webService和客户端程序
ü 服务器端(实现四则运算)
l 编写add.h头文件
如:int ns__add(int num1, int num2, int* sum);
l 使用gsoap/bin目录下的soapcpp2.exe程序编译头文件。
把soapcpp2.exe拷贝到add.h目录下,用cmd执行soapcpp2.exeadd.h。
( 编译头文件并生成xml等文件。其中,soapH.h andsoapC.cpp包含了数据类型的描述,soapClient.cpp给客户端使用,soapServer.cpp给服务端使用。)
l 新建一个win32控制台工程,加入wsock32.lib库,将刚才生成的那些文件添加到工程中。然后编写webserver.cpp主程序:
编译时将gsoap_win32目录下stdsoap2.cpp,stdsoap2.h文件加入工程。
#include "add.h"
#include "add.nsmap"
#include ”stdsoap2.h”
int main(intargc, char* argv[])
{
int m, s;
struct soap add_soap; //定义结构体struct soap的对象add_soap
soap_init(&add_soap); //初始化add_soap对象
//soap_set_namespaces(&add_soap,add_namespaces);
if (argc < 2)
{
printf("usage: %s \n", argv[0]);
exit(1);
}
else
{
//启动时,输入soap监听的端口atoi(argv[1]),将add_soap与端口绑定(TCP连接)
m = soap_bind(&add_soap, NULL, atoi(argv[1]),100);
if (m < 0)
{
soap_print_fault(&add_soap, stderr);
exit(-1);
}
fprintf(stderr, "Socket connectionsuccessful: master socket = %d\n", m);
for ( ; ; ) //循环等待监听
{
s = soap_accept(&add_soap); //等待并接受客户端的连接
if (s < 0)
{
soap_print_fault(&add_soap,stderr);
exit(-1);
}
fprintf(stderr, "Socketconnection successful: slave socket = %d\n", s);
soap_serve(&add_soap);//该句说明该server的服务
soap_end(&add_soap);
}
}
return 0;
}
//server端的实现函数与add.h中声明的函数相同,但是多了一个当前的soap连接的参数
int ns__add(struct soap *add_soap, int num1, int num2, int*sum)
{
*sum = num1 + num2;
return 0;
}
ü 客户端
#include
#include “stdsoap2.h”
#include "soapH.h"
#include "add.nsmap"
int add(const char* server,int num1, int num2, int *sum); //声明调用函数server是webservice的地址
int main(int argc, char **argv)
{
int result = -1;
char* server="http://localhost:4567"; //webservice的服务器地址
int num1 = 0;
int num2 = 0;
int sum = 0;
if( argc < 3 )
{
printf("usage: %s num1 num2 \n", argv[0]);
exit(0);
}
num1 = atoi(argv[1]);
num2 = atoi(argv[2]);
result = add(server, num1, num2, &sum); //调用webservice接口
if (result != 0)
{
printf("soap err,errcode =%d\n", result);
}
else
{
printf("%d+%d=%d\n", num1,num2, sum );
}
return 0;
}
//调用webservice接口通信
int add( const char* server, int num1, int num2, int *sum )
{
struct soap add_soap; //创建soap对象
int result = 0;
soap_init(&add_soap); //初始化soap对象
// soap_set_namespaces(&add_soap,add_namespaces);
//该函数是客户端调用的主要函数,后面几个参数和add.h中声明的一样,
//前面多了3个参数,函数名是接口函数名ns__add前面加上soap_call_
soap_call_ns__add( &add_soap,server, "", num1, num2, sum );
if(add_soap.error)
{
printf("soaperror:%d,%s,%s\n", add_soap.error, *soap_faultcode(&add_soap),*soap_faultstring(&add_soap) );
result = add_soap.error;
}
soap_end(&add_soap);
soap_done(&add_soap);
return result;
}
1.3 gsoap工具
1.3.1soapcpp2.exe
实例:命令行执行soapcpp2.exe add.h
生成:add.nsmap soapH.h soapC.c soapStub.h soapClient.c soapServer.c soapClientlib.c soapServerlib.c
add.add.req.xml add.add.res.xml
生成webservice的代码框架。
功能
编译头文件并生成源文件、xml文件等。其中,soapH.h和 soapC.cpp包含了数据类型的描述,soapClient.cpp给客户端使用,soapServer.cpp给服务端使用。
用法
soapcpp2 [选项] 头文件
[选项]
-i 生成server的proxy和object,这种object继承于soap struct。
-C 仅生成客户端client代码
-S 仅生成服务端server代码
-x 不生成xml文件。不用此项的话,将对头文件中定义的每个operation生成一个描述性的xml文件
-L 不生成soapClientLib文件和soapServerLib文件
-p name 修改文件名前缀,代替soap
-q name 指定代理类和对象使用的名空间name,包含文件名前缀
1.3.2wsdl2h.exe
功能
ü 利用WSDL和XML schemas生成包含WS属性和操作的C++风格gSoap头文件。
ü wsdl2h.exe用作wsdl和.h文件的转换,生成头文件。
使用方法
wsdl2h.exe [选项] wsdl文件的url
选项:
-o filename.h 将wsdl转化为filename.h头文件。
-s 不生成STL代码
-c 生成纯C风格的头文件,这将去除C++的一些特性
-n name 使用name代替默认前缀ns
-t filename.dat 使用filename.dat代替默认的typemap.dat文件
-zX 兼容之前的X版本
1.3.3实例
ü 使用wsdl2h.exe生成calc.wsdl的头文件calc.h
wsdl2h -o calc.h http://www.genivia.com/calc.wsdl
ü 使用soapcpp2.exe生成calc.h对应的xml文件及webservice相关的头文件及源文件
第二节 使用Gsoap生成ONVIF框架
ü Onvif的框架生成是实现onvif协议的第一步,当然也可不用工具生成框架,完全按照协议来自己实现。
2.1 ONVIF框架生成
工具及平台
Gsoap版本:Gsoap2.8.27 http://sourceforge.net/projects/gsoap2/files/gSOAP/
Onvif :WSDL文档(2015.12.15最新文档)
框架代码生成:windows下wsdl2、soapcpp2
步骤
ü 使用wsdl2h生成onvif.h,这里使用在线的方式,免得还要手动添加好多.xsd文件
wsdl2h –o onvif.h -c -s -t typemap.dat \
http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl
http://www.onvif.org/onvif/ver10/events/wsdl/event.wsdl
http://www.onvif.org/onvif/ver10/display.wsdl
http://www.onvif.org/onvif/ver10/deviceio.wsdl
http://www.onvif.org/onvif/ver20/imaging/wsdl/imaging.wsdl
http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl
http://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl
http://www.onvif.org/onvif/ver10/receiver.wsdl
http://www.onvif.org/onvif/ver10/recording.wsdl
http://www.onvif.org/onvif/ver10/search.wsdl
http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl
http://www.onvif.org/onvif/ver10/replay.wsdl
http://www.onvif.org/onvif/ver20/analytics/wsdl/analytics.wsdl
http://www.onvif.org/onvif/ver10/analyticsdevice.wsdl
http://www.onvif.org/onvif/ver10/schema/onvif.xsd
http://www.onvif.org/ver10/actionengine.wsdl
http://www.onvif.org/ver10/pacs/accesscontrol.wsdl
http://www.onvif.org/ver10/pacs/doorcontrol.wsdl
http://www.onvif.org/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl
http://www.onvif.org/ver10/accessrules/wsdl/accessrules.wsdl
http://www.onvif.org/ver10/credential/wsdl/credential.wsdl
http://www.onvif.org/ver10/schedule/wsdl/schedule.wsdl
ü 在onvif.h中加入#import “wsse.h”,用来做安全验证
ü 使用soapcpp2生成C文件 : soapcpp2 -c onvif.h -x
-I D:\work\onvif_frame\gsoap_2.8.27\gsoap-2.8\gsoap\import
-ID:\work\onvif_frame\gsoap_2.8.27\gsoap-2.8\gsoap\custom
-ID:\work\onvif_frame\gsoap_2.8.27\gsoap-2.8\gsoap
注意:做服务器端开发,server和client端的代码都要生成,因为sever端的代码要用到client中的函数这样就不需要自己写了。
http://blog.youkuaiyun.com/hbuxiaofei/article/details/50314759
2.2ONVIF编码
ü 使用函数soap_wsse_add_UsernameTokenDigest进行用户名与密码认证。
int iRet = soap_wsse_add_UsernameTokenDigest(soap,NULL, “用户名”, “密码”);
2.3 ONVIF安全认证—openssl
² 在ONVIF_WG-APG-Application_Programmer's_Guide.pdf文档中第6章描述了onvif加密方式。
² Soap通信的验证机制是WS_UsernameToken,流加密的方式是HTTPS。
² onvif的用户验证是基于WS_UsernameToken,而且密码是Digest而不是明文。
² WS_UsernameToken加密,就是将用户名\密码\Nonce\Created都包含在了header里面。
将#passwordDigest换成#passwordText的话,密码就是明文的,当然onvif说了,密码是Digest
获取Digest的公式:
Digest = B64ENCODE( SHA1( B64DECODE(Nonce ) + Date + Password ) )
在获取设备参数之前,每次都需要作鉴权处理。
2.3.1 鉴权
² 对于设有用户名和密码的设备,在进行一些操作之前需要进行鉴权。
² gsoap用soap_wsse_add_UsernameTokenDigest()函数进行鉴权。
l #include<wsseapi.h> //包含了 soap_wsse_add_UsernameTokenDigest()函数。
l 要使用这个头文件还需要添加几个相关的文件(具体需要包含的文件后面详说),并且需要安装openssl。
l 函数原型:
int iRet = soap_wsse_add_UsernameTokenDigest(SOAP *soap,
const char* id, //通常设置为NULL
const char* username, //登陆用户名
const char* password); //登陆密码
2.3.1 编译openssl
² 下载ActivePerl:http://www.activestate.com/activeperl/downloads
l 安装ActivePerl。
l 在cmd里输入 perl -v看看是否能出来版本号。若提示找不到,就手动设一下环境变量,把“你的路径\Perl64\bin;你的路径\Perl64\site\bin;”添加到PATH里。
² 下载openssl:http://www.openssl.org/source/
l 解压。在根目录下有INSTALL.W32,INSTALL.W64之类的文件,描述了在各个系统下怎么样编译openssl。参考这些文件进行如下的操作。
l 打开vs2010的命令提示符输入:
>cd bin;
>vcvars32.bat
l cd到openssl目录下:
32位输入:
> perl Configure VC-WIN32
> ms\do_nasm
> nmake -f ms\ntdll.mak
l 64位输入:
> perl Configure VC-WIN64A
> ms\do_win64a
> nmake -f ms\ntdll.mak
执行成功后会在out32dll(out32dll64)文件夹里生成lib,dll等文件。
libeay32.lib, libeay32.dll
ssleay.lib, ssleay32.dll
使用上述方法编译openssl1.1.0f可能不能成功。还需以下步骤:
从http://www.nasm.us/ 下载并安装Nasm汇编器。
² vs使用openssl
l 添加对openssl的支持,在wsdl2.h生成的onvif.h中添加
#import "wsa.h"
#import "wsse.h"
l 在vs工程里
添加包含目录:openssl\inc32
添加导入库文件:libeay32.lib ssleay32.lib
添加动态库文件:libeay32.dll ssleay32.dll
添加预处理器定义:WITH_DOM WITH_OPENSSL
(不添加预处理器定义会出现“ERR_get_error”: 找不到标识符、“RAND_pseudo_bytes”: 找不到标识符)
² 鉴权函数使用
l 将gsoap\plugin目录下的mecevp, smdevp, threads, wsaapi, wsseapi的.h和.c文件拷贝到工程目录下(要用cpp可以直接更改后缀)。
l 在include“wsseapi.h”,就能使用soap_wsse_add_UsernameTokenDigest函数进行鉴权了。
2.3.2 openssl使用
OpenSSL(open Secure Sockets Layer)是一个强大的安全套接字层密码库。
l Apache使用它加密HTTPS,OpenSSH使用它加密SSH。
l 但是,你不应该只将其作为一个库来使用,它还是一个多用途的、跨平台的密码工具。
OpenSSL 不仅仅是 SSL。它可以实现消息摘要、文件的加密和解密、数字证书、数字签名 和随机数字。
OpenSSL 不只是 API,它还是一个命令行工具。命令行工具可以完成与 API 同样的工作, 而且更进一步,可以测试 SSL 服务器和客户机。它还让开发人员对 OpenSSL 的能力有一个 认识。
一些 Linux 的发行版本附带了 OpenSSL 的二进制版本。
头文件ssl.h、bio.h 和 err.h
/* OpenSSL headers */
#include "openssl/bio.h"
#include "openssl/ssl.h"
#include "openssl/err.h"
/* Initializing OpenSSL */
SSL_load_error_strings();
ERR_load_BIO_strings();
OpenSSL_add_all_algorithms();
2.3.3 openssl建立非安全连接
l 不管连接是安全的还是不安全的,OpenSSL 都使用了一个名为 BIO 的抽象库来处理包括文件和套接字在内的各种类型的通信。
l 在建立连接(无论安全与否)之前,要创建一个指向 BIO 对象的指针。这类似于在标准 C 中 为文件流创建 FILE 指针。
BIO * bio;
bio = BIO_new_connect("hostname:port"); //与服务器建立连接,如www.ibm.com:80
if(bio == NULL)
{
/* Handle the failure */
}
if(BIO_do_connect(bio) <= 0)
{
/* Handle failed connection */
}
l 与服务器进行通信
不管 BIO 对象是套接字还是文件,对其进行的读和写操作都是通过以下两个函数来完成的: BIO_read 和 BIO_write 。
BIO_read 将尝试从服务器读取一定数目的字节; (分阻塞和非阻塞)
BIO_write 会试着将字节写入套接字。
l 关闭连接
/* To reuse the connection, use this line */
BIO_reset(bio);
/* To free it from memory, use this line */
BIO_free_all(bio);
第三节 认证方式
nonce(随机数)、timestamp(时间戳)、signatrue(签名)
3.1 Basic认证
1、HTTP规范里定义的Basic认证——Basic认证及其安全问题
1) Basic认证是一个流程比较简单的协议,整个过程可以分为以下三个步骤:
a) 客户端使用GET方法向服务器请求资源。
b) 服务器返回401响应码和WWW-Authentication:Basic realm=”Family”响应头要求客户端进行身份验证。其中realm声明了资源所在的域。
例子:
www-Authenticate: Basic realm=”bcad28dde190”
c) 浏览器接收到以上HTTP响应头后,弹出登录框要求用户输入用户名和密码;
用户提交的用户名和密码通过冒号串联起来并对其进行BASE64编码后再提交到服务器;
服务器对提交上来的BASE64字符串进行验证,如果验证通过则返回200响应码。
Base64(用户名:密码)的结果:
Base64(“admin:123456abc”) = “YWRtaW46MTIzNDU2YWJj”
2) Basic认证虽然简单、方便,但它只能作为对非敏感资源的访问认证,因为它并不安全,主要表现在以下几个方面:
<1> 客户端提交的用户名和密码只经过简单的编码,攻击者只要窃听到该数据包,便可很容易的将其反编码为原始用户名和密码。
<2> 即使客户端使用了一种比BASE64更复杂的编码方式使得攻击者无法对其反编码,攻击者也可以使用fiddler等工具将拦截到的HTTP报文重新提交给服务器,
服务器只对编码的字符串进行验证,所以验证同样能通过。这种攻击方法称之为重放攻击(Replay-Attack)。
以上两个问题也是各种身份认证协议需要考虑到的安全问题,包括OAuth、Digest认证、NTLM认证等等认证机制都使用了nonce和timestamp来解决这些问题。
3.2 Nonce随机数
2、Nonce、Timestamp——解决Replay-Attack问题(重放攻击)
(http://www.cnblogs.com/bestzrz/archive/2011/09/03/2164620.html)
1) Nonce是由服务器生成的一个随机数,在客户端第一次请求页面时将其发回客户端;
www-Authenticate:Digest realm=”bcad288dde190”, nonce=”a8e8e43483844d2a61578781ea5e7d1f”,stale=”FALSE”
2) 客户端拿到这个Nonce,将其与用户密码串联在一起并进行非可逆加密(MD5、SHA1等等),
3) 客户端然后将这个加密后的字符串和用户名、Nonce、加密算法名称一起发回服务器。
Authorization : Digest username=”admin”, realm=”bcad288dde190”, nonce=” a8e8e43483844d2a61578781ea5e7d1f”
reponse=”10b789bd1dd846539875df5c09eae836”
4)服务器使用接收到的用户名到数据库搜索密码,然后跟客户端使用同样的算法对其进行加密;
5)接着服务器将加密后的字符串与客户端提交上来的加密字符串进行比较,如果两个字符串一致就表示用户身份有效。
<1> 这样就解决了用户密码明文被窃取的问题,攻击者就算知道了算法名和nonce也无法解密出密码。
<2> 每个nonce只能供一个用户使用一次,这样就可以防止攻击者使用重放攻击,因为该Http报文已经无效。
<3> 可选的实现方式是把每一次请求的Nonce保存到数据库,客户端再一次提交请求时将请求头中得Nonce与数据库中得数据作比较,
如果已存在该Nonce,则证明该请求有可能是恶意的。
<4> 然而这种解决方案也有个问题,很有可能在两次正常的资源请求中,产生的随机数是一样的,这样就造成正常的请求也被当成了攻击,
随着数据库中保存的随机数不断增多,这个问题就会变得很明显。所以,还需要加上另外一个参数Timestamp(时间戳)。
<5> Timestamp是根据服务器当前时间生成的一个字符串,与nonce放在一起,可以表示服务器在某个时间点生成的随机数。
这样就算生成的随机数相同,但因为它们生成的时间点不一样,所以也算有效的随机数。
<6> 问题又来了,随着用户访问的增加,数据库中保存的nonce/timestamp/username数据量会变得非常大。
对于这个问题,可选的解决方案是对数据设定一个“过期时间”,比如说在数据库中保存超过一天的数据将会被清除。
<7> 如果是这样的,攻击者可以等待一天后,再将拦截到的HTTP报文提交到服务器,这时候因为nonce/timestamp/username数据已被服务器清除,
请求将会被认为是有效的。要解决这个问题,就需要给时间戳设置一个超时时间,
比如说将时间戳与服务器当前时间比较,如果相差一天则认为该时间戳是无效的
第四节 创建soap对象
struct soap*m_soap;
创建sopa对象
m_soap = soap_new();
m_soap->recv_timeout = 100; //设置超时
soap_set_namespace(m_soap, namespaces);
设置soap头
struct SOAP_ENV__Header header;
soap_default_SOAP_ENV__HEADER(m_soap, &header);
释放soap
soap_destory(m_soap);
soap_end(m_soap);
soap_free(m_soap);
第五节 搜索设备
wsdd__ProbeType req;
struct __wsdd__ProbeMatches resp;
发送广播消息探测
soap_send___wsdd__Probe(soap, “soap.udp://239.255.255.255.250:3702”,NULL, &req);
接收响应消息
soap_recv___wsdd__ProbeMathces(soap, &resp);
第六节 获取设备能力集
struct _tds__GetCapabilities tds__GetCapabilities;
struct _tds__GetCapabilitiesResponse tds__GetCapabilitiesResponse;
soap_call__tds___GetCapabilites(soap, “http: //IPAdress:port/onvif/device_service”,NULL,
&tds__GetCapabilities,tds__GetCapabilitiesResponse);
第七节 获取设备属性
_trt__GetProfiles trt__GetProfiles;
_trt__GetProfilesResponse trt__GetProfilesResponse;
soap_call___trt__GetProfiles(soap, “http: //IPAdress:port/onvif/device_service”,NULL,
&trt__GetProfiles, trt__GetProfilesResponse );
获得实时预览的Token
第八节 获取实时预览RTSP的URL
_trt__GetStreamUri trt__GetStreamUri;
_trt__GetStreamUriResponse trt__GetStreamUriResponse;
soap_call___trt__GetStreamUri(soap, “http://IPAdress:port/onvif/Media”, NULL,
&trt__GetStreamUri, trt__GetStreamUriResponse);
第九节 获取历史回放RTSP的URL
获得回放的Token
_trc__GetRecodings trc__GetRecodings;
_trc__GetRecodingsResponse trc__GetRecodingsResponse;
soap_call___trc__GetRecoding(soap, “http://IPAdress:Port/onvif/recoding_service”,NULL,
&trc__GetRecodings, trc__GetRecodingsResponse);
获取回放的URI
_trp__GetReplayUri trp__GetReplayUri;
_trp__GetReplayUriResponse trp__GetReplayUriResponse;
soap_call___trp__GetReplayUri(soap, “http://IPAdress:Port/onvif/replay_service”,NULL,
&trp__GetReplayUri, trp__GetReplayUriResponse);
第十节 云台操控
char* ptzEndPort = “http://192.168.207.65:80/onvif/PTZ”;
_tptz__ContinuousMove request;
_tptz__ContinuousMoveResponse response;
//设置参数
request.ProfileToken = “Profile_1”;
tt__PTZSpeed ptzSpeed;
tt__Vector2D vector2D;
vector2D.x = 1; 取值为[-1, 1],为负数时表示往左,为正数时表示往右
vector2D.y = 0;
vector2D.space = NULL;
ptzSpeed.PanTilt = & vector2D; //Pan上下,Tilt左右
ptzSpeed.Zoom = NULL;
int ret = soap_call___tptz__ContinuousMove(soap,ptzEndPort, NULL, &request, response); 连续移动
停止移动
soap_call___tptz__Stop();
class tt__PTZSpeed : public xsd__anyType
{
public:
tt__Vector2D* PanTitle;
tt__Vector1D* Zoom;
};
class tt__PTZVector : public xsd__anyType
{
public:
tt__Vector2D* PanTitle;
tt__Vector1D* Zoom;
};
class tt__Vector1D : public xsd__anyType
{
public:
float x; // 必须属性,取值范围[0, 1]
space; //可选属性
};
class tt__Vector1D : public xsd__anyType
{
public:
float x; // 必须属性,取值范围[0, 1]
float y; // 必须属性,取值范围[0, 1]
space; //可选属性
};
调焦
int soap_call___timg__Move(… , _timg__Move*timg__Move, _timg__MoveResponse& timg_MoveResp);
soap_call___timg__Stop()
第十一节 录像检索
_tse__GetRecodingSummary tse_GetRecodingSummary;
_tse__GetRecodingSummaryResponse tse_GetRecodingSummaryResponse;
soap_call___tse__GetRecordingSummary(soap,soap_search_endpoint, NULL,
& tse_GetRecodingSummary, tse_GetRecodingSummaryResponse);
得到DVR中的录像开始时间和结束时间。
time_t startTime = tse_GetRecodingSummaryResponse.Summary->DataFrom;
time_t endTime = tse_GetRecodingSummaryResponse.Summary->DataUntil;