Skip to content
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 support for SNI #89

Merged
merged 17 commits into from
Dec 16, 2012
Merged

add support for SNI #89

merged 17 commits into from
Dec 16, 2012

Conversation

t-8ch
Copy link
Contributor

@t-8ch t-8ch commented Jul 28, 2012

this makes urrlib3 use SNI if available.

testing can be done using https://sni.velox.ch/
I didn't include an automatic test, as this would fail on old versions of python or (open)ssl.
(SNI was added in 3.2)
I could however add a test which tests the version of python.

@shazow
Copy link
Member

shazow commented Jul 30, 2012

Hi @t-8ch, I haven't heard of SNI, very cool.

Some things I'd like to see before we can merge this into urllib3:

  • We require 100% test coverage. The tests can't use any external resources (ie. should run offline). Take a look at some of our existing SSL tests. I'm okay with the tests for SNI only running for Python 3.2+.
  • I would suggest breaking out the meat in your commit to a separate standalone helper. Take a look at urllib3.util.is_connection_dropped(conn) for example. Maybe something like urllib3.util.ssl_wrap_socket(conn). This way the core urllib3 core can be unaware of the nuances we're performing here.
  • Please try to keep the coding style inline with the surrounding code of the library. Especially in terms of documentation, naming, spacing, etc.

Thank you!

@t-8ch
Copy link
Contributor Author

t-8ch commented Jul 30, 2012

@shazow:
If I understand correctly, Python just doesn't offer server side support for SNI yet.

Quote from the docs: "Specifying server_hostname will also raise a ValueError if server_side is true"

I could add config files for apache/nginx if this would be more useful.

@shazow
Copy link
Member

shazow commented Jul 30, 2012

Mmm good point. http://bugs.python.org/issue8109

