blob: 5f06d5f4c826e2270b27aea679ebe031432ed9aa [file] [log] [blame]
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +01001/*! \file osmo-mslookup-client.c
2 * Distributed GSM: find the location of subscribers, for example by multicast DNS,
3 * to obtain HLR, SIP or SMPP server addresses (or arbitrary service names).
4 */
5/*
6 * (C) 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
7 * (C) 2019 by Neels Hofmeyr <neels@hofmeyr.de>
8 *
9 * All Rights Reserved
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License along
22 * with this program. If not, see <http://www.gnu.org/licenses/>.
23 */
24
25#include <stdio.h>
26#include <unistd.h>
27#include <getopt.h>
28#include <errno.h>
29#include <talloc.h>
30#include <sys/un.h>
31
32#include <osmocom/core/application.h>
33#include <osmocom/core/logging.h>
34#include <osmocom/core/select.h>
35#include <osmocom/core/socket.h>
36#include <osmocom/mslookup/mslookup_client.h>
37#include <osmocom/mslookup/mslookup_client_mdns.h>
38#include <osmocom/mslookup/mdns_sock.h>
39#include <osmocom/mslookup/mdns.h>
40
41#define CSV_HEADERS "query\tresult\tlast\tage\tv4_ip\tv4_port\tv6_ip\tv6_port"
42
43static void print_version(void)
44{
45 printf("osmo-mslookup-client version %s\n", PACKAGE_VERSION);
46 printf("\n"
47 "Copyright (C) 2019 by sysmocom - s.f.m.c. GmbH\n"
48 "Copyright (C) 2019 by Neels Hofmeyr <neels@hofmeyr.de>\n"
49 "This program is free software; you can redistribute it and/or modify\n"
50 "it under the terms of the GNU General Public License as published by\n"
51 "the Free Software Foundation; either version 2 of the License, or\n"
52 "(at your option) any later version.\n"
53 "\n");
54}
55
56static void print_help()
57{
58 print_version();
59 printf(
60"Standalone mslookup client for Distributed GSM\n"
61"\n"
62"Receiving mslookup results means listening for responses on a socket. Often,\n"
63"integration (e.g. FreeSwitch dialplan.py) makes it hard to select() on a socket\n"
64"to read responses, because that interferes with the main program (e.g.\n"
65"FreeSwitch's dialplan.py seems to be integrated with an own select() main loop\n"
66"that interferes with osmo_select_main(), or an smpp.py uses\n"
67"smpplib.client.listen() as main loop, etc.).\n"
68"\n"
69"This program provides a trivial solution, by outsourcing the mslookup main loop\n"
70"to a separate process. Communication is done via cmdline arg and stdout pipe or\n"
71"a (blocking) unix domain socket, results are returned in CSV or JSON format.\n"
72"\n"
73"This can be done one-shot, i.e. exit as soon as the response has been\n"
74"determined, or in daemon form, i.e. continuously listen for requests and return\n"
75"responses.\n"
76"\n"
77"About running a local daemon: it is unintuitive to connect to a socket to solve\n"
78"a problem of reading from a socket -- it seems like just more of the same\n"
79"problem. The reasons why the daemon is in fact useful are:\n"
80"- The osmo-mslookup-client daemon will return only those results matching\n"
81" requests issued on that socket connection.\n"
82"- A program can simply blockingly recv() from the osmo-mslookup-client socket\n"
83" instead of needing to run osmo_select_main() so that libosmo-mslookup is able\n"
84" to asynchronously receive responses from remote servers.\n"
85"- Only one long-lived multicast socket needs to be opened instead of a new\n"
86" socket for each request.\n"
87"\n"
88"Output is in CSV or json, see --format. The default is tab-separated CSV\n"
89"with these columns:\n"
90CSV_HEADERS "\n"
91"\n"
92"One-shot operation example:\n"
93"$ osmo-mslookup-client 1000-@sip.voice.12345.msisdn -f json\n"
94"{\"query\": \"sip.voice.12345.msisdn\", \"result\": \"result\", \"last\": true, \"age\": 5, \"v4\": [\"1.2.3.7\", \"23\"]}\n"
95"$\n"
96"\n"
97"Daemon operation example:\n"
98"$ osmo-mslookup-client -s /tmp/mslookup -d\n"
99"(and a client program then connects to /tmp/mslookup, find an implementation\n"
100"example below)\n"
101"\n"
102"Integrating with calling programs can be done by:\n"
103"- call osmo-mslookup-client with the query string as argument.\n"
104" It will open a multicast DNS socket, send out a query and wait for the\n"
105" matching response. It will print the result on stdout and exit.\n"
106" This method launches a new process for every mslookup query,\n"
107" and creates a short-lived multicast listener for each invocation.\n"
108" This is fine for low activity, but does not scale well.\n"
109"\n"
110"- invoke osmo-mslookup-client --socket /tmp/mslookup -d.\n"
111" Individual queries can be sent by connecting to that unix domain socket,\n"
112" blockingly reading the response when it arrives and disconnecting.\n"
113" This way only one process keeps one multicast listener open.\n"
114" Callers can connect to this socket without spawning processes.\n"
115" This is recommended for scale.\n"
116"\n"
117"Python example clients for {CSV,JSON}x{cmdline,socket} can be found here:\n"
118"http://git.osmocom.org/osmo-hlr/tree/contrib/dgsm/osmo-mslookup-pipe.py\n"
119"http://git.osmocom.org/osmo-hlr/tree/contrib/dgsm/osmo-mslookup-socket.py\n"
120"\n"
121"\n"
122"Options:\n"
123"\n"
124"[[delay-][timeout]@]service.number.id\n"
125" A service query string with optional individual timeout.\n"
126" The same format is also used on a daemon socket, if any.\n"
127" The timeout consists of the min-delay and the timeout numbers,\n"
128" corresponding to the --min-delay and --timeout options, in milliseconds.\n"
129" These options apply if a query string lacks own numbers.\n"
130" Examples:\n"
131" gsup.hlr.1234567.imsi Use cmdline timeout settings\n"
132" 5000@gsup.hlr.1234567.imsi Return N results for 5 seconds\n"
133" 1000-5000@sip.voice.123.msisdn Same, but silent for first second\n"
134" 10000-@smpp.sms.567.msisdn Return 1 result after 10 seconds\n"
135"\n"
136"--format -f csv (default)\n"
137" Format result lines in CSV format.\n"
138"--no-csv-headers -H\n"
139" If the format is 'csv', by default, the first output line prints the\n"
140" CSV headers used for CSV output format. This option disables these CSV\n"
141" headers.\n"
142"\n"
143"--format -f json\n"
144" Format result lines in json instead of semicolon separated, like:\n"
145" {\"query\": \"sip.voice.12345.msisdn\", \"result\": \"ok\", \"v4\": [\"10.9.8.7\", \"5060\"]}\n"
146"\n"
147"--daemon -d\n"
148" Keep running after a request has been serviced\n"
149"\n"
150"--mdns-ip -m " OSMO_MSLOOKUP_MDNS_IP4 " -m " OSMO_MSLOOKUP_MDNS_IP6 "\n"
151"--mdns-port -M " OSMO_STRINGIFY_VAL(OSMO_MSLOOKUP_MDNS_PORT) "\n"
152" Set multicast IP address / port to send mDNS requests and listen for\n"
153" mDNS reponses\n"
154"--mdns-domain-suffix -D " OSMO_MDNS_DOMAIN_SUFFIX_DEFAULT "\n"
155" Append this suffix to each mDNS query's domain to avoid colliding with the\n"
156" top-level domains administrated by IANA.\n"
157"\n"
158"--min-delay -t 1000 (in milliseconds)\n"
159" Set minimum delay to wait before returning any results.\n"
160" When this timeout has elapsed, the best current result is returned,\n"
161" if any is available.\n"
162" Responses arriving after the min-delay has elapsed which have a younger\n"
163" age than previous results are returned immediately.\n"
164" Note: When a response with age of zero comes in, the result is returned\n"
165" immediately and the request is discarded: non-daemon mode exits, daemon\n"
166" mode ignores later results.\n"
167"\n"
168"--timeout -T 1000 (in milliseconds)\n"
169" Set timeout after which to stop listening for responses.\n"
170" If this is smaller than -t, the value from -t will be used for -T as well.\n"
171" Note: When a response with age of zero comes in, the result is returned\n"
172" immediately and the request is discarded: non-daemon mode exits, daemon\n"
173" mode ignores later results.\n"
174"\n"
175"--socket -s /path/to/unix-domain-socket\n"
176" Listen to requests from and write responses to a UNIX domain socket.\n"
177"\n"
178"--send -S <query> <age> <ip1> <port1> <ip2> <port2>\n"
179" Do not query, but send an mslookup result. This is useful only for\n"
180" testing. Examples:\n"
181" --send foo.123.msisdn 300 23.42.17.11 1234\n"
182" --send foo.123.msisdn 300 2323:4242:1717:1111::42 1234\n"
183" --send foo.123.msisdn 300 23.42.17.11 1234 2323:4242:1717:1111::42 1234\n"
184"\n"
185"--quiet -q\n"
186" Do not print errors to stderr, do not log to stderr.\n"
187"\n"
188"--help -h\n"
189" This help\n"
190);
191}
192
193enum result_format {
194 FORMAT_CSV = 0,
195 FORMAT_JSON,
196};
197
198static struct {
199 bool daemon;
200 struct osmo_sockaddr_str mdns_addr;
201 uint32_t min_delay;
202 uint32_t timeout;
203 const char *socket_path;
204 const char *format_str;
205 const char *mdns_domain_suffix;
206 bool csv_headers;
207 bool send;
208 bool quiet;
209} cmdline_opts = {
210 .mdns_addr = { .af=AF_INET, .ip=OSMO_MSLOOKUP_MDNS_IP4, .port=OSMO_MSLOOKUP_MDNS_PORT },
211 .min_delay = 1000,
212 .timeout = 1000,
213 .csv_headers = true,
214 .mdns_domain_suffix = OSMO_MDNS_DOMAIN_SUFFIX_DEFAULT,
215};
216
217#define print_error(fmt, args...) do { \
218 if (!cmdline_opts.quiet) \
219 fprintf(stderr, fmt, ##args); \
220 } while (0)
221
222char g_buf[1024];
223
224long long int parse_int(long long int minval, long long int maxval, const char *arg, int *rc)
225{
226 long long int val;
227 char *endptr;
228 if (rc)
229 *rc = -1;
230 if (!arg)
231 return -1;
232 errno = 0;
233 val = strtoll(arg, &endptr, 10);
234 if (errno || val < minval || val > maxval || *endptr)
235 return -1;
236 if (rc)
237 *rc = 0;
238 return val;
239}
240
241int cb_doing_nothing(struct osmo_fd *fd, unsigned int what)
242{
243 return 0;
244}
245
246/* --send: Just send a response, for manual testing. */
247int do_send(int argc, char ** argv)
248{
249 /* parse args <query> <age> <v4-ip> <v4-port> <v6-ip> <v6-port> */
250#define ARG(NR) ((argc > NR)? argv[NR] : NULL)
251 const char *query_str = ARG(0);
252 const char *age_str = ARG(1);
253 const char *ip_strs[2][2] = {
254 { ARG(2), ARG(3) },
255 { ARG(4), ARG(5) },
256 };
257 struct osmo_mslookup_query q = {};
258 struct osmo_mslookup_result r = { .rc = OSMO_MSLOOKUP_RC_RESULT };
259 int i;
260 int rc;
261 void *ctx = talloc_named_const(NULL, 0, __func__);
262 struct osmo_mdns_sock *sock;
263
264 if (!query_str) {
265 print_error("--send needs a query string like foo.123456.imsi\n");
266 return 1;
267 }
268 if (osmo_mslookup_query_init_from_domain_str(&q, query_str)) {
269 print_error("Invalid query string '%s', need a query string like foo.123456.imsi\n",
270 query_str);
271 return 1;
272 }
273
274 if (!age_str) {
275 print_error("--send needs an age\n");
276 return 1;
277 }
278 r.age = parse_int(0, UINT32_MAX, age_str, &rc);
279 if (rc) {
280 print_error("invalid age\n");
281 return 1;
282 }
283
284 for (i = 0; i < 2; i++) {
285 struct osmo_sockaddr_str addr;
286 uint16_t port;
287 if (!ip_strs[i][0])
288 continue;
289 port = parse_int(1, 65535, ip_strs[i][1] ? : "2342", &rc);
290 if (rc) {
291 print_error("invalid port: %s\n", ip_strs[i][1] ? : "NULL");
292 return 1;
293 }
294 if (osmo_sockaddr_str_from_str(&addr, ip_strs[i][0], port)) {
295 print_error("invalid IP addr: %s\n", ip_strs[i][0]);
296 return 1;
297 }
298 if (addr.af == AF_INET)
299 r.host_v4 = addr;
300 else
301 r.host_v6 = addr;
302 }
303
304 printf("Sending mDNS to " OSMO_SOCKADDR_STR_FMT ": %s\n", OSMO_SOCKADDR_STR_FMT_ARGS(&cmdline_opts.mdns_addr),
305 osmo_mslookup_result_name_c(ctx, &q, &r));
306
307 rc = 1;
308 sock = osmo_mdns_sock_init(ctx, cmdline_opts.mdns_addr.ip, cmdline_opts.mdns_addr.port,
309 cb_doing_nothing, NULL, 0);
310 if (!sock) {
311 print_error("unable to open mDNS socket\n");
312 goto exit_cleanup;
313 }
314
315 struct msgb *msg = osmo_mdns_result_encode(ctx, 0, &q, &r, cmdline_opts.mdns_domain_suffix);
316 if (!msg) {
317 print_error("unable to encode mDNS response\n");
Oliver Smith544b15d2020-01-13 15:02:59 +0100318 goto exit_cleanup_sock;
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +0100319 }
320
321 if (osmo_mdns_sock_send(sock, msg)) {
322 print_error("unable to send mDNS message\n");
Oliver Smith544b15d2020-01-13 15:02:59 +0100323 goto exit_cleanup_sock;
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +0100324 }
325
326 rc = 0;
Oliver Smith544b15d2020-01-13 15:02:59 +0100327exit_cleanup_sock:
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +0100328 osmo_mdns_sock_cleanup(sock);
Oliver Smith544b15d2020-01-13 15:02:59 +0100329exit_cleanup:
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +0100330 talloc_free(ctx);
331 return rc;
332}
333
334static struct {
335 void *ctx;
336 unsigned int requests_handled;
337 struct osmo_fd socket_ofd;
338 struct osmo_mslookup_client *mslookup_client;
339 struct llist_head queries;
340 struct llist_head socket_clients;
341 enum result_format format;
342} globals = {
343 .queries = LLIST_HEAD_INIT(globals.queries),
344 .socket_clients = LLIST_HEAD_INIT(globals.socket_clients),
345};
346
347typedef void (*formatter_t)(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r);
348
349void formatter_csv(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r)
350{
351 struct osmo_strbuf sb = { .buf=buf, .len=buflen };
352 OSMO_STRBUF_PRINTF(sb, "%s", query_str);
353
354 if (!r)
355 OSMO_STRBUF_PRINTF(sb, "\tERROR\t\t\t\t\t\t");
356 else {
357 OSMO_STRBUF_PRINTF(sb, "\t%s", osmo_mslookup_result_code_name(r->rc));
358 OSMO_STRBUF_PRINTF(sb, "\t%s", r->last ? "last" : "not-last");
359 OSMO_STRBUF_PRINTF(sb, "\t%u", r->age);
360 switch (r->rc) {
361 case OSMO_MSLOOKUP_RC_RESULT:
362 if (osmo_sockaddr_str_is_nonzero(&r->host_v4))
363 OSMO_STRBUF_PRINTF(sb, "\t%s\t%u", r->host_v4.ip, r->host_v4.port);
364 else
365 OSMO_STRBUF_PRINTF(sb, "\t\t");
366 if (osmo_sockaddr_str_is_nonzero(&r->host_v6))
367 OSMO_STRBUF_PRINTF(sb, "\t%s\t%u", r->host_v6.ip, r->host_v6.port);
368 else
369 OSMO_STRBUF_PRINTF(sb, "\t\t");
370 break;
371 default:
372 OSMO_STRBUF_PRINTF(sb, "\t\t\t\t\t");
373 break;
374 }
375 }
376}
377
378void formatter_json(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r)
379{
380 struct osmo_strbuf sb = { .buf=buf, .len=buflen };
381 OSMO_STRBUF_PRINTF(sb, "{\"query\": \"%s\"", query_str);
382
383 if (!r)
384 OSMO_STRBUF_PRINTF(sb, ", \"result\": \"ERROR\"");
385 else {
386 OSMO_STRBUF_PRINTF(sb, ", \"result\": \"%s\"", osmo_mslookup_result_code_name(r->rc));
387 OSMO_STRBUF_PRINTF(sb, ", \"last\": %s", r->last ? "true" : "false");
388 OSMO_STRBUF_PRINTF(sb, ", \"age\": %u", r->age);
389 if (r->rc == OSMO_MSLOOKUP_RC_RESULT) {
390 if (osmo_sockaddr_str_is_nonzero(&r->host_v4))
391 OSMO_STRBUF_PRINTF(sb, ", \"v4\": [\"%s\", \"%u\"]", r->host_v4.ip, r->host_v4.port);
392 if (osmo_sockaddr_str_is_nonzero(&r->host_v6))
393 OSMO_STRBUF_PRINTF(sb, ", \"v6\": [\"%s\", \"%u\"]", r->host_v6.ip, r->host_v6.port);
394 }
395 }
396 OSMO_STRBUF_PRINTF(sb, "}");
397}
398
399formatter_t formatters[] = {
400 [FORMAT_CSV] = formatter_csv,
401 [FORMAT_JSON] = formatter_json,
402};
403
404void respond_str_stdout(const char *str) {
405 fprintf(stdout, "%s\n", str);
406 fflush(stdout);
407}
408
409void start_query_str(const char *query_str);
410void start_query_strs(char **query_strs, size_t query_strs_len);
411
412struct socket_client {
413 struct llist_head entry;
414 struct osmo_fd ofd;
415 char query_str[1024];
416};
417
418static void socket_client_close(struct socket_client *c)
419{
420 struct osmo_fd *ofd = &c->ofd;
421
422 close(ofd->fd);
423 ofd->fd = -1;
424 osmo_fd_unregister(ofd);
425
426 llist_del(&c->entry);
427 talloc_free(c);
428}
429
430void socket_client_respond_result(struct socket_client *c, const char *response)
431{
432 write(c->ofd.fd, response, strlen(response));
433}
434
435static int socket_read_cb(struct osmo_fd *ofd)
436{
437 struct socket_client *c = ofd->data;
438 int rc;
439 char rxbuf[1024];
440 char *query_with_timeout;
441 char *query_str;
442 char *at;
443
444 rc = recv(ofd->fd, rxbuf, sizeof(rxbuf), 0);
445 if (rc == 0)
446 goto close;
447
448 if (rc < 0) {
449 if (errno == EAGAIN)
450 return 0;
451 goto close;
452 }
453
454 if (rc >= sizeof(c->query_str))
455 goto close;
456
457 rxbuf[rc] = '\0';
458 query_with_timeout = strtok(rxbuf, "\r\n");
459 at = strchr(query_with_timeout, '@');
460 query_str = at ? at + 1 : query_with_timeout;
461
462 if (c->query_str[0]) {
463 print_error("ERROR: Only one query per client connect is allowed;"
464 " received '%s' and '%s' on the same connection\n",
465 c->query_str, query_str);
466 formatters[globals.format](g_buf, sizeof(g_buf), query_str, NULL);
467 socket_client_respond_result(c, g_buf);
468 return 0;
469 }
470
471 OSMO_STRLCPY_ARRAY(c->query_str, query_str);
472 start_query_str(query_with_timeout);
473 printf("query: %s\n", query_with_timeout);
474 return rc;
475
476close:
477 socket_client_close(c);
478 return -1;
479}
480
481static int socket_cb(struct osmo_fd *ofd, unsigned int flags)
482{
483 int rc = 0;
484
485 if (flags & BSC_FD_READ)
486 rc = socket_read_cb(ofd);
487 if (rc < 0)
488 return rc;
489
490 return rc;
491}
492
493int socket_accept(struct osmo_fd *ofd, unsigned int flags)
494{
495 struct socket_client *c;
496 struct sockaddr_un un_addr;
497 socklen_t len;
498 int rc;
499
500 len = sizeof(un_addr);
501 rc = accept(ofd->fd, (struct sockaddr*)&un_addr, &len);
502 if (rc < 0) {
503 print_error("Failed to accept a new connection\n");
504 return -1;
505 }
506
507 c = talloc_zero(globals.ctx, struct socket_client);
508 OSMO_ASSERT(c);
509 c->ofd.fd = rc;
510 c->ofd.when = BSC_FD_READ;
511 c->ofd.cb = socket_cb;
512 c->ofd.data = c;
513
514 if (osmo_fd_register(&c->ofd) != 0) {
515 print_error("Failed to register new connection fd\n");
516 close(c->ofd.fd);
517 c->ofd.fd = -1;
518 talloc_free(c);
519 return -1;
520 }
521
522 llist_add(&c->entry, &globals.socket_clients);
523
524 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
525 write(c->ofd.fd, CSV_HEADERS, strlen(CSV_HEADERS));
526
527 return 0;
528}
529
530int socket_init(const char *sock_path)
531{
532 struct osmo_fd *ofd = &globals.socket_ofd;
533 int rc;
534
535 ofd->fd = osmo_sock_unix_init(SOCK_SEQPACKET, 0, sock_path, OSMO_SOCK_F_BIND);
536 if (ofd->fd < 0) {
537 print_error("Could not create unix socket: %s: %s\n", sock_path, strerror(errno));
538 return -1;
539 }
540
541 ofd->when = BSC_FD_READ;
542 ofd->cb = socket_accept;
543
544 rc = osmo_fd_register(ofd);
545 if (rc < 0) {
546 print_error("Could not register listen fd: %d\n", rc);
547 close(ofd->fd);
548 return rc;
549 }
550 return 0;
551}
552
553void socket_close()
554{
555 struct socket_client *c, *n;
556 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry)
557 socket_client_close(c);
558 if (osmo_fd_is_registered(&globals.socket_ofd)) {
559 close(globals.socket_ofd.fd);
560 globals.socket_ofd.fd = -1;
561 osmo_fd_unregister(&globals.socket_ofd);
562 }
563}
564
565struct query {
566 struct llist_head entry;
567
568 char query_str[128];
569 struct osmo_mslookup_query query;
570 uint32_t handle;
571};
572
573void respond_result(const char *query_str, const struct osmo_mslookup_result *r)
574{
575 struct socket_client *c, *n;
576 formatters[globals.format](g_buf, sizeof(g_buf), query_str, r);
577 respond_str_stdout(g_buf);
578
579 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry) {
580 if (!strcmp(query_str, c->query_str)) {
581 socket_client_respond_result(c, g_buf);
582 if (r->last)
583 socket_client_close(c);
584 }
585 }
586 if (r->last)
587 globals.requests_handled++;
588}
589
590void respond_err(const char *query_str)
591{
592 respond_result(query_str, NULL);
593}
594
595struct query *query_by_handle(uint32_t request_handle)
596{
597 struct query *q;
598 llist_for_each_entry(q, &globals.queries, entry) {
599 if (request_handle == q->handle)
600 return q;
601 }
602 return NULL;
603}
604
605void mslookup_result_cb(struct osmo_mslookup_client *client,
606 uint32_t request_handle,
607 const struct osmo_mslookup_query *query,
608 const struct osmo_mslookup_result *result)
609{
610 struct query *q = query_by_handle(request_handle);
611 if (!q)
612 return;
613 respond_result(q->query_str, result);
614 if (result->last) {
615 llist_del(&q->entry);
616 talloc_free(q);
617 }
618}
619
620void start_query_str(const char *query_str)
621{
622 struct query *q;
623 const char *domain_str = query_str;
624 char *at;
625 struct osmo_mslookup_query_handling h = {
626 .min_wait_milliseconds = cmdline_opts.min_delay,
627 .result_timeout_milliseconds = cmdline_opts.timeout,
628 .result_cb = mslookup_result_cb,
629 };
630
631 at = strchr(query_str, '@');
632 if (at) {
633 int rc;
634 char timeouts[16];
635 char *dash;
636 char *timeout;
637
638 domain_str = at + 1;
639
640 h.min_wait_milliseconds = h.result_timeout_milliseconds = 0;
641
642 if (osmo_print_n(timeouts, sizeof(timeouts), query_str, at - query_str) >= sizeof(timeouts)) {
643 print_error("ERROR: timeouts part too long in query string\n");
644 respond_err(domain_str);
645 return;
646 }
647
648 dash = strchr(timeouts, '-');
649 if (dash) {
650 char min_delay[16];
651 osmo_print_n(min_delay, sizeof(min_delay), timeouts, dash - timeouts);
652 h.min_wait_milliseconds = parse_int(0, UINT32_MAX, min_delay, &rc);
653 if (rc) {
654 print_error("ERROR: invalid min-delay number: %s\n", min_delay);
655 respond_err(domain_str);
656 return;
657 }
658 timeout = dash + 1;
659 } else {
660 timeout = timeouts;
661 }
662 if (*timeout) {
663 h.result_timeout_milliseconds = parse_int(0, UINT32_MAX, timeout, &rc);
664 if (rc) {
665 print_error("ERROR: invalid timeout number: %s\n", timeout);
666 respond_err(domain_str);
667 return;
668 }
669 }
670 }
671
672 if (strlen(domain_str) >= sizeof(q->query_str)) {
673 print_error("ERROR: query string is too long: '%s'\n", domain_str);
674 respond_err(domain_str);
675 return;
676 }
677
678 q = talloc_zero(globals.ctx, struct query);
679 OSMO_ASSERT(q);
680 OSMO_STRLCPY_ARRAY(q->query_str, domain_str);
681
682 if (osmo_mslookup_query_init_from_domain_str(&q->query, q->query_str)) {
683 print_error("ERROR: cannot parse query string: '%s'\n", domain_str);
684 respond_err(domain_str);
685 talloc_free(q);
686 return;
687 }
688
689 q->handle = osmo_mslookup_client_request(globals.mslookup_client, &q->query, &h);
690 if (!q->handle) {
691 print_error("ERROR: cannot send query: '%s'\n", domain_str);
692 respond_err(domain_str);
693 talloc_free(q);
694 return;
695 }
696
697 llist_add(&q->entry, &globals.queries);
698}
699
700void start_query_strs(char **query_strs, size_t query_strs_len)
701{
702 int i;
703 for (i = 0; i < query_strs_len; i++)
704 start_query_str(query_strs[i]);
705}
706
707int main(int argc, char **argv)
708{
709 int rc = EXIT_FAILURE;
710 globals.ctx = talloc_named_const(NULL, 0, "osmo-mslookup-client");
711
712 osmo_init_logging2(globals.ctx, NULL);
713 log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
714 log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END);
715 log_set_print_level(osmo_stderr_target, 1);
716 log_set_print_category(osmo_stderr_target, 1);
717 log_set_print_category_hex(osmo_stderr_target, 0);
718 log_set_print_extended_timestamp(osmo_stderr_target, 1);
719 log_set_use_color(osmo_stderr_target, 0);
720
721 while (1) {
722 int c;
723 long long int val;
724 char *endptr;
725 int option_index = 0;
726
727 static struct option long_options[] = {
728 { "format", 1, 0, 'f' },
729 { "no-csv-headers", 0, 0, 'H' },
730 { "daemon", 0, 0, 'd' },
731 { "mdns-ip", 1, 0, 'm' },
732 { "mdns-port", 1, 0, 'M' },
733 { "mdns-domain-suffix", 1, 0, 'D' },
734 { "timeout", 1, 0, 'T' },
735 { "min-delay", 1, 0, 't' },
736 { "socket", 1, 0, 's' },
737 { "send", 0, 0, 'S' },
738 { "quiet", 0, 0, 'q' },
739 { "help", 0, 0, 'h' },
740 { "version", 0, 0, 'V' },
741 {}
742 };
743
744#define PARSE_INT(TARGET, MINVAL, MAXVAL) do { \
745 int _rc; \
746 TARGET = parse_int(MINVAL, MAXVAL, optarg, &_rc); \
747 if (_rc) { \
748 print_error("Invalid " #TARGET ": %s\n", optarg); \
749 goto program_exit; \
750 } \
751 } while (0)
752
753 c = getopt_long(argc, argv, "f:Hdm:M:D:t:T:s:SqhV", long_options, &option_index);
754
755 if (c == -1)
756 break;
757
758 switch (c) {
759 case 'f':
760 cmdline_opts.format_str = optarg;
761 break;
762 case 'H':
763 cmdline_opts.csv_headers = false;
764 break;
765 case 'd':
766 cmdline_opts.daemon = true;
767 break;
768 case 'm':
769 if (osmo_sockaddr_str_from_str(&cmdline_opts.mdns_addr, optarg, cmdline_opts.mdns_addr.port)
770 || !osmo_sockaddr_str_is_nonzero(&cmdline_opts.mdns_addr)) {
771 print_error("Invalid mDNS IP address: %s\n", optarg);
772 goto program_exit;
773 }
774 break;
775 case 'M':
776 PARSE_INT(cmdline_opts.mdns_addr.port, 1, 65535);
777 break;
778 case 'D':
779 cmdline_opts.mdns_domain_suffix = optarg;
780 break;
781 case 't':
782 PARSE_INT(cmdline_opts.min_delay, 0, UINT32_MAX);
783 break;
784 case 'T':
785 PARSE_INT(cmdline_opts.timeout, 0, UINT32_MAX);
786 break;
787 case 's':
788 cmdline_opts.socket_path = optarg;
789 break;
790 case 'S':
791 cmdline_opts.send = true;
792 break;
793 case 'q':
794 cmdline_opts.quiet = true;
795 break;
796
797 case 'h':
798 print_help();
799 rc = 0;
800 goto program_exit;
801 case 'V':
802 print_version();
803 rc = 0;
804 goto program_exit;
805
806 default:
807 /* catch unknown options *as well as* missing arguments. */
808 print_error("Error in command line options. Exiting.\n");
809 goto program_exit;
810 }
811 }
812
813 if (cmdline_opts.send) {
814 if (cmdline_opts.daemon || cmdline_opts.format_str || cmdline_opts.socket_path) {
815 print_error("--send option cannot have any listening related args.");
816 }
817 rc = do_send(argc - optind, argv + optind);
818 goto program_exit;
819 }
820
821 if (!cmdline_opts.daemon && !(argc - optind)) {
822 print_help();
823 goto program_exit;
824 }
825
826 if (cmdline_opts.daemon && !cmdline_opts.timeout) {
827 print_error("In daemon mode, --timeout must not be zero.\n");
828 goto program_exit;
829 }
830
831 if (cmdline_opts.quiet)
832 log_target_destroy(osmo_stderr_target);
833
834 if (cmdline_opts.format_str) {
835 if (osmo_str_startswith("json", cmdline_opts.format_str))
836 globals.format = FORMAT_JSON;
837 else if (osmo_str_startswith("csv", cmdline_opts.format_str))
838 globals.format = FORMAT_CSV;
839 else {
840 print_error("Invalid format: %s\n", cmdline_opts.format_str);
841 goto program_exit;
842 }
843 }
844
845 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
846 respond_str_stdout(CSV_HEADERS);
847
848 globals.mslookup_client = osmo_mslookup_client_new(globals.ctx);
849 if (!globals.mslookup_client
850 || !osmo_mslookup_client_add_mdns(globals.mslookup_client,
851 cmdline_opts.mdns_addr.ip, cmdline_opts.mdns_addr.port,
852 -1, cmdline_opts.mdns_domain_suffix)) {
853 print_error("Failed to start mDNS client\n");
854 goto program_exit;
855 }
856
857 if (cmdline_opts.socket_path) {
858 if (socket_init(cmdline_opts.socket_path))
859 goto program_exit;
860 }
861
862 start_query_strs(&argv[optind], argc - optind);
863
864 while (1) {
865 osmo_select_main_ctx(0);
866
867 if (!cmdline_opts.daemon
868 && globals.requests_handled
869 && llist_empty(&globals.queries))
870 break;
871 }
872
873 rc = 0;
874program_exit:
875 osmo_mslookup_client_free(globals.mslookup_client);
876 socket_close();
877 log_fini();
878 talloc_free(globals.ctx);
879 return rc;
880}