Line data Source code
1 : /**
2 : Copyright (c) 2024 Stappler LLC <admin@stappler.dev>
3 :
4 : Permission is hereby granted, free of charge, to any person obtaining a copy
5 : of this software and associated documentation files (the "Software"), to deal
6 : in the Software without restriction, including without limitation the rights
7 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 : copies of the Software, and to permit persons to whom the Software is
9 : furnished to do so, subject to the following conditions:
10 :
11 : The above copyright notice and this permission notice shall be included in
12 : all copies or substantial portions of the Software.
13 :
14 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 : THE SOFTWARE.
21 : **/
22 :
23 : #include "SPWebHostController.h"
24 : #include "SPWebHostComponent.h"
25 : #include "SPWebWasmComponent.h"
26 : #include "SPWebHost.h"
27 : #include "SPWebRoot.h"
28 : #include "SPWebDbd.h"
29 :
30 : #include "SPValid.h"
31 : #include "SPDbFieldExtensions.h"
32 : #include "SPJsonWebToken.h"
33 : #include "SPDso.h"
34 : #include "SPWebVirtualFile.h"
35 : #include "SPPugCache.h"
36 :
37 : namespace STAPPLER_VERSIONIZED stappler::web {
38 :
39 0 : HostController::~HostController() { }
40 :
41 25 : HostController::HostController(Root *root, pool_t *pool)
42 25 : : _root(root), _rootPool(pool)
43 : #if DEBUG
44 25 : , _pugCache(pug::Template::Options::getPretty(), [this] (const StringView &str) { handleTemplateError(str); })
45 : #else
46 : , _pugCache(pug::Template::Options::getDefault(), [this] (const StringView &str) { handleTemplateError(str); })
47 : #endif
48 : {
49 25 : _rootPool = memory::pool::acquire();
50 25 : auto d = VirtualFile::getList();
51 350 : for (auto &it : d) {
52 325 : if (it.name.ends_with(".pug") || it.name.ends_with(".spug")) {
53 200 : _pugCache.addTemplate(toString("virtual:/", it.name), it.content.str<Interface>());
54 : } else {
55 125 : _pugCache.addContent(toString("virtual:/", it.name), it.content.str<Interface>());
56 : }
57 : }
58 :
59 25 : memset(_hostSecret.data(), 0, _hostSecret.size());
60 25 : }
61 :
62 25 : db::Scheme HostController::makeUserScheme() const {
63 : return db::Scheme(config::USER_SCHEME_NAME, {
64 50 : db::Field::Text("name", db::Transform::Alias, db::Flags::Required),
65 50 : db::Field::Bytes("pubkey", db::Transform::PublicKey, db::Flags::Indexed),
66 50 : db::Field::Password("password", db::PasswordSalt(config::DEFAULT_PASSWORD_SALT), db::Flags::Required | db::Flags::Protected),
67 50 : db::Field::Boolean("isAdmin", Value(false)),
68 75 : db::Field::Extra("data", Vector<db::Field>({
69 50 : db::Field::Text("email", db::Transform::Email),
70 50 : db::Field::Text("public"),
71 50 : db::Field::Text("desc"),
72 : })),
73 50 : db::Field::Text("email", db::Transform::Email, db::Flags::Unique),
74 50 : });
75 : }
76 :
77 25 : db::Scheme HostController::makeFileScheme() const {
78 : return db::Scheme(config::FILE_SCHEME_NAME, {
79 50 : db::Field::Text("location", db::Transform::Url),
80 50 : db::Field::Text("type", db::Flags::ReadOnly),
81 50 : db::Field::Integer("size", db::Flags::ReadOnly),
82 50 : db::Field::Integer("mtime", db::Flags::AutoMTime | db::Flags::ReadOnly),
83 50 : db::Field::Extra("image", Vector<db::Field>{
84 50 : db::Field::Integer("width"),
85 50 : db::Field::Integer("height"),
86 : })
87 50 : });
88 : }
89 :
90 25 : db::Scheme HostController::makeErrorScheme() const {
91 : return db::Scheme(config::ERROR_SCHEME_NAME, {
92 50 : db::Field::Boolean("hidden", Value(false)),
93 50 : db::Field::Boolean("delivered", Value(false)),
94 50 : db::Field::Text("name"),
95 50 : db::Field::Text("documentRoot"),
96 50 : db::Field::Text("url"),
97 50 : db::Field::Text("request"),
98 50 : db::Field::Text("ip"),
99 50 : db::Field::Data("headers"),
100 50 : db::Field::Data("data"),
101 50 : db::Field::Integer("time"),
102 100 : db::Field::Custom(new db::FieldTextArray("tags", db::Flags::Indexed,
103 50 : db::DefaultFn([&] (const Value &data) -> Value {
104 50 : Vector<String> tags;
105 125 : for (auto &it : data.getArray("data")) {
106 75 : auto text = it.getString("source");
107 75 : if (!text.empty()) {
108 75 : emplace_ordered(tags, text);
109 : }
110 75 : }
111 :
112 50 : Value ret;
113 125 : for (auto &it : tags) {
114 75 : ret.addString(it);
115 : }
116 100 : return ret;
117 100 : })))
118 50 : });
119 : }
120 :
121 25 : bool HostController::loadComponent(const Host &serv, const HostComponentInfo &info) {
122 25 : switch (info.type) {
123 25 : case HostComponentType::Dso:
124 25 : return loadDsoComponent(serv, info);
125 : break;
126 0 : case HostComponentType::Wasm:
127 0 : return loadWasmComponent(serv, info);
128 : break;
129 : }
130 0 : return false;
131 : }
132 :
133 25 : void HostController::initComponents(const Host &serv, const Vector<HostComponentInfo> &val) {
134 50 : for (auto &it : val) {
135 25 : loadComponent(serv, it);
136 : }
137 25 : }
138 :
139 0 : void HostController::initSession(const Value &val) {
140 0 : _session.init(val);
141 0 : }
142 :
143 0 : void HostController::initWebhook(const Value &val) {
144 0 : _webhook.init(val);
145 0 : }
146 :
147 0 : void HostController::setSessionParam(StringView n, StringView v) {
148 0 : _session.setParam(n, v);
149 0 : }
150 0 : void HostController::setWebhookParam(StringView n, StringView v) {
151 0 : _webhook.setParam(n, v);
152 0 : }
153 :
154 0 : void HostController::setForceHttps() {
155 0 : _forceHttps = true;
156 0 : }
157 :
158 0 : void HostController::setHostSecret(StringView w) {
159 0 : if (w.starts_with("sha2:")) {
160 0 : w += "sha2:"_len;
161 0 : _hostSecret = crypto::Gost3411_512::hmac(w, w);
162 0 : } else if (w.starts_with("gost:")) {
163 0 : w += "gost:"_len;
164 0 : _hostSecret = crypto::Gost3411_512::hmac(w, w);
165 : } else {
166 0 : _hostSecret = crypto::Gost3411_512::hmac(w, w);
167 : }
168 0 : }
169 :
170 0 : void HostController::setHostKey(crypto::PrivateKey &&key) {
171 0 : _hostPrivKey = move(key);
172 0 : _hostPubKey = _hostPrivKey.exportPublic();
173 0 : }
174 :
175 0 : void HostController::addAllowed(StringView r) {
176 0 : auto p = valid::readIpRange(r);
177 0 : if (p.first && p.second) {
178 0 : _allowedIps.emplace_back(p.first, p.second);
179 : }
180 0 : }
181 :
182 25 : void HostController::init(const Host &serv) {
183 25 : _defaultUserScheme = makeUserScheme();
184 25 : _defaultFileScheme = makeFileScheme();
185 25 : _defaultErrorScheme = makeErrorScheme();
186 :
187 25 : _schemes.emplace(_defaultUserScheme.getName(), &_defaultUserScheme);
188 25 : _schemes.emplace(_defaultFileScheme.getName(), &_defaultFileScheme);
189 25 : _schemes.emplace(_defaultErrorScheme.getName(), &_defaultErrorScheme);
190 :
191 25 : if (!_componentsToLoad.empty()) {
192 25 : initComponents(serv, _componentsToLoad);
193 : }
194 25 : }
195 :
196 0 : bool HostController::initKeyPair(const Host &serv, const db::Adapter &a, BytesView fp) {
197 0 : if (_hostPrivKey.generate(crypto::KeyType::GOST3410_2012_512)) {
198 0 : _hostPubKey = _hostPrivKey.exportPublic();
199 : }
200 :
201 0 : Bytes priv;
202 0 : _hostPrivKey.exportPem([&] (BytesView data) {
203 0 : priv = data.bytes<Interface>();
204 0 : });
205 :
206 0 : crypto::PrivateKey tmpKey(config::INTERNAL_PRIVATE_KEY);
207 :
208 0 : auto tok = AesToken<Interface>::create(AesToken<Interface>::Keys{ nullptr, &tmpKey, BytesView(_hostSecret) });
209 0 : tok.setBytes(move(priv), "priv");
210 0 : if (auto d = tok.exportData(AesToken<Interface>::Fingerprint(crypto::HashFunction::GOST_3411, fp))) {
211 : std::array<uint8_t, string::Sha512::Length + 4> data;
212 0 : memcpy(data.data(), "srv:", 4);
213 0 : memcpy(data.data() + 4, fp.data(), fp.size());
214 :
215 0 : a.set(data, d, TimeInterval::seconds(60 * 60 * 365 * 100)); // 100 years
216 0 : return true;
217 0 : }
218 0 : return false;
219 0 : }
220 :
221 0 : void HostController::initHostKeys(const Host &serv, const db::Adapter &a) {
222 0 : if (_hostPrivKey) {
223 0 : return;
224 : }
225 :
226 0 : auto fp = crypto::Gost3411_512::hmac(_hostInfo.hostname, _hostSecret);
227 :
228 : std::array<uint8_t, string::Sha512::Length + 4> data;
229 0 : memcpy(data.data(), "srv:", 4);
230 0 : memcpy(data.data() + 4, fp.data(), fp.size());
231 :
232 0 : if (auto d = a.get(data)) {
233 0 : crypto::PrivateKey tmpKey(config::INTERNAL_PRIVATE_KEY);
234 0 : if (auto tok = AesToken<Interface>::parse(d,
235 0 : AesToken<Interface>::Fingerprint(crypto::HashFunction::GOST_3411, BytesView(fp)),
236 0 : AesToken<Interface>::Keys{ nullptr, &tmpKey, BytesView(_hostSecret) })) {
237 0 : auto &d = tok.getBytes("priv");
238 0 : _hostPrivKey.import(d);
239 0 : if (_hostPrivKey) {
240 0 : _hostPubKey = _hostPrivKey.exportPublic();
241 : }
242 0 : }
243 0 : }
244 :
245 0 : if (!_hostPubKey) {
246 0 : initKeyPair(serv, a, fp);
247 : }
248 : }
249 :
250 25 : void HostController::handleChildInit(const Host &host, pool_t *p) {
251 25 : _rootPool = p;
252 50 : for (auto &it : _components) {
253 25 : _currentComponent = it.second->getName();
254 25 : it.second->handleChildInit(host);
255 25 : _currentComponent = StringView();
256 : }
257 :
258 25 : _childInit = true;
259 :
260 25 : auto pool = getCurrentPool();
261 :
262 : db::sql::Driver::Handle db;
263 25 : if (!_dbParams.empty()) {
264 : // run custom dbd
265 25 : _customDbd = DbdModule::create(_rootPool, _root, move(_dbParams));
266 25 : _dbDriver = _customDbd->getDriver();
267 25 : db = _customDbd->openConnection(pool);
268 : } else {
269 : // setup apache httpd dbd
270 0 : db = _root->dbdOpen(pool, host);
271 0 : if (db.get()) {
272 0 : _dbDriver = openInternalDriver(db);
273 : } else {
274 0 : log::error("web::HostController", "fail to open internal DB connection");
275 : }
276 : }
277 :
278 25 : if (!_schemes.empty() && !db.get()) {
279 0 : _loadingFalled = true;
280 : }
281 :
282 25 : if (!_loadingFalled) {
283 25 : db::Scheme::initSchemes(_schemes);
284 :
285 25 : perform_temporary([&, this] {
286 25 : _dbDriver->performWithStorage(db, [&, this] (const db::Adapter &storage) {
287 50 : storage.init(db::BackendInterface::Config{StringView(_hostInfo.hostname), host.getFileScheme()}, _schemes);
288 :
289 25 : if (_hostSecret != string::Sha512::Buf{0}) {
290 0 : initHostKeys(host, storage);
291 : }
292 :
293 50 : for (auto &it : _components) {
294 25 : _currentComponent = it.second->getName();
295 25 : it.second->handleStorageInit(host, storage);
296 25 : _currentComponent = String();
297 : }
298 25 : });
299 25 : }, p);
300 : }
301 :
302 25 : if (db.get()) {
303 25 : if (_customDbd) {
304 25 : _customDbd->closeConnection(db);
305 : } else {
306 0 : _root->dbdClose(host, db);
307 : }
308 : }
309 25 : }
310 :
311 3425 : void HostController::initTransaction(db::Transaction &t) {
312 6850 : for (auto &it : _components) {
313 3425 : it.second->initTransaction(t);
314 : }
315 3425 : }
316 :
317 0 : void HostController::setDbParams(StringView str) {
318 0 : perform([&, this] {
319 0 : Root::parseParameterList(_dbParams, str);
320 0 : }, _rootPool);
321 0 : }
322 :
323 550 : db::sql::Driver::Handle HostController::openConnection(pool_t *pool, bool bindConnection) const {
324 550 : if (_customDbd) {
325 550 : auto h = _customDbd->openConnection(pool);
326 550 : if (bindConnection) {
327 0 : pool::cleanup_register(pool, [h, dbd = _customDbd] {
328 0 : dbd->closeConnection(h);
329 0 : });
330 : }
331 550 : return h;
332 : } else {
333 0 : auto h = _root->dbdOpen(pool, Host(const_cast<HostController *>(this)));
334 0 : if (bindConnection) {
335 0 : pool::cleanup_register(pool, [h, this] {
336 0 : _root->dbdClose(Host(const_cast<HostController *>(this)), h);
337 0 : });
338 : }
339 0 : return h;
340 : }
341 : }
342 :
343 550 : void HostController::closeConnection(db::sql::Driver::Handle h) const {
344 550 : if (_customDbd) {
345 550 : _customDbd->closeConnection(h);
346 : } else {
347 0 : _root->dbdClose(Host(const_cast<HostController *>(this)), h);
348 : }
349 550 : }
350 :
351 0 : db::sql::Driver *HostController::openInternalDriver(db::sql::Driver::Handle) {
352 0 : log::error("web::HostController", "VirtualServerConfig::openInternalDriver is not implemented");
353 0 : return nullptr;
354 : }
355 :
356 25 : bool HostController::loadDsoComponent(const Host &serv, const HostComponentInfo &info) {
357 25 : Dso h;
358 25 : if (info.file.empty()) {
359 25 : h = Dso(StringView(), DsoFlags::Self);
360 : } else {
361 0 : auto path = resolvePath(info.file);
362 0 : if (path.empty()) {
363 0 : return false;
364 : }
365 :
366 0 : h = Dso(StringView(path));
367 0 : }
368 :
369 25 : if (h) {
370 25 : auto sym = h.sym<HostComponent::Symbol>(info.symbol);
371 25 : if (sym) {
372 25 : auto comp = sym(serv, info);
373 25 : if (comp) {
374 25 : _components.emplace(comp->getName(), comp);
375 25 : _typedComponents.emplace(std::type_index(typeid(*comp)), comp);
376 25 : return true;
377 : } else {
378 0 : log::error("web::HostController", "Symbol ", info.symbol ," not found in DSO '", info.file, "'");
379 : }
380 : } else {
381 0 : log::error("web::HostController", "DSO: ", h.getError());
382 : }
383 : } else {
384 0 : log::error("web::HostController", "DSO: ", h.getError());
385 : }
386 0 : return false;
387 25 : }
388 :
389 0 : bool HostController::loadWasmComponent(const Host &serv, const HostComponentInfo &info) {
390 0 : auto module = loadWasmModule(info.name, info.file);
391 0 : if (!module) {
392 0 : return false;
393 : }
394 :
395 0 : auto comp = WasmComponent::load(serv, info, module);
396 0 : if (comp) {
397 0 : _components.emplace(comp->getName().str<Interface>(), comp);
398 0 : return true;
399 : } else {
400 0 : log::error("web::HostController", "Wasm: fail to load component: ", info.name, " from ", info.file);
401 : }
402 0 : return false;
403 : }
404 :
405 0 : wasm::Module *HostController::loadWasmModule(StringView name, StringView str) {
406 0 : auto path = resolvePath(str);
407 0 : if (path.empty()) {
408 0 : return nullptr;
409 : }
410 :
411 0 : auto it = _wasmModules.find(path);
412 0 : if (it != _wasmModules.end()) {
413 0 : return it->second;
414 : }
415 :
416 0 : auto mod = Rc<wasm::Module>::create(name, FilePath(path));
417 0 : if (mod) {
418 0 : _wasmModules.emplace(StringView(path).pdup(_wasmModules.get_allocator()), mod);
419 0 : return mod;
420 : }
421 0 : return nullptr;
422 0 : }
423 :
424 0 : String HostController::resolvePath(StringView path) const {
425 0 : for (auto &it : _sourceRoot) {
426 0 : auto str = filepath::merge<Interface>(it, path);
427 0 : if (str.front() == '/') {
428 0 : if (filesystem::exists(str)) {
429 0 : return str;
430 : }
431 : } else {
432 0 : str = filesystem::currentDir<Interface>(str);
433 0 : if (filesystem::exists(str)) {
434 0 : return str;
435 : }
436 : }
437 0 : }
438 :
439 0 : auto str = filepath::merge<Interface>(_hostInfo.documentRoot, path);
440 0 : if (filesystem::exists(str)) {
441 0 : return str;
442 : }
443 0 : return String();
444 0 : }
445 :
446 0 : void HostController::handleTemplateError(const StringView &str) {
447 : #if DEBUG
448 0 : std::cout << "[Template]: " << str << "\n";
449 : #endif
450 0 : log::error("Template", "Template compilation error", Value(str));
451 0 : }
452 :
453 : }
|