mirror of
https://github.com/PurpleI2P/i2pd.git
synced 2024-10-31 09:20:38 +00:00
568 lines
18 KiB
C++
568 lines
18 KiB
C++
#include <string.h>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <boost/asio.hpp>
|
|
#include <boost/asio/ssl.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/err.h>
|
|
#include <zlib.h>
|
|
|
|
#include "Crypto.h"
|
|
#include "I2PEndian.h"
|
|
#include "Reseed.h"
|
|
#include "FS.h"
|
|
#include "Log.h"
|
|
#include "Identity.h"
|
|
#include "NetDb.hpp"
|
|
#include "HTTP.h"
|
|
#include "util.h"
|
|
#include "Config.h"
|
|
|
|
namespace i2p
|
|
{
|
|
namespace data
|
|
{
|
|
|
|
Reseeder::Reseeder()
|
|
{
|
|
}
|
|
|
|
Reseeder::~Reseeder()
|
|
{
|
|
}
|
|
|
|
/** @brief tries to bootstrap into I2P network (from local files and servers, with respect of options)
|
|
*/
|
|
void Reseeder::Bootstrap ()
|
|
{
|
|
std::string su3FileName; i2p::config::GetOption("reseed.file", su3FileName);
|
|
std::string zipFileName; i2p::config::GetOption("reseed.zipfile", zipFileName);
|
|
|
|
if (su3FileName.length() > 0) // bootstrap from SU3 file or URL
|
|
{
|
|
int num;
|
|
if (su3FileName.length() > 8 && su3FileName.substr(0, 8) == "https://")
|
|
{
|
|
num = ReseedFromSU3Url (su3FileName); // from https URL
|
|
}
|
|
else
|
|
{
|
|
num = ProcessSU3File (su3FileName.c_str ());
|
|
}
|
|
if (num == 0)
|
|
LogPrint (eLogWarning, "Reseed: failed to reseed from ", su3FileName);
|
|
}
|
|
else if (zipFileName.length() > 0) // bootstrap from ZIP file
|
|
{
|
|
int num = ProcessZIPFile (zipFileName.c_str ());
|
|
if (num == 0)
|
|
LogPrint (eLogWarning, "Reseed: failed to reseed from ", zipFileName);
|
|
}
|
|
else // bootstrap from reseed servers
|
|
{
|
|
int num = ReseedFromServers ();
|
|
if (num == 0)
|
|
LogPrint (eLogWarning, "Reseed: failed to reseed from servers");
|
|
}
|
|
}
|
|
|
|
/** @brief bootstrap from random server, retry 10 times
|
|
* @return number of entries added to netDb
|
|
*/
|
|
int Reseeder::ReseedFromServers ()
|
|
{
|
|
std::string reseedURLs; i2p::config::GetOption("reseed.urls", reseedURLs);
|
|
std::vector<std::string> httpsReseedHostList;
|
|
boost::split(httpsReseedHostList, reseedURLs, boost::is_any_of(","), boost::token_compress_on);
|
|
|
|
if (reseedURLs.length () == 0)
|
|
{
|
|
LogPrint (eLogWarning, "Reseed: No reseed servers specified");
|
|
return 0;
|
|
}
|
|
|
|
int reseedRetries = 0;
|
|
while (reseedRetries < 10)
|
|
{
|
|
auto ind = rand () % httpsReseedHostList.size ();
|
|
std::string reseedUrl = httpsReseedHostList[ind] + "i2pseeds.su3";
|
|
auto num = ReseedFromSU3Url (reseedUrl);
|
|
if (num > 0) return num; // success
|
|
reseedRetries++;
|
|
}
|
|
LogPrint (eLogWarning, "Reseed: failed to reseed from servers after 10 attempts");
|
|
return 0;
|
|
}
|
|
|
|
/** @brief bootstrap from HTTPS URL with SU3 file
|
|
* @param url
|
|
* @return number of entries added to netDb
|
|
*/
|
|
int Reseeder::ReseedFromSU3Url (const std::string& url)
|
|
{
|
|
LogPrint (eLogInfo, "Reseed: Downloading SU3 from ", url);
|
|
std::string su3 = HttpsRequest (url);
|
|
if (su3.length () > 0)
|
|
{
|
|
std::stringstream s(su3);
|
|
return ProcessSU3Stream (s);
|
|
}
|
|
else
|
|
{
|
|
LogPrint (eLogWarning, "Reseed: SU3 download failed");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int Reseeder::ProcessSU3File (const char * filename)
|
|
{
|
|
std::ifstream s(filename, std::ifstream::binary);
|
|
if (s.is_open ())
|
|
return ProcessSU3Stream (s);
|
|
else
|
|
{
|
|
LogPrint (eLogError, "Reseed: Can't open file ", filename);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int Reseeder::ProcessZIPFile (const char * filename)
|
|
{
|
|
std::ifstream s(filename, std::ifstream::binary);
|
|
if (s.is_open ())
|
|
{
|
|
s.seekg (0, std::ios::end);
|
|
auto len = s.tellg ();
|
|
s.seekg (0, std::ios::beg);
|
|
return ProcessZIPStream (s, len);
|
|
}
|
|
else
|
|
{
|
|
LogPrint (eLogError, "Reseed: Can't open file ", filename);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
const char SU3_MAGIC_NUMBER[]="I2Psu3";
|
|
int Reseeder::ProcessSU3Stream (std::istream& s)
|
|
{
|
|
char magicNumber[7];
|
|
s.read (magicNumber, 7); // magic number and zero byte 6
|
|
if (strcmp (magicNumber, SU3_MAGIC_NUMBER))
|
|
{
|
|
LogPrint (eLogError, "Reseed: Unexpected SU3 magic number");
|
|
return 0;
|
|
}
|
|
s.seekg (1, std::ios::cur); // su3 file format version
|
|
SigningKeyType signatureType;
|
|
s.read ((char *)&signatureType, 2); // signature type
|
|
signatureType = be16toh (signatureType);
|
|
uint16_t signatureLength;
|
|
s.read ((char *)&signatureLength, 2); // signature length
|
|
signatureLength = be16toh (signatureLength);
|
|
s.seekg (1, std::ios::cur); // unused
|
|
uint8_t versionLength;
|
|
s.read ((char *)&versionLength, 1); // version length
|
|
s.seekg (1, std::ios::cur); // unused
|
|
uint8_t signerIDLength;
|
|
s.read ((char *)&signerIDLength, 1); // signer ID length
|
|
uint64_t contentLength;
|
|
s.read ((char *)&contentLength, 8); // content length
|
|
contentLength = be64toh (contentLength);
|
|
s.seekg (1, std::ios::cur); // unused
|
|
uint8_t fileType;
|
|
s.read ((char *)&fileType, 1); // file type
|
|
if (fileType != 0x00) // zip file
|
|
{
|
|
LogPrint (eLogError, "Reseed: Can't handle file type ", (int)fileType);
|
|
return 0;
|
|
}
|
|
s.seekg (1, std::ios::cur); // unused
|
|
uint8_t contentType;
|
|
s.read ((char *)&contentType, 1); // content type
|
|
if (contentType != 0x03) // reseed data
|
|
{
|
|
LogPrint (eLogError, "Reseed: Unexpected content type ", (int)contentType);
|
|
return 0;
|
|
}
|
|
s.seekg (12, std::ios::cur); // unused
|
|
|
|
s.seekg (versionLength, std::ios::cur); // skip version
|
|
char signerID[256];
|
|
s.read (signerID, signerIDLength); // signerID
|
|
signerID[signerIDLength] = 0;
|
|
|
|
bool verify; i2p::config::GetOption("reseed.verify", verify);
|
|
if (verify)
|
|
{
|
|
//try to verify signature
|
|
auto it = m_SigningKeys.find (signerID);
|
|
if (it != m_SigningKeys.end ())
|
|
{
|
|
// TODO: implement all signature types
|
|
if (signatureType == SIGNING_KEY_TYPE_RSA_SHA512_4096)
|
|
{
|
|
size_t pos = s.tellg ();
|
|
size_t tbsLen = pos + contentLength;
|
|
uint8_t * tbs = new uint8_t[tbsLen];
|
|
s.seekg (0, std::ios::beg);
|
|
s.read ((char *)tbs, tbsLen);
|
|
uint8_t * signature = new uint8_t[signatureLength];
|
|
s.read ((char *)signature, signatureLength);
|
|
// RSA-raw
|
|
{
|
|
// calculate digest
|
|
uint8_t digest[64];
|
|
SHA512 (tbs, tbsLen, digest);
|
|
// encrypt signature
|
|
BN_CTX * bnctx = BN_CTX_new ();
|
|
BIGNUM * s = BN_new (), * n = BN_new ();
|
|
BN_bin2bn (signature, signatureLength, s);
|
|
BN_bin2bn (it->second, i2p::crypto::RSASHA5124096_KEY_LENGTH, n);
|
|
BN_mod_exp (s, s, i2p::crypto::GetRSAE (), n, bnctx); // s = s^e mod n
|
|
uint8_t * enSigBuf = new uint8_t[signatureLength];
|
|
i2p::crypto::bn2buf (s, enSigBuf, signatureLength);
|
|
// digest is right aligned
|
|
// we can't use RSA_verify due wrong padding in SU3
|
|
if (memcmp (enSigBuf + (signatureLength - 64), digest, 64))
|
|
LogPrint (eLogWarning, "Reseed: SU3 signature verification failed");
|
|
else
|
|
verify = false; // verified
|
|
delete[] enSigBuf;
|
|
BN_free (s); BN_free (n);
|
|
BN_CTX_free (bnctx);
|
|
}
|
|
|
|
delete[] signature;
|
|
delete[] tbs;
|
|
s.seekg (pos, std::ios::beg);
|
|
}
|
|
else
|
|
LogPrint (eLogWarning, "Reseed: Signature type ", signatureType, " is not supported");
|
|
}
|
|
else
|
|
LogPrint (eLogWarning, "Reseed: Certificate for ", signerID, " not loaded");
|
|
}
|
|
|
|
if (verify) // not verified
|
|
{
|
|
LogPrint (eLogError, "Reseed: SU3 verification failed");
|
|
return 0;
|
|
}
|
|
|
|
// handle content
|
|
return ProcessZIPStream (s, contentLength);
|
|
}
|
|
|
|
const uint32_t ZIP_HEADER_SIGNATURE = 0x04034B50;
|
|
const uint32_t ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014B50;
|
|
const uint16_t ZIP_BIT_FLAG_DATA_DESCRIPTOR = 0x0008;
|
|
int Reseeder::ProcessZIPStream (std::istream& s, uint64_t contentLength)
|
|
{
|
|
int numFiles = 0;
|
|
size_t contentPos = s.tellg ();
|
|
while (!s.eof ())
|
|
{
|
|
uint32_t signature;
|
|
s.read ((char *)&signature, 4);
|
|
signature = le32toh (signature);
|
|
if (signature == ZIP_HEADER_SIGNATURE)
|
|
{
|
|
// next local file
|
|
s.seekg (2, std::ios::cur); // version
|
|
uint16_t bitFlag;
|
|
s.read ((char *)&bitFlag, 2);
|
|
bitFlag = le16toh (bitFlag);
|
|
uint16_t compressionMethod;
|
|
s.read ((char *)&compressionMethod, 2);
|
|
compressionMethod = le16toh (compressionMethod);
|
|
s.seekg (4, std::ios::cur); // skip fields we don't care about
|
|
uint32_t compressedSize, uncompressedSize;
|
|
uint32_t crc_32;
|
|
s.read ((char *)&crc_32, 4);
|
|
crc_32 = le32toh (crc_32);
|
|
s.read ((char *)&compressedSize, 4);
|
|
compressedSize = le32toh (compressedSize);
|
|
s.read ((char *)&uncompressedSize, 4);
|
|
uncompressedSize = le32toh (uncompressedSize);
|
|
uint16_t fileNameLength, extraFieldLength;
|
|
s.read ((char *)&fileNameLength, 2);
|
|
fileNameLength = le16toh (fileNameLength);
|
|
if ( fileNameLength > 255 ) {
|
|
// too big
|
|
LogPrint(eLogError, "Reseed: SU3 fileNameLength too large: ", fileNameLength);
|
|
return numFiles;
|
|
}
|
|
s.read ((char *)&extraFieldLength, 2);
|
|
extraFieldLength = le16toh (extraFieldLength);
|
|
char localFileName[255];
|
|
s.read (localFileName, fileNameLength);
|
|
localFileName[fileNameLength] = 0;
|
|
s.seekg (extraFieldLength, std::ios::cur);
|
|
// take care about data desriptor if presented
|
|
if (bitFlag & ZIP_BIT_FLAG_DATA_DESCRIPTOR)
|
|
{
|
|
size_t pos = s.tellg ();
|
|
if (!FindZipDataDescriptor (s))
|
|
{
|
|
LogPrint (eLogError, "Reseed: SU3 archive data descriptor not found");
|
|
return numFiles;
|
|
}
|
|
s.read ((char *)&crc_32, 4);
|
|
crc_32 = le32toh (crc_32);
|
|
s.read ((char *)&compressedSize, 4);
|
|
compressedSize = le32toh (compressedSize) + 4; // ??? we must consider signature as part of compressed data
|
|
s.read ((char *)&uncompressedSize, 4);
|
|
uncompressedSize = le32toh (uncompressedSize);
|
|
|
|
// now we know compressed and uncompressed size
|
|
s.seekg (pos, std::ios::beg); // back to compressed data
|
|
}
|
|
|
|
LogPrint (eLogDebug, "Reseed: Proccessing file ", localFileName, " ", compressedSize, " bytes");
|
|
if (!compressedSize)
|
|
{
|
|
LogPrint (eLogWarning, "Reseed: Unexpected size 0. Skipped");
|
|
continue;
|
|
}
|
|
|
|
uint8_t * compressed = new uint8_t[compressedSize];
|
|
s.read ((char *)compressed, compressedSize);
|
|
if (compressionMethod) // we assume Deflate
|
|
{
|
|
z_stream inflator;
|
|
memset (&inflator, 0, sizeof (inflator));
|
|
inflateInit2 (&inflator, -MAX_WBITS); // no zlib header
|
|
uint8_t * uncompressed = new uint8_t[uncompressedSize];
|
|
inflator.next_in = compressed;
|
|
inflator.avail_in = compressedSize;
|
|
inflator.next_out = uncompressed;
|
|
inflator.avail_out = uncompressedSize;
|
|
int err;
|
|
if ((err = inflate (&inflator, Z_SYNC_FLUSH)) >= 0)
|
|
{
|
|
uncompressedSize -= inflator.avail_out;
|
|
if (crc32 (0, uncompressed, uncompressedSize) == crc_32)
|
|
{
|
|
i2p::data::netdb.AddRouterInfo (uncompressed, uncompressedSize);
|
|
numFiles++;
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: CRC32 verification failed");
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: SU3 decompression error ", err);
|
|
delete[] uncompressed;
|
|
inflateEnd (&inflator);
|
|
}
|
|
else // no compression
|
|
{
|
|
i2p::data::netdb.AddRouterInfo (compressed, compressedSize);
|
|
numFiles++;
|
|
}
|
|
delete[] compressed;
|
|
if (bitFlag & ZIP_BIT_FLAG_DATA_DESCRIPTOR)
|
|
s.seekg (12, std::ios::cur); // skip data descriptor section if presented (12 = 16 - 4)
|
|
}
|
|
else
|
|
{
|
|
if (signature != ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE)
|
|
LogPrint (eLogWarning, "Reseed: Missing zip central directory header");
|
|
break; // no more files
|
|
}
|
|
size_t end = s.tellg ();
|
|
if (end - contentPos >= contentLength)
|
|
break; // we are beyond contentLength
|
|
}
|
|
if (numFiles) // check if routers are not outdated
|
|
{
|
|
auto ts = i2p::util::GetMillisecondsSinceEpoch ();
|
|
int numOutdated = 0;
|
|
i2p::data::netdb.VisitRouterInfos (
|
|
[&numOutdated, ts](std::shared_ptr<const RouterInfo> r)
|
|
{
|
|
if (r && ts > r->GetTimestamp () + 10*i2p::data::NETDB_MAX_EXPIRATION_TIMEOUT*1000LL) // 270 hours
|
|
{
|
|
LogPrint (eLogError, "Reseed: router ", r->GetIdentHash().ToBase64 (), " is outdated by ", (ts - r->GetTimestamp ())/1000LL/3600LL, " hours");
|
|
numOutdated++;
|
|
}
|
|
});
|
|
if (numOutdated > numFiles/2) // more than half
|
|
{
|
|
LogPrint (eLogError, "Reseed: mammoth's shit\n"
|
|
" *_____*\n"
|
|
" *_*****_*\n"
|
|
" *_(O)_(O)_*\n"
|
|
" **____V____**\n"
|
|
" **_________**\n"
|
|
" **_________**\n"
|
|
" *_________*\n"
|
|
" ***___***");
|
|
i2p::data::netdb.ClearRouterInfos ();
|
|
numFiles = 0;
|
|
}
|
|
}
|
|
return numFiles;
|
|
}
|
|
|
|
const uint8_t ZIP_DATA_DESCRIPTOR_SIGNATURE[] = { 0x50, 0x4B, 0x07, 0x08 };
|
|
bool Reseeder::FindZipDataDescriptor (std::istream& s)
|
|
{
|
|
size_t nextInd = 0;
|
|
while (!s.eof ())
|
|
{
|
|
uint8_t nextByte;
|
|
s.read ((char *)&nextByte, 1);
|
|
if (nextByte == ZIP_DATA_DESCRIPTOR_SIGNATURE[nextInd])
|
|
{
|
|
nextInd++;
|
|
if (nextInd >= sizeof (ZIP_DATA_DESCRIPTOR_SIGNATURE))
|
|
return true;
|
|
}
|
|
else
|
|
nextInd = 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Reseeder::LoadCertificate (const std::string& filename)
|
|
{
|
|
SSL_CTX * ctx = SSL_CTX_new (TLS_method ());
|
|
int ret = SSL_CTX_use_certificate_file (ctx, filename.c_str (), SSL_FILETYPE_PEM);
|
|
if (ret)
|
|
{
|
|
SSL * ssl = SSL_new (ctx);
|
|
X509 * cert = SSL_get_certificate (ssl);
|
|
// verify
|
|
if (cert)
|
|
{
|
|
// extract issuer name
|
|
char name[100];
|
|
X509_NAME_oneline (X509_get_issuer_name(cert), name, 100);
|
|
char * cn = strstr (name, "CN=");
|
|
if (cn)
|
|
{
|
|
cn += 3;
|
|
char * terminator = strchr (cn, '/');
|
|
if (terminator) terminator[0] = 0;
|
|
}
|
|
// extract RSA key (we need n only, e = 65537)
|
|
RSA * key = EVP_PKEY_get0_RSA (X509_get_pubkey (cert));
|
|
const BIGNUM * n, * e, * d;
|
|
RSA_get0_key(key, &n, &e, &d);
|
|
PublicKey value;
|
|
i2p::crypto::bn2buf (n, value, 512);
|
|
if (cn)
|
|
m_SigningKeys[cn] = value;
|
|
else
|
|
LogPrint (eLogError, "Reseed: Can't find CN field in ", filename);
|
|
}
|
|
SSL_free (ssl);
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: Can't open certificate file ", filename);
|
|
SSL_CTX_free (ctx);
|
|
}
|
|
|
|
void Reseeder::LoadCertificates ()
|
|
{
|
|
std::string certDir = i2p::fs::DataDirPath("certificates", "reseed");
|
|
std::vector<std::string> files;
|
|
int numCertificates = 0;
|
|
|
|
if (!i2p::fs::ReadDir(certDir, files)) {
|
|
LogPrint(eLogWarning, "Reseed: Can't load reseed certificates from ", certDir);
|
|
return;
|
|
}
|
|
|
|
for (const std::string & file : files) {
|
|
if (file.compare(file.size() - 4, 4, ".crt") != 0) {
|
|
LogPrint(eLogWarning, "Reseed: ignoring file ", file);
|
|
continue;
|
|
}
|
|
LoadCertificate (file);
|
|
numCertificates++;
|
|
}
|
|
LogPrint (eLogInfo, "Reseed: ", numCertificates, " certificates loaded");
|
|
}
|
|
|
|
std::string Reseeder::HttpsRequest (const std::string& address)
|
|
{
|
|
i2p::http::URL url;
|
|
if (!url.parse(address)) {
|
|
LogPrint(eLogError, "Reseed: failed to parse url: ", address);
|
|
return "";
|
|
}
|
|
url.schema = "https";
|
|
if (!url.port)
|
|
url.port = 443;
|
|
|
|
boost::asio::io_service service;
|
|
boost::system::error_code ecode;
|
|
auto it = boost::asio::ip::tcp::resolver(service).resolve (
|
|
boost::asio::ip::tcp::resolver::query (url.host, std::to_string(url.port)), ecode);
|
|
if (!ecode)
|
|
{
|
|
boost::asio::ssl::context ctx(service, boost::asio::ssl::context::sslv23);
|
|
ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
|
|
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> s(service, ctx);
|
|
s.lowest_layer().connect (*it, ecode);
|
|
if (!ecode)
|
|
{
|
|
SSL_set_tlsext_host_name(s.native_handle(), url.host.c_str ());
|
|
s.handshake (boost::asio::ssl::stream_base::client, ecode);
|
|
if (!ecode)
|
|
{
|
|
LogPrint (eLogDebug, "Reseed: Connected to ", url.host, ":", url.port);
|
|
i2p::http::HTTPReq req;
|
|
req.uri = url.to_string();
|
|
req.AddHeader("User-Agent", "Wget/1.11.4");
|
|
req.AddHeader("Connection", "close");
|
|
s.write_some (boost::asio::buffer (req.to_string()));
|
|
// read response
|
|
std::stringstream rs;
|
|
char recv_buf[1024]; size_t l = 0;
|
|
do {
|
|
l = s.read_some (boost::asio::buffer (recv_buf, sizeof(recv_buf)), ecode);
|
|
if (l) rs.write (recv_buf, l);
|
|
} while (!ecode && l);
|
|
// process response
|
|
std::string data = rs.str();
|
|
i2p::http::HTTPRes res;
|
|
int len = res.parse(data);
|
|
if (len <= 0) {
|
|
LogPrint(eLogWarning, "Reseed: incomplete/broken response from ", url.host);
|
|
return "";
|
|
}
|
|
if (res.code != 200) {
|
|
LogPrint(eLogError, "Reseed: failed to reseed from ", url.host, ", http code ", res.code);
|
|
return "";
|
|
}
|
|
data.erase(0, len); /* drop http headers from response */
|
|
LogPrint(eLogDebug, "Reseed: got ", data.length(), " bytes of data from ", url.host);
|
|
if (res.is_chunked()) {
|
|
std::stringstream in(data), out;
|
|
if (!i2p::http::MergeChunkedResponse(in, out)) {
|
|
LogPrint(eLogWarning, "Reseed: failed to merge chunked response from ", url.host);
|
|
return "";
|
|
}
|
|
LogPrint(eLogDebug, "Reseed: got ", data.length(), "(", out.tellg(), ") bytes of data from ", url.host);
|
|
data = out.str();
|
|
}
|
|
return data;
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: SSL handshake failed: ", ecode.message ());
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: Couldn't connect to ", url.host, ": ", ecode.message ());
|
|
}
|
|
else
|
|
LogPrint (eLogError, "Reseed: Couldn't resolve address ", url.host, ": ", ecode.message ());
|
|
return "";
|
|
}
|
|
}
|
|
}
|
|
|