Skip to content

Commit ae38457

Browse files
authored
Merge pull request #149 from cloudblue/LITE-26945-fix-redirect-location-when-trailing-slash
LITE-26945 fix redirect location based on proxy X-forwarded-.... headers
2 parents 34d6579 + 75715a7 commit ae38457

File tree

3 files changed

+184
-4
lines changed

3 files changed

+184
-4
lines changed

connect/eaas/runner/handlers/web.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ async def __call__(self, scope, receive, send):
4848
await super().__call__(scope, receive, send) # pragma: no cover
4949

5050

51+
class _ProxyHeadersMiddleware:
52+
def __init__(self, app):
53+
self.app = app
54+
55+
async def __call__(self, scope, receive, send):
56+
if scope['type'] == 'http': # pragma: no branch
57+
headers = dict(scope['headers'])
58+
if b'x-forwarded-host' in headers:
59+
headers[b'host'] = headers[b'x-forwarded-host']
60+
scope['headers'] = list(headers.items())
61+
if b'x-forwarded-proto' in headers:
62+
scope['scheme'] = headers[b'x-forwarded-proto'].decode('ascii')
63+
64+
await self.app(scope, receive, send)
65+
66+
5167
class WebApp(ApplicationHandlerBase):
5268
"""
5369
Handle the lifecycle of an extension.
@@ -137,6 +153,8 @@ def get_asgi_application(self):
137153
_OpenApiCORSMiddleware,
138154
allow_origins=['*'],
139155
)
156+
app.add_middleware(_ProxyHeadersMiddleware)
157+
140158
if hasattr(self.get_application(), 'get_middlewares'):
141159
self.setup_middlewares(app, self.get_application().get_middlewares() or [])
142160

tests/handlers/test_web.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from connect.eaas.runner.handlers.web import (
4040
WebApp,
4141
_OpenApiCORSMiddleware,
42+
_ProxyHeadersMiddleware,
4243
)
4344

4445

@@ -203,13 +204,15 @@ def get_middlewares(cls):
203204
assert mocked_fastapi.add_middleware.mock_calls[0].args[0] == _OpenApiCORSMiddleware
204205
assert mocked_fastapi.add_middleware.mock_calls[0].kwargs['allow_origins'] == ['*']
205206

206-
assert mocked_fastapi.add_middleware.mock_calls[1].args[0] == BaseHTTPMiddleware
207-
assert mocked_fastapi.add_middleware.mock_calls[1].kwargs['dispatch'] == middleware_fn
207+
assert mocked_fastapi.add_middleware.mock_calls[1].args[0] == _ProxyHeadersMiddleware
208208

209-
assert mocked_fastapi.add_middleware.mock_calls[2].args[0] == MiddlewareClass
209+
assert mocked_fastapi.add_middleware.mock_calls[2].args[0] == BaseHTTPMiddleware
210+
assert mocked_fastapi.add_middleware.mock_calls[2].kwargs['dispatch'] == middleware_fn
210211

211212
assert mocked_fastapi.add_middleware.mock_calls[3].args[0] == MiddlewareClass
212-
assert mocked_fastapi.add_middleware.mock_calls[3].kwargs['arg1'] == 'val1'
213+
214+
assert mocked_fastapi.add_middleware.mock_calls[4].args[0] == MiddlewareClass
215+
assert mocked_fastapi.add_middleware.mock_calls[4].kwargs['arg1'] == 'val1'
213216

214217
if static_root:
215218
mocked_static_files.assert_called_once_with(directory=static_root)

tests/workers/test_web.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,165 @@ def test_url(self):
289289
)
290290

291291

292+
@pytest.mark.parametrize(
293+
'task_options',
294+
(
295+
WebTaskOptions(
296+
correlation_id='correlation_id',
297+
reply_to='reply_to',
298+
api_key='api_key',
299+
installation_id='installation_id',
300+
),
301+
WebTaskOptions(
302+
correlation_id='correlation_id',
303+
reply_to='reply_to',
304+
api_key='api_key',
305+
installation_id='installation_id',
306+
connect_correlation_id='connect_correlation_id',
307+
user_id='user_id',
308+
account_id='account_id',
309+
account_role='account_role',
310+
call_type='user',
311+
call_source='ui',
312+
),
313+
),
314+
)
315+
@pytest.mark.asyncio
316+
async def test_http_call_redirect(mocker, ws_server, unused_port, settings_payload, task_options):
317+
setup_response = copy.deepcopy(settings_payload)
318+
setup_response['logging']['logging_api_key'] = 'logging_api_key'
319+
setup_response['logging']['log_level'] = None
320+
mocker.patch(
321+
'connect.eaas.runner.config.get_environment',
322+
return_value={
323+
'ws_address': f'127.0.0.1:{unused_port}',
324+
'api_address': f'127.0.0.1:{unused_port}',
325+
'api_key': 'SU-000:XXXX',
326+
'environment_id': 'ENV-000-0001',
327+
'instance_id': 'INS-000-0002',
328+
'background_task_max_execution_time': 300,
329+
'interactive_task_max_execution_time': 120,
330+
'scheduled_task_max_execution_time': 43200,
331+
'webapp_port': 53575,
332+
},
333+
)
334+
335+
ui_modules = {
336+
'settings': {
337+
'label': 'Settings',
338+
'url': '/static/settings.html',
339+
},
340+
}
341+
342+
@account_settings_page('Settings', '/static/settings.html')
343+
@web_app(router)
344+
class MyExtension(WebApplicationBase):
345+
@classmethod
346+
def get_descriptor(cls):
347+
return {
348+
'readme_url': 'https://read.me',
349+
'changelog_url': 'https://change.log',
350+
}
351+
352+
@classmethod
353+
def get_static_root(cls):
354+
return None
355+
356+
@router.get('/test/url')
357+
def test_url(self):
358+
return {'test': 'ok'}
359+
360+
mocker.patch.object(
361+
WebApp,
362+
'load_application',
363+
return_value=MyExtension,
364+
)
365+
366+
mocker.patch('connect.eaas.runner.workers.web.get_version', return_value='24.1')
367+
368+
web_task = WebTask(
369+
options=task_options,
370+
request=HttpRequest(
371+
method='GET',
372+
url='/api/test/url/',
373+
headers={
374+
'X-Forwarded-Host': 'my.proxy.host',
375+
'X-Forwarded-Proto': 'https',
376+
},
377+
),
378+
)
379+
380+
data_to_send = [
381+
Message(
382+
version=2,
383+
message_type=MessageType.SETUP_RESPONSE,
384+
data=SetupResponse(**setup_response),
385+
).dict(),
386+
Message(
387+
version=2,
388+
message_type=MessageType.WEB_TASK,
389+
data=web_task,
390+
).dict(),
391+
]
392+
393+
handler = WSHandler(
394+
'/public/v1/devops/ws/ENV-000-0001/INS-000-0002/webapp',
395+
data_to_send,
396+
['receive', 'send', 'send', 'receive'],
397+
)
398+
399+
config = ConfigHelper(secure=False)
400+
ext_handler = WebApp(config)
401+
402+
worker = None
403+
async with ws_server(handler):
404+
worker = WebWorker(ext_handler, mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock())
405+
task = asyncio.create_task(worker.start())
406+
await asyncio.sleep(.5)
407+
worker.stop()
408+
await task
409+
410+
handler.assert_received(
411+
Message(
412+
version=2,
413+
message_type=MessageType.SETUP_REQUEST,
414+
data=SetupRequest(
415+
event_subscriptions=None,
416+
ui_modules=ui_modules,
417+
variables=[],
418+
schedulables=None,
419+
repository={
420+
'readme_url': 'https://read.me',
421+
'changelog_url': 'https://change.log',
422+
},
423+
runner_version='24.1',
424+
),
425+
),
426+
)
427+
handler.assert_received(
428+
Message(
429+
version=2,
430+
message_type=MessageType.WEB_TASK,
431+
data=WebTask(
432+
options=task_options,
433+
request=HttpRequest(
434+
method='GET',
435+
url='/api/test/url/',
436+
headers={},
437+
),
438+
response=HttpResponse(
439+
status=307,
440+
headers={
441+
'content-length': '0',
442+
'location': 'https://my.proxy.host/api/test/url',
443+
},
444+
content='',
445+
),
446+
),
447+
),
448+
)
449+
450+
292451
@pytest.mark.asyncio
293452
async def test_http_call_exception(mocker, ws_server, unused_port, settings_payload):
294453
setup_response = copy.deepcopy(settings_payload)

0 commit comments

Comments
 (0)