From 71d969c40b33286902ff22f3d97e8d18199a6fec Mon Sep 17 00:00:00 2001 From: Nimetu Date: Tue, 11 Jul 2017 22:23:38 +0300 Subject: [PATCH] Changed: Keep track of HSTS header and rewrite http to https as needed --HG-- branch : develop --- code/nel/include/nel/gui/group_html.h | 4 +- code/nel/include/nel/gui/http_hsts.h | 73 ++++++++ code/nel/src/gui/group_html.cpp | 66 ++++++- code/nel/src/gui/http_hsts.cpp | 245 ++++++++++++++++++++++++++ code/ryzom/client/src/init.cpp | 3 + code/ryzom/client/src/release.cpp | 2 + 6 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 code/nel/include/nel/gui/http_hsts.h create mode 100644 code/nel/src/gui/http_hsts.cpp diff --git a/code/nel/include/nel/gui/group_html.h b/code/nel/include/nel/gui/group_html.h index da90095a3..969eac1f7 100644 --- a/code/nel/include/nel/gui/group_html.h +++ b/code/nel/include/nel/gui/group_html.h @@ -793,7 +793,7 @@ namespace NLGUI void doBrowseLocalFile(const std::string &filename); // load remote content using either GET or POST - void doBrowseRemoteUrl(const std::string &url, const std::string &referer, bool doPost = false, const SFormFields &formfields = SFormFields()); + void doBrowseRemoteUrl(std::string url, const std::string &referer, bool doPost = false, const SFormFields &formfields = SFormFields()); // render html string as new browser page bool renderHtmlString(const std::string &html); @@ -861,7 +861,7 @@ namespace NLGUI // BnpDownload system void initBnpDownload(); void checkBnpDownload(); - bool addBnpDownload(const std::string &url, const std::string &action, const std::string &script, const std::string &md5sum); + bool addBnpDownload(std::string url, const std::string &action, const std::string &script, const std::string &md5sum); std::string localBnpName(const std::string &url); void releaseDownloads(); diff --git a/code/nel/include/nel/gui/http_hsts.h b/code/nel/include/nel/gui/http_hsts.h new file mode 100644 index 000000000..2693461cd --- /dev/null +++ b/code/nel/include/nel/gui/http_hsts.h @@ -0,0 +1,73 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2010 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef CL_HTTP_HSTS_H +#define CL_HTTP_HSTS_H + +#include "nel/misc/types_nl.h" + +namespace NLGUI +{ + // ******************************************************************************** + struct SHSTSObject + { + public: + SHSTSObject(uint64 expires = 0, bool includeSubDomains = false) + : Expires(expires) + , IncludeSubDomains(includeSubDomains) + { } + + uint64 Expires; + bool IncludeSubDomains; + }; + + /** + * Keeping track of HSTS header + * \author Meelis Mägi (nimetu) + * \date 2017 + */ + class CStrictTransportSecurity + { + public: + typedef std::map THSTSObject; + + static CStrictTransportSecurity* getInstance(); + static void release(); + + public: + bool isSecureHost(const std::string &domain) const; + + // ************************************************************************ + void init(const std::string& fname); + void save(); + + void erase(const std::string &domain); + void set(const std::string &domain, uint64 expires, bool includeSubDomains); + bool get(const std::string &domain, SHSTSObject &hsts) const; + void setFromHeader(const std::string &domain, const std::string &header); + + void serial(NLMISC::IStream& f); + private: + static CStrictTransportSecurity* instance; + + ~CStrictTransportSecurity(); + + std::string _Filename; + THSTSObject _Domains; + }; + +} +#endif diff --git a/code/nel/src/gui/group_html.cpp b/code/nel/src/gui/group_html.cpp index e99149cec..0c082ae58 100644 --- a/code/nel/src/gui/group_html.cpp +++ b/code/nel/src/gui/group_html.cpp @@ -46,6 +46,7 @@ #include "nel/misc/big_file.h" #include "nel/gui/url_parser.h" #include "nel/gui/http_cache.h" +#include "nel/gui/http_hsts.h" #include "nel/gui/curl_certificates.h" using namespace std; @@ -71,6 +72,26 @@ namespace NLGUI CGroupHTML::SWebOptions CGroupHTML::options; + // Return URL with https is host is in HSTS list + static std::string upgradeInsecureUrl(const std::string &url) + { + if (toLower(url.substr(0, 7)) != "http://") { + return url; + } + + CUrlParser uri(url); + if (!CStrictTransportSecurity::getInstance()->isSecureHost(uri.host)){ + return url; + } + + #ifdef LOG_DL + nlwarning("HSTS url : '%s', using https", url.c_str()); + #endif + uri.scheme = "https"; + + return uri.toString(); + } + // Active cURL www transfer class CCurlWWWData { @@ -149,6 +170,27 @@ namespace NLGUI return ""; } + bool hasHSTSHeader() + { + // ignore header if not secure connection + if (toLower(Url.substr(0, 8)) != "https://") + { + return false; + } + + return HeadersRecv.count("strict-transport-security") > 0; + } + + const std::string getHSTSHeader() + { + if (hasHSTSHeader()) + { + return HeadersRecv["strict-transport-security"]; + } + + return ""; + } + public: CURL *Request; @@ -406,7 +448,7 @@ namespace NLGUI // Add a image download request in the multi_curl void CGroupHTML::addImageDownload(const string &url, CViewBase *img, const CStyleParams &style, TImageType type) { - string finalUrl = getAbsoluteUrl(url); + string finalUrl = upgradeInsecureUrl(getAbsoluteUrl(url)); // Search if we are not already downloading this url. for(uint i = 0; i < Curls.size(); i++) @@ -421,7 +463,7 @@ namespace NLGUI } } - // use requested url for local name + // use requested url for local name (cache) string dest = localImageName(url); #ifdef LOG_DL nlwarning("add to download '%s' dest '%s' img %p", finalUrl.c_str(), dest.c_str(), img); @@ -467,8 +509,10 @@ namespace NLGUI } // Add a bnp download request in the multi_curl, return true if already downloaded - bool CGroupHTML::addBnpDownload(const string &url, const string &action, const string &script, const string &md5sum) + bool CGroupHTML::addBnpDownload(string url, const string &action, const string &script, const string &md5sum) { + url = upgradeInsecureUrl(getAbsoluteUrl(url)); + // Search if we are not already downloading this url. for(uint i = 0; i < Curls.size(); i++) { @@ -577,6 +621,12 @@ namespace NLGUI #ifdef LOG_DL nlwarning("(%s) web transfer '%p' completed with status %d, http %d, url (len %d) '%s'", _Id.c_str(), _CurlWWW->Request, res, code, _CurlWWW->Url.size(), _CurlWWW->Url.c_str()); #endif + // save HSTS header from all requests regardless of HTTP code + if (res == CURLE_OK && _CurlWWW->hasHSTSHeader()) + { + CUrlParser uri(_CurlWWW->Url); + CStrictTransportSecurity::getInstance()->setFromHeader(uri.host, _CurlWWW->getHSTSHeader()); + } if (res != CURLE_OK) { @@ -663,6 +713,12 @@ namespace NLGUI #endif curl_multi_remove_handle(MultiCurl, it->data->Request); + // save HSTS header from all requests regardless of HTTP code + if (res == CURLE_OK && it->data->hasHSTSHeader()) + { + CStrictTransportSecurity::getInstance()->setFromHeader(uri.host, it->data->getHSTSHeader()); + } + string tmpfile = it->dest + ".tmp"; if(res != CURLE_OK || r < 200 || r >= 300 || (!it->md5sum.empty() && (it->md5sum != getMD5(tmpfile).toString()))) { @@ -5239,7 +5295,7 @@ namespace NLGUI } // *************************************************************************** - void CGroupHTML::doBrowseRemoteUrl(const std::string &url, const std::string &referer, bool doPost, const SFormFields &formfields) + void CGroupHTML::doBrowseRemoteUrl(std::string url, const std::string &referer, bool doPost, const SFormFields &formfields) { // Stop previous request and remove content stopBrowse (); @@ -5253,6 +5309,8 @@ namespace NLGUI else setTitle (_TitlePrefix + " - " + CI18N::get("uiPleaseWait")); + url = upgradeInsecureUrl(url); + #if LOG_DL nlwarning("(%s) browse url (trusted=%s) '%s', referer='%s', post='%s', nb form values %d", _Id.c_str(), (_TrustedDomain ? "true" :"false"), url.c_str(), referer.c_str(), (doPost ? "true" : "false"), formfields.Values.size()); diff --git a/code/nel/src/gui/http_hsts.cpp b/code/nel/src/gui/http_hsts.cpp new file mode 100644 index 000000000..980bdabda --- /dev/null +++ b/code/nel/src/gui/http_hsts.cpp @@ -0,0 +1,245 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2010 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#include "stdpch.h" +#include "nel/gui/http_hsts.h" + +using namespace std; +using namespace NLMISC; + +#ifdef DEBUG_NEW +#define new DEBUG_NEW +#endif + +namespace NLGUI { + CStrictTransportSecurity* CStrictTransportSecurity::instance = NULL; + CStrictTransportSecurity* CStrictTransportSecurity::getInstance() + { + if (!instance) + { + instance= new CStrictTransportSecurity(); + } + return instance; + } + + void CStrictTransportSecurity::release() + { + delete instance; + instance = NULL; + } + + CStrictTransportSecurity::~CStrictTransportSecurity() + { + save(); + } + + // ************************************************************************ + bool CStrictTransportSecurity::isSecureHost(const std::string &domain) const + { + SHSTSObject hsts; + if (get(domain, hsts)) + { + time_t currentTime; + time(¤tTime); + + return (hsts.Expires < currentTime); + } + + return false; + } + + // ************************************************************************ + void CStrictTransportSecurity::erase(const std::string &domain) + { + if (_Domains.count(domain) > 0) + { + _Domains.erase(domain); + } + } + + void CStrictTransportSecurity::set(const std::string &domain, uint64 expires, bool includeSubDomains) + { + if (expires == 0) + { + erase(domain); + return; + } + + _Domains[domain].Expires = expires; + _Domains[domain].IncludeSubDomains = includeSubDomains; + } + + bool CStrictTransportSecurity::get(const std::string &domain, SHSTSObject &hsts) const + { + if (domain.empty() || _Domains.empty()) + return false; + + if (_Domains.count(domain) > 0) + { + hsts = _Domains.at(domain); + return true; + } + + size_t firstOf = domain.find_first_of("."); + size_t lastOf = domain.find_last_of("."); + while(firstOf != lastOf) + { + std::string tmp; + tmp = domain.substr(firstOf+1); + if (_Domains.count(tmp)) + { + if (_Domains.at(tmp).IncludeSubDomains) + { + hsts = _Domains.at(tmp); + return true; + } + + return false; + } + + firstOf = domain.find_first_of(".", firstOf + 1); + } + + return false; + } + + void CStrictTransportSecurity::init(const std::string &fname) + { + _Domains.clear(); + _Filename = fname; + + if (_Filename.empty() || !CFile::fileExists(_Filename)) + { + return; + } + + CIFile in; + if (!in.open(_Filename)) + { + nlwarning("Unable to open %s for reading", _Filename.c_str()); + return; + } + + serial(in); + } + + void CStrictTransportSecurity::save() + { + if (_Filename.empty()) + return; + + if (_Domains.empty()) + { + CFile::deleteFile(_Filename); + return; + } + + COFile out; + if (!out.open(_Filename)) + { + nlwarning("Unable to open %s for writing", _Filename.c_str()); + return; + } + + serial(out); + out.close(); + } + + void CStrictTransportSecurity::serial(NLMISC::IStream& f) + { + try + { + f.serialVersion(1); + // HSTS + f.serialCheck(NELID("STSH")); + + if (f.isReading()) + { + uint32 nbItems; + f.serial(nbItems); + for(uint32 k = 0; k < nbItems; ++k) + { + std::string domain; + f.serial(domain); + f.serial(_Domains[domain].Expires); + f.serial(_Domains[domain].IncludeSubDomains); + } + } + else + { + uint32 nbItems = _Domains.size(); + f.serial(nbItems); + for (THSTSObject::iterator it = _Domains.begin(); it != _Domains.end(); ++it) + { + std::string domain(it->first); + f.serial(domain); + f.serial(_Domains[domain].Expires); + f.serial(_Domains[domain].IncludeSubDomains); + } + } + } + catch (...) + { + _Domains.clear(); + nlwarning("Invalid HTST file format (%s)", _Filename.c_str()); + } + } + + // *************************************************************************** + void CStrictTransportSecurity::setFromHeader(const std::string &domain, const std::string &header) + { + // max-age=; includeSubdomains; preload; + std::vector elements; + NLMISC::splitString(toLower(header), ";", elements); + if (elements.empty()) return; + + time_t currentTime; + time(¤tTime); + + uint64 expire = 0; + bool includeSubDomains = false; + + for(uint i=0; i< elements.size(); ++i) + { + std::string str(trim(elements[i])); + if (str.substr(0, 8) == "max-age=") + { + uint64 ttl; + if (fromString(str.substr(8), ttl)) + { + if (ttl > 0) + { + expire = currentTime + ttl; + } + } + } + else if (str == "includesubdomains") + { + includeSubDomains = true; + } + } + + if (expire == 0) + { + erase(domain); + } + else + { + set(domain, expire, includeSubDomains); + } + } + +} diff --git a/code/ryzom/client/src/init.cpp b/code/ryzom/client/src/init.cpp index 900f3c2c1..244ef3384 100644 --- a/code/ryzom/client/src/init.cpp +++ b/code/ryzom/client/src/init.cpp @@ -69,6 +69,7 @@ #include "interface_v3/sbrick_manager.h" #include "nel/gui/widget_manager.h" #include "nel/gui/http_cache.h" +#include "nel/gui/http_hsts.h" // #include "gabarit.h" #include "hair_set.h" @@ -1370,6 +1371,8 @@ void prelogInit() CHttpCache::getInstance()->setCacheIndex("cache/cache.index"); CHttpCache::getInstance()->init(); + CStrictTransportSecurity::getInstance()->init("save/hsts-list.save"); + // Register the reflected classes registerInterfaceElements(); diff --git a/code/ryzom/client/src/release.cpp b/code/ryzom/client/src/release.cpp index a1f3a29c2..ff569b22a 100644 --- a/code/ryzom/client/src/release.cpp +++ b/code/ryzom/client/src/release.cpp @@ -95,6 +95,7 @@ #include "nel/gui/lua_manager.h" #include "item_group_manager.h" #include "nel/gui/http_cache.h" +#include "nel/gui/http_hsts.h" /////////// // USING // @@ -688,6 +689,7 @@ void release() CViewRenderer::release(); CIXml::releaseLibXml(); CHttpCache::release(); + CStrictTransportSecurity::release(); #if FINAL_VERSION // openURL ("http://www.ryzomcore.org/exit/");