/** * Address.h * * An AMQP address in the "amqp://user:password@hostname:port/vhost" notation * * @author Emiel Bruijntjes * @copyright 2015 - 2018 Copernica BV */ /** * Include guard */ #pragma once /** * Includes */ #include /** * Set up namespace */ namespace AMQP { /** * Class definition */ class Address { private: /** * Helper class to do case insensitive comparison */ struct icasecmp { /** * Comparison operator <. Should exhibit SWO. * @param lhs * @param rhs * @return bool lhs < rhs */ bool operator() (const std::string& lhs, const std::string& rhs) const { return strcasecmp(lhs.c_str(), rhs.c_str()) < 0; } }; private: /** * The auth method * @var bool */ bool _secure = false; /** * Login data (username + password) * @var Login */ Login _login; /** * The hostname * @var std::string */ std::string _hostname; /** * Port number * @var uint16_t */ uint16_t _port = 5672; /** * The vhost * @var std::string */ std::string _vhost; /** * Extra provided options after the question mark /vhost?option=value * @var std::map */ std::map _options; /** * The default port * @return uint16_t */ uint16_t defaultport() const { return _secure ? 5671 : 5672; } public: /** * Constructor to parse an address string * The address should start with "amqp:// * @param data * @param size * @throws std::runtime_error */ Address(const char *data, size_t size) : _vhost("/") { // position of the last byte const char *last = data + size; // must start with ampqs:// to have a secure connection (and we also assign a different default port) if (strncmp(data, "amqps://", 8) == 0) _secure = true; // otherwise protocol must be amqp:// else if (strncmp(data, "amqp://", 7) != 0) throw std::runtime_error("AMQP address should start with \"amqp://\" or \"amqps://\""); // assign default port (we may overwrite it later) _port = defaultport(); // begin of the string was parsed data += _secure ? 8 : 7; // do we have a '@' to split user-data and hostname? const char *at = (const char *)memchr(data, '@', last - data); // do we have one? if (at != nullptr) { // size of the user:password size_t loginsize = at - data; // colon could split username and password const char *colon = (const char *)memchr(data, ':', loginsize); // assign the login _login = Login( std::string(data, colon ? colon - data : loginsize), std::string(colon ? colon + 1 : "", colon ? at - colon - 1 : 0) ); // set data to the start of the hostname data = at + 1; } // find out where the vhost is set (starts with a slash) const char *slash = (const char *)memchr(data, '/', last - data); // where to start looking for the question mark, we also want to support urls where the // hostname does not have a slash. const char *start = slash ? slash : data; // we search for the ? for extra options const char *qm = static_cast(memchr(start, '?', last - start)); // if there is a questionmark, we need to parse all options if (qm != nullptr && last - qm > 1) { // we start at question mark now start = qm; do { // find the next equals sign and start of the next parameter const char *equals = (const char *)memchr(start + 1, '=', last - start - 1); const char *next = (const char *)memchr(start + 1, '&', last - start - 1); // assign it to the options if we found an equals sign if (equals) _options[std::string(start + 1, equals - start - 1)] = std::string(equals + 1, (next ? next - equals : last - equals) - 1); // we now have a new start, the next '&...' start = next; // keep iterating as long as there are more vars } while (start); } // was a vhost set? if (slash != nullptr && last - slash > 1) _vhost.assign(slash + 1, (qm ? qm - slash : last - slash) - 1); // the hostname is everything until the slash, check is portnumber was set const char *colon = (const char *)memchr(data, ':', last - data); // was a portnumber specified (colon must appear before the slash of the vhost) if (colon && (!slash || colon < slash)) { // a portnumber was set to _hostname.assign(data, colon - data); // calculate the port _port = atoi(std::string(colon + 1, slash ? slash - colon - 1 : last - colon - 1).data()); } else { // no portnumber was set _hostname.assign(data, slash ? slash - data : last - data); } } /** * Constructor to parse an address string * The address should start with amqp:// or amqps:// * @param data * @throws std::runtime_error */ Address(const char *data) : Address(data, strlen(data)) {} /** * Constructor based on std::string * @param address */ Address(const std::string &address) : Address(address.data(), address.size()) {} /** * Constructor based on already known properties * @param host * @param port * @param login * @param vhost * @param secure */ Address(std::string host, uint16_t port, Login login, std::string vhost, bool secure = false) : _secure(secure), _login(std::move(login)), _hostname(std::move(host)), _port(port), _vhost(std::move(vhost)) {} /** * Destructor */ virtual ~Address() = default; /** * Should we open a secure connection? * @return bool */ bool secure() const { return _secure; } /** * Expose the login data * @return Login */ const Login &login() const { return _login; } /** * Host name * @return std::string */ const std::string &hostname() const { return _hostname; } /** * Port number * @return uint16_t */ uint16_t port() const { return _port; } /** * The vhost to connect to * @return std::string */ const std::string &vhost() const { return _vhost; } /** * Get access to the options * @return std::map */ const decltype(_options) &options() const { return _options; } /** * Cast to a string * @return std::string */ operator std::string () const { // result object std::string str(_secure ? "amqps://" : "amqp://"); // append login str.append(_login.user()).append(":").append(_login.password()).append("@").append(_hostname); // do we need a special portnumber? if (_port != 5672) str.append(":").append(std::to_string(_port)); // append default vhost str.append("/"); // do we have a special vhost? if (_vhost != "/") str.append(_vhost); // iterate over all options, appending them if (!_options.empty()) { // first append a question mark str.push_back('?'); // iterate over all the options for (const auto &kv : _options) str.append(kv.first).append("=").append(kv.second).append("&"); // remove the extra & str.erase(str.size() - 1); } // done return str; } /** * Comparison operator * @param that * @return bool */ bool operator==(const Address &that) const { // security setting should match if (_secure != that._secure) return false; // logins must match if (_login != that._login) return false; // hostname must match, but are not case sensitive if (strcasecmp(_hostname.data(), that._hostname.data()) != 0) return false; // portnumber must match if (_port != that._port) return false; // and the vhosts, they must match too if (_vhost != that._vhost) return false; // and the options as well return _options == that._options; } /** * Comparison operator * @param that * @return bool */ bool operator!=(const Address &that) const { // the opposite of operator== return !operator==(that); } /** * Comparison operator that is useful if addresses have to be ordered * @param that * @return bool */ bool operator<(const Address &that) const { // compare auth methods (amqp comes before amqps) if (_secure != that._secure) return !_secure; // compare logins if (_login != that._login) return _login < that._login; // hostname must match, but are not case sensitive int result = strcasecmp(_hostname.data(), that._hostname.data()); // if hostnames are not equal, we know the result if (result != 0) return result < 0; // portnumber must match if (_port != that._port) return _port < that._port; // and finally compare the vhosts if (_vhost < that._vhost) return _vhost < that._vhost; // and finally lexicographically compare the options return _options < that._options; } /** * Friend function to allow writing the address to a stream * @param stream * @param address * @return std::ostream */ friend std::ostream &operator<<(std::ostream &stream, const Address &address) { // start with the protocol and login stream << (address._secure ? "amqps://" : "amqp://"); // do we have a login? if (address._login) stream << address._login << "@"; // write hostname stream << address._hostname; // do we need a special portnumber? if (address._port != address.defaultport()) stream << ":" << address._port; // append default vhost stream << "/"; // do we have a special vhost or options? if (address._vhost != "/") stream << address._vhost; // iterate over all options, appending them if (!address._options.empty()) { // first append a question mark stream << '?'; // is this the first option? bool first = true; // iterate over all the options for (const auto &kv : address._options) { // write the pair to the stream stream << (first ? "" : "&") << kv.first << "=" << kv.second; // no longer on first option first = false; } } // done return stream; } /** * Get an integer option * @param name * @param fallback * @return T */ template ::value>::type* = nullptr> T option(const char *name, T fallback) const { // find the const char* version of the option const char *value = option(name); // if there is a value, convert it to integral, otherwise return the fallback return value ? static_cast(atoll(value)) : fallback; } /** * Get a const char * option, returns nullptr if it does not exist. * @return const char * */ const char *option(const char *name) const { // find the option auto iter = _options.find(name); // if not found, we return the default if (iter == _options.end()) return nullptr; // return the value in the map return iter->second.c_str(); } }; /** * End of namespace */ }