quatanium / python-onvif

ONVIF Client Implementation in Python
MIT License
474 stars 321 forks source link

Allow the use of a time delta parameter between local and camera time. #37

Closed tgaugry closed 8 years ago

tgaugry commented 8 years ago

This allow authentication on cameras without being time synchronized. A simple constructor parameter as been added; if set to false (default), it behave like before. if set to true, it uses camera own time for authentication.

c = ONVIFCamera('172.20.9.56', 80, "root", "password") c.devicemgmt.SetHostname({'Name': 'newname'}) will fail if time difference is too high (error 400)

c = ONVIFCamera('172.20.9.56', 80, "root", "password", adjust_time=True) c.devicemgmt.SetHostname({'Name': 'newname'}) will succeed

tgaugry commented 8 years ago

Made the diff

sgould420 commented 8 years ago

@miuhaki: I have been using adjust_time=True with my camera and it works as expected. But I do not understand how this actually works. I understand that when I create a service, a UsernameDigestTokenDtDiff is created by set_wsse(). And then I can see that whenever I call a function from that service, for example, mycam.devicemgmt.GetHostname(), that somehow UsernameDigestTokenDtDiff.setcreated() is called. How does this function setCreated() get called? where can I see in the code that these functions will call setcreated() on the UsernameDigestToken? I know that the functions for the services are defined somehow in the wsdl files, but I do not understand how to figure out what each function is actually doing, and what other functions they will call. Please explain if you can. Thanks.

tgaugry commented 8 years ago

@sgould420: It's magic. Deal with it. :) Joking aside, the answer is that the SOAP client (suds) calls it. Or more exactly, it calls xml() method (defined in UsernameDigestToken and UsernameToken) in binding.py .

So if you want to see the code, you'll have to look in suds and suds-passworddigest. Be warned that you'll have to go through quite a few layer of abstraction to reach the actual code.

As per the fix itself, it change setcreated to use a (timedelta+utctime) instead of just the utctime.

Feel free to ask again if you want more details.

sgould420 commented 8 years ago

@miuhaki: Thanks for pointing me in the right direction! For the record, your answer was clear and correct, and I definitely should have stopped at "It's magic", especially since it is all working as expected.

Instead I reinstalled suds and suds-passworddigest, from source and tried to figure out what is actually going on. Since this took a long time, in case anyone else ever lands here on google, here are my notes:

First we create a new service, e.g.: media_service = mycam.create_media_service() This calls create_onvif_service(), which creates a new ONVIFService object initialized with the wsdl file for that service, which in turn creates a suds Client object as it’s ws_client attribute, also using the wsdl file for that service as the url. Most of the magic happens when this suds Client object is first initialized, right after setting the Client’s options attribute to some defaults by creating a suds Options object (wsse is None by default at this point). Specifically, during init, a suds DefinitionsReader object is created (from reader.py) passing it our Options instance, and calls DefinitionsReader.open(url). This calls init on a new suds Definitions Object (from wsdl.py), also initialized with the same Options instance. Near the end of the Definitions init, it calls Definitions.add_methods() for the service. This is when the suds Binding object (from binding.py) is actually created and initialized with the Definitions object itself, This is also where every method defined in the wsdl file gets associated with that binding. Then after the suds Client object is created in ONVIFService.init, a UsernameDigestTokenDtDiff is created in ONVIFService.set_wsse(). A UsernameDigestTokenDtDiff is just a suds_passworddigest UsernameDigestToken, but with a time offset parameter that can be adjusted, by overriding the setcreated() function of the suds_passworddigest UsernameDigestToken. A UsernameDigestToken is just a suds UsernameToken from wsse.py with stuff added for Web Services Security PasswordDigest authentication. But the important part here is that it overrides the suds UsernameToken.xml() function with it’s own UsernameDigestToken.xml() which calls UsernameDigestToken.generate_digest() which will then use our overrided UsernameDigestTokenDtDiff’s setcreated() with the adjusted date. That token is then added to a suds Security object and added to our suds Client object’s options with self.ws_client.set_options(wsse=security). set_options has an overcomplicated way of setting options using Skin and Unskin objects. So at this point, our Client.wsdl.options points to the same instance of Options as Client.options, and so our suds Client.wsdl.options.wsse is set to this Security object that contains our UsernameDigestTokenDtDiff.

Later when we call some method defined by our service, e.g. media_service.getProfiles() this happens: ONVIFService.getattr() calls getattr() on our suds Client object’s .service attribute which is a suds ServiceSelector object that was created when we first created our suds Client object. That calls getattr() on a suds PortSelector object, which calls gettattr() on a suds MethodSelector, which uses MethodSelector.getitem() to search through a dictionary of defined methods, and if one exists with that name, in this case “GetProfiles”, it creates a suds Method object for that method and returns it. This Method object gets returned all the way back to ONVIFService.getattr() which then calls ONVIFService.service_wrapper.wrapped.call() on that Method object which in turn calls it's own Method.call(). Method.call() creates a suds SoapClient object, and then calls SoapClient.invoke(). invoke() gets the Binding for that method, that was originally created when we first created our service, and calls Binding.get_message() which in turn calls Binding.headercontent(). Now it checks the options of our Definitions object and if wsse was set to something then it calls wsse.xml(). wsse is the Security object we added to the options earlier, and so Security.xml() calls our UsernameDigestToken.xml() which as described above calls generate_digest() and finally, UsernameDigestTokenDtDiff.setcreated(). YAY :)

The reason all of this was important to me is that some of the IP cameras that I am using in my application occasionally reset themselves to default settings and fail to use NTP to set their date. So they think its January 1, 1970, and reject any attempts to communicate. Setting adjust_time=True solves this problem. So thanks for the fix!