summaryrefslogtreecommitdiff
path: root/tests/end2end/fixtures/webserver_sub.py
blob: b02c605d18ec127ed203ca1b4f26d40f25b6967a (plain)
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2015-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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.
#
# qutebrowser 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 qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

"""Web server for end2end tests.

This script gets called as a QProcess from end2end/conftest.py.

Some of the handlers here are inspired by the server project, but simplified
for qutebrowser's needs. Note that it probably doesn't handle e.g. multiple
parameters or headers with the same name properly.
"""

import sys
import json
import time
import threading
import pathlib
from http import HTTPStatus

import cheroot.wsgi
import flask

app = flask.Flask(__name__)
_redirect_later_event = None


END2END_DIR = pathlib.Path(__file__).resolve().parents[1]


@app.route('/')
def root():
    """Show simple text."""
    return flask.Response(b'qutebrowser test webserver, '
                          b'<a href="/user-agent">user agent</a>')


@app.route('/data/<path:path>')
@app.route('/data2/<path:path>')  # for per-URL settings
def send_data(path):
    """Send a given data file to qutebrowser.

    If a directory is requested, its index.html is sent.
    """
    data_dir = END2END_DIR / 'data'
    if (data_dir / path).is_dir():
        path += '/index.html'

    if path == 'service-worker/worker.js':
        # For some reason, Flask returns this with a text/plain mimetype on GitHub
        # Actions with Windows?!
        flask.send_file(path, mimetype='text/javascript')

    return flask.send_from_directory(data_dir, path)


@app.route('/redirect-later')
def redirect_later():
    """302 redirect to / after the given delay.

    If delay is -1, wait until a request on redirect-later-continue is done.
    """
    global _redirect_later_event
    delay = float(flask.request.args.get('delay', '1'))
    if delay == -1:
        _redirect_later_event = threading.Event()
        ok = _redirect_later_event.wait(timeout=30 * 1000)
        assert ok
        _redirect_later_event = None
    else:
        time.sleep(delay)
    x = flask.redirect('/')
    return x


@app.route('/redirect-later-continue')
def redirect_later_continue():
    """Continue a redirect-later request."""
    if _redirect_later_event is None:
        return flask.Response(b'Timed out or no redirect pending.')
    else:
        _redirect_later_event.set()
        return flask.Response(b'Continued redirect.')


@app.route('/redirect-self')
def redirect_self():
    """302 Redirects to itself."""
    return app.make_response(flask.redirect(flask.url_for('redirect_self')))


@app.route('/redirect/<int:n>')
def redirect_n_times(n):
    """302 Redirects n times."""
    assert n > 0
    return flask.redirect(flask.url_for('redirect_n_times', n=n-1))


@app.route('/relative-redirect')
def relative_redirect():
    """302 Redirect once."""
    response = app.make_response('')
    response.status_code = HTTPStatus.FOUND
    response.headers['Location'] = flask.url_for('root')
    return response


@app.route('/absolute-redirect')
def absolute_redirect():
    """302 Redirect once."""
    response = app.make_response('')
    response.status_code = HTTPStatus.FOUND
    response.headers['Location'] = flask.url_for('root', _external=True)
    return response


@app.route('/redirect-to')
def redirect_to():
    """302/3XX Redirects to the given URL."""
    # We need to build the response manually and convert to UTF-8 to prevent
    # werkzeug from "fixing" the URL. This endpoint should set the Location
    # header to the exact string supplied.
    response = app.make_response('')
    response.status_code = HTTPStatus.FOUND
    response.headers['Location'] = flask.request.args['url'].encode('utf-8')
    return response


@app.route('/content-size')
def content_size():
    """Send two bytes of data without a content-size."""
    def generate_bytes():
        yield b'*'
        time.sleep(0.2)
        yield b'*'

    response = flask.Response(generate_bytes(), headers={
        "Content-Type": "application/octet-stream",
    })
    response.status_code = HTTPStatus.OK
    return response


@app.route('/twenty-mb')
def twenty_mb():
    """Send 20MB of data."""
    def generate_bytes():
        yield b'*' * 20 * 1024 * 1024

    response = flask.Response(generate_bytes(), headers={
        "Content-Type": "application/octet-stream",
        "Content-Length": str(20 * 1024 * 1024),
    })
    response.status_code = HTTPStatus.OK
    return response


