diff --git a/libraries/ESP8266mDNS/ESP8266mDNS.cpp b/libraries/ESP8266mDNS/ESP8266mDNS.cpp index 5721c5272..640aa2754 100644 --- a/libraries/ESP8266mDNS/ESP8266mDNS.cpp +++ b/libraries/ESP8266mDNS/ESP8266mDNS.cpp @@ -4,6 +4,8 @@ ESP8266 Multicast DNS (port of CC3000 Multicast DNS library) Version 1.1 Copyright (c) 2013 Tony DiCola (tony@tonydicola.com) ESP8266 port (c) 2015 Ivan Grokhotkov (ivan@esp8266.com) +MDNS-SD Suport 2015 Hristo Gochkov + License (MIT license): Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,14 +36,15 @@ License (MIT license): #include "ESP8266mDNS.h" #include -extern "C" -{ +#include "debug.h" + +extern "C" { #include "osapi.h" #include "ets_sys.h" + #include "ip_addr.h" + #include "user_interface.h" } -#include "debug.h" - #include "WiFiUdp.h" #include "lwip/opt.h" #include "lwip/udp.h" @@ -52,131 +55,58 @@ extern "C" +//#define MDNS_DEBUG_ERR +//#define MDNS_DEBUG_TX +//#define MDNS_DEBUG_RX -// #define MDNS_DEBUG +#define MDNS_NAME_REF 0xC000 -#define HEADER_SIZE 12 -#define QDCOUNT_OFFSET 4 -#define A_RECORD_SIZE 14 -#define NSEC_RECORD_SIZE 20 -#define TTL_OFFSET 4 -#define IP_OFFSET 10 +#define MDNS_TYPE_AAAA 0x001C +#define MDNS_TYPE_A 0x0001 +#define MDNS_TYPE_PTR 0x000C +#define MDNS_TYPE_SRV 0x0021 +#define MDNS_TYPE_TXT 0x0010 +#define MDNS_TYPE_NSEC 0x002F + +#define MDNS_CLASS_IN 0x0001 +#define MDNS_CLASS_IN_FLUSH_CACHE 0x8001 + +#define _conn_read32() (((uint32_t)_conn->read() << 24) | ((uint32_t)_conn->read() << 16) | ((uint32_t)_conn->read() << 8) | _conn->read()) +#define _conn_read16() (((uint16_t)_conn->read() << 8) | _conn->read()) +#define _conn_read8() _conn->read() +#define _conn_readS(b,l) _conn->read((char*)(b),l); static const IPAddress MDNS_MULTICAST_ADDR(224, 0, 0, 251); static const int MDNS_MULTICAST_TTL = 1; static const int MDNS_PORT = 5353; +MDNSResponder::MDNSResponder() : _conn(0) { _services = 0; } +MDNSResponder::~MDNSResponder() {} -MDNSResponder::MDNSResponder() - : _expected(NULL) - , _expectedLen(0) - , _response(NULL) - , _responseLen(0) - , _index(0) - , _conn(0) -{ } - -MDNSResponder::~MDNSResponder() { - if (_expected) { - delete[] _expected; - } - if (_response) { - delete[] _response; - } -} - -bool MDNSResponder::begin(const char* domain, IPAddress addr, uint32_t ttlSeconds) -{ - _localAddr = addr; - // Construct DNS request/response fully qualified domain name of form: - // , , 5, "local" +bool MDNSResponder::begin(const char* domain){ + // Open the MDNS socket if it isn't already open. + size_t n = strlen(domain); - if (n > 255) { - // Can only handle domains that are 255 chars in length. + if (n > 255) { // Can only handle domains that are 255 chars in length. return false; } - _expectedLen = 12 + n; - if (_expected) { - delete[] _expected; - } - _expected = new uint8_t[_expectedLen]; - _expected[0] = (uint8_t)n; - + // Copy in domain characters as lowercase - for (int i = 0; i < n; ++i) { - _expected[1+i] = tolower(domain[i]); - } - - // Values for: - // - 5 (length) - // - "local" - // - 0x00 (end of domain) - // - 0x00 0x01 (A record query) - // - 0x00 0x01 (Class IN) - const uint8_t local[] = { 0x05, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x00, 0x01 }; - memcpy(&_expected[1+n], local, 11); - - // Construct DNS query response - // TODO: Move these to flash or just construct in code. - const uint8_t respHeader[] = { 0x00, 0x00, // ID = 0 - 0x84, 0x00, // Flags = response + authoritative answer - 0x00, 0x00, // Question count = 0 - 0x00, 0x01, // Answer count = 1 - 0x00, 0x00, // Name server records = 0 - 0x00, 0x01 // Additional records = 1 - }; - // Generate positive response for IPV4 address - const uint8_t aRecord[] = { 0x00, 0x01, // Type = 1, A record/IPV4 address - 0x80, 0x01, // Class = Internet, with cache flush bit - 0x00, 0x00, 0x00, 0x00, // TTL in seconds, to be filled in later - 0x00, 0x04, // Length of record - 0x00, 0x00, 0x00, 0x00 // IP address, to be filled in later - }; - // Generate negative response for IPV6 address (CC3000 doesn't support IPV6) - const uint8_t nsecRecord[] = { 0xC0, 0x0C, // Name offset - 0x00, 0x2F, // Type = 47, NSEC (overloaded by MDNS) - 0x80, 0x01, // Class = Internet, with cache flush bit - 0x00, 0x00, 0x00, 0x00, // TTL in seconds, to be filled in later - 0x00, 0x08, // Length of record - 0xC0, 0x0C, // Next domain = offset to FQDN - 0x00, // Block number = 0 - 0x04, // Length of bitmap = 4 bytes - 0x40, 0x00, 0x00, 0x00 // Bitmap value = Only first bit (A record/IPV4) is set - }; - - // Allocate memory for response. - int queryFQDNLen = _expectedLen - 4; - _responseLen = HEADER_SIZE + queryFQDNLen + A_RECORD_SIZE + NSEC_RECORD_SIZE; - if (_response) { - delete[] _response; - } - _response = new uint8_t[_responseLen]; - - // Copy data into response. - memcpy(_response, respHeader, HEADER_SIZE); - memcpy(_response + HEADER_SIZE, _expected, queryFQDNLen); - uint8_t* records = _response + HEADER_SIZE + queryFQDNLen; - memcpy(records, aRecord, A_RECORD_SIZE); - memcpy(records + A_RECORD_SIZE, nsecRecord, NSEC_RECORD_SIZE); - - // Add TTL to records. - uint8_t ttl[4] = { (uint8_t)(ttlSeconds >> 24), (uint8_t)(ttlSeconds >> 16), (uint8_t)(ttlSeconds >> 8), (uint8_t)ttlSeconds }; - memcpy(records + TTL_OFFSET, ttl, 4); - memcpy(records + A_RECORD_SIZE + 2 + TTL_OFFSET, ttl, 4); - - // Add IP address to response - - uint32_t ipAddress = (uint32_t) addr; - - records[IP_OFFSET + 3] = (uint8_t)(ipAddress >> 24); - records[IP_OFFSET + 2] = (uint8_t)(ipAddress >> 16); - records[IP_OFFSET + 1] = (uint8_t)(ipAddress >> 8); - records[IP_OFFSET + 0] = (uint8_t) ipAddress; + for (int i = 0; i < n; ++i) + _hostName[i] = tolower(domain[i]); + _hostName[n] = '\0'; + + os_strcpy(_boardName, ARDUINO_BOARD); // Open the MDNS socket if it isn't already open. if (!_conn) { + uint32_t ourIp = _getOurIp(); + if(ourIp == 0){ + return false; + } + ip_addr_t ifaddr; - ifaddr.addr = (uint32_t) addr; + ifaddr.addr = ourIp; ip_addr_t multicast_addr; multicast_addr.addr = (uint32_t) MDNS_MULTICAST_ADDR; @@ -202,55 +132,369 @@ void MDNSResponder::update() { if (!_conn->next()) { return; } + _parsePacket(); +} - // Read available data. - int n = _conn->getSize(); +void MDNSResponder::addService(char *name, char *proto, uint16_t port){ + if(_getServicePort(name, proto) != 0) return; + if(os_strlen(name) > 32 || os_strlen(proto) != 3) return; //bad arguments + struct MDNSService *srv = (struct MDNSService*)(os_malloc(sizeof(struct MDNSService))); + os_strcpy(srv->_name, name); + os_strcpy(srv->_proto, proto); + srv->_port = port; + srv->_next = 0; + if(_services) _services->_next = srv; + else _services = srv; +} - _index = 0; - -#ifdef MDNS_DEBUG - Serial.println("{"); -#endif - // Look for domain name in request and respond with canned response if found. - for (int i = 0; i < n; ++i) { - uint8_t ch = tolower(_conn->read()); - -#ifdef MDNS_DEBUG - String str(ch, 16); - Serial.print("0x"); - Serial.print(str); - Serial.print(", "); -#endif - // Check character matches expected. - if (ch == _expected[_index]) - { - _index++; - // Check if domain name was found and send a response. - if (_index == _expectedLen) { - // Send response to multicast address. -#ifdef MDNS_DEBUG - Serial.print("responding, i="); - Serial.println(i); -#endif - ip_addr_t multicast_addr; - multicast_addr.addr = (uint32_t) MDNS_MULTICAST_ADDR; - - _conn->append(reinterpret_cast(_response), _responseLen); - _conn->send(); - - _index = 0; - } - } - else if (ch == _expected[0]) { - // Found a character that doesn't match, but does match the start of the domain. - _index = 1; - } - else { - // Found a character that doesn't match the expected character or start of domain. - _index = 0; +uint16_t MDNSResponder::_getServicePort(char *name, char *proto){ + MDNSService* servicePtr; + for (servicePtr = _services; servicePtr; servicePtr = servicePtr->_next) { + if(servicePtr->_port > 0 && strcmp(servicePtr->_name, name) == 0 && strcmp(servicePtr->_proto, proto) == 0){ + return servicePtr->_port; } } -#ifdef MDNS_DEBUG - Serial.println("}"); -#endif + return 0; } + +uint32_t MDNSResponder::_getOurIp(){ + if(wifi_get_opmode() & STATION_MODE){ + struct ip_info staIpInfo; + wifi_get_ip_info(STATION_IF, &staIpInfo); + return staIpInfo.ip.addr; + } +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_NO_LOCAL_IP\n"); +#endif + return 0; +} + +void MDNSResponder::_parsePacket(){ + int i; + char tmp; + bool serviceParsed = false; + bool protoParsed = false; + bool localParsed = false; + + char hostName[255]; + uint8_t hostNameLen; + + char serviceName[32]; + uint8_t serviceNameLen; + uint16_t servicePort; + + char protoName[32]; + uint8_t protoNameLen; + + uint16_t packetHeader[6]; + + for(i=0; i<6; i++) packetHeader[i] = _conn_read16(); + + if((packetHeader[1] & 0x8000) != 0){ //not parsing responses yet + _conn->flush(); + return; + } + + // PARSE REQUEST NAME + + hostNameLen = _conn_read8(); + _conn_readS(hostName, hostNameLen); + hostName[hostNameLen] = '\0'; + + if(hostName[0] == '_'){ + serviceParsed = true; + memcpy(serviceName, hostName+1, hostNameLen); + serviceNameLen = hostNameLen-1; + hostNameLen = 0; + } + + if(hostNameLen > 0 && strcmp(_hostName, hostName) != 0){ +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_NO_HOST: %s\n", hostName); +#endif + _conn->flush(); + return; + } + + if(!serviceParsed){ + serviceNameLen = _conn_read8(); + _conn_readS(serviceName, serviceNameLen); + serviceName[serviceNameLen] = '\0'; + + if(serviceName[0] == '_'){ + memcpy(serviceName, serviceName+1, serviceNameLen); + serviceNameLen--; + serviceParsed = true; + } else if(serviceNameLen == 5 && strcmp("local", serviceName) == 0){ + tmp = _conn_read8(); + if(tmp == 0){ + serviceParsed = true; + serviceNameLen = 0; + protoParsed = true; + protoNameLen = 0; + localParsed = true; + } else { +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_FQDN: %s\n", serviceName); +#endif + _conn->flush(); + return; + } + } else { +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_SERVICE: %s\n", serviceName); +#endif + _conn->flush(); + return; + } + } + + if(!protoParsed){ + protoNameLen = _conn_read8(); + _conn_readS(protoName, protoNameLen); + protoName[protoNameLen] = '\0'; + if(protoNameLen == 4 && protoName[0] == '_'){ + memcpy(protoName, protoName+1, protoNameLen); + protoNameLen--; + protoParsed = true; + } else { +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_PROTO: %s\n", protoName); +#endif + _conn->flush(); + return; + } + } + + if(!localParsed){ + char localName[32]; + uint8_t localNameLen = _conn_read8(); + _conn_readS(localName, localNameLen); + localName[localNameLen] = '\0'; + tmp = _conn_read8(); + if(localNameLen == 5 && strcmp("local", localName) == 0 && tmp == 0){ + localParsed = true; + } else { +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_FQDN: %s\n", localName); +#endif + _conn->flush(); + return; + } + } + + if(serviceNameLen > 0 && protoNameLen > 0){ + servicePort = _getServicePort(serviceName, protoName); + if(servicePort == 0){ +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_NO_SERVICE: %s\n", serviceName); +#endif + _conn->flush(); + return; + } + } else if(serviceNameLen > 0 || protoNameLen > 0){ +#ifdef MDNS_DEBUG_ERR + os_printf("ERR_SERVICE_PROTO: %s\n", serviceName); +#endif + _conn->flush(); + return; + } + + // RESPOND + +#ifdef MDNS_DEBUG_RX + os_printf("RX: REQ, ID:%u, Q:%u, A:%u, NS:%u, ADD:%u\n", packetHeader[0], packetHeader[2], packetHeader[3], packetHeader[4], packetHeader[5]); +#endif + + uint16_t currentType; + uint16_t currentClass; + + int numQuestions = packetHeader[2]; + if(numQuestions > 4) numQuestions = 4; + uint16_t questions[4]; + int question = 0; + + while(numQuestions--){ + currentType = _conn_read16(); + if(currentType & MDNS_NAME_REF){ //new header handle it better! + currentType = _conn_read16(); + } + currentClass = _conn_read16(); + if(currentClass & MDNS_CLASS_IN) questions[question++] = currentType; + + if(numQuestions > 0){ + if(_conn_read16() != 0xC00C){//new question but for another host/service + _conn->flush(); + numQuestions = 0; + } + } + +#ifdef MDNS_DEBUG_RX + os_printf("REQ: "); + if(hostNameLen > 0) os_printf("%s.", hostName); + if(serviceNameLen > 0) os_printf("_%s.", serviceName); + if(protoNameLen > 0) os_printf("_%s.", protoName); + os_printf("local. "); + + if(currentType == MDNS_TYPE_AAAA) os_printf(" AAAA "); + else if(currentType == MDNS_TYPE_A) os_printf(" A "); + else if(currentType == MDNS_TYPE_PTR) os_printf(" PTR "); + else if(currentType == MDNS_TYPE_SRV) os_printf(" SRV "); + else if(currentType == MDNS_TYPE_TXT) os_printf(" TXT "); + else if(currentType == MDNS_TYPE_NSEC) os_printf(" NSEC "); + else os_printf(" 0x%04X ", currentType); + + if(currentClass == MDNS_CLASS_IN) os_printf(" IN "); + else if(currentClass == MDNS_CLASS_IN_FLUSH_CACHE) os_printf(" IN[F] "); + else os_printf(" 0x%04X ", currentClass); + + os_printf("\n"); +#endif + } + uint8_t responseMask = 0; + for(i=0;iflush(); + uint8_t head[12] = { + 0x00, 0x00, //ID = 0 + 0x84, 0x00, //Flags = response + authoritative answer + 0x00, 0x00, //Question count + 0x00, answerCount, //Answer count + 0x00, 0x00, //Name server records + 0x00, 0x00, //Additional records + }; + _conn->append(reinterpret_cast(head), 12); + + if((replyMask & 0x8) == 0){ + _conn->append(reinterpret_cast(&nameLen), 1); + _conn->append(reinterpret_cast(_hostName), nameLen); + } + + if(replyMask & 0xE){ + uint8_t servHead[2] = {(uint8_t)(serviceLen+1), '_'}; + uint8_t protoHead[2] = {0x4, '_'}; + _conn->append(reinterpret_cast(servHead), 2); + _conn->append(reinterpret_cast(service), serviceLen); + _conn->append(reinterpret_cast(protoHead), 2); + _conn->append(reinterpret_cast(proto), 3); + } + + uint8_t local[7] = { + 0x05, //strlen(_local) + 0x6C, 0x6F, 0x63, 0x61, 0x6C, //local + 0x00, //End of domain + }; + _conn->append(reinterpret_cast(local), 7); + + // PTR Response + if(replyMask & 0x8){ + uint8_t ptr[10] = { + 0x00, 0x0c, //PTR record query + 0x00, 0x01, //Class IN + 0x00, 0x00, 0x11, 0x94, //TTL 4500 + 0x00, (uint8_t)(3 + nameLen), //***DATA LEN (3 + strlen(host)) + }; + _conn->append(reinterpret_cast(ptr), 10); + _conn->append(reinterpret_cast(&nameLen), 1); + _conn->append(reinterpret_cast(_hostName), nameLen); + uint8_t ptrTail[2] = {0xC0, 0x0C}; + _conn->append(reinterpret_cast(ptrTail), 2); + } + + // TXT Response + if(replyMask & 0x4){ + if(replyMask & 0x8){//send the name + uint8_t txtHead[2] = {0xC0, (uint8_t)(36 + serviceLen)}; + _conn->append(reinterpret_cast(txtHead), 2); + } + + uint8_t boardNameLen = os_strlen(_boardName); + + uint8_t txt[24] = { + 0x00, 0x10, //Type TXT + 0x80, 0x01, //Class IN, with cache flush + 0x00, 0x00, 0x11, 0x94, //TTL 4500 + 0x00, 0x0e, //DATA LEN + (uint8_t)(6 + boardNameLen), //strlen(board=) + strlen(boardName) + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x3d //board= + }; + _conn->append(reinterpret_cast(txt), 17); + _conn->append(reinterpret_cast(_boardName), boardNameLen); + + } + + // SRV Response + if(replyMask & 0x2){ + if(replyMask & 0xC){//send the name + uint8_t srvHead[2] = {0xC0, 0x0C}; + if(replyMask & 0x8) + srvHead[1] = 36 + serviceLen; + _conn->append(reinterpret_cast(srvHead), 2); + } + + uint8_t srv[16] = { + 0x00, 0x21, //Type SRV + 0x80, 0x01, //Class IN, with cache flush + 0x00, 0x00, 0x00, 0x78, //TTL 120 + 0x00, (uint8_t)(9 + nameLen), //DATA LEN (9 + strlen(host)) + 0x00, 0x00, //Priority 0 + 0x00, 0x00, //Weight 0 + (uint8_t)((port >> 8) & 0xFF), (uint8_t)(port & 0xFF) + }; + _conn->append(reinterpret_cast(srv), 16); + _conn->append(reinterpret_cast(&nameLen), 1); + _conn->append(reinterpret_cast(_hostName), nameLen); + uint8_t srvTail[2] = {0xC0, (uint8_t)(20 + serviceLen + nameLen)}; + if(replyMask & 0x8) + srvTail[1] = 19 + serviceLen; + _conn->append(reinterpret_cast(srvTail), 2); + } + + // A Response + if(replyMask & 0x1){ + uint32_t ip = _getOurIp(); + if(replyMask & 0x2){//send the name (no compression for now) + _conn->append(reinterpret_cast(&nameLen), 1); + _conn->append(reinterpret_cast(_hostName), nameLen); + _conn->append(reinterpret_cast(local), 7); + } + + uint8_t aaa[14] = { + 0x00, 0x01, //TYPE A + 0x80, 0x01, //Class IN, with cache flush + 0x00, 0x00, 0x00, 0x78, //TTL 120 + 0x00, 0x04, //DATA LEN + (uint8_t)(ip & 0xFF), (uint8_t)((ip >> 8) & 0xFF), (uint8_t)((ip >> 16) & 0xFF), (uint8_t)((ip >> 24) & 0xFF) + }; + _conn->append(reinterpret_cast(aaa), 14); + } + _conn->send(); +} + +MDNSResponder MDNS = MDNSResponder(); diff --git a/libraries/ESP8266mDNS/ESP8266mDNS.h b/libraries/ESP8266mDNS/ESP8266mDNS.h index 9a1fc56d3..4a8fa6ad1 100644 --- a/libraries/ESP8266mDNS/ESP8266mDNS.h +++ b/libraries/ESP8266mDNS/ESP8266mDNS.h @@ -46,29 +46,51 @@ License (MIT license): #include "ESP8266WiFi.h" #include "WiFiUdp.h" +//this should be defined at build time +#ifndef ARDUINO_BOARD +#define ARDUINO_BOARD "generic" +#endif class UdpContext; +struct MDNSService { + MDNSService* _next; + char _name[32]; + char _proto[3]; + uint16_t _port; +}; + class MDNSResponder { public: MDNSResponder(); ~MDNSResponder(); - bool begin(const char* domain, IPAddress addr, uint32_t ttlSeconds = 3600); + bool begin(const char* hostName); + //for compatibility + bool begin(const char* hostName, IPAddress ip, uint32_t ttl=120){ + return begin(hostName); + } void update(); - + + void addService(char *service, char *proto, uint16_t port); + void addService(const char *service, const char *proto, uint16_t port){ + addService((char *)service, (char *)proto, port); + } + void addService(String service, String proto, uint16_t port){ + addService(service.c_str(), proto.c_str(), port); + } + private: - // Expected query values - uint8_t* _expected; - int _expectedLen; - // Current parsing state - int _index; - // Response data - uint8_t* _response; - int _responseLen; - // Socket for MDNS communication + struct MDNSService * _services; UdpContext* _conn; - // local IP Address - IPAddress _localAddr; + char _hostName[128]; + char _boardName[64]; + + uint32_t _getOurIp(); + uint16_t _getServicePort(char *service, char *proto); + void _parsePacket(); + void _reply(uint8_t replyMask, char * service, char *proto, uint16_t port); }; +extern MDNSResponder MDNS; + #endif //ESP8266MDNS_H diff --git a/libraries/ESP8266mDNS/examples/mDNS_Web_Server/mDNS_Web_Server.ino b/libraries/ESP8266mDNS/examples/mDNS_Web_Server/mDNS_Web_Server.ino index 08ed4b4eb..710ddcaea 100644 --- a/libraries/ESP8266mDNS/examples/mDNS_Web_Server/mDNS_Web_Server.ino +++ b/libraries/ESP8266mDNS/examples/mDNS_Web_Server/mDNS_Web_Server.ino @@ -23,9 +23,6 @@ const char* ssid = "............"; const char* password = ".............."; -// multicast DNS responder -MDNSResponder mdns; - // TCP server at port 80 will respond to HTTP requests WiFiServer server(80); @@ -53,7 +50,7 @@ void setup(void) // the fully-qualified domain name is "esp8266.local" // - second argument is the IP address to advertise // we send our IP address on the WiFi network - if (!mdns.begin("esp8266", WiFi.localIP())) { + if (!MDNS.begin("esp8266")) { Serial.println("Error setting up MDNS responder!"); while(1) { delay(1000); @@ -64,6 +61,9 @@ void setup(void) // Start TCP (HTTP) server server.begin(); Serial.println("TCP server started"); + + // Add service to MDNS-SD + MDNS.addService("http", "tcp", 80); } void loop(void) diff --git a/libraries/ESP8266mDNS/keywords.txt b/libraries/ESP8266mDNS/keywords.txt index 64dff9102..4a538e080 100644 --- a/libraries/ESP8266mDNS/keywords.txt +++ b/libraries/ESP8266mDNS/keywords.txt @@ -6,14 +6,17 @@ # Datatypes (KEYWORD1) ####################################### +ESP8266mDNS KEYWORD1 MDNSResponder KEYWORD1 +MDNS KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) ####################################### -begin KEYWORD2 -update KEYWORD2 +begin KEYWORD2 +update KEYWORD2 +addService KEYWORD2 ####################################### # Constants (LITERAL1)