diff --git a/mongoose.c b/mongoose.c index 1056805f77..d624fa661f 100644 --- a/mongoose.c +++ b/mongoose.c @@ -410,69 +410,246 @@ static const uint8_t mdns_answer[] = { 0, 4 // 2 bytes - address length }; -static void mdns_cb(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_READ) { - struct mg_dns_header *qh = (struct mg_dns_header *) c->recv.buf; - if (c->recv.len > 12 && (qh->flags & mg_htons(0xF800)) == 0) { - // flags -> !resp, opcode=0 => query; ignore other opcodes and responses - struct mg_dns_rr rr; // Parse first question, offset 12 is header size - size_t n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, true, &rr); - MG_VERBOSE(("mDNS request parsed, result=%d", (int) n)); - if (n > 0) { - // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 - // buf and h declared here to ease future expansion to DNS-SD - uint8_t buf[sizeof(struct mg_dns_header) + 256 + sizeof(mdns_answer) + 4]; - struct mg_dns_header *h = (struct mg_dns_header *) buf; - char local_name[63 + 7]; // name label + '.' + local label + '\0' - uint8_t name_len = (uint8_t) strlen((char *)c->fn_data); - struct mg_dns_message dm; - bool unicast = (rr.aclass & MG_BIT(15)) != 0; // QU - // uint16_t q = mg_ntohs(qh->num_questions); - rr.aclass &= (uint16_t) ~MG_BIT(15); // remove "QU" (unicast response) - qh->num_questions = mg_htons(1); // parser sanity - mg_dns_parse(c->recv.buf, c->recv.len, &dm); - if (name_len > (sizeof(local_name) - 7)) // leave room for .local\0 - name_len = sizeof(local_name) - 7; - memcpy(local_name, c->fn_data, name_len); - strcpy(local_name + name_len, ".local"); // ensure proper name.local\0 - if (strcmp(local_name, dm.name) == 0) { - uint8_t *p = &buf[sizeof(*h)]; - memset(h, 0, sizeof(*h)); // clear header - h->txnid = unicast ? qh->txnid : 0; // RFC-6762 18.1 - // RFC-6762 6: 0 questions, 1 Answer, 0 Auth, 0 Additional RRs - h->num_answers = mg_htons(1); // only one answer - h->flags = mg_htons(0x8400); // Authoritative response - *p++ = name_len; // label 1 - memcpy(p, c->fn_data, name_len), p += name_len; - *p++ = 5; // label 2 - memcpy(p, "local", 5), p += 5; - *p++ = 0; // no more labels - memcpy(p, mdns_answer, sizeof(mdns_answer)), p += sizeof(mdns_answer); +static uint8_t *build_name(struct mg_str *name, uint8_t *p) { + *p++ = (uint8_t) name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + *p++ = 5; // label 2 + memcpy(p, "local", 5), p += 5; + *p++ = 0; // no more labels + return p; +} + +static uint8_t *build_a_record(struct mg_connection *c, uint8_t *p) { + memcpy(p, mdns_answer, sizeof(mdns_answer)), p += sizeof(mdns_answer); #if MG_ENABLE_TCPIP - memcpy(p, &c->mgr->ifp->ip, 4), p += 4; + memcpy(p, &c->mgr->ifp->ip, 4), p += 4; #else - memcpy(p, c->data, 4), p += 4; + memcpy(p, c->data, 4), p += 4; #endif - if (!unicast) memcpy(&c->rem, &c->loc, sizeof(c->rem)); - mg_send(c, buf, (size_t)(p - buf)); // And send it! - MG_DEBUG(("mDNS %c response sent", unicast ? 'U' : 'M')); + return p; +} + +static uint8_t *build_srv_name(uint8_t *p, struct mg_dnssd_record *r) { + *p++ = (uint8_t) r->srvcproto.len - 5; // label 1, up to '._tcp' + memcpy(p, r->srvcproto.buf, r->srvcproto.len), p += r->srvcproto.len; + p[-5] = 4; // label 2, '_tcp', overwrite '.' + *p++ = 5; // label 3 + memcpy(p, "local", 5), p += 5; + *p++ = 0; // no more labels + return p; +} + +#if 0 +// TODO(): for listing +static uint8_t *build_mysrv_name(struct mg_str *name, uint8_t *p, + struct mg_dnssd_record *r) { + *p++ = name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + return build_srv_name(p, r); +} +#endif + +static uint8_t *build_ptr_record(struct mg_str *name, uint8_t *p, uint16_t o) { + uint16_t offset = mg_htons(o); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 12; // overwrite record type + p += sizeof(mdns_answer); + p[-1] = (uint8_t) name->len + + 3; // overwrite response length, label length + label + offset + *p++ = (uint8_t) name->len; // response: label 1 + memcpy(p, name->buf, name->len), p += name->len; // copy label + memcpy(p, &offset, 2); + *p |= 0xC0, p += 2; + return p; +} + +static uint8_t *build_srv_record(struct mg_str *name, uint8_t *p, + struct mg_dnssd_record *r, uint16_t o) { + uint16_t port = mg_htons(r->port); + uint16_t offset = mg_htons(o); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 33; // overwrite record type + p += sizeof(mdns_answer); + p[-1] = (uint8_t) name->len + 9; // overwrite response length (4+2+1+2) + *p++ = 0; // priority + *p++ = 0; + *p++ = 0; // weight + *p++ = 0; + memcpy(p, &port, 2), p += 2; // port + *p++ = (uint8_t) name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + memcpy(p, &offset, 2); + *p |= 0xC0, p += 2; + return p; +} + +static uint8_t *build_txt_record(uint8_t *p, struct mg_dnssd_record *r) { + uint16_t len = mg_htons((uint16_t) r->txt.len); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 16; // overwrite record type + p += sizeof(mdns_answer); + memcpy(p - 2, &len, 2); // overwrite response length + memcpy(p, r->txt.buf, r->txt.len), p += r->txt.len; // copy record verbatim + return p; +} + +// RFC-6762 16: case-insensitivity --> RFC-1034, 1035 + +static void handle_mdns_record(struct mg_connection *c) { + struct mg_dns_header *qh = (struct mg_dns_header *) c->recv.buf; + struct mg_dns_rr rr; + size_t n; + // flags -> !resp, opcode=0 => query; ignore other opcodes and responses + if (c->recv.len <= 12 || (qh->flags & mg_htons(0xF800)) != 0) return; + // Parse first question, offset 12 is header size + n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, true, &rr); + MG_VERBOSE(("mDNS request parsed, result=%d", (int) n)); + if (n > 0) { + // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 + uint8_t buf[sizeof(struct mg_dns_header) + 256 + sizeof(mdns_answer) + 4]; + struct mg_dns_header *h = (struct mg_dns_header *) buf; + uint8_t *p = &buf[sizeof(*h)]; + char name[256]; + uint8_t name_len; + // uint16_t q = mg_ntohs(qh->num_questions); + struct mg_str defname = mg_str((const char *) c->fn_data); + struct mg_str *respname; + struct mg_mdns_req req; + memset(&req, 0, sizeof(req)); + req.is_unicast = (rr.aclass & MG_BIT(15)) != 0; // QU + rr.aclass &= (uint16_t) ~MG_BIT(15); // remove "QU" (unicast response) + qh->num_questions = mg_htons(1); // parser sanity + mg_dns_parse_name(c->recv.buf, c->recv.len, 12, name, sizeof(name)); + name_len = (uint8_t) strlen(name); // verify it ends in .local + if (strcmp(".local", &name[name_len - 6]) != 0 || + (rr.aclass != 1 && rr.aclass != 0xff)) + return; + name[name_len -= 6] = '\0'; // remove .local + MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, + (unsigned int) rr.aclass, name)); + if (rr.atype == 1) { // A + // TODO(): ensure c->fn_data ends in \0 + // if we have a name to match, go; otherwise users will match and fill + // req.r.name and set req.is_resp + if (c->fn_data != NULL && mg_casecmp((char *) c->fn_data, name) != 0) + return; + req.is_resp = (c->fn_data != NULL); + req.reqname = mg_str_n(name, name_len); + mg_call(c, MG_EV_MDNS_A, &req); + } else // users have to match the request to something in their db, then + // fill req.r and set req.is_resp + if (rr.atype == 12) { // PTR + if (strcmp("_services._dns-sd._udp", name) == 0) req.is_listing = true; + MG_DEBUG( + ("PTR request for %s", req.is_listing ? "services listing" : name)); + req.reqname = mg_str_n(name, name_len); + mg_call(c, MG_EV_MDNS_PTR, &req); + } else if (rr.atype == 33 || rr.atype == 16) { // SRV or TXT + MG_DEBUG(("%s request for %s", rr.atype == 33 ? "SRV" : "TXT", name)); + // if possible, check it starts with our name, users will check it ends + // in a service name they handle + if (c->fn_data != NULL) { + if (mg_strcasecmp(defname, mg_str_n(name, defname.len)) != 0 || + name[defname.len] != '.') + return; + req.reqname = + mg_str_n(name + defname.len + 1, name_len - defname.len - 1); + MG_DEBUG( + ("That's us, handing %.*s", req.reqname.len, req.reqname.buf)); + } else { + req.reqname = mg_str_n(name, name_len); } + mg_call(c, rr.atype == 33 ? MG_EV_MDNS_SRV : MG_EV_MDNS_TXT, &req); + } else { // unhandled record + return; } - } + if (!req.is_resp) return; + respname = req.respname.buf != NULL ? &req.respname : &defname; + + memset(h, 0, sizeof(*h)); // clear header + h->txnid = req.is_unicast ? qh->txnid : 0; // RFC-6762 18.1 + h->num_answers = mg_htons(1); // RFC-6762 6: 0 questions, 1 Answer + h->flags = mg_htons(0x8400); // Authoritative response + if (req.is_listing) { + // TODO(): RFC-6762 6: each responder SHOULD delay its response by a + // random amount of time selected with uniform random distribution in the + // range 20-120 ms. + // TODO(): + return; + } else if (rr.atype == 12) { // PTR requested, serve PTR + SRV + TXT + A + // TODO(): RFC-6762 6: each responder SHOULD delay its response by a + // random amount of time selected with uniform random distribution in the + // range 20-120 ms. Response to PTR is local_name._myservice._tcp.local + uint8_t *o = p, *aux; + uint16_t offset; + if (respname->buf == NULL || respname->len == 0) return; + h->num_other_prs = mg_htons(3); // 3 additional records + p = build_srv_name(p, req.r); + aux = build_ptr_record(respname, p, (uint16_t) (o - buf)); + o = p + sizeof(mdns_answer); // point to PTR response (full srvc name) + offset = mg_htons((uint16_t) (o - buf)); + o = p - 7; // point to '.local' label (\x05local\x00) + p = aux; + memcpy(p, &offset, 2); // point to full srvc name, in record + *p |= 0xC0, p += 2; + aux = p; + p = build_srv_record(respname, p, req.r, (uint16_t) (o - buf)); + o = aux + sizeof(mdns_answer) + 6; // point to target in SRV + memcpy(p, &offset, 2); // point to full srvc name, in record + *p |= 0xC0, p += 2; + p = build_txt_record(p, req.r); + offset = mg_htons((uint16_t) (o - buf)); + memcpy(p, &offset, 2); // point to target name, in record + *p |= 0xC0, p += 2; + p = build_a_record(c, p); + } else if (rr.atype == 16) { // TXT requested + p = build_srv_name(p, req.r); + p = build_txt_record(p, req.r); + } else if (rr.atype == 33) { // SRV requested, serve SRV + A + uint8_t *o, *aux; + uint16_t offset; + if (respname->buf == NULL || respname->len == 0) return; + h->num_other_prs = mg_htons(1); // 1 additional record + p = build_srv_name(p, req.r); + o = p - 7; // point to '.local' label (\x05local\x00) + aux = p; + p = build_srv_record(respname, p, req.r, (uint16_t) (o - buf)); + o = aux + sizeof(mdns_answer) + 6; // point to target in SRV + offset = mg_htons((uint16_t) (o - buf)); + memcpy(p, &offset, 2); // point to target name, in record + *p |= 0xC0, p += 2; + p = build_a_record(c, p); + } else { // A requested + // RFC-6762 6: 0 Auth, 0 Additional RRs + if (respname->buf == NULL || respname->len == 0) return; + p = build_name(respname, p); + p = build_a_record(c, p); + } + if (!req.is_unicast) memcpy(&c->rem, &c->loc, sizeof(c->rem)); + mg_send(c, buf, (size_t) (p - buf)); // And send it! + MG_DEBUG(("mDNS %c response sent", req.is_unicast ? 'U' : 'M')); + } +} + +static void mdns_cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_READ) { + handle_mdns_record(c); mg_iobuf_del(&c->recv, 0, c->recv.len); } (void) ev_data; } void mg_multicast_add(struct mg_connection *c, char *ip); -struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, char *name) { +struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, + void *fn_data) { struct mg_connection *c = - mg_listen(mgr, "udp://224.0.0.251:5353", mdns_cb, name); - if (c != NULL) mg_multicast_add(c, (char *)"224.0.0.251"); + mg_listen(mgr, "udp://224.0.0.251:5353", fn, fn_data); + if (c == NULL) return NULL; + c->pfn = mdns_cb, c->pfn_data = fn_data; + mg_multicast_add(c, (char *) "224.0.0.251"); return c; } - #ifdef MG_ENABLE_LINES #line 1 "src/event.c" #endif diff --git a/mongoose.h b/mongoose.h index 3a72304fa5..4b534da725 100644 --- a/mongoose.h +++ b/mongoose.h @@ -1641,6 +1641,10 @@ enum { MG_EV_MQTT_OPEN, // MQTT CONNACK received int *connack_status_code MG_EV_SNTP_TIME, // SNTP time received uint64_t *epoch_millis MG_EV_WAKEUP, // mg_wakeup() data received struct mg_str *data + MG_EV_MDNS_A, // mDNS A record request struct mg_mdns_req * + MG_EV_MDNS_PTR, // mDNS PTR record request struct mg_mdns_req * + MG_EV_MDNS_SRV, // mDNS SRV record request struct mg_mdns_req * + MG_EV_MDNS_TXT, // mDNS TXT record request struct mg_mdns_req * MG_EV_USER // Starting ID for user events }; @@ -2950,13 +2954,33 @@ struct mg_dns_rr { uint16_t alen; // Address length }; + +// DNS-SD response record +struct mg_dnssd_record { + struct mg_str srvcproto; // service.proto, service name + struct mg_str txt; // TXT record contents + uint16_t port; // SRV record port +}; + +// mDNS request +struct mg_mdns_req { + struct mg_dns_rr *rr; + struct mg_dnssd_record *r; + struct mg_str reqname; // requested name in RR + struct mg_str respname; // actual name in response + struct mg_addr addr; + bool is_listing; + bool is_resp; + bool is_unicast; +}; + void mg_resolve(struct mg_connection *, const char *url); void mg_resolve_cancel(struct mg_connection *); bool mg_dns_parse(const uint8_t *buf, size_t len, struct mg_dns_message *); size_t mg_dns_parse_rr(const uint8_t *buf, size_t len, size_t ofs, bool is_question, struct mg_dns_rr *); -struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, char *name); +struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, void *fn_data); diff --git a/src/dns.c b/src/dns.c index 20ab8984ae..8bcd78b866 100644 --- a/src/dns.c +++ b/src/dns.c @@ -289,65 +289,242 @@ static const uint8_t mdns_answer[] = { 0, 4 // 2 bytes - address length }; -static void mdns_cb(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_READ) { - struct mg_dns_header *qh = (struct mg_dns_header *) c->recv.buf; - if (c->recv.len > 12 && (qh->flags & mg_htons(0xF800)) == 0) { - // flags -> !resp, opcode=0 => query; ignore other opcodes and responses - struct mg_dns_rr rr; // Parse first question, offset 12 is header size - size_t n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, true, &rr); - MG_VERBOSE(("mDNS request parsed, result=%d", (int) n)); - if (n > 0) { - // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 - // buf and h declared here to ease future expansion to DNS-SD - uint8_t buf[sizeof(struct mg_dns_header) + 256 + sizeof(mdns_answer) + 4]; - struct mg_dns_header *h = (struct mg_dns_header *) buf; - char local_name[63 + 7]; // name label + '.' + local label + '\0' - uint8_t name_len = (uint8_t) strlen((char *)c->fn_data); - struct mg_dns_message dm; - bool unicast = (rr.aclass & MG_BIT(15)) != 0; // QU - // uint16_t q = mg_ntohs(qh->num_questions); - rr.aclass &= (uint16_t) ~MG_BIT(15); // remove "QU" (unicast response) - qh->num_questions = mg_htons(1); // parser sanity - mg_dns_parse(c->recv.buf, c->recv.len, &dm); - if (name_len > (sizeof(local_name) - 7)) // leave room for .local\0 - name_len = sizeof(local_name) - 7; - memcpy(local_name, c->fn_data, name_len); - strcpy(local_name + name_len, ".local"); // ensure proper name.local\0 - if (strcmp(local_name, dm.name) == 0) { - uint8_t *p = &buf[sizeof(*h)]; - memset(h, 0, sizeof(*h)); // clear header - h->txnid = unicast ? qh->txnid : 0; // RFC-6762 18.1 - // RFC-6762 6: 0 questions, 1 Answer, 0 Auth, 0 Additional RRs - h->num_answers = mg_htons(1); // only one answer - h->flags = mg_htons(0x8400); // Authoritative response - *p++ = name_len; // label 1 - memcpy(p, c->fn_data, name_len), p += name_len; - *p++ = 5; // label 2 - memcpy(p, "local", 5), p += 5; - *p++ = 0; // no more labels - memcpy(p, mdns_answer, sizeof(mdns_answer)), p += sizeof(mdns_answer); +static uint8_t *build_name(struct mg_str *name, uint8_t *p) { + *p++ = (uint8_t) name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + *p++ = 5; // label 2 + memcpy(p, "local", 5), p += 5; + *p++ = 0; // no more labels + return p; +} + +static uint8_t *build_a_record(struct mg_connection *c, uint8_t *p) { + memcpy(p, mdns_answer, sizeof(mdns_answer)), p += sizeof(mdns_answer); #if MG_ENABLE_TCPIP - memcpy(p, &c->mgr->ifp->ip, 4), p += 4; + memcpy(p, &c->mgr->ifp->ip, 4), p += 4; #else - memcpy(p, c->data, 4), p += 4; + memcpy(p, c->data, 4), p += 4; +#endif + return p; +} + +static uint8_t *build_srv_name(uint8_t *p, struct mg_dnssd_record *r) { + *p++ = (uint8_t) r->srvcproto.len - 5; // label 1, up to '._tcp' + memcpy(p, r->srvcproto.buf, r->srvcproto.len), p += r->srvcproto.len; + p[-5] = 4; // label 2, '_tcp', overwrite '.' + *p++ = 5; // label 3 + memcpy(p, "local", 5), p += 5; + *p++ = 0; // no more labels + return p; +} + +#if 0 +// TODO(): for listing +static uint8_t *build_mysrv_name(struct mg_str *name, uint8_t *p, + struct mg_dnssd_record *r) { + *p++ = name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + return build_srv_name(p, r); +} #endif - if (!unicast) memcpy(&c->rem, &c->loc, sizeof(c->rem)); - mg_send(c, buf, (size_t)(p - buf)); // And send it! - MG_DEBUG(("mDNS %c response sent", unicast ? 'U' : 'M')); + +static uint8_t *build_ptr_record(struct mg_str *name, uint8_t *p, uint16_t o) { + uint16_t offset = mg_htons(o); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 12; // overwrite record type + p += sizeof(mdns_answer); + p[-1] = (uint8_t) name->len + + 3; // overwrite response length, label length + label + offset + *p++ = (uint8_t) name->len; // response: label 1 + memcpy(p, name->buf, name->len), p += name->len; // copy label + memcpy(p, &offset, 2); + *p |= 0xC0, p += 2; + return p; +} + +static uint8_t *build_srv_record(struct mg_str *name, uint8_t *p, + struct mg_dnssd_record *r, uint16_t o) { + uint16_t port = mg_htons(r->port); + uint16_t offset = mg_htons(o); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 33; // overwrite record type + p += sizeof(mdns_answer); + p[-1] = (uint8_t) name->len + 9; // overwrite response length (4+2+1+2) + *p++ = 0; // priority + *p++ = 0; + *p++ = 0; // weight + *p++ = 0; + memcpy(p, &port, 2), p += 2; // port + *p++ = (uint8_t) name->len; // label 1 + memcpy(p, name->buf, name->len), p += name->len; + memcpy(p, &offset, 2); + *p |= 0xC0, p += 2; + return p; +} + +static uint8_t *build_txt_record(uint8_t *p, struct mg_dnssd_record *r) { + uint16_t len = mg_htons((uint16_t) r->txt.len); + memcpy(p, mdns_answer, sizeof(mdns_answer)); + p[1] = 16; // overwrite record type + p += sizeof(mdns_answer); + memcpy(p - 2, &len, 2); // overwrite response length + memcpy(p, r->txt.buf, r->txt.len), p += r->txt.len; // copy record verbatim + return p; +} + +// RFC-6762 16: case-insensitivity --> RFC-1034, 1035 + +static void handle_mdns_record(struct mg_connection *c) { + struct mg_dns_header *qh = (struct mg_dns_header *) c->recv.buf; + struct mg_dns_rr rr; + size_t n; + // flags -> !resp, opcode=0 => query; ignore other opcodes and responses + if (c->recv.len <= 12 || (qh->flags & mg_htons(0xF800)) != 0) return; + // Parse first question, offset 12 is header size + n = mg_dns_parse_rr(c->recv.buf, c->recv.len, 12, true, &rr); + MG_VERBOSE(("mDNS request parsed, result=%d", (int) n)); + if (n > 0) { + // RFC-6762 Appendix C, RFC2181 11: m(n + 1-63), max 255 + 0x0 + uint8_t buf[sizeof(struct mg_dns_header) + 256 + sizeof(mdns_answer) + 4]; + struct mg_dns_header *h = (struct mg_dns_header *) buf; + uint8_t *p = &buf[sizeof(*h)]; + char name[256]; + uint8_t name_len; + // uint16_t q = mg_ntohs(qh->num_questions); + struct mg_str defname = mg_str((const char *) c->fn_data); + struct mg_str *respname; + struct mg_mdns_req req; + memset(&req, 0, sizeof(req)); + req.is_unicast = (rr.aclass & MG_BIT(15)) != 0; // QU + rr.aclass &= (uint16_t) ~MG_BIT(15); // remove "QU" (unicast response) + qh->num_questions = mg_htons(1); // parser sanity + mg_dns_parse_name(c->recv.buf, c->recv.len, 12, name, sizeof(name)); + name_len = (uint8_t) strlen(name); // verify it ends in .local + if (strcmp(".local", &name[name_len - 6]) != 0 || + (rr.aclass != 1 && rr.aclass != 0xff)) + return; + name[name_len -= 6] = '\0'; // remove .local + MG_VERBOSE(("RR %u %u %s", (unsigned int) rr.atype, + (unsigned int) rr.aclass, name)); + if (rr.atype == 1) { // A + // TODO(): ensure c->fn_data ends in \0 + // if we have a name to match, go; otherwise users will match and fill + // req.r.name and set req.is_resp + if (c->fn_data != NULL && mg_casecmp((char *) c->fn_data, name) != 0) + return; + req.is_resp = (c->fn_data != NULL); + req.reqname = mg_str_n(name, name_len); + mg_call(c, MG_EV_MDNS_A, &req); + } else // users have to match the request to something in their db, then + // fill req.r and set req.is_resp + if (rr.atype == 12) { // PTR + if (strcmp("_services._dns-sd._udp", name) == 0) req.is_listing = true; + MG_DEBUG( + ("PTR request for %s", req.is_listing ? "services listing" : name)); + req.reqname = mg_str_n(name, name_len); + mg_call(c, MG_EV_MDNS_PTR, &req); + } else if (rr.atype == 33 || rr.atype == 16) { // SRV or TXT + MG_DEBUG(("%s request for %s", rr.atype == 33 ? "SRV" : "TXT", name)); + // if possible, check it starts with our name, users will check it ends + // in a service name they handle + if (c->fn_data != NULL) { + if (mg_strcasecmp(defname, mg_str_n(name, defname.len)) != 0 || + name[defname.len] != '.') + return; + req.reqname = + mg_str_n(name + defname.len + 1, name_len - defname.len - 1); + MG_DEBUG( + ("That's us, handing %.*s", req.reqname.len, req.reqname.buf)); + } else { + req.reqname = mg_str_n(name, name_len); } + mg_call(c, rr.atype == 33 ? MG_EV_MDNS_SRV : MG_EV_MDNS_TXT, &req); + } else { // unhandled record + return; } + if (!req.is_resp) return; + respname = req.respname.buf != NULL ? &req.respname : &defname; + + memset(h, 0, sizeof(*h)); // clear header + h->txnid = req.is_unicast ? qh->txnid : 0; // RFC-6762 18.1 + h->num_answers = mg_htons(1); // RFC-6762 6: 0 questions, 1 Answer + h->flags = mg_htons(0x8400); // Authoritative response + if (req.is_listing) { + // TODO(): RFC-6762 6: each responder SHOULD delay its response by a + // random amount of time selected with uniform random distribution in the + // range 20-120 ms. + // TODO(): + return; + } else if (rr.atype == 12) { // PTR requested, serve PTR + SRV + TXT + A + // TODO(): RFC-6762 6: each responder SHOULD delay its response by a + // random amount of time selected with uniform random distribution in the + // range 20-120 ms. Response to PTR is local_name._myservice._tcp.local + uint8_t *o = p, *aux; + uint16_t offset; + if (respname->buf == NULL || respname->len == 0) return; + h->num_other_prs = mg_htons(3); // 3 additional records + p = build_srv_name(p, req.r); + aux = build_ptr_record(respname, p, (uint16_t) (o - buf)); + o = p + sizeof(mdns_answer); // point to PTR response (full srvc name) + offset = mg_htons((uint16_t) (o - buf)); + o = p - 7; // point to '.local' label (\x05local\x00) + p = aux; + memcpy(p, &offset, 2); // point to full srvc name, in record + *p |= 0xC0, p += 2; + aux = p; + p = build_srv_record(respname, p, req.r, (uint16_t) (o - buf)); + o = aux + sizeof(mdns_answer) + 6; // point to target in SRV + memcpy(p, &offset, 2); // point to full srvc name, in record + *p |= 0xC0, p += 2; + p = build_txt_record(p, req.r); + offset = mg_htons((uint16_t) (o - buf)); + memcpy(p, &offset, 2); // point to target name, in record + *p |= 0xC0, p += 2; + p = build_a_record(c, p); + } else if (rr.atype == 16) { // TXT requested + p = build_srv_name(p, req.r); + p = build_txt_record(p, req.r); + } else if (rr.atype == 33) { // SRV requested, serve SRV + A + uint8_t *o, *aux; + uint16_t offset; + if (respname->buf == NULL || respname->len == 0) return; + h->num_other_prs = mg_htons(1); // 1 additional record + p = build_srv_name(p, req.r); + o = p - 7; // point to '.local' label (\x05local\x00) + aux = p; + p = build_srv_record(respname, p, req.r, (uint16_t) (o - buf)); + o = aux + sizeof(mdns_answer) + 6; // point to target in SRV + offset = mg_htons((uint16_t) (o - buf)); + memcpy(p, &offset, 2); // point to target name, in record + *p |= 0xC0, p += 2; + p = build_a_record(c, p); + } else { // A requested + // RFC-6762 6: 0 Auth, 0 Additional RRs + if (respname->buf == NULL || respname->len == 0) return; + p = build_name(respname, p); + p = build_a_record(c, p); } + if (!req.is_unicast) memcpy(&c->rem, &c->loc, sizeof(c->rem)); + mg_send(c, buf, (size_t) (p - buf)); // And send it! + MG_DEBUG(("mDNS %c response sent", req.is_unicast ? 'U' : 'M')); + } +} + +static void mdns_cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_READ) { + handle_mdns_record(c); mg_iobuf_del(&c->recv, 0, c->recv.len); } (void) ev_data; } void mg_multicast_add(struct mg_connection *c, char *ip); -struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, char *name) { +struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, + void *fn_data) { struct mg_connection *c = - mg_listen(mgr, "udp://224.0.0.251:5353", mdns_cb, name); - if (c != NULL) mg_multicast_add(c, (char *)"224.0.0.251"); + mg_listen(mgr, "udp://224.0.0.251:5353", fn, fn_data); + if (c == NULL) return NULL; + c->pfn = mdns_cb, c->pfn_data = fn_data; + mg_multicast_add(c, (char *) "224.0.0.251"); return c; } - diff --git a/src/dns.h b/src/dns.h index fd266a484a..32e484aade 100644 --- a/src/dns.h +++ b/src/dns.h @@ -31,10 +31,30 @@ struct mg_dns_rr { uint16_t alen; // Address length }; + +// DNS-SD response record +struct mg_dnssd_record { + struct mg_str srvcproto; // service.proto, service name + struct mg_str txt; // TXT record contents + uint16_t port; // SRV record port +}; + +// mDNS request +struct mg_mdns_req { + struct mg_dns_rr *rr; + struct mg_dnssd_record *r; + struct mg_str reqname; // requested name in RR + struct mg_str respname; // actual name in response + struct mg_addr addr; + bool is_listing; + bool is_resp; + bool is_unicast; +}; + void mg_resolve(struct mg_connection *, const char *url); void mg_resolve_cancel(struct mg_connection *); bool mg_dns_parse(const uint8_t *buf, size_t len, struct mg_dns_message *); size_t mg_dns_parse_rr(const uint8_t *buf, size_t len, size_t ofs, bool is_question, struct mg_dns_rr *); -struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, char *name); +struct mg_connection *mg_mdns_listen(struct mg_mgr *mgr, mg_event_handler_t fn, void *fn_data); diff --git a/src/event.h b/src/event.h index 0a08dc337b..baf7525e82 100644 --- a/src/event.h +++ b/src/event.h @@ -27,5 +27,9 @@ enum { MG_EV_MQTT_OPEN, // MQTT CONNACK received int *connack_status_code MG_EV_SNTP_TIME, // SNTP time received uint64_t *epoch_millis MG_EV_WAKEUP, // mg_wakeup() data received struct mg_str *data + MG_EV_MDNS_A, // mDNS A record request struct mg_mdns_req * + MG_EV_MDNS_PTR, // mDNS PTR record request struct mg_mdns_req * + MG_EV_MDNS_SRV, // mDNS SRV record request struct mg_mdns_req * + MG_EV_MDNS_TXT, // mDNS TXT record request struct mg_mdns_req * MG_EV_USER // Starting ID for user events }; diff --git a/tutorials/stm32/nucleo-f746zg-make-baremetal-builtin-cmsis_driver/main.c b/tutorials/stm32/nucleo-f746zg-make-baremetal-builtin-cmsis_driver/main.c index faa813ea78..72548daeb3 100644 --- a/tutorials/stm32/nucleo-f746zg-make-baremetal-builtin-cmsis_driver/main.c +++ b/tutorials/stm32/nucleo-f746zg-make-baremetal-builtin-cmsis_driver/main.c @@ -60,7 +60,7 @@ int main(void) { MG_INFO(("Initialising application...")); web_init(&mgr); - mg_mdns_listen(&mgr, "Mongoose"); // Start mDNS server + mg_mdns_listen(&mgr, NULL, "Mongoose"); // Start mDNS server MG_INFO(("Starting event loop")); for (;;) { diff --git a/tutorials/udp/mdns-sd-server/Makefile b/tutorials/udp/mdns-sd-server/Makefile new file mode 100644 index 0000000000..8b63f3c88a --- /dev/null +++ b/tutorials/udp/mdns-sd-server/Makefile @@ -0,0 +1,25 @@ +PROG ?= example # Program we are building +DELETE = rm -rf # Command to remove files +OUT ?= -o $(PROG) # Compiler argument for output file +SOURCES = main.c mongoose.c # Source code files +CFLAGS = -W -Wall -Wextra -g -I. # Build options + +# Mongoose build options. See https://mongoose.ws/documentation/#build-options +CFLAGS_MONGOOSE += -DMG_ENABLE_MDNS=1 -DMG_ENABLE_LINES + +ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe + PROG ?= example.exe # Use .exe suffix for the binary + CC = gcc # Use MinGW gcc compiler + CFLAGS += -lws2_32 # Link against Winsock library + DELETE = cmd /C del /Q /F /S # Command prompt command to delete files + OUT ?= -o $(PROG) # Build output +endif + +all: $(PROG) # Default target. Build and run program + $(RUN) ./$(PROG) $(ARGS) + +$(PROG): $(SOURCES) # Build program from sources + $(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT) + +clean: # Cleanup. Delete built program and all build artifacts + $(DELETE) $(PROG) *.o *.obj *.exe *.dSYM diff --git a/tutorials/udp/mdns-sd-server/README.md b/tutorials/udp/mdns-sd-server/README.md new file mode 100644 index 0000000000..ff1b1ff764 --- /dev/null +++ b/tutorials/udp/mdns-sd-server/README.md @@ -0,0 +1,19 @@ +```sh +$ avahi-browse -r _myservice._tcp ++ virbr0 IPv4 Mongoose _myservice._tcp local += virbr0 IPv4 Mongoose _myservice._tcp local + hostname = [Mongoose.local] + address = [192.168.69.11] + port = [9876] + txt = [] +``` + +```sh +$ avahi-browse -r _myservice._udp ++ virbr0 IPv4 Mongoose _myservice._udp local += virbr0 IPv4 Mongoose _myservice._udp local + hostname = [Mongoose.local] + address = [192.168.69.11] + port = [9876] + txt = ["{\"property\": \"value\"}"] +``` diff --git a/tutorials/udp/mdns-sd-server/main.c b/tutorials/udp/mdns-sd-server/main.c new file mode 100644 index 0000000000..0fbf37a66a --- /dev/null +++ b/tutorials/udp/mdns-sd-server/main.c @@ -0,0 +1,79 @@ +// Copyright (c) 2022 Cesanta Software Limited +// All rights reserved + +#include "mongoose.h" + +// A TXT record data containing JSON, if actually needed. +// The whole record must not exceed 256 bytes, so keep this small +char txt[] = "\x15{\"property\": \"value\"}"; +// string length followed by string data, data length = 21 + 1 = 22 bytes + +// DNS-SD service records +static const struct mg_dnssd_record srvcs[] = { + {{"_myservice._tcp", 15}, {"", 0}, 9876}, + {{"_myservice._udp", 15}, {txt, 22}, 9876}}; + +// DNS-SD service records database +struct dnssd_db { + struct mg_dnssd_record *srvcs; // service record data + uint8_t num; // number of records in db +}; + +static const struct dnssd_db db_ = { + (struct mg_dnssd_record *) srvcs, + sizeof(srvcs) / sizeof(struct mg_dnssd_record)}; + +static struct mg_dnssd_record *dnssd_lookup(const struct dnssd_db *db, + struct mg_str *name) { + unsigned int i; + for (i = 0; i < db->num; i++) { + if (mg_strcasecmp(db->srvcs[i].srvcproto, *name) == 0) break; + } + if (i == db->num) return NULL; + return &db->srvcs[i]; +} + +// we can do more complex stuff, this is a simple implementation +static void fn(struct mg_connection *c, int ev, void *ev_data) { + struct mg_mdns_req *req = (struct mg_mdns_req *) ev_data; + if (ev == MG_EV_MDNS_PTR) { // do we serve this service ? + struct mg_dnssd_record *r; + if (req->is_listing) return; // TODO(): handle services listing + r = dnssd_lookup(&db_, &req->reqname); + if (r == NULL) return; + req->r = r; + req->is_resp = true; + } else if (ev == MG_EV_MDNS_SRV || ev == MG_EV_MDNS_TXT) { + // Mongoose already checked the host name for us, otherwise we need to do + // that ourselves first, then check for service name in db + struct mg_dnssd_record *r = dnssd_lookup(&db_, &req->reqname); + if (r == NULL) return; + req->r = r; + req->is_resp = true; + } else if (ev == MG_EV_MDNS_A) { // is this us ? + // Mongoose already checked the host name for us, otherwise we need to do + // that ourselves + } + (void) c; +} + +int main(void) { + uint32_t response_ip = inet_addr("192.168.69.11"); + struct mg_mgr mgr; + static struct mg_connection *c; + mg_log_set(MG_LL_DEBUG); // Set log level + mg_mgr_init(&mgr); // Initialise event manager + + // Desired name must NOT have any dots in it, nor a domain + c = mg_mdns_listen(&mgr, fn, "Mongoose"); // Start mDNS server + if (c == NULL) return 1; + // if not using our built-in TCP/IP stack, pass the IP address you want to + // use as a response, this depends on your underlying TCP/IP stack and number + // of interfaces available + memcpy(c->data, &response_ip, sizeof(response_ip)); + + for (;;) mg_mgr_poll(&mgr, 1000); // Event loop + + mg_mgr_free(&mgr); + return 0; +} diff --git a/tutorials/udp/mdns-sd-server/mongoose.c b/tutorials/udp/mdns-sd-server/mongoose.c new file mode 120000 index 0000000000..5e522bbcd4 --- /dev/null +++ b/tutorials/udp/mdns-sd-server/mongoose.c @@ -0,0 +1 @@ +../../../mongoose.c \ No newline at end of file diff --git a/tutorials/udp/mdns-sd-server/mongoose.h b/tutorials/udp/mdns-sd-server/mongoose.h new file mode 120000 index 0000000000..ee4ac82323 --- /dev/null +++ b/tutorials/udp/mdns-sd-server/mongoose.h @@ -0,0 +1 @@ +../../../mongoose.h \ No newline at end of file diff --git a/tutorials/udp/mdns-server/main.c b/tutorials/udp/mdns-server/main.c index 6c4f4179bc..9f57953000 100644 --- a/tutorials/udp/mdns-server/main.c +++ b/tutorials/udp/mdns-server/main.c @@ -11,7 +11,7 @@ int main(void) { mg_mgr_init(&mgr); // Initialise event manager // Desired name must NOT have any dots in it, nor a domain - c = mg_mdns_listen(&mgr, "Mongoose"); // Start mDNS server + c = mg_mdns_listen(&mgr, NULL, "Mongoose"); // Start mDNS server if (c == NULL) return 1; // if not using our built-in TCP/IP stack, pass the IP address you want to // use as a response, this depends on your underlying TCP/IP stack and number