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

Use SNI for ssl connections #109

Merged
merged 10 commits into from
Oct 25, 2018
Merged

Conversation

ojomio
Copy link
Contributor

@ojomio ojomio commented Oct 10, 2018

No description provided.

ojomio and others added 4 commits October 10, 2018 22:27
…t, `wrap_socket()` does not allow this

If explicit `server_hostname` ssl option is specified, it is used as SNI, otherwise the content of the `Host` HTTP header or the host from the requested URL
@ojomio
Copy link
Contributor Author

ojomio commented Oct 15, 2018

Does anybody want to get it merged?

@ml31415
Copy link
Collaborator

ml31415 commented Oct 18, 2018

Plese fix all existing test cases, and also add test cases for the things you intend to fix with your pull request. Also, avoiding further external dependencies would be great.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 18, 2018

At the moment I created this PR, all tests passed, however, it seems, that google changed
something and event master fails:


py36 runtests: commands[4] | py.test src/geventhttpclient/tests
=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.6.5, pytest-3.8.2, py-1.6.0, pluggy-0.7.1
rootdir: /home/crystal2/github/geventhttpclient, inifile:
collected 85 items                                                                                                                                                                                         

src/geventhttpclient/tests/test_client.py FF.F....FF.F........                                                                                                                                       [ 23%]
src/geventhttpclient/tests/test_headers.py .............                                                                                                                                             [ 38%]
src/geventhttpclient/tests/test_httplib.py ...                                                                                                                                                       [ 42%]
src/geventhttpclient/tests/test_keep_alive.py .....                                                                                                                                                  [ 48%]
src/geventhttpclient/tests/test_network_failures.py .........                                                                                                                                        [ 58%]
src/geventhttpclient/tests/test_no_module_ssl.py ..                                                                                                                                                  [ 61%]
src/geventhttpclient/tests/test_parser.py .......                                                                                                                                                    [ 69%]
src/geventhttpclient/tests/test_ssl.py ....                                                                                                                                                          [ 74%]
src/geventhttpclient/tests/test_url.py ..............                                                                                                                                                [ 90%]
src/geventhttpclient/tests/test_useragent.py ........                                                                                                                                                [100%]

================================================================================================= FAILURES =================================================================================================
____________________________________________________________________________________________ test_client_simple ____________________________________________________________________________________________

    def test_client_simple():
        client = HTTPClient('www.google.fr')
        assert client.port == 80
        response = client.get('/')
>       assert response.status_code == 200
E       AssertionError: assert 302 == 200
E        +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'http://www.google.com/sorry/index?continue=http://www.google....html; charset=UTF-8', 'server': 'HTTP server (unknown)', 'content-length': '324', 'x-xss-protection': '1; mode=block'}>.status_code

src/geventhttpclient/tests/test_client.py:41: AssertionError
____________________________________________________________________________________ test_client_without_leading_slash _____________________________________________________________________________________

    def test_client_without_leading_slash():
        client = HTTPClient('www.google.fr')
        with client.get("") as response:
>           assert response.status_code == 200
E           AssertionError: assert 302 == 200
E            +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'http://www.google.com/sorry/index?continue=http://www.google....html; charset=UTF-8', 'server': 'HTTP server (unknown)', 'content-length': '324', 'x-xss-protection': '1; mode=block'}>.status_code

src/geventhttpclient/tests/test_client.py:48: AssertionError
________________________________________________________________________________________ test_request_with_headers _________________________________________________________________________________________

    def test_request_with_headers():
        client = HTTPClient('www.google.fr')
        response = client.get('/', headers=test_headers)
>       assert response.status_code == 200
E       AssertionError: assert 302 == 200
E        +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'http://www.google.com/sorry/index?continue=http://www.google....html; charset=UTF-8', 'server': 'HTTP server (unknown)', 'content-length': '324', 'x-xss-protection': '1; mode=block'}>.status_code

src/geventhttpclient/tests/test_client.py:59: AssertionError
______________________________________________________________________________________ test_response_context_manager _______________________________________________________________________________________

    def test_response_context_manager():
        client = HTTPClient.from_url('http://www.google.fr/')
        r = None
        with client.get('/') as response:
>           assert response.status_code == 200
E           AssertionError: assert 302 == 200
E            +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'http://www.google.com/sorry/index?continue=http://www.google....html; charset=UTF-8', 'server': 'HTTP server (unknown)', 'content-length': '324', 'x-xss-protection': '1; mode=block'}>.status_code

src/geventhttpclient/tests/test_client.py:84: AssertionError
_____________________________________________________________________________________________ test_client_ssl ______________________________________________________________________________________________

    @pytest.mark.skipif(
        os.environ.get("TRAVIS") == "true",
        reason="We have issues on travis with the SSL tests"
    )
    def test_client_ssl():
        client = HTTPClient('www.google.fr', ssl=True)
        assert client.port == 443
        response = client.get('/')
