-
-
Notifications
You must be signed in to change notification settings - Fork 191
Expand file tree
/
Copy pathper_route_auth.cpp
More file actions
161 lines (142 loc) · 6.32 KB
/
Copy pathper_route_auth.cpp
File metadata and controls
161 lines (142 loc) · 6.32 KB
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
/*
This file is part of libhttpserver
Copyright (C) 2011-2026 Sebastiano Merlino
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; either
version 2.1 of the License, or (at your option) any later version.
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
*/
// Demonstrates DR-012 / PRD-HOOK-REQ-006: a `before_handler` hook
// registered via `http_resource::add_hook(...)` is per-route. The hook
// fires only when *this* resource is dispatched -- a sibling route
// registered on the same webserver is not touched.
//
// The example wires two resources:
//
// /public -- always 200 OK, no auth.
// /private -- a per-route before_handler hook checks the Authorization
// header (HTTP Basic). Missing or invalid credentials
// short-circuit with 401; valid credentials let the
// handler run and return 200 OK.
//
// SECURITY NOTE (CWE-798): the expected credentials are loaded from the
// environment (AUTH_USER / AUTH_PASS); do not hardcode them. Set both
// before running:
//
// export AUTH_USER=alice AUTH_PASS=hunter2
// ./per_route_auth
//
// TRANSPORT NOTE: HTTP Basic auth sends credentials base64-encoded but
// NOT encrypted. In production, serve only over TLS (use
// create_webserver().use_ssl(...)).
//
// CONSTANT-TIME NOTE: The credential comparison below uses
// `constant_time_equal` (defined further down), which is the
// production-ready form: it runs in time independent of the secret
// bytes, closing the CWE-208 timing side-channel that a naive
// std::string_view::operator== (short-circuiting on the first differing
// byte) would open.
//
// Then:
//
// curl -v http://localhost:8080/public # 200 OK
// curl -v http://localhost:8080/private # 401
// curl -v -u alice:hunter2 http://localhost:8080/private # 200 OK
//
// Contrast with `centralized_authentication.cpp`, which installs the
// server-wide `auth_handler` setter (the v1 alias for a webserver-wide
// before_handler hook). Per-route `add_hook` is the right shape when
// only a subset of routes need protection without dragging the rest of
// the surface through an auth_skip_paths list.
#include <cstddef>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <httpserver.hpp>
namespace hs = httpserver;
namespace {
// Constant-time equality: the runtime depends only on a.size(), never on
// where a mismatch occurs, closing the CWE-208 timing side-channel. The
// length is not the secret here, so an early length check is fine (this
// mirrors OpenSSL's CRYPTO_memcmp, which also requires equal lengths).
bool constant_time_equal(std::string_view a, std::string_view b) {
if (a.size() != b.size()) return false;
volatile unsigned char diff = 0;
for (std::size_t i = 0; i < a.size(); ++i) {
diff |= static_cast<unsigned char>(a[i] ^ b[i]);
}
return diff == 0;
}
class hello_resource : public hs::http_resource {
public:
hs::http_response render_get(const hs::http_request&) override {
return hs::http_response::string("Hello, World!");
}
};
class private_resource : public hs::http_resource {
public:
hs::http_response render_get(const hs::http_request&) override {
return hs::http_response::string("Welcome to the private area.");
}
};
} // namespace
int main() {
const char* expected_user = std::getenv("AUTH_USER");
const char* expected_pass = std::getenv("AUTH_PASS");
if (expected_user == nullptr || expected_pass == nullptr) {
std::cerr << "per_route_auth: set AUTH_USER and AUTH_PASS before "
"running.\n";
return 1;
}
hs::webserver ws{hs::create_webserver(8080)};
auto pub = std::make_shared<hello_resource>();
auto priv = std::make_shared<private_resource>();
// The hook is registered on `priv` ONLY. Dispatches against `pub`
// never enter this callback -- a sibling-route observation that is
// the load-bearing property of per-route hooks (DR-012).
std::string user{expected_user};
std::string pass{expected_pass};
auto h = priv->add_hook(hs::hook_phase::before_handler,
std::function<hs::hook_action(hs::before_handler_ctx&)>(
[user, pass](hs::before_handler_ctx& ctx) {
// Null should never occur at before_handler on a matched
// route, but fail closed (deny) rather than pass on any
// unexpected null to avoid a fail-open security hole
// (CWE-636).
if (ctx.request == nullptr) {
return hs::hook_action::respond_with(
hs::http_response::unauthorized(
"Basic", "private-realm", "Unauthorized"));
}
std::string_view u = ctx.request->get_user();
std::string_view p = ctx.request->get_pass();
// The && short-circuits at the *field* level (user then
// pass), which does not leak per-byte password timing --
// each field compare is itself constant-time.
if (constant_time_equal(u, user) &&
constant_time_equal(p, pass)) {
return hs::hook_action::pass();
}
return hs::hook_action::respond_with(
hs::http_response::unauthorized(
"Basic", "private-realm", "Unauthorized"));
}));
// Keep the handle in scope for the full server lifetime; its
// destructor at scope exit removes the registration. Since main()
// never returns (start(true) blocks), this is the simple shape.
ws.register_path("/public", pub);
ws.register_path("/private", priv);
ws.start(true); // blocking
return 0;
}