Skip to content

Commit 43c5577

Browse files
author
Michael Faust
committed
added unittest
added unittest for CherryPy ssl fix certificate and key file have been created using: ``openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt``
1 parent b7c544f commit 43c5577

File tree

3 files changed

+243
-0
lines changed

3 files changed

+243
-0
lines changed

test/server.crt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDazCCAlOgAwIBAgIUHVokn0Hj/tV1U9AE749qXVsncdgwDQYJKoZIhvcNAQEL
3+
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA1MTMxNDA3MjFaFw0yNjA1
5+
MTMxNDA3MjFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
6+
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
7+
AQUAA4IBDwAwggEKAoIBAQDyRq07rTRIY8oAyoF2hqzOntANk9ksJ+YCvbykcssd
8+
kOlLwT795KDfFXBZMYIL9Iz7IBCdoCZSp1YphQh5HsnhkNNFKeUC3F1PDS8VFjPt
9+
hor84q+emfDO/EP9Pq/ZFgcoTAaucgIkVRkCtUuPKhfWDF8yISnCthmGoJHh4yNu
10+
rja3LgecBHu3Wj6mft6QTAQ8538LnsltyFg2TxtXuuHN06fHnqeWdlrEs2Dp0gJT
11+
yOQky/RSxJo4Wb2Wts+eeDJF85dRgdSwjV5FCQcA7hRXX0wAIQu+wbNIjc1mnIC9
12+
qbLxsa1Y65HDOuub4+XqSTPHYyQD2913JuICDaYR3CV1AgMBAAGjUzBRMB0GA1Ud
13+
DgQWBBSP5ZeK+PdQaXoKqlYr15ZjOfVDHzAfBgNVHSMEGDAWgBSP5ZeK+PdQaXoK
14+
qlYr15ZjOfVDHzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDQ
15+
GqC13PKawdUNaEhXoSgsywsFlSOU5pBIqiQGytpFOFKohHOOySTySb2rHAoCdHQB
16+
428c9IpwIrCa2ahkXkLaiVL5HVg9ZDduuQBw4vKUXexJUy6muZo5Ov8HtQLHAtUF
17+
iOEoi/MkMja/xLCDxEA6d4VNwfCFX9vbLcI12oioIZZHzpUdNyTs49Cwf/PECwUL
18+
FAY87y75H4rjDJvsTeOuxFIe1xRF7Ik9V3C1ef9k1GW341cWM5yQIzHSiJOzKDsb
19+
CkiF5w74QTujth2t/zdAjNZbLeMNYcgPNB4mr6iaD4RJgZWRwlRHmyQMDPuKxDEc
20+
Km9w57IdYygjyw5jOx5f
21+
-----END CERTIFICATE-----

