blob: 4a9e587d97241b0c775e2fbeec4423bf658bbd54 [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");
Oliver Smith9e533f62020-01-13 15:11:53 +0100459 if (!query_with_timeout) {
460 print_error("ERROR: failed to read line from socket\n");
461 goto close;
462 }
463
Neels Hofmeyr52ef60f2019-11-20 12:37:41 +0100464 at = strchr(query_with_timeout, '@');
465 query_str = at ? at + 1 : query_with_timeout;
466
467 if (c->query_str[0]) {
468 print_error("ERROR: Only one query per client connect is allowed;"
469 " received '%s' and '%s' on the same connection\n",
470 c->query_str, query_str);
471 formatters[globals.format](g_buf, sizeof(g_buf), query_str, NULL);
472 socket_client_respond_result(c, g_buf);
473 return 0;
474 }
475
476 OSMO_STRLCPY_ARRAY(c->query_str, query_str);
477 start_query_str(query_with_timeout);
478 printf("query: %s\n", query_with_timeout);
479 return rc;
480
481close:
482 socket_client_close(c);
483 return -1;
484}
485
486static int socket_cb(struct osmo_fd *ofd, unsigned int flags)
487{
488 int rc = 0;
489
490 if (flags & BSC_FD_READ)
491 rc = socket_read_cb(ofd);
492 if (rc < 0)
493 return rc;
494
495 return rc;
496}
497
498int socket_accept(struct osmo_fd *ofd, unsigned int flags)
499{
500 struct socket_client *c;
501 struct sockaddr_un un_addr;
502 socklen_t len;
503 int rc;
504
505 len = sizeof(un_addr);
506 rc = accept(ofd->fd, (struct sockaddr*)&un_addr, &len);
507 if (rc < 0) {
508 print_error("Failed to accept a new connection\n");
509 return -1;
510 }
511
512 c = talloc_zero(globals.ctx, struct socket_client);
513 OSMO_ASSERT(c);
514 c->ofd.fd = rc;
515 c->ofd.when = BSC_FD_READ;
516 c->ofd.cb = socket_cb;
517 c->ofd.data = c;
518
519 if (osmo_fd_register(&c->ofd) != 0) {
520 print_error("Failed to register new connection fd\n");
521 close(c->ofd.fd);
522 c->ofd.fd = -1;
523 talloc_free(c);
524 return -1;
525 }
526
527 llist_add(&c->entry, &globals.socket_clients);
528
529 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
530 write(c->ofd.fd, CSV_HEADERS, strlen(CSV_HEADERS));
531
532 return 0;
533}
534
535int socket_init(const char *sock_path)
536{
537 struct osmo_fd *ofd = &globals.socket_ofd;
538 int rc;
539
540 ofd->fd = osmo_sock_unix_init(SOCK_SEQPACKET, 0, sock_path, OSMO_SOCK_F_BIND);
541 if (ofd->fd < 0) {
542 print_error("Could not create unix socket: %s: %s\n", sock_path, strerror(errno));
543 return -1;
544 }
545
546 ofd->when = BSC_FD_READ;
547 ofd->cb = socket_accept;
548
549 rc = osmo_fd_register(ofd);
550 if (rc < 0) {
551 print_error("Could not register listen fd: %d\n", rc);
552 close(ofd->fd);
553 return rc;
554 }
555 return 0;
556}
557
558void socket_close()
559{
560 struct socket_client *c, *n;
561 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry)
562 socket_client_close(c);
563 if (osmo_fd_is_registered(&globals.socket_ofd)) {
564 close(globals.socket_ofd.fd);
565 globals.socket_ofd.fd = -1;
566 osmo_fd_unregister(&globals.socket_ofd);
567 }
568}
569
570struct query {
571 struct llist_head entry;
572
573 char query_str[128];
574 struct osmo_mslookup_query query;
575 uint32_t handle;
576};
577
578void respond_result(const char *query_str, const struct osmo_mslookup_result *r)
579{
580 struct socket_client *c, *n;
581 formatters[globals.format](g_buf, sizeof(g_buf), query_str, r);
582 respond_str_stdout(g_buf);
583
584 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry) {
585 if (!strcmp(query_str, c->query_str)) {
586 socket_client_respond_result(c, g_buf);
587 if (r->last)
588 socket_client_close(c);
589 }
590 }
591 if (r->last)
592 globals.requests_handled++;
593}
594
595void respond_err(const char *query_str)
596{
597 respond_result(query_str, NULL);
598}
599
600struct query *query_by_handle(uint32_t request_handle)
601{
602 struct query *q;
603 llist_for_each_entry(q, &globals.queries, entry) {
604 if (request_handle == q->handle)
605 return q;
606 }
607 return NULL;
608}
609
610void mslookup_result_cb(struct osmo_mslookup_client *client,
611 uint32_t request_handle,
612 const struct osmo_mslookup_query *query,
613 const struct osmo_mslookup_result *result)
614{
615 struct query *q = query_by_handle(request_handle);
616 if (!q)
617 return;
618 respond_result(q->query_str, result);
619 if (result->last) {
620 llist_del(&q->entry);
621 talloc_free(q);
622 }
623}
624
625void start_query_str(const char *query_str)
626{
627 struct query *q;
628 const char *domain_str = query_str;
629 char *at;
630 struct osmo_mslookup_query_handling h = {
631 .min_wait_milliseconds = cmdline_opts.min_delay,
632 .result_timeout_milliseconds = cmdline_opts.timeout,
633 .result_cb = mslookup_result_cb,
634 };
635
636 at = strchr(query_str, '@');
637 if (at) {
638 int rc;
639 char timeouts[16];
640 char *dash;
641 char *timeout;
642
643 domain_str = at + 1;
644
645 h.min_wait_milliseconds = h.result_timeout_milliseconds = 0;
646
647 if (osmo_print_n(timeouts, sizeof(timeouts), query_str, at - query_str) >= sizeof(timeouts)) {
648 print_error("ERROR: timeouts part too long in query string\n");
649 respond_err(domain_str);
650 return;
651 }
652
653 dash = strchr(timeouts, '-');
654 if (dash) {
655 char min_delay[16];
656 osmo_print_n(min_delay, sizeof(min_delay), timeouts, dash - timeouts);
657 h.min_wait_milliseconds = parse_int(0, UINT32_MAX, min_delay, &rc);
658 if (rc) {
659 print_error("ERROR: invalid min-delay number: %s\n", min_delay);
660 respond_err(domain_str);
661 return;
662 }
663 timeout = dash + 1;
664 } else {
665 timeout = timeouts;
666 }
667 if (*timeout) {
668 h.result_timeout_milliseconds = parse_int(0, UINT32_MAX, timeout, &rc);
669 if (rc) {
670 print_error("ERROR: invalid timeout number: %s\n", timeout);
671 respond_err(domain_str);
672 return;
673 }
674 }
675 }
676
677 if (strlen(domain_str) >= sizeof(q->query_str)) {
678 print_error("ERROR: query string is too long: '%s'\n", domain_str);
679 respond_err(domain_str);
680 return;
681 }
682
683 q = talloc_zero(globals.ctx, struct query);
684 OSMO_ASSERT(q);
685 OSMO_STRLCPY_ARRAY(q->query_str, domain_str);
686
687 if (osmo_mslookup_query_init_from_domain_str(&q->query, q->query_str)) {
688 print_error("ERROR: cannot parse query string: '%s'\n", domain_str);
689 respond_err(domain_str);
690 talloc_free(q);
691 return;
692 }
693
694 q->handle = osmo_mslookup_client_request(globals.mslookup_client, &q->query, &h);
695 if (!q->handle) {
696 print_error("ERROR: cannot send query: '%s'\n", domain_str);
697 respond_err(domain_str);
698 talloc_free(q);
699 return;
700 }
701
702 llist_add(&q->entry, &globals.queries);
703}
704
705void start_query_strs(char **query_strs, size_t query_strs_len)
706{
707 int i;
708 for (i = 0; i < query_strs_len; i++)
709 start_query_str(query_strs[i]);
710}
711
712int main(int argc, char **argv)
713{
714 int rc = EXIT_FAILURE;
715 globals.ctx = talloc_named_const(NULL, 0, "osmo-mslookup-client");
716
717 osmo_init_logging2(globals.ctx, NULL);
718 log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
719 log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END);
720 log_set_print_level(osmo_stderr_target, 1);
721 log_set_print_category(osmo_stderr_target, 1);
722 log_set_print_category_hex(osmo_stderr_target, 0);
723 log_set_print_extended_timestamp(osmo_stderr_target, 1);
724 log_set_use_color(osmo_stderr_target, 0);
725
726 while (1) {
727 int c;
728 long long int val;
729 char *endptr;
730 int option_index = 0;
731
732 static struct option long_options[] = {
733 { "format", 1, 0, 'f' },
734 { "no-csv-headers", 0, 0, 'H' },
735 { "daemon", 0, 0, 'd' },
736 { "mdns-ip", 1, 0, 'm' },
737 { "mdns-port", 1, 0, 'M' },
738 { "mdns-domain-suffix", 1, 0, 'D' },
739 { "timeout", 1, 0, 'T' },
740 { "min-delay", 1, 0, 't' },
741 { "socket", 1, 0, 's' },
742 { "send", 0, 0, 'S' },
743 { "quiet", 0, 0, 'q' },
744 { "help", 0, 0, 'h' },
745 { "version", 0, 0, 'V' },
746 {}
747 };
748
749#define PARSE_INT(TARGET, MINVAL, MAXVAL) do { \
750 int _rc; \
751 TARGET = parse_int(MINVAL, MAXVAL, optarg, &_rc); \
752 if (_rc) { \
753 print_error("Invalid " #TARGET ": %s\n", optarg); \
754 goto program_exit; \
755 } \
756 } while (0)
757
758 c = getopt_long(argc, argv, "f:Hdm:M:D:t:T:s:SqhV", long_options, &option_index);
759
760 if (c == -1)
761 break;
762
763 switch (c) {
764 case 'f':
765 cmdline_opts.format_str = optarg;
766 break;
767 case 'H':
768 cmdline_opts.csv_headers = false;
769 break;
770 case 'd':
771 cmdline_opts.daemon = true;
772 break;
773 case 'm':
774 if (osmo_sockaddr_str_from_str(&cmdline_opts.mdns_addr, optarg, cmdline_opts.mdns_addr.port)
775 || !osmo_sockaddr_str_is_nonzero(&cmdline_opts.mdns_addr)) {
776 print_error("Invalid mDNS IP address: %s\n", optarg);
777 goto program_exit;
778 }
779 break;
780 case 'M':
781 PARSE_INT(cmdline_opts.mdns_addr.port, 1, 65535);
782 break;
783 case 'D':
784 cmdline_opts.mdns_domain_suffix = optarg;
785 break;
786 case 't':
787 PARSE_INT(cmdline_opts.min_delay, 0, UINT32_MAX);
788 break;
789 case 'T':
790 PARSE_INT(cmdline_opts.timeout, 0, UINT32_MAX);
791 break;
792 case 's':
793 cmdline_opts.socket_path = optarg;
794 break;
795 case 'S':
796 cmdline_opts.send = true;
797 break;
798 case 'q':
799 cmdline_opts.quiet = true;
800 break;
801
802 case 'h':
803 print_help();
804 rc = 0;
805 goto program_exit;
806 case 'V':
807 print_version();
808 rc = 0;
809 goto program_exit;
810
811 default:
812 /* catch unknown options *as well as* missing arguments. */
813 print_error("Error in command line options. Exiting.\n");
814 goto program_exit;
815 }
816 }
817
818 if (cmdline_opts.send) {
819 if (cmdline_opts.daemon || cmdline_opts.format_str || cmdline_opts.socket_path) {
820 print_error("--send option cannot have any listening related args.");
821 }
822 rc = do_send(argc - optind, argv + optind);
823 goto program_exit;
824 }
825
826 if (!cmdline_opts.daemon && !(argc - optind)) {
827 print_help();
828 goto program_exit;
829 }
830
831 if (cmdline_opts.daemon && !cmdline_opts.timeout) {
832 print_error("In daemon mode, --timeout must not be zero.\n");
833 goto program_exit;
834 }
835
836 if (cmdline_opts.quiet)
837 log_target_destroy(osmo_stderr_target);
838
839 if (cmdline_opts.format_str) {
840 if (osmo_str_startswith("json", cmdline_opts.format_str))
841 globals.format = FORMAT_JSON;
842 else if (osmo_str_startswith("csv", cmdline_opts.format_str))
843 globals.format = FORMAT_CSV;
844 else {
845 print_error("Invalid format: %s\n", cmdline_opts.format_str);
846 goto program_exit;
847 }
848 }
849
850 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
851 respond_str_stdout(CSV_HEADERS);
852
853 globals.mslookup_client = osmo_mslookup_client_new(globals.ctx);
854 if (!globals.mslookup_client
855 || !osmo_mslookup_client_add_mdns(globals.mslookup_client,
856 cmdline_opts.mdns_addr.ip, cmdline_opts.mdns_addr.port,
857 -1, cmdline_opts.mdns_domain_suffix)) {
858 print_error("Failed to start mDNS client\n");
859 goto program_exit;
860 }
861
862 if (cmdline_opts.socket_path) {
863 if (socket_init(cmdline_opts.socket_path))
864 goto program_exit;
865 }
866
867 start_query_strs(&argv[optind], argc - optind);
868
869 while (1) {
870 osmo_select_main_ctx(0);
871
872 if (!cmdline_opts.daemon
873 && globals.requests_handled
874 && llist_empty(&globals.queries))
875 break;
876 }
877
878 rc = 0;
879program_exit:
880 osmo_mslookup_client_free(globals.mslookup_client);
881 socket_close();
882 log_fini();
883 talloc_free(globals.ctx);
884 return rc;
885}