Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ list(
src/server/handlers/handler_base.yaml
src/server/handlers/http_handler_base.yaml
src/server/handlers/http_handler_static.yaml
src/server/handlers/http_handler_static_stream.yaml
src/server/handlers/implicit_options.yaml
src/server/handlers/ping.yaml
src/server/handlers/server_monitor.yaml
Expand Down
3 changes: 3 additions & 0 deletions core/functional_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-metrics)
add_subdirectory(static_service)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-static-service)

add_subdirectory(static_streaming_service)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-static-streaming-service)

add_subdirectory(slow_start)
add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-slow-start)

Expand Down
7 changes: 7 additions & 0 deletions core/functional_tests/static_streaming_service/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
project(userver-core-tests-static-streaming-service CXX)

add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} userver::core)

userver_testsuite_add_simple(WORKING_DIRECTORY tests-serve-from-root)
userver_testsuite_add_simple(WORKING_DIRECTORY tests-serve-from-subpath)
12 changes: 12 additions & 0 deletions core/functional_tests/static_streaming_service/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include <userver/utest/using_namespace_userver.hpp>

#include <userver/components/minimal_server_component_list.hpp>
#include <userver/server/handlers/http_handler_static_stream.hpp>
#include <userver/utils/daemon_run.hpp>

int main(int argc, char* argv[]) {
const auto component_list =
components::MinimalServerComponentList()
.Append<server::handlers::HttpHandlerStaticStream>();
return utils::DaemonMain(argc, argv, component_list);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>userver</title>
</head>
<body>
File not found
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hidden file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file in recurse dir
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
index.html file in subdirs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>userver</title>
</head>
<body>
Welcome to userver
</body>
</html>
31 changes: 31 additions & 0 deletions core/functional_tests/static_streaming_service/static_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
components_manager:
task_processors: # Task processor is an executor for coroutine tasks

main-task-processor: # Make a task processor for CPU-bound coroutine tasks.
worker_threads: 4 # Process tasks in 4 threads.

fs-task-processor: # Make a separate task processor for filesystem bound tasks.
worker_threads: 4

default_task_processor: main-task-processor # Task processor in which components start.

components: # Configuring components that were registered via component_list
server:
listener: # configuring the main listening socket...
port: 8080 # ...to listen on this port and...
task_processor: main-task-processor # ...process incoming requests on this task processor.
logging:
fs-task-processor: fs-task-processor
loggers:
default:
file_path: '@stderr'
level: debug
overflow_behavior: discard # Drop logs if the system is too busy to write them down.

handler-static-stream: # Finally! Static streaming handler.
dir: /var/www
path: /* # Registering handlers '/*' find files.
method: GET # Handle only GET requests.
response-body-stream: true
task_processor: main-task-processor # Run it on CPU bound task processor
fs-task-processor: fs-task-processor # Run file IO on blocking task processor
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# [Static service sample - config hook]
import pathlib

import pytest

pytest_plugins = ['pytest_userver.plugins.core']

USERVER_CONFIG_HOOKS = ['static_config_hook']


@pytest.fixture(scope='session')
def static_config_hook(service_source_dir):
def _patch_config(config_yaml, config_vars):
components = config_yaml['components_manager']['components']
if 'handler-static-stream' in components:
components['handler-static-stream']['dir'] = str(
pathlib.Path(service_source_dir).joinpath('public'),
)

return _patch_config
# [Static service sample - config hook]
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import pytest


async def test_file_not_found(service_client):
response = await service_client.get('/file.not')
assert response.status == 404
assert b'File not found' in response.content


@pytest.mark.parametrize('path', ['/index.html', '/'])
async def test_file(service_client, service_source_dir, path):
response = await service_client.get(path)
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
file = service_source_dir.joinpath('public') / 'index.html'
assert response.content.decode() == file.open().read()


async def test_file_recursive(service_client, service_source_dir):
response = await service_client.get('/dir1/dir2/data.html')
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
assert response.content == b'file in recurse dir\n'
file = service_source_dir.joinpath('public') / 'dir1' / 'dir2' / 'data.html'
assert response.content.decode() == file.open().read()


@pytest.mark.parametrize('path', ['/dir1/dir2', '/dir1/dir2/'])
async def test_file_recursive_index(service_client, service_source_dir, path):
response = await service_client.get(path)
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
file = service_source_dir.joinpath('public') / 'dir1' / 'dir2' / 'index.html'
assert response.content.decode() == file.open().read()


async def test_hidden_file(service_client, service_source_dir):
response = await service_client.get('/dir1/.hidden_file.txt')
assert response.status == 200
file = service_source_dir.joinpath('public') / 'dir1' / '.hidden_file.txt'
assert response.content.decode() == file.open().read()


async def test_invalid_path(service_client):
response = await service_client.get('/../../../../../../../../../../../../../etc/passwd')
assert response.status == 404
assert b'File not found' in response.content
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pathlib

import pytest

pytest_plugins = ['pytest_userver.plugins.core']

USERVER_CONFIG_HOOKS = ['static_config_hook']


@pytest.fixture(scope='session')
def static_config_hook(service_source_dir):
def _patch_config(config_yaml, config_vars):
components = config_yaml['components_manager']['components']
if 'handler-static-stream' in components:
components['handler-static-stream']['dir'] = str(
pathlib.Path(service_source_dir).joinpath('public'),
)
components['handler-static-stream']['path'] = '/possible/to/work/from/subpath/*'

return _patch_config
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
import yarl


async def test_file_not_found(service_client):
response = await service_client.get('/possible/to/work/from/subpath/file.not')
assert response.status == 404
assert b'File not found' in response.content


@pytest.mark.parametrize(
'path',
[
'/possible/to/work/from/subpath/index.html',
],
)
async def test_file(service_client, service_source_dir, path):
response = await service_client.get(path)
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
file = service_source_dir.joinpath('public') / 'index.html'
assert response.content.decode() == file.open().read()


async def test_file_recursive(service_client, service_source_dir):
response = await service_client.get('/possible/to/work/from/subpath/dir1/dir2/data.html')
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
assert response.content == b'file in recurse dir\n'
file = service_source_dir.joinpath('public') / 'dir1' / 'dir2' / 'data.html'
assert response.content.decode() == file.open().read()


@pytest.mark.parametrize(
'path',
['/possible/to/work/from/subpath/dir1/dir2', '/possible/to/work/from/subpath/dir1/dir2/'],
)
async def test_file_recursive_index(service_client, service_source_dir, path):
response = await service_client.get(path)
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
file = service_source_dir.joinpath('public') / 'dir1' / 'dir2' / 'index.html'
assert response.content.decode() == file.open().read()


async def test_hidden_file(service_client, service_source_dir):
response = await service_client.get('/possible/to/work/from/subpath/dir1/.hidden_file.txt')
assert response.status == 200
file = service_source_dir.joinpath('public') / 'dir1' / '.hidden_file.txt'
assert response.content.decode() == file.open().read()


async def test_invalid_path(service_client, service_source_dir):
response = await service_client.get(
'/possible/to/work/from/subpath/dir1/../../../../../../../../../../../../../etc/passwd')
assert response.status == 404
# assert response.headers['Content-Type'] == 'text/html'
# assert b'File not found' in response.content #TODO: test issue, unable to pass 'encoded' path to client.get
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once

/// @file userver/server/handlers/http_handler_static.hpp
/// @brief @copybrief server::handlers::HttpHandlerStaticStream

#include <userver/dynamic_config/source.hpp>
#include <userver/server/handlers/http_handler_base.hpp>

USERVER_NAMESPACE_BEGIN

namespace server::handlers {

// clang-format off

/// @ingroup userver_components userver_http_handlers
///
/// @brief Streaming handler that returns HTTP 200 if file exist and returns file data with mapped content/type.
///
/// Path arguments of this handle are passed to `dir` property to get the file.
///
/// @code{.yaml}
/// handler-static-stream:
/// dir: /var/www # Path to the directory with files
/// @endcode
///
/// the `handler-static-stream` with `path: /files/*` on request to `/files/some/file.html`
/// would return file at path `/var/www/some/file.html`.
///
/// ## HttpHandlerStaticStream Dynamic config
/// * @ref USERVER_FILES_CONTENT_TYPE_MAP
///
/// \ref userver_http_handlers "Userver HTTP Handlers".
///
/// ## Static options:
/// Inherits all the options from server::handlers::HttpHandlerBase and adds the
/// following ones:
///
/// Name | Description | Default value
/// ------------------ | ----------------------------------------------------------------------------------------- | -------------
/// dir | Base directory path | /var/www
/// directory-file | File to return for directory requests. File name (not path) search in requested directory | "index.html"
/// not-found-file | File to return for missing files | "/404.html"
/// buffer-size | Single read buffer size in bytes | 8192
///

// clang-format on

class HttpHandlerStaticStream final : public HttpHandlerBase {
public:
/// @ingroup userver_component_names
/// @brief The default name of server::handlers::HttpHandlerStaticStream
static constexpr std::string_view kName = "handler-static-stream";

using HttpHandlerBase::HttpHandlerBase;

HttpHandlerStaticStream(const components::ComponentConfig& config, const components::ComponentContext& context);

static yaml_config::Schema GetStaticConfigSchema();

void HandleStreamRequest(http::HttpRequest&, request::RequestContext&, http::ResponseBodyStream&) const override;

private:
dynamic_config::Source config_;
const std::string base_dir_;
const std::size_t buffer_size_;
const std::string directory_file_;
const std::string not_found_file_;
engine::TaskProcessor& fs_task_processor_;
};

} // namespace server::handlers

USERVER_NAMESPACE_END
Loading