Skip to content

Commit c40e4dc

Browse files
committed
Merge pull request #48 from neo4j/1.0-graceful-ssl-degradation
Graceful SSL degradation
2 parents 6debbc9 + e7a0034 commit c40e4dc

File tree

6 files changed

+65
-12
lines changed

6 files changed

+65
-12
lines changed

examples/test_examples.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
# limitations under the License.
2020

2121

22-
from unittest import skip
22+
from unittest import skip, skipUnless
2323

24-
from neo4j.v1 import TRUST_ON_FIRST_USE, TRUST_SIGNED_CERTIFICATES
24+
from neo4j.v1 import TRUST_ON_FIRST_USE, TRUST_SIGNED_CERTIFICATES, SSL_AVAILABLE
2525
from test.util import ServerTestCase
2626

2727
# Do not change the contents of this tagged section without good reason*
@@ -74,11 +74,13 @@ def test_configuration(self):
7474
# end::configuration[]
7575
return driver
7676

77+
@skipUnless(SSL_AVAILABLE, "Bolt over TLS is not supported by this version of Python")
7778
def test_tls_require_encryption(self):
7879
# tag::tls-require-encryption[]
7980
driver = GraphDatabase.driver("bolt://localhost", auth=basic_auth("neo4j", "password"), encrypted=True)
8081
# end::tls-require-encryption[]
8182

83+
@skipUnless(SSL_AVAILABLE, "Bolt over TLS is not supported by this version of Python")
8284
def test_tls_trust_on_first_use(self):
8385
# tag::tls-trust-on-first-use[]
8486
driver = GraphDatabase.driver("bolt://localhost", auth=basic_auth("neo4j", "password"), encrypted=True, trust=TRUST_ON_FIRST_USE)

neo4j/v1/connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
from os.path import dirname, isfile
3030
from select import select
3131
from socket import create_connection, SHUT_RDWR, error as SocketError
32-
from ssl import HAS_SNI, SSLError
3332
from struct import pack as struct_pack, unpack as struct_unpack, unpack_from as struct_unpack_from
3433

3534
from .constants import DEFAULT_PORT, DEFAULT_USER_AGENT, KNOWN_HOSTS, MAGIC_PREAMBLE, \
3635
TRUST_DEFAULT, TRUST_ON_FIRST_USE
3736
from .compat import hex2
3837
from .exceptions import ProtocolError
3938
from .packstream import Packer, Unpacker
39+
from .ssl_compat import SSL_AVAILABLE, HAS_SNI, SSLError
4040

4141

4242
# Signature bytes for each message type
@@ -385,7 +385,7 @@ def connect(host, port=None, ssl_context=None, **config):
385385
raise
386386

387387
# Secure the connection if an SSL context has been provided
388-
if ssl_context:
388+
if ssl_context and SSL_AVAILABLE:
389389
if __debug__: log_info("~~ [SECURE] %s", host)
390390
try:
391391
s = ssl_context.wrap_socket(s, server_hostname=host if HAS_SNI else None)

neo4j/v1/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from os.path import expanduser, join
2323

2424
from ..meta import version
25+
from .ssl_compat import SSL_AVAILABLE
2526

2627

2728
DEFAULT_PORT = 7687
@@ -31,7 +32,7 @@
3132

3233
MAGIC_PREAMBLE = 0x6060B017
3334

34-
ENCRYPTED_DEFAULT = True
35+
ENCRYPTED_DEFAULT = SSL_AVAILABLE
3536

3637
TRUST_ON_FIRST_USE = 0
3738
TRUST_SIGNED_CERTIFICATES = 1

neo4j/v1/session.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ class which can be used to obtain `Driver` instances that are used for
2929
from __future__ import division
3030

3131
from collections import deque, namedtuple
32-
from ssl import SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED
3332

3433
from .compat import integer, string, urlparse
3534
from .connection import connect, Response, RUN, PULL_ALL
3635
from .constants import ENCRYPTED_DEFAULT, TRUST_DEFAULT, TRUST_SIGNED_CERTIFICATES
3736
from .exceptions import CypherError
37+
from .ssl_compat import SSL_AVAILABLE, SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED
3838
from .types import hydrated
3939

4040

@@ -84,6 +84,18 @@ def driver(url, **config):
8484
return Driver(url, **config)
8585

8686

