Reputation: 205
I need to use Onvif PTZ features for IP Camera Model (HNP322-IR/32X) in C# model
The problem is that the Camera uses Digest WEB Authentication with MD5 (I found that the hard way)
The only conclusion I arrived to is to use Onvif generic code as no public library works.
I used this class which is by far the most reliable code that might work among many on the 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;
}
}
}
I also added Two online services:
1- http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl
2- http://onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl
Now, in the following function, I have Implemented Digest auth request with the correct credentials so I expect PTZClient to get authorized and connected
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;
}
But instead, I get this response
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"'.
I know that Digest auth work like this:
1- Client makes request
2- Client gets back a nonce from the server and a 401 authentication request
3- Client sends back the following response array (username, realm, generate_md5_key(nonce, username, realm, URI, password_given_by_user_to_browser)) (yea, that's very simplified)
4- The server takes username and realm (plus it knows the URI the client is requesting) and it looks up the password for that username. Then it goes and does its own version of generate_md5_key(nonce, username, realm, URI, password_I_have_for_this_user_in_my_db)
5- It compares the output of generate_md5() that it got with the one the client sent, if they match the client sent the correct password. If they don't match the password sent was wrong.
Now, I think that both ptzClient.GetConfigurations() and mediaClient.GetProfiles() are only sending the request once and not completing authentication process.
I even tried generic request using the code in that repository, But I get this error
System.IO.IOException: The response ended prematurely
So the camera looks like it's not even responding since the first request is a normal GET method and it appear to response only to a Digest auth request and the response are always the same in this case
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"'.
So, how I'm supposed to get the header with the required Realm and nonce if the camera only response back with it after a digest auth request (which won't get authorized since I need the header first)
I have been trying to figure it out for the last 2 weeks, but with no result.
Even Postman should handle this and pull the information from the first request and then send another authorized one, but it won't.
Upvotes: 0
Views: 1168
Reputation: 205
I have found the problem, the User I was giving back then didn't have permission to use ONVIF, therefore authenticating was failing, the response I was getting from the ONVIF service wasn't clear unfortunately so I started searching in the wrong direction when it was just a user permission in device settings.
Upvotes: 0