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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
|
#!/usr/bin/env python3
"""\
@file test_llsdmessage_peer.py
@author Nat Goodspeed
@date 2008-10-09
@brief This script asynchronously runs the executable (with args) specified on
the command line, returning its result code. While that executable is
running, we provide dummy local services for use by C++ tests.
$LicenseInfo:firstyear=2008&license=viewerlgpl$
Second Life Viewer Source Code
Copyright (C) 2012-2013, Linden Research, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation;
version 2.1 of the License only.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
$/LicenseInfo$
"""
import os
import sys
import time
import select
import getopt
from io import StringIO
from http.server import HTTPServer, BaseHTTPRequestHandler
from llbase import llsd
# we're in llcorehttp/tests ; testrunner.py is found in llmessage/tests
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
"llmessage", "tests"))
from testrunner import freeport, run, debug, VERBOSE
class TestHTTPRequestHandler(BaseHTTPRequestHandler):
"""This subclass of BaseHTTPRequestHandler is to receive and echo
LLSD-flavored messages sent by the C++ LLHTTPClient.
Target URLs are fairly free-form and are assembled by
concatinating fragments. Currently defined fragments
are:
- '/reflect/' Request headers are bounced back to caller
after prefixing with 'X-Reflect-'
- '/fail/' Body of request can contain LLSD with
'reason' string and 'status' integer
which will become response header.
- '/bug2295/' 206 response, no data in body:
-- '/bug2295/0/' "Content-Range: bytes 0-75/2983"
-- '/bug2295/1/' "Content-Range: bytes 0-75/*"
-- '/bug2295/2/' "Content-Range: bytes 0-75/2983",
"Content-Length: 0"
-- '/bug2295/00000018/0/' Generates PARTIAL_FILE (18) error in libcurl.
"Content-Range: bytes 0-75/2983",
"Content-Length: 76"
-- '/bug2295/inv_cont_range/0/' Generates HE_INVALID_CONTENT_RANGE error in llcorehttp.
- '/503/' Generate 503 responses with various kinds
of 'retry-after' headers
-- '/503/0/' "Retry-After: 2"
-- '/503/1/' "Retry-After: Thu, 31 Dec 2043 23:59:59 GMT"
-- '/503/2/' "Retry-After: Fri, 31 Dec 1999 23:59:59 GMT"
-- '/503/3/' "Retry-After: "
-- '/503/4/' "Retry-After: (*#*(@*(@(")"
-- '/503/5/' "Retry-After: aklsjflajfaklsfaklfasfklasdfklasdgahsdhgasdiogaioshdgo"
-- '/503/6/' "Retry-After: 1 2 3 4 5 6 7 8 9 10"
Some combinations make no sense, there's no effort to protect
you from that.
"""
ignore_exceptions = (Exception,)
def read(self):
# The following logic is adapted from the library module
# SimpleXMLRPCServer.py.
# Get arguments by reading body of request.
# We read this in chunks to avoid straining
# socket.read(); around the 10 or 15Mb mark, some platforms
# begin to have problems (bug #792570).
try:
size_remaining = int(self.headers["content-length"])
except (KeyError, ValueError):
return ""
max_chunk_size = 10*1024*1024
L = bytes()
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
chunk = self.rfile.read(chunk_size)
L += chunk
size_remaining -= len(chunk)
return L.decode("utf-8")
# end of swiped read() logic
def read_xml(self):
# This approach reads the entire POST data into memory first
return llsd.parse(self.read())
## # This approach attempts to stream in the LLSD XML from self.rfile,
## # assuming that the underlying XML parser reads its input file
## # incrementally. Unfortunately I haven't been able to make it work.
## tree = xml_parse(self.rfile)
## debug("Finished raw parse")
## debug("parsed XML tree %s", tree)
## debug("parsed root node %s", tree.getroot())
## debug("root node tag %s", tree.getroot().tag)
## return llsd.to_python(tree.getroot())
def do_HEAD(self):
self.do_GET(withdata=False)
def do_GET(self, withdata=True):
# Of course, don't attempt to read data.
try:
self.answer(dict(reply="success", status=200,
reason="Your GET operation worked"))
except self.ignore_exceptions as e:
print("Exception during GET (ignoring): %s" % str(e), file=sys.stderr)
def do_POST(self):
# Read the provided POST data.
# self.answer(self.read())
try:
self.answer(dict(reply="success", status=200,
reason=self.read()))
except self.ignore_exceptions as e:
print("Exception during POST (ignoring): %s" % str(e), file=sys.stderr)
def do_PUT(self):
# Read the provided PUT data.
# self.answer(self.read())
try:
self.answer(dict(reply="success", status=200,
reason=self.read()))
except self.ignore_exceptions as e:
print("Exception during PUT (ignoring): %s" % str(e), file=sys.stderr)
def answer(self, data, withdata=True):
debug("%s.answer(%s): self.path = %r", self.__class__.__name__, data, self.path)
if "/sleep/" in self.path:
time.sleep(30)
if "/503/" in self.path:
# Tests for various kinds of 'Retry-After' header parsing
body = None
if "/503/0/" in self.path:
self.send_response(503)
self.send_header("retry-after", "2")
elif "/503/1/" in self.path:
self.send_response(503)
self.send_header("retry-after", "Thu, 31 Dec 2043 23:59:59 GMT")
elif "/503/2/" in self.path:
self.send_response(503)
self.send_header("retry-after", "Fri, 31 Dec 1999 23:59:59 GMT")
elif "/503/3/" in self.path:
self.send_response(503)
self.send_header("retry-after", "")
elif "/503/4/" in self.path:
self.send_response(503)
self.send_header("retry-after", "(*#*(@*(@(")
elif "/503/5/" in self.path:
self.send_response(503)
self.send_header("retry-after", "aklsjflajfaklsfaklfasfklasdfklasdgahsdhgasdiogaioshdgo")
elif "/503/6/" in self.path:
self.send_response(503)
self.send_header("retry-after", "1 2 3 4 5 6 7 8 9 10")
else:
# Unknown request
self.send_response(400)
body = "Unknown /503/ path in server"
if "/reflect/" in self.path:
self.reflect_headers()
self.send_header("Content-type", "text/plain")
self.end_headers()
if body:
self.wfile.write(body)
elif "/bug2295/" in self.path:
# Test for https://jira.secondlife.com/browse/BUG-2295
#
# Client can receive a header indicating data should
# appear in the body without actually getting the body.
# Library needs to defend against this case.
#
body = None
if "/bug2295/0/" in self.path:
self.send_response(206)
self.send_header("Content-Range", "bytes 0-75/2983")
elif "/bug2295/1/" in self.path:
self.send_response(206)
self.send_header("Content-Range", "bytes 0-75/*")
elif "/bug2295/2/" in self.path:
self.send_response(206)
self.send_header("Content-Range", "bytes 0-75/2983")
self.send_header("Content-Length", "0")
elif "/bug2295/00000012/0/" in self.path:
self.send_response(206)
self.send_header("Content-Range", "bytes 0-75/2983")
self.send_header("Content-Length", "76")
elif "/bug2295/inv_cont_range/0/" in self.path:
self.send_response(206)
self.send_header("Content-Range", "bytes 0-75/2983")
body = "Some text, but not enough."
else:
# Unknown request
self.send_response(400)
if "/reflect/" in self.path:
self.reflect_headers()
self.send_header("Content-type", "text/plain")
self.end_headers()
if body:
self.wfile.write(body.encode("utf-8"))
elif "fail" not in self.path:
data = data.copy() # we're going to modify
# Ensure there's a "reply" key in data, even if there wasn't before
data["reply"] = data.get("reply", llsd.LLSD("success"))
response = llsd.format_xml(data)
debug("success: %s", response)
self.send_response(200)
if "/reflect/" in self.path:
self.reflect_headers()
self.send_header("Content-type", "application/llsd+xml")
self.send_header("Content-Length", str(len(response)))
self.send_header("X-LL-Special", "Mememememe");
self.end_headers()
if withdata:
self.wfile.write(response)
else: # fail requested
status = data.get("status", 500)
# self.responses maps an int status to a (short, long) pair of
# strings. We want the longer string. That's why we pass a string
# pair to get(): the [1] will select the second string, whether it
# came from self.responses or from our default pair.
reason = data.get("reason",
self.responses.get(status,
("fail requested",
"Your request specified failure status %s "
"without providing a reason" % status))[1])
debug("fail requested: %s: %r", status, reason)
self.send_error(status, reason)
if "/reflect/" in self.path:
self.reflect_headers()
self.end_headers()
def reflect_headers(self):
for (name, val) in self.headers.items():
# print("Header: %s %s" % (name, val), file=sys.stderr)
self.send_header("X-Reflect-" + name, val)
if not VERBOSE:
# When VERBOSE is set, skip both these overrides because they exist to
# suppress output.
def log_request(self, code, size=None):
# For present purposes, we don't want the request splattered onto
# stderr, as it would upset devs watching the test run
pass
def log_error(self, format, *args):
# Suppress error output as well
pass
class Server(HTTPServer):
# This pernicious flag is on by default in HTTPServer. But proper
# operation of freeport() absolutely depends on it being off.
allow_reuse_address = False
# Override of BaseServer.handle_error(). Not too interested
# in errors and the default handler emits a scary traceback
# to stderr which annoys some. Disable this override to get
# default behavior which *shouldn't* cause the program to return
# a failure status.
def handle_error(self, request, client_address):
print('-'*40)
print('Ignoring exception during processing of request from %' % (client_address))
print('-'*40)
if __name__ == "__main__":
do_valgrind = False
path_search = False
options, args = getopt.getopt(sys.argv[1:], "V", ["valgrind"])
for option, value in options:
if option == "-V" or option == "--valgrind":
do_valgrind = True
# function to make a server with specified port
make_server = lambda port: Server(('127.0.0.1', port), TestHTTPRequestHandler)
if not sys.platform.startswith("win"):
# Instantiate a Server(TestHTTPRequestHandler) on a port chosen by the
# runtime.
httpd = make_server(0)
else:
# "Then there's Windows"
# Instantiate a Server(TestHTTPRequestHandler) on the first free port
# in the specified port range.
httpd, port = freeport(range(8000, 8020), make_server)
# Pass the selected port number to the subject test program via the
# environment. We don't want to impose requirements on the test program's
# command-line parsing -- and anyway, for C++ integration tests, that's
# performed in TUT code rather than our own.
os.environ["LL_TEST_PORT"] = str(httpd.server_port)
debug("$LL_TEST_PORT = %s", httpd.server_port)
if do_valgrind:
args = ["valgrind", "--log-file=./valgrind.log"] + args
path_search = True
sys.exit(run(server_inst=httpd, use_path=path_search, *args))
|