test/server.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDyRq07rTRIY8oA
3+
yoF2hqzOntANk9ksJ+YCvbykcssdkOlLwT795KDfFXBZMYIL9Iz7IBCdoCZSp1Yp
4+
hQh5HsnhkNNFKeUC3F1PDS8VFjPthor84q+emfDO/EP9Pq/ZFgcoTAaucgIkVRkC
5+
tUuPKhfWDF8yISnCthmGoJHh4yNurja3LgecBHu3Wj6mft6QTAQ8538LnsltyFg2
6+
TxtXuuHN06fHnqeWdlrEs2Dp0gJTyOQky/RSxJo4Wb2Wts+eeDJF85dRgdSwjV5F
7+
CQcA7hRXX0wAIQu+wbNIjc1mnIC9qbLxsa1Y65HDOuub4+XqSTPHYyQD2913JuIC
8+
DaYR3CV1AgMBAAECggEAHYenzcJKwRgIoxgLt5qqrXSF/2Gp8svaKTNfLtwfDbd/
9+
+A/R0bhwM0C1tOln5HUmSeWaoNvIUAK9acohQkIScT/pwGBe3X5mkSAWQQe3xJfF
10+
kRVAOqCgzVnKH6/oVxlsPekmV1TmFe+ZYM8gKo8C4MAZSk7ofCcd7V7c6R96Th8J
11+
LBiMteUCJbUqO8HxWrDX2BuRei74vJNysdACD/21qBnLPqXjqgnp4H5M1rQl8x6G
12+
OrLdkCQklwb3s+4hioaHbd0i4fvbYeaf/F5ouw6Tqd1zQMZ9bMn/dkeuOR2R1frI
13+
K3TTQHxpC1RqZJAU5TXrp0xlC3VXEAlpmoNa7eaYAQKBgQD9W30SossLqle5i1cJ
14+
Zh58e8Ag3xWrz/OVJQ+PlcS/hweEbuw/MOWXQZqYUznPK89Hd9BXtV311G2GF6zQ
15+
AV6RzTz8UmOmZD343hlite7IJTbUeqYyz6AYPEOLayVHiHZByAMJkr5x2+Wae6TT
16+
RkdyNLPwIbFijRZk7be1S2r1gQKBgQD0zZlX/OMI5a1GIlQmzY13Dx0Pyta05d6H
17+
KUMC2Svr/kIYkGyY2xE3EHy7GIzFD/zm3B9eeTIwJYHbniGJ6YAeNzBdPG8nk2R8
18+
w+1Mj/yliCOwHG89VO830Xovf0EIH+a20+PAtByqvoV2RTbzMS88mTh8URqhtmM/
19+
jK1zG0Gx9QKBgQDlDH4xh+2LOVAf1YI1ZBYxsmtLDIP6FYGAl8XOqLb79GZuax24
20+
L0uRiGTsS2mbC19UnFRFxxkQMyFlNigs0OAfbm4xK4cdmciRIrHOlO4wEbzVMaDp
21+
lN2Gq4zhEVfdqNhItjtQv1Lfes7D7/5eZ04WSOFYOg21LBpP2r3X8DvdgQKBgQCo
22+
CMxeKhbJD6ZdgsjSjbux4qznHys7pqGVk0wNE3bjmXZTGCeC0LRDYMzNPC+8QJou
23+
+R+LIJPDmqtFTYjl+mJX2zgWd5owxyptvasQJ7GbChS9GPd+WOOPI/nDyoygAA3E
24+
pzMpHjijNv2zThVG3xb2eJHeO2mVYPVFNNIGNcplVQKBgH6tNsGlVZDzz6SuOPsT
25+
+3ekTghz52X0BcYtCZVetCdMUkGjA6IhIpW/YNGDS07P4MpONlguThEOOVZJziMg
26+
i0lNFpWThsslsoW+2Os1B3Aim+Tkv7pRPlAVLl10w8Xy0mKW+RU7mtn4kfljSnMt
27+
NGqZSCK3d2AXzjO/axo/xYjT
28+
-----END PRIVATE KEY-----