>       assert response.status_code == 200
E       assert 302 == 200
E        +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'https://www.google.com/sorry/index?continue=https://www.googl..., 'content-length': '326', 'x-xss-protection': '1; mode=block', 'alt-svc': 'quic=":443"; ma=2592000; v="44,43,39,35"'}>.status_code

src/geventhttpclient/tests/test_client.py:96: AssertionError
_____________________________________________________________________________________ test_multi_queries_greenlet_safe _____________________________________________________________________________________

    def test_multi_queries_greenlet_safe():
        client = HTTPClient('www.google.fr', concurrency=3)
        group = gevent.pool.Group()
        event = gevent.event.Event()
    
        def run(i):
            event.wait()
            response = client.get('/')
            return response, response.read()
    
        count = 0
    
        gevent.spawn_later(0.2, event.set)
        for response, content in group.imap_unordered(run, xrange(5)):
>           assert response.status_code == 200
E           AssertionError: assert 302 == 200
E            +  where 302 = <HTTPSocketPoolResponse status=302 headers={'location': 'http://www.google.com/sorry/index?continue=http://www.google....html; charset=UTF-8', 'server': 'HTTP server (unknown)', 'content-length': '324', 'x-xss-protection': '1; mode=block'}>.status_code

src/geventhttpclient/tests/test_client.py:127: AssertionError
=================================================================================== 6 failed, 79 passed in 6.57 seconds ====================================================================================
ERROR: InvocationError: '/home/crystal2/github/geventhttpclient/.tox/py36/bin/py.test src/geventhttpclient/tests'
_________________________________________________________________________________________________ summary __________________________________________________________________________________________________
ERROR:   py27: commands failed
ERROR:   py35: InterpreterNotFound: python3.5
ERROR:   py36: commands failed

And I 'm bit confused about why you asked

test cases for the things you intend to fix with your pull request

because that is what I added to test_ssl.py. If that is not enough, could you please point what I do not cover with them?

@ml31415
Copy link
Collaborator

ml31415 commented Oct 19, 2018

test cases for the things you intend to fix with your pull request

because that is what I added to test_ssl.py. If that is not enough, could you please point what I do not cover with them?

Sorry, my fault, had missed that when I briefly looked over it. Didn't go through it thoroughly, as the tests were still failing. I suggest to replace google with http://httpbin.org/ if the issues remain, for all occurences of google in the tests.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 19, 2018

Ok, here is the update.
I had to use github.com instead of httpbin.org for ssl test, apparently, because httpbin.org requires SNI, and it only works if context factory is in use (bare wrap_socket lacks this functionality), but i didnt want to make the test code more complex than needed

@ml31415
Copy link
Collaborator

ml31415 commented Oct 19, 2018

About dpkt, if I see it correctly, it's merely using the functions tls_multi_factory and parse_variable_array and the unpacking of the data field of TLSHandshake, all together maybe 30 lines of code to replicate. I'd rather prefer to simply copy that functionality into our ssl handling than having an external dependency. I'm aware that there are ups and downs to that approach, but imho it's the more hazzle-free variant in the long run.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 19, 2018

hmm, I'll see if I manage to extract it on the weekend. this is dev Dep anyway, so I thought it wouldn't be that bad

@ml31415
Copy link
Collaborator

ml31415 commented Oct 19, 2018

Sure. And thanks already for contributing to our little project here!

@ojomio
Copy link
Contributor Author

ojomio commented Oct 19, 2018

It seems there is more than 30 lines, as it makes use of of TLSRecord, which inherits from base Packet class , so it would be cumbersome to try to extract all this machinery from the dpkt.
We may try to parse it manually, but i prefer to use the tools available

Copy link
Collaborator

@gwik gwik left a comment

Choose a reason for hiding this comment

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

Thanks for this contribution. Supporting SNI is a must to keep this library alive.
I'm confused by the failing CI, has dpkt introduced a breaking change ?
I will gratefully merge this as soon as the CI is green.
For the added dependency don't copy source code. I understand Michael's point of view but I don't think it worth for a test dep.

insecure=False,
proxy_host=None, proxy_port=None, version=HTTP_11,
headers_type=Headers):
if headers is None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't get this change, what's the point ? I'm out of the python world for some time now...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ojomio
Copy link
Contributor Author

ojomio commented Oct 22, 2018

Apparently, Travis did not install the dev requirements, I tried to correct it in the config, but someone probably need to actually install it

@ojomio
Copy link
Contributor Author

ojomio commented Oct 23, 2018

would anybody please try to install the travis script if I'm right about the cause of the error?

