From ae9962e7069c9293cd93d55f08b825d5e3717318 Mon Sep 17 00:00:00 2001 From: Sebastian Hahta <joseha@utu.fi> Date: Mon, 1 Jun 2020 13:51:25 +0300 Subject: [PATCH] HTTP Basic Auth for websocket --- components/common/cpp/CMakeLists.txt | 1 + components/common/cpp/include/ftl/uri.hpp | 3 + .../common/cpp/include/ftl/utility/base64.hpp | 35 +++ components/common/cpp/src/uri.cpp | 220 ++++++++------- components/common/cpp/src/utility/base64.cpp | 256 ++++++++++++++++++ .../net/cpp/include/ftl/net/ws_internal.hpp | 6 +- components/net/cpp/src/ws_internal.cpp | 13 +- 7 files changed, 434 insertions(+), 100 deletions(-) create mode 100644 components/common/cpp/include/ftl/utility/base64.hpp create mode 100644 components/common/cpp/src/utility/base64.cpp diff --git a/components/common/cpp/CMakeLists.txt b/components/common/cpp/CMakeLists.txt index 7c5794d1b..fe1d7671b 100644 --- a/components/common/cpp/CMakeLists.txt +++ b/components/common/cpp/CMakeLists.txt @@ -12,6 +12,7 @@ set(COMMONSRC src/timer.cpp src/profiler.cpp src/exception.cpp + src/utility/base64.cpp ) check_function_exists(uriParseSingleUriA HAVE_URIPARSESINGLE) diff --git a/components/common/cpp/include/ftl/uri.hpp b/components/common/cpp/include/ftl/uri.hpp index 24123f168..71e3bf456 100644 --- a/components/common/cpp/include/ftl/uri.hpp +++ b/components/common/cpp/include/ftl/uri.hpp @@ -44,6 +44,8 @@ namespace ftl { const std::string &getFragment() const { return m_frag; } std::string getQuery() const; const std::string &getBaseURI() const { return m_base; }; + bool hasUserInfo() const; + const std::string &getUserInfo() const; /** * Get the URI without query parameters, and limit path to length N. @@ -74,6 +76,7 @@ namespace ftl { std::string m_path; std::string m_frag; std::string m_base; + std::string m_userinfo; std::vector<std::string> m_pathseg; int m_port; scheme_t m_proto; diff --git a/components/common/cpp/include/ftl/utility/base64.hpp b/components/common/cpp/include/ftl/utility/base64.hpp new file mode 100644 index 000000000..197bd7df3 --- /dev/null +++ b/components/common/cpp/include/ftl/utility/base64.hpp @@ -0,0 +1,35 @@ +// +// base64 encoding and decoding with C++. +// Version: 2.rc.04 (release candidate) +// + +#ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A +#define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A + +#include <string> + +#if __cplusplus >= 201703L +#include <string_view> +#endif // __cplusplus >= 201703L + +std::string base64_encode (std::string const& s, bool url = false); +std::string base64_encode_pem (std::string const& s); +std::string base64_encode_mime(std::string const& s); + +std::string base64_decode(std::string const& s, bool remove_linebreaks = false); +std::string base64_encode(unsigned char const*, size_t len, bool url = false); + +#if __cplusplus >= 201703L +// +// Interface with std::string_view rather than const std::string& +// Requires C++17 +// Provided by Yannic Bonenberger (https://github.com/Yannic) +// +std::string base64_encode (std::string_view s, bool url = false); +std::string base64_encode_pem (std::string_view s); +std::string base64_encode_mime(std::string_view s); + +std::string base64_decode(std::string_view s, bool remove_linebreaks = false); +#endif // __cplusplus >= 201703L + +#endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */ diff --git a/components/common/cpp/src/uri.cpp b/components/common/cpp/src/uri.cpp index 6884720d1..fa059a0a8 100644 --- a/components/common/cpp/src/uri.cpp +++ b/components/common/cpp/src/uri.cpp @@ -15,27 +15,28 @@ using ftl::uri_t; using std::string; URI::URI(uri_t puri) { - _parse(puri); + _parse(puri); } URI::URI(const std::string &puri) { - _parse(puri.c_str()); + _parse(puri.c_str()); } URI::URI(const URI &c) { - m_valid = c.m_valid; - m_host = c.m_host; - m_port = c.m_port; - m_proto = c.m_proto; - m_path = c.m_path; - m_pathseg = c.m_pathseg; - m_qmap = c.m_qmap; - m_base = c.m_base; + m_valid = c.m_valid; + m_host = c.m_host; + m_port = c.m_port; + m_proto = c.m_proto; + m_path = c.m_path; + m_pathseg = c.m_pathseg; + m_qmap = c.m_qmap; + m_base = c.m_base; + m_userinfo = c.m_userinfo; m_frag = c.m_frag; } void URI::_parse(uri_t puri) { - UriUriA uri; + UriUriA uri; std::string suri = puri; @@ -56,83 +57,98 @@ void URI::_parse(uri_t puri) { } #ifdef HAVE_URIPARSESINGLE - const char *errpos; - if (uriParseSingleUriA(&uri, puri, &errpos) != URI_SUCCESS) { + const char *errpos; + if (uriParseSingleUriA(&uri, puri, &errpos) != URI_SUCCESS) { #else - UriParserStateA uris; - uris.uri = &uri; - if (uriParseUriA(&uris, suri.c_str()) != URI_SUCCESS) { + UriParserStateA uris; + uris.uri = &uri; + if (uriParseUriA(&uris, suri.c_str()) != URI_SUCCESS) { #endif - m_valid = false; - m_host = "none"; - m_port = -1; - m_proto = SCHEME_NONE; - m_path = ""; + m_valid = false; + m_host = "none"; + m_port = -1; + m_proto = SCHEME_NONE; + m_path = ""; m_frag = ""; - } else { - m_host = std::string(uri.hostText.first, uri.hostText.afterLast - uri.hostText.first); - - std::string prototext = std::string(uri.scheme.first, uri.scheme.afterLast - uri.scheme.first); - if (prototext == "tcp") m_proto = SCHEME_TCP; - else if (prototext == "udp") m_proto = SCHEME_UDP; - else if (prototext == "ftl") m_proto = SCHEME_FTL; - else if (prototext == "http") m_proto = SCHEME_HTTP; - else if (prototext == "ws") m_proto = SCHEME_WS; - else if (prototext == "ipc") m_proto = SCHEME_IPC; + } else { + m_host = std::string(uri.hostText.first, uri.hostText.afterLast - uri.hostText.first); + + std::string prototext = std::string(uri.scheme.first, uri.scheme.afterLast - uri.scheme.first); + if (prototext == "tcp") m_proto = SCHEME_TCP; + else if (prototext == "udp") m_proto = SCHEME_UDP; + else if (prototext == "ftl") m_proto = SCHEME_FTL; + else if (prototext == "http") m_proto = SCHEME_HTTP; + else if (prototext == "ws") m_proto = SCHEME_WS; + else if (prototext == "ipc") m_proto = SCHEME_IPC; else if (prototext == "device") m_proto = SCHEME_DEVICE; else if (prototext == "file") m_proto = SCHEME_FILE; - else m_proto = SCHEME_OTHER; - m_protostr = prototext; - - std::string porttext = std::string(uri.portText.first, uri.portText.afterLast - uri.portText.first); - m_port = atoi(porttext.c_str()); - - for (auto h=uri.pathHead; h!=NULL; h=h->next) { - auto pstr = std::string( - h->text.first, h->text.afterLast - h->text.first); - - m_path += "/"; - m_path += pstr; - m_pathseg.push_back(pstr); - } - - //string query = std::string(uri.query.first, uri.query.afterLast - uri.query.first); - if (uri.query.afterLast - uri.query.first > 0) { - UriQueryListA *queryList; - int itemCount; - if (uriDissectQueryMallocA(&queryList, &itemCount, uri.query.first, - uri.query.afterLast) != URI_SUCCESS) { - // Failure - } - - UriQueryListA *item = queryList; - while (item) { - m_qmap[item->key] = item->value; - item = item->next; - } - - uriFreeQueryListA(queryList); - } - - uriFreeUriMembersA(&uri); + else m_proto = SCHEME_OTHER; + m_protostr = prototext; + + std::string porttext = std::string(uri.portText.first, uri.portText.afterLast - uri.portText.first); + m_port = atoi(porttext.c_str()); + m_userinfo = std::string(uri.userInfo.first, uri.userInfo.afterLast - uri.userInfo.first); + + for (auto h=uri.pathHead; h!=NULL; h=h->next) { + auto pstr = std::string( + h->text.first, h->text.afterLast - h->text.first); + + m_path += "/"; + m_path += pstr; + m_pathseg.push_back(pstr); + } + + //string query = std::string(uri.query.first, uri.query.afterLast - uri.query.first); + if (uri.query.afterLast - uri.query.first > 0) { + UriQueryListA *queryList; + int itemCount; + if (uriDissectQueryMallocA(&queryList, &itemCount, uri.query.first, + uri.query.afterLast) != URI_SUCCESS) { + // Failure + } + + UriQueryListA *item = queryList; + while (item) { + m_qmap[item->key] = item->value; + item = item->next; + } + uriFreeQueryListA(queryList); + } + + uriFreeUriMembersA(&uri); auto fraglast = (uri.query.first != NULL) ? uri.query.first : uri.fragment.afterLast; if (uri.fragment.first != NULL && fraglast - uri.fragment.first > 0) { m_frag = std::string(uri.fragment.first, fraglast - uri.fragment.first); } - m_valid = m_proto != SCHEME_NONE && (m_host.size() > 0 || m_path.size() > 0); - - if (m_valid) { - if (m_qmap.size() > 0) m_base = std::string(uri.scheme.first, uri.query.first - uri.scheme.first - 1); - else if (uri.fragment.first != NULL) m_base = std::string(uri.scheme.first, uri.fragment.first - uri.scheme.first - 1); - else m_base = std::string(uri.scheme.first); - } - } + m_valid = m_proto != SCHEME_NONE && (m_host.size() > 0 || m_path.size() > 0); + + if (m_valid) { + // remove userinfo from base uri + const char *start = uri.scheme.first; + if (m_userinfo != "") { + m_base = std::string(start, uri.userInfo.first - start); + start = uri.userInfo.afterLast + 1; + } + else { + m_base = std::string(""); + } + if (m_qmap.size() > 0) { + m_base += std::string(start, uri.query.first - start - 1); + } + else if (uri.fragment.first != NULL) { + m_base += std::string(start, uri.fragment.first - start - 1); + } + else { + m_base += std::string(start); + } + } + } } string URI::to_string() const { - return (m_qmap.size() > 0) ? m_base + "?" + getQuery() : m_base; + return (m_qmap.size() > 0) ? m_base + "?" + getQuery() : m_base; } string URI::getPathSegment(int n) const { @@ -142,42 +158,42 @@ string URI::getPathSegment(int n) const { } string URI::getBaseURI(int n) { - if (n >= (int)m_pathseg.size()) return m_base; - if (n >= 0) { - string r = m_protostr + string("://") + m_host + ((m_port != 0) ? string(":") + std::to_string(m_port) : ""); - for (int i=0; i<n; i++) { + if (n >= (int)m_pathseg.size()) return m_base; + if (n >= 0) { + string r = m_protostr + string("://") + m_host + ((m_port != 0) ? string(":") + std::to_string(m_port) : ""); + for (int i=0; i<n; i++) { r += "/"; - r += getPathSegment(i); - } - - return r; - } else if (m_pathseg.size()+n >= 0) { - string r = m_protostr + string("://") + m_host + ((m_port != 0) ? string(":") + std::to_string(m_port) : ""); - size_t N = m_pathseg.size()+n; - for (size_t i=0; i<N; i++) { + r += getPathSegment(i); + } + + return r; + } else if (m_pathseg.size()+n >= 0) { + string r = m_protostr + string("://") + m_host + ((m_port != 0) ? string(":") + std::to_string(m_port) : ""); + size_t N = m_pathseg.size()+n; + for (size_t i=0; i<N; i++) { r += "/"; - r += getPathSegment(i); - } + r += getPathSegment(i); + } - return r; - } else return ""; + return r; + } else return ""; } string URI::getQuery() const { - string q; - for (auto x : m_qmap) { - if (q.length() > 0) q += "&"; - q += x.first + "=" + x.second; - } - return q; + string q; + for (auto x : m_qmap) { + if (q.length() > 0) q += "&"; + q += x.first + "=" + x.second; + } + return q; }; void URI::setAttribute(const string &key, const string &value) { - m_qmap[key] = value; + m_qmap[key] = value; } void URI::setAttribute(const string &key, int value) { - m_qmap[key] = std::to_string(value); + m_qmap[key] = std::to_string(value); } void URI::to_json(nlohmann::json &json) { @@ -202,3 +218,11 @@ void URI::to_json(nlohmann::json &json) { } } } + +bool URI::hasUserInfo() const { + return m_userinfo != ""; +} + +const std::string &URI::getUserInfo() const { + return m_userinfo; +} diff --git a/components/common/cpp/src/utility/base64.cpp b/components/common/cpp/src/utility/base64.cpp new file mode 100644 index 000000000..1dc38826a --- /dev/null +++ b/components/common/cpp/src/utility/base64.cpp @@ -0,0 +1,256 @@ +/* + base64.cpp and base64.h + + base64 encoding and decoding with C++. + More information at + https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp + + Version: 2.rc.04 (release candidate) + + Copyright (C) 2004-2017, 2020 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +*/ + +#include "ftl/utility/base64.hpp" + + // + // Depending on the url parameter in base64_chars, one of + // two sets of base64 characters needs to be chosen. + // They differ in their last two characters. + // +const char* base64_chars[2] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-_"}; + +static unsigned int pos_of_char(const unsigned char chr) { + // + // Return the position of chr within base64_encode() + // + + if (chr >= 'A' && chr <= 'Z') return chr - 'A'; + else if (chr >= 'a' && chr <= 'z') return chr - 'a' + ('Z' - 'A') + 1; + else if (chr >= '0' && chr <= '9') return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2; + else if (chr == '+' || chr == '-') return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters ( + else if (chr == '/' || chr == '_') return 63; // Ditto for '/' and '_' + + throw "If input is correct, this line should never be reached."; +} + +static std::string insert_linebreaks(std::string str, size_t distance) { + // + // Provided by https://github.com/JomaCorpFX, adapted by me. + // + if (!str.length()) { + return ""; + } + + size_t pos = distance; + + while (pos < str.size()) { + str.insert(pos, "\n"); + pos += distance + 1; + } + + return str; +} + +template <typename String, unsigned int line_length> +static std::string encode_with_line_breaks(String s) { + return insert_linebreaks(base64_encode(s, false), line_length); +} + +template <typename String> +static std::string encode_pem(String s) { + return encode_with_line_breaks<String, 64>(s); +} + +template <typename String> +static std::string encode_mime(String s) { + return encode_with_line_breaks<String, 76>(s); +} + +template <typename String> +static std::string encode(String s, bool url) { + return base64_encode(reinterpret_cast<const unsigned char*>(s.data()), s.length(), url); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url) { + + size_t len_encoded = (in_len +2) / 3 * 4; + + unsigned char trailing_char = url ? '.' : '='; + + // + // Choose set of base64 characters. They differ + // for the last two positions, depending on the url + // parameter. + // A bool (as is the parameter url) is guaranteed + // to evaluate to either 0 or 1 in C++ therfore, + // the correct character set is chosen by subscripting + // base64_chars with url. + // + const char* base64_chars_ = base64_chars[url]; + + std::string ret; + ret.reserve(len_encoded); + + unsigned int pos = 0; + + while (pos < in_len) { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]); + + if (pos+1 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]); + + if (pos+2 < in_len) { + ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]); + ret.push_back(base64_chars_[ bytes_to_encode[pos + 2] & 0x3f]); + } + else { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); + ret.push_back(trailing_char); + } + } + else { + + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); + ret.push_back(trailing_char); + ret.push_back(trailing_char); + } + + pos += 3; + } + + + return ret; +} + +template <typename String> +static std::string decode(String encoded_string, bool remove_linebreaks) { + // + // decode(…) is templated so that it can be used with String = const std::string& + // or std::string_view (requires at least C++17) + // + + if (remove_linebreaks) { + + if (! encoded_string.length() ) { + return ""; + } + + std::string copy(encoded_string); + + size_t pos=0; + while ((pos = copy.find("\n", pos)) != std::string::npos) { + copy.erase(pos, 1); + } + + return base64_decode(copy, false); + + } + + size_t length_of_string = encoded_string.length(); + if (!length_of_string) return std::string(""); + + size_t in_len = length_of_string; + size_t pos = 0; + + // + // The approximate length (bytes) of the decoded string might be one ore + // two bytes smaller, depending on the amount of trailing equal signs + // in the encoded string. This approximation is needed to reserve + // enough space in the string to be returned. + // + size_t approx_length_of_decoded_string = length_of_string / 4 * 3; + std::string ret; + ret.reserve(approx_length_of_decoded_string); + + while (pos < in_len) { + + unsigned int pos_of_char_1 = pos_of_char(encoded_string[pos+1] ); + + ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char(encoded_string[pos+0]) ) << 2 ) + ( (pos_of_char_1 & 0x30 ) >> 4))); + + if (encoded_string[pos+2] != '=' && encoded_string[pos+2] != '.') { // accept URL-safe base 64 strings, too, so check for '.' also. + + unsigned int pos_of_char_2 = pos_of_char(encoded_string[pos+2] ); + ret.push_back(static_cast<std::string::value_type>( (( pos_of_char_1 & 0x0f) << 4) + (( pos_of_char_2 & 0x3c) >> 2))); + + if (encoded_string[pos+3] != '=' && encoded_string[pos+3] != '.') { + ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char_2 & 0x03 ) << 6 ) + pos_of_char(encoded_string[pos+3]) )); + } + } + + pos += 4; + } + + return ret; +} + +std::string base64_decode(std::string const& s, bool remove_linebreaks) { + return decode(s, remove_linebreaks); +} + +std::string base64_encode(std::string const& s, bool url) { + return encode(s, url); +} + +std::string base64_encode_pem (std::string const& s) { + return encode_pem(s); +} + +std::string base64_encode_mime(std::string const& s) { + return encode_mime(s); +} + +#if __cplusplus >= 201703L +// +// Interface with std::string_view rather than const std::string& +// Requires C++17 +// Provided by Yannic Bonenberger (https://github.com/Yannic) +// + +std::string base64_encode(std::string_view s, bool url) { + return encode(s, url); +} + +std::string base64_encode_pem(std::string_view s) { + return encode_pem(s); +} + +std::string base64_encode_mime(std::string_view s) { + return encode_mime(s); +} + +std::string base64_decode(std::string_view s, bool remove_linebreaks) { + return decode(s, remove_linebreaks); +} + +#endif // __cplusplus >= 201703L diff --git a/components/net/cpp/include/ftl/net/ws_internal.hpp b/components/net/cpp/include/ftl/net/ws_internal.hpp index 29fa3ff68..fb457578e 100644 --- a/components/net/cpp/include/ftl/net/ws_internal.hpp +++ b/components/net/cpp/include/ftl/net/ws_internal.hpp @@ -32,6 +32,10 @@ struct wsheader_type { uint8_t masking_key[4]; }; +struct ws_options { + std::string userinfo = ""; +}; + /** * Websocket dispatch parser. Given a raw socket buffer and its length, this * function parses the websocket header and if valid and containing enough data @@ -49,7 +53,7 @@ int ws_parse(msgpack::unpacker &buf, wsheader_type &ws); */ int ws_prepare(wsheader_type::opcode_type, bool useMask, size_t len, char *buffer, size_t maxlen); -bool ws_connect(SOCKET sockfd, const ftl::URI &uri); +bool ws_connect(SOCKET sockfd, const ftl::URI &uri, const ws_options &options=ws_options()); }; }; diff --git a/components/net/cpp/src/ws_internal.cpp b/components/net/cpp/src/ws_internal.cpp index db9393d2a..28318ab5b 100644 --- a/components/net/cpp/src/ws_internal.cpp +++ b/components/net/cpp/src/ws_internal.cpp @@ -6,7 +6,10 @@ #include <loguru.hpp> #include <cstring> + #include <ftl/net/ws_internal.hpp> +#include <ftl/utility/base64.hpp> + #include <memory> @@ -184,7 +187,7 @@ int ftl::net::ws_prepare(wsheader_type::opcode_type op, bool useMask, size_t len return (int)header_size; } -bool ftl::net::ws_connect(SOCKET sockfd, const URI &uri) { +bool ftl::net::ws_connect(SOCKET sockfd, const URI &uri, const ws_options &options) { string http = ""; int status; int i; @@ -196,11 +199,19 @@ bool ftl::net::ws_connect(SOCKET sockfd, const URI &uri) { } else { http += "Host: "+uri.getHost()+":"+std::to_string(uri.getPort())+"\r\n"; } + if (uri.hasUserInfo()) { + //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization + http += "Authorization: Basic "; + http += base64_encode(uri.getUserInfo()) + "\r\n"; + } + http += "Upgrade: websocket\r\n"; http += "Connection: Upgrade\r\n"; http += "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"; http += "Sec-WebSocket-Version: 13\r\n"; http += "\r\n"; + // TODO: Check/process HTTP response code + int rc = ::send(sockfd, http.c_str(), (int)http.length(), 0); if (rc != (int)http.length()) { LOG(ERROR) << "Could not send Websocket http request... (" << rc << ", " << errno << ")"; -- GitLab