test/test_cherrypy_ssl.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# -*- coding: utf-8 -*-
2+
"""Tests for the CherryPy and ws4py libraries with SSL support.
3+
"""
4+
5+
# ruff: noqa: D102,D103,D105
6+
7+
import datetime
8+
import os
9+
import time
10+
import unittest
11+
from threading import Thread
12+
13+
import cherrypy
14+
15+
from ws4py.client.threadedclient import WebSocketClient
16+
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
17+
from ws4py.websocket import WebSocket
18+
19+
root = os.path.dirname( os.path.abspath( __file__ ) )
20+
21+
NUMBERS_TO_SEND = 10 # send this amount of numbers back and forth between client and server
22+
TIMEOUT_LIMIT = 20 # CherryPy server timeout limit in seconds
23+
24+
global_data = {
25+
"sent" : [],
26+
"received" : [],
27+
"timedout" : False,
28+
}
29+
30+
class EchoClient( WebSocketClient ):
31+
def opened( self ):
32+
self.send( 1 )
33+
34+
def send( self, payload, binary=False ):
35+
global_data[ "sent" ].append( payload )
36+
return super().send( str( payload ), binary )
37+
38+
def closed( self, code, reason=None ):
39+
cherrypy.engine.exit() # close the CherryPy server
40+
41+
def received_message( self, m ):
42+
m = str( m ).strip()
43+
try:
44+
number = int( m )
45+
global_data[ "received" ].append( number )
46+
if number < NUMBERS_TO_SEND:
47+
# increase number and send it back
48+
self.send( number + 1 )
49+
else:
50+
self.close( 1000, "Done" )
51+
except ( TypeError, ValueError ):
52+
return
53+
54+
55+
class BroadcastWebSocketHandler( WebSocket ):
56+
def received_message( self, message ):
57+
cherrypy.engine.publish( 'websocket-broadcast', str( message ) )
58+
59+
60+
class Root:
61+
@cherrypy.expose
62+
def ws( self ):
63+
pass
64+
65+
66+
def wait_for_cherrypy_engine_started():
67+
while ( cherrypy.engine.state != cherrypy.engine.states.STARTED ):
68+
time.sleep( 0.5 )
69+
70+
71+
def run_echo_client( url ):
72+
wait_for_cherrypy_engine_started()
73+
try:
74+
ws = EchoClient( url )
75+
ws.connect()
76+
ws.run_forever()
77+
except KeyboardInterrupt:
78+
ws.close()
79+
80+
81+
def run_echo_client_thread( host, port, ssl = False ):
82+
"""Run the EchoClient in a separate thread.
83+
"""
84+
url = "wss://%s:%d/ws" % (host, port) if ssl else "ws://%s:%d/ws" % (host, port)
85+
t = Thread( target=run_echo_client, daemon=True, name="WebSocketClient", args=( url, ) )
86+
t.start()
87+
return t
88+
89+
90+
def run_cherrypy_server( host, port, ssl = False ):
91+
config = {
92+
"global" : {
93+
"server.ssl_module" : "builtin" if ssl else None,
94+
"server.ssl_certificate" : os.path.join( root, "server.crt" ) if ssl else None,
95+
"server.ssl_private_key" : os.path.join( root, "server.key" ) if ssl else None,
96+
"server.socket_host" : host,
97+
"server.socket_port" : port,
98+
"log.screen" : False,
99+
'engine.autoreload.on' : False,
100+
},
101+
"/ws" : {
102+
"tools.websocket.on" : True,
103+
"tools.websocket.handler_cls" : BroadcastWebSocketHandler,
104+
'tools.websocket.protocols' : [ 'some-protocol' ],
105+
'tools.gzip.on' : False,
106+
'tools.caching.on' : False,
107+
'tools.sessions.on' : False,
108+
},
109+
}
110+
111+
WebSocketPlugin( cherrypy.engine ).subscribe()
112+
cherrypy.tools.websocket = WebSocketTool()
113+
cherrypy.quickstart( Root(), "/", config=config )
114+
115+
116+
class TimeoutSignaller( Thread ):
117+
def __init__( self, limit, handler ):
118+
Thread.__init__( self, name="TimeoutSignaller" )
119+
self.limit = limit
120+
self.running = True
121+
self.handler = handler
122+
self.daemon = True
123+
assert callable( handler ), "Timeout Handler needs to be a method"
124+
125+
def run( self ):
126+
timeout_limit = datetime.datetime.now() + datetime.timedelta( seconds=self.limit )
127+
while self.running:
128+
if datetime.datetime.now() >= timeout_limit:
129+
self.handler()
130+
self.stop_run()
131+
break
132+
133+
def stop_run( self ):
134+
self.running = False
135+
136+
137+
class CherryPyTimeout:
138+
def __init__( self, seconds=0, minutes=0, hours=0 ):
139+
self.seconds = ( hours * 3600 ) + ( minutes * 60 ) + seconds
140+
self.signal = TimeoutSignaller( self.seconds, self.signal_handler )
141+
self.ok = True
142+
143+
def __enter__( self ):
144+
self.signal.start()
145+
return self
146+
147+
def __exit__( self, exc_type, exc_val, exc_tb ):
148+
self.signal.stop_run()
149+
150+
def done( self ):
151+
self.signal.stop_run()
152+
153+
def signal_handler( self ):
154+
if cherrypy.engine.state == cherrypy.engine.states.STARTED:
155+
global_data[ "timedout" ] = True
156+
cherrypy.engine.exit()
157+
self.ok = False
158+
159+
160+
def run( host, port, ssl ):
161+
run_echo_client_thread( host, port, ssl=ssl )
162+
run_cherrypy_server( host=host, port=port, ssl=ssl )
163+
164+
165+
def run_with_timeout( host, port, ssl ):
166+
with CherryPyTimeout( seconds=TIMEOUT_LIMIT ) as t:
167+
run( host=host, port=port, ssl=ssl )
168+
t.done()
169+
if not t.ok:
170+
global_data[ "timedout" ] = True
171+
raise TimeoutError( "CherryPy server timed out" )
172+
173+
class CherryPySSLTest(unittest.TestCase):
174+
def test_ssl( self ):
175+
global_data[ "sent" ] = []
176+
global_data[ "received" ] = []
177+
global_data[ "timedout" ] = False
178+
179+
run_with_timeout( host="127.0.0.1", port=8877, ssl=True )
180+
181+
assert not global_data[ "timedout" ], "Test timed out"
182+
assert len( global_data[ "sent" ] ) > 0, "No data sent to the server"
183+
assert len( global_data[ "received" ] ) > 0, "No data received from the server"
184+
assert global_data[ "sent" ] == global_data[ "received" ], "Sent and received data do not match"
185+
assert global_data[ "sent" ] == list( range( 1, NUMBERS_TO_SEND + 1 ) ), "Sent data does not match expected data"
186+
187+
188+
if __name__ == "__main__":
189+
suite = unittest.TestSuite()
190+
loader = unittest.TestLoader()
191+
for testcase in [CherryPySSLTest]:
192+
tests = loader.loadTestsFromTestCase(testcase)
193+
suite.addTests(tests)
194+
unittest.TextTestRunner(verbosity=2).run(suite)

0 commit comments

Comments
 (0)