setup.py Outdated
install_requires=requirements)
install_requires=requirements,
extras_require={
'dev': get_requirements('requirements-dev.txt'),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this handle the "-r requirements.txt" correctly?

.travis.yml Outdated
@@ -6,7 +6,7 @@ python:
- "3.5"
- "3.6"
install:
- "pip install ."
- "pip install .[dev]"
Copy link
Collaborator

@ml31415 ml31415 Oct 23, 2018

Choose a reason for hiding this comment

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

How about

- pip install -r requirements-dev.txt
- pip install .

Can't find a proper documentation for this [] syntax right now. Keeping simple things simple, and that should work in any case

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also

before_install:
  - pip install -U pip

might be an option, in case the 9.0.1 on Travis is not recent enough for that extras_require syntax.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 23, 2018

I have no idea why the travis build hangs, tox on my machine feels good:

=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.6.5, pytest-3.9.2, py-1.7.0, pluggy-0.8.0
rootdir: /home/crystal2/github/geventhttpclient, inifile:
collected 88 items                                                                                                                                                                                         

src/geventhttpclient/tests/test_client.py ....................                                                                                                                                       [ 22%]
src/geventhttpclient/tests/test_headers.py .............                                                                                                                                             [ 37%]
src/geventhttpclient/tests/test_httplib.py ...                                                                                                                                                       [ 40%]
src/geventhttpclient/tests/test_keep_alive.py .....                                                                                                                                                  [ 46%]
src/geventhttpclient/tests/test_network_failures.py .........                                                                                                                                        [ 56%]
src/geventhttpclient/tests/test_no_module_ssl.py ..                                                                                                                                                  [ 59%]
src/geventhttpclient/tests/test_parser.py .......                                                                                                                                                    [ 67%]
src/geventhttpclient/tests/test_ssl.py .......                                                                                                                                                       [ 75%]
src/geventhttpclient/tests/test_url.py ..............                                                                                                                                                [ 90%]
src/geventhttpclient/tests/test_useragent.py ........                                                                                                                                                [100%]

======================================================================================== 88 passed in 8.35 seconds =========================================================================================
_________________________________________________________________________________________________ summary __________________________________________________________________________________________________
  py27: commands succeeded
ERROR:   py35: InterpreterNotFound: python3.5
  py36: commands succeeded

@ml31415
Copy link
Collaborator

ml31415 commented Oct 24, 2018

Did you try it with the same dependency versions like on travis? Just to exclude issues with them.

  • certifi-2018.10.15
  • dpkt-1.9.1
  • gevent-1.3.7
  • greenlet-0.4.15

@ojomio
Copy link
Contributor Author

ojomio commented Oct 24, 2018

I now think this is somehow related with hostname resolving, I 'll try to mock it, because I currently rely on gethostbyaddr and it may return something that may not resolve back to localhost

@ml31415
Copy link
Collaborator

ml31415 commented Oct 24, 2018

Just for the record, tox also runs fine on my machine. You might try to set -v and -s for pytest, to get some more details into the travis log, and temporarily throw in some debug prints into the test case. It might hang in some other place you didn't expect, some gevent loop story or whatever.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 25, 2018

I think that is it

@ml31415
Copy link
Collaborator

ml31415 commented Oct 25, 2018

Looks good to me. @gwik ?

@gwik
Copy link
Collaborator

gwik commented Oct 25, 2018 via email

@ml31415 ml31415 merged commit 3fefedd into geventhttpclient:master Oct 25, 2018
@ojomio
Copy link
Contributor Author

ojomio commented Oct 25, 2018

It was tough, thanks)

@ml31415
Copy link
Collaborator

ml31415 commented Oct 25, 2018

Just went through some older bug reports to see if they were related and possibly fixed. Trying

from geventhttpclient.useragent import UserAgent
ua.urlopen('https://bison.streethawk.com/').read()
>>> CertificateError: hostname 'bison.streethawk.com' doesn't match either of '*.pointzi.com', 'pointzi.com'

Chrome and Firefox seem to have no issues with that site and get a correct certificate. Might be worth to have another look I guess.

@ojomio
Copy link
Contributor Author

ojomio commented Oct 25, 2018

In [11]: ua = UserAgent( ssl_context_factory=gevent.ssl.create_default_context)

In [12]: ua.urlopen('https://bison.streethawk.com/').read()
Out[12]: '<!doctype html>\r\n<html lang="en">\r\n<head>\r\n ...

I'm not hyper experienced with SSL and don't know why we shouldn't make this context default context , but without context there is no way to use SNI, because gevent/_ssl3.py:

def wrap_socket(sock, keyfile=None, certfile=None,
                server_side=False, cert_reqs=CERT_NONE,
                ssl_version=PROTOCOL_SSLv23, ca_certs=None,
                do_handshake_on_connect=True,
                suppress_ragged_eofs=True,
                ciphers=None):

    return SSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
                     server_side=server_side, cert_reqs=cert_reqs,
                     ssl_version=ssl_version, ca_certs=ca_certs,
                     do_handshake_on_connect=do_handshake_on_connect,
                     suppress_ragged_eofs=suppress_ragged_eofs,
                     ciphers=ciphers)

@ml31415
Copy link
Collaborator

ml31415 commented Oct 25, 2018

Me neither, but if that context is necessary for SNI to work, it should be set. Or is there any harmful side effect I'd be missing?

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.

3 participants