forked from thispc/psiphon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpsi_api.py
289 lines (241 loc) · 11.7 KB
/
psi_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/python
#
# Copyright (c) 2012, Psiphon Inc.
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import sys
import httplib
import ssl
import binascii
import json
sys.path.insert(0, 'SocksiPy')
import socks
import socket
socket.socket = socks.socksocket
import urllib2
#
# Psiphon 3 Server API
#
class Psiphon3Server(object):
def __init__(self, servers, propagation_channel_id, sponsor_id, client_version, client_platform):
self.servers = servers
server_entry = binascii.unhexlify(servers[0]).split(" ")
(self.ip_address, self.web_server_port, self.web_server_secret,
self.web_server_certificate) = server_entry[:4]
# read the new json config element of the server entry, if present
self.extended_config = None
if len(server_entry) > 4:
try:
self.extended_config = json.loads(' '.join(server_entry[4:]))
except Exception:
pass
self.propagation_channel_id = propagation_channel_id
self.sponsor_id = sponsor_id
self.client_version = client_version
self.client_platform = client_platform
self.handshake_response = None
self.client_session_id = os.urandom(16).encode('hex')
socks.setdefaultproxy()
handler = CertificateMatchingHTTPSHandler(self.web_server_certificate)
self.opener = urllib2.build_opener(handler)
def set_socks_proxy(self, proxy_port):
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', proxy_port)
def _has_extended_config_key(self, key):
if not self.extended_config: return False
return key in self.extended_config
def _has_extended_config_value(self, key):
if not self._has_extended_config_key(key): return False
return ((type(self.extended_config[key]) == str and len(self.extended_config[key]) > 0) or
(type(self.extended_config[key]) == unicode and len(self.extended_config[key]) > 0) or
(type(self.extended_config[key]) == int and self.extended_config[key] != 0) or
(type(self.extended_config[key]) == list))
# This will return False if there is not enough information in the server entry to determine
# if the relay protocol is supported.
def relay_not_supported(self, relay_protocol):
if relay_protocol not in ['SSH', 'OSSH']: return True
if self._has_extended_config_value('capabilities'):
return relay_protocol not in self.extended_config['capabilities']
if relay_protocol == 'SSH':
if (self._has_extended_config_key('sshPort') and
not self._has_extended_config_value('sshPort')): return True
elif relay_protocol == 'OSSH':
if (self._has_extended_config_key('sshObfuscatedPort') and
not self._has_extended_config_value('sshObfuscatedPort')): return True
if (self._has_extended_config_key('sshObfuscatedKey') and
not self._has_extended_config_value('sshObfuscatedKey')): return True
else:
return True
return False
def can_attempt_relay_before_handshake(self, relay_protocol):
if relay_protocol not in ['SSH', 'OSSH']: return False
if not self._has_extended_config_value('sshUsername'): return False
if not self._has_extended_config_value('sshPassword'): return False
if not self._has_extended_config_value('sshHostKey'): return False
if relay_protocol == 'SSH':
if not self._has_extended_config_value('sshPort'): return False
elif relay_protocol == 'OSSH':
if not self._has_extended_config_value('sshObfuscatedPort'): return False
if not self._has_extended_config_value('sshObfuscatedKey'): return False
else:
return False
return True
# handshake
# Note that self.servers may be updated with newly discovered servers after a successful handshake
# TODO: upgrade the current server entry if not self.extended_config
# TODO: page view regexes
def handshake(self, relay_protocol):
request_url = (self._common_request_url(relay_protocol) % ('handshake',) + '&' +
'&'.join(['known_server=%s' % (binascii.unhexlify(server).split(" ")[0],) for server in self.servers]))
response = self.opener.open(request_url).read()
self.handshake_response = {'Upgrade': '',
'SSHPort': '',
'SSHUsername': '',
'SSHPassword': '',
'SSHHostKey': '',
'SSHSessionID': '',
'SSHObfuscatedPort': '',
'SSHObfuscatedKey': '',
'PSK': '',
'Homepage': []}
for line in response.split('\n'):
key, value = line.split(': ', 1)
if key in self.handshake_response.keys():
if type(self.handshake_response[key]) == list:
self.handshake_response[key].append(value)
else:
self.handshake_response[key] = value
if key == 'Server':
# discovery
if value not in self.servers:
self.servers.insert(1, value)
if key == 'SSHSessionID':
self.ssh_session_id = value
return self.handshake_response
def get_ip_address(self):
return self.ip_address
def get_ssh_port(self):
if self.handshake_response:
return self.handshake_response['SSHPort']
if self._has_extended_config_value('sshPort'):
return self.extended_config['sshPort']
return None
def get_username(self):
if self.handshake_response:
return self.handshake_response['SSHUsername']
if self._has_extended_config_value('sshUsername'):
return self.extended_config['sshUsername']
return None
def get_password(self):
if self.handshake_response:
return self.handshake_response['SSHPassword']
if self._has_extended_config_value('sshPassword'):
return self.extended_config['sshPassword']
return None
def get_password_for_ssh_authentication(self):
return self.client_session_id + self.get_password()
def get_host_key(self):
if self.handshake_response:
return self.handshake_response['SSHHostKey']
if self._has_extended_config_value('sshHostKey'):
return self.extended_config['sshHostKey']
return None
def get_obfuscated_ssh_port(self):
if self.handshake_response:
return self.handshake_response['SSHObfuscatedPort']
if self._has_extended_config_value('sshObfuscatedPort'):
return self.extended_config['sshObfuscatedPort']
return None
def get_obfuscate_keyword(self):
if self.handshake_response:
return self.handshake_response['SSHObfuscatedKey']
if self._has_extended_config_value('sshObfuscatedKey'):
return self.extended_config['sshObfuscatedKey']
return None
# TODO: download
# connected
# For SSH and OSSH, SSHSessionID from the handshake response is used when session_id is None
# For VPN, the VPN IP Address should be used for session_id (ie. 10.0.0.2)
def connected(self, relay_protocol, session_id=None):
if not session_id and relay_protocol in ['SSH', 'OSSH']:
session_id = self.ssh_session_id
assert session_id is not None
request_url = (self._common_request_url(relay_protocol) % ('connected',) +
'&session_id=%s' % (session_id,))
self.opener.open(request_url)
# disconnected
# For SSH and OSSH, SSHSessionID from the handshake response is used when session_id is None
# For VPN, this should not be called
def disconnected(self, relay_protocol, session_id=None):
assert relay_protocol not in ['VPN']
if not session_id and relay_protocol in ['SSH', 'OSSH']:
session_id = self.ssh_session_id
assert session_id is not None
request_url = (self._common_request_url(relay_protocol) % ('status',) +
'&session_id=%s&connected=%s' % (session_id, '0'))
self.opener.open(request_url)
# TODO: failed
# TODO: status
def _common_request_url(self, relay_protocol):
assert relay_protocol in ['VPN','SSH','OSSH']
return 'https://%s:%s/%%s?server_secret=%s&propagation_channel_id=%s&sponsor_id=%s&client_version=%s&client_platform=%s&relay_protocol=%s&client_session_id=%s' % (
self.ip_address, self.web_server_port, self.web_server_secret,
self.propagation_channel_id, self.sponsor_id, self.client_version,
self.client_platform, relay_protocol, self.client_session_id)
#
# CertificateMatchingHTTPSHandler
#
# Adapted from CertValidatingHTTPSConnection and VerifiedHTTPSHandler
# http://stackoverflow.com/questions/1087227/validate-ssl-certificates-with-python
#
class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
def __init__(self, host, cert, reason):
httplib.HTTPException.__init__(self)
self.host = host
self.cert = cert
self.reason = reason
def __str__(self):
return ('Host %s returned an invalid certificate (%s) %s\n' %
(self.host, self.reason, self.cert))
class CertificateMatchingHTTPSConnection(httplib.HTTPConnection):
def __init__(self, host, expected_server_certificate, **kwargs):
httplib.HTTPConnection.__init__(self, host, **kwargs)
self.expected_server_certificate = expected_server_certificate
def connect(self):
sock = socket.create_connection((self.host, self.port))
self.sock = ssl.wrap_socket(sock)
cert = ssl.DER_cert_to_PEM_cert(self.sock.getpeercert(True))
# Remove newlines and -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
cert = ''.join(cert.splitlines())[len('-----BEGIN CERTIFICATE-----'):-len('-----END CERTIFICATE-----')]
if cert != self.expected_server_certificate:
raise InvalidCertificateException(self.host, cert,
'server presented the wrong certificate')
class CertificateMatchingHTTPSHandler(urllib2.HTTPSHandler):
def __init__(self, expected_server_certificate):
urllib2.AbstractHTTPHandler.__init__(self)
self.expected_server_certificate = expected_server_certificate
def https_open(self, req):
def http_class_wrapper(host, **kwargs):
return CertificateMatchingHTTPSConnection(
host, self.expected_server_certificate, **kwargs)
try:
return self.do_open(http_class_wrapper, req)
except urllib2.URLError, e:
if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
raise InvalidCertificateException(req.host, '',
e.reason.args[1])
raise
https_request = urllib2.HTTPSHandler.do_request_