87+
_warned_about_insecure_default = False
88+
89+
90+
def _warn_about_insecure_default():
91+
global _warned_about_insecure_default
92+
if not SSL_AVAILABLE and not _warned_about_insecure_default:
93+
from warnings import warn
94+
warn("Bolt over TLS is only available in Python 2.7.9+ and Python 3.3+ "
95+
"so communications are not secure")
96+
_warned_about_insecure_default = True
97+
98+
8799
class Driver(object):
88100
""" Accessor for a specific graph database resource.
89101
"""
@@ -99,9 +111,15 @@ def __init__(self, url, **config):
99111
self.config = config
100112
self.max_pool_size = config.get("max_pool_size", DEFAULT_MAX_POOL_SIZE)
101113
self.session_pool = deque()
102-
self.encrypted = encrypted = config.get("encrypted", ENCRYPTED_DEFAULT)
114+
try:
115+
self.encrypted = encrypted = config["encrypted"]
116+
except KeyError:
117+
_warn_about_insecure_default()
118+
self.encrypted = encrypted = ENCRYPTED_DEFAULT
103119
self.trust = trust = config.get("trust", TRUST_DEFAULT)
104120
if encrypted:
121+
if not SSL_AVAILABLE:
122+
raise RuntimeError("Bolt over TLS is only available in Python 2.7.9+ and Python 3.3+")
105123
ssl_context = SSLContext(PROTOCOL_SSLv23)
106124
ssl_context.options |= OP_NO_SSLv2
107125
if trust >= TRUST_SIGNED_CERTIFICATES:

neo4j/v1/ssl_compat.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
4+
# Copyright (c) 2002-2016 "Neo Technology,"
5+
# Network Engine for Objects in Lund AB [http://neotechnology.com]
6+
#
7+
# This file is part of Neo4j.
8+
#
9+
# Licensed under the Apache License, Version 2.0 (the "License");
10+
# you may not use this file except in compliance with the License.
11+
# You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
21+
try:
22+
from ssl import SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED, HAS_SNI, SSLError
23+
except ImportError:
24+
SSL_AVAILABLE = False
25+
SSLContext = None
26+
PROTOCOL_SSLv23 = None
27+
OP_NO_SSLv2 = None
28+
CERT_REQUIRED = None
29+
HAS_SNI = None
30+
SSLError = None
31+
else:
32+
SSL_AVAILABLE = True

test/test_session.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121

2222
from socket import socket
2323
from ssl import SSLSocket
24+
from unittest import skipUnless
2425

2526
from mock import patch
27+
2628
from neo4j.v1.constants import TRUST_ON_FIRST_USE
2729
from neo4j.v1.exceptions import CypherError
28-
from neo4j.v1.session import GraphDatabase, basic_auth, Record
30+
from neo4j.v1.session import GraphDatabase, basic_auth, Record, SSL_AVAILABLE
2931
from neo4j.v1.types import Node, Relationship, Path
3032

3133
from test.util import ServerTestCase
@@ -90,10 +92,6 @@ def test_sessions_are_not_reused_if_still_in_use(self):
9092

9193
class SecurityTestCase(ServerTestCase):
9294

93-
def test_default_session_uses_tofu(self):
94-
driver = GraphDatabase.driver("bolt://localhost")
95-
assert driver.trust == TRUST_ON_FIRST_USE
96-
9795
def test_insecure_session_uses_normal_socket(self):
9896
driver = GraphDatabase.driver("bolt://localhost", auth=auth_token, encrypted=False)
9997
session = driver.session()
@@ -102,6 +100,7 @@ def test_insecure_session_uses_normal_socket(self):
102100
assert connection.der_encoded_server_certificate is None
103101
session.close()
104102

103+
@skipUnless(SSL_AVAILABLE, "Bolt over TLS is not supported by this version of Python")
105104
def test_tofu_session_uses_secure_socket(self):
106105
driver = GraphDatabase.driver("bolt://localhost", auth=auth_token, encrypted=True, trust=TRUST_ON_FIRST_USE)
107106
session = driver.session()
@@ -110,6 +109,7 @@ def test_tofu_session_uses_secure_socket(self):
110109
assert connection.der_encoded_server_certificate is not None
111110
session.close()
112111

112+
@skipUnless(SSL_AVAILABLE, "Bolt over TLS is not supported by this version of Python")
113113
def test_tofu_session_trusts_certificate_after_first_use(self):
114114
driver = GraphDatabase.driver("bolt://localhost", auth=auth_token, encrypted=True, trust=TRUST_ON_FIRST_USE)
115115
session = driver.session()

0 commit comments

Comments
 (0)