New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add functionality to authenticate via TCPIP #389
Add functionality to authenticate via TCPIP #389
Conversation
Codecov Report
@@ Coverage Diff @@
## main #389 +/- ##
=======================================
Coverage 99.03% 99.03%
=======================================
Files 88 88
Lines 8977 8994 +17
=======================================
+ Hits 8890 8907 +17
Misses 87 87
Flags with carried forward coverage won't be shown. Click here to find out more.
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
@@ -430,12 +442,14 @@ def open_from_uri(cls, uri): | |||
raise NotImplementedError("Invalid scheme or not yet " "implemented.") | |||
|
|||
@classmethod | |||
def open_tcpip(cls, host, port): | |||
def open_tcpip(cls, host, port, username=None, password=""): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an idea, maybe it would be useful to adopt the approach requests
takes:
an auth=
keyword argument which can hold different things.
For example
auth=("user", "password")
- some custom auth
https://requests.readthedocs.io/en/latest/user/authentication/
Some class-based auth is probably overkill for the moment, but using a single argument would allow flexibility in the future (for example if API keys could be used, or reading from a file).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the comment, I quite like the idea. That would make it much more flexible and the auth
keyword could hold different authentication arguments for different instruments, i.e., whatever is required. Will implement that.
This will make it more flexible down the line to support authentications that are not username/password based.
@slagroommarino: If you have time to test (would still be great), you would now supply the username, password as: osa = instruments.yokogawa.Yokogawa6370.open_tcip(host="IP_ADDRESS", port="PORT", auth=("USER", "PWD")) Here's the updated version for encrypted authentication, see first comment for details on how to use this. Thanks already in advance! def _authenticate(self, auth):
username, password = auth
_ = self.query(f'open "{username}"')
resp = self.query("AUTHENTICATE CRAM-MD5 OK")
# hash it
pwd = hashlib.md5()
pwd.update(resp)
pwd.update(password)
resp = self.query(pwd.hexdigest())
if "ready" not in resp.lower():
raise ConnectionError("Could not authenticate with username / password") @scasagrande: Codecov upload seems to be failing... |
Thanks for the reminder, I'll try to test it somewhere next week! |
I managed to get hold of the OSA this morning, but unfortunately I can not get this to work. As a debug message, I get
which happens on line 55, when it tries to query the username: _ = self.query(cmd) It seems that I cannot send any command or query from the auth function. |
Not sure I understand the error message you get, however, it looks like I should have written in line 55: _ = self.query(f'OPEN "{username}"') but didnt capitalize the |
I was trying to figure out why the debugger does not give so much information, as I could not get into the query function itself whilst debugging. When I explicitly add the OSA instance in the parameters: _ = self.query(self, f'OPEN "{username}"') it does not give the error I described before, but it starts complaining about |
Wow, the tests did not catch that at all... weird. I could reproduce the error with just a listening socket. Now |
Progress! It just fails a little later though as it mentions that the Yokogawa object has no @property
def prompt(self):
"""
Gets/sets the prompt used for communication.
The prompt refers to a character that is sent back from the instrument
after it has finished processing your last command. Typically this is
used to indicate to an end-user that the device is ready for input when
connected to a serial-terminal interface.
In IK, the prompt is specified that that it (and its associated
termination character) are read in. The value read in from the device
is also checked against the stored prompt value to make sure that
everything is still in sync.
:type: `str`
"""
return self._prompt It was a quick test, hopefully it is helpful. But may be I can spend a bit more time on it tomorrow. |
Makes sense, prompt was not initialized since I put the authentication into the wrong place. My "test server" didn't answer, so It just waited in the query loop and never reached the I had to move the authentication out of One more time, I think this should work for you. One test is currently failing, will fix that in a bit :) Thanks for your patience! |
The real-life test is not failing, I managed to download the spectrum! Thanks for taking the time to implement this 😄 |
This is great news, thanks for testing @slagroommarino. If I may bother you a bit further: now that this works I incorporated the MD5 hashing of the password such that we don't end up sending a plaintext password over TCP/IP. Only the Tests are currently failing, that's because I only changed the routine for now and will adopt the tests if this actually works (also gotta run in a couple of minutes ;)) Thanks! |
This is looking great!! |
Thanks @scasagrande, hope the MD5 hashed version works too :) |
I'll try tomorrow morning, as the machine is in heavy use at the moment :) |
thank you! |
Alright, there seems to be a connection as the machine's 'remote' indicator is on, but it does not continue with the rest of the commands. When I close the connection from the machine, the script stops with the error: Removing the hash fixes the problem, but that is of course not a solution :P Btw, the coming three weeks I'm not in the office, so next time testing will be a while. |
Sorry, I was out myself for a week. This is very strange, I don't really know why this should be. It seems as if it forgets to send a terminator all of a sudden (or sends a wrong one?) |
That is wild haha, never new that that was a thing. I can try tomorrow to add or change the user with an actual username and password ;) |
Indeed, quite the fancy instrument you got there. Both should hopefully work now, the anonymous user as well as any other user/password combination, the latter being submitted hashed. Fingers crossed... |
Alright, the test works with user 'anonymous', but fails for every other user I try. Here the output:
The script ends with a socket timeout 'before reading a termination character'. |
Weird. One last ditch effort, looking at some other code I might have missed sending the
From the output, it seems like the request to send a challenge fails and it just returns |
Any chance to give this one a run @slagroommarino? Sorry to bother you and no worries if you don't have time at the moment. Really appreciate your help with this! |
Yes busy times ;) Unfortunately, the result has not changed. The debugger stops at line 78 in the yokogawa6370.py: 73 # hash it
74 if username.lower() != "anonymous": # so we need an actual password
75 pwd = hashlib.md5()
76 pwd.update(bytes(resp, "utf-8"))
77 pwd.update(bytes(password, "utf-8"))
78 resp = self.query(pwd.hexdigest()) |
Thanks for testing and sorry for the long delay. I don't understand why this is failing anymore. Re-reading the manual results in no further insight of what might be going on here... @scasagrande: What do you think? The authentication is now implemented as an option for any device and hashing - if wanted and functioning - can be implemented for other devices on a per-device level... |
Well most of this PR is specific to the |
Okay, tests are passing now, @slagroommarino if you have time to test this once more (sorry for the hassle and thanks for all your checks and time!) then I'd be ready to give this for review :) |
Just checking in: Did you have any chance to have a look at this @slagroommarino? |
There seems to be no time for testing with the hardware... Since (1) the unittests are functional and (2) this has been tested before thanks to @slagroommarino, I mark this ready for review. |
Hello all, I see "CRAM-MD5", but it is not secure :/ About old unsecure mechanisms and it is not new: |
@scasagrande what do you think of this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems fine other than my one comment
@@ -115,6 +115,8 @@ def read_raw(self, size=-1): | |||
"a termination character." | |||
) | |||
result += c | |||
# todo remove me | |||
print(f"REC: {result}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove me
Well, it's been a while. How are things looking @scasagrande? Should have incorporated the changes. Mostly asking because we could start testing for 3.12 and drop 3.7... (of course not in this PR) 😄 |
Whoops! |
This was first mentioned in #386. The Yokogawa 6370 authentication via TCPIP can go with username, password, or with anonymous user identification, where the username
anonymous
is sent and any password is valid.I added a private
_authenticate
routine toinstrument
and to the Yokogawa itself (where it is actually implemented). The authentication will likely be slightly different for different instruments, therefore it is specific to the instrument.@slagroommarino it would be great if you can test this with the actual instrument!
The one thing that might be iffy: the password authentication is in plain text. While the instrument accepts MD5 hashing, the way it would have to be implemented requires user interaction, see manual page 3-8. Instead of a password, a challenge string is requested after sending the username, that challenge string together with the password would have to be MD5 hashed (
hashlib
could be used), and then sent back to the instrument. The password is not stored inik
, however, even disassociated will remain in memory. This is hard to get rid off I think without an actual user interaction.What we could do, instead of sending the password in plain text via TCPIP is to do the MD5 hashing in
ik
, in this case inside the_authenticate
in the instrument. What do you think @scasagrande? The manual is unfortunately not very clear on how things are hashed together, my best guess would be the following routine inside the instrument (andimport hashlib
) at the start:However, since the manual is not very clear, I wonder if @slagroommarino could test this before implementation.
With the discussion, I mark this as a draft PR for now.