我需要在 C# 模型中为网络摄像机模型 (HNP322-IR/32X) 使用 Onvif PTZ 功能
问题是相机使用带有 MD5 的 Digest WEB 身份验证(我很难找到)
我得出的唯一结论是使用 Onvif 遗传密码,因为没有公共图书馆作品。
我使用了这个类,它是迄今为止最可靠的代码,可能在 Internet 上的许多代码中工作
using ONVIFPTZControl.OnvifMedia10;
using ONVIFPTZControl.OnvifPTZService;
using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security.Tokens;
using System.Text;
using System.Timers;
using System.Windows;
namespace ONVIFPTZControl
{
public class Controller
{
private enum Direction { None, Up, Down, Left, Right };
ONVIFPTZControl.OnvifMedia10.MediaClient mediaClient;
PTZClient ptzClient;
Profile profile;
OnvifPTZService.PTZSpeed velocity;
PTZVector vector;
PTZConfigurationOptions options;
bool relative = false;
bool initialised = false;
Timer timer;
Direction direction;
float panDistance;
float tiltDistance;
public string ErrorMessage { get; private set; }
public bool Initialised { get { return initialised; } }
public int PanIncrements { get; set; } = 20;
public int TiltIncrements { get; set; } = 20;
public double TimerInterval { get; set; } = 1500;
public Controller(bool relative = false)
{
this.relative = relative;
}
public bool Initialise(string cameraAddress, string userName, string password)
{
bool result = false;
try
{
var messageElement = new TextMessageEncodingBindingElement()
{
MessageVersion = MessageVersion.CreateVersion(
EnvelopeVersion.Soap12, AddressingVersion.None)
};
HttpTransportBindingElement httpBinding = new HttpTransportBindingElement()
{
AuthenticationScheme = AuthenticationSchemes.Digest
};
CustomBinding bind = new CustomBinding(messageElement, httpBinding);
mediaClient = new ONVIFPTZControl.OnvifMedia10.MediaClient(bind,
new EndpointAddress($"http://{cameraAddress}/onvif/Media"));
mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
ptzClient = new PTZClient(bind,
new EndpointAddress($"http://{cameraAddress}/onvif/PTZ"));
ptzClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
ptzClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
ptzClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
var profs = mediaClient.GetProfiles();
profile = mediaClient.GetProfile(profs[0].token);
var configs = ptzClient.GetConfigurations();
options = ptzClient.GetConfigurationOptions(configs[0].token);
velocity = new OnvifPTZService.PTZSpeed()
{
PanTilt = new OnvifPTZService.Vector2D()
{
x = 0,
y = 0,
space = options.Spaces.ContinuousPanTiltVelocitySpace[0].URI,
},
Zoom = new OnvifPTZService.Vector1D()
{
x = 0,
space = options.Spaces.ContinuousZoomVelocitySpace[0].URI,
}
};
if (relative)
{
timer = new Timer(TimerInterval);
timer.Elapsed += Timer_Elapsed;
velocity.PanTilt.space = options.Spaces.RelativePanTiltTranslationSpace[0].URI;
panDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Max -
options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Min) / PanIncrements;
tiltDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Max -
options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Min) / TiltIncrements;
}
vector = new PTZVector()
{
PanTilt = new OnvifPTZService.Vector2D()
{
x = 0,
y = 0,
space = options.Spaces.RelativePanTiltTranslationSpace[0].URI
}
};
ErrorMessage = "";
result = initialised = true;
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
return result;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
Move();
}
public void PanLeft()
{
if (initialised)
{
if (relative)
{
direction = Direction.Left;
Move();
}
else
{
velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Min;
velocity.PanTilt.y = 0;
ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
}
}
}
public void PanRight()
{
if (initialised)
{
if (relative)
{
direction = Direction.Right;
Move();
}
else
{
velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
velocity.PanTilt.y = 0;
ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
}
}
}
public void TiltUp()
{
if (initialised)
{
if (relative)
{
direction = Direction.Up;
Move();
}
else
{
velocity.PanTilt.x = 0;
velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
}
}
}
public void TiltDown()
{
if (initialised)
{
if (relative)
{
direction = Direction.Down;
Move();
}
else
{
velocity.PanTilt.x = 0;
velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Min;
ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
}
}
}
public void Stop()
{
if (initialised)
{
if (relative)
timer.Enabled = false;
direction = Direction.None;
ptzClient.Stop(profile.token, true, true);
}
}
private void Move()
{
bool move = true;
switch (direction)
{
case Direction.Up:
velocity.PanTilt.x = 0;
velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
vector.PanTilt.x = 0;
vector.PanTilt.y = tiltDistance;
break;
case Direction.Down:
velocity.PanTilt.x = 0;
velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
vector.PanTilt.x = 0;
vector.PanTilt.y = -tiltDistance;
break;
case Direction.Left:
velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
velocity.PanTilt.y = 0;
vector.PanTilt.x = -panDistance;
vector.PanTilt.y = 0;
break;
case Direction.Right:
velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
velocity.PanTilt.y = 0;
vector.PanTilt.x = panDistance;
vector.PanTilt.y = 0;
break;
case Direction.None:
default:
move = false;
break;
}
if (move)
{
ptzClient.RelativeMove(profile.token, vector, velocity);
}
timer.Enabled = true;
}
}
}
我还添加了两个在线服务:
1- http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl
2- http://onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl
现在,在下面的函数中,我已经使用正确的凭据实施了摘要身份验证请求,因此我希望 PTZClient 获得授权和连接
public bool Initialise(string cameraAddress, string userName, string password)
{
bool result = false;
try
{
var messageElement = new TextMessageEncodingBindingElement()
{
MessageVersion = MessageVersion.CreateVersion(
EnvelopeVersion.Soap12, AddressingVersion.None)
};
HttpTransportBindingElement httpBinding = new HttpTransportBindingElement()
{
AuthenticationScheme = AuthenticationSchemes.Digest
};
CustomBinding bind = new CustomBinding(messageElement, httpBinding);
mediaClient = new ONVIFPTZControl.OnvifMedia10.MediaClient(bind,
new EndpointAddress($"http://{cameraAddress}/onvif/Media"));
mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
ptzClient = new PTZClient(bind,
new EndpointAddress($"http://{cameraAddress}/onvif/PTZ"));
ptzClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
ptzClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
ptzClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
var profs = mediaClient.GetProfiles();
profile = mediaClient.GetProfile(profs[0].token);
var configs = ptzClient.GetConfigurations();
options = ptzClient.GetConfigurationOptions(configs[0].token);
velocity = new OnvifPTZService.PTZSpeed()
{
PanTilt = new OnvifPTZService.Vector2D()
{
x = 0,
y = 0,
space = options.Spaces.ContinuousPanTiltVelocitySpace[0].URI,
},
Zoom = new OnvifPTZService.Vector1D()
{
x = 0,
space = options.Spaces.ContinuousZoomVelocitySpace[0].URI,
}
};
if (relative)
{
timer = new Timer(TimerInterval);
timer.Elapsed += Timer_Elapsed;
velocity.PanTilt.space = options.Spaces.RelativePanTiltTranslationSpace[0].URI;
panDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Max -
options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Min) / PanIncrements;
tiltDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Max -
options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Min) / TiltIncrements;
}
vector = new PTZVector()
{
PanTilt = new OnvifPTZService.Vector2D()
{
x = 0,
y = 0,
space = options.Spaces.RelativePanTiltTranslationSpace[0].URI
}
};
ErrorMessage = "";
result = initialised = true;
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
return result;
}
但相反,我得到了这个回应
The HTTP request is unauthorized with client authentication scheme 'Digest'. The authentication header received from the server was 'Digest qop="auth", realm="IP Camera(G6877)", nonce="393038343a31363637303766363a51cd6c2b3d4f0a0b9942d2dfb810023b", stale="FALSE"'.
我知道 Digest auth 是这样工作的:
1-客户提出要求
2- 客户端从服务器取回随机数和 401 身份验证请求
3-客户端发回以下响应数组(用户名,领域,generate_md5_key(nonce,用户名,领域,URI,password_given_by_user_to_browser))(是的,这非常简单)
4- 服务器获取用户名和领域(加上它知道客户端请求的 URI)并查找该用户名的密码。然后它开始并执行自己版本的 generate_md5_key(nonce, username, realm, URI, password_I_have_for_this_user_in_my_db)
5- 它比较 generate_md5() 的输出和客户端发送的输出,如果它们匹配客户端发送的正确密码。如果它们不匹配,则发送的密码是错误的。
现在,我认为 ptzClient.GetConfigurations() 和 mediaClient.GetProfiles() 只发送一次请求并且没有完成身份验证过程。
我什至尝试使用那个 repository 中的代码进行通用请求,但是我得到了这个错误
System.IO.IOException: The response ended prematurely
所以相机看起来甚至没有响应,因为第一个请求是一个普通的 GET 方法,它似乎只响应摘要身份验证请求,并且在这种情况下响应始终相同
The HTTP request is unauthorized with client authentication scheme 'Digest'. The authentication header received from the server was 'Digest qop="auth", realm="IP Camera(G6877)", nonce="393038343a31363637303766363a51cd6c2b3d4f0a0b9942d2dfb810023b", stale="FALSE"'.
所以,如果相机仅在摘要身份验证请求后才响应它,我应该如何获得具有所需领域和随机数的标头(由于我首先需要标头,因此不会获得授权)
过去两周我一直在想办法,但没有结果。
即使是 Postman 也应该处理这个并从第一个请求中提取信息,然后发送另一个授权请求,但它不会.