Anklang-0.3.0.dev712+gdc4e642f anklang-0.3.0.dev712+gdc4e642f
ASE — Anklang Sound Engine (C++)

« « « Anklang Documentation
Loading...
Searching...
No Matches
jsonapi.cc
Go to the documentation of this file.
1 // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2#include "jsonapi.hh"
3#include "server.hh"
4#include "main.hh"
5#include "internal.hh"
6
7#define GCDEBUG(...) Ase::debug ("gc", __VA_ARGS__)
8#define GCDEBUG_ENABLED() Ase::debug_key_enabled ("gc")
9
10namespace Ase {
11
12static String subprotocol_authentication;
13
14void
15jsonapi_set_subprotocol (const String &subprotocol)
16{
17 subprotocol_authentication = subprotocol;
18}
19
20// == JsonapiConnection ==
21class JsonapiConnection;
22using JsonapiConnectionP = std::shared_ptr<JsonapiConnection>;
23using JsonapiConnectionW = std::weak_ptr<JsonapiConnection>;
24static JsonapiConnectionP current_message_conection;
25
26static bool
27is_localhost (const String &url, int port)
28{
29 return Re::search ("^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?/", url, Re::I) >= 0;
30}
31
33 Jsonipc::InstanceMap imap_, gcmap_;
34 void
35 log (const String &message) override
36 {
37 printerr ("%s: %s\n", nickname(), message);
38 }
39 int
40 validate() override
41 {
42 using namespace AnsiColors;
43 const auto C1 = color (BOLD), C0 = color (BOLD_OFF);
44 const Info info = get_info();
45 const String origin = info.header ("Origin") + "/";
46 const bool localhost_origin = is_localhost (origin, info.lport);
47 const bool subproto_ok = (info.subs.size() == 0 && subprotocol_authentication.empty()) ||
48 (info.subs.size() == 1 && subprotocol_authentication == info.subs[0]);
49 if (localhost_origin && subproto_ok)
50 return 0; // OK
51 // log rejection
52 String why;
53 if (!localhost_origin) why = "Bad Origin";
54 else if (!subproto_ok) why = "Bad Subprotocol";
55 const String ua = info.header ("User-Agent");
56 if (logflags_ & 2)
57 log (string_format ("%sREJECT:%s %s:%d/ (%s) - %s", C1, C0, info.remote, info.rport, why, ua));
58 return -1; // reject
59 }
60 void
61 opened() override
62 {
63 using namespace AnsiColors;
64 const auto C1 = color (BOLD), C0 = color (BOLD_OFF);
65 const Info info = get_info();
66 const String ua = info.header ("User-Agent");
67 if (logflags_ & 4)
68 log (string_format ("%sACCEPT:%s %s:%d/ - %s", C1, C0, info.remote, info.rport, ua));
69 }
70 void
71 closed() override
72 {
73 using namespace AnsiColors;
74 const auto C1 = color (BOLD), C0 = color (BOLD_OFF);
75 if (logflags_ & 4)
76 log (string_format ("%sCLOSED%s", C1, C0));
77 trigger_destroy_hooks();
78 }
79 void
80 message (const String &message) override
81 {
83 assert_return (conp);
84 struct MessageData { // keep arguments alive, even after __func__ returns
86 String message, reply;
88 MessageData (JsonapiConnectionP c, const String &m) : message (m), conp (c) {}
89 };
90 auto data = std::make_shared<MessageData> (conp, message);
91 main_jobs += [data, this] () {
92 if (main_loop->has_quit())
93 return;
94 current_message_conection = data->conp;
95 data->reply = this->handle_jsonipc (data->message);
96 current_message_conection = nullptr;
97 data->sem.post();
98 };
99 nickname(); // cache socket nickname for use during errors
100 // wait with timeout, checking if main loop has quit to avoid deadlock on shutdown
101 const uint64_t timeout_us = 50 * 1000;
102 do {
103 const int ret = data->sem.wait_for (timeout_us);
104 if (ret == 0)
105 break; // semaphore was posted, job completed successfully
106 if (data->reply.empty() && main_loop->has_quit())
107 data->reply = "{id:0,error:{code:-32601,message:\"Method not found: endpoint shutting down\"}}\n";
108 } while (data->reply.empty());
109 // when queueing asynchronously, we have to use WebSocketConnectionP
110 if (!data->reply.empty())
111 send_text (data->reply);
112 }
113 String handle_jsonipc (const std::string &message);
114 std::vector<JsTrigger> triggers_; // HINT: use unordered_map if this becomes slow
115public:
116 explicit JsonapiConnection (WebSocketConnection::Internals &internals, int logflags) :
117 WebSocketConnection (internals, logflags)
118 {}
120 {
121 trigger_destroy_hooks();
122 }
123 bool
124 renew_gc ()
125 {
126 const bool starting_gc = imap_.mark_unused();
127 GCDEBUG ("%s: imap_=%d%s\n", __func__, imap_.size(), starting_gc ? " (duplicate)" : "");
128 return starting_gc; // false: duplicate request, waiting for report_gc
129 }
130 bool
131 report_gc (const std::vector<size_t> &ids)
132 {
133 const size_t preerved = imap_.purge_unused (ids);
134 GCDEBUG ("%s: considered=%d retained=%d purged=%d active=%d\n", __func__,
135 ids.size(), preerved, ids.size() - preerved, imap_.size());
136 return imap_.size();
137 }
139 trigger_lookup (const String &id)
140 {
141 for (auto it = triggers_.begin(); it != triggers_.end(); it++)
142 if (id == it->id())
143 return *it;
144 return {};
145 }
146 void
147 trigger_remove (const String &id)
148 {
149 trigger_lookup (id).destroy();
150 }
151 void
152 trigger_create (const String &id)
153 {
154 using namespace Jsonipc;
156 assert_return (jsonapi_connection_p);
157 std::weak_ptr<JsonapiConnection> selfw = jsonapi_connection_p;
158 const int logflags = logflags_;
159 // marshal remote trigger
160 auto trigger_remote = [selfw, id, logflags] (ValueS &&args) // weak_ref avoids cycles
161 {
162 JsonapiConnectionP selfp = selfw.lock();
163 return_unless (selfp);
164 const String msg = jsonobject_to_string ("method", id /*"Jsonapi/Trigger/_%%%"*/, "params", args);
165 if (logflags & 8)
166 selfp->log (string_format ("⬰ %s", msg));
167 selfp->send_text (msg);
168 };
169 JsTrigger trigger = JsTrigger::create (id, trigger_remote);
170 triggers_.push_back (trigger);
171 // marshall remote destroy notification and erase triggers_ entry
172 auto erase_trigger = [selfw, id, logflags] () // weak_ref avoids cycles
173 {
174 std::shared_ptr<JsonapiConnection> selfp = selfw.lock();
175 return_unless (selfp);
176 if (selfp->is_open())
177 {
178 ValueS args { id };
179 const String msg = jsonobject_to_string ("method", "Jsonapi/Trigger/killed", "params", args);
180 if (logflags & 8)
181 selfp->log (string_format ("↚ %s", msg));
182 selfp->send_text (msg);
183 }
184 Aux::erase_first (selfp->triggers_, [id] (auto &t) { return id == t.id(); });
185 };
186 trigger.ondestroy (erase_trigger);
187 }
188 void
189 trigger_destroy_hooks()
190 {
192 old.swap (triggers_); // speed up erase_trigger() searches
193 for (auto &trigger : old)
194 trigger.destroy();
195 custom_data_destroy();
196 }
197};
198
200jsonapi_make_connection (WebSocketConnection::Internals &internals, int logflags)
201{
202 return std::make_shared<JsonapiConnection> (internals, logflags);
203}
204
205#define ERROR500(WHAT) \
206 Jsonipc::bad_invocation (-32500, \
207 __FILE__ ":" \
208 ASE_CPP_STRINGIFY (__LINE__) ": " \
209 "Internal Server Error: " \
210 WHAT)
211#define assert_500(c) (__builtin_expect (static_cast<bool> (c), 1) ? (void) 0 : throw ERROR500 (#c) )
212
214make_dispatcher()
215{
216 using namespace Jsonipc;
217 static IpcDispatcher *dispatcher = [] () {
218 dispatcher = new IpcDispatcher();
219 dispatcher->add_method ("Jsonapi/renew-gc",
220 [] (CallbackInfo &cbi)
221 {
222 assert_500 (current_message_conection);
223 if (cbi.n_args() > 0)
224 throw Jsonipc::bad_invocation (-32602, "Invalid params");
225 const auto ret = current_message_conection->renew_gc ();
226 cbi.set_result (to_json (ret, cbi.allocator()).Move());
227 });
228 dispatcher->add_method ("Jsonapi/report-gc",
229 [] (CallbackInfo &cbi)
230 {
231 assert_500 (current_message_conection);
232 if (cbi.n_args() != 1)
233 throw Jsonipc::bad_invocation (-32602, "Invalid params");
234 const auto ids = from_json<std::vector<size_t>> (cbi.ntharg (0));
235 const auto ret = current_message_conection->report_gc (ids);
236 cbi.set_result (to_json (ret, cbi.allocator()).Move());
237 });
238 dispatcher->add_method ("Jsonapi/initialize",
239 [] (CallbackInfo &cbi)
240 {
241 assert_500 (current_message_conection);
242 Server &server = ASE_SERVER;
243 std::shared_ptr<Server> serverp = shared_ptr_cast<Server> (&server);
244 cbi.set_result (to_json (serverp, cbi.allocator()).Move());
245 });
246 dispatcher->add_method ("Jsonapi/Trigger/create",
247 [] (CallbackInfo &cbi)
248 {
249 assert_500 (current_message_conection);
250 const String triggerid = cbi.n_args() == 1 ? from_json<String> (cbi.ntharg (0)) : "";
251 if (triggerid.compare (0, 17, "Jsonapi/Trigger/_") != 0)
252 throw Jsonipc::bad_invocation (-32602, "Invalid params");
253 current_message_conection->trigger_create (triggerid);
254 });
255 dispatcher->add_method ("Jsonapi/Trigger/remove",
256 [] (CallbackInfo &cbi)
257 {
258 assert_500 (current_message_conection);
259 const String triggerid = cbi.n_args() == 1 ? from_json<String> (cbi.ntharg (0)) : "";
260 if (triggerid.compare (0, 17, "Jsonapi/Trigger/_") != 0)
261 throw Jsonipc::bad_invocation (-32602, "Invalid params");
262 current_message_conection->trigger_remove (triggerid);
263 });
264 return dispatcher;
265 } ();
266 return dispatcher;
267}
268
269String
270JsonapiConnection::handle_jsonipc (const std::string &message)
271{
272 if (logflags_ & 8)
273 log (string_format ("→ %s", message.size() > 1024 ? message.substr (0, 1020) + "..." + message.back() : message));
274 Jsonipc::Scope message_scope (imap_);
275 String reply;
276 { // enfore notifies *before* reply (and the corresponding log() messages)
277 CoalesceNotifies coalesce_notifies; // coalesce multiple "notify:detail" emissions
278 reply = make_dispatcher()->dispatch_message (message);
279 } // coalesced notifications occour *here*
280 if (logflags_ & 8)
281 {
282 const char *errorat = strstr (reply.c_str(), "\"error\":{");
283 if (errorat && errorat > reply.c_str() && (errorat[-1] == ',' || errorat[-1] == '{'))
284 {
285 using namespace AnsiColors;
286 auto R1 = color (BOLD) + color (FG_RED), R0 = color (FG_DEFAULT) + color (BOLD_OFF);
287 log (string_format ("%s←%s %s", R1, R0, reply));
288 }
289 else
290 log (string_format ("← %s", reply.size() > 1024 ? reply.substr (0, 1020) + "..." + reply.back() : reply));
291 }
292 return reply;
293}
294
295// == JsTrigger ==
297 using Func = std::function<void (ValueS)>;
298 const String id;
299 Func func;
300 using VoidFunc = std::function<void()>;
301 std::vector<VoidFunc> destroyhooks;
302 friend class JsTrigger;
303 /*ctor*/ Impl () = delete;
304 /*copy*/ Impl (const Impl&) = delete;
305 Impl& operator= (const Impl&) = delete;
306public:
307 ~Impl ()
308 {
309 destroy();
310 }
311 Impl (const Func &f, const String &_id) :
312 id (_id), func (f)
313 {}
314 void
315 destroy ()
316 {
317 func = nullptr;
318 while (!destroyhooks.empty())
319 {
320 VoidFunc destroyhook = destroyhooks.back();
321 destroyhooks.pop_back();
322 destroyhook();
323 }
324 }
325};
326
327void
328JsTrigger::ondestroy (const VoidFunc &vf)
329{
330 assert_return (p_);
331 if (vf)
332 p_->destroyhooks.push_back (vf);
333}
334
335void
336JsTrigger::call (ValueS &&args) const
337{
338 assert_return (p_);
339 if (p_->func)
340 p_->func (std::move (args));
341}
342
343JsTrigger
344JsTrigger::create (const String &triggerid, const JsTrigger::Impl::Func &f)
345{
346 JsTrigger trigger;
347 trigger.p_ = std::make_shared<JsTrigger::Impl> (f, triggerid);
348 assert_return (f != nullptr, trigger);
349 return trigger;
350}
351
352String
353JsTrigger::id () const
354{
355 return p_ ? p_->id : "";
356}
357
358void
359JsTrigger::destroy ()
360{
361 if (p_)
362 p_->destroy();
363}
364
365JsTrigger::operator bool () const noexcept
366{
367 return p_ && p_->func;
368}
369
370JsTrigger
371ConvertJsTrigger::lookup (const String &triggerid)
372{
373 if (current_message_conection)
374 return current_message_conection->trigger_lookup (triggerid);
375 assert_return (current_message_conection, {});
376 return {};
377}
378
379CustomDataContainer*
380jsonapi_connection_data ()
381{
382 if (current_message_conection)
383 return current_message_conection.get();
384 return nullptr;
385}
386
387JsonapiBinarySender
388jsonapi_connection_sender ()
389{
390 return_unless (current_message_conection, {});
391 JsonapiConnectionW conw = current_message_conection;
392 return [conw] (const String &blob) {
393 JsonapiConnectionP conp = conw.lock();
394 return conp ? conp->send_binary (blob) : false;
395 };
396}
397
398} // Ase
399
400// Build generated bindings
402
403#include "testing.hh"
404
405namespace { // Anon
406
407TEST_INTEGRITY (jsonapi_tests);
408static void
409jsonapi_tests()
410{
411 using namespace Ase;
412 using IdStringMap = std::map<size_t,std::string>;
413 IdStringMap tmap;
414 for (size_t i = 1; i <= 99; i++)
415 tmap[1000 - i] = string_format ("%d", i);
416 TASSERT (tmap.size() == 99);
417 for (auto it = tmap.begin(), next = it; it != tmap.end() ? ++next, 1 : 0; it = next) // keep next ahead of it, but avoid ++end
418 tmap.erase (it);
419 TASSERT (tmap.size() == 0);
420}
421
422} // Anon
T back(T... args)
DataListContainer - typesafe storage and retrieval of arbitrary members.
Definition utils.hh:86
Callback mechanism for Jsonapi/Jsonipc.
Definition value.hh:123
static ssize_t search(const String &regex, const String &input, Flags=DEFAULT)
Find regex in input and return match position >= 0 or return < 0 otherwise.
Definition regex.cc:217
bool send_text(const String &message)
Returns true if text message was sent.
Definition websocket.cc:264
Keep track of temporary instances during IpcDispatcher::dispatch_message().
Definition jsonipc.hh:163
T empty(T... args)
#define assert_return(expr,...)
Return from the current function if expr is unmet and issue an assertion warning.
Definition internal.hh:29
#define return_unless(cond,...)
Return silently if cond does not evaluate to true with return value ...
Definition internal.hh:73
#define TEST_INTEGRITY(FUNC)
Register func as an integrity test.
Definition internal.hh:79
std::string color(Colors acolor, Colors c1, Colors c2, Colors c3, Colors c4, Colors c5, Colors c6)
Return ANSI code for the specified color if stdout & stderr should be colorized, see colorize_tty().
Definition platform.cc:191
size_t erase_first(C &container, const std::function< bool(typename C::value_type const &value)> &pred)
Erase first element for which pred() is true in vector or list.
Definition utils.hh:268
The Anklang C++ API namespace.
Definition api.hh:9
std::string string_format(const char *format, const Args &...args) __attribute__((__format__(__printf__
Format a string similar to sprintf(3) with support for std::string and std::ostringstream convertible...
JobQueue main_jobs(call_main_loop)
Execute a job callback in the event loop.
Definition main.hh:43
std::string String
Convenience alias for std::string.
Definition cxxaux.hh:35
T next(T... args)
T size(T... args)
typedef uint64_t
strstr
Context for calling C++ functions from Json.
Definition jsonipc.hh:450
Jsonipc exception that is relayed to caller when thrown during invocations.
Definition jsonipc.hh:144
T substr(T... args)
#define TASSERT(cond)
Unconditional test assertion, enters breakpoint if not fullfilled.
Definition testing.hh:24