@app.route('/500-inline')
def internal_error_attachment():
    """A 500 error with Content-Disposition: inline."""
    response = flask.Response(b"", headers={
        "Content-Type": "application/octet-stream",
        "Content-Disposition": 'inline; filename="attachment.jpg"',
    })
    response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR
    return response


@app.route('/500')
def internal_error():
    """A normal 500 error."""
    r = flask.make_response()
    r.status_code = HTTPStatus.INTERNAL_SERVER_ERROR
    return r


@app.route('/cookies')
def view_cookies():
    """Show cookies."""
    return flask.jsonify(cookies=flask.request.cookies)


@app.route('/cookies/set')
def set_cookies():
    """Set cookie(s) as provided by the query string."""
    r = app.make_response(flask.redirect(flask.url_for('view_cookies')))
    for key, value in flask.request.args.items():
        r.set_cookie(key=key, value=value)
    return r


@app.route('/basic-auth/<user>/<passwd>')
def basic_auth(user='user', passwd='passwd'):
    """Prompt the user for authorization using HTTP Basic Auth."""
    auth = flask.request.authorization
    if not auth or auth.username != user or auth.password != passwd:
        r = flask.make_response()
        r.status_code = HTTPStatus.UNAUTHORIZED
        r.headers = {'WWW-Authenticate': 'Basic realm="Fake Realm"'}
        return r

    return flask.jsonify(authenticated=True, user=user)


@app.route('/drip')
def drip():
    """Drip data over a duration."""
    duration = float(flask.request.args.get('duration'))
    numbytes = int(flask.request.args.get('numbytes'))
    pause = duration / numbytes

    def generate_bytes():
        for _ in range(numbytes):
            yield "*".encode('utf-8')
            time.sleep(pause)

    response = flask.Response(generate_bytes(), headers={
        "Content-Type": "application/octet-stream",
        "Content-Length": str(numbytes),
    })
    response.status_code = HTTPStatus.OK
    return response


@app.route('/404')
def status_404():
    r = flask.make_response()
    r.status_code = HTTPStatus.NOT_FOUND
    return r


@app.route('/headers')
def view_headers():
    """Return HTTP headers."""
    return flask.jsonify(headers=dict(flask.request.headers))


@app.route('/headers-link/<int:port>')
def headers_link(port):
    """Get a (possibly cross-origin) link to /headers."""
    return flask.render_template('headers-link.html', port=port)


@app.route('/response-headers')
def response_headers():
    """Return a set of response headers from the query string."""
    headers = flask.request.args
    response = flask.jsonify(headers)
    response.headers.extend(headers)

    response = flask.jsonify(dict(response.headers))
    response.headers.extend(headers)
    return response


@app.route('/query')
def query():
    return flask.jsonify(flask.request.args)


@app.route('/user-agent')
def view_user_agent():
    """Return User-Agent."""
    return flask.jsonify({'user-agent': flask.request.headers['user-agent']})


@app.route('/favicon.ico')
def favicon():
    icon_dir = END2END_DIR.parents[1] / 'icons'
    return flask.send_from_directory(
        icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon')


@app.after_request
def log_request(response):
    """Log a webserver request."""
    request = flask.request
    data = {
        'verb': request.method,
        'path': request.full_path if request.query_string else request.path,
        'status': response.status_code,
    }
    print(json.dumps(data), file=sys.stderr, flush=True)
    return response


class WSGIServer(cheroot.wsgi.Server):

    """A custom WSGIServer that prints a line on stderr when it's ready.

    Attributes:
        _ready: Internal state for the 'ready' property.
        _printed_ready: Whether the initial ready message was printed.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._ready = False
        self._printed_ready = False

    @property
    def ready(self):
        return self._ready

    @ready.setter
    def ready(self, value):
        if value and not self._printed_ready:
            print(' * Running on http://127.0.0.1:{}/ (Press CTRL+C to quit)'
                  .format(self.bind_addr[1]), file=sys.stderr, flush=True)
            self._printed_ready = True
        self._ready = value


def main():
    app.template_folder = END2END_DIR / 'templates'
    assert app.template_folder.is_dir(), app.template_folder

    port = int(sys.argv[1])
    server = WSGIServer(('127.0.0.1', port), app)
    server.start()


if __name__ == '__main__':
    main()