Fulcrum/src/Util.cpp
Calin Culianu 3d8e3de4e7
Added WebSockets classes and low-level code
Implemented RFC 6455 from: https://tools.ietf.org/html/rfc6455

Our implementation lives in WebSockets.h and WebSockets.cpp. It provides
low-level code for serializing/deserializing data to/from the wire as
well as a higher level "Wrapper" class that derives from QTcpSocket and
is used to wrap an existing QTcpSocket as a facade.

The WebSockets::Wrapper is intented to be used asynchronously via the
readyRead() or messagesReady() signal.

Writing to the WebSocket can be accomplished via the write() method (as
with a regular QTcpSocket), in which case the entire QByteArray buffer
is sent down the wire as a single message (possibly fragmented) of
either Text or Binary type (default Text).

Additionally, custom methods sendText() and sendBinary() are provided.

PING/PONG support is provided via an optional automatic mechanism (on by
default).  CLOSE is handled automatically as well.
2020-04-15 19:48:21 +03:00

425 lines
14 KiB
C++

//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#include "App.h"
#include "Logger.h"
#include "Util.h"
// below headers are for getN*Processors, etc.
#if defined(Q_OS_DARWIN)
# include <sys/types.h>
# include <sys/sysctl.h>
# include <mach/mach_time.h>
#elif defined(Q_OS_LINUX)
# include <unistd.h>
# include <time.h>
#elif defined(Q_OS_WINDOWS)
#define WIN32_LEAN_AND_MEAN 1
# include <windows.h>
#endif
#include <iostream>
#include <thread>
namespace Util {
QString basename(const QString &s) {
QRegExp re("[\\/]");
auto toks = s.split(re);
return toks.last();
}
#if defined(Q_OS_LINUX)
static int64_t getAbsTimeNS()
{
struct timespec ts;
// Note: CLOCK_MONOTONIC does *not* include the time spent suspended. If we want that, then we can Use
// CLOCK_BOOTTIME here for that.
if (clock_gettime(CLOCK_MONOTONIC, &ts)) {
ts = {0, 0};
// We can't do a Warning() or Error() here because that would cause infinite recursion.
// This is an unlikely and also pretty fatal situation, though, so we must warn
std::cerr << "Fatal: clock_gettime for CLOCK_MONOTONIC returned error status: %s" << strerror(errno) << std::endl;
}
return int64_t(ts.tv_sec * 1000000000LL) + int64_t(ts.tv_nsec);
}
static int64_t absT0 = getAbsTimeNS();
qint64 getTimeNS() {
const auto now = getAbsTimeNS();
return now - absT0;
}
qint64 getTime() {
return getTimeNS()/1000000LL;
}
bool isClockSteady() { return true; }
#elif defined(Q_OS_WINDOWS)
// Windows lacks a decent high resolution clock source on some C++ implementations (such as MinGW). So we
// query the OS's QPC mechanism, which, on Windows 7+ is very fast to query and guaranteed to be accurate, and also
// monotocic ("steady").
static int64_t getAbsTimeNS()
{
static __int64 freq = 0;
__int64 ct, factor;
if (!freq) {
QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
}
QueryPerformanceCounter((LARGE_INTEGER *)&ct); // reads the current time (in system units)
factor = 1000000000LL/freq;
if (factor <= 0) factor = 1;
return int64_t(ct * factor);
}
static qint64 absT0 = qint64(getAbsTimeNS()); // initializes static data inside getAbsTimeNS() once at startup in main thread.
qint64 getTimeNS() {
const auto now = getAbsTimeNS();
return now - absT0;
}
qint64 getTime() {
return getTimeNS()/1000000LL;
}
bool isClockSteady() { return true; }
#else
// MacOS or generic platform (on MacOS with clang this happens to be very accurate)
static const auto t0 = std::chrono::high_resolution_clock::now();
qint64 getTime() {
const auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now - t0).count();
}
qint64 getTimeNS() {
const auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::nanoseconds>(now - t0).count();
}
bool isClockSteady() {
return std::chrono::high_resolution_clock::is_steady;
}
#endif
qint64 getTimeMicros() {
return getTimeNS()/1000LL;
}
double getTimeSecs() {
return double(getTime()) / 1e3;
}
namespace Json {
QVariant parseString(const QString &str, bool expectMap) {
QJsonParseError e;
QJsonDocument d = QJsonDocument::fromJson(str.toUtf8(), &e);
if (d.isNull())
throw ParseError(QString("Error parsing Json from string: %1").arg(e.errorString()));
auto v = d.toVariant();
if (expectMap && v.type() != QVariant::Map)
throw Error("Json Error, expected map, got a list instead");
if (!expectMap && v.type() != QVariant::List)
throw Error("Json Error, expected list, got a map instead");
return v;
}
QVariant parseFile(const QString &file, bool expectMap) {
QFile f(file);
if (!f.open(QFile::ReadOnly))
throw Error(QString("Could not open file: %1").arg(file));
QString s(f.readAll());
return parseString(s, expectMap);
}
QString toString(const QVariant &v, bool compact) {
if (v.isNull() || !v.isValid()) throw Error("Empty or invalid QVariant passed to Json::toString");
auto d = QJsonDocument::fromVariant(v);
if (d.isNull())
throw Error("Bad QVariant pased to Json::toString");
return d.toJson(compact ? QJsonDocument::Compact : QJsonDocument::Indented);
}
} // end namespace Json
bool VoidFuncOnObjectNoThrow(const QObject *obj, const std::function<void()> & lambda, int timeout_ms)
{
try {
LambdaOnObject<void>(obj, lambda, timeout_ms);
return true;
} catch (const Exception &) {}
return false;
}
#if defined(Q_OS_DARWIN)
unsigned getNVirtualProcessors()
{
static std::atomic<unsigned> nVProcs = 0;
if (!nVProcs) {
int a = 0;
size_t b = sizeof(a);
if (0 == sysctlbyname("hw.ncpu",&a, &b, nullptr, 0)) {
nVProcs = unsigned(a); // this returns virtual CPUs which isn't always what we want..
}
}
return nVProcs.load() ? nVProcs.load() : 1;
}
unsigned getNPhysicalProcessors()
{
static std::atomic<unsigned> nProcs = 0;
if (!nProcs) {
int a = 0;
size_t b = sizeof(a);
if (0 == sysctlbyname("hw.physicalcpu",&a,&b,nullptr,0)) {
nProcs = unsigned(a);
}
//Debug() << "nProcs = " << nProcs;// << " a:" << a << " b:" << b;
}
return nProcs.load() ? nProcs.load() : 1;
}
#elif defined(Q_OS_LINUX)
unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
unsigned getNPhysicalProcessors() {
static std::atomic<unsigned> nProcs = 0;
if (!nProcs) {
nProcs = unsigned(sysconf(_SC_NPROCESSORS_ONLN));
}
return nProcs.load() ? nProcs.load() : 1;
}
#else
unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
unsigned getNPhysicalProcessors() { return std::thread::hardware_concurrency(); }
#endif
QByteArray ParseHexFast(const QByteArray &hex, bool checkDigits)
{
const int size = hex.size();
QByteArray ret(size / 2, Qt::Initialization::Uninitialized);
if (UNLIKELY(size % 2)) {
// bad / not hex because not even number of chars.
ret.clear();
return ret;
}
const char *d = hex.constData(), * const dend = d + size;
for (char c1, c2, *out = ret.data(); d < dend; d += 2, ++out) {
constexpr char offset_A = 'A' - 0xa,
offset_a = 'a' - 0xa,
offset_0 = '0';
// slightly unrolled loop, does 2 chars at a time
c1 = d[0];
c2 = d[1];
// c1
if (c1 <= '9') // this is the most likely for any random digit, so we check this first
c1 -= offset_0;
else if (c1 >= 'a') // next, we anticipate lcase, so we do this check first
c1 -= offset_a;
else // c1 >= 'A'
c1 -= offset_A;
// c2
if (c2 <= '9') // this is the most likely for any random digit, so we check this first
c2 -= offset_0;
else if (c2 >= 'a') // next, we anticipate lcase, so we do this check first
c2 -= offset_a;
else // c2 >= 'A'
c2 -= offset_A;
// The below is slowish... we can just accept bad hex data as 'corrupt' ...
// checkDigit = false allows us to skip this check, making this function >5x faster!
if (UNLIKELY(checkDigits && (c1 < 0 || c1 > 0xf || c2 < 0 || c2 > 0xf))) { // ensure data was actually in range
ret.clear();
break;
}
*out = char(c1 << 4) | c2;
}
return ret;
}
QByteArray ToHexFast(const QByteArray &ba)
{
QByteArray ret(ba.size()*2, Qt::Initialization::Uninitialized);
if (!ToHexFastInPlace(ba, ret.data(), size_t(ret.size())))
ret.clear();
return ret;
}
bool ToHexFastInPlace(const QByteArray &ba, char *out, size_t bufsz)
{
const int size = ba.size();
if (bufsz < size_t(size*2))
return false;
const char *cur = ba.constData(), * const end = cur + size;
for (char c1, c2; cur < end; ++cur, out += 2) {
constexpr char dist_from_9_to_a = ('a'-'9')-1;
c1 = ((*cur >> 4) & 0xf) + '0';
c2 = (*cur & 0xf) + '0';
if (c1 > '9') c1 += dist_from_9_to_a;
if (c2 > '9') c2 += dist_from_9_to_a;
out[0] = c1;
out[1] = c2;
}
return true;
}
} // end namespace Util
Log::Log() {}
Log::Log(Color c)
{
setColor(c);
}
Log::Log(const char *fmt...)
: s()
{
va_list ap;
va_start(ap,fmt);
str = QString::vasprintf(fmt,ap);
va_end(ap);
s.setString(&str, QIODevice::WriteOnly|QIODevice::Append);
}
Log::~Log()
{
if (doprt) {
App *ourApp = app();
s.flush(); // does nothing probably..
// note: we always want to log the timestamp, even in syslog mode.
// this is because if logging from a thread, log lines may be out-of-order.
// The timestamp is the only record of the actual order in which things
// occurred. Currently the timestamp is to 4 decimal places (hundreds of micros)
const auto unow = Util::getTimeNS()/1000LL;
const QString tsStr = QString::asprintf("[%lld.%04d] ", unow/1000000LL, int((unow/100LL)%10000));
QString thrdStr = "";
if (QThread *th = QThread::currentThread(); th && ourApp && th != ourApp->thread()) {
QString thrdName = th->objectName();
if (thrdName.trimmed().isEmpty()) thrdName = QString::asprintf("%p", reinterpret_cast<void *>(QThread::currentThreadId()));
thrdStr = QStringLiteral("<Thr: %1> ").arg(thrdName);
}
Logger *logger = ourApp ? ourApp->logger() : nullptr;
QString theString = tsStr + thrdStr + (logger && logger->isaTTY() ? colorize(str, color) : str);
if (logger) {
emit logger->log(level, theString);
} else {
// just print to console for now..
std::cerr << Q2C(theString) << std::endl << std::flush;
}
}
}
/* static */
QString Log::colorString(Color c) {
const char *suffix = "[0m"; // normal
switch(c) {
case Black: suffix = "[30m"; break;
case Red: suffix = "[31m"; break;
case Green: suffix = "[32m"; break;
case Yellow: suffix = "[33m"; break;
case Blue: suffix = "[34m"; break;
case Magenta: suffix = "[35m"; break;
case Cyan: suffix = "[36m"; break;
case White: suffix = "[37m"; break;
case BrightBlack: suffix = "[30;1m"; break;
case BrightRed: suffix = "[31;1m"; break;
case BrightGreen: suffix = "[1,32m"; break;
case BrightYellow: suffix = "[33;1m"; break;
case BrightBlue: suffix = "[34;1m"; break;
case BrightMagenta: suffix = "[35;1m"; break;
case BrightCyan: suffix = "[36;1m"; break;
case BrightWhite: suffix = "[37;1m"; break;
default:
// will just use normal
break;
}
static const char prefix[2] = { 033, 0 }; // esc 033 in octal
return QString::asprintf("%s%s", prefix, suffix);
}
QString Log::colorize(const QString &str, Color c) {
QString colorStr = useColor && c != Normal ? colorString(c) : "";
QString normalStr = useColor && c != Normal ? colorString(Normal) : "";
return colorStr + str + normalStr;
}
template <> Log & Log::operator<<(const Color &c) { setColor(c); return *this; }
Debug::~Debug()
{
level = Logger::Level::Debug;
doprt = isEnabled();
if (!doprt) return;
if (!colorOverridden) color = Cyan;
str = QString("(Debug) ") + str;
}
bool Debug::forceEnable = false;
bool Debug::isEnabled() {
auto ourApp = app();
return forceEnable || !ourApp || !ourApp->options || ourApp->options->verboseDebug;
}
Trace::~Trace()
{
level = Logger::Level::Debug;
doprt = isEnabled();
if (!doprt) return;
if (!colorOverridden) color = Green;
str = QString("(Trace) ") + str;
}
bool Trace::forceEnable = false;
bool Trace::isEnabled() {
auto ourApp = app();
return forceEnable
|| (ourApp && ourApp->options && ourApp->options->verboseTrace && ourApp->options->verboseDebug); // both trace and debug must be on
}
Error::~Error()
{
level = Logger::Level::Critical;
if (!colorOverridden) color = BrightRed;
}
Warning::~Warning()
{
level = Logger::Level::Warning;
if (!colorOverridden) color = Yellow;
}
Fatal::~Fatal()
{
level = Logger::Level::Fatal;
str = QString("FATAL: ") + str;
if (!colorOverridden) color = BrightRed;
}
FatalAssert::FatalAssert(bool expr)
: assertion(expr)
{
doprt = !assertion;
}
FatalAssert::~FatalAssert()
{
if ((doprt = !assertion)) {
level = Logger::Level::Fatal;
str = QString("ASSERTION FAILED: ") + str;
if (!colorOverridden) color = BrightRed;
}
}