I don't feel very comfortable merging in untested code at this point and I would really like to avoid having to fire up nginx/apache to test some code. Some ideas:

  1. We could keep this bug open until it becomes feasible. You're welcome to maintain a separate fork of urllib3 until we can merge it in.
  2. You could try to implement some basic socket-level tests (have a look at https://github.com/shazow/urllib3/blob/master/test/with_dummyserver/test_socketlevel.py for example). Not sure how hard this would be.
  3. We could put the meat of the code in urllib3.contrib (where untested code lives), but it won't be used by default.

Either way, I'd still perform the suggested refactoring. :)

@t-8ch
Copy link
Contributor Author

t-8ch commented Jul 30, 2012

Should have read your last post earlier.
Anyhow, the refactoring is done.
Do you want another pull request?
(you can find the branch at https://github.com/t-8ch/urllib3/tree/sni)

I have also a test ready (using mentioned website).
The layout of this one is a bit messy, so if you want it after all I would like to know where to place it and it's external certificate.

Addition:
I'd like to avoid fiddling with low level SSL stuff. But maybe a dummyserver using plain C and OpenSSL would be doable

@shazow
Copy link
Member

shazow commented Jul 30, 2012

If you merge your sni branch into add_sni, it should appear in this pull request. You could merge it and then rename add_sni to sni, if you prefer.

@t-8ch
Copy link
Contributor Author

t-8ch commented Jul 31, 2012

Wouldn't the merging include all the useless stuff from add_sni?
sni is much cleaner. All of its commits pass 100% of test coverage.

@shazow
Copy link
Member

shazow commented Jul 31, 2012

What do you mean by useless stuff? Commits that got overwritten later? I'm not very obsessive about keeping the commit history super clean. I rather keep context.

Conflicts:
	urllib3/connectionpool.py
@t-8ch
Copy link
Contributor Author

t-8ch commented Jul 31, 2012

well, I did a clean stepwise rewrite of the patchset, which introduces utils.ssl_wrap_socket.
I have now merged the new tree into the old, messy one.
The clean one is still valid but both are now equal. Only their history differs.
So you are free to choose. I would prefer merging https://github.com/t-8ch/urllib3/tree/sni but feel free to choose.

@shazow
Copy link
Member

shazow commented Jul 31, 2012

Very nice, thank you @t-8ch. Any plans for the unit testing part?

Don't worry about the patchset history. Worst case, I can squash it before I merge. :)

@t-8ch
Copy link
Contributor Author

t-8ch commented Aug 1, 2012

Im not sure about handling situations, where the tests are run in an offline or in amanaged environment.
I am in favor of just ignoring everything except urrlib3.exceptions.SSLError.
If something different is wrong this should be handled fine by the remaining tests.

@shazow
Copy link
Member

shazow commented Aug 2, 2012

All tests must be able to run offline, ideally completely automated (without explicitly running other commands/servers alongside).

@t-8ch
Copy link
Contributor Author

t-8ch commented Aug 2, 2012

What about checking if the desired hostname is present in the raw handshake?
It should only be there if SNI is used and the hostname later in HTTP is encrypted anyway.

This only tests if the hostname somehow ends up in the request but everything else would be unittesting python or openssl.

@shazow
Copy link
Member

shazow commented Aug 2, 2012

That would be a great start. :) Especially we can get full code coverage with that, then I'd be happy to merge it in.

@t-8ch
Copy link
Contributor Author

t-8ch commented Aug 2, 2012

Do you have any hints on how to eavesdrop my own socket?

@shazow
Copy link
Member

shazow commented Aug 2, 2012

I think doing something like the tests here should do the trick: https://github.com/shazow/urllib3/blob/master/test/with_dummyserver/test_socketlevel.py

@t-8ch
Copy link
Contributor Author

t-8ch commented Aug 4, 2012

Would you also accept an contrib module, which uses pyopenssl?
I am thinking about something along the lines of:

# Do normal stuff
urllib3.util.ssl_wrap_socket = urllib3.contrib.pyopenssl.ssl_wrap_socket
# begin using urllib3

Some quick testing indicated, overwriting things like this should work.
One could provide two functions in this module:
One which uses the native version if available and the other one always uses
pyopenssl.

For the record:
This would help here: https://github.com/kennethreitz/requests/issues/749

@shazow
Copy link
Member

shazow commented Aug 4, 2012

Just took a look at the latest diff for this pull request, looking great! Thank you for seeing it through, @t-8ch.

Regarding pyopenssl:

I'm definitely +1 adding pyopenssl support. As you probably noticed, we already do some conditional support for SSL depending if Python is compiled with +ssl enabled. We could do the same thing for pyopenssl, to use it if it's available.

Regarding whether to put it in contrib:

I'm treating contrib as "experimental" code which isn't used by default but users can import it and use it explicitly if they want. So we could have something like urllib3.contrib.pyopenssl.enable_pyopenssl_wrap_socket() or somesuch which replaces which ssl_wrap_socket urllib3 uses. I'd prefer not to overwrite urllib3.util.ssl_wrap_socket in case people are using it as a standalone helper for other code. Instead, we should have a local urllib3.connectionpool.ssl_wrap_socket which can get reassigned.

That said, if we can get appropriate test coverage on the new pyopenssl code paths, then I'd be happy to include it in the core library. If we're unsure, then we can start with contrib and move it out of contrib later.

If you're up for taking a crack at this, maybe we should start a separate pull request thread for pyopenssl? I'll take a closer look at this SNI pull request over the next week and hopefully merge it in. :)

shazow added a commit that referenced this pull request Aug 4, 2012
@pythonmobile
Copy link

I tried to use sni with urllib3 and I see this problem that when I goto https://randomstring.mydomain.com -- it lands on one of the servers that actually is running on mydomain.com and does not fail (randomstring should actually give an error). This is the github install of urllib3. I also tried with https://github.com/t-8ch/urllib3.git and had the same problem. I hope this can be fixed soon.

@t-8ch
Copy link
Contributor Author

t-8ch commented Aug 10, 2012

Are you using a version of python >= 3.2?
Is this a public URL, which I could test myself?

@pythonmobile
Copy link

Thanks for the quick response. I'm using python 2.7. Is it not supposed to work with 2.xx?

@pythonmobile
Copy link

Indeed it will require pyopenssl. I think that should be top priority :-)

@t-8ch t-8ch mentioned this pull request Nov 21, 2012
return context.wrap_socket(sock, server_hostname=server_hostname)
return context.wrap_socket(sock)

else: # Platform-specific: Python < 3.2
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going to move that code into a seperate function, I would prefer to do that switch outside of the function definition, that it is evaluated only during import and not on every call.

if SSLContext:
    ssl_wrap_socket(...) # Python 3.2 implementation using SSLContext
else:
    ssl_wrap_socket(...) # Fallback implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shazow, @wallunit

What would be nicer? The same API for both method, ignoring server_name for the fallback function,
or something like this:

if SSLContext:
    def ssl_wrap_socket(...): # new implementation, arguments a superset of the fallback one
else:
    ssl_wrap_socket = ssl.wrap_socket

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best behaviour would be to silently ignore non critical arguments like server_name but fail if security critical arguments get added later.

@shazow
Copy link
Member

shazow commented Nov 21, 2012

@wallunit The review is much appreciated, thank you! Do you think it makes sense to merge your changes into this pull? (I agree with much of what you commented.)

@snoack
Copy link

snoack commented Nov 21, 2012

Are you talking about the code cleanup changes, I suggested above. Or are you talking about my own SNI patch?

@shazow
Copy link
Member

shazow commented Nov 21, 2012

@wallunit Both. :)

if SSLContext: # Platform-specific: Python >= 3.2
context = SSLContext(PROTOCOL_SSLv23)
context.verify_mode = cert_reqs
if context.verify_mode != CERT_NONE:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason, for doing that check? The old/fallback implementation doesn't explicitly ignores ca_certs if cert_reqs is CERT_NONE. So why do we do so, here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonnna fix this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it again:
If ca_certs is None, SSLContext.verify_locations() dies with:

TypeError: cafile and capath cannot be both omitted

whereas ssl.wrap_socket() just doesn't care.

This way people, not wanting to do verification had to specify ca_certs nevertheless.

The try-catch is necessary, as SSLContext.load_verify_location() raises a TypeError whereas the guts and unittests of urllib3 expect a SSLError.

We could remove the check, catch the Error and check if it's about a invalid ca_path, swallow it in this case and reraise it otherwaise. But I think this wouldn't be really nice.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, you have to check whether ca_certs is given, but that's not the check you do in the code I commented on, above. In fact SSLSocket.__init__ as called by wrap_socket in Python 3.2 uses following code:

if ca_certs:
    self.context.load_verify_locations(ca_certs)

But it doesn't check for verify_mode != CERT_NONE, like you do above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, now I got it.

@snoack
Copy link

snoack commented Nov 21, 2012

@shazow Of course I think that my suggestions regarding your patch make sense, otherwise I wouldn't have suggested them, right? ;) However my patch is still missing a test case. But feel free to merge my implementation with your test case together.

@shazow
Copy link
Member

shazow commented Nov 21, 2012

@wallunit Not my patch so much as @t-8ch's. ;)

Anyways, thanks again for the feedback, keep it coming! I'll take a look at it next week.

Thomas Weißschuh added 2 commits November 22, 2012 08:48
if SSLContext: # Platform-specific: Python >= 3.2
context = SSLContext(PROTOCOL_SSLv23)
context.verify_mode = cert_reqs
if context.verify_mode != CERT_NONE:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still should be if ca_certs: instead of if context.verify_mode != CERT_NONE:. See discussion form previous version.

thanks @wallunit
@snoack
Copy link

snoack commented Nov 22, 2012

The patch looks good now. I hope it will be merged soon.

@t-8ch
Copy link
Contributor Author

t-8ch commented Nov 22, 2012

Thanks for your review!
Let's look what @shazow will do with it and what happens with #109.

@t-8ch
Copy link
Contributor Author

t-8ch commented Nov 22, 2012

I have pushed a new branch to my repo which provides a new argument ssl_version to util.ssl_wrap_socket() which should make this pull request functional compatible with #109

@t-8ch
Copy link
Contributor Author

t-8ch commented Dec 1, 2012

@shazow any updates on this?

@@ -83,7 +93,7 @@ def split_first(s, delims):

def parse_url(url):
"""
Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
Given a url, return a parsed :class:`.Url` named tuple. Best-effort is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namedtuple is a thing. :)

@shazow
Copy link
Member

shazow commented Dec 2, 2012

I took a quick look, looks very reasonable. Unfortunately I don't have much time to validate and merge in non-trivial changes right now.

If this is urgently blocking anyone, I suggest maintaining a fork with this patch for the time being. Hopefully I'll have the time for some of the bigger bugs in the next few weeks. Please keep bugging me until then. :)

Thomas Weißschuh added 4 commits December 2, 2012 09:42
@shazow shazow merged commit 73a77a8 into urllib3:master Dec 16, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants