|
|
|
/*
|
|
|
|
* Copyright (c) 2013-2024, The PurpleI2P Project
|
|
|
|
*
|
|
|
|
* This file is part of Purple i2pd project and licensed under BSD3
|
|
|
|
*
|
|
|
|
* See full license text in LICENSE file at top of project tree
|
|
|
|
*/
|
|
|
|
|
|
|
|
#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"
|
|
|
|
#include "Socks5.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 ()
|
|
|
|
{
|
|
|
|
bool ipv6; i2p::config::GetOption("ipv6", ipv6);
|
|
|
|
bool ipv4; i2p::config::GetOption("ipv4", ipv4);
|
|
|
|
bool yggdrasil; i2p::config::GetOption("meshnets.yggdrasil", yggdrasil);
|
|
|
|
|
|
|
|
std::vector<std::string> httpsReseedHostList;
|
|
|
|
if (ipv4 || ipv6)
|
|
|
|
{
|
|
|
|
std::string reseedURLs; i2p::config::GetOption("reseed.urls", reseedURLs);
|
|
|
|
if (!reseedURLs.empty ())
|
|
|
|
boost::split(httpsReseedHostList, reseedURLs, boost::is_any_of(","), boost::token_compress_on);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<std::string> yggReseedHostList;
|
|
|
|
if (yggdrasil && !i2p::util::net::GetYggdrasilAddress ().is_unspecified ())
|
|
|
|
{
|
|
|
|
LogPrint (eLogInfo, "Reseed: Yggdrasil is supported");
|
|
|
|
std::string yggReseedURLs; i2p::config::GetOption("reseed.yggurls", yggReseedURLs);
|
|
|
|
if (!yggReseedURLs.empty ())
|
|
|
|
boost::split(yggReseedHostList, yggReseedURLs, boost::is_any_of(","), boost::token_compress_on);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (httpsReseedHostList.empty () && yggReseedHostList.empty())
|
|
|
|
{
|
|
|
|
LogPrint (eLogWarning, "Reseed: No reseed servers specified");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int reseedRetries = 0;
|
|
|
|
while (reseedRetries < 10)
|
|
|
|
{
|
|
|
|
auto ind = rand () % (httpsReseedHostList.size () + yggReseedHostList.size ());
|
|
|
|
bool isHttps = ind < httpsReseedHostList.size ();
|
|
|
|
std::string reseedUrl = isHttps ? httpsReseedHostList[ind] :
|
|
|
|
yggReseedHostList[ind - httpsReseedHostList.size ()];
|
|
|
|
reseedUrl += "i2pseeds.su3";
|
|
|
|
auto num = ReseedFromSU3Url (reseedUrl, isHttps);
|
|
|
|
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, bool isHttps)
|
|
|
|
{
|
|
|
|
LogPrint (eLogInfo, "Reseed: Downloading SU3 from ", url);
|
|
|
|
std::string su3 = isHttps ? HttpsRequest (url) : YggdrasilRequest (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 (eLogCritical, "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 (eLogCritical, "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, 512, n); // RSA 4096 assumed
|
|
|
|
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 (eLogCritical, "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 descriptor 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: Processing 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)
|
|
|
|
const 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 (eLogCritical, "Reseed: Can't open certificate file ", filename);
|
|
|
|
SSL_CTX_free (ctx);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reseeder::LoadCertificates ()
|
|
|
|
{
|
|
|
|
std::string certDir = i2p::fs::GetCertsDir() + i2p::fs::dirSep + "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 proxyUrl;
|
|
|
|
std::string proxy; i2p::config::GetOption("reseed.proxy", proxy);
|
|
|
|
// check for proxy url
|
|
|
|
if(proxy.size()) {
|
|
|
|
// parse
|
|
|
|
if(proxyUrl.parse(proxy)) {
|
|
|
|
if (proxyUrl.schema == "http" && !proxyUrl.port) {
|
|
|
|
proxyUrl.port = 80;
|
|
|
|
} else if (proxyUrl.schema == "socks" && !proxyUrl.port) {
|
|
|
|
proxyUrl.port = 1080;
|
|
|
|
}
|
|
|
|
// check for valid proxy url schema
|
|
|
|
if (proxyUrl.schema != "http" && proxyUrl.schema != "socks") {
|
|
|
|
LogPrint(eLogCritical, "Reseed: Bad proxy url: ", proxy);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
LogPrint(eLogCritical, "Reseed: Bad proxy url: ", proxy);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
i2p::http::URL url;
|
|
|
|
if (!url.parse(address)) {
|
|
|
|
LogPrint(eLogCritical, "Reseed: Failed to parse url: ", address);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
url.schema = "https";
|
|
|
|
if (!url.port)
|
|
|
|
url.port = 443;
|
|
|
|
|
|
|
|
boost::asio::io_context service;
|
|
|
|
boost::system::error_code ecode;
|
|
|
|
|
|
|
|
boost::asio::ssl::context ctx(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);
|
|
|
|
|
|
|
|
if(proxyUrl.schema.size())
|
|
|
|
{
|
|
|
|
// proxy connection
|
|
|
|
auto it = boost::asio::ip::tcp::resolver(service).resolve (proxyUrl.host, std::to_string(proxyUrl.port), ecode);
|
|
|
|
if(!ecode)
|
|
|
|
{
|
|
|
|
s.lowest_layer().connect(*it.begin (), ecode);
|
|
|
|
if(!ecode)
|
|
|
|
{
|
|
|
|
auto & sock = s.next_layer();
|
|
|
|
if(proxyUrl.schema == "http")
|
|
|
|
{
|
|
|
|
i2p::http::HTTPReq proxyReq;
|
|
|
|
i2p::http::HTTPRes proxyRes;
|
|
|
|
proxyReq.method = "CONNECT";
|
|
|
|
proxyReq.version = "HTTP/1.1";
|
|
|
|
proxyReq.uri = url.host + ":" + std::to_string(url.port);
|
|
|
|
auto auth = i2p::http::CreateBasicAuthorizationString (proxyUrl.user, proxyUrl.pass);
|
|
|
|
if (!auth.empty ())
|
|
|
|
proxyReq.AddHeader("Proxy-Authorization", auth);
|
|
|
|
|
|
|
|
boost::asio::streambuf writebuf, readbuf;
|
|
|
|
std::ostream out(&writebuf);
|
|
|
|
out << proxyReq.to_string();
|
|
|
|
|
|
|
|
boost::asio::write(sock, writebuf.data(), boost::asio::transfer_all(), ecode);
|
|
|
|
if (ecode)
|
|
|
|
{
|
|
|
|
sock.close();
|
|
|
|
LogPrint(eLogError, "Reseed: HTTP CONNECT write error: ", ecode.message());
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
boost::asio::read_until(sock, readbuf, "\r\n\r\n", ecode);
|
|
|
|
if (ecode)
|
|
|
|
{
|
|
|
|
sock.close();
|
|
|
|
LogPrint(eLogError, "Reseed: HTTP CONNECT read error: ", ecode.message());
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
if(proxyRes.parse(std::string {boost::asio::buffers_begin(readbuf.data ()), boost::asio::buffers_begin(readbuf.data ()) + readbuf.size ()}) <= 0)
|
|
|
|
{
|
|
|
|
sock.close();
|
|
|
|
LogPrint(eLogError, "Reseed: HTTP CONNECT malformed reply");
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
if(proxyRes.code != 200)
|
|
|
|
{
|
|
|
|
sock.close();
|
|
|
|
LogPrint(eLogError, "Reseed: HTTP CONNECT got bad status: ", proxyRes.code);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// assume socks if not http, is checked before this for other types
|
|
|
|
// TODO: support username/password auth etc
|
|
|
|
bool success = false;
|
|
|
|
i2p::transport::Socks5Handshake (sock, std::make_pair(url.host, url.port),
|
|
|
|
[&success](const boost::system::error_code& ec)
|
|
|
|
{
|
|
|
|
if (!ec)
|
|
|
|
success = true;
|
|
|
|
else
|
|
|
|
LogPrint (eLogError, "Reseed: SOCKS handshake failed: ", ec.message());
|
|
|
|
});
|
|
|
|
service.run (); // execute all async operations
|
|
|
|
if (!success)
|
|
|
|
{
|
|
|
|
sock.close();
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// direct connection
|
|
|
|
auto endpoints = boost::asio::ip::tcp::resolver(service).resolve (url.host, std::to_string(url.port), ecode);
|
|
|
|
if (!ecode)
|
|
|
|
{
|
|
|
|
bool connected = false;
|
|
|
|
for (const auto& it: endpoints)
|
|
|
|
{
|
|
|
|
boost::asio::ip::tcp::endpoint ep = it;
|
|
|
|
bool supported = false;
|
|
|
|
if (!ep.address ().is_unspecified ())
|
|
|
|
{
|
|
|
|
if (ep.address ().is_v4 ())
|
|
|
|
supported = i2p::context.SupportsV4 ();
|
|
|
|
else if (ep.address ().is_v6 ())
|
|
|
|
supported = i2p::util::net::IsYggdrasilAddress (ep.address ()) ?
|
|
|
|
i2p::context.SupportsMesh () : i2p::context.SupportsV6 ();
|
|
|
|
}
|
|
|
|
if (supported)
|
|
|
|
{
|
|
|
|
s.lowest_layer().connect (ep, ecode);
|
|
|
|
if (!ecode)
|
|
|
|
{
|
|
|
|
LogPrint (eLogDebug, "Reseed: Resolved to ", ep.address ());
|
|
|
|
connected = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!connected)
|
|
|
|
{
|
|
|
|
LogPrint(eLogError, "Reseed: Failed to connect to ", url.host);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
return ReseedRequest (s, url.to_string());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
LogPrint (eLogError, "Reseed: SSL handshake failed: ", ecode.message ());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
LogPrint (eLogError, "Reseed: Couldn't connect to ", url.host, ": ", ecode.message ());
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename Stream>
|
|
|
|
std::string Reseeder::ReseedRequest (Stream& s, const std::string& uri)
|
|
|
|
{
|
|
|
|
boost::system::error_code ecode;
|
|
|
|
i2p::http::HTTPReq req;
|
|
|
|
req.uri = uri;
|
|
|
|
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 ", uri);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
if (res.code != 200) {
|
|
|
|
LogPrint(eLogError, "Reseed: Failed to reseed from ", uri, ", http code ", res.code);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
data.erase(0, len); /* drop http headers from response */
|
|
|
|
LogPrint(eLogDebug, "Reseed: Got ", data.length(), " bytes of data from ", uri);
|
|
|
|
if (res.is_chunked()) {
|
|
|
|
std::stringstream in(data), out;
|
|
|
|
if (!i2p::http::MergeChunkedResponse(in, out)) {
|
|
|
|
LogPrint(eLogWarning, "Reseed: Failed to merge chunked response from ", uri);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
LogPrint(eLogDebug, "Reseed: Got ", data.length(), "(", out.tellg(), ") bytes of data from ", uri);
|
|
|
|
data = out.str();
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string Reseeder::YggdrasilRequest (const std::string& address)
|
|
|
|
{
|
|
|
|
i2p::http::URL url;
|
|
|
|
if (!url.parse(address))
|
|
|
|
{
|
|
|
|
LogPrint(eLogError, "Reseed: Failed to parse url: ", address);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
url.schema = "http";
|
|
|
|
if (!url.port) url.port = 80;
|
|
|
|
|
|
|
|
boost::system::error_code ecode;
|
|
|
|
boost::asio::io_context service;
|
|
|
|
boost::asio::ip::tcp::socket s(service, boost::asio::ip::tcp::v6());
|
|
|
|
|
|
|
|
auto endpoints = boost::asio::ip::tcp::resolver(service).resolve (url.host, std::to_string(url.port), ecode);
|
|
|
|
if (!ecode)
|
|
|
|
{
|
|
|
|
bool connected = false;
|
|
|
|
for (const auto& it: endpoints)
|
|
|
|
{
|
|
|
|
boost::asio::ip::tcp::endpoint ep = it;
|
|
|
|
if (
|
|
|
|
i2p::util::net::IsYggdrasilAddress (ep.address ()) &&
|
|
|
|
i2p::context.SupportsMesh ()
|
|
|
|
)
|
|
|
|
{
|
|
|
|
LogPrint (eLogDebug, "Reseed: Yggdrasil: Resolved to ", ep.address ());
|
|
|
|
s.connect (ep, ecode);
|
|
|
|
if (!ecode)
|
|
|
|
{
|
|
|
|
connected = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!connected)
|
|
|
|
{
|
|
|
|
LogPrint(eLogError, "Reseed: Yggdrasil: Failed to connect to ", url.host);
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ecode)
|
|
|
|
{
|
|
|
|
LogPrint (eLogDebug, "Reseed: Yggdrasil: Connected to ", url.host, ":", url.port);
|
|
|
|
return ReseedRequest (s, url.to_string());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
LogPrint (eLogError, "Reseed: Yggdrasil: Couldn't connect to ", url.host, ": ", ecode.message ());
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|