@mwguerra/hull 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/assets/hull-logo.png +0 -0
- package/assets/hull-logo.svg +5 -0
- package/bin/hull.js +4 -0
- package/devtools/dist/index.html +29 -0
- package/host/CMakeLists.txt +101 -0
- package/host/README.md +94 -0
- package/host/linux.Dockerfile +26 -0
- package/host/src/bindings/credentials.hpp +35 -0
- package/host/src/bindings/database.hpp +51 -0
- package/host/src/bindings/files.hpp +58 -0
- package/host/src/bindings/http.hpp +84 -0
- package/host/src/bindings/printer.hpp +281 -0
- package/host/src/bindings/storage.hpp +71 -0
- package/host/src/db_core.hpp +198 -0
- package/host/src/dispatcher.hpp +81 -0
- package/host/src/file_store.hpp +91 -0
- package/host/src/keychain.hpp +157 -0
- package/host/src/main.cpp +386 -0
- package/host/src/paths.hpp +62 -0
- package/host/src/secure.hpp +124 -0
- package/host/src/serve.hpp +113 -0
- package/host/test/db_test.cpp +80 -0
- package/host/test/secure_files_test.cpp +68 -0
- package/host/third_party/sqlite/sqlite3.c +269376 -0
- package/host/third_party/sqlite/sqlite3.h +14347 -0
- package/package.json +58 -0
- package/src/bridge/bridge-core.js +92 -0
- package/src/bridge/index.js +139 -0
- package/src/bridge/native-store.js +34 -0
- package/src/cli/build.js +122 -0
- package/src/cli/config.js +102 -0
- package/src/cli/dev.js +158 -0
- package/src/cli/eject.js +39 -0
- package/src/cli/host.js +61 -0
- package/src/cli/index.js +54 -0
- package/src/cli/installer.js +265 -0
- package/src/cli/release.js +178 -0
- package/src/cli/start.js +45 -0
- package/src/cli/timing.js +22 -0
- package/src/cli/vite.js +16 -0
- package/src/react/index.js +30 -0
- package/src/vue/index.js +31 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// IMPORTANT: httplib before any webview/GTK/X11 headers (Linux macro clash).
|
|
3
|
+
#include <httplib.h>
|
|
4
|
+
|
|
5
|
+
#include <thread>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <utility>
|
|
8
|
+
#include <nlohmann/json.hpp>
|
|
9
|
+
#include "dispatcher.hpp"
|
|
10
|
+
#include "keychain.hpp"
|
|
11
|
+
|
|
12
|
+
using json = nlohmann::json;
|
|
13
|
+
|
|
14
|
+
// Split a full URL into base ("https://host:port") and path ("/a/b?x=1").
|
|
15
|
+
inline std::pair<std::string, std::string> split_url(const std::string& url) {
|
|
16
|
+
auto scheme = url.find("://");
|
|
17
|
+
auto slash = url.find('/', scheme == std::string::npos ? 0 : scheme + 3);
|
|
18
|
+
if (slash == std::string::npos) return {url, "/"};
|
|
19
|
+
return {url.substr(0, slash), url.substr(slash)};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
inline std::string host_of(const std::string& base) {
|
|
23
|
+
auto scheme = base.find("://");
|
|
24
|
+
std::string rest = scheme == std::string::npos ? base : base.substr(scheme + 3);
|
|
25
|
+
auto colon = rest.find(':');
|
|
26
|
+
return colon == std::string::npos ? rest : rest.substr(0, colon);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
inline void register_http_bindings(Dispatcher& d) {
|
|
30
|
+
// httpPost(url, body) -> { ok, status, body }
|
|
31
|
+
d.on("httpPost", [](const json& a, Reply reply) {
|
|
32
|
+
std::thread([a, reply]() {
|
|
33
|
+
json out;
|
|
34
|
+
try {
|
|
35
|
+
const std::string url = a.at(0).get<std::string>();
|
|
36
|
+
const json payload = a.at(1);
|
|
37
|
+
auto [base, path] = split_url(url);
|
|
38
|
+
httplib::Client cli(base);
|
|
39
|
+
cli.set_connection_timeout(5);
|
|
40
|
+
cli.set_read_timeout(15);
|
|
41
|
+
cli.enable_server_certificate_verification(true);
|
|
42
|
+
httplib::Headers headers = {{"Accept", "application/json"}};
|
|
43
|
+
if (auto token = secrets::load(host_of(base), "default"))
|
|
44
|
+
headers.emplace("Authorization", "Bearer " + *token);
|
|
45
|
+
auto res = cli.Post(path.c_str(), headers, payload.dump(), "application/json");
|
|
46
|
+
if (!res) {
|
|
47
|
+
out = {{"ok", false}, {"error", httplib::to_string(res.error())}};
|
|
48
|
+
} else {
|
|
49
|
+
json body;
|
|
50
|
+
try { body = json::parse(res->body); } catch (...) { body = res->body; }
|
|
51
|
+
out = {{"ok", res->status >= 200 && res->status < 300}, {"status", res->status}, {"body", body}};
|
|
52
|
+
}
|
|
53
|
+
} catch (const std::exception& e) { out = {{"ok", false}, {"error", e.what()}}; }
|
|
54
|
+
reply(out);
|
|
55
|
+
}).detach();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// httpGet(url) -> { ok, status, body }
|
|
59
|
+
d.on("httpGet", [](const json& a, Reply reply) {
|
|
60
|
+
std::thread([a, reply]() {
|
|
61
|
+
json out;
|
|
62
|
+
try {
|
|
63
|
+
const std::string url = a.at(0).get<std::string>();
|
|
64
|
+
auto [base, path] = split_url(url);
|
|
65
|
+
httplib::Client cli(base);
|
|
66
|
+
cli.set_connection_timeout(5);
|
|
67
|
+
cli.set_read_timeout(15);
|
|
68
|
+
cli.enable_server_certificate_verification(true);
|
|
69
|
+
httplib::Headers headers = {{"Accept", "application/json"}};
|
|
70
|
+
if (auto token = secrets::load(host_of(base), "default"))
|
|
71
|
+
headers.emplace("Authorization", "Bearer " + *token);
|
|
72
|
+
auto res = cli.Get(path.c_str(), headers);
|
|
73
|
+
if (!res) {
|
|
74
|
+
out = {{"ok", false}, {"error", httplib::to_string(res.error())}};
|
|
75
|
+
} else {
|
|
76
|
+
json body;
|
|
77
|
+
try { body = json::parse(res->body); } catch (...) { body = res->body; }
|
|
78
|
+
out = {{"ok", res->status >= 200 && res->status < 300}, {"status", res->status}, {"body", body}};
|
|
79
|
+
}
|
|
80
|
+
} catch (const std::exception& e) { out = {{"ok", false}, {"error", e.what()}}; }
|
|
81
|
+
reply(out);
|
|
82
|
+
}).detach();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <string>
|
|
3
|
+
#include <vector>
|
|
4
|
+
#include <thread>
|
|
5
|
+
#include <nlohmann/json.hpp>
|
|
6
|
+
#include "dispatcher.hpp"
|
|
7
|
+
|
|
8
|
+
using json = nlohmann::json;
|
|
9
|
+
|
|
10
|
+
namespace printing {
|
|
11
|
+
|
|
12
|
+
struct Printer { std::string name; bool is_default = false; };
|
|
13
|
+
|
|
14
|
+
std::vector<Printer> list();
|
|
15
|
+
bool print_raw(const std::string& printer, const std::string& job_name,
|
|
16
|
+
const std::string& bytes);
|
|
17
|
+
// Render plain text as a normal print job (GDI on Windows; text/plain via CUPS on
|
|
18
|
+
// macOS/Linux) so it works with ANY printer — Microsoft Print to PDF, OneNote, and
|
|
19
|
+
// physical laser printers — not just ESC/POS thermal printers. Single page, word-wrapped.
|
|
20
|
+
bool print_text(const std::string& printer, const std::string& job_name,
|
|
21
|
+
const std::string& text);
|
|
22
|
+
|
|
23
|
+
// Build a minimal ESC/POS receipt: init, text lines, feed, full cut.
|
|
24
|
+
inline std::string escpos_message(const std::string& text) {
|
|
25
|
+
std::string out;
|
|
26
|
+
out += "\x1B\x40"; // ESC @ -> initialize
|
|
27
|
+
out += text;
|
|
28
|
+
out += "\n\n\n\n"; // feed
|
|
29
|
+
out += "\x1D\x56\x00"; // GS V 0 -> full cut
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ----------------------------- Windows -----------------------------
|
|
34
|
+
#if defined(_WIN32)
|
|
35
|
+
} // namespace printing
|
|
36
|
+
#include <windows.h>
|
|
37
|
+
#include <winspool.h>
|
|
38
|
+
namespace printing {
|
|
39
|
+
|
|
40
|
+
inline std::wstring utf8_to_wide(const std::string& s) {
|
|
41
|
+
if (s.empty()) return std::wstring();
|
|
42
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
|
|
43
|
+
std::wstring w(n, L'\0');
|
|
44
|
+
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
|
|
45
|
+
return w;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
inline std::vector<Printer> list() {
|
|
49
|
+
std::vector<Printer> result;
|
|
50
|
+
DWORD needed = 0, returned = 0;
|
|
51
|
+
EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, nullptr, 4,
|
|
52
|
+
nullptr, 0, &needed, &returned);
|
|
53
|
+
if (needed == 0) return result;
|
|
54
|
+
std::vector<BYTE> buf(needed);
|
|
55
|
+
if (EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, nullptr, 4,
|
|
56
|
+
buf.data(), needed, &needed, &returned)) {
|
|
57
|
+
auto* info = reinterpret_cast<PRINTER_INFO_4W*>(buf.data());
|
|
58
|
+
for (DWORD i = 0; i < returned; ++i) {
|
|
59
|
+
std::wstring w(info[i].pPrinterName);
|
|
60
|
+
result.push_back({std::string(w.begin(), w.end()), false});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Mark the default printer.
|
|
64
|
+
wchar_t def[256]; DWORD n = 256;
|
|
65
|
+
if (GetDefaultPrinterW(def, &n)) {
|
|
66
|
+
std::wstring w(def); std::string d(w.begin(), w.end());
|
|
67
|
+
for (auto& p : result) if (p.name == d) p.is_default = true;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
inline bool print_raw(const std::string& printer, const std::string& job_name,
|
|
73
|
+
const std::string& bytes) {
|
|
74
|
+
std::wstring wprinter = utf8_to_wide(printer);
|
|
75
|
+
HANDLE h = nullptr;
|
|
76
|
+
if (!OpenPrinterW(const_cast<LPWSTR>(wprinter.c_str()), &h, nullptr)) return false;
|
|
77
|
+
|
|
78
|
+
std::wstring wjob = utf8_to_wide(job_name);
|
|
79
|
+
DOC_INFO_1W di{};
|
|
80
|
+
di.pDocName = const_cast<LPWSTR>(wjob.c_str());
|
|
81
|
+
di.pDatatype = const_cast<LPWSTR>(L"RAW"); // send bytes verbatim (ESC/POS or plain text)
|
|
82
|
+
|
|
83
|
+
bool ok = false;
|
|
84
|
+
if (StartDocPrinterW(h, 1, reinterpret_cast<LPBYTE>(&di))) {
|
|
85
|
+
if (StartPagePrinter(h)) {
|
|
86
|
+
DWORD written = 0;
|
|
87
|
+
ok = WritePrinter(h, const_cast<char*>(bytes.data()),
|
|
88
|
+
(DWORD)bytes.size(), &written) && written == bytes.size();
|
|
89
|
+
EndPagePrinter(h);
|
|
90
|
+
}
|
|
91
|
+
EndDocPrinter(h);
|
|
92
|
+
}
|
|
93
|
+
ClosePrinter(h);
|
|
94
|
+
return ok;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
inline bool print_text(const std::string& printer, const std::string& job_name,
|
|
98
|
+
const std::string& text) {
|
|
99
|
+
const std::wstring wprinter = utf8_to_wide(printer);
|
|
100
|
+
const std::wstring wjob = utf8_to_wide(job_name);
|
|
101
|
+
const std::wstring wtext = utf8_to_wide(text);
|
|
102
|
+
// GDI device context for the printer driver -> a real, rendered job (PDF/OneNote/laser).
|
|
103
|
+
HDC dc = CreateDCW(L"WINSPOOL", wprinter.c_str(), nullptr, nullptr);
|
|
104
|
+
if (!dc) return false;
|
|
105
|
+
bool ok = false;
|
|
106
|
+
DOCINFOW di{}; di.cbSize = sizeof(di); di.lpszDocName = wjob.c_str();
|
|
107
|
+
if (StartDocW(dc, &di) > 0) {
|
|
108
|
+
if (StartPage(dc) > 0) {
|
|
109
|
+
const int dpiX = GetDeviceCaps(dc, LOGPIXELSX);
|
|
110
|
+
const int dpiY = GetDeviceCaps(dc, LOGPIXELSY);
|
|
111
|
+
HFONT font = CreateFontW(-MulDiv(11, dpiY, 72), 0, 0, 0, FW_NORMAL, FALSE, FALSE,
|
|
112
|
+
FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
|
|
113
|
+
CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
|
|
114
|
+
DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
|
|
115
|
+
HGDIOBJ oldFont = font ? SelectObject(dc, font) : nullptr;
|
|
116
|
+
RECT r{ dpiX / 2, dpiY / 2, // ~0.5" margins
|
|
117
|
+
GetDeviceCaps(dc, HORZRES) - dpiX / 2,
|
|
118
|
+
GetDeviceCaps(dc, VERTRES) - dpiY / 2 };
|
|
119
|
+
DrawTextW(dc, wtext.c_str(), -1, &r,
|
|
120
|
+
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX | DT_EXPANDTABS);
|
|
121
|
+
if (oldFont) SelectObject(dc, oldFont);
|
|
122
|
+
if (font) DeleteObject(font);
|
|
123
|
+
ok = (EndPage(dc) > 0);
|
|
124
|
+
}
|
|
125
|
+
EndDoc(dc);
|
|
126
|
+
}
|
|
127
|
+
DeleteDC(dc);
|
|
128
|
+
return ok;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ----------------------------- macOS / Linux (CUPS) -----------------------------
|
|
132
|
+
#else
|
|
133
|
+
} // namespace printing
|
|
134
|
+
#include <cups/cups.h>
|
|
135
|
+
namespace printing {
|
|
136
|
+
|
|
137
|
+
inline std::vector<Printer> list() {
|
|
138
|
+
std::vector<Printer> result;
|
|
139
|
+
cups_dest_t* dests = nullptr;
|
|
140
|
+
int n = cupsGetDests(&dests);
|
|
141
|
+
for (int i = 0; i < n; ++i) {
|
|
142
|
+
result.push_back({dests[i].name ? dests[i].name : "",
|
|
143
|
+
dests[i].is_default != 0});
|
|
144
|
+
}
|
|
145
|
+
cupsFreeDests(n, dests);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
inline bool print_raw(const std::string& printer, const std::string& job_name,
|
|
150
|
+
const std::string& bytes) {
|
|
151
|
+
int job = cupsCreateJob(CUPS_HTTP_DEFAULT, printer.c_str(), job_name.c_str(),
|
|
152
|
+
0, nullptr);
|
|
153
|
+
if (job == 0) return false;
|
|
154
|
+
if (cupsStartDocument(CUPS_HTTP_DEFAULT, printer.c_str(), job, job_name.c_str(),
|
|
155
|
+
CUPS_FORMAT_RAW, 1) != HTTP_STATUS_CONTINUE)
|
|
156
|
+
return false;
|
|
157
|
+
cupsWriteRequestData(CUPS_HTTP_DEFAULT, bytes.data(), bytes.size());
|
|
158
|
+
return cupsFinishDocument(CUPS_HTTP_DEFAULT, printer.c_str()) == IPP_STATUS_OK;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
inline bool print_text(const std::string& printer, const std::string& job_name,
|
|
162
|
+
const std::string& text) {
|
|
163
|
+
int job = cupsCreateJob(CUPS_HTTP_DEFAULT, printer.c_str(), job_name.c_str(), 0, nullptr);
|
|
164
|
+
if (job == 0) return false;
|
|
165
|
+
// CUPS_FORMAT_TEXT ("text/plain") -> CUPS renders via its text filter (any printer).
|
|
166
|
+
if (cupsStartDocument(CUPS_HTTP_DEFAULT, printer.c_str(), job, job_name.c_str(),
|
|
167
|
+
CUPS_FORMAT_TEXT, 1) != HTTP_STATUS_CONTINUE)
|
|
168
|
+
return false;
|
|
169
|
+
cupsWriteRequestData(CUPS_HTTP_DEFAULT, text.data(), text.size());
|
|
170
|
+
return cupsFinishDocument(CUPS_HTTP_DEFAULT, printer.c_str()) == IPP_STATUS_OK;
|
|
171
|
+
}
|
|
172
|
+
#endif
|
|
173
|
+
|
|
174
|
+
} // namespace printing
|
|
175
|
+
|
|
176
|
+
// ---- Portable raw-TCP sender (e.g. ESC/POS to a network printer on port 9100) ----
|
|
177
|
+
#if defined(_WIN32)
|
|
178
|
+
#include <winsock2.h>
|
|
179
|
+
#include <ws2tcpip.h>
|
|
180
|
+
#else
|
|
181
|
+
#include <sys/socket.h>
|
|
182
|
+
#include <netdb.h>
|
|
183
|
+
#include <unistd.h>
|
|
184
|
+
#endif
|
|
185
|
+
|
|
186
|
+
inline bool print_to_socket(const std::string& host, int port, const std::string& bytes) {
|
|
187
|
+
#if defined(_WIN32)
|
|
188
|
+
WSADATA wsa;
|
|
189
|
+
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
|
|
190
|
+
#endif
|
|
191
|
+
bool ok = false;
|
|
192
|
+
addrinfo hints{};
|
|
193
|
+
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
|
|
194
|
+
hints.ai_socktype = SOCK_STREAM;
|
|
195
|
+
addrinfo* res = nullptr;
|
|
196
|
+
if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res) == 0) {
|
|
197
|
+
for (addrinfo* p = res; p; p = p->ai_next) {
|
|
198
|
+
#if defined(_WIN32)
|
|
199
|
+
SOCKET fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
|
200
|
+
if (fd == INVALID_SOCKET) continue;
|
|
201
|
+
#else
|
|
202
|
+
int fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
|
203
|
+
if (fd < 0) continue;
|
|
204
|
+
#endif
|
|
205
|
+
if (connect(fd, p->ai_addr, (int)p->ai_addrlen) == 0) {
|
|
206
|
+
ok = true;
|
|
207
|
+
size_t sent = 0;
|
|
208
|
+
while (sent < bytes.size()) {
|
|
209
|
+
int n = (int)send(fd, bytes.data() + sent, (int)(bytes.size() - sent), 0);
|
|
210
|
+
if (n <= 0) { ok = false; break; }
|
|
211
|
+
sent += (size_t)n;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
#if defined(_WIN32)
|
|
215
|
+
closesocket(fd);
|
|
216
|
+
#else
|
|
217
|
+
::close(fd);
|
|
218
|
+
#endif
|
|
219
|
+
if (ok) break;
|
|
220
|
+
}
|
|
221
|
+
freeaddrinfo(res);
|
|
222
|
+
}
|
|
223
|
+
#if defined(_WIN32)
|
|
224
|
+
WSACleanup();
|
|
225
|
+
#endif
|
|
226
|
+
return ok;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- Bindings ----
|
|
230
|
+
inline void register_printer_bindings(Dispatcher& d) {
|
|
231
|
+
// listPrinters() -> { ok, printers: [{name, isDefault}] }
|
|
232
|
+
d.on("listPrinters", [](const json&, Reply reply) {
|
|
233
|
+
json arr = json::array();
|
|
234
|
+
for (const auto& p : printing::list())
|
|
235
|
+
arr.push_back({{"name", p.name}, {"isDefault", p.is_default}});
|
|
236
|
+
reply(json{{"ok", true}, {"printers", arr}});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// printMessage(printer, text) -> { ok }
|
|
240
|
+
// Prints `text` as a normal text document, so it works with ANY printer (Microsoft
|
|
241
|
+
// Print to PDF, OneNote, physical laser printers). For ESC/POS thermal receipt
|
|
242
|
+
// printers use printReceipt / printNetwork instead.
|
|
243
|
+
d.on("printMessage", [](const json& a, Reply reply) {
|
|
244
|
+
std::thread([a, reply]() {
|
|
245
|
+
json out;
|
|
246
|
+
try {
|
|
247
|
+
bool ok = printing::print_text(a.at(0).get<std::string>(), "App message",
|
|
248
|
+
a.at(1).get<std::string>());
|
|
249
|
+
out = {{"ok", ok}};
|
|
250
|
+
} catch (const std::exception& e) { out = {{"ok", false}, {"error", e.what()}}; }
|
|
251
|
+
reply(out);
|
|
252
|
+
}).detach();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// printReceipt(printer, text) -> { ok }
|
|
256
|
+
// Raw ESC/POS to a local (spooler) thermal/receipt printer: init, text, feed, cut.
|
|
257
|
+
d.on("printReceipt", [](const json& a, Reply reply) {
|
|
258
|
+
std::thread([a, reply]() {
|
|
259
|
+
json out;
|
|
260
|
+
try {
|
|
261
|
+
bool ok = printing::print_raw(a.at(0).get<std::string>(), "App receipt",
|
|
262
|
+
printing::escpos_message(a.at(1).get<std::string>()));
|
|
263
|
+
out = {{"ok", ok}};
|
|
264
|
+
} catch (const std::exception& e) { out = {{"ok", false}, {"error", e.what()}}; }
|
|
265
|
+
reply(out);
|
|
266
|
+
}).detach();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// printNetwork(host, port, text) -> { ok }
|
|
270
|
+
d.on("printNetwork", [](const json& a, Reply reply) {
|
|
271
|
+
std::thread([a, reply]() {
|
|
272
|
+
json out;
|
|
273
|
+
try {
|
|
274
|
+
bool ok = print_to_socket(a.at(0).get<std::string>(), a.at(1).get<int>(),
|
|
275
|
+
printing::escpos_message(a.at(2).get<std::string>()));
|
|
276
|
+
out = {{"ok", ok}};
|
|
277
|
+
} catch (const std::exception& e) { out = {{"ok", false}, {"error", e.what()}}; }
|
|
278
|
+
reply(out);
|
|
279
|
+
}).detach();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <string>
|
|
3
|
+
#include <fstream>
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <optional>
|
|
6
|
+
#include <filesystem>
|
|
7
|
+
#include <nlohmann/json.hpp>
|
|
8
|
+
#include "dispatcher.hpp"
|
|
9
|
+
#include "paths.hpp" // storage::app_data_dir / lock_down
|
|
10
|
+
#include "secure.hpp" // at-rest crypto layer (NullCipher by default)
|
|
11
|
+
|
|
12
|
+
namespace fs = std::filesystem;
|
|
13
|
+
using json = nlohmann::json;
|
|
14
|
+
|
|
15
|
+
namespace storage {
|
|
16
|
+
|
|
17
|
+
// ---- Key/value settings store (passed through the secure layer at rest) ----
|
|
18
|
+
inline fs::path settings_path() { return app_data_dir() / "settings.dat"; }
|
|
19
|
+
|
|
20
|
+
inline json read_settings() {
|
|
21
|
+
std::ifstream f(settings_path(), std::ios::binary);
|
|
22
|
+
if (!f) return json::object();
|
|
23
|
+
std::string blob((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
|
24
|
+
if (blob.empty()) return json::object();
|
|
25
|
+
try {
|
|
26
|
+
auto plain = secure::decrypt(blob);
|
|
27
|
+
if (!plain) return json::object();
|
|
28
|
+
return json::parse(*plain);
|
|
29
|
+
} catch (...) {
|
|
30
|
+
return json::object();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
inline void write_settings(const json& j) {
|
|
35
|
+
const std::string blob = secure::encrypt(j.dump());
|
|
36
|
+
fs::path tmp = settings_path();
|
|
37
|
+
tmp += ".tmp";
|
|
38
|
+
{ std::ofstream f(tmp, std::ios::binary | std::ios::trunc);
|
|
39
|
+
f.write(blob.data(), (std::streamsize)blob.size()); }
|
|
40
|
+
fs::rename(tmp, settings_path()); // atomic replace
|
|
41
|
+
lock_down(settings_path());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
} // namespace storage
|
|
45
|
+
|
|
46
|
+
inline void register_storage_bindings(Dispatcher& d) {
|
|
47
|
+
d.on("saveSetting", [&d](const json& a, Reply reply) {
|
|
48
|
+
try {
|
|
49
|
+
auto key = a.at(0).get<std::string>();
|
|
50
|
+
json s = storage::read_settings();
|
|
51
|
+
s[key] = a.at(1);
|
|
52
|
+
storage::write_settings(s);
|
|
53
|
+
d.emit("settings:changed", {{"key", key}, {"value", a.at(1)}}); // C++ -> UI
|
|
54
|
+
reply(json{{"ok", true}});
|
|
55
|
+
} catch (const std::exception& e) { reply(json{{"ok", false}, {"error", e.what()}}); }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
d.on("loadSetting", [](const json& a, Reply reply) {
|
|
59
|
+
try {
|
|
60
|
+
json s = storage::read_settings();
|
|
61
|
+
auto key = a.at(0).get<std::string>();
|
|
62
|
+
reply(json{{"ok", true}, {"value", s.contains(key) ? s[key] : json(nullptr)}});
|
|
63
|
+
} catch (const std::exception& e) { reply(json{{"ok", false}, {"error", e.what()}}); }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
d.on("loadAllSettings", [](const json&, Reply reply) {
|
|
67
|
+
try {
|
|
68
|
+
reply(json{{"ok", true}, {"value", storage::read_settings()}});
|
|
69
|
+
} catch (const std::exception& e) { reply(json{{"ok", false}, {"error", e.what()}}); }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// SQLite core for Hull: a small, safe, parameterized SQL surface returning JSON.
|
|
3
|
+
// Webview-free on purpose (only paths.hpp + sqlite3 + json) so it can be unit-tested
|
|
4
|
+
// standalone and reused by the bridge bindings (database.hpp).
|
|
5
|
+
//
|
|
6
|
+
// Security/speed posture:
|
|
7
|
+
// - one statement per exec/query/get (prepare_v2 ignores trailing SQL) -> no stacked
|
|
8
|
+
// "...; DROP TABLE" injection; multi-statement only via batch([...]) arrays
|
|
9
|
+
// - parameters are bound, never concatenated (sqlite3_bind_*)
|
|
10
|
+
// - DB lives in the per-user app dir, chmod 0600 on POSIX
|
|
11
|
+
// - WAL + NORMAL sync + busy_timeout for speed; foreign_keys + trusted_schema=OFF
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <mutex>
|
|
14
|
+
#include <stdexcept>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <nlohmann/json.hpp>
|
|
17
|
+
#include "paths.hpp"
|
|
18
|
+
#include "secure.hpp" // active() + the per-install key (secure build only)
|
|
19
|
+
#include "sqlite3.h" // vendored SQLite by default; SQLCipher's header in the secure build
|
|
20
|
+
|
|
21
|
+
using json = nlohmann::json;
|
|
22
|
+
|
|
23
|
+
namespace hulldb {
|
|
24
|
+
|
|
25
|
+
// Optional path override (used by tests). Default: <app_data_dir>/app.db
|
|
26
|
+
inline std::string& path_override() { static std::string p; return p; }
|
|
27
|
+
inline void set_db_path(const std::string& p) { path_override() = p; }
|
|
28
|
+
|
|
29
|
+
inline std::mutex& mtx() { static std::mutex m; return m; }
|
|
30
|
+
|
|
31
|
+
inline std::string db_file() {
|
|
32
|
+
if (!path_override().empty()) return path_override();
|
|
33
|
+
return (storage::app_data_dir() / "app.db").string();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Lazily open the single connection and apply pragmas. Caller must hold mtx().
|
|
37
|
+
inline sqlite3* handle() {
|
|
38
|
+
static sqlite3* db = nullptr;
|
|
39
|
+
if (db) return db;
|
|
40
|
+
const std::string path = db_file();
|
|
41
|
+
if (sqlite3_open(path.c_str(), &db) != SQLITE_OK) {
|
|
42
|
+
std::string err = db ? sqlite3_errmsg(db) : "open failed";
|
|
43
|
+
if (db) { sqlite3_close(db); db = nullptr; }
|
|
44
|
+
throw std::runtime_error("sqlite open: " + err);
|
|
45
|
+
}
|
|
46
|
+
storage::lock_down(path); // 0600 on POSIX
|
|
47
|
+
#if defined(HULL_CRYPTO)
|
|
48
|
+
// SQLCipher: set the encryption key (raw 32-byte key from the keychain, hex) as the
|
|
49
|
+
// very first operation, before any other SQL. The DB file is AES-encrypted at rest.
|
|
50
|
+
{
|
|
51
|
+
auto k = secure::data_key();
|
|
52
|
+
static const char* H = "0123456789abcdef";
|
|
53
|
+
std::string hex;
|
|
54
|
+
hex.reserve(k.size() * 2);
|
|
55
|
+
for (unsigned char c : k) { hex.push_back(H[c >> 4]); hex.push_back(H[c & 0xF]); }
|
|
56
|
+
const std::string pragma = "PRAGMA key = \"x'" + hex + "'\";";
|
|
57
|
+
sqlite3_exec(db, pragma.c_str(), nullptr, nullptr, nullptr);
|
|
58
|
+
}
|
|
59
|
+
#endif
|
|
60
|
+
for (const char* p : {
|
|
61
|
+
"PRAGMA journal_mode=WAL;",
|
|
62
|
+
"PRAGMA synchronous=NORMAL;",
|
|
63
|
+
"PRAGMA foreign_keys=ON;",
|
|
64
|
+
"PRAGMA busy_timeout=5000;",
|
|
65
|
+
"PRAGMA trusted_schema=OFF;",
|
|
66
|
+
}) {
|
|
67
|
+
sqlite3_exec(db, p, nullptr, nullptr, nullptr);
|
|
68
|
+
}
|
|
69
|
+
return db;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
inline std::string b64(const unsigned char* data, int len) {
|
|
73
|
+
static const char* T =
|
|
74
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
75
|
+
std::string out;
|
|
76
|
+
out.reserve(((len + 2) / 3) * 4);
|
|
77
|
+
for (int i = 0; i < len; i += 3) {
|
|
78
|
+
int n = data[i] << 16;
|
|
79
|
+
if (i + 1 < len) n |= data[i + 1] << 8;
|
|
80
|
+
if (i + 2 < len) n |= data[i + 2];
|
|
81
|
+
out.push_back(T[(n >> 18) & 63]);
|
|
82
|
+
out.push_back(T[(n >> 12) & 63]);
|
|
83
|
+
out.push_back(i + 1 < len ? T[(n >> 6) & 63] : '=');
|
|
84
|
+
out.push_back(i + 2 < len ? T[n & 63] : '=');
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
inline void bind_value(sqlite3_stmt* st, int i, const json& v) {
|
|
90
|
+
if (v.is_null()) sqlite3_bind_null(st, i);
|
|
91
|
+
else if (v.is_boolean()) sqlite3_bind_int(st, i, v.get<bool>() ? 1 : 0);
|
|
92
|
+
else if (v.is_number_integer()) sqlite3_bind_int64(st, i, v.get<int64_t>());
|
|
93
|
+
else if (v.is_number_unsigned()) sqlite3_bind_int64(st, i, (int64_t)v.get<uint64_t>());
|
|
94
|
+
else if (v.is_number_float()) sqlite3_bind_double(st, i, v.get<double>());
|
|
95
|
+
else if (v.is_string()) {
|
|
96
|
+
const std::string s = v.get<std::string>();
|
|
97
|
+
sqlite3_bind_text(st, i, s.c_str(), (int)s.size(), SQLITE_TRANSIENT);
|
|
98
|
+
} else {
|
|
99
|
+
throw std::runtime_error(
|
|
100
|
+
"unsupported parameter type — use null/boolean/number/string "
|
|
101
|
+
"(JSON.stringify objects/arrays into a TEXT column)");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
inline void bind_all(sqlite3_stmt* st, const json& params) {
|
|
106
|
+
if (params.is_null()) return;
|
|
107
|
+
if (!params.is_array()) throw std::runtime_error("params must be an array");
|
|
108
|
+
for (int i = 0; i < (int)params.size(); ++i) bind_value(st, i + 1, params[i]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
inline json row_to_json(sqlite3_stmt* st) {
|
|
112
|
+
json row = json::object();
|
|
113
|
+
const int cols = sqlite3_column_count(st);
|
|
114
|
+
for (int c = 0; c < cols; ++c) {
|
|
115
|
+
const char* name = sqlite3_column_name(st, c);
|
|
116
|
+
switch (sqlite3_column_type(st, c)) {
|
|
117
|
+
case SQLITE_INTEGER: row[name] = (int64_t)sqlite3_column_int64(st, c); break;
|
|
118
|
+
case SQLITE_FLOAT: row[name] = sqlite3_column_double(st, c); break;
|
|
119
|
+
case SQLITE_TEXT:
|
|
120
|
+
row[name] = std::string(reinterpret_cast<const char*>(sqlite3_column_text(st, c)),
|
|
121
|
+
sqlite3_column_bytes(st, c));
|
|
122
|
+
break;
|
|
123
|
+
case SQLITE_BLOB:
|
|
124
|
+
row[name] = b64(reinterpret_cast<const unsigned char*>(sqlite3_column_blob(st, c)),
|
|
125
|
+
sqlite3_column_bytes(st, c));
|
|
126
|
+
break;
|
|
127
|
+
default: row[name] = nullptr; break; // SQLITE_NULL
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return row;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Run ONE statement (caller holds mtx). Returns { rows, changes, lastInsertRowid }.
|
|
134
|
+
inline json run_one(sqlite3* db, const std::string& sql, const json& params) {
|
|
135
|
+
sqlite3_stmt* st = nullptr;
|
|
136
|
+
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) != SQLITE_OK) {
|
|
137
|
+
throw std::runtime_error(sqlite3_errmsg(db));
|
|
138
|
+
}
|
|
139
|
+
bind_all(st, params);
|
|
140
|
+
json rows = json::array();
|
|
141
|
+
int rc;
|
|
142
|
+
while ((rc = sqlite3_step(st)) == SQLITE_ROW) rows.push_back(row_to_json(st));
|
|
143
|
+
if (rc != SQLITE_DONE) {
|
|
144
|
+
std::string e = sqlite3_errmsg(db);
|
|
145
|
+
sqlite3_finalize(st);
|
|
146
|
+
throw std::runtime_error(e);
|
|
147
|
+
}
|
|
148
|
+
sqlite3_finalize(st);
|
|
149
|
+
return json{{"rows", rows},
|
|
150
|
+
{"changes", sqlite3_changes(db)},
|
|
151
|
+
{"lastInsertRowid", (int64_t)sqlite3_last_insert_rowid(db)}};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- Public API (each throws std::runtime_error on failure) ----
|
|
155
|
+
|
|
156
|
+
inline json exec(const std::string& sql, const json& params) {
|
|
157
|
+
std::lock_guard<std::mutex> lock(mtx());
|
|
158
|
+
json r = run_one(handle(), sql, params);
|
|
159
|
+
return json{{"ok", true}, {"changes", r["changes"]}, {"lastInsertRowid", r["lastInsertRowid"]}};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
inline json query(const std::string& sql, const json& params) {
|
|
163
|
+
std::lock_guard<std::mutex> lock(mtx());
|
|
164
|
+
json r = run_one(handle(), sql, params);
|
|
165
|
+
return json{{"ok", true}, {"rows", r["rows"]}};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
inline json get(const std::string& sql, const json& params) {
|
|
169
|
+
std::lock_guard<std::mutex> lock(mtx());
|
|
170
|
+
json r = run_one(handle(), sql, params);
|
|
171
|
+
return json{{"ok", true}, {"row", r["rows"].empty() ? json(nullptr) : r["rows"][0]}};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Run several statements atomically (one transaction; rollback on any error).
|
|
175
|
+
inline json batch(const json& statements) {
|
|
176
|
+
if (!statements.is_array()) throw std::runtime_error("batch expects an array of { sql, params }");
|
|
177
|
+
std::lock_guard<std::mutex> lock(mtx());
|
|
178
|
+
sqlite3* db = handle();
|
|
179
|
+
run_one(db, "BEGIN IMMEDIATE;", json(nullptr));
|
|
180
|
+
try {
|
|
181
|
+
json results = json::array();
|
|
182
|
+
for (const auto& s : statements) {
|
|
183
|
+
const std::string sql = s.at("sql").get<std::string>();
|
|
184
|
+
const json params = s.contains("params") ? s["params"] : json::array();
|
|
185
|
+
json r = run_one(db, sql, params);
|
|
186
|
+
results.push_back({{"changes", r["changes"]},
|
|
187
|
+
{"lastInsertRowid", r["lastInsertRowid"]},
|
|
188
|
+
{"rows", r["rows"]}});
|
|
189
|
+
}
|
|
190
|
+
run_one(db, "COMMIT;", json(nullptr));
|
|
191
|
+
return json{{"ok", true}, {"results", results}};
|
|
192
|
+
} catch (...) {
|
|
193
|
+
try { run_one(db, "ROLLBACK;", json(nullptr)); } catch (...) {}
|
|
194
|
+
throw;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
} // namespace hulldb
|