blob: 1c0efe971536d261d6b71232ed93ac8c84748c82 [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");
318 goto exit_cleanup;
319 }
320
321 if (osmo_mdns_sock_send(sock, msg)) {
322 print_error("unable to send mDNS message\n");
323 goto exit_cleanup;
324 }
325
326 rc = 0;
327exit_cleanup:
328 osmo_mdns_sock_cleanup(sock);
329 talloc_free(ctx);
330 return rc;
331}
332
333static struct {
334 void *ctx;
335 unsigned int requests_handled;
336 struct osmo_fd socket_ofd;
337 struct osmo_mslookup_client *mslookup_client;
338 struct llist_head queries;
339 struct llist_head socket_clients;
340 enum result_format format;
341} globals = {
342 .queries = LLIST_HEAD_INIT(globals.queries),
343 .socket_clients = LLIST_HEAD_INIT(globals.socket_clients),
344};
345
346typedef void (*formatter_t)(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r);
347
348void formatter_csv(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r)
349{
350 struct osmo_strbuf sb = { .buf=buf, .len=buflen };
351 OSMO_STRBUF_PRINTF(sb, "%s", query_str);
352
353 if (!r)
354 OSMO_STRBUF_PRINTF(sb, "\tERROR\t\t\t\t\t\t");
355 else {
356 OSMO_STRBUF_PRINTF(sb, "\t%s", osmo_mslookup_result_code_name(r->rc));
357 OSMO_STRBUF_PRINTF(sb, "\t%s", r->last ? "last" : "not-last");
358 OSMO_STRBUF_PRINTF(sb, "\t%u", r->age);
359 switch (r->rc) {
360 case OSMO_MSLOOKUP_RC_RESULT:
361 if (osmo_sockaddr_str_is_nonzero(&r->host_v4))
362 OSMO_STRBUF_PRINTF(sb, "\t%s\t%u", r->host_v4.ip, r->host_v4.port);
363 else
364 OSMO_STRBUF_PRINTF(sb, "\t\t");
365 if (osmo_sockaddr_str_is_nonzero(&r->host_v6))
366 OSMO_STRBUF_PRINTF(sb, "\t%s\t%u", r->host_v6.ip, r->host_v6.port);
367 else
368 OSMO_STRBUF_PRINTF(sb, "\t\t");
369 break;
370 default:
371 OSMO_STRBUF_PRINTF(sb, "\t\t\t\t\t");
372 break;
373 }
374 }
375}
376
377void formatter_json(char *buf, size_t buflen, const char *query_str, const struct osmo_mslookup_result *r)
378{
379 struct osmo_strbuf sb = { .buf=buf, .len=buflen };
380 OSMO_STRBUF_PRINTF(sb, "{\"query\": \"%s\"", query_str);
381
382 if (!r)
383 OSMO_STRBUF_PRINTF(sb, ", \"result\": \"ERROR\"");
384 else {
385 OSMO_STRBUF_PRINTF(sb, ", \"result\": \"%s\"", osmo_mslookup_result_code_name(r->rc));
386 OSMO_STRBUF_PRINTF(sb, ", \"last\": %s", r->last ? "true" : "false");
387 OSMO_STRBUF_PRINTF(sb, ", \"age\": %u", r->age);
388 if (r->rc == OSMO_MSLOOKUP_RC_RESULT) {
389 if (osmo_sockaddr_str_is_nonzero(&r->host_v4))
390 OSMO_STRBUF_PRINTF(sb, ", \"v4\": [\"%s\", \"%u\"]", r->host_v4.ip, r->host_v4.port);
391 if (osmo_sockaddr_str_is_nonzero(&r->host_v6))
392 OSMO_STRBUF_PRINTF(sb, ", \"v6\": [\"%s\", \"%u\"]", r->host_v6.ip, r->host_v6.port);
393 }
394 }
395 OSMO_STRBUF_PRINTF(sb, "}");
396}
397
398formatter_t formatters[] = {
399 [FORMAT_CSV] = formatter_csv,
400 [FORMAT_JSON] = formatter_json,
401};
402
403void respond_str_stdout(const char *str) {
404 fprintf(stdout, "%s\n", str);
405 fflush(stdout);
406}
407
408void start_query_str(const char *query_str);
409void start_query_strs(char **query_strs, size_t query_strs_len);
410
411struct socket_client {
412 struct llist_head entry;
413 struct osmo_fd ofd;
414 char query_str[1024];
415};
416
417static void socket_client_close(struct socket_client *c)
418{
419 struct osmo_fd *ofd = &c->ofd;
420
421 close(ofd->fd);
422 ofd->fd = -1;
423 osmo_fd_unregister(ofd);
424
425 llist_del(&c->entry);
426 talloc_free(c);
427}
428
429void socket_client_respond_result(struct socket_client *c, const char *response)
430{
431 write(c->ofd.fd, response, strlen(response));
432}
433
434static int socket_read_cb(struct osmo_fd *ofd)
435{
436 struct socket_client *c = ofd->data;
437 int rc;
438 char rxbuf[1024];
439 char *query_with_timeout;
440 char *query_str;
441 char *at;
442
443 rc = recv(ofd->fd, rxbuf, sizeof(rxbuf), 0);
444 if (rc == 0)
445 goto close;
446
447 if (rc < 0) {
448 if (errno == EAGAIN)
449 return 0;
450 goto close;
451 }
452
453 if (rc >= sizeof(c->query_str))
454 goto close;
455
456 rxbuf[rc] = '\0';
457 query_with_timeout = strtok(rxbuf, "\r\n");
458 at = strchr(query_with_timeout, '@');
459 query_str = at ? at + 1 : query_with_timeout;
460
461 if (c->query_str[0]) {
462 print_error("ERROR: Only one query per client connect is allowed;"
463 " received '%s' and '%s' on the same connection\n",
464 c->query_str, query_str);
465 formatters[globals.format](g_buf, sizeof(g_buf), query_str, NULL);
466 socket_client_respond_result(c, g_buf);
467 return 0;
468 }
469
470 OSMO_STRLCPY_ARRAY(c->query_str, query_str);
471 start_query_str(query_with_timeout);
472 printf("query: %s\n", query_with_timeout);
473 return rc;
474
475close:
476 socket_client_close(c);
477 return -1;
478}
479
480static int socket_cb(struct osmo_fd *ofd, unsigned int flags)
481{
482 int rc = 0;
483
484 if (flags & BSC_FD_READ)
485 rc = socket_read_cb(ofd);
486 if (rc < 0)
487 return rc;
488
489 return rc;
490}
491
492int socket_accept(struct osmo_fd *ofd, unsigned int flags)
493{
494 struct socket_client *c;
495 struct sockaddr_un un_addr;
496 socklen_t len;
497 int rc;
498
499 len = sizeof(un_addr);
500 rc = accept(ofd->fd, (struct sockaddr*)&un_addr, &len);
501 if (rc < 0) {
502 print_error("Failed to accept a new connection\n");
503 return -1;
504 }
505
506 c = talloc_zero(globals.ctx, struct socket_client);
507 OSMO_ASSERT(c);
508 c->ofd.fd = rc;
509 c->ofd.when = BSC_FD_READ;
510 c->ofd.cb = socket_cb;
511 c->ofd.data = c;
512
513 if (osmo_fd_register(&c->ofd) != 0) {
514 print_error("Failed to register new connection fd\n");
515 close(c->ofd.fd);
516 c->ofd.fd = -1;
517 talloc_free(c);
518 return -1;
519 }
520
521 llist_add(&c->entry, &globals.socket_clients);
522
523 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
524 write(c->ofd.fd, CSV_HEADERS, strlen(CSV_HEADERS));
525
526 return 0;
527}
528
529int socket_init(const char *sock_path)
530{
531 struct osmo_fd *ofd = &globals.socket_ofd;
532 int rc;
533
534 ofd->fd = osmo_sock_unix_init(SOCK_SEQPACKET, 0, sock_path, OSMO_SOCK_F_BIND);
535 if (ofd->fd < 0) {
536 print_error("Could not create unix socket: %s: %s\n", sock_path, strerror(errno));
537 return -1;
538 }
539
540 ofd->when = BSC_FD_READ;
541 ofd->cb = socket_accept;
542
543 rc = osmo_fd_register(ofd);
544 if (rc < 0) {
545 print_error("Could not register listen fd: %d\n", rc);
546 close(ofd->fd);
547 return rc;
548 }
549 return 0;
550}
551
552void socket_close()
553{
554 struct socket_client *c, *n;
555 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry)
556 socket_client_close(c);
557 if (osmo_fd_is_registered(&globals.socket_ofd)) {
558 close(globals.socket_ofd.fd);
559 globals.socket_ofd.fd = -1;
560 osmo_fd_unregister(&globals.socket_ofd);
561 }
562}
563
564struct query {
565 struct llist_head entry;
566
567 char query_str[128];
568 struct osmo_mslookup_query query;
569 uint32_t handle;
570};
571
572void respond_result(const char *query_str, const struct osmo_mslookup_result *r)
573{
574 struct socket_client *c, *n;
575 formatters[globals.format](g_buf, sizeof(g_buf), query_str, r);
576 respond_str_stdout(g_buf);
577
578 llist_for_each_entry_safe(c, n, &globals.socket_clients, entry) {
579 if (!strcmp(query_str, c->query_str)) {
580 socket_client_respond_result(c, g_buf);
581 if (r->last)
582 socket_client_close(c);
583 }
584 }
585 if (r->last)
586 globals.requests_handled++;
587}
588
589void respond_err(const char *query_str)
590{
591 respond_result(query_str, NULL);
592}
593
594struct query *query_by_handle(uint32_t request_handle)
595{
596 struct query *q;
597 llist_for_each_entry(q, &globals.queries, entry) {
598 if (request_handle == q->handle)
599 return q;
600 }
601 return NULL;
602}
603
604void mslookup_result_cb(struct osmo_mslookup_client *client,
605 uint32_t request_handle,
606 const struct osmo_mslookup_query *query,
607 const struct osmo_mslookup_result *result)
608{
609 struct query *q = query_by_handle(request_handle);
610 if (!q)
611 return;
612 respond_result(q->query_str, result);
613 if (result->last) {
614 llist_del(&q->entry);
615 talloc_free(q);
616 }
617}
618
619void start_query_str(const char *query_str)
620{
621 struct query *q;
622 const char *domain_str = query_str;
623 char *at;
624 struct osmo_mslookup_query_handling h = {
625 .min_wait_milliseconds = cmdline_opts.min_delay,
626 .result_timeout_milliseconds = cmdline_opts.timeout,
627 .result_cb = mslookup_result_cb,
628 };
629
630 at = strchr(query_str, '@');
631 if (at) {
632 int rc;
633 char timeouts[16];
634 char *dash;
635 char *timeout;
636
637 domain_str = at + 1;
638
639 h.min_wait_milliseconds = h.result_timeout_milliseconds = 0;
640
641 if (osmo_print_n(timeouts, sizeof(timeouts), query_str, at - query_str) >= sizeof(timeouts)) {
642 print_error("ERROR: timeouts part too long in query string\n");
643 respond_err(domain_str);
644 return;
645 }
646
647 dash = strchr(timeouts, '-');
648 if (dash) {
649 char min_delay[16];
650 osmo_print_n(min_delay, sizeof(min_delay), timeouts, dash - timeouts);
651 h.min_wait_milliseconds = parse_int(0, UINT32_MAX, min_delay, &rc);
652 if (rc) {
653 print_error("ERROR: invalid min-delay number: %s\n", min_delay);
654 respond_err(domain_str);
655 return;
656 }
657 timeout = dash + 1;
658 } else {
659 timeout = timeouts;
660 }
661 if (*timeout) {
662 h.result_timeout_milliseconds = parse_int(0, UINT32_MAX, timeout, &rc);
663 if (rc) {
664 print_error("ERROR: invalid timeout number: %s\n", timeout);
665 respond_err(domain_str);
666 return;
667 }
668 }
669 }
670
671 if (strlen(domain_str) >= sizeof(q->query_str)) {
672 print_error("ERROR: query string is too long: '%s'\n", domain_str);
673 respond_err(domain_str);
674 return;
675 }
676
677 q = talloc_zero(globals.ctx, struct query);
678 OSMO_ASSERT(q);
679 OSMO_STRLCPY_ARRAY(q->query_str, domain_str);
680
681 if (osmo_mslookup_query_init_from_domain_str(&q->query, q->query_str)) {
682 print_error("ERROR: cannot parse query string: '%s'\n", domain_str);
683 respond_err(domain_str);
684 talloc_free(q);
685 return;
686 }
687
688 q->handle = osmo_mslookup_client_request(globals.mslookup_client, &q->query, &h);
689 if (!q->handle) {
690 print_error("ERROR: cannot send query: '%s'\n", domain_str);
691 respond_err(domain_str);
692 talloc_free(q);
693 return;
694 }
695
696 llist_add(&q->entry, &globals.queries);
697}
698
699void start_query_strs(char **query_strs, size_t query_strs_len)
700{
701 int i;
702 for (i = 0; i < query_strs_len; i++)
703 start_query_str(query_strs[i]);
704}
705
706int main(int argc, char **argv)
707{
708 int rc = EXIT_FAILURE;
709 globals.ctx = talloc_named_const(NULL, 0, "osmo-mslookup-client");
710
711 osmo_init_logging2(globals.ctx, NULL);
712 log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
713 log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END);
714 log_set_print_level(osmo_stderr_target, 1);
715 log_set_print_category(osmo_stderr_target, 1);
716 log_set_print_category_hex(osmo_stderr_target, 0);
717 log_set_print_extended_timestamp(osmo_stderr_target, 1);
718 log_set_use_color(osmo_stderr_target, 0);
719
720 while (1) {
721 int c;
722 long long int val;
723 char *endptr;
724 int option_index = 0;
725
726 static struct option long_options[] = {
727 { "format", 1, 0, 'f' },
728 { "no-csv-headers", 0, 0, 'H' },
729 { "daemon", 0, 0, 'd' },
730 { "mdns-ip", 1, 0, 'm' },
731 { "mdns-port", 1, 0, 'M' },
732 { "mdns-domain-suffix", 1, 0, 'D' },
733 { "timeout", 1, 0, 'T' },
734 { "min-delay", 1, 0, 't' },
735 { "socket", 1, 0, 's' },
736 { "send", 0, 0, 'S' },
737 { "quiet", 0, 0, 'q' },
738 { "help", 0, 0, 'h' },
739 { "version", 0, 0, 'V' },
740 {}
741 };
742
743#define PARSE_INT(TARGET, MINVAL, MAXVAL) do { \
744 int _rc; \
745 TARGET = parse_int(MINVAL, MAXVAL, optarg, &_rc); \
746 if (_rc) { \
747 print_error("Invalid " #TARGET ": %s\n", optarg); \
748 goto program_exit; \
749 } \
750 } while (0)
751
752 c = getopt_long(argc, argv, "f:Hdm:M:D:t:T:s:SqhV", long_options, &option_index);
753
754 if (c == -1)
755 break;
756
757 switch (c) {
758 case 'f':
759 cmdline_opts.format_str = optarg;
760 break;
761 case 'H':
762 cmdline_opts.csv_headers = false;
763 break;
764 case 'd':
765 cmdline_opts.daemon = true;
766 break;
767 case 'm':
768 if (osmo_sockaddr_str_from_str(&cmdline_opts.mdns_addr, optarg, cmdline_opts.mdns_addr.port)
769 || !osmo_sockaddr_str_is_nonzero(&cmdline_opts.mdns_addr)) {
770 print_error("Invalid mDNS IP address: %s\n", optarg);
771 goto program_exit;
772 }
773 break;
774 case 'M':
775 PARSE_INT(cmdline_opts.mdns_addr.port, 1, 65535);
776 break;
777 case 'D':
778 cmdline_opts.mdns_domain_suffix = optarg;
779 break;
780 case 't':
781 PARSE_INT(cmdline_opts.min_delay, 0, UINT32_MAX);
782 break;
783 case 'T':
784 PARSE_INT(cmdline_opts.timeout, 0, UINT32_MAX);
785 break;
786 case 's':
787 cmdline_opts.socket_path = optarg;
788 break;
789 case 'S':
790 cmdline_opts.send = true;
791 break;
792 case 'q':
793 cmdline_opts.quiet = true;
794 break;
795
796 case 'h':
797 print_help();
798 rc = 0;
799 goto program_exit;
800 case 'V':
801 print_version();
802 rc = 0;
803 goto program_exit;
804
805 default:
806 /* catch unknown options *as well as* missing arguments. */
807 print_error("Error in command line options. Exiting.\n");
808 goto program_exit;
809 }
810 }
811
812 if (cmdline_opts.send) {
813 if (cmdline_opts.daemon || cmdline_opts.format_str || cmdline_opts.socket_path) {
814 print_error("--send option cannot have any listening related args.");
815 }
816 rc = do_send(argc - optind, argv + optind);
817 goto program_exit;
818 }
819
820 if (!cmdline_opts.daemon && !(argc - optind)) {
821 print_help();
822 goto program_exit;
823 }
824
825 if (cmdline_opts.daemon && !cmdline_opts.timeout) {
826 print_error("In daemon mode, --timeout must not be zero.\n");
827 goto program_exit;
828 }
829
830 if (cmdline_opts.quiet)
831 log_target_destroy(osmo_stderr_target);
832
833 if (cmdline_opts.format_str) {
834 if (osmo_str_startswith("json", cmdline_opts.format_str))
835 globals.format = FORMAT_JSON;
836 else if (osmo_str_startswith("csv", cmdline_opts.format_str))
837 globals.format = FORMAT_CSV;
838 else {
839 print_error("Invalid format: %s\n", cmdline_opts.format_str);
840 goto program_exit;
841 }
842 }
843
844 if (globals.format == FORMAT_CSV && cmdline_opts.csv_headers)
845 respond_str_stdout(CSV_HEADERS);
846
847 globals.mslookup_client = osmo_mslookup_client_new(globals.ctx);
848 if (!globals.mslookup_client
849 || !osmo_mslookup_client_add_mdns(globals.mslookup_client,
850 cmdline_opts.mdns_addr.ip, cmdline_opts.mdns_addr.port,
851 -1, cmdline_opts.mdns_domain_suffix)) {
852 print_error("Failed to start mDNS client\n");
853 goto program_exit;
854 }
855
856 if (cmdline_opts.socket_path) {
857 if (socket_init(cmdline_opts.socket_path))
858 goto program_exit;
859 }
860
861 start_query_strs(&argv[optind], argc - optind);
862
863 while (1) {
864 osmo_select_main_ctx(0);
865
866 if (!cmdline_opts.daemon
867 && globals.requests_handled
868 && llist_empty(&globals.queries))
869 break;
870 }
871
872 rc = 0;
873program_exit:
874 osmo_mslookup_client_free(globals.mslookup_client);
875 socket_close();
876 log_fini();
877 talloc_free(globals.ctx);
878 return rc;
879}