diff options
| author | 2021-09-23 00:24:36 +0100 | |
|---|---|---|
| committer | 2021-09-23 00:24:36 +0100 | |
| commit | b5d7c564282f0b25458c3ca9dbd6aa9eec7d60d5 (patch) | |
| tree | f4b687e3ca0c5edfefd3ae7cdd3329012a603486 | |
| parent | Merge branch 'insp3' into master. (diff) | |
| parent | Use CXX11_OVERRIDE instead of the override keyword. (diff) | |
Merge branch 'insp3' into master.
| -rw-r--r-- | docs/conf/modules.conf.example | 16 | ||||
| -rw-r--r-- | docs/conf/opers.conf.example | 1 | ||||
| -rw-r--r-- | include/iohook.h | 3 | ||||
| -rw-r--r-- | include/modules/ssl.h | 19 | ||||
| -rw-r--r-- | src/modules/extra/m_ssl_gnutls.cpp | 22 | ||||
| -rw-r--r-- | src/modules/extra/m_ssl_mbedtls.cpp | 31 | ||||
| -rw-r--r-- | src/modules/extra/m_ssl_openssl.cpp | 34 | ||||
| -rw-r--r-- | src/modules/m_haproxy.cpp | 5 | ||||
| -rw-r--r-- | src/modules/m_repeat.cpp | 3 | ||||
| -rw-r--r-- | src/modules/m_websocket.cpp | 46 |
10 files changed, 114 insertions, 66 deletions
diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example index 20ead6fd9..40391d71b 100644 --- a/docs/conf/modules.conf.example +++ b/docs/conf/modules.conf.example @@ -2361,19 +2361,19 @@ # Requires SHA-1 hash support available in the sha1 module. #<module name="websocket"> # +# defaultmode: The default frame mode if a client does not send a +# WebSocket subprotocol. Potential values are "text" to +# encode messages as UTF-8 text frames, "binary" to send +# messages as raw binary frames, or "reject" to close +# connections which do not request a subprotocol. Defaults +# to "text". # proxyranges: A space-delimited list of glob or CIDR matches to trust # the X-Real-IP or X-Forwarded-For headers from. If enabled # the server will use the IP address specified by those HTTP # headers. You should NOT enable this unless you are using # a HTTP proxy like nginx as it will allow IP spoofing. -# sendastext: Whether to re-encode messages as UTF-8 before sending to -# WebSocket clients. This is recommended as the WebSocket -# protocol requires all text frames to be sent as UTF-8. -# If you do not have this enabled messages will be sent as -# binary frames instead. Clients can override this using a -# WebSocket subprotocol. See the docs page for more info. -#<websocket proxyranges="192.0.2.0/24 198.51.100.*" -# sendastext="yes"> +#<websocket defaultmode="text" +# proxyranges="192.0.2.0/24 198.51.100.*"> # # If you use the websocket module you MUST specify one or more origins # which are allowed to connect to the server. You should set this as diff --git a/docs/conf/opers.conf.example b/docs/conf/opers.conf.example index 6aa05c792..36133f9dc 100644 --- a/docs/conf/opers.conf.example +++ b/docs/conf/opers.conf.example @@ -30,6 +30,7 @@ # - channels/ignore-noctcp: allows opers with this priv to send a CTCP to a +C channel. # - channels/ignore-nonicks: allows opers with this priv to change their nick when on a +N channel. # - channels/ignore-opmoderated: allows opers with this priv to ignore +U. + # - channels/ignore-repeat: allows opers with this priv to be immune to repeat punishment on a +E channel. # - channels/restricted-create: allows opers with this priv to create channels if the restrictchans module is loaded. # - users/flood/increased-buffers: allows opers with this priv to send and receive data without worrying about being disconnected for exceeding limits (*NOTE). # - users/flood/no-fakelag: prevents opers from being penalized with fake lag for flooding (*NOTE). diff --git a/include/iohook.h b/include/iohook.h index 968621323..8055667a8 100644 --- a/include/iohook.h +++ b/include/iohook.h @@ -80,6 +80,9 @@ class IOHook : public Cullable IOHook(std::shared_ptr<IOHookProvider> provider) : prov(provider) { } + /** Determines whether this I/O hook is ready to send and receive data. */ + virtual bool IsHookReady() const { return true; } + /** * Called when the hooked socket has data to write, or when the socket engine returns it as writable * @param sock Hooked socket diff --git a/include/modules/ssl.h b/include/modules/ssl.h index 66d29aca1..f616b39c8 100644 --- a/include/modules/ssl.h +++ b/include/modules/ssl.h @@ -153,10 +153,26 @@ public: class SSLIOHook : public IOHook { protected: + /** An enumeration of possible TLS (SSL) socket states. */ + enum Status + { + /** The SSL socket has just been opened or has been closed. */ + STATUS_NONE, + + /** The SSL socket is currently handshaking. */ + STATUS_HANDSHAKING, + + /** The SSL handshake has completed and data can be sent. */ + STATUS_OPEN + }; + /** Peer TLS certificate, set by the TLS module */ reference<ssl_cert> certificate; + /** The status of the TLS (SSL) connection. */ + Status status = STATUS_NONE; + /** Reduce elements in a send queue by appending later elements to the first element until there are no more * elements to append or a desired length is reached * @param sendq SendQ to work on @@ -230,6 +246,9 @@ class SSLIOHook : public IOHook * returns True if the server name was retrieved; otherwise, false. */ virtual bool GetServerName(std::string& out) const = 0; + + /** @copydoc IOHook::IsHookReady */ + bool IsHookReady() const override { return status == STATUS_OPEN; } }; /** Helper functions for obtaining TLS client certificates and key fingerprints diff --git a/src/modules/extra/m_ssl_gnutls.cpp b/src/modules/extra/m_ssl_gnutls.cpp index bb90538f7..79a9747d6 100644 --- a/src/modules/extra/m_ssl_gnutls.cpp +++ b/src/modules/extra/m_ssl_gnutls.cpp @@ -53,8 +53,6 @@ # pragma comment(lib, "libgnutls-30.lib") #endif -enum issl_status { ISSL_NONE, ISSL_HANDSHAKING, ISSL_HANDSHAKEN }; - #if INSPIRCD_GNUTLS_HAS_VERSION(3, 3, 5) #define INSPIRCD_GNUTLS_HAS_RECV_PACKET #endif @@ -611,7 +609,6 @@ class GnuTLSIOHook : public SSLIOHook { private: gnutls_session_t sess = nullptr; - issl_status status = ISSL_NONE; #ifdef INSPIRCD_GNUTLS_HAS_CORK size_t gbuffersize = 0; #endif @@ -625,7 +622,7 @@ class GnuTLSIOHook : public SSLIOHook } sess = NULL; certificate = NULL; - status = ISSL_NONE; + status = STATUS_NONE; } // Returns 1 if handshake succeeded, 0 if it is still in progress, -1 if it failed @@ -638,7 +635,7 @@ class GnuTLSIOHook : public SSLIOHook if(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) { // Handshake needs resuming later, read() or write() would have blocked. - this->status = ISSL_HANDSHAKING; + this->status = STATUS_HANDSHAKING; if (gnutls_record_get_direction(this->sess) == 0) { @@ -663,7 +660,7 @@ class GnuTLSIOHook : public SSLIOHook else { // Change the session state - this->status = ISSL_HANDSHAKEN; + this->status = STATUS_OPEN; VerifyCertificate(); @@ -765,9 +762,9 @@ info_done_dealloc: // Returns 1 if application I/O should proceed, 0 if it must wait for the underlying protocol to progress, -1 on fatal error int PrepareIO(StreamSocket* sock) { - if (status == ISSL_HANDSHAKEN) + if (status == STATUS_OPEN) return 1; - else if (status == ISSL_HANDSHAKING) + else if (status == STATUS_HANDSHAKING) { // The handshake isn't finished, try to finish it return Handshake(sock); @@ -915,7 +912,7 @@ info_done_dealloc: if (prepret <= 0) return prepret; - // If we resumed the handshake then this->status will be ISSL_HANDSHAKEN. + // If we resumed the handshake then this->status will be STATUS_OPEN. { GnuTLS::DataReader reader(sess); ssize_t ret = reader.ret(); @@ -1011,7 +1008,7 @@ info_done_dealloc: void GetCiphersuite(std::string& out) const override { - if (!IsHandshakeDone()) + if (!IsHookReady()) return; out.append(UnknownIfNULL(gnutls_protocol_get_name(gnutls_protocol_get_version(sess)))).push_back('-'); out.append(UnknownIfNULL(gnutls_kx_get_name(gnutls_kx_get(sess)))).push_back('-'); @@ -1039,7 +1036,6 @@ info_done_dealloc: } GnuTLS::Profile& GetProfile(); - bool IsHandshakeDone() const { return (status == ISSL_HANDSHAKEN); } }; int GnuTLS::X509Credentials::cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, gnutls_retr2_st* st) @@ -1181,7 +1177,7 @@ class ModuleSSLGnuTLS : public Module } catch (ModuleException& ex) { - ServerInstance->Logs.Log(MODNAME, LOG_DEFAULT, ex.GetReason() + " Not applying settings."); + ServerInstance->SNO.WriteToSnoMask('a', "Failed to reload the GnuTLS TLS (SSL) profiles. " + ex.GetReason()); } } @@ -1208,7 +1204,7 @@ class ModuleSSLGnuTLS : public Module ModResult OnCheckReady(LocalUser* user) override { const GnuTLSIOHook* const iohook = static_cast<GnuTLSIOHook*>(user->eh.GetModHook(this)); - if ((iohook) && (!iohook->IsHandshakeDone())) + if ((iohook) && (!iohook->IsHookReady())) return MOD_RES_DENY; return MOD_RES_PASSTHRU; } diff --git a/src/modules/extra/m_ssl_mbedtls.cpp b/src/modules/extra/m_ssl_mbedtls.cpp index 2c95da3c7..9c025ad56 100644 --- a/src/modules/extra/m_ssl_mbedtls.cpp +++ b/src/modules/extra/m_ssl_mbedtls.cpp @@ -523,25 +523,18 @@ namespace mbedTLS class mbedTLSIOHook : public SSLIOHook { - enum Status - { - ISSL_NONE, - ISSL_HANDSHAKING, - ISSL_HANDSHAKEN - }; - + private: mbedtls_ssl_context sess; - Status status; void CloseSession() { - if (status == ISSL_NONE) + if (status == STATUS_NONE) return; mbedtls_ssl_close_notify(&sess); mbedtls_ssl_free(&sess); certificate = NULL; - status = ISSL_NONE; + status = STATUS_NONE; } // Returns 1 if handshake succeeded, 0 if it is still in progress, -1 if it failed @@ -551,7 +544,7 @@ class mbedTLSIOHook : public SSLIOHook if (ret == 0) { // Change the session state - this->status = ISSL_HANDSHAKEN; + this->status = STATUS_OPEN; VerifyCertificate(); @@ -561,7 +554,7 @@ class mbedTLSIOHook : public SSLIOHook return 1; } - this->status = ISSL_HANDSHAKING; + this->status = STATUS_HANDSHAKING; if (ret == MBEDTLS_ERR_SSL_WANT_READ) { SocketEngine::ChangeEventMask(sock, FD_WANT_POLL_READ | FD_WANT_NO_WRITE); @@ -581,9 +574,9 @@ class mbedTLSIOHook : public SSLIOHook // Returns 1 if application I/O should proceed, 0 if it must wait for the underlying protocol to progress, -1 on fatal error int PrepareIO(StreamSocket* sock) { - if (status == ISSL_HANDSHAKEN) + if (status == STATUS_OPEN) return 1; - else if (status == ISSL_HANDSHAKING) + else if (status == STATUS_HANDSHAKING) { // The handshake isn't finished, try to finish it return Handshake(sock); @@ -685,7 +678,6 @@ class mbedTLSIOHook : public SSLIOHook public: mbedTLSIOHook(std::shared_ptr<IOHookProvider> hookprov, StreamSocket* sock, bool isserver) : SSLIOHook(hookprov) - , status(ISSL_NONE) { mbedtls_ssl_init(&sess); if (isserver) @@ -711,7 +703,7 @@ class mbedTLSIOHook : public SSLIOHook if (prepret <= 0) return prepret; - // If we resumed the handshake then this->status will be ISSL_HANDSHAKEN. + // If we resumed the handshake then this->status will be STATUS_OPEN. char* const readbuf = ServerInstance->GetReadBuffer(); const size_t readbufsize = ServerInstance->Config->NetBufferSize; int ret = mbedtls_ssl_read(&sess, reinterpret_cast<unsigned char*>(readbuf), readbufsize); @@ -802,7 +794,7 @@ class mbedTLSIOHook : public SSLIOHook void GetCiphersuite(std::string& out) const override { - if (!IsHandshakeDone()) + if (!IsHookReady()) return; out.append(mbedtls_ssl_get_version(&sess)).push_back('-'); @@ -822,7 +814,6 @@ class mbedTLSIOHook : public SSLIOHook } mbedTLS::Profile& GetProfile(); - bool IsHandshakeDone() const { return (status == ISSL_HANDSHAKEN); } }; class mbedTLSIOHookProvider : public SSLIOHookProvider @@ -952,7 +943,7 @@ class ModuleSSLmbedTLS : public Module } catch (ModuleException& ex) { - ServerInstance->Logs.Log(MODNAME, LOG_DEFAULT, ex.GetReason() + " Not applying settings."); + ServerInstance->SNO.WriteToSnoMask('a', "Failed to reload the mbedTLS TLS profiles. " + ex.GetReason()); } } @@ -973,7 +964,7 @@ class ModuleSSLmbedTLS : public Module ModResult OnCheckReady(LocalUser* user) override { const mbedTLSIOHook* const iohook = static_cast<mbedTLSIOHook*>(user->eh.GetModHook(this)); - if ((iohook) && (!iohook->IsHandshakeDone())) + if ((iohook) && (!iohook->IsHookReady())) return MOD_RES_DENY; return MOD_RES_PASSTHRU; } diff --git a/src/modules/extra/m_ssl_openssl.cpp b/src/modules/extra/m_ssl_openssl.cpp index 75adbd4e3..480a45ce1 100644 --- a/src/modules/extra/m_ssl_openssl.cpp +++ b/src/modules/extra/m_ssl_openssl.cpp @@ -52,8 +52,6 @@ # pragma comment(lib, "libeay32.lib") #endif -enum issl_status { ISSL_NONE, ISSL_HANDSHAKING, ISSL_OPEN }; - static bool SelfSigned = false; static int exdataindex; @@ -501,7 +499,6 @@ class OpenSSLIOHook : public SSLIOHook { private: SSL* sess; - issl_status status; bool data_to_write = false; // Returns 1 if handshake succeeded, 0 if it is still in progress, -1 if it failed @@ -516,13 +513,13 @@ class OpenSSLIOHook : public SSLIOHook if (err == SSL_ERROR_WANT_READ) { SocketEngine::ChangeEventMask(user, FD_WANT_POLL_READ | FD_WANT_NO_WRITE); - this->status = ISSL_HANDSHAKING; + this->status = STATUS_HANDSHAKING; return 0; } else if (err == SSL_ERROR_WANT_WRITE) { SocketEngine::ChangeEventMask(user, FD_WANT_NO_READ | FD_WANT_SINGLE_WRITE); - this->status = ISSL_HANDSHAKING; + this->status = STATUS_HANDSHAKING; return 0; } else @@ -536,7 +533,7 @@ class OpenSSLIOHook : public SSLIOHook // Handshake complete. VerifyCertificate(); - status = ISSL_OPEN; + status = STATUS_OPEN; SocketEngine::ChangeEventMask(user, FD_WANT_POLL_READ | FD_WANT_NO_WRITE | FD_ADD_TRIAL_WRITE); @@ -558,7 +555,7 @@ class OpenSSLIOHook : public SSLIOHook } sess = NULL; certificate = NULL; - status = ISSL_NONE; + status = STATUS_NONE; } void VerifyCertificate() @@ -621,14 +618,14 @@ class OpenSSLIOHook : public SSLIOHook void SSLInfoCallback(int where, int rc) { - if ((where & SSL_CB_HANDSHAKE_START) && (status == ISSL_OPEN)) + if ((where & SSL_CB_HANDSHAKE_START) && (status == STATUS_OPEN)) { if (GetProfile().AllowRenegotiation()) return; // The other side is trying to renegotiate, kill the connection and change status - // to ISSL_NONE so CheckRenego() closes the session - status = ISSL_NONE; + // to STATUS_NONE so CheckRenego() closes the session + status = STATUS_NONE; BIO* bio = SSL_get_rbio(sess); EventHandler* eh = static_cast<StreamSocket*>(BIO_get_data(bio)); SocketEngine::Shutdown(eh, 2); @@ -637,7 +634,7 @@ class OpenSSLIOHook : public SSLIOHook bool CheckRenego(StreamSocket* sock) { - if (status != ISSL_NONE) + if (status != STATUS_NONE) return true; ServerInstance->Logs.Log(MODNAME, LOG_DEBUG, "Session %p killed, attempted to renegotiate", (void*)sess); @@ -649,9 +646,9 @@ class OpenSSLIOHook : public SSLIOHook // Returns 1 if application I/O should proceed, 0 if it must wait for the underlying protocol to progress, -1 on fatal error int PrepareIO(StreamSocket* sock) { - if (status == ISSL_OPEN) + if (status == STATUS_OPEN) return 1; - else if (status == ISSL_HANDSHAKING) + else if (status == STATUS_HANDSHAKING) { // The handshake isn't finished, try to finish it return Handshake(sock); @@ -668,7 +665,7 @@ class OpenSSLIOHook : public SSLIOHook OpenSSLIOHook(std::shared_ptr<IOHookProvider> hookprov, StreamSocket* sock, SSL* session) : SSLIOHook(hookprov) , sess(session) - , status(ISSL_NONE) + , data_to_write(false) { // Create BIO instance and store a pointer to the socket in it which will be used by the read and write functions BIO* bio = BIO_new(biomethods); @@ -692,7 +689,7 @@ class OpenSSLIOHook : public SSLIOHook if (prepret <= 0) return prepret; - // If we resumed the handshake then this->status will be ISSL_OPEN + // If we resumed the handshake then this->status will be STATUS_OPEN { ERR_clear_error(); char* buffer = ServerInstance->GetReadBuffer(); @@ -811,7 +808,7 @@ class OpenSSLIOHook : public SSLIOHook void GetCiphersuite(std::string& out) const override { - if (!IsHandshakeDone()) + if (!IsHookReady()) return; out.append(SSL_get_version(sess)).push_back('-'); out.append(SSL_get_cipher(sess)); @@ -827,7 +824,6 @@ class OpenSSLIOHook : public SSLIOHook return true; } - bool IsHandshakeDone() const { return (status == ISSL_OPEN); } OpenSSL::Profile& GetProfile(); }; @@ -1009,7 +1005,7 @@ class ModuleSSLOpenSSL : public Module } catch (ModuleException& ex) { - ServerInstance->Logs.Log(MODNAME, LOG_DEFAULT, ex.GetReason() + " Not applying settings."); + ServerInstance->SNO.WriteToSnoMask('a', "Failed to reload the OpenSSL TLS (SSL) profiles. " + ex.GetReason()); } } @@ -1031,7 +1027,7 @@ class ModuleSSLOpenSSL : public Module ModResult OnCheckReady(LocalUser* user) override { const OpenSSLIOHook* const iohook = static_cast<OpenSSLIOHook*>(user->eh.GetModHook(this)); - if ((iohook) && (!iohook->IsHandshakeDone())) + if ((iohook) && (!iohook->IsHookReady())) return MOD_RES_DENY; return MOD_RES_PASSTHRU; } diff --git a/src/modules/m_haproxy.cpp b/src/modules/m_haproxy.cpp index b300a15d6..68c99eee1 100644 --- a/src/modules/m_haproxy.cpp +++ b/src/modules/m_haproxy.cpp @@ -384,6 +384,11 @@ class HAProxyHook : public IOHookMiddle sock->AddIOHook(this); } + bool IsHookReady() const override + { + return state == HPS_CONNECTED; + } + ssize_t OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) override { // We don't need to implement this. diff --git a/src/modules/m_repeat.cpp b/src/modules/m_repeat.cpp index af53fb6c2..2b1a82eec 100644 --- a/src/modules/m_repeat.cpp +++ b/src/modules/m_repeat.cpp @@ -388,6 +388,9 @@ class RepeatModule : public Module if (res == MOD_RES_ALLOW) return MOD_RES_PASSTHRU; + if (user->HasPrivPermission("channels/ignore-repeat")) + return MOD_RES_PASSTHRU; + if (rm.MatchLine(memb, settings, details.text)) { if (settings->Action == ChannelSettings::ACT_BLOCK) diff --git a/src/modules/m_websocket.cpp b/src/modules/m_websocket.cpp index 6cf721424..242ecc372 100644 --- a/src/modules/m_websocket.cpp +++ b/src/modules/m_websocket.cpp @@ -35,17 +35,29 @@ static dynamic_reference_nocheck<HashProvider>* sha1; struct WebSocketConfig { + enum DefaultMode + { + // Reject connections if a subprotocol is not requested. + DM_REJECT, + + // Use binary frames if a subprotocol is not requested. + DM_BINARY, + + // Use UTF-8 text frames if a subprotocol is not requested. + DM_TEXT + }; + typedef std::vector<std::string> OriginList; typedef std::vector<std::string> ProxyRanges; // The HTTP origins that can connect to the server. OriginList allowedorigins; + // The method to use if a subprotocol is not negotiated. + DefaultMode defaultmode; + // The IP ranges which send trustworthy X-Real-IP or X-Forwarded-For headers. ProxyRanges proxyranges; - - // Whether to send as UTF-8 text instead of binary data. - bool sendastext; }; class WebSocketHookProvider : public IOHookProvider @@ -406,8 +418,10 @@ class WebSocketHook : public IOHookMiddle irc::spacesepstream protostream(protocolheader.ExtractValue(recvq)); for (std::string proto; protostream.GetToken(proto); ) { + bool is_binary = stdalgo::string::equalsci(proto, "binary.inspircd.org"); bool is_text = stdalgo::string::equalsci(proto, "text.inspircd.org"); - if (stdalgo::string::equalsci(proto, "binary.inspircd.org") || is_text) + + if (is_binary || is_text) { selectedproto = proto; sendastext = is_text; @@ -416,6 +430,12 @@ class WebSocketHook : public IOHookMiddle } } + if (selectedproto.empty() && config.defaultmode == WebSocketConfig::DM_REJECT) + { + FailHandshake(sock, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request that did not send the Sec-WebSocket-Protocol header"); + return -1; + } + HTTPHeaderFinder keyheader; if (!keyheader.Find(recvq, "Sec-WebSocket-Key:", 18, reqend)) { @@ -452,11 +472,16 @@ class WebSocketHook : public IOHookMiddle WebSocketHook(std::shared_ptr<IOHookProvider> Prov, StreamSocket* sock, WebSocketConfig& cfg) : IOHookMiddle(Prov) , config(cfg) - , sendastext(cfg.sendastext) + , sendastext(config.defaultmode != WebSocketConfig::DM_BINARY) { sock->AddIOHook(this); } + bool IsHookReady() const override + { + return state == STATE_ESTABLISHED; + } + ssize_t OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) override { StreamSocket::SendQueue& mysendq = GetSendQ(); @@ -570,7 +595,16 @@ class ModuleWebSocket : public Module } auto tag = ServerInstance->Config->ConfValue("websocket"); - config.sendastext = tag->getBool("sendastext", true); + + const std::string defaultmodestr = tag->getString("defaultmode", tag->getBool("sendastext", true) ? "text" : "binary", 1); + if (stdalgo::string::equalsci(defaultmodestr, "reject")) + config.defaultmode = WebSocketConfig::DM_REJECT; + else if (stdalgo::string::equalsci(defaultmodestr, "binary")) + config.defaultmode = WebSocketConfig::DM_BINARY; + else if (stdalgo::string::equalsci(defaultmodestr, "text")) + config.defaultmode = WebSocketConfig::DM_TEXT; + else + throw ModuleException(defaultmodestr + " is an invalid value for <websocket:defaultmode>; acceptable values are 'binary', 'text' and 'reject', at " + tag->source.str()); irc::spacesepstream proxyranges(tag->getString("proxyranges")); for (std::string proxyrange; proxyranges.GetToken(proxyrange); ) |
