]>
Commit | Line | Data |
---|---|---|
95ef30a1 JM |
1 | #!/usr/bin/python |
2 | ||
3 | ''' | |
4 | Python WebSocket library with support for "wss://" encryption. | |
31407abc JM |
5 | Copyright 2010 Joel Martin |
6 | Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) | |
95ef30a1 JM |
7 | |
8 | You can make a cert/key with openssl using: | |
9 | openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem | |
10 | as taken from http://docs.python.org/dev/library/ssl.html#certificates | |
11 | ||
12 | ''' | |
13 | ||
486cd527 | 14 | import sys, socket, ssl, struct, traceback |
6ee61a4c | 15 | import os, resource, errno, signal # daemonizing |
95ef30a1 | 16 | from base64 import b64encode, b64decode |
5958fb49 JM |
17 | try: |
18 | from hashlib import md5 | |
19 | except: | |
20 | from md5 import md5 # Support python 2.4 | |
21 | from urlparse import urlsplit | |
22 | from cgi import parse_qsl | |
95ef30a1 | 23 | |
6ee61a4c JM |
24 | settings = { |
25 | 'listen_host' : '', | |
26 | 'listen_port' : None, | |
27 | 'handler' : None, | |
28 | 'cert' : None, | |
29 | 'ssl_only' : False, | |
30 | 'daemon' : True, | |
31 | 'record' : None, } | |
95ef30a1 JM |
32 | |
33 | server_handshake = """HTTP/1.1 101 Web Socket Protocol Handshake\r | |
34 | Upgrade: WebSocket\r | |
35 | Connection: Upgrade\r | |
486cd527 JM |
36 | %sWebSocket-Origin: %s\r |
37 | %sWebSocket-Location: %s://%s%s\r | |
38 | %sWebSocket-Protocol: sample\r | |
95ef30a1 | 39 | \r |
486cd527 | 40 | %s""" |
95ef30a1 JM |
41 | |
42 | policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n""" | |
43 | ||
44 | def traffic(token="."): | |
45 | sys.stdout.write(token) | |
46 | sys.stdout.flush() | |
47 | ||
1eba7b42 | 48 | def encode(buf): |
55dee432 | 49 | buf = b64encode(buf) |
1eba7b42 | 50 | |
a9469926 | 51 | return "\x00%s\xff" % buf |
1eba7b42 | 52 | |
95ef30a1 JM |
53 | def decode(buf): |
54 | """ Parse out WebSocket packets. """ | |
55 | if buf.count('\xff') > 1: | |
55dee432 | 56 | return [b64decode(d[1:]) for d in buf.split('\xff')] |
95ef30a1 | 57 | else: |
55dee432 | 58 | return [b64decode(buf[1:-1])] |
95ef30a1 | 59 | |
1eba7b42 JM |
60 | def parse_handshake(handshake): |
61 | ret = {} | |
62 | req_lines = handshake.split("\r\n") | |
63 | if not req_lines[0].startswith("GET "): | |
64 | raise Exception("Invalid handshake: no GET request line") | |
65 | ret['path'] = req_lines[0].split(" ")[1] | |
66 | for line in req_lines[1:]: | |
67 | if line == "": break | |
c95c24e7 | 68 | var, val = line.split(": ") |
1eba7b42 | 69 | ret[var] = val |
95ef30a1 | 70 | |
1eba7b42 JM |
71 | if req_lines[-2] == "": |
72 | ret['key3'] = req_lines[-1] | |
73 | ||
74 | return ret | |
75 | ||
76 | def gen_md5(keys): | |
77 | key1 = keys['Sec-WebSocket-Key1'] | |
78 | key2 = keys['Sec-WebSocket-Key2'] | |
79 | key3 = keys['key3'] | |
80 | spaces1 = key1.count(" ") | |
81 | spaces2 = key2.count(" ") | |
82 | num1 = int("".join([c for c in key1 if c.isdigit()])) / spaces1 | |
83 | num2 = int("".join([c for c in key2 if c.isdigit()])) / spaces2 | |
84 | ||
85 | return md5(struct.pack('>II8s', num1, num2, key3)).digest() | |
95ef30a1 JM |
86 | |
87 | ||
6ee61a4c | 88 | def do_handshake(sock): |
6ee61a4c | 89 | |
95ef30a1 JM |
90 | # Peek, but don't read the data |
91 | handshake = sock.recv(1024, socket.MSG_PEEK) | |
92 | #print "Handshake [%s]" % repr(handshake) | |
1eba7b42 JM |
93 | if handshake == "": |
94 | print "Ignoring empty handshake" | |
95 | sock.close() | |
96 | return False | |
97 | elif handshake.startswith("<policy-file-request/>"): | |
95ef30a1 JM |
98 | handshake = sock.recv(1024) |
99 | print "Sending flash policy response" | |
100 | sock.send(policy_response) | |
101 | sock.close() | |
102 | return False | |
103 | elif handshake.startswith("\x16"): | |
104 | retsock = ssl.wrap_socket( | |
105 | sock, | |
106 | server_side=True, | |
6ee61a4c | 107 | certfile=settings['cert'], |
95ef30a1 JM |
108 | ssl_version=ssl.PROTOCOL_TLSv1) |
109 | scheme = "wss" | |
6ee61a4c JM |
110 | print " using SSL/TLS" |
111 | elif settings['ssl_only']: | |
459b2578 JM |
112 | print "Non-SSL connection disallowed" |
113 | sock.close() | |
114 | return False | |
95ef30a1 JM |
115 | else: |
116 | retsock = sock | |
117 | scheme = "ws" | |
6ee61a4c | 118 | print " using plain (not SSL) socket" |
95ef30a1 | 119 | handshake = retsock.recv(4096) |
c3785ae1 | 120 | #print "handshake: " + repr(handshake) |
486cd527 | 121 | h = parse_handshake(handshake) |
95ef30a1 | 122 | |
486cd527 JM |
123 | if h.get('key3'): |
124 | trailer = gen_md5(h) | |
125 | pre = "Sec-" | |
1eba7b42 | 126 | print " using protocol version 76" |
486cd527 JM |
127 | else: |
128 | trailer = "" | |
129 | pre = "" | |
1eba7b42 | 130 | print " using protocol version 75" |
486cd527 JM |
131 | |
132 | response = server_handshake % (pre, h['Origin'], pre, scheme, | |
133 | h['Host'], h['path'], pre, trailer) | |
134 | ||
c3785ae1 | 135 | #print "sending response:", repr(response) |
486cd527 | 136 | retsock.send(response) |
95ef30a1 JM |
137 | return retsock |
138 | ||
31407abc | 139 | def daemonize(keepfd=None): |
6ee61a4c JM |
140 | os.umask(0) |
141 | os.chdir('/') | |
142 | os.setgid(os.getgid()) # relinquish elevations | |
143 | os.setuid(os.getuid()) # relinquish elevations | |
144 | ||
145 | # Double fork to daemonize | |
146 | if os.fork() > 0: os._exit(0) # Parent exits | |
147 | os.setsid() # Obtain new process group | |
148 | if os.fork() > 0: os._exit(0) # Parent exits | |
149 | ||
150 | # Signal handling | |
151 | def terminate(a,b): os._exit(0) | |
152 | signal.signal(signal.SIGTERM, terminate) | |
153 | signal.signal(signal.SIGINT, signal.SIG_IGN) | |
154 | ||
155 | # Close open files | |
156 | maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] | |
157 | if maxfd == resource.RLIM_INFINITY: maxfd = 256 | |
158 | for fd in reversed(range(maxfd)): | |
159 | try: | |
31407abc JM |
160 | if fd != keepfd: |
161 | os.close(fd) | |
162 | else: | |
163 | print "Keeping fd: %d" % fd | |
6ee61a4c JM |
164 | except OSError, exc: |
165 | if exc.errno != errno.EBADF: raise | |
166 | ||
167 | # Redirect I/O to /dev/null | |
168 | os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) | |
169 | os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) | |
170 | os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) | |
171 | ||
172 | ||
173 | def start_server(): | |
174 | ||
95ef30a1 JM |
175 | lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
176 | lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
6ee61a4c | 177 | lsock.bind((settings['listen_host'], settings['listen_port'])) |
95ef30a1 | 178 | lsock.listen(100) |
31407abc JM |
179 | |
180 | if settings['daemon']: daemonize(keepfd=lsock.fileno()) | |
181 | ||
95ef30a1 JM |
182 | while True: |
183 | try: | |
6ee61a4c JM |
184 | csock = startsock = None |
185 | print 'waiting for connection on port %s' % settings['listen_port'] | |
95ef30a1 JM |
186 | startsock, address = lsock.accept() |
187 | print 'Got client connection from %s' % address[0] | |
6ee61a4c | 188 | csock = do_handshake(startsock) |
95ef30a1 JM |
189 | if not csock: continue |
190 | ||
6ee61a4c | 191 | settings['handler'](csock) |
95ef30a1 JM |
192 | |
193 | except Exception: | |
194 | print "Ignoring exception:" | |
195 | print traceback.format_exc() | |
196 | if csock: csock.close() | |
6ee61a4c | 197 | if startsock and startsock != csock: startsock.close() |