From a7b41574a83ecd2fc9800b44fd96a5f945aa1f81 Mon Sep 17 00:00:00 2001 From: Anton Thomasson Date: Sun, 13 Oct 2024 19:32:55 +0200 Subject: [PATCH] Add ippdiscover(y) --- Makefile | 7 +- README.md | 4 + bytestream | 2 +- lib/ippdiscovery.cpp | 400 ++++++++++++++++++++++++++++++++++++++++++ lib/ippdiscovery.h | 51 ++++++ lib/list.h | 2 +- lib/map.h | 21 +++ lib/udpsocket.h | 75 ++++++++ tests/Makefile | 2 +- tests/test.cpp | 14 ++ utils/ippdiscover.cpp | 40 +++++ 11 files changed, 613 insertions(+), 5 deletions(-) create mode 100644 lib/ippdiscovery.cpp create mode 100644 lib/ippdiscovery.h create mode 100644 lib/map.h create mode 100644 lib/udpsocket.h create mode 100644 utils/ippdiscover.cpp diff --git a/Makefile b/Makefile index 9e1699b..40e83a7 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ SILLY_CLANG_FLAGS = -Wno-unqualified-std-cast-call VPATH = bytestream lib utils json11 -OFFICIAL = ppm2pwg pwg2ppm pdf2printable baselinify ippclient +OFFICIAL = ppm2pwg pwg2ppm pdf2printable baselinify ippclient ippdiscover EXTRAS = hexdump ippdecode bsplit all: $(OFFICIAL) $(EXTRAS) @@ -61,8 +61,11 @@ ippclient: ippmsg.o ippattr.o ippprinter.o ippprintjob.o printparameters.o ippcl minimime: minimime_main.o minimime.o bytestream.o $(CXX) $^ $(LDFLAGS) -o $@ +ippdiscover: ippdiscover.o ippdiscovery.o bytestream.o + $(CXX) $^ $(LDFLAGS) -o $@ + clean: - rm -f *.o ppm2pwg pwg2ppm pdf2printable pdf2printable_mad hexdump baselinify baselinify_mad bsplit ippclient minimime fuzz + rm -f *.o $(OFFICIAL) $(EXTRAS) analyze: $(CLANGXX) --analyze $(CXXFLAGS) $(SILLY_CLANG_FLAGS) lib/*.cpp utils/*.cpp diff --git a/README.md b/README.md index c17789e..c9a337a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ An IPP client that harnesses the above tools for converting files to be printed. This is a port/rewrite/clean-up of the core parts of SeaPrint in regular (non-Qt) C++. The plan is to swap over to using this once fature parity is achieved. +## ippdiscover + +A DNS-SD/mDNS "simple resolver" which looks for ipp and ipps printers on the local network. + ## Building Install dependencies: diff --git a/bytestream b/bytestream index 4d5435a..5d9b316 160000 --- a/bytestream +++ b/bytestream @@ -1 +1 @@ -Subproject commit 4d5435a88f21d5b6b9e7216801281498a9c86b2a +Subproject commit 5d9b3166a5f46caa8b1aadfe18680ceb5306f7f5 diff --git a/lib/ippdiscovery.cpp b/lib/ippdiscovery.cpp new file mode 100644 index 0000000..76ef537 --- /dev/null +++ b/lib/ippdiscovery.cpp @@ -0,0 +1,400 @@ +#include "ippdiscovery.h" + +#include "stringutils.h" +#include "log.h" + +#include +#include +#include +#include +#include + +using namespace std::literals; + +#define A 1 +#define PTR 12 +#define TXT 16 +#define AAAA 28 +#define SRV 33 + +#define ALL 255 //for querying + +std::ostream& operator<<(std::ostream& os, List sl) +{ + os << join_string(sl, ", "); + return os; +} + +template +std::ostream& operator<<(std::ostream& os, Map m) +{ + List sl; + for(const std::pair p : m) + { + std::stringstream ss; + ss << p.first << ": " << p.second; + sl.push_back(ss.str()); + } + os << sl; + return os; +} + +std::string ip4str(uint32_t ip) +{ + return std::to_string((ip >> 24) & 0xff) + "." + + std::to_string((ip >> 16) & 0xff) + "." + + std::to_string((ip >> 8) & 0xff) + "." + + std::to_string(ip & 0xff); +} + +std::string make_addr(std::string proto, uint16_t defaultPort, uint16_t port, std::string ip, std::string rp) +{ + std::string maybePort = port != defaultPort ? ":"+std::to_string(port) : ""; + std::string addr = proto+"://"+ip+maybePort+"/"+rp; + return addr; +} + +std::string make_ipp_addr(uint16_t port, std::string ip, std::string rp) +{ + return make_addr("ipp", 631, port, ip, rp); +} + +std::string make_ipps_addr(uint16_t port, std::string ip, std::string rp) +{ + return make_addr("ipps", 443, port, ip, rp); +} + +List get_addr(Bytestream& bts, std::set seenReferences={}) +{ + List addr; + while(!(bts.next(0))) + { + if((bts.peek()&0xc0)==0xc0) + { + uint16_t ref = bts.get() & 0x0fff; + if(seenReferences.count(ref) != 0) + { + throw(std::logic_error("Circular reference")); + } + seenReferences.insert(ref); + Bytestream tmp = bts; + tmp.setPos(ref); + addr += get_addr(tmp, seenReferences); + break; + } + else + { + addr.push_back(bts.getString(bts.get())); + } + } + return addr; +} + +std::string get_addr_str(Bytestream& bts) +{ + return join_string(get_addr(bts), "."); +} + +IppDiscovery::IppDiscovery(std::function callback) +: _callback(callback) +{ +} + +IppDiscovery::~IppDiscovery() +{ +} + +void IppDiscovery::sendQuery(uint16_t qtype, List addrs) +{ + std::chrono::time_point nowClock = std::chrono::system_clock::now(); + std::time_t now = std::chrono::system_clock::to_time_t(nowClock); + std::time_t aWhileAgo = std::chrono::system_clock::to_time_t(nowClock - 2s); + + for(const auto& [k, v] : _outstandingQueries) + { + if(_outstandingQueries[k] < aWhileAgo) + { // Housekeeping for _outstandingQueries + DBG(<< "skipping " << k.second); + _outstandingQueries.erase(k); + } + else if(k.first == qtype && addrs.contains(k.second)) + { // we recently asked about this, remove it + addrs.remove(k.second); + } + } + + if(addrs.empty()) + { + return; + } + + DBG(<< "querying " << qtype << " " << addrs); + + Bytestream query; + Map suffixPositions; + + uint16_t flags = 0; + uint16_t questions = addrs.size(); + + query << _transactionId++ << flags << questions << (uint16_t)0 << (uint16_t)0 << (uint16_t)0; + + for(std::string addr : addrs) + { + _outstandingQueries.insert({{qtype, addr}, now}); + + List addrParts = split_string(addr, "."); + std::string addrPart, restAddr; + while(!addrParts.empty()) + { + restAddr = join_string(addrParts, "."); + if(suffixPositions.contains(restAddr)) + { + query << (uint16_t)(0xc000 | (0x0fff & suffixPositions[restAddr])); + break; + } + else + { + // We are putting in at least one part of the address, remember where that was + suffixPositions.insert({restAddr, query.size()}); + addrPart = addrParts.takeFront(); + query << (uint8_t)addrPart.size() << addrPart; + } + } + if(addrParts.empty()) + { + // Whole addr was put in without c-pointers, 0-terminate it + query << (uint8_t)0; + } + + query << qtype << (uint16_t)0x0001; + + } + + _socket.send(query); + +} + +void IppDiscovery::update() +{ + List> ippsIpRps; + std::string target, rp; + + for(std::string it : _ippsPtrs) + { + if(!_targets.contains(it) || !_ports.contains(it) || !_rps.contains(it)) + { + continue; + } + + uint16_t port = _ports.at(it); + target = _targets.at(it); + rp = _rps.at(it); + + if(_As.contains(target)) + { + for(std::string ip : _As.at(target)) + { + std::string addr = make_ipps_addr(port, ip, rp); + if(!_found.contains(addr)) + { // Don't add duplicates + ippsIpRps.push_back({ip, rp}); + _found.push_back(addr); + _callback(addr); + } + } + } + } + + for(std::string it : _ippPtrs) + { + if(!_targets.contains(it) || !_ports.contains(it) || !_rps.contains(it)) + { + continue; + } + + uint16_t port = _ports.at(it); + target = _targets.at(it); + rp = _rps.at(it); + + if(_As.contains(target)) + { + for(std::string ip : _As.at(target)) + { + std::string addr = make_ipp_addr(port, ip, rp); + if(!_found.contains(addr) && !ippsIpRps.contains({ip, rp})) + { // Don't add duplicates, don't add if already added as IPPS + _found.push_back(addr); + _callback(addr); + } + } + } + } +} + +void IppDiscovery::updateAndQueryPtrs(List& ptrs, List newPtrs) +{ + for(std::string ptr : newPtrs) + { + if(ptrs.contains(ptr)) + { + continue; + } + else + { + ptrs.push_back(ptr); + } + // If pointer does not resolve to a target or is missing information, query about it + if(!_rps.contains(ptr)) + { + // Avahi *really* hates sending TXT to anything else than a TXT query + sendQuery(TXT, {ptr}); + } + if(!_targets.contains(ptr) || !_ports.contains(ptr)) + { + sendQuery(SRV, {ptr}); + } + } +} + +void IppDiscovery::discover() +{ + sendQuery(PTR, {"_ipp._tcp.local", "_ipps._tcp.local"}); + + Bytestream resp; + + while((resp = _socket.receive(2))) + { + List newIppPtrs; + List newIppsPtrs; + List newTargets; + + std::string qaddr, aaddr, tmpname, target; + + uint16_t transactionidResp, flags, questions, answerRRs, authRRs, addRRs; + + try + { + resp >> transactionidResp >> flags >> questions >> answerRRs >> authRRs >> addRRs; + size_t totalRRs = answerRRs + authRRs + addRRs; + + for(size_t i = 0; i < questions; i++) + { + uint16_t qtype, qflags; + qaddr = get_addr_str(resp); + resp >> qtype >> qflags; + } + + for(size_t i = 0; i < totalRRs; i++) + { + uint16_t atype, aflags, len; + uint32_t ttl; + + aaddr = get_addr_str(resp); + resp >> atype >> aflags >> ttl >> len; + + uint16_t pos_before = resp.pos(); + switch(atype) + { + case PTR: + { + tmpname = get_addr_str(resp); + if(string_ends_with(aaddr, "_ipp._tcp.local")) + { + newIppPtrs.push_back(tmpname); + } + else if(string_ends_with(aaddr, "_ipps._tcp.local")) + { + newIppsPtrs.push_back(tmpname); + } + break; + } + case TXT: + { + while(resp.pos() < pos_before+len) + { + uint8_t len = resp.get(); + if(resp >>= std::string("rp=")) + { + std::string tmprp = resp.getString(len - 3); + _rps[aaddr] = tmprp; + } + else + { + resp += len; + } + } + break; + } + case SRV: + { + uint16_t prio, w, port; + resp >> prio >> w >> port; + target = get_addr_str(resp); + _ports[aaddr] = port; + _targets[aaddr] = target; + if(!newTargets.contains(target)) + { + newTargets.push_back(target); + } + break; + } + case A: + { + std::string ip = ip4str(resp.get()); + _As[aaddr].push_back(ip); + break; + } + default: + { + resp += len; + break; + } + + if(resp.pos() != pos_before + len) + { + throw(std::logic_error("failed to parse DNS record")); + } + } + } + } + catch(const std::exception& e) + { + std::cerr << e.what(); + } + + DBG(<< "------------"); + DBG(<< "new ipp ptrs: " << newIppPtrs); + DBG(<< "new ipps ptrs: " << newIppsPtrs); + DBG(<< "ipp ptrs: " << _ippPtrs); + DBG(<< "ipps ptrs: " << _ippsPtrs); + DBG(<< "rps: " << _rps); + DBG(<< "ports: " << _ports); + DBG(<< "new targets: " << newTargets); + DBG(<< "targets: " << _targets); + DBG(<< "As: " << _As); + + // These will send one query per unique new ptr. + // some responders doesn't give TXT records for more than one thing at at time :( + updateAndQueryPtrs(_ippsPtrs, newIppsPtrs); + updateAndQueryPtrs(_ippPtrs, newIppPtrs); + + List unresolvedAddrs; + + for(std::string t : newTargets) + { + // If target does not resolve to an address, query about it + if(!_As.contains(t)) + { + unresolvedAddrs.push_back(t); + } + } + + if(!unresolvedAddrs.empty()) + { + sendQuery(A, unresolvedAddrs); + } + + update(); + + } +} diff --git a/lib/ippdiscovery.h b/lib/ippdiscovery.h new file mode 100644 index 0000000..a6aa1f0 --- /dev/null +++ b/lib/ippdiscovery.h @@ -0,0 +1,51 @@ +#ifndef IPPDISCOVERY_H +#define IPPDISCOVERY_H + +#include +#include +#include +#include + +#include "map.h" +#include "list.h" +#include "bytestream.h" +#include "udpsocket.h" + +class IppDiscovery +{ +public: + IppDiscovery(std::function callback); + + IppDiscovery() = delete; + IppDiscovery(const IppDiscovery&) = delete; + IppDiscovery& operator=(const IppDiscovery&) = delete; + ~IppDiscovery(); + + void discover(); + +private: + void sendQuery(uint16_t qtype, List addrs); + void update(); + void updateAndQueryPtrs(List& ptrs, List new_ptrs); + + UdpSocket _socket = UdpSocket("224.0.0.251", 5353); + + uint16_t _transactionId = 0; + + List _ippPtrs; + List _ippsPtrs; + + Map _rps; + Map _ports; + Map _targets; + + Map> _As; + + Map, std::time_t> _outstandingQueries; + List _found; + + std::function _callback; + +}; + +#endif // IPPDISCOVERY_H diff --git a/lib/list.h b/lib/list.h index 5a33111..8c4cff7 100644 --- a/lib/list.h +++ b/lib/list.h @@ -36,7 +36,7 @@ class List: public std::list List& operator+=(const List& other) { - this->insert(this->cbegin(), other.cbegin(), other.cend()); + this->insert(this->cend(), other.cbegin(), other.cend()); return *this; } }; diff --git a/lib/map.h b/lib/map.h new file mode 100644 index 0000000..a7d6814 --- /dev/null +++ b/lib/map.h @@ -0,0 +1,21 @@ +#ifndef MAP_H +#define MAP_H + +#include + +template +class Map: public std::map +{ +public: + using std::map::map; + +#if __cplusplus < 202002L + bool contains(const K elem) const + { + return std::map::find(elem) != std::map::cend(); + } +#endif + +}; + +#endif // MAP_H diff --git a/lib/udpsocket.h b/lib/udpsocket.h new file mode 100644 index 0000000..70d33e6 --- /dev/null +++ b/lib/udpsocket.h @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "bytestream.h" + +class UdpSocket +{ + UdpSocket() = delete; + UdpSocket(const UdpSocket&) = delete; + UdpSocket& operator=(const UdpSocket&) = delete; + +public: + + UdpSocket(std::string addr, uint16_t port) + { + // Creating socket file descriptor + if((_sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) + { + throw(std::runtime_error("socket creation failed")); + } + + memset(&_servaddr, 0, sizeof(_servaddr)); + _servaddr.sin_family = AF_INET; + _servaddr.sin_port = htons(port); + _servaddr.sin_addr.s_addr = inet_addr(addr.c_str()); + } + + ~UdpSocket() + { + close(_sock); + } + + ssize_t send(Bytestream msg) + { + return sendto(_sock, (const char*)msg.raw(), msg.size(), MSG_CONFIRM, + (const sockaddr*)&_servaddr, sizeof(_servaddr)); + } + + Bytestream receive(time_t tmo_sec=0, time_t tmo_usec=0) + { + Bytestream msg; + char buffer[BS_REASONABLE_FILE_SIZE]; + + timeval tv; + tv.tv_sec = tmo_sec; + tv.tv_usec = tmo_usec; + if(setsockopt(_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) + { + throw(std::runtime_error("setsockopt failed")); + } + + sockaddr_in servaddr; + memset(&servaddr, 0, sizeof(servaddr)); + socklen_t len = sizeof(servaddr); + + ssize_t n = recvfrom(_sock, buffer, sizeof(buffer), 0, (sockaddr*)&servaddr, &len); + if(n > 0) + { + msg = Bytestream(buffer, n); + } + return msg; + } + +private: + int _sock; + sockaddr_in _servaddr; + +}; diff --git a/tests/Makefile b/tests/Makefile index 3bfdbda..e518889 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -17,7 +17,7 @@ json11.o: json11.cpp %.o: %.cpp $(CXX) -c $(CXXFLAGS) $< -test: bytestream.o ippprinter.o ippprintjob.o curlrequester.o printparameters.o ppm2pwg.o pwg2ppm.o pdf2printable.o baselinify.o ippmsg.o ippattr.o json11.o minimime.o test.o +test: bytestream.o ippprinter.o ippprintjob.o curlrequester.o printparameters.o ppm2pwg.o pwg2ppm.o pdf2printable.o baselinify.o ippmsg.o ippattr.o json11.o minimime.o ippdiscovery.o test.o $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ clean: diff --git a/tests/test.cpp b/tests/test.cpp index 4f87bb9..3cbed33 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -2256,3 +2256,17 @@ TEST(converter) == "application/bbb"); } + +extern List get_addr(Bytestream& bts, std::set seenReferences={}); + +TEST(malicious_dns) +{ + Bytestream bts; + ASSERT_THROW(get_addr(bts), out_of_range); + + // Address with a c-reference loop tr.ol.ol.ol... + bts << (uint8_t)2 << "tr" << (uint8_t)2 << "ol" << (uint16_t)0xc003; + + ASSERT_THROW(get_addr(bts), logic_error); + +} diff --git a/utils/ippdiscover.cpp b/utils/ippdiscover.cpp new file mode 100644 index 0000000..d4793ac --- /dev/null +++ b/utils/ippdiscover.cpp @@ -0,0 +1,40 @@ +#include +#include "ippdiscovery.h" +#include "argget.h" +#include "log.h" + +inline void print_error(std::string hint, std::string argHelp) +{ + std::cerr << hint << std::endl << std::endl << argHelp << std::endl; +} + +int main(int argc, char** argv) +{ + bool help = false; + bool verbose = false; + + SwitchArg helpOpt(help, {"-h", "--help"}, "Print this help text"); + SwitchArg verboseOpt(verbose, {"-v", "--verbose"}, "Be verbose"); + + ArgGet args({&helpOpt, &verboseOpt}); + + bool correctArgs = args.get_args(argc, argv); + if(help) + { + std::cout << args.argHelp() << std::endl; + return 0; + } + else if(!correctArgs) + { + print_error(args.errmsg(), args.argHelp()); + return 1; + } + + if(verbose) + { + LogController::instance().enable(LogController::Debug); + } + + IppDiscovery ippDiscovery([](std::string addr){std::cout << addr << std::endl;}); + ippDiscovery.discover(); +}