| /* High level mDNS encoding and decoding functions for whole messages: |
| * Request message (header, question) |
| * Answer message (header, resource record 1, ... resource record N)*/ |
| |
| /* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de> |
| * |
| * All Rights Reserved |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with this program. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #include <errno.h> |
| #include <string.h> |
| #include <osmocom/hlr/logging.h> |
| #include <osmocom/mslookup/mdns_msg.h> |
| |
| /*! Encode request message into one mDNS packet, consisting of the header section and one question section. |
| * \returns 0 on success, -EINVAL on error. |
| */ |
| int osmo_mdns_msg_request_encode(void *ctx, struct msgb *msg, const struct osmo_mdns_msg_request *req) |
| { |
| struct osmo_mdns_rfc_header hdr = {0}; |
| struct osmo_mdns_rfc_question qst = {0}; |
| |
| hdr.id = req->id; |
| hdr.qdcount = 1; |
| osmo_mdns_rfc_header_encode(msg, &hdr); |
| |
| qst.domain = req->domain; |
| qst.qtype = req->type; |
| qst.qclass = OSMO_MDNS_RFC_CLASS_IN; |
| if (osmo_mdns_rfc_question_encode(ctx, msg, &qst) != 0) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /*! Decode request message from a mDNS packet, consisting of the header section and one question section. |
| * \returns allocated request message on success, NULL on error. |
| */ |
| struct osmo_mdns_msg_request *osmo_mdns_msg_request_decode(void *ctx, const uint8_t *data, size_t data_len) |
| { |
| struct osmo_mdns_rfc_header hdr = {0}; |
| size_t hdr_len = sizeof(struct osmo_mdns_rfc_header); |
| struct osmo_mdns_rfc_question* qst = NULL; |
| struct osmo_mdns_msg_request *ret = NULL; |
| |
| if (data_len < hdr_len || osmo_mdns_rfc_header_decode(data, hdr_len, &hdr) != 0 || hdr.qr != 0) |
| return NULL; |
| |
| qst = osmo_mdns_rfc_question_decode(ctx, data + hdr_len, data_len - hdr_len); |
| if (!qst) |
| return NULL; |
| |
| ret = talloc_zero(ctx, struct osmo_mdns_msg_request); |
| ret->id = hdr.id; |
| ret->domain = talloc_strdup(ret, qst->domain); |
| ret->type = qst->qtype; |
| |
| talloc_free(qst); |
| return ret; |
| } |
| |
| /*! Initialize the linked list for resource records in a answer message. */ |
| void osmo_mdns_msg_answer_init(struct osmo_mdns_msg_answer *ans) |
| { |
| *ans = (struct osmo_mdns_msg_answer){}; |
| INIT_LLIST_HEAD(&ans->records); |
| } |
| |
| /*! Encode answer message into one mDNS packet, consisting of the header section and N resource records. |
| * |
| * To keep things simple, this sends the domain with each resource record. Other DNS implementations make use of |
| * "message compression", which would send a question section with the domain before the resource records, and then |
| * point inside each resource record with an offset back to the domain in the question section (RFC 1035 4.1.4). |
| * \returns 0 on success, -EINVAL on error. |
| */ |
| int osmo_mdns_msg_answer_encode(void *ctx, struct msgb *msg, const struct osmo_mdns_msg_answer *ans) |
| { |
| struct osmo_mdns_rfc_header hdr = {0}; |
| struct osmo_mdns_record *ans_record; |
| |
| hdr.id = ans->id; |
| hdr.qr = 1; |
| hdr.ancount = llist_count(&ans->records); |
| osmo_mdns_rfc_header_encode(msg, &hdr); |
| |
| llist_for_each_entry(ans_record, &ans->records, list) { |
| struct osmo_mdns_rfc_record rec = {0}; |
| |
| rec.domain = ans->domain; |
| rec.type = ans_record->type; |
| rec.class = OSMO_MDNS_RFC_CLASS_IN; |
| rec.ttl = 0; |
| rec.rdlength = ans_record->length; |
| rec.rdata = ans_record->data; |
| |
| if (osmo_mdns_rfc_record_encode(ctx, msg, &rec) != 0) |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| /*! Decode answer message from a mDNS packet. |
| * |
| * Answer messages must consist of one header and one or more resource records. An additional question section or |
| * message compression (RFC 1035 4.1.4) are not supported. |
| * \returns allocated answer message on success, NULL on error. |
| */ |
| struct osmo_mdns_msg_answer *osmo_mdns_msg_answer_decode(void *ctx, const uint8_t *data, size_t data_len) |
| { |
| struct osmo_mdns_rfc_header hdr = {}; |
| size_t hdr_len = sizeof(struct osmo_mdns_rfc_header); |
| struct osmo_mdns_msg_answer *ret = talloc_zero(ctx, struct osmo_mdns_msg_answer); |
| |
| /* Parse header section */ |
| if (data_len < hdr_len || osmo_mdns_rfc_header_decode(data, hdr_len, &hdr) != 0 || hdr.qr != 1) |
| goto error; |
| ret->id = hdr.id; |
| data_len -= hdr_len; |
| data += hdr_len; |
| |
| /* Parse resource records */ |
| INIT_LLIST_HEAD(&ret->records); |
| while (data_len) { |
| size_t record_len; |
| struct osmo_mdns_rfc_record *rec; |
| struct osmo_mdns_record* ret_record; |
| |
| rec = osmo_mdns_rfc_record_decode(ret, data, data_len, &record_len); |
| if (!rec) |
| goto error; |
| |
| /* Copy domain to ret */ |
| if (ret->domain) { |
| if (strcmp(ret->domain, rec->domain) != 0) { |
| LOGP(DMSLOOKUP, LOGL_ERROR, "domain mismatch in resource records ('%s' vs '%s')\n", |
| ret->domain, rec->domain); |
| goto error; |
| } |
| } |
| else |
| ret->domain = talloc_strdup(ret, rec->domain); |
| |
| /* Add simplified record to ret */ |
| ret_record = talloc_zero(ret, struct osmo_mdns_record); |
| ret_record->type = rec->type; |
| ret_record->length = rec->rdlength; |
| ret_record->data = talloc_memdup(ret_record, rec->rdata, rec->rdlength); |
| llist_add_tail(&ret_record->list, &ret->records); |
| |
| data += record_len; |
| data_len -= record_len; |
| talloc_free(rec); |
| } |
| |
| /* Verify record count */ |
| if (llist_count(&ret->records) != hdr.ancount) { |
| LOGP(DMSLOOKUP, LOGL_ERROR, "amount of parsed records (%i) doesn't match count in header (%i)\n", |
| llist_count(&ret->records), hdr.ancount); |
| goto error; |
| } |
| |
| return ret; |
| error: |
| talloc_free(ret); |
| return NULL; |
| } |
| |
| /*! Get a TXT resource record, which stores a key=value string. |
| * \returns allocated resource record on success, NULL on error. |
| */ |
| static struct osmo_mdns_record *_osmo_mdns_record_txt_encode(void *ctx, const char *key, const char *value) |
| { |
| struct osmo_mdns_record *ret = talloc_zero(ctx, struct osmo_mdns_record); |
| size_t len = strlen(key) + 1 + strlen(value); |
| |
| if (len > OSMO_MDNS_RFC_MAX_CHARACTER_STRING_LEN - 1) |
| return NULL; |
| |
| /* redundant len is required, see RFC 1035 3.3.14 and 3.3. */ |
| ret->data = (uint8_t *)talloc_asprintf(ctx, "%c%s=%s", (char)len, key, value); |
| if (!ret->data) |
| return NULL; |
| ret->type = OSMO_MDNS_RFC_RECORD_TYPE_TXT; |
| ret->length = len + 1; |
| return ret; |
| } |
| |
| /*! Get a TXT resource record, which stores a key=value string, but build value from a format string. |
| * \returns allocated resource record on success, NULL on error. |
| */ |
| struct osmo_mdns_record *osmo_mdns_record_txt_keyval_encode(void *ctx, const char *key, const char *value_fmt, ...) |
| { |
| va_list ap; |
| char *value = NULL; |
| struct osmo_mdns_record *r; |
| |
| if (!value_fmt) |
| return _osmo_mdns_record_txt_encode(ctx, key, ""); |
| |
| va_start(ap, value_fmt); |
| value = talloc_vasprintf(ctx, value_fmt, ap); |
| va_end(ap); |
| if (!value) |
| return NULL; |
| r = _osmo_mdns_record_txt_encode(ctx, key, value); |
| talloc_free(value); |
| return r; |
| } |
| |
| /*! Decode a TXT resource record, which stores a key=value string. |
| * \returns 0 on success, -EINVAL on error. |
| */ |
| int osmo_mdns_record_txt_keyval_decode(const struct osmo_mdns_record *rec, |
| char *key_buf, size_t key_size, char *value_buf, size_t value_size) |
| { |
| const char *key_value; |
| const char *key_value_end; |
| const char *sep; |
| const char *value; |
| |
| if (rec->type != OSMO_MDNS_RFC_RECORD_TYPE_TXT) |
| return -EINVAL; |
| |
| key_value = (const char *)rec->data; |
| key_value_end = key_value + rec->length; |
| |
| /* Verify and then skip the redundant string length byte */ |
| if (*key_value != rec->length - 1) |
| return -EINVAL; |
| key_value++; |
| |
| if (key_value >= key_value_end) |
| return -EINVAL; |
| |
| /* Find equals sign */ |
| sep = osmo_strnchr(key_value, key_value_end - key_value, '='); |
| if (!sep) |
| return -EINVAL; |
| |
| /* Parse key */ |
| osmo_print_n(key_buf, key_size, key_value, sep - key_value); |
| |
| /* Parse value */ |
| value = sep + 1; |
| osmo_print_n(value_buf, value_size, value, key_value_end - value); |
| return 0; |
| } |