From b455e81b76dd366597c4768ac596b00e6d76f972 Mon Sep 17 00:00:00 2001 From: amcewen Date: Tue, 25 Jan 2011 16:29:38 +0000 Subject: [PATCH] Proposed fix for issue 243, adding DNS to the Ethernet library. Uses a slightly modified version of the agreed API as the host/port parameters have been moved from the Client constructor to the Client::connect methods. This means it's possible for errors to be returned if the DNS lookup fails and also reduces the RAM footprint of the Client class as it no longer needs to store the host/port for later use in Client::connect. --- libraries/Ethernet/Client.cpp | 22 +- libraries/Ethernet/Client.h | 6 +- libraries/Ethernet/Dns.cpp | 423 ++++++++++++++++++ libraries/Ethernet/Dns.h | 41 ++ libraries/Ethernet/IPAddress.h | 4 + .../examples/PachubeClient/PachubeClient.pde | 7 +- .../PachubeClientString.pde | 18 +- .../examples/TelnetClient/TelnetClient.pde | 4 +- .../Ethernet/examples/WebClient/WebClient.pde | 4 +- 9 files changed, 503 insertions(+), 26 deletions(-) create mode 100644 libraries/Ethernet/Dns.cpp create mode 100644 libraries/Ethernet/Dns.h diff --git a/libraries/Ethernet/Client.cpp b/libraries/Ethernet/Client.cpp index e8d7e5c6c..f146ac5ed 100644 --- a/libraries/Ethernet/Client.cpp +++ b/libraries/Ethernet/Client.cpp @@ -10,16 +10,32 @@ extern "C" { #include "Ethernet.h" #include "Client.h" #include "Server.h" +#include "Dns.h" uint16_t Client::_srcport = 1024; +Client::Client() : _sock(MAX_SOCK_NUM) { +} + Client::Client(uint8_t sock) : _sock(sock) { } -Client::Client(IPAddress& ip, uint16_t port) : _ip(ip), _port(port), _sock(MAX_SOCK_NUM) { +int Client::connect(const char* host, uint16_t port) { + // Look up the host first + int ret = 0; + DNSClient dns; + IPAddress remote_addr; + + dns.begin(Ethernet.dnsServerIP()); + ret = dns.getHostByName(host, remote_addr); + if (ret == 1) { + return connect(remote_addr, port); + } else { + return ret; + } } -uint8_t Client::connect() { +int Client::connect(IPAddress ip, uint16_t port) { if (_sock != MAX_SOCK_NUM) return 0; @@ -38,7 +54,7 @@ uint8_t Client::connect() { if (_srcport == 0) _srcport = 1024; socket(_sock, SnMR::TCP, _srcport, 0); - if (!::connect(_sock, _ip.raw_address(), _port)) { + if (!::connect(_sock, ip.raw_address(), port)) { _sock = MAX_SOCK_NUM; return 0; } diff --git a/libraries/Ethernet/Client.h b/libraries/Ethernet/Client.h index 03c9cffe4..582f4938a 100644 --- a/libraries/Ethernet/Client.h +++ b/libraries/Ethernet/Client.h @@ -8,10 +8,10 @@ class Client : public Stream { public: Client(); Client(uint8_t sock); - Client(IPAddress& ip, uint16_t port); uint8_t status(); - uint8_t connect(); + int connect(IPAddress ip, uint16_t port); + int connect(const char *host, uint16_t port); virtual void write(uint8_t); virtual void write(const char *str); virtual void write(const uint8_t *buf, size_t size); @@ -29,8 +29,6 @@ public: private: static uint16_t _srcport; uint8_t _sock; - IPAddress _ip; - uint16_t _port; }; #endif diff --git a/libraries/Ethernet/Dns.cpp b/libraries/Ethernet/Dns.cpp new file mode 100644 index 000000000..3a31e35a4 --- /dev/null +++ b/libraries/Ethernet/Dns.cpp @@ -0,0 +1,423 @@ +// Arduino DNS client for WizNet5100-based Ethernet shield +// (c) Copyright 2009-2010 MCQN Ltd. +// Released under Apache License, version 2.0 + +#include "w5100.h" +#include "Udp.h" +#include "util.h" + +#include "Dns.h" +#include +//#include +#include "wiring.h" + + +#define SOCKET_NONE 255 +// Various flags and header field values for a DNS message +#define UDP_HEADER_SIZE 8 +#define DNS_HEADER_SIZE 12 +#define TTL_SIZE 4 +#define QUERY_FLAG (0) +#define RESPONSE_FLAG (1<<15) +#define QUERY_RESPONSE_MASK (1<<15) +#define OPCODE_STANDARD_QUERY (0) +#define OPCODE_INVERSE_QUERY (1<<11) +#define OPCODE_STATUS_REQUEST (2<<11) +#define OPCODE_MASK (15<<11) +#define AUTHORITATIVE_FLAG (1<<10) +#define TRUNCATION_FLAG (1<<9) +#define RECURSION_DESIRED_FLAG (1<<8) +#define RECURSION_AVAILABLE_FLAG (1<<7) +#define RESP_NO_ERROR (0) +#define RESP_FORMAT_ERROR (1) +#define RESP_SERVER_FAILURE (2) +#define RESP_NAME_ERROR (3) +#define RESP_NOT_IMPLEMENTED (4) +#define RESP_REFUSED (5) +#define RESP_MASK (15) +#define TYPE_A (0x0001) +#define CLASS_IN (0x0001) +#define LABEL_COMPRESSION_MASK (0xC0) +// Port number that DNS servers listen on +#define DNS_PORT 53 + +// Possible return codes from ProcessResponse +#define SUCCESS 1 +#define TIMED_OUT -1 +#define INVALID_SERVER -2 +#define TRUNCATED -3 +#define INVALID_RESPONSE -4 + +void DNSClient::begin(const IPAddress& aDNSServer) +{ + iDNSServer = aDNSServer; + iRequestId = 0; +} + + +int DNSClient::inet_aton(const char* aIPAddrString, IPAddress& aResult) +{ + // See if we've been given a valid IP address + const char* p =aIPAddrString; + while (*p && + ( (*p == '.') || (*p >= '0') || (*p <= '9') )) + { + p++; + } + + if (*p == '\0') + { + // It's looking promising, we haven't found any invalid characters + p = aIPAddrString; + int segment =0; + int segmentValue =0; + while (*p && (segment < 4)) + { + if (*p == '.') + { + // We've reached the end of a segment + if (segmentValue > 255) + { + // You can't have IP address segments that don't fit in a byte + return 0; + } + else + { + aResult[segment] = (byte)segmentValue; + segment++; + segmentValue = 0; + } + } + else + { + // Next digit + segmentValue = (segmentValue*10)+(*p - '0'); + } + p++; + } + // We've reached the end of address, but there'll still be the last + // segment to deal with + if ((segmentValue > 255) || (segment > 3)) + { + // You can't have IP address segments that don't fit in a byte, + // or more than four segments + return 0; + } + else + { + aResult[segment] = (byte)segmentValue; + return 1; + } + } + else + { + return 0; + } +} + +int DNSClient::getHostByName(const char* aHostname, IPAddress& aResult) +{ + int ret =0; + + // See if it's a numeric IP address + if (inet_aton(aHostname, aResult)) + { + // It is, our work here is done + return 1; + } + + // Check we've got a valid DNS server to use + if (iDNSServer == INADDR_NONE) + { + return INVALID_SERVER; + } + + // Find a socket to use + if (iUdp.begin(1024+(millis() & 0xF)) == 1) + { + // Try up to three times + int retries = 0; +// while ((retries < 3) && (ret <= 0)) + { + // Send DNS request + ret = iUdp.beginPacket(iDNSServer, DNS_PORT); + if (ret != 0) + { + // Now output the request data + ret = BuildRequest(aHostname); + if (ret != 0) + { + // And finally send the request + ret = iUdp.endPacket(); + if (ret != 0) + { + // Now wait for a response + int wait_retries = 0; + ret = TIMED_OUT; + while ((wait_retries < 3) && (ret == TIMED_OUT)) + { + ret = ProcessResponse(5000, aResult); + wait_retries++; + } + } + } + } + retries++; + } + + // We're done with the socket now + iUdp.stop(); + } + + return ret; +} + +uint16_t DNSClient::BuildRequest(const char* aName) +{ + // Build header + // 1 1 1 1 1 1 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ID | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // |QR| Opcode |AA|TC|RD|RA| Z | RCODE | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | QDCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ANCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | NSCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ARCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // As we only support one request at a time at present, we can simplify + // some of this header + iRequestId = millis(); // generate a random ID + uint16_t twoByteBuffer; + + // FIXME We should also check that there's enough space available to write to, rather + // FIXME than assume there's enough space (as the code does at present) + iUdp.write((uint8_t*)&iRequestId, sizeof(iRequestId)); + + twoByteBuffer = htons(QUERY_FLAG | OPCODE_STANDARD_QUERY | RECURSION_DESIRED_FLAG); + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + + twoByteBuffer = htons(1); // One question record + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + + twoByteBuffer = 0; // Zero answer records + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + // and zero additional records + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + + // Build question + const char* start =aName; + const char* end =start; + uint8_t len; + // Run through the name being requested + while (*end) + { + // Find out how long this section of the name is + end = start; + while (*end && (*end != '.') ) + { + end++; + } + + if (end-start > 0) + { + // Write out the size of this section + len = end-start; + iUdp.write(&len, sizeof(len)); + // And then write out the section + iUdp.write((uint8_t*)start, end-start); + } + start = end+1; + } + + // We've got to the end of the question name, so + // terminate it with a zero-length section + len = 0; + iUdp.write(&len, sizeof(len)); + // Finally the type and class of question + twoByteBuffer = htons(TYPE_A); + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + + twoByteBuffer = htons(CLASS_IN); // Internet class of question + iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer)); + // Success! Everything buffered okay + return 1; +} + + +uint16_t DNSClient::ProcessResponse(int aTimeout, IPAddress& aAddress) +{ + uint32_t startTime = millis(); + + // Wait for a response packet + while(iUdp.parsePacket() <= 0) + { + if((millis() - startTime) > aTimeout) + return TIMED_OUT; + delay(50); + } + + // We've had a reply! + // Read the UDP header + uint8_t header[DNS_HEADER_SIZE]; // Enough space to reuse for the DNS header + // Check that it's a response from the right server and the right port + if ( (iDNSServer != iUdp.remoteIP()) || + (iUdp.remotePort() != DNS_PORT) ) + { + // It's not from who we expected + return INVALID_SERVER; + } + + // Read through the rest of the response + if (iUdp.available() < DNS_HEADER_SIZE) + { + return TRUNCATED; + } + iUdp.read(header, DNS_HEADER_SIZE); + + uint16_t header_flags = htons(*((uint16_t*)&header[2])); + // Check that it's a response to this request + if ( ( iRequestId != (*((uint16_t*)&header[0])) ) || + (header_flags & QUERY_RESPONSE_MASK != RESPONSE_FLAG) ) + { + // Mark the entire packet as read + iUdp.flush(); + return INVALID_RESPONSE; + } + // Check for any errors in the response (or in our request) + // although we don't do anything to get round these + if ( (header_flags & TRUNCATION_FLAG) || (header_flags & RESP_MASK) ) + { + // Mark the entire packet as read + iUdp.flush(); + return -5; //INVALID_RESPONSE; + } + + // And make sure we've got (at least) one answer + uint16_t answerCount = htons(*((uint16_t*)&header[6])); + if (answerCount == 0 ) + { + // Mark the entire packet as read + iUdp.flush(); + return -6; //INVALID_RESPONSE; + } + + // Skip over any questions + for (int i =0; i < htons(*((uint16_t*)&header[4])); i++) + { + // Skip over the name + uint8_t len; + do + { + iUdp.read(&len, sizeof(len)); + if (len > 0) + { + // Don't need to actually read the data out for the string, just + // advance ptr to beyond it + while(len--) + { + iUdp.read(); // we don't care about the returned byte + } + } + } while (len != 0); + + // Now jump over the type and class + for (int i =0; i < 4; i++) + { + iUdp.read(); // we don't care about the returned byte + } + } + + // Now we're up to the bit we're interested in, the answer + // There might be more than one answer (although we'll just use the first + // type A answer) and some authority and additional resource records but + // we're going to ignore all of them. + + for (int i =0; i < answerCount; i++) + { + // Skip the name + uint8_t len; + do + { + iUdp.read(&len, sizeof(len)); + if ((len & LABEL_COMPRESSION_MASK) == 0) + { + // It's just a normal label + if (len > 0) + { + // And it's got a length + // Don't need to actually read the data out for the string, + // just advance ptr to beyond it + while(len--) + { + iUdp.read(); // we don't care about the returned byte + } + } + } + else + { + // This is a pointer to a somewhere else in the message for the + // rest of the name. We don't care about the name, and RFC1035 + // says that a name is either a sequence of labels ended with a + // 0 length octet or a pointer or a sequence of labels ending in + // a pointer. Either way, when we get here we're at the end of + // the name + // Skip over the pointer + iUdp.read(); // we don't care about the returned byte + // And set len so that we drop out of the name loop + len = 0; + } + } while (len != 0); + + // Check the type and class + uint16_t answerType; + uint16_t answerClass; + iUdp.read((uint8_t*)&answerType, sizeof(answerType)); + iUdp.read((uint8_t*)&answerClass, sizeof(answerClass)); + + // Ignore the Time-To-Live as we don't do any caching + for (int i =0; i < TTL_SIZE; i++) + { + iUdp.read(); // we don't care about the returned byte + } + + // And read out the length of this answer + // Don't need header_flags anymore, so we can reuse it here + iUdp.read((uint8_t*)&header_flags, sizeof(header_flags)); + + if ( (htons(answerType) == TYPE_A) && (htons(answerClass) == CLASS_IN) ) + { + if (htons(header_flags) != 4) + { + // It's a weird size + // Mark the entire packet as read + iUdp.flush(); + return -9;//INVALID_RESPONSE; + } + iUdp.read(aAddress.raw_address(), 4); + return SUCCESS; + } + else + { + // This isn't an answer type we're after, move onto the next one + for (int i =0; i < htons(header_flags); i++) + { + iUdp.read(); // we don't care about the returned byte + } + } + } + + // Mark the entire packet as read + iUdp.flush(); + + // If we get here then we haven't found an answer + return -10;//INVALID_RESPONSE; +} + diff --git a/libraries/Ethernet/Dns.h b/libraries/Ethernet/Dns.h new file mode 100644 index 000000000..9582351f2 --- /dev/null +++ b/libraries/Ethernet/Dns.h @@ -0,0 +1,41 @@ +// Arduino DNS client for WizNet5100-based Ethernet shield +// (c) Copyright 2009-2010 MCQN Ltd. +// Released under Apache License, version 2.0 + +#ifndef DNSClient_h +#define DNSClient_h + +#include + +class DNSClient +{ +public: + // ctor + void begin(const IPAddress& aDNSServer); + + /** Convert a numeric IP address string into a four-byte IP address. + @param aIPAddrString IP address to convert + @param aResult IPAddress structure to store the returned IP address + @result 1 if aIPAddrString was successfully converted to an IP address, + else error code + */ + int inet_aton(const char *aIPAddrString, IPAddress& aResult); + + /** Resolve the given hostname to an IP address. + @param aHostname Name to be resolved + @param aResult IPAddress structure to store the returned IP address + @result 1 if aIPAddrString was successfully converted to an IP address, + else error code + */ + int getHostByName(const char* aHostname, IPAddress& aResult); + +protected: + uint16_t BuildRequest(const char* aName); + uint16_t ProcessResponse(int aTimeout, IPAddress& aAddress); + + IPAddress iDNSServer; + uint16_t iRequestId; + UDP iUdp; +}; + +#endif diff --git a/libraries/Ethernet/IPAddress.h b/libraries/Ethernet/IPAddress.h index 1d19bbd22..487e420bd 100644 --- a/libraries/Ethernet/IPAddress.h +++ b/libraries/Ethernet/IPAddress.h @@ -63,6 +63,10 @@ public: friend class Client; friend class Server; friend class DhcpClass; + friend class DNSClient; }; +const IPAddress INADDR_NONE(0,0,0,0); + + #endif diff --git a/libraries/Ethernet/examples/PachubeClient/PachubeClient.pde b/libraries/Ethernet/examples/PachubeClient/PachubeClient.pde index 687fa437e..af9bf1f05 100644 --- a/libraries/Ethernet/examples/PachubeClient/PachubeClient.pde +++ b/libraries/Ethernet/examples/PachubeClient/PachubeClient.pde @@ -28,11 +28,8 @@ byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; -// The address of the server you want to connect to (pachube.com): -IPAddress server(209,40,205,190); - // initialize the library instance: -Client client(server, 80); +Client client; long lastConnectionTime = 0; // last time you connected to the server, in milliseconds boolean lastConnected = false; // state of the connection last time through the main loop @@ -85,7 +82,7 @@ void loop() { // this method makes a HTTP connection to the server: void sendData(int thisData) { // if there's a successful connection: - if (client.connect()) { + if (client.connect("www.pachube.com", 80)) { Serial.println("connecting..."); // send the HTTP PUT request. // fill in your feed address here: diff --git a/libraries/Ethernet/examples/PachubeClientString/PachubeClientString.pde b/libraries/Ethernet/examples/PachubeClientString/PachubeClientString.pde index 0db6e219a..e6dbf09ea 100644 --- a/libraries/Ethernet/examples/PachubeClientString/PachubeClientString.pde +++ b/libraries/Ethernet/examples/PachubeClientString/PachubeClientString.pde @@ -28,16 +28,9 @@ // fill in your address here: byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; -// assign an IP address for the controller: -IPAddress ip(192,169,1,20); -IPAddress gateway(192,168,1,1); -IPAddress subnet(255, 255, 255, 0); - -// The address of the server you want to connect to (pachube.com): -IPAddress server(209,40,205,190); // initialize the library instance: -Client client(server, 80); +Client client; long lastConnectionTime = 0; // last time you connected to the server, in milliseconds boolean lastConnected = false; // state of the connection last time through the main loop @@ -45,8 +38,13 @@ const int postingInterval = 10000; //delay between updates to Pachube.com void setup() { // start the ethernet connection and serial port: - Ethernet.begin(mac, ip); Serial.begin(9600); + if (Ethernet.begin(mac) == 0) { + Serial.println("Failed to configure Ethernet using DHCP"); + // no point in carrying on, so do nothing forevermore: + for(;;) + ; + } // give the ethernet module time to boot up: delay(1000); } @@ -92,7 +90,7 @@ void loop() { // this method makes a HTTP connection to the server: void sendData(String thisData) { // if there's a successful connection: - if (client.connect()) { + if (client.connect("www.pachube.com", 80)) { Serial.println("connecting..."); // send the HTTP PUT request. // fill in your feed address here: diff --git a/libraries/Ethernet/examples/TelnetClient/TelnetClient.pde b/libraries/Ethernet/examples/TelnetClient/TelnetClient.pde index 95756d076..7502d218d 100644 --- a/libraries/Ethernet/examples/TelnetClient/TelnetClient.pde +++ b/libraries/Ethernet/examples/TelnetClient/TelnetClient.pde @@ -33,7 +33,7 @@ IPAddress server(1,1,1,1); // with the IP address and port of the server // that you want to connect to (port 23 is default for telnet; // if you're using Processing's ChatServer, use port 10002): -Client client(server, 10002); +Client client; void setup() { // start the Ethernet connection: @@ -45,7 +45,7 @@ void setup() { Serial.println("connecting..."); // if you get a connection, report back via serial: - if (client.connect()) { + if (client.connect(server, 10002)) { Serial.println("connected"); } else { diff --git a/libraries/Ethernet/examples/WebClient/WebClient.pde b/libraries/Ethernet/examples/WebClient/WebClient.pde index 74d34e0ce..646c3aadd 100644 --- a/libraries/Ethernet/examples/WebClient/WebClient.pde +++ b/libraries/Ethernet/examples/WebClient/WebClient.pde @@ -23,7 +23,7 @@ IPAddress server(173,194,33,104); // Google // Initialize the Ethernet client library // with the IP address and port of the server // that you want to connect to (port 80 is default for HTTP): -Client client(server, 80); +Client client; void setup() { // start the serial library: @@ -40,7 +40,7 @@ void setup() { Serial.println("connecting..."); // if you get a connection, report back via serial: - if (client.connect()) { + if (client.connect(server, 80)) { Serial.println("connected"); // Make a HTTP request: client.println("GET /search?q=arduino HTTP/1.0");