diff --git a/.github/workflows/build-qv2ray-cmake.yml b/.github/workflows/build-qv2ray-cmake.yml new file mode 100644 index 0000000..cc8f37c --- /dev/null +++ b/.github/workflows/build-qv2ray-cmake.yml @@ -0,0 +1,146 @@ +name: Nekoray build matrix - cmake + +on: + workflow_dispatch: + inputs: + tag: + description: 'Release Tag' + required: true + publish: + description: 'Publish: If want ignore' + required: false +jobs: + build: + strategy: + matrix: + platform: [ windows-2022, ubuntu-18.04 ] + arch: [ x64 ] + build_type: [ Release ] + qt_version: [ 5.15.2 ] + include: + - platform: windows-2022 + arch: x64 + qtarch: win64_msvc2019_64 + fail-fast: false + + runs-on: ${{ matrix.platform }} + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Checking out sources + uses: actions/checkout@v2 + with: + submodules: "recursive" + - name: Install MSVC compiler + if: matrix.platform == 'windows-2022' + uses: ilammy/msvc-dev-cmd@v1 + with: + # 14.1 is for vs2017, 14.2 is vs2019, following the upstream vcpkg build from Qv2ray-deps repo + toolset: 14.2 + arch: ${{ matrix.arch }} + - name: Cache Qt + id: cache-qt + uses: actions/cache@v3 + with: + path: ${{ runner.workspace }}/Qt + key: QtCache-${{ matrix.platform }}-${{ matrix.arch }}-${{ matrix.qt_version }} + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt_version }} + py7zrversion: ' ' + aqtversion: ' ' + setup-python: false + cached: ${{ steps.cache-qt.outputs.cache-hit }} + # ========================================================================================================= Other install + - name: Windows - ${{ matrix.arch }} - ${{ matrix.qt_version }} - Setup Ninja + if: matrix.platform == 'windows-2022' + uses: ashutoshvarma/setup-ninja@master + with: + # ninja version to download. Default: 1.10.0 + version: 1.10.0 + - name: Linux - ${{ matrix.arch }} - ${{ matrix.qt_version }} - Setup Ninja + shell: bash + if: matrix.platform == 'ubuntu-18.04' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build + - name: Install Golang + uses: actions/setup-go@v2 + with: + stable: false + go-version: 1.18.5 + # ========================================================================================================= 编译与 Qt 无关的依赖 + - name: Cache Download + id: cache-deps + uses: actions/cache@v2 + with: + path: libs/deps + key: ${{ hashFiles('libs/build*.sh') }} + - name: Build Dependencies + shell: bash + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + ./libs/build_deps_all.sh + # ========================================================================================================= Generate MakeFile and Build + - name: Windows - ${{ matrix.qt_version }} - Generate MakeFile and Build + shell: bash + if: matrix.platform == 'windows-2022' + env: + CC: cl.exe + CXX: cl.exe + run: | + mkdir build + cd build + cmake .. -GNinja \ + -DCMAKE_PREFIX_PATH=./libs/deps/built \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake --build . --parallel $(nproc) + cd .. + ./libs/deploy_windows64.sh + - name: Linux - ${{ matrix.qt_version }} - Generate MakeFile and Build + shell: bash + if: matrix.platform == 'ubuntu-18.04' + run: | + mkdir build + cd build + cmake .. -GNinja \ + -DCMAKE_PREFIX_PATH=./libs/deps/built \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake --build . --parallel $(nproc) + cd .. + ./libs/deploy_linux64.sh + # ========================================================================================================= Deployments + - name: Uploading Artifact + uses: actions/upload-artifact@master + with: + name: NekoRay-${{ github.sha }}-${{ matrix.platform }}-${{ matrix.arch }} + path: ./deployment/ + publish: + name: Publish Release + if: github.event.inputs.publish != 'y' + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: NekoRay-${{ github.sha }}-ubuntu-18.04-x64 + path: artifacts-linux + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: NekoRay-${{ github.sha }}-windows-2022-x64 + path: artifacts-windows + - name: Release + run: | + wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz + tar -xvf ghr.tar.gz + mv ghr*linux_amd64/ghr . + mkdir apks + find artifacts-linux -name "*.tar.gz" -exec cp {} apks \; + find artifacts-windows -name "*.zip" -exec cp {} apks \; + ./ghr -delete -t "${{ github.token }}" -n "${{ github.event.inputs.tag }}" "${{ github.event.inputs.tag }}" apks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a148a15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Custom +/nekoray/ +/build/ +CMakeLists.txt.user* +/cmake-build-* +/build-* +.vscode +.idea + +# Deploy +/deployment diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..72a74b8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3rdparty/QHotkey"] + path = 3rdparty/QHotkey + url = https://github.com/Skycoder42/QHotkey.git diff --git a/3rdparty/QHotkey b/3rdparty/QHotkey new file mode 160000 index 0000000..52e25ac --- /dev/null +++ b/3rdparty/QHotkey @@ -0,0 +1 @@ +Subproject commit 52e25acf221e5ac86ce648f6922620fb2d6a7121 diff --git a/3rdparty/QThreadCreateThread.hpp b/3rdparty/QThreadCreateThread.hpp new file mode 100644 index 0000000..2822ba8 --- /dev/null +++ b/3rdparty/QThreadCreateThread.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +// FOR OLD QT + +class QThreadCreateThread : public QThread { +public: + explicit QThreadCreateThread(std::future &&future) + : m_future(std::move(future)) { + // deleteLater + connect(this, &QThread::finished, this, &QThread::deleteLater); + } + +private: + void run() override { + m_future.get(); + } + + std::future m_future; +}; + +inline QThread *createThreadImpl(std::future &&future) { + return new QThreadCreateThread(std::move(future)); +} + +template +QThread *createQThread(Function &&f, Args &&... args) { + using DecayedFunction = typename std::decay::type; + auto threadFunction = + [f = static_cast(std::forward(f))](auto &&... largs) mutable -> void { + (void) std::invoke(std::move(f), std::forward(largs)...); + }; + + return createThreadImpl(std::async(std::launch::deferred, + std::move(threadFunction), + std::forward(args)...)); +} diff --git a/3rdparty/QtExtKeySequenceEdit.cpp b/3rdparty/QtExtKeySequenceEdit.cpp new file mode 100644 index 0000000..02bb80c --- /dev/null +++ b/3rdparty/QtExtKeySequenceEdit.cpp @@ -0,0 +1,22 @@ +#include "QtExtKeySequenceEdit.h" + +QtExtKeySequenceEdit::QtExtKeySequenceEdit(QWidget *parent) + : QKeySequenceEdit(parent) { +} + +QtExtKeySequenceEdit::~QtExtKeySequenceEdit() { +} + +void QtExtKeySequenceEdit::keyPressEvent(QKeyEvent *pEvent) { + QKeySequenceEdit::keyPressEvent(pEvent); + + QKeySequence keySeq = keySequence(); + if (keySeq.count() <= 0) { + return; + } + int key = keySeq[0]; + if (key == Qt::Key_Backspace || key == Qt::Key_Delete) { + key = 0; + } + setKeySequence(key); +} diff --git a/3rdparty/QtExtKeySequenceEdit.h b/3rdparty/QtExtKeySequenceEdit.h new file mode 100644 index 0000000..ebeb848 --- /dev/null +++ b/3rdparty/QtExtKeySequenceEdit.h @@ -0,0 +1,11 @@ +#include + +class QtExtKeySequenceEdit : public QKeySequenceEdit { +public: + QtExtKeySequenceEdit(QWidget *parent); + + ~QtExtKeySequenceEdit(); + +protected: + virtual void keyPressEvent(QKeyEvent *pEvent); +}; diff --git a/3rdparty/RunGuard.hpp b/3rdparty/RunGuard.hpp new file mode 100644 index 0000000..3702610 --- /dev/null +++ b/3rdparty/RunGuard.hpp @@ -0,0 +1,98 @@ +#ifndef RUNGUARD_H +#define RUNGUARD_H + +#include +#include +#include +#include + +class RunGuard { + +public: + RunGuard(const QString &key); + + ~RunGuard(); + + bool isAnotherRunning(); + + bool tryToRun(); + + void release(); + +private: + const QString key; + const QString memLockKey; + const QString sharedmemKey; + + QSharedMemory sharedMem; + QSystemSemaphore memLock; + + Q_DISABLE_COPY(RunGuard) +}; + +namespace { + + QString generateKeyHash(const QString &key, const QString &salt) { + QByteArray data; + + data.append(key.toUtf8()); + data.append(salt.toUtf8()); + data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); + + return data; + } + +} + + +RunGuard::RunGuard(const QString &key) + : key(key), memLockKey(generateKeyHash(key, "_memLockKey")), + sharedmemKey(generateKeyHash(key, "_sharedmemKey")), sharedMem(sharedmemKey), memLock(memLockKey, 1) { + memLock.acquire(); + { + QSharedMemory fix(sharedmemKey); // Fix for *nix: http://habrahabr.ru/post/173281/ + fix.attach(); + } + memLock.release(); +} + +RunGuard::~RunGuard() { + release(); +} + +bool RunGuard::isAnotherRunning() { + if (sharedMem.isAttached()) + return false; + + memLock.acquire(); + const bool isRunning = sharedMem.attach(); + if (isRunning) + sharedMem.detach(); + memLock.release(); + + return isRunning; +} + +bool RunGuard::tryToRun() { + if (isAnotherRunning()) // Extra check + return false; + + memLock.acquire(); + const bool result = sharedMem.create(sizeof(quint64)); + memLock.release(); + if (!result) { + release(); + return false; + } + + return true; +} + +void RunGuard::release() { + memLock.acquire(); + if (sharedMem.isAttached()) + sharedMem.detach(); + memLock.release(); +} + +#endif // RUNGUARD_H diff --git a/3rdparty/VT100Parser.hpp b/3rdparty/VT100Parser.hpp new file mode 100644 index 0000000..9887a3f --- /dev/null +++ b/3rdparty/VT100Parser.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +inline QString cleanVT100String(const QString &in) { + QString out; + bool in_033 = false; + for (auto &&chr: in) { + if (chr == '\033') { + in_033 = true; + continue; + } + if (in_033) { + if (chr == 'm') { + in_033 = false; + } + continue; + } + out += chr; + } + return out; +} diff --git a/3rdparty/WinCommander.cpp b/3rdparty/WinCommander.cpp new file mode 100644 index 0000000..7d2245b --- /dev/null +++ b/3rdparty/WinCommander.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2014 UpdateNode UG (haftungsbeschränkt) +** Contact: code@updatenode.com +** +** This file is part of the UpdateNode Client. +** +** Commercial License Usage +** Licensees holding valid commercial UpdateNode license may use this file +** under the terms of the the Apache License, Version 2.0 +** Full license description file: LICENSE.COM +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation. Please review the following information to ensure the +** GNU General Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** Full license description file: LICENSE.GPL +** +****************************************************************************/ + +#include "WinCommander.hpp" + +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#define MAX_KEY_LENGTH 255 +#define MAX_VALUE_NAME 16383 +#endif + + +/*! +Executes a command elevated specified by \apath , using paramters \aparameters. +\n +Parameter /aaWait decides if the function should return immediatelly after it's\n +execution or wait for the exit of the launched process +\n +Returns the return value of the executed command +*/ +uint WinCommander::runProcessElevated(const QString &path, + const QStringList ¶meters, + const QString &workingDir, bool aWait) { + uint result = 0; + +#ifdef Q_OS_WIN + QString params; + HWND hwnd = NULL; + LPCTSTR pszPath = (LPCTSTR)path.utf16(); + foreach(QString item, parameters) + params += "\"" + item + "\" "; + + LPCTSTR pszParameters = (LPCTSTR)params.utf16(); + QString dir; + if (workingDir.isEmpty()) + dir = QDir::toNativeSeparators(QDir::currentPath()); + else + dir = QDir::toNativeSeparators(workingDir); + LPCTSTR pszDirectory = (LPCTSTR)dir.utf16(); + + SHELLEXECUTEINFO shex; + DWORD dwCode = 0; + + ZeroMemory(&shex, sizeof(shex)); + + shex.cbSize = sizeof(shex); + shex.fMask = SEE_MASK_NOCLOSEPROCESS; + shex.hwnd = hwnd; + shex.lpVerb = TEXT("runas"); + shex.lpFile = pszPath; + shex.lpParameters = pszParameters; + shex.lpDirectory = pszDirectory; + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow + shex.nShow = SW_SHOWMINIMIZED; + + ShellExecuteEx(&shex); + if (shex.hProcess) + { + if(aWait) + { + WaitForSingleObject(shex.hProcess, INFINITE ); + GetExitCodeProcess(shex.hProcess, &dwCode); + } + CloseHandle (shex.hProcess) ; + } + else + return -1; + + result = (uint)dwCode; +#else + Q_UNUSED(path); + Q_UNUSED(parameters); + Q_UNUSED(workingDir); + Q_UNUSED(aWait); +#endif + return result; +} + +/*! +Executes a command elevated specified by \apath , using paramters \aparameters. +\n +Parameter /aaWait decides if the function should return immediatelly after it's\n +execution or wait for the exit of the launched process +\n +Returns the return value of the executed command +*/ +uint WinCommander::runProcessElevated(const QString &path, const QString ¶meters, const QString &workingDir, + bool aWait) { + return runProcessElevated(path, QStringList() << parameters, workingDir, aWait); +} diff --git a/3rdparty/WinCommander.hpp b/3rdparty/WinCommander.hpp new file mode 100644 index 0000000..0b6c728 --- /dev/null +++ b/3rdparty/WinCommander.hpp @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2014 UpdateNode UG (haftungsbeschränkt) +** Contact: code@updatenode.com +** +** This file is part of the UpdateNode Client. +** +** Commercial License Usage +** Licensees holding valid commercial UpdateNode license may use this file +** under the terms of the the Apache License, Version 2.0 +** Full license description file: LICENSE.COM +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation. Please review the following information to ensure the +** GNU General Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** Full license description file: LICENSE.GPL +** +****************************************************************************/ + +#ifndef WINCOMMANDER_H +#define WINCOMMANDER_H + +#include +#include + +class WinCommander { +public: + static uint runProcessElevated(const QString &path, + const QStringList ¶meters = QStringList(), + const QString &workingDir = QString(), + bool aWait = true); + + static uint runProcessElevated(const QString &path, + const QString ¶meters = QString(), + const QString &workingDir = QString(), + bool aWait = true); +}; + +#endif // WINCOMMANDER_H \ No newline at end of file diff --git a/3rdparty/ZxingQtReader.hpp b/3rdparty/ZxingQtReader.hpp new file mode 100644 index 0000000..423c756 --- /dev/null +++ b/3rdparty/ZxingQtReader.hpp @@ -0,0 +1,386 @@ +#pragma once +/* + * Copyright 2020 Axel Waggershauser + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ZXing/ReadBarcode.h" + +#include +#include +#include + +#ifdef QT_MULTIMEDIA_LIB +#include +#include +#endif + +// This is some sample code to start a discussion about how a minimal and header-only Qt wrapper/helper could look like. + +namespace ZXingQt { + + Q_NAMESPACE + +//TODO: find a better way to export these enums to QML than to duplicate their definition +// #ifdef Q_MOC_RUN produces meta information in the moc output but it does end up working in qml +#ifdef QT_QML_LIB + enum class BarcodeFormat +{ + None = 0, ///< Used as a return value if no valid barcode has been detected + Aztec = (1 << 0), ///< Aztec (2D) + Codabar = (1 << 1), ///< Codabar (1D) + Code39 = (1 << 2), ///< Code39 (1D) + Code93 = (1 << 3), ///< Code93 (1D) + Code128 = (1 << 4), ///< Code128 (1D) + DataBar = (1 << 5), ///< GS1 DataBar, formerly known as RSS 14 + DataBarExpanded = (1 << 6), ///< GS1 DataBar Expanded, formerly known as RSS EXPANDED + DataMatrix = (1 << 7), ///< DataMatrix (2D) + EAN8 = (1 << 8), ///< EAN-8 (1D) + EAN13 = (1 << 9), ///< EAN-13 (1D) + ITF = (1 << 10), ///< ITF (Interleaved Two of Five) (1D) + MaxiCode = (1 << 11), ///< MaxiCode (2D) + PDF417 = (1 << 12), ///< PDF417 (1D) or (2D) + QRCode = (1 << 13), ///< QR Code (2D) + UPCA = (1 << 14), ///< UPC-A (1D) + UPCE = (1 << 15), ///< UPC-E (1D) + MicroQRCode = (1 << 16), ///< Micro QR Code (2D) + + OneDCodes = Codabar | Code39 | Code93 | Code128 | EAN8 | EAN13 | ITF | DataBar | DataBarExpanded | UPCA | UPCE, + TwoDCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode, +}; + +enum class DecodeStatus +{ + NoError = 0, + NotFound, + FormatError, + ChecksumError, +}; +#else + using ZXing::BarcodeFormat; + using ZXing::DecodeStatus; +#endif + + using ZXing::DecodeHints; + using ZXing::Binarizer; + using ZXing::BarcodeFormats; + + Q_ENUM_NS(BarcodeFormat) + Q_ENUM_NS(DecodeStatus) + + template + QDebug operator<<(QDebug dbg, const T& v) + { + return dbg.noquote() << QString::fromStdString(ToString(v)); + } + + class Position : public ZXing::Quadrilateral + { + Q_GADGET + + Q_PROPERTY(QPoint topLeft READ topLeft) + Q_PROPERTY(QPoint topRight READ topRight) + Q_PROPERTY(QPoint bottomRight READ bottomRight) + Q_PROPERTY(QPoint bottomLeft READ bottomLeft) + + using Base = ZXing::Quadrilateral; + + public: + using Base::Base; + }; + + class Result : private ZXing::Result + { + Q_GADGET + + Q_PROPERTY(BarcodeFormat format READ format) + Q_PROPERTY(QString formatName READ formatName) + Q_PROPERTY(QString text READ text) + Q_PROPERTY(QByteArray rawBytes READ rawBytes) + Q_PROPERTY(bool isValid READ isValid) + Q_PROPERTY(DecodeStatus status READ status) + Q_PROPERTY(Position position READ position) + + QString _text; + QByteArray _rawBytes; + Position _position; + + public: + Result() : ZXing::Result(ZXing::DecodeStatus::NotFound) {} // required for qmetatype machinery + + explicit Result(ZXing::Result&& r) : ZXing::Result(std::move(r)) { + _text = QString::fromWCharArray(ZXing::Result::text().c_str()); + _rawBytes = QByteArray(reinterpret_cast(ZXing::Result::rawBytes().data()), + Size(ZXing::Result::rawBytes())); + auto& pos = ZXing::Result::position(); + auto qp = [&pos](int i) { return QPoint(pos[i].x, pos[i].y); }; + _position = {qp(0), qp(1), qp(2), qp(3)}; + } + + using ZXing::Result::isValid; + + BarcodeFormat format() const { return static_cast(ZXing::Result::format()); } + DecodeStatus status() const { return static_cast(ZXing::Result::status()); } + QString formatName() const { return QString::fromStdString(ZXing::ToString(ZXing::Result::format())); } + const QString& text() const { return _text; } + const QByteArray& rawBytes() const { return _rawBytes; } + const Position& position() const { return _position; } + + // For debugging/development + int runTime = 0; + Q_PROPERTY(int runTime MEMBER runTime) + }; + + inline Result ReadBarcode(const QImage& img, const DecodeHints& hints = {}) + { + using namespace ZXing; + + auto ImgFmtFromQImg = [](const QImage& img) { + switch (img.format()) { + case QImage::Format_ARGB32: + case QImage::Format_RGB32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + return ImageFormat::BGRX; +#else + return ImageFormat::XRGB; +#endif + case QImage::Format_RGB888: return ImageFormat::RGB; + case QImage::Format_RGBX8888: + case QImage::Format_RGBA8888: return ImageFormat::RGBX; + case QImage::Format_Grayscale8: return ImageFormat::Lum; + default: return ImageFormat::None; + } + }; + + auto exec = [&](const QImage& img) { + return Result(ZXing::ReadBarcode( + {img.bits(), img.width(), img.height(), ImgFmtFromQImg(img), static_cast(img.bytesPerLine())}, hints)); + }; + + return ImgFmtFromQImg(img) == ImageFormat::None ? exec(img.convertToFormat(QImage::Format_Grayscale8)) : exec(img); + } + +#ifdef QT_MULTIMEDIA_LIB + inline Result ReadBarcode(const QVideoFrame& frame, const DecodeHints& hints = {}) +{ + using namespace ZXing; + + auto img = frame; // shallow copy just get access to non-const map() function + if (!frame.isValid() || !img.map(QAbstractVideoBuffer::ReadOnly)){ + qWarning() << "invalid QVideoFrame: could not map memory"; + return {}; + } + //TODO c++17: SCOPE_EXIT([&] { img.unmap(); }); + + ImageFormat fmt = ImageFormat::None; + int pixStride = 0; + int pixOffset = 0; + + switch (img.pixelFormat()) { + case QVideoFrame::Format_ARGB32: + case QVideoFrame::Format_ARGB32_Premultiplied: + case QVideoFrame::Format_RGB32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::BGRX; +#else + fmt = ImageFormat::XRGB; +#endif + break; + + case QVideoFrame::Format_RGB24: fmt = ImageFormat::RGB; break; + + case QVideoFrame::Format_BGRA32: + case QVideoFrame::Format_BGRA32_Premultiplied: + case QVideoFrame::Format_BGR32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::RGBX; +#else + fmt = ImageFormat::XBGR; +#endif + break; + + case QVideoFrame::Format_BGR24: fmt = ImageFormat::BGR; break; + + case QVideoFrame::Format_AYUV444: + case QVideoFrame::Format_AYUV444_Premultiplied: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 3; +#else + fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 2; +#endif + break; + + case QVideoFrame::Format_YUV444: fmt = ImageFormat::Lum, pixStride = 3; break; + case QVideoFrame::Format_YUV420P: + case QVideoFrame::Format_NV12: + case QVideoFrame::Format_NV21: + case QVideoFrame::Format_IMC1: + case QVideoFrame::Format_IMC2: + case QVideoFrame::Format_IMC3: + case QVideoFrame::Format_IMC4: + case QVideoFrame::Format_YV12: fmt = ImageFormat::Lum; break; + case QVideoFrame::Format_UYVY: fmt = ImageFormat::Lum, pixStride = 2, pixOffset = 1; break; + case QVideoFrame::Format_YUYV: fmt = ImageFormat::Lum, pixStride = 2; break; + + case QVideoFrame::Format_Y8: fmt = ImageFormat::Lum; break; + case QVideoFrame::Format_Y16: fmt = ImageFormat::Lum, pixStride = 2, pixOffset = 1; break; + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + case QVideoFrame::Format_ABGR32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::RGBX; +#else + fmt = ImageFormat::XBGR; +#endif + break; +#endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + case QVideoFrame::Format_YUV422P: fmt = ImageFormat::Lum; break; +#endif + default: break; + } + + Result res; + if (fmt != ImageFormat::None) { + res = Result( + ZXing::ReadBarcode({img.bits() + pixOffset, img.width(), img.height(), fmt, img.bytesPerLine(), pixStride}, + hints)); + } else { + if (QVideoFrame::imageFormatFromPixelFormat(img.pixelFormat()) != QImage::Format_Invalid) + res = ReadBarcode(img.image(), hints); + } + + img.unmap(); + + return res; +} + +#define ZQ_PROPERTY(Type, name, setter) \ +public: \ + Q_PROPERTY(Type name READ name WRITE setter NOTIFY name##Changed) \ + Type name() const noexcept { return DecodeHints::name(); } \ + Q_SLOT void setter(const Type& newVal) \ + { \ + if (name() != newVal) { \ + DecodeHints::setter(newVal); \ + emit name##Changed(); \ + } \ + } \ + Q_SIGNAL void name##Changed(); + +class VideoFilter : public QAbstractVideoFilter, private DecodeHints +{ + Q_OBJECT + +public: + VideoFilter(QObject* parent = nullptr) : QAbstractVideoFilter(parent) {} + + QVideoFilterRunnable* createFilterRunnable() override; + + // TODO: find out how to properly expose QFlags to QML + // simply using ZQ_PROPERTY(BarcodeFormats, formats, setFormats) + // results in the runtime error "can't assign int to formats" + Q_PROPERTY(int formats READ formats WRITE setFormats NOTIFY formatsChanged) + int formats() const noexcept + { + auto fmts = DecodeHints::formats(); + return *reinterpret_cast(&fmts); + } + Q_SLOT void setFormats(int newVal) + { + if (formats() != newVal) { + DecodeHints::setFormats(static_cast(newVal)); + emit formatsChanged(); + qDebug() << DecodeHints::formats(); + } + } + Q_SIGNAL void formatsChanged(); + + ZQ_PROPERTY(bool, tryRotate, setTryRotate) + ZQ_PROPERTY(bool, tryHarder, setTryHarder) + +public slots: + Result process(const QVideoFrame& image) + { + QElapsedTimer t; + t.start(); + + auto res = ReadBarcode(image, *this); + + res.runTime = t.elapsed(); + + emit newResult(res); + if (res.isValid()) + emit foundBarcode(res); + return res; + } + +signals: + void newResult(Result result); + void foundBarcode(Result result); +}; + +#undef ZX_PROPERTY + +class VideoFilterRunnable : public QVideoFilterRunnable +{ + VideoFilter* _filter = nullptr; + +public: + explicit VideoFilterRunnable(VideoFilter* filter) : _filter(filter) {} + + QVideoFrame run(QVideoFrame* input, const QVideoSurfaceFormat& /*surfaceFormat*/, RunFlags /*flags*/) override + { + _filter->process(*input); + return *input; + } +}; + +inline QVideoFilterRunnable* VideoFilter::createFilterRunnable() +{ + return new VideoFilterRunnable(this); +} + +#endif // QT_MULTIMEDIA_LIB + +} // namespace ZXingQt + + +Q_DECLARE_METATYPE(ZXingQt::Position) +Q_DECLARE_METATYPE(ZXingQt::Result) + +#ifdef QT_QML_LIB + +#include + +namespace ZXingQt { + +inline void registerQmlAndMetaTypes() +{ + qRegisterMetaType("BarcodeFormat"); + qRegisterMetaType("DecodeStatus"); + + // supposedly the Q_DECLARE_METATYPE should be used with the overload without a custom name + // but then the qml side complains about "unregistered type" + qRegisterMetaType("Position"); + qRegisterMetaType("Result"); + + qmlRegisterUncreatableMetaObject( + ZXingQt::staticMetaObject, "ZXing", 1, 0, "ZXing", "Access to enums & flags only"); + qmlRegisterType("ZXing", 1, 0, "VideoFilter"); +} + +} // namespace ZXingQt + +#endif // QT_QML_LIB \ No newline at end of file diff --git a/3rdparty/qrcodegen.cpp b/3rdparty/qrcodegen.cpp new file mode 100644 index 0000000..0957b79 --- /dev/null +++ b/3rdparty/qrcodegen.cpp @@ -0,0 +1,830 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "qrcodegen.hpp" + +using std::int8_t; +using std::uint8_t; +using std::size_t; +using std::vector; + + +namespace qrcodegen { + +/*---- Class QrSegment ----*/ + +QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) : + modeBits(mode) { + numBitsCharCount[0] = cc0; + numBitsCharCount[1] = cc1; + numBitsCharCount[2] = cc2; +} + + +int QrSegment::Mode::getModeBits() const { + return modeBits; +} + + +int QrSegment::Mode::numCharCountBits(int ver) const { + return numBitsCharCount[(ver + 7) / 17]; +} + + +const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14); +const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); +const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16); +const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12); +const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0); + + +QrSegment QrSegment::makeBytes(const vector &data) { + if (data.size() > static_cast(INT_MAX)) + throw std::length_error("Data too long"); + BitBuffer bb; + for (uint8_t b : data) + bb.appendBits(b, 8); + return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); +} + + +QrSegment QrSegment::makeNumeric(const char *digits) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *digits != '\0'; digits++, charCount++) { + char c = *digits; + if (c < '0' || c > '9') + throw std::domain_error("String contains non-numeric characters"); + accumData = accumData * 10 + (c - '0'); + accumCount++; + if (accumCount == 3) { + bb.appendBits(static_cast(accumData), 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + bb.appendBits(static_cast(accumData), accumCount * 3 + 1); + return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); +} + + +QrSegment QrSegment::makeAlphanumeric(const char *text) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *text != '\0'; text++, charCount++) { + const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); + if (temp == nullptr) + throw std::domain_error("String contains unencodable characters in alphanumeric mode"); + accumData = accumData * 45 + static_cast(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + bb.appendBits(static_cast(accumData), 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + bb.appendBits(static_cast(accumData), 6); + return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); +} + + +vector QrSegment::makeSegments(const char *text) { + // Select the most efficient segment encoding automatically + vector result; + if (*text == '\0'); // Leave result empty + else if (isNumeric(text)) + result.push_back(makeNumeric(text)); + else if (isAlphanumeric(text)) + result.push_back(makeAlphanumeric(text)); + else { + vector bytes; + for (; *text != '\0'; text++) + bytes.push_back(static_cast(*text)); + result.push_back(makeBytes(bytes)); + } + return result; +} + + +QrSegment QrSegment::makeEci(long assignVal) { + BitBuffer bb; + if (assignVal < 0) + throw std::domain_error("ECI assignment value out of range"); + else if (assignVal < (1 << 7)) + bb.appendBits(static_cast(assignVal), 8); + else if (assignVal < (1 << 14)) { + bb.appendBits(2, 2); + bb.appendBits(static_cast(assignVal), 14); + } else if (assignVal < 1000000L) { + bb.appendBits(6, 3); + bb.appendBits(static_cast(assignVal), 21); + } else + throw std::domain_error("ECI assignment value out of range"); + return QrSegment(Mode::ECI, 0, std::move(bb)); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, const std::vector &dt) : + mode(&md), + numChars(numCh), + data(dt) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, std::vector &&dt) : + mode(&md), + numChars(numCh), + data(std::move(dt)) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +int QrSegment::getTotalBits(const vector &segs, int version) { + int result = 0; + for (const QrSegment &seg : segs) { + int ccbits = seg.mode->numCharCountBits(version); + if (seg.numChars >= (1L << ccbits)) + return -1; // The segment's length doesn't fit the field's bit width + if (4 + ccbits > INT_MAX - result) + return -1; // The sum will overflow an int type + result += 4 + ccbits; + if (seg.data.size() > static_cast(INT_MAX - result)) + return -1; // The sum will overflow an int type + result += static_cast(seg.data.size()); + } + return result; +} + + +bool QrSegment::isNumeric(const char *text) { + for (; *text != '\0'; text++) { + char c = *text; + if (c < '0' || c > '9') + return false; + } + return true; +} + + +bool QrSegment::isAlphanumeric(const char *text) { + for (; *text != '\0'; text++) { + if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) + return false; + } + return true; +} + + +const QrSegment::Mode &QrSegment::getMode() const { + return *mode; +} + + +int QrSegment::getNumChars() const { + return numChars; +} + + +const std::vector &QrSegment::getData() const { + return data; +} + + +const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + + + +/*---- Class QrCode ----*/ + +int QrCode::getFormatBits(Ecc ecl) { + switch (ecl) { + case Ecc::LOW : return 1; + case Ecc::MEDIUM : return 0; + case Ecc::QUARTILE: return 3; + case Ecc::HIGH : return 2; + default: throw std::logic_error("Unreachable"); + } +} + + +QrCode QrCode::encodeText(const char *text, Ecc ecl) { + vector segs = QrSegment::makeSegments(text); + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) { + vector segs{QrSegment::makeBytes(data)}; + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, + int minVersion, int maxVersion, int mask, bool boostEcl) { + if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) + throw std::invalid_argument("Invalid value"); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment::getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + std::ostringstream sb; + if (dataUsedBits == -1) + sb << "Segment too long"; + else { + sb << "Data length = " << dataUsedBits << " bits, "; + sb << "Max capacity = " << dataCapacityBits << " bits"; + } + throw data_too_long(sb.str()); + } + } + assert(dataUsedBits != -1); + + // Increase the error correction level while the data still fits in the current version number + for (Ecc newEcl : {Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + } + + // Concatenate all segments to create the data bit string + BitBuffer bb; + for (const QrSegment &seg : segs) { + bb.appendBits(static_cast(seg.getMode().getModeBits()), 4); + bb.appendBits(static_cast(seg.getNumChars()), seg.getMode().numCharCountBits(version)); + bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); + } + assert(bb.size() == static_cast(dataUsedBits)); + + // Add terminator and pad up to a byte if applicable + size_t dataCapacityBits = static_cast(getNumDataCodewords(version, ecl)) * 8; + assert(bb.size() <= dataCapacityBits); + bb.appendBits(0, std::min(4, static_cast(dataCapacityBits - bb.size()))); + bb.appendBits(0, (8 - static_cast(bb.size() % 8)) % 8); + assert(bb.size() % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + bb.appendBits(padByte, 8); + + // Pack bits into bytes in big endian + vector dataCodewords(bb.size() / 8); + for (size_t i = 0; i < bb.size(); i++) + dataCodewords.at(i >> 3) |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); + + // Create the QR Code object + return QrCode(version, ecl, dataCodewords, mask); +} + + +QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int msk) : + // Initialize fields and check arguments + version(ver), + errorCorrectionLevel(ecl) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version value out of range"); + if (msk < -1 || msk > 7) + throw std::domain_error("Mask value out of range"); + size = ver * 4 + 17; + size_t sz = static_cast(size); + modules = vector >(sz, vector(sz)); // Initially all light + isFunction = vector >(sz, vector(sz)); + + // Compute ECC, draw modules + drawFunctionPatterns(); + const vector allCodewords = addEccAndInterleave(dataCodewords); + drawCodewords(allCodewords); + + // Do masking + if (msk == -1) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + applyMask(i); + drawFormatBits(i); + long penalty = getPenaltyScore(); + if (penalty < minPenalty) { + msk = i; + minPenalty = penalty; + } + applyMask(i); // Undoes the mask due to XOR + } + } + assert(0 <= msk && msk <= 7); + mask = msk; + applyMask(msk); // Apply the final choice of mask + drawFormatBits(msk); // Overwrite old format bits + + isFunction.clear(); + isFunction.shrink_to_fit(); +} + + +int QrCode::getVersion() const { + return version; +} + + +int QrCode::getSize() const { + return size; +} + + +QrCode::Ecc QrCode::getErrorCorrectionLevel() const { + return errorCorrectionLevel; +} + + +int QrCode::getMask() const { + return mask; +} + + +bool QrCode::getModule(int x, int y) const { + return 0 <= x && x < size && 0 <= y && y < size && module(x, y); +} + + +void QrCode::drawFunctionPatterns() { + // Draw horizontal and vertical timing patterns + for (int i = 0; i < size; i++) { + setFunctionModule(6, i, i % 2 == 0); + setFunctionModule(i, 6, i % 2 == 0); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + drawFinderPattern(3, 3); + drawFinderPattern(size - 4, 3); + drawFinderPattern(3, size - 4); + + // Draw numerous alignment patterns + const vector alignPatPos = getAlignmentPatternPositions(); + size_t numAlign = alignPatPos.size(); + for (size_t i = 0; i < numAlign; i++) { + for (size_t j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) + drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); + } + } + + // Draw configuration data + drawFormatBits(0); // Dummy mask value; overwritten later in the constructor + drawVersion(); +} + + +void QrCode::drawFormatBits(int msk) { + // Calculate error correction code and pack bits + int data = getFormatBits(errorCorrectionLevel) << 3 | msk; // errCorrLvl is uint2, msk is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setFunctionModule(8, i, getBit(bits, i)); + setFunctionModule(8, 7, getBit(bits, 6)); + setFunctionModule(8, 8, getBit(bits, 7)); + setFunctionModule(7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setFunctionModule(14 - i, 8, getBit(bits, i)); + + // Draw second copy + for (int i = 0; i < 8; i++) + setFunctionModule(size - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setFunctionModule(8, size - 15 + i, getBit(bits, i)); + setFunctionModule(8, size - 8, true); // Always dark +} + + +void QrCode::drawVersion() { + if (version < 7) + return; + + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = static_cast(version) << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 18; i++) { + bool bit = getBit(bits, i); + int a = size - 11 + i % 3; + int b = i / 3; + setFunctionModule(a, b, bit); + setFunctionModule(b, a, bit); + } +} + + +void QrCode::drawFinderPattern(int x, int y) { + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm + int xx = x + dx, yy = y + dy; + if (0 <= xx && xx < size && 0 <= yy && yy < size) + setFunctionModule(xx, yy, dist != 2 && dist != 4); + } + } +} + + +void QrCode::drawAlignmentPattern(int x, int y) { + for (int dy = -2; dy <= 2; dy++) { + for (int dx = -2; dx <= 2; dx++) + setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1); + } +} + + +void QrCode::setFunctionModule(int x, int y, bool isDark) { + size_t ux = static_cast(x); + size_t uy = static_cast(y); + modules .at(uy).at(ux) = isDark; + isFunction.at(uy).at(ux) = true; +} + + +bool QrCode::module(int x, int y) const { + return modules.at(static_cast(y)).at(static_cast(x)); +} + + +vector QrCode::addEccAndInterleave(const vector &data) const { + if (data.size() != static_cast(getNumDataCodewords(version, errorCorrectionLevel))) + throw std::invalid_argument("Invalid argument"); + + // Calculate parameter numbers + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast(errorCorrectionLevel)][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockLen = rawCodewords / numBlocks; + + // Split data into blocks and append ECC to each block + vector > blocks; + const vector rsDiv = reedSolomonComputeDivisor(blockEccLen); + for (int i = 0, k = 0; i < numBlocks; i++) { + vector dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); + k += static_cast(dat.size()); + const vector ecc = reedSolomonComputeRemainder(dat, rsDiv); + if (i < numShortBlocks) + dat.push_back(0); + dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); + blocks.push_back(std::move(dat)); + } + + // Interleave (not concatenate) the bytes from every block into a single sequence + vector result; + for (size_t i = 0; i < blocks.at(0).size(); i++) { + for (size_t j = 0; j < blocks.size(); j++) { + // Skip the padding byte in short blocks + if (i != static_cast(shortBlockLen - blockEccLen) || j >= static_cast(numShortBlocks)) + result.push_back(blocks.at(j).at(i)); + } + } + assert(result.size() == static_cast(rawCodewords)); + return result; +} + + +void QrCode::drawCodewords(const vector &data) { + if (data.size() != static_cast(getNumRawDataModules(version) / 8)) + throw std::invalid_argument("Invalid argument"); + + size_t i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < size; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + size_t x = static_cast(right - j); // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + size_t y = static_cast(upward ? size - 1 - vert : vert); // Actual y coordinate + if (!isFunction.at(y).at(x) && i < data.size() * 8) { + modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + } + assert(i == data.size() * 8); +} + + +void QrCode::applyMask(int msk) { + if (msk < 0 || msk > 7) + throw std::domain_error("Mask value out of range"); + size_t sz = static_cast(size); + for (size_t y = 0; y < sz; y++) { + for (size_t x = 0; x < sz; x++) { + bool invert; + switch (msk) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + default: throw std::logic_error("Unreachable"); + } + modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); + } + } +} + + +long QrCode::getPenaltyScore() const { + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < size; y++) { + bool runColor = false; + int runX = 0; + std::array runHistory = {}; + for (int x = 0; x < size; x++) { + if (module(x, y) == runColor) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + finderPenaltyAddHistory(runX, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < size; x++) { + bool runColor = false; + int runY = 0; + std::array runHistory = {}; + for (int y = 0; y < size; y++) { + if (module(x, y) == runColor) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + finderPenaltyAddHistory(runY, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < size - 1; y++) { + for (int x = 0; x < size - 1; x++) { + bool color = module(x, y); + if ( color == module(x + 1, y) && + color == module(x, y + 1) && + color == module(x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (const vector &row : modules) { + for (bool color : row) { + if (color) + dark++; + } + } + int total = size * size; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + int k = static_cast((std::abs(dark * 20L - total * 10L) + total - 1) / total) - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + return result; +} + + +vector QrCode::getAlignmentPatternPositions() const { + if (version == 1) + return vector(); + else { + int numAlign = version / 7 + 2; + int step = (version == 32) ? 26 : + (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + vector result; + for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) + result.insert(result.begin(), pos); + result.insert(result.begin(), 6); + return result; + } +} + + +int QrCode::getNumRawDataModules(int ver) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version number out of range"); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + + +int QrCode::getNumDataCodewords(int ver, Ecc ecl) { + return getNumRawDataModules(ver) / 8 + - ECC_CODEWORDS_PER_BLOCK [static_cast(ecl)][ver] + * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; +} + + +vector QrCode::reedSolomonComputeDivisor(int degree) { + if (degree < 1 || degree > 255) + throw std::domain_error("Degree out of range"); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. + vector result(static_cast(degree)); + result.at(result.size() - 1) = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // and drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (size_t j = 0; j < result.size(); j++) { + result.at(j) = reedSolomonMultiply(result.at(j), root); + if (j + 1 < result.size()) + result.at(j) ^= result.at(j + 1); + } + root = reedSolomonMultiply(root, 0x02); + } + return result; +} + + +vector QrCode::reedSolomonComputeRemainder(const vector &data, const vector &divisor) { + vector result(divisor.size()); + for (uint8_t b : data) { // Polynomial division + uint8_t factor = b ^ result.at(0); + result.erase(result.begin()); + result.push_back(0); + for (size_t i = 0; i < result.size(); i++) + result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor); + } + return result; +} + + +uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) { + // Russian peasant multiplication + int z = 0; + for (int i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + assert(z >> 8 == 0); + return static_cast(z); +} + + +int QrCode::finderPenaltyCountPatterns(const std::array &runHistory) const { + int n = runHistory.at(1); + assert(n <= size * 3); + bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 && runHistory.at(4) == n && runHistory.at(5) == n; + return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0) + + (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0); +} + + +int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const { + if (currentRunColor) { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory); + currentRunLength = 0; + } + currentRunLength += size; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory); + return finderPenaltyCountPatterns(runHistory); +} + + +void QrCode::finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const { + if (runHistory.at(0) == 0) + currentRunLength += size; // Add light border to initial run + std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, runHistory.end()); + runHistory.at(0) = currentRunLength; +} + + +bool QrCode::getBit(long x, int i) { + return ((x >> i) & 1) != 0; +} + + +/*---- Tables of constants ----*/ + +const int QrCode::PENALTY_N1 = 3; +const int QrCode::PENALTY_N2 = 3; +const int QrCode::PENALTY_N3 = 40; +const int QrCode::PENALTY_N4 = 10; + + +const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + + +data_too_long::data_too_long(const std::string &msg) : + std::length_error(msg) {} + + + +/*---- Class BitBuffer ----*/ + +BitBuffer::BitBuffer() + : std::vector() {} + + +void BitBuffer::appendBits(std::uint32_t val, int len) { + if (len < 0 || len > 31 || val >> len != 0) + throw std::domain_error("Value out of range"); + for (int i = len - 1; i >= 0; i--) // Append bit by bit + this->push_back(((val >> i) & 1) != 0); +} + +} diff --git a/3rdparty/qrcodegen.hpp b/3rdparty/qrcodegen.hpp new file mode 100644 index 0000000..9448982 --- /dev/null +++ b/3rdparty/qrcodegen.hpp @@ -0,0 +1,549 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include +#include +#include +#include +#include + + +namespace qrcodegen { + +/* + * A segment of character/binary/control data in a QR Code symbol. + * Instances of this class are immutable. + * The mid-level way to create a segment is to take the payload data + * and call a static factory function such as QrSegment::makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and call the QrSegment() constructor with appropriate values. + * This segment class imposes no length restrictions, but QR Codes have restrictions. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + */ +class QrSegment final { + + /*---- Public helper enumeration ----*/ + + /* + * Describes how a segment's data bits are interpreted. Immutable. + */ + public: class Mode final { + + /*-- Constants --*/ + + public: static const Mode NUMERIC; + public: static const Mode ALPHANUMERIC; + public: static const Mode BYTE; + public: static const Mode KANJI; + public: static const Mode ECI; + + + /*-- Fields --*/ + + // The mode indicator bits, which is a uint4 value (range 0 to 15). + private: int modeBits; + + // Number of character count bits for three different version ranges. + private: int numBitsCharCount[3]; + + + /*-- Constructor --*/ + + private: Mode(int mode, int cc0, int cc1, int cc2); + + + /*-- Methods --*/ + + /* + * (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15). + */ + public: int getModeBits() const; + + /* + * (Package-private) Returns the bit width of the character count field for a segment in + * this mode in a QR Code at the given version number. The result is in the range [0, 16]. + */ + public: int numCharCountBits(int ver) const; + + }; + + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte vectors are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ + public: static QrSegment makeBytes(const std::vector &data); + + + /* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ + public: static QrSegment makeNumeric(const char *digits); + + + /* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static QrSegment makeAlphanumeric(const char *text); + + + /* + * Returns a list of zero or more segments to represent the given text string. The result + * may use various segment modes and switch modes to optimize the length of the bit stream. + */ + public: static std::vector makeSegments(const char *text); + + + /* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ + public: static QrSegment makeEci(long assignVal); + + + /*---- Public static helper functions ----*/ + + /* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ + public: static bool isNumeric(const char *text); + + + /* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static bool isAlphanumeric(const char *text); + + + + /*---- Instance fields ----*/ + + /* The mode indicator of this segment. Accessed through getMode(). */ + private: const Mode *mode; + + /* The length of this segment's unencoded data. Measured in characters for + * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + * Always zero or positive. Not the same as the data's bit length. + * Accessed through getNumChars(). */ + private: int numChars; + + /* The data bits of this segment. Accessed through getData(). */ + private: std::vector data; + + + /*---- Constructors (low level) ----*/ + + /* + * Creates a new QR Code segment with the given attributes and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is copied and stored. + */ + public: QrSegment(const Mode &md, int numCh, const std::vector &dt); + + + /* + * Creates a new QR Code segment with the given parameters and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is moved and stored. + */ + public: QrSegment(const Mode &md, int numCh, std::vector &&dt); + + + /*---- Methods ----*/ + + /* + * Returns the mode field of this segment. + */ + public: const Mode &getMode() const; + + + /* + * Returns the character count field of this segment. + */ + public: int getNumChars() const; + + + /* + * Returns the data bits of this segment. + */ + public: const std::vector &getData() const; + + + // (Package-private) Calculates the number of bits needed to encode the given segments at + // the given version. Returns a non-negative number if successful. Otherwise returns -1 if a + // segment has too many characters to fit its length field, or the total bits exceeds INT_MAX. + public: static int getTotalBits(const std::vector &segs, int version); + + + /*---- Private constant ----*/ + + /* The set of all legal characters in alphanumeric mode, where + * each character value maps to the index in the string. */ + private: static const char *ALPHANUMERIC_CHARSET; + +}; + + + +/* + * A QR Code symbol, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * Instances of this class represent an immutable square grid of dark and light cells. + * The class provides static factory functions to create a QR Code from text or binary data. + * The class covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary(). + * - Mid level: Custom-make the list of segments and call QrCode::encodeSegments(). + * - Low level: Custom-make the array of data codeword bytes (including + * segment headers and final padding, excluding error correction codewords), + * supply the appropriate version number, and call the QrCode() constructor. + * (Note that all ways require supplying the desired error correction level.) + */ +class QrCode final { + + /*---- Public helper enumeration ----*/ + + /* + * The error correction level in a QR Code symbol. + */ + public: enum class Ecc { + LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords + MEDIUM , // The QR Code can tolerate about 15% erroneous codewords + QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + HIGH , // The QR Code can tolerate about 30% erroneous codewords + }; + + + // Returns a value in the range 0 to 3 (unsigned 2-bit integer). + private: static int getFormatBits(Ecc ecl); + + + + /*---- Static factory functions (high level) ----*/ + + /* + * Returns a QR Code representing the given Unicode text string at the given error correction level. + * As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer + * UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible + * QR Code version is automatically chosen for the output. The ECC level of the result may be higher than + * the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeText(const char *text, Ecc ecl); + + + /* + * Returns a QR Code representing the given binary data at the given error correction level. + * This function always encodes using the binary segment mode, not any text mode. The maximum number of + * bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. + * The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeBinary(const std::vector &data, Ecc ecl); + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a QR Code representing the given segments with the given encoding parameters. + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask number is either between 0 to 7 (inclusive) to force that + * mask, or -1 to automatically choose an appropriate mask (which may be slow). + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a mid-level API; the high-level API is encodeText() and encodeBinary(). + */ + public: static QrCode encodeSegments(const std::vector &segs, Ecc ecl, + int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters + + + + /*---- Instance fields ----*/ + + // Immutable scalar parameters: + + /* The version number of this QR Code, which is between 1 and 40 (inclusive). + * This determines the size of this barcode. */ + private: int version; + + /* The width and height of this QR Code, measured in modules, between + * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ + private: int size; + + /* The error correction level used in this QR Code. */ + private: Ecc errorCorrectionLevel; + + /* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). + * Even if a QR Code is created with automatic masking requested (mask = -1), + * the resulting object still has a mask value between 0 and 7. */ + private: int mask; + + // Private grids of modules/pixels, with dimensions of size*size: + + // The modules of this QR Code (false = light, true = dark). + // Immutable after constructor finishes. Accessed through getModule(). + private: std::vector > modules; + + // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. + private: std::vector > isFunction; + + + + /*---- Constructor (low level) ----*/ + + /* + * Creates a new QR Code with the given version number, + * error correction level, data codeword bytes, and mask number. + * This is a low-level API that most users should not use directly. + * A mid-level API is the encodeSegments() function. + */ + public: QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, int msk); + + + + /*---- Public instance methods ----*/ + + /* + * Returns this QR Code's version, in the range [1, 40]. + */ + public: int getVersion() const; + + + /* + * Returns this QR Code's size, in the range [21, 177]. + */ + public: int getSize() const; + + + /* + * Returns this QR Code's error correction level. + */ + public: Ecc getErrorCorrectionLevel() const; + + + /* + * Returns this QR Code's mask, in the range [0, 7]. + */ + public: int getMask() const; + + + /* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for light or true for dark. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (light) is returned. + */ + public: bool getModule(int x, int y) const; + + + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Reads this object's version field, and draws and marks all function modules. + private: void drawFunctionPatterns(); + + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. + private: void drawFormatBits(int msk); + + + // Draws two copies of the version bits (with its own error correction code), + // based on this object's version field, iff 7 <= version <= 40. + private: void drawVersion(); + + + // Draws a 9*9 finder pattern including the border separator, + // with the center module at (x, y). Modules can be out of bounds. + private: void drawFinderPattern(int x, int y); + + + // Draws a 5*5 alignment pattern, with the center module + // at (x, y). All modules must be in bounds. + private: void drawAlignmentPattern(int x, int y); + + + // Sets the color of a module and marks it as a function module. + // Only used by the constructor. Coordinates must be in bounds. + private: void setFunctionModule(int x, int y, bool isDark); + + + // Returns the color of the module at the given coordinates, which must be in range. + private: bool module(int x, int y) const; + + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate error correction + // codewords appended to it, based on this object's version and error correction level. + private: std::vector addEccAndInterleave(const std::vector &data) const; + + + // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire + // data area of this QR Code. Function modules need to be marked off before this is called. + private: void drawCodewords(const std::vector &data); + + + // XORs the codeword modules in this QR Code with the given mask pattern. + // The function modules must be marked and the codeword bits must be drawn + // before masking. Due to the arithmetic of XOR, calling applyMask() with + // the same mask value a second time will undo the mask. A final well-formed + // QR Code needs exactly one (not zero, two, etc.) mask applied. + private: void applyMask(int msk); + + + // Calculates and returns the penalty score based on state of this QR Code's current modules. + // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. + private: long getPenaltyScore() const; + + + + /*---- Private helper functions ----*/ + + // Returns an ascending list of positions of alignment patterns for this version number. + // Each position is in the range [0,177), and are used on both the x and y axes. + // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. + private: std::vector getAlignmentPatternPositions() const; + + + // Returns the number of data bits that can be stored in a QR Code of the given version number, after + // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. + // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. + private: static int getNumRawDataModules(int ver); + + + // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any + // QR Code of the given version number and error correction level, with remainder bits discarded. + // This stateless pure function could be implemented as a (40*4)-cell lookup table. + private: static int getNumDataCodewords(int ver, Ecc ecl); + + + // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be + // implemented as a lookup table over all possible parameter values, instead of as an algorithm. + private: static std::vector reedSolomonComputeDivisor(int degree); + + + // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. + private: static std::vector reedSolomonComputeRemainder(const std::vector &data, const std::vector &divisor); + + + // Returns the product of the two given field elements modulo GF(2^8/0x11D). + // All inputs are valid. This could be implemented as a 256*256 lookup table. + private: static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y); + + + // Can only be called immediately after a light run is added, and + // returns either 0, 1, or 2. A helper function for getPenaltyScore(). + private: int finderPenaltyCountPatterns(const std::array &runHistory) const; + + + // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). + private: int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const; + + + // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). + private: void finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const; + + + // Returns true iff the i'th bit of x is set to 1. + private: static bool getBit(long x, int i); + + + /*---- Constants and tables ----*/ + + // The minimum version number supported in the QR Code Model 2 standard. + public: static constexpr int MIN_VERSION = 1; + + // The maximum version number supported in the QR Code Model 2 standard. + public: static constexpr int MAX_VERSION = 40; + + + // For use in getPenaltyScore(), when evaluating which mask is best. + private: static const int PENALTY_N1; + private: static const int PENALTY_N2; + private: static const int PENALTY_N3; + private: static const int PENALTY_N4; + + + private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; + private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; + +}; + + + +/*---- Public exception class ----*/ + +/* + * Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include: + * - Decrease the error correction level if it was greater than Ecc::LOW. + * - If the encodeSegments() function was called with a maxVersion argument, then increase + * it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other + * factory functions because they search all versions up to QrCode::MAX_VERSION.) + * - Split the text data into better or optimal segments in order to reduce the number of bits required. + * - Change the text or binary data to be shorter. + * - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). + * - Propagate the error upward to the caller/user. + */ +class data_too_long : public std::length_error { + + public: explicit data_too_long(const std::string &msg); + +}; + + + +/* + * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. + */ +class BitBuffer final : public std::vector { + + /*---- Constructor ----*/ + + // Creates an empty bit buffer (length 0). + public: BitBuffer(); + + + + /*---- Method ----*/ + + // Appends the given number of low-order bits of the given value + // to this buffer. Requires 0 <= len <= 31 and val < 2^len. + public: void appendBits(std::uint32_t val, int len); + +}; + +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..89e3cb0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,245 @@ +cmake_minimum_required(VERSION 3.5) + +project(nekoray VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# WINDOWS PDB FILE +if (WIN32) + if (MSVC) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") + endif () +endif () + +# Find Qt + +if (NOT QT_VERSION_MAJOR) + set(QT_VERSION_MAJOR 5) +endif () +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network Svg LinguistTools) + +if (NKR_CROSS) + set_property(TARGET Qt5::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) + set_property(TARGET Qt5::uic PROPERTY IMPORTED_LOCATION /usr/bin/uic) + set_property(TARGET Qt5::rcc PROPERTY IMPORTED_LOCATION /usr/bin/rcc) + set_property(TARGET Qt5::lrelease PROPERTY IMPORTED_LOCATION /usr/bin/lrelease) + set_property(TARGET Qt5::lupdate PROPERTY IMPORTED_LOCATION /usr/bin/lupdate) +endif () + +# Windows +include("cmake/fuck_windows/fuck.cmake") + +# My dependencies +include("cmake/print.cmake") +set(MY_DEPS_DIR "${CMAKE_SOURCE_DIR}/libs/deps/built") +list(APPEND CMAKE_PREFIX_PATH ${MY_DEPS_DIR}) + +# NKR +include("cmake/nkr.cmake") + +find_package(Threads) + +if (NKR_NO_EXTERNAL) + add_compile_definitions(NKR_NO_EXTERNAL) +else () + if (NKR_NO_GRPC) + add_compile_definitions(NKR_NO_GRPC) + else () + # My proto + include("cmake/myproto.cmake") + list(APPEND NKR_EXTERNAL_TARGETS myproto) + endif () + + # yaml-cpp (static) + find_package(yaml-cpp CONFIG REQUIRED) # only Release is built + list(APPEND NKR_EXTERNAL_TARGETS yaml-cpp) + + # zxing-cpp + find_package(ZXing CONFIG REQUIRED) + list(APPEND NKR_EXTERNAL_TARGETS ZXing::ZXing) + + # QHotkey (static submodule) + set(QHOTKEY_INSTALL OFF) + add_subdirectory(3rdparty/QHotkey) + list(APPEND NKR_EXTERNAL_TARGETS qhotkey) +endif () + +# debug print +if (DBG_CMAKE) + print_all_variables() + print_target_properties(myproto) + print_target_properties(yaml-cpp) + print_target_properties(ZXing::ZXing) + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time") + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time") +endif () + +# Sources +set(PROJECT_SOURCES + ${PLATFORM_FUCKING_SOURCES} + + main/main.cpp + main/NekoRay.cpp + main/NekoRay_Utils.cpp + + 3rdparty/qrcodegen.cpp + 3rdparty/QtExtKeySequenceEdit.cpp + + qv2ray/ui/LogHighlighter.cpp + qv2ray/ui/QvAutoCompleteTextEdit.cpp + qv2ray/utils/HTTPRequestHelper.cpp + qv2ray/components/proxy/QvProxyConfigurator.cpp + qv2ray/ui/widgets/common/QJsonModel.cpp + + qv2ray/ui/widgets/editors/w_JsonEditor.cpp + qv2ray/ui/widgets/editors/w_JsonEditor.hpp + qv2ray/ui/widgets/editors/w_JsonEditor.ui + + rpc/gRPC.cpp + + db/Database.cpp + db/TrafficLooper.cpp + db/ProfileFilter.cpp + + fmt/AbstractBean.cpp + fmt/Bean2CoreObj.cpp + fmt/Bean2External.cpp + fmt/Bean2Link.cpp + fmt/InsecureHint.cpp + fmt/Link2Bean.cpp + db/ConfigBuilder.cpp + fmt/ChainBean.hpp # translate + + sub/GroupUpdater.cpp + + sys/ExternalProcess.cpp + sys/AutoRun.cpp + + ui/ThemeManager.cpp + + ui/mainwindow_grpc.cpp + ui/mainwindow.cpp + ui/mainwindow.h + ui/mainwindow.ui + + ui/edit/dialog_edit_profile.h + ui/edit/dialog_edit_profile.cpp + ui/edit/dialog_edit_profile.ui + ui/edit/dialog_edit_group.h + ui/edit/dialog_edit_group.cpp + ui/edit/dialog_edit_group.ui + + ui/edit/edit_chain.h + ui/edit/edit_chain.cpp + ui/edit/edit_chain.ui + ui/edit/edit_socks_http.h + ui/edit/edit_socks_http.cpp + ui/edit/edit_socks_http.ui + ui/edit/edit_shadowsocks.h + ui/edit/edit_shadowsocks.cpp + ui/edit/edit_shadowsocks.ui + ui/edit/edit_vmess.h + ui/edit/edit_vmess.cpp + ui/edit/edit_vmess.ui + ui/edit/edit_trojan_vless.h + ui/edit/edit_trojan_vless.cpp + ui/edit/edit_trojan_vless.ui + + ui/edit/edit_naive.h + ui/edit/edit_naive.cpp + ui/edit/edit_naive.ui + + ui/edit/edit_custom.h + ui/edit/edit_custom.cpp + ui/edit/edit_custom.ui + + ui/dialog_basic_settings.cpp + ui/dialog_basic_settings.h + ui/dialog_basic_settings.ui + + ui/dialog_manage_groups.cpp + ui/dialog_manage_groups.h + ui/dialog_manage_groups.ui + + ui/dialog_manage_routes.cpp + ui/dialog_manage_routes.h + ui/dialog_manage_routes.ui + + ui/dialog_hotkey.cpp + ui/dialog_hotkey.h + ui/dialog_hotkey.ui + + ui/widget/ProxyItem.cpp + ui/widget/ProxyItem.h + ui/widget/ProxyItem.ui + ui/widget/GroupItem.cpp + ui/widget/GroupItem.h + ui/widget/GroupItem.ui + + res/neko.qrc + res/theme/feiyangqingyun/qss.qrc + ${QV2RAY_RC} + ) + +# Translations +set(TS_FILES + translations/zh_CN.ts + ) +qt_create_translation(QM_FILES ${PROJECT_SOURCES} ${TS_FILES} OPTIONS -locations none) +configure_file(translations/translations.qrc ${CMAKE_BINARY_DIR} COPYONLY) +set(PROJECT_SOURCES ${PROJECT_SOURCES} ${TS_FILES} ${QM_FILES} ${CMAKE_BINARY_DIR}/translations.qrc) + +# Qt exe +if (${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(nekoray + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + ) + # Define target properties for Android with Qt 6 as: + # set_property(TARGET nekoray APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + # ${CMAKE_CURRENT_SOURCE_DIR}/android) + # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation +else () + if (ANDROID) + add_library(nekoray SHARED + ${PROJECT_SOURCES} + ) + # Define properties for Android with Qt 5 after find_package() calls as: + # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") + else () + add_executable(nekoray + ${PROJECT_SOURCES} + ) + endif () +endif () + +# Target + +set_property(TARGET nekoray PROPERTY AUTOUIC ON) +set_property(TARGET nekoray PROPERTY AUTOMOC ON) +set_property(TARGET nekoray PROPERTY AUTORCC ON) + +# Target Link + +target_link_libraries(nekoray PRIVATE + Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg + Threads::Threads + ${NKR_EXTERNAL_TARGETS} + ${PLATFORM_FUCKING_LIBRARIES} + ) + +set_target_properties(nekoray PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE + ) + +if (QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(nekoray) +endif () diff --git a/README.md b/README.md new file mode 100644 index 0000000..446f1e2 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# NekoRay + +基于 Qt/C++ 的跨平台代理配置管理器( 使用 Matsuri 定制版 v2ray-core ) + +目前支持 Windows / Linux amd64 开箱即用 + +Qt/C++ based cross-platform proxy configuration manager ( Use Matsuri custom version of v2ray-core ) + +Support Windows / Linux amd64 out of the box now. + +## 下载 Download + +便携格式,无安装器。转到 Releases 下载预编译的二进制文件,解压后即可使用。 + +### GitHub Releases 下载 + +[![GitHub All Releases](https://img.shields.io/github/downloads/Matsuridayo/nekoray/total?label=downloads-total&logo=github&style=flat-square)](https://github.com/Matsuridayo/nekoray/releases) + +## 更改记录 & 发布频道 Changelog & Telegram channel + +https://t.me/Matsuridayo + +## 项目主页 & 文档 Homepage & Documents + +https://matsuridayo.github.io + +### 运行参数 + +- `-many` 无视同目录正在运行的实例,强行开启新的实例 (0.11+) +- `-appdata` 开启后配置文件会放在共享目录,无法多开和自动升级 (0.11+) + +### 代理 + +| 协议 | 状态 | 配置编辑 | 分享链接生成 | 分享链接解析 | Clash 配置解析 | +|--------------|--------|------|--------|-----------|------------| +| Socks | ✅ | ✅ | ✅ | ✅ | ✅ | +| HTTP | ✅ | ✅ | ✅ | ✅ | ✅ | +| Shadowsocks | ✅ (经典) | ✅ | ✅ | 常见格式 | ✅ | +| VMess | ✅ | ✅ | ✅ | v2rayN 格式 | ✅ | +| Trojan | ✅ | ✅ | ✅ | 标准&常见格式 | ✅ | +| VLESS | ✅ | ✅ | ✅ | ✅ | 不适用 | +| NaïveProxy | ✅ | ✅ | ✅ | ✅ | 不适用 | +| Hysteria | ✅ | ❌ | ❌ | ❌ | 不适用 | + +## Linux 运行 & 简易编译教程 + +**使用 Linux 系统相信您已具备基本的排错能力, +本项目不提供特定发行版/架构的支持,预编译文件不能满足您的需求时,请自行编译/适配。** + +系统要求: Qt5 运行环境,一般桌面 Linux 已经安装,如果没有请用包管理器安装,如 + +`apt install libqt5gui5 libqt5x11extras5` + +运行: `./launcher` 或 部分系统可双击打开 + +launcher 参数 + +* `./launcher -- -appdata` `--` 后的参数传递给主程序 +* `-debug` Debug mode +* `-theme` Use local QT theme (unstable) (1.0+) + +已知部分 x86_64 Linux 发行版无法使用预编译版、非 x86_64 暂无适配,可以尝试自行编译。 + +### 编译 + +准备工作 + +``` +git submodule init +git submodule update +``` + +| CMake 参数 | 默认值 | 含义 | +|-------------------------|-----|-------------------------| +| QT_VERSION_MAJOR | 5 | QT版本 | +| NKR_NO_EXTERNAL | | 不包含外部C++依赖(如ZXing/gRPC) | +| NKR_NO_GRPC | | 不包含gRPC | +| NKR_CROSS | | | + +### 简单编译法 + +条件: + +1. C++ 依赖: `qt5 protobuf yaml-cpp zxing-cpp` 已用包管理器安装,并符合版本要求 +2. Qt 版本必须大于等于 5.15 +3. 系统为 `x86-64-linux-gnu` + +``` +mkdir build +cd build +cmake -GNinja .. +ninja +``` + +编译完成后得到 `nekoray` + +解压 Release 的压缩包,替换其中的 `nekoray`,删除 `launcher` 即可使用。 + +### 复杂编译法 + +C++ 部分 + +当您的发行版没有上面几个 C++ 依赖包,或者版本不符合要求时,可以参考 libs 文件夹内的默认编译脚本自行编译。 + +依赖搜寻 prefix 为 `libs/deps/bulit` + +编译完成后得到 `nekoray` + +Go 部分 + +1. 把 `Matsuridayo/Matsuri` `Matsuridayo/v2ray-core` 置于 `../` +2. 进入 `go` 文件夹 `go build` 得到 `nekoray_core`。 + +非官方构建无需编译 `updater` `launcher` + +## Credits + +- [v2fly/v2ray-core](https://github.com/v2fly/v2ray-core) +- [MatsuriDayo/Matsuri](https://github.com/MatsuriDayo/Matsuri) +- [MatsuriDayo/v2ray-core](https://github.com/MatsuriDayo/v2ray-core) +- [SagerNet/sing-box](https://github.com/SagerNet/sing-box) +- [Qt](https://www.qt.io/) +- [protobuf](https://github.com/protocolbuffers/protobuf) +- [yaml-cpp](https://github.com/jbeder/yaml-cpp) +- [zxing-cpp](https://github.com/nu-book/zxing-cpp) +- [QHotkey](https://github.com/Skycoder42/QHotkey) diff --git a/assets/nekoray.png b/assets/nekoray.png new file mode 100644 index 0000000..2624860 Binary files /dev/null and b/assets/nekoray.png differ diff --git a/assets/qtbase_zh_CN.qm b/assets/qtbase_zh_CN.qm new file mode 100644 index 0000000..8f4354d Binary files /dev/null and b/assets/qtbase_zh_CN.qm differ diff --git a/cmake/fuck_windows/VersionInfo.in b/cmake/fuck_windows/VersionInfo.in new file mode 100644 index 0000000..89625c1 --- /dev/null +++ b/cmake/fuck_windows/VersionInfo.in @@ -0,0 +1,82 @@ +#pragma once + +#ifndef PRODUCT_VERSION_MAJOR + #define PRODUCT_VERSION_MAJOR @PRODUCT_VERSION_MAJOR@ +#endif + +#ifndef PRODUCT_VERSION_MINOR + #define PRODUCT_VERSION_MINOR @PRODUCT_VERSION_MINOR@ +#endif + +#ifndef PRODUCT_VERSION_PATCH + #define PRODUCT_VERSION_PATCH @PRODUCT_VERSION_PATCH@ +#endif + +#ifndef PRODUCT_VERSION_BUILD + #define PRODUCT_VERSION_BUILD @PRODUCT_VERSION_REVISION@ +#endif + +#ifndef FILE_VERSION_MAJOR + #define FILE_VERSION_MAJOR @PRODUCT_VERSION_MAJOR@ +#endif + +#ifndef FILE_VERSION_MINOR + #define FILE_VERSION_MINOR @PRODUCT_VERSION_MINOR@ +#endif + +#ifndef FILE_VERSION_PATCH + #define FILE_VERSION_PATCH @PRODUCT_VERSION_PATCH@ +#endif + +#ifndef FILE_VERSION_BUILD + #define FILE_VERSION_BUILD @PRODUCT_VERSION_REVISION@ +#endif + +#ifndef __TO_STRING + #define __TO_STRING_IMPL(x) #x + #define __TO_STRING(x) __TO_STRING_IMPL(x) +#endif + +#define PRODUCT_VERSION_MAJOR_MINOR_STR __TO_STRING(PRODUCT_VERSION_MAJOR) "." __TO_STRING(PRODUCT_VERSION_MINOR) +#define PRODUCT_VERSION_MAJOR_MINOR_PATCH_STR PRODUCT_VERSION_MAJOR_MINOR_STR "." __TO_STRING(PRODUCT_VERSION_PATCH) +#define PRODUCT_VERSION_FULL_STR PRODUCT_VERSION_MAJOR_MINOR_PATCH_STR "." __TO_STRING(PRODUCT_VERSION_BUILD) +#define PRODUCT_VERSION_RESOURCE PRODUCT_VERSION_MAJOR,PRODUCT_VERSION_MINOR,PRODUCT_VERSION_PATCH,PRODUCT_VERSION_BUILD +#define PRODUCT_VERSION_RESOURCE_STR PRODUCT_VERSION_FULL_STR "\0" + +#define FILE_VERSION_MAJOR_MINOR_STR __TO_STRING(FILE_VERSION_MAJOR) "." __TO_STRING(FILE_VERSION_MINOR) +#define FILE_VERSION_MAJOR_MINOR_PATCH_STR FILE_VERSION_MAJOR_MINOR_STR "." __TO_STRING(FILE_VERSION_PATCH) +#define FILE_VERSION_FULL_STR FILE_VERSION_MAJOR_MINOR_PATCH_STR "." __TO_STRING(FILE_VERSION_BUILD) +#define FILE_VERSION_RESOURCE FILE_VERSION_MAJOR,FILE_VERSION_MINOR,FILE_VERSION_PATCH,FILE_VERSION_BUILD +#define FILE_VERSION_RESOURCE_STR FILE_VERSION_FULL_STR "\0" + +#ifndef PRODUCT_ICON + #define PRODUCT_ICON "@PRODUCT_ICON@" +#endif + +#ifndef PRODUCT_COMMENTS + #define PRODUCT_COMMENTS "@PRODUCT_COMMENTS@\0" +#endif + +#ifndef PRODUCT_COMPANY_NAME + #define PRODUCT_COMPANY_NAME "@PRODUCT_COMPANY_NAME@\0" +#endif + +#ifndef PRODUCT_COMPANY_COPYRIGHT + #define PRODUCT_COMPANY_COPYRIGHT "@PRODUCT_COMPANY_COPYRIGHT@\0" +#endif + +#ifndef PRODUCT_FILE_DESCRIPTION + #define PRODUCT_FILE_DESCRIPTION "@PRODUCT_FILE_DESCRIPTION@\0" +#endif + +#ifndef PRODUCT_INTERNAL_NAME + #define PRODUCT_INTERNAL_NAME "@PRODUCT_NAME@\0" +#endif + +#ifndef PRODUCT_ORIGINAL_FILENAME + #define PRODUCT_ORIGINAL_FILENAME "@PRODUCT_ORIGINAL_FILENAME@\0" +#endif + +#ifndef PRODUCT_BUNDLE + #define PRODUCT_BUNDLE "@PRODUCT_BUNDLE@\0" +#endif diff --git a/cmake/fuck_windows/VersionResource.rc b/cmake/fuck_windows/VersionResource.rc new file mode 100644 index 0000000..b5462ac --- /dev/null +++ b/cmake/fuck_windows/VersionResource.rc @@ -0,0 +1,52 @@ +#include "VersionInfo.h" + +#if defined(__MINGW64__) || defined(__MINGW32__) + // MinGW-w64, MinGW + #if defined(__has_include) && __has_include() + #include + #else + #include + #include + #endif +#else + // MSVC, Windows SDK + #include +#endif + +IDI_ICON1 ICON PRODUCT_ICON + +LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FILE_VERSION_RESOURCE + PRODUCTVERSION PRODUCT_VERSION_RESOURCE + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000904b0" + BEGIN + VALUE "Comments", PRODUCT_COMMENTS + VALUE "CompanyName", PRODUCT_COMPANY_NAME + VALUE "FileDescription", PRODUCT_FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_RESOURCE_STR + VALUE "InternalName", PRODUCT_INTERNAL_NAME + VALUE "LegalCopyright", PRODUCT_COMPANY_COPYRIGHT + VALUE "OriginalFilename", PRODUCT_ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_BUNDLE + VALUE "ProductVersion", PRODUCT_VERSION_RESOURCE_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x9, 1200 + END +END diff --git a/cmake/fuck_windows/fuck.cmake b/cmake/fuck_windows/fuck.cmake new file mode 100644 index 0000000..e9b455d --- /dev/null +++ b/cmake/fuck_windows/fuck.cmake @@ -0,0 +1,27 @@ +if (WIN32) + set(PLATFORM_FUCKING_SOURCES 3rdparty/WinCommander.cpp) + + include(cmake/fuck_windows/generate_product_version.cmake) + generate_product_version( + QV2RAY_RC + NAME "Nekoray" + BUNDLE "Nekoray Project Family" + ICON "${CMAKE_SOURCE_DIR}/res/nekoray.ico" + COMPANY_NAME "Nekoray Workgroup" + COMPANY_COPYRIGHT "Nekoray Workgroup" + FILE_DESCRIPTION "Nekoray Main Application" + ) + add_definitions(-DUNICODE -D_UNICODE -DNOMINMAX) + set(GUI_TYPE WIN32) + if (MINGW) + if (NOT DEFINED MinGW_ROOT) + set(MinGW_ROOT "C:/msys64/mingw64") + endif () + else () + add_compile_options("/utf-8") + add_compile_options("/std:c++17") + add_definitions(-D_WIN32_WINNT=0x600 -D_SCL_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_WARNINGS) + set(PLATFORM_FUCKING_LIBRARIES wininet wsock32 ws2_32 user32 Rasapi32 Iphlpapi) + list(APPEND PLATFORM_FUCKING_SOURCES sys/windows/MiniDump.cpp) + endif () +endif () diff --git a/cmake/fuck_windows/generate_product_version.cmake b/cmake/fuck_windows/generate_product_version.cmake new file mode 100644 index 0000000..bc395d4 --- /dev/null +++ b/cmake/fuck_windows/generate_product_version.cmake @@ -0,0 +1,107 @@ +include (CMakeParseArguments) + +set (GenerateProductVersionCurrentDir ${CMAKE_CURRENT_LIST_DIR}) + +# generate_product_version() function +# +# This function uses VersionInfo.in template file and VersionResource.rc file +# to generate WIN32 resource with version information and general resource strings. +# +# Usage: +# generate_product_version( +# SomeOutputResourceVariable +# NAME MyGreatProject +# ICON ${PATH_TO_APP_ICON} +# VERSION_MAJOR 2 +# VERSION_MINOR 3 +# VERSION_PATCH ${BUILD_COUNTER} +# VERSION_REVISION ${BUILD_REVISION} +# ) +# where BUILD_COUNTER and BUILD_REVISION could be values from your CI server. +# +# You can use generated resource for your executable targets: +# add_executable(target-name ${target-files} ${SomeOutputResourceVariable}) +# +# You can specify resource strings in arguments: +# NAME - name of executable (no defaults, ex: Microsoft Word) +# BUNDLE - bundle (${NAME} is default, ex: Microsoft Office) +# ICON - path to application icon (${CMAKE_SOURCE_DIR}/product.ico by default) +# VERSION_MAJOR - 1 is default +# VERSION_MINOR - 0 is default +# VERSION_PATCH - 0 is default +# VERSION_REVISION - 0 is default +# COMPANY_NAME - your company name (no defaults) +# COMPANY_COPYRIGHT - ${COMPANY_NAME} (C) Copyright ${CURRENT_YEAR} is default +# COMMENTS - ${NAME} v${VERSION_MAJOR}.${VERSION_MINOR} is default +# ORIGINAL_FILENAME - ${NAME} is default +# INTERNAL_NAME - ${NAME} is default +# FILE_DESCRIPTION - ${NAME} is default +function(generate_product_version outfiles) + set (options) + set (oneValueArgs + NAME + BUNDLE + ICON + VERSION_MAJOR + VERSION_MINOR + VERSION_PATCH + VERSION_REVISION + COMPANY_NAME + COMPANY_COPYRIGHT + COMMENTS + ORIGINAL_FILENAME + INTERNAL_NAME + FILE_DESCRIPTION) + set (multiValueArgs) + cmake_parse_arguments(PRODUCT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT PRODUCT_BUNDLE OR "${PRODUCT_BUNDLE}" STREQUAL "") + set(PRODUCT_BUNDLE "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_ICON OR "${PRODUCT_ICON}" STREQUAL "") + set(PRODUCT_ICON "${CMAKE_SOURCE_DIR}/product.ico") + endif() + + if (NOT PRODUCT_VERSION_MAJOR EQUAL 0 AND (NOT PRODUCT_VERSION_MAJOR OR "${PRODUCT_VERSION_MAJOR}" STREQUAL "")) + set(PRODUCT_VERSION_MAJOR 1) + endif() + if (NOT PRODUCT_VERSION_MINOR EQUAL 0 AND (NOT PRODUCT_VERSION_MINOR OR "${PRODUCT_VERSION_MINOR}" STREQUAL "")) + set(PRODUCT_VERSION_MINOR 0) + endif() + if (NOT PRODUCT_VERSION_PATCH EQUAL 0 AND (NOT PRODUCT_VERSION_PATCH OR "${PRODUCT_VERSION_PATCH}" STREQUAL "")) + set(PRODUCT_VERSION_PATCH 0) + endif() + if (NOT PRODUCT_VERSION_REVISION EQUAL 0 AND (NOT PRODUCT_VERSION_REVISION OR "${PRODUCT_VERSION_REVISION}" STREQUAL "")) + set(PRODUCT_VERSION_REVISION 0) + endif() + + if (NOT PRODUCT_COMPANY_COPYRIGHT OR "${PRODUCT_COMPANY_COPYRIGHT}" STREQUAL "") + string(TIMESTAMP PRODUCT_CURRENT_YEAR "%Y") + set(PRODUCT_COMPANY_COPYRIGHT "${PRODUCT_COMPANY_NAME} (C) Copyright ${PRODUCT_CURRENT_YEAR}") + endif() + if (NOT PRODUCT_COMMENTS OR "${PRODUCT_COMMENTS}" STREQUAL "") + set(PRODUCT_COMMENTS "${PRODUCT_NAME} v${PRODUCT_VERSION_MAJOR}.${PRODUCT_VERSION_MINOR}") + endif() + if (NOT PRODUCT_ORIGINAL_FILENAME OR "${PRODUCT_ORIGINAL_FILENAME}" STREQUAL "") + set(PRODUCT_ORIGINAL_FILENAME "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_INTERNAL_NAME OR "${PRODUCT_INTERNAL_NAME}" STREQUAL "") + set(PRODUCT_INTERNAL_NAME "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_FILE_DESCRIPTION OR "${PRODUCT_FILE_DESCRIPTION}" STREQUAL "") + set(PRODUCT_FILE_DESCRIPTION "${PRODUCT_NAME}") + endif() + + set (_VersionInfoFile ${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.h) + set (_VersionResourceFile ${CMAKE_CURRENT_BINARY_DIR}/VersionResource.rc) + configure_file( + ${GenerateProductVersionCurrentDir}/VersionInfo.in + ${_VersionInfoFile} + @ONLY) + configure_file( + ${GenerateProductVersionCurrentDir}/VersionResource.rc + ${_VersionResourceFile} + COPYONLY) + list(APPEND ${outfiles} ${_VersionInfoFile} ${_VersionResourceFile}) + set (${outfiles} ${${outfiles}} PARENT_SCOPE) +endfunction() diff --git a/cmake/myproto.cmake b/cmake/myproto.cmake new file mode 100644 index 0000000..89ddb70 --- /dev/null +++ b/cmake/myproto.cmake @@ -0,0 +1,14 @@ +find_package(Protobuf CONFIG REQUIRED) + +set(PROTO_FILES + go/gen/libcore.proto + ) + +add_library(myproto ${PROTO_FILES}) +target_link_libraries(myproto + PUBLIC + protobuf::libprotobuf + ) +target_include_directories(myproto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + +protobuf_generate(TARGET myproto LANGUAGE cpp) diff --git a/cmake/nkr.cmake b/cmake/nkr.cmake new file mode 100644 index 0000000..6ae6652 --- /dev/null +++ b/cmake/nkr.cmake @@ -0,0 +1,10 @@ +# Release +file(STRINGS nekoray_version.txt NKR_VERSION) +add_compile_definitions(NKR_VERSION=\"${NKR_VERSION}\") + +# Debug +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DNKR_DEBUG") + +if (NKR_USE_APPDATA) + add_compile_definitions(NKR_USE_APPDATA) +endif () diff --git a/cmake/print.cmake b/cmake/print.cmake new file mode 100644 index 0000000..89705d2 --- /dev/null +++ b/cmake/print.cmake @@ -0,0 +1,43 @@ +macro(print_all_variables) + message(STATUS "print_all_variables------------------------------------------{") + get_cmake_property(_variableNames VARIABLES) + foreach (_variableName ${_variableNames}) + message(STATUS "${_variableName}=${${_variableName}}") + endforeach() + message(STATUS "print_all_variables------------------------------------------}") +endmacro() + +# Get all propreties that cmake supports +if(NOT CMAKE_PROPERTY_LIST) + execute_process(COMMAND cmake --help-property-list OUTPUT_VARIABLE CMAKE_PROPERTY_LIST) + + # Convert command output into a CMake list + string(REGEX REPLACE ";" "\\\\;" CMAKE_PROPERTY_LIST "${CMAKE_PROPERTY_LIST}") + string(REGEX REPLACE "\n" ";" CMAKE_PROPERTY_LIST "${CMAKE_PROPERTY_LIST}") +endif() + +function(print_properties) + message("CMAKE_PROPERTY_LIST = ${CMAKE_PROPERTY_LIST}") +endfunction() + +function(print_target_properties target) + if(NOT TARGET ${target}) + message(STATUS "There is no target named '${target}'") + return() + endif() + + foreach(property ${CMAKE_PROPERTY_LIST}) + string(REPLACE "" "${CMAKE_BUILD_TYPE}" property ${property}) + + # Fix https://stackoverflow.com/questions/32197663/how-can-i-remove-the-the-location-property-may-not-be-read-from-target-error-i + if(property STREQUAL "LOCATION" OR property MATCHES "^LOCATION_" OR property MATCHES "_LOCATION$") + continue() + endif() + + get_property(was_set TARGET ${target} PROPERTY ${property} SET) + if(was_set) + get_target_property(value ${target} ${property}) + message("${target} ${property} = ${value}") + endif() + endforeach() +endfunction() diff --git a/core_commit.txt b/core_commit.txt new file mode 100644 index 0000000..55ceba3 --- /dev/null +++ b/core_commit.txt @@ -0,0 +1 @@ +edf4fcff77a0dfd40b20076faf45767a3394f5eb diff --git a/db/ConfigBuilder.cpp b/db/ConfigBuilder.cpp new file mode 100644 index 0000000..23f9e6a --- /dev/null +++ b/db/ConfigBuilder.cpp @@ -0,0 +1,475 @@ +#include "db/ConfigBuilder.hpp" +#include "db/Database.hpp" +#include "fmt/includes.h" + +namespace NekoRay { + + void ApplyCustomOutboundJsonSettings(const QJsonObject &custom, QJsonObject &outbound) { + // 合并 + if (custom.isEmpty()) return; + for (const auto &key: custom.keys()) { + if (outbound.contains(key)) { + auto v = custom[key]; + auto v_orig = outbound[key]; + if (v.isObject() && v_orig.isObject()) {// isObject 则合并? + auto vo = v.toObject(); + QJsonObject vo_orig = v_orig.toObject(); + ApplyCustomOutboundJsonSettings(vo, vo_orig); + outbound[key] = vo_orig; + } else { + outbound[key] = v; + } + } else { + outbound[key] = custom[key]; + } + } + } + + QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest) { + auto result = QSharedPointer(new BuildConfigResult); + auto status = QSharedPointer(new BuildConfigStatus); + status->result = result; + + // Log + auto logObj = QJsonObject{{"loglevel", dataStore->log_level}}; + result->coreConfig.insert("log", logObj); + + // Inbounds + QJsonObject sniffing{{"destOverride", dataStore->fake_dns ? + QJsonArray{"fakedns", "http", "tls", "quic"} + : QJsonArray{"http", "tls", "quic"}}, + {"enabled", true}, + {"metadataOnly", false}, + {"routeOnly", dataStore->sniffing_mode == SniffingMode::FOR_ROUTING},}; + + // socks-in + if (InRange(dataStore->inbound_socks_port, 0, 65535) && !forTest) { + QJsonObject socksInbound; + socksInbound["tag"] = "socks-in"; + socksInbound["protocol"] = "socks"; + socksInbound["listen"] = dataStore->inbound_address; + socksInbound["port"] = dataStore->inbound_socks_port; + socksInbound["settings"] = QJsonObject({{"auth", "noauth"}, + {"udp", true},}); + if (dataStore->fake_dns || dataStore->sniffing_mode != SniffingMode::DISABLE) { + socksInbound["sniffing"] = sniffing; + } + status->inbounds += socksInbound; + } + // http-in + if (InRange(dataStore->inbound_http_port, 0, 65535) && !forTest) { + QJsonObject socksInbound; + socksInbound["tag"] = "http-in"; + socksInbound["protocol"] = "http"; + socksInbound["listen"] = dataStore->inbound_address; + socksInbound["port"] = dataStore->inbound_http_port; + if (dataStore->sniffing_mode != SniffingMode::DISABLE) { + socksInbound["sniffing"] = sniffing; + } + status->inbounds += socksInbound; + } + + // Outbounds + QList> ents; + if (ent->type == "chain") { + auto list = ent->ChainBean()->list; + std::reverse(std::begin(list), std::end(list)); + for (auto id: list) { + ents += profileManager->GetProfile(id); + if (ents.last() == nullptr) { + result->error = QString("chain missing ent: %1").arg(id); + return result; + } + if (ents.last()->type == "chain") { + result->error = QString("chain in chain is not allowed: %1").arg(id); + return result; + } + } + } else { + ents += ent; + } + status->currentEnt = ent.get(); + QString tagProxy = BuildChain(0, ents, status); + if (!result->error.isEmpty()) return result; + + // direct & bypass & block + status->outbounds += QJsonObject{{"protocol", "freedom"}, + {"tag", "direct"},}; + status->outbounds += QJsonObject{{"protocol", "freedom"}, + {"tag", "bypass"},}; + status->outbounds += QJsonObject{{"protocol", "blackhole"}, + {"tag", "block"},}; + + // block for tun + if (!forTest) { + status->routingRules += QJsonObject{{"type", "field"}, + {"ip", QJsonArray{"224.0.0.0/3", "169.254.0.0/16",},}, + {"outboundTag", "block"},}; + status->routingRules += QJsonObject{{"type", "field"}, + {"port", "135-139"}, + {"outboundTag", "block"},}; + } + + // DNS Routing (tun2socks 用到,防污染) + if (dataStore->dns_routing && !forTest) { + QJsonObject dnsOut; + dnsOut["protocol"] = "dns"; + dnsOut["tag"] = "dns-out"; + QJsonObject dnsOut_settings; + dnsOut_settings["network"] = "tcp"; + dnsOut_settings["port"] = 53; + dnsOut_settings["address"] = "8.8.8.8"; + dnsOut_settings["userLevel"] = 1; + dnsOut["settings"] = dnsOut_settings; + dnsOut["proxySettings"] = QJsonObject{ + {"tag", tagProxy}, + {"transportLayer", true} + }; + + status->outbounds += dnsOut; + status->routingRules += QJsonObject{ + {"type", "field"}, + {"port", "53"}, + {"inboundTag", QJsonArray{"socks-in", "http-in"}}, + {"outboundTag", "dns-out"}, + }; + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{"dns-in"}}, + {"outboundTag", "dns-out"}, + }; + } + + // custom inbound + QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray()) + + result->coreConfig.insert("inbounds", status->inbounds); + result->coreConfig.insert("outbounds", status->outbounds); + + // dns domain user rule + for (const auto &line: SplitLines(dataStore->routing->proxy_domain)) { + if (line.startsWith("#")) continue; + if (dataStore->dns_routing) status->domainListDNSRemote += line; + status->domainListRemote += line; + } + for (const auto &line: SplitLines(dataStore->routing->direct_domain)) { + if (line.startsWith("#")) continue; + if (dataStore->dns_routing) status->domainListDNSDirect += line; + status->domainListDirect += line; + } + for (const auto &line: SplitLines(dataStore->routing->block_domain)) { + if (line.startsWith("#")) continue; + status->domainListBlock += line; + } + + // final add DNS + QJsonObject dns; + QJsonArray dnsServers; + + // FakeDNS + QJsonObject dnsServerFake; + dnsServerFake["address"] = "fakedns"; + dnsServerFake["domains"] = status->domainListDNSRemote; + if (dataStore->fake_dns && !forTest) dnsServers += dnsServerFake; + + // remote + QJsonObject dnsServerRemote; + dnsServerRemote["address"] = dataStore->remote_dns; + dnsServerRemote["domains"] = status->domainListDNSRemote; + if (!forTest) dnsServers += dnsServerRemote; + + //direct + auto directDnsAddress = dataStore->direct_dns; + if (directDnsAddress.contains("://")) { + auto directDnsIp = SubStrBefore(SubStrAfter(directDnsAddress, "://"), "/"); + if (IsIpAddress(directDnsIp)) { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"ip", QJsonArray{directDnsIp}}, + {"outboundTag", "direct"}, + }); + } else { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"domain", QJsonArray{directDnsIp}}, + {"outboundTag", "direct"}, + }); + } + } else if (directDnsAddress != "localhost") { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"ip", QJsonArray{directDnsAddress}}, + {"outboundTag", "direct"}, + }); + } + dnsServers += QJsonObject{{"address", directDnsAddress}, + {"domains", status->domainListDNSDirect}, + {"skipFallback", true},}; + + dns["disableFallbackIfMatch"] = true; + dns["hosts"] = status->hosts; + dns["servers"] = dnsServers; + dns["tag"] = "dns"; + result->coreConfig.insert("dns", dns); + + // Routing + QJsonObject routing; + routing["domainStrategy"] = dataStore->domain_strategy; + routing["domainMatcher"] = dataStore->domain_matcher == DomainMatcher::MPH ? "mph" : "linear"; + + // ip user rule + QJsonObject routingRule_tmp; + routingRule_tmp["type"] = "field"; + + // block + routingRule_tmp["outboundTag"] = "block"; + for (const auto &line: SplitLines(dataStore->routing->block_ip)) { + if (line.startsWith("#")) continue; + status->ipListBlock += line; + } + // final add block route + if (!status->ipListBlock.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListBlock; + status->routingRules += tmp; + } + if (!status->domainListBlock.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListBlock; + status->routingRules += tmp; + } + + // proxy + routingRule_tmp["outboundTag"] = tagProxy; + for (const auto &line: SplitLines(dataStore->routing->proxy_ip)) { + if (line.startsWith("#")) continue; + status->ipListRemote += line; + } + // final add proxy route + if (!status->ipListRemote.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListRemote; + status->routingRules += tmp; + } + if (!status->domainListRemote.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListRemote; + status->routingRules += tmp; + } + + // bypass + routingRule_tmp["outboundTag"] = "bypass"; + for (const auto &line: SplitLines(dataStore->routing->direct_ip)) { + if (line.startsWith("#")) continue; + status->ipListDirect += line; + } + // final add bypass route + if (!status->ipListDirect.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListDirect; + status->routingRules += tmp; + } + if (!status->domainListDirect.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListDirect; + status->routingRules += tmp; + } + + // final add routing rule + // custom routing rule + auto routingRules = QString2QJsonObject(dataStore->routing->custom)["rules"].toArray(); + QJSONARRAY_ADD(routingRules, QString2QJsonObject(dataStore->custom_route_global)["rules"].toArray()) + QJSONARRAY_ADD(routingRules, status->routingRules) + routing["rules"] = routingRules; + result->coreConfig.insert("routing", routing); + + // Policy & stats + QJsonObject policy; + QJsonObject levels; + QJsonObject level1; + level1["connIdle"] = 30; + levels["1"] = level1; + policy["levels"] = levels; + + QJsonObject policySystem; + policySystem["statsOutboundDownlink"] = true; + policySystem["statsOutboundUplink"] = true; + policy["system"] = policySystem; + result->coreConfig.insert("policy", policy); + result->coreConfig.insert("stats", QJsonObject()); + + return result; + } + + QString BuildChain(int chainId, const QList> &ents, + const QSharedPointer &status) { + QString chainTag = "c-" + Int2String(chainId); + bool muxApplied = false; + + QString pastTag; + int index = 0; + + for (const auto &ent: ents) { + // tagOut: v2ray outbound tag for a profile + // profile2 (in) (global) tag g-(id) + // profile1 tag (chainTag)-(id) + // profile0 (out) tag (chainTag)-(id) / single: chainTag=g-(id) + auto tagOut = chainTag + "-" + Int2String(ent->id); + + // needGlobal: can only contain one? + bool needGlobal = false; + + // first profile set as global + if (index == ents.length() - 1) { + needGlobal = true; + tagOut = "g-" + Int2String(ent->id); + } + + if (needGlobal) { + if (status->globalProfiles.contains(ent->id)) { + continue; + } + status->globalProfiles += ent->id; + } + + if (index > 0) { + // chain rules: past + if (!ents[index - 1]->bean->NeedExternal()) { + auto replaced = status->outbounds.last().toObject(); + replaced["proxySettings"] = QJsonObject{ + {"tag", tagOut}, + {"transportLayer", true}, + }; + status->outbounds.removeLast(); + status->outbounds += replaced; + } else { + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{pastTag + "-mapping"}}, + {"outboundTag", tagOut}, + }; + } + } else { + // index == 0 means last profile in chain / not chain + chainTag = tagOut; + status->result->outboundStat = ent->traffic_data; + } + + // chain rules: this + auto mapping_port = MkPort(); + if (ent->bean->NeedExternal()) { + status->inbounds += QJsonObject{ + {"protocol", "dokodemo-door"}, + {"tag", tagOut + "-mapping"}, + {"listen", "127.0.0.1"}, + {"port", mapping_port}, + {"settings", QJsonObject{ // to + {"address", ent->bean->serverAddress}, + {"port", ent->bean->serverPort}, + {"network", "tcp,udp"}, + }}, + }; + // no chain rule and not outbound, so need to set to direct + if (index == ents.length() - 1) { + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{tagOut + "-mapping"}}, + {"outboundTag", "direct"}, + }; + } + } + + // Outbound + + QJsonObject outbound; + fmt::CoreObjOutboundBuildResult coreR; + fmt::ExternalBuildResult extR; + + if (ent->bean->NeedExternal()) { + auto ext_socks_port = MkPort(); + extR = ent->bean->BuildExternal(mapping_port, ext_socks_port); + if (!extR.error.isEmpty()) { // rejected + status->result->error = extR.error; + return ""; + } + + // SOCKS OUTBOUND + outbound["protocol"] = "socks"; + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + server["address"] = "127.0.0.1"; + server["port"] = ext_socks_port; + servers.push_back(server); + settings["servers"] = servers; + outbound["settings"] = settings; + + // EXTERNAL PROCESS + auto extC = new sys::ExternalProcess(ent->bean->DisplayType(), + extR.program, extR.arguments, extR.env); + status->result->ext += extC; + } else { + coreR = ent->bean->BuildCoreObj(); + if (!coreR.error.isEmpty()) { // rejected + status->result->error = coreR.error; + return ""; + } + outbound = coreR.outbound; + } + + // outbound misc + outbound["tag"] = tagOut; + outbound["domainStrategy"] = dataStore->outbound_domain_strategy; + ent->traffic_data->id = ent->id; + ent->traffic_data->tag = tagOut.toStdString(); + status->result->outboundStats += ent->traffic_data; + + // apply mux + if (dataStore->mux_cool > 0 && !muxApplied) { + // TODO refactor mux settings + if (ent->type == "vmess" || ent->type == "trojan" || ent->type == "vless") { + auto muxObj = QJsonObject{ + {"enabled", true}, + {"concurrency", dataStore->mux_cool}, + }; + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr && !stream->packet_encoding.isEmpty()) { + muxObj["packetEncoding"] = stream->packet_encoding; + } + outbound["mux"] = muxObj; + muxApplied = true; + } + } + + // apply custom outbound settings + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + ApplyCustomOutboundJsonSettings(QString2QJsonObject(*((QString *) custom_item->ptr)), outbound); + } + + // Bypass Lookup for the first profile + if (index == ents.length() - 1 && !IsIpAddress(ent->bean->serverAddress)) { + if (dataStore->enhance_resolve_server_domain) { + status->result->tryDomains += ent->bean->serverAddress; + } else { + status->domainListDNSDirect += "full:" + ent->bean->serverAddress; + } + } + + status->outbounds += outbound; + pastTag = tagOut; + index++; + } + + // this is a chain + if (ents.length() > 1) { + // Chain ent traffic stat + status->currentEnt->traffic_data->id = status->currentEnt->id; + status->currentEnt->traffic_data->tag = chainTag.toStdString(); + status->result->outboundStats += status->currentEnt->traffic_data; + } + + return chainTag; + } + +} \ No newline at end of file diff --git a/db/ConfigBuilder.hpp b/db/ConfigBuilder.hpp new file mode 100644 index 0000000..ff86ff6 --- /dev/null +++ b/db/ConfigBuilder.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "ProxyEntity.hpp" +#include "sys/ExternalProcess.hpp" + +namespace NekoRay { + class BuildConfigResult { + public: + QString error; + QJsonObject coreConfig; + QStringList tryDomains; + + QList> outboundStats; // all, but not including "bypass" "block" + QSharedPointer outboundStat; // main + + QList ext; + }; + + class BuildConfigStatus { + public: + QSharedPointer result; + + QJsonArray domainListDNSRemote; + QJsonArray domainListDNSDirect; + QJsonArray domainListRemote; + QJsonArray domainListDirect; + QJsonArray ipListRemote; + QJsonArray ipListDirect; + + QJsonArray domainListBlock; + QJsonArray ipListBlock; + + QJsonArray routingRules; + QJsonObject hosts; + + QJsonArray inbounds; + QJsonArray outbounds; + + QList globalProfiles; + + ProxyEntity *currentEnt; + }; + + QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest); + + QString BuildChain(int chainId, const QList> &ents, + const QSharedPointer &status); +} diff --git a/db/Database.cpp b/db/Database.cpp new file mode 100644 index 0000000..07b0fb4 --- /dev/null +++ b/db/Database.cpp @@ -0,0 +1,260 @@ +#include "Database.hpp" + +#include "fmt/includes.h" + +#include + +namespace NekoRay { + + ProfileManager *profileManager = new ProfileManager(); + + ProfileManager::ProfileManager() : JsonStore("groups/pm.json") { + _hooks_after_load.push_back([=]() { LoadManager(); }); + _hooks_before_save.push_back([=]() { SaveManager(); }); + _add(new configItem("profiles", &_profiles, itemType::integerList)); + _add(new configItem("groups", &_groups, itemType::integerList)); + } + + void ProfileManager::LoadManager() { + for (auto id: _profiles) { + profiles[id] = LoadProxyEntity(QString("profiles/%1.json").arg(id)); + } + for (auto id: _groups) { + groups[id] = LoadGroup(QString("groups/%1.json").arg(id)); + } + } + + void ProfileManager::SaveManager() { + } + + QSharedPointer ProfileManager::LoadProxyEntity(const QString &jsonPath) { + // Load type + ProxyEntity ent0(nullptr, nullptr); + ent0.fn = jsonPath; + auto validJson = ent0.Load(); + auto type = ent0.type; + + // Load content + QSharedPointer ent; + bool validType = validJson; + + if (validType) { + ent = NewProxyEntity(type); + validType = ent->bean->version != -114514; + } + + if (validType) { + // 加载前设置好 fn + ent->load_control_force = true; + ent->fn = jsonPath; + ent->Load(); + return ent; + } else { + // 返回一个假的? + ent->bean->name = "[Load Error]"; + return ent; + } + } + + // 新建的不给 fn 和 id + + QSharedPointer ProfileManager::NewProxyEntity(const QString &type) { + fmt::AbstractBean *bean; + + if (type == "socks") { + bean = new fmt::SocksHttpBean(NekoRay::fmt::SocksHttpBean::type_Socks5); + } else if (type == "http") { + bean = new fmt::SocksHttpBean(NekoRay::fmt::SocksHttpBean::type_HTTP); + } else if (type == "shadowsocks") { + bean = new fmt::ShadowSocksBean(); + } else if (type == "chain") { + bean = new fmt::ChainBean(); + } else if (type == "vmess") { + bean = new fmt::VMessBean(); + } else if (type == "trojan") { + bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_Trojan); + } else if (type == "vless") { + bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_VLESS); + } else if (type == "naive") { + bean = new fmt::NaiveBean(); + } else if (type == "custom") { + bean = new fmt::CustomBean(); + } else { + bean = new fmt::AbstractBean(-114514); + } + + auto ent = QSharedPointer(new ProxyEntity(bean, type)); + return ent; + } + + QSharedPointer ProfileManager::NewGroup() { + auto ent = QSharedPointer(new Group()); + return ent; + } + + // ProxyEntity + + ProxyEntity::ProxyEntity(fmt::AbstractBean *bean, QString _type) { + type = std::move(_type); + _add(new configItem("type", &type, itemType::string)); + _add(new configItem("id", &id, itemType::integer)); + _add(new configItem("gid", &gid, itemType::integer)); + + // 可以不关联 bean,只加载 ProxyEntity 的信息 + if (bean != nullptr) { + this->bean = QSharedPointer(bean); + // 有虚函数就要在这里 dynamic_cast + _add(new configItem("bean", dynamic_cast(bean), itemType::jsonStore)); + _add(new configItem("traffic", dynamic_cast(traffic_data.get()), itemType::jsonStore)); + } + }; + + QString ProxyEntity::DisplayLatency() const { + if (latency < 0) { + return QObject::tr("Unavailable"); + } else if (latency > 0) { + return QString("%1 ms").arg(latency); + } else { + return ""; + } + } + + // Profile + + int ProfileManager::NewProfileID() const { + if (profiles.empty()) { return 0; } else { return profiles.lastKey() + 1; } + } + + bool ProfileManager::AddProfile(const QSharedPointer &ent, int gid) { + if (ent->id >= 0) { + return false; + } + + ent->gid = gid < 0 ? dataStore->current_group : gid; + ent->id = NewProfileID(); + profiles[ent->id] = ent; + _profiles.push_back(ent->id); + Save(); + + ent->fn = QString("profiles/%1.json").arg(ent->id); + ent->Save(); + return true; + } + + void ProfileManager::DeleteProfile(int id) { + if (id < 0) return; + if (dataStore->started_id == id) return; + profiles.remove(id); + _profiles.removeAll(id); + Save(); + QFile(QString("profiles/%1.json").arg(id)).remove(); + } + + void ProfileManager::MoveProfile(const QSharedPointer &ent, int gid) { + if (gid == ent->gid || gid < 0) return; + auto oldGroup = GetGroup(ent->gid); + if (oldGroup != nullptr && !oldGroup->order.isEmpty()) { + oldGroup->order.removeAll(ent->id); + oldGroup->Save(); + } + auto newGroup = GetGroup(gid); + if (newGroup != nullptr && !newGroup->order.isEmpty()) { + newGroup->order.push_back(ent->id); + newGroup->Save(); + } + ent->gid = gid; + ent->Save(); + } + + QSharedPointer ProfileManager::GetProfile(int id) { + if (profiles.contains(id)) { + return profiles[id]; + } + return nullptr; + } + + //Group + + Group::Group() { + _add(new configItem("id", &id, itemType::integer)); + _add(new configItem("archive", &archive, itemType::boolean)); + _add(new configItem("name", &name, itemType::string)); + _add(new configItem("order", &order, itemType::integerList)); + _add(new configItem("url", &url, itemType::string)); + _add(new configItem("info", &info, itemType::string)); + } + + QSharedPointer ProfileManager::LoadGroup(const QString &jsonPath) { + QSharedPointer ent = QSharedPointer(new Group()); + ent->fn = jsonPath; + ent->Load(); + return ent; + } + + int ProfileManager::NewGroupID() const { + if (groups.empty()) { return 0; } else { return groups.lastKey() + 1; } + } + + bool ProfileManager::AddGroup(const QSharedPointer &ent) { + if (ent->id >= 0) { + return false; + } + + ent->id = NewGroupID(); + groups[ent->id] = ent; + _groups.push_back(ent->id); + Save(); + + ent->fn = QString("groups/%1.json").arg(ent->id); + ent->Save(); + return true; + } + + void ProfileManager::DeleteGroup(int gid) { + if (groups.count() == 1) return; + QList toDelete; + for (const auto &profile: profiles) { + if (profile->gid == gid) toDelete += profile->id; // map访问中,不能操作 + } + for (const auto &id: toDelete) { + DeleteProfile(id); + } + groups.remove(gid); + _groups.removeAll(gid); + Save(); + QFile(QString("groups/%1.json").arg(gid)).remove(); + } + + QSharedPointer ProfileManager::GetGroup(int id) { + if (groups.contains(id)) { + return groups[id]; + } + return nullptr; + } + + QSharedPointer ProfileManager::CurrentGroup() { + return GetGroup(NekoRay::dataStore->current_group); + } + + QList> Group::Profiles() const { + QList> ret; + for (const auto &ent: profileManager->profiles) { + if (id == ent->gid) ret += ent; + } + return ret; + } + + QList> Group::ProfilesWithOrder() const { + if (order.isEmpty()) { + return Profiles(); + } else { + QList> ret; + for (auto _id: order) { + auto ent = profileManager->GetProfile(_id); + if (ent != nullptr) ret += ent; + } + return ret; + } + } + +} \ No newline at end of file diff --git a/db/Database.hpp b/db/Database.hpp new file mode 100644 index 0000000..64d8336 --- /dev/null +++ b/db/Database.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "ProxyEntity.hpp" +#include "Group.hpp" + +namespace NekoRay { + class ProfileManager : public JsonStore { + public: + QMap> profiles; + QMap> groups; + + // JSON + QList _profiles; + QList _groups; // with order + + ProfileManager(); + + [[nodiscard]] static QSharedPointer NewProxyEntity(const QString &type); + + [[nodiscard]] static QSharedPointer NewGroup(); + + bool AddProfile(const QSharedPointer &ent, int gid = -1); + + void DeleteProfile(int id); + + void MoveProfile(const QSharedPointer &ent, int gid); + + QSharedPointer GetProfile(int id); + + bool AddGroup(const QSharedPointer &ent); + + void DeleteGroup(int gid); + + QSharedPointer GetGroup(int id); + + QSharedPointer CurrentGroup(); + + private: + void LoadManager(); + + void SaveManager(); + + [[nodiscard]] int NewProfileID() const; + + [[nodiscard]] int NewGroupID() const; + + static QSharedPointer LoadProxyEntity(const QString &jsonPath); + + static QSharedPointer LoadGroup(const QString &jsonPath); + }; + + extern ProfileManager *profileManager; +} diff --git a/db/Group.hpp b/db/Group.hpp new file mode 100644 index 0000000..7de92c3 --- /dev/null +++ b/db/Group.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "ProxyEntity.hpp" + +namespace NekoRay { + class Group : public JsonStore { + public: + int id = -1; + bool archive = false; + QString name = ""; + QList order; + QString url = ""; + QString info = ""; + + Group(); + + // 按 id 顺序 + [[nodiscard]] QList> Profiles() const; + + // 按 显示 顺序 + [[nodiscard]] QList> ProfilesWithOrder() const; + }; +} diff --git a/db/ProfileFilter.cpp b/db/ProfileFilter.cpp new file mode 100644 index 0000000..84383c9 --- /dev/null +++ b/db/ProfileFilter.cpp @@ -0,0 +1,77 @@ +#include "ProfileFilter.hpp" + +namespace NekoRay { + void ProfileFilter::Uniq(const QList> &in, + QList> &out, + bool by_address, bool keep_last) { + QMap> hashMap; + + for (const auto &ent: in) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (hashMap.contains(key)) { + if (keep_last) { + out.removeAll(hashMap[key]); + hashMap[key] = ent; + out += ent; + } + } else { + hashMap[key] = ent; + out += ent; + } + } + } + + void + ProfileFilter::Common(const QList> &src, + const QList> &dst, + QList> &out, + bool by_address, bool keep_last) { + QMap> hashMap; + + for (const auto &ent: src) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + hashMap[key] = ent; + } + for (const auto &ent: dst) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (hashMap.contains(key)) { + if (keep_last) { + out += ent; + } else { + out += hashMap[key]; + } + } + } + } + + void ProfileFilter::OnlyInSrc(const QList> &src, + const QList> &dst, + QList> &out, + bool by_address) { + QMap hashMap; + + for (const auto &ent: dst) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + hashMap[key] = true; + } + for (const auto &ent: src) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (!hashMap.contains(key)) out += ent; + } + } + + void + ProfileFilter::OnlyInSrc_ByPointer(const QList> &src, + const QList> &dst, + QList> &out) { + for (const auto &ent: src) { + if (!dst.contains(ent)) out += ent; + } + } + +} \ No newline at end of file diff --git a/db/ProfileFilter.hpp b/db/ProfileFilter.hpp new file mode 100644 index 0000000..d860a27 --- /dev/null +++ b/db/ProfileFilter.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "ProxyEntity.hpp" + +namespace NekoRay { + class ProfileFilter { + public: + static void Uniq( + const QList> &in, + QList> &out, + bool by_address = false, //def by bean + bool keep_last = false //def keep first + ); + + static void Common( + const QList> &src, + const QList> &dst, + QList> &out, + bool by_address = false, //def by bean + bool keep_last = false //def keep first + ); + + static void OnlyInSrc( + const QList> &src, + const QList> &dst, + QList> &out, + bool by_address = false //def by bean + ); + + static void OnlyInSrc_ByPointer( + const QList> &src, + const QList> &dst, + QList> &out + ); + }; +} diff --git a/db/ProxyEntity.hpp b/db/ProxyEntity.hpp new file mode 100644 index 0000000..ecb4c63 --- /dev/null +++ b/db/ProxyEntity.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "TrafficData.hpp" +#include "fmt/AbstractBean.hpp" + +namespace NekoRay { + namespace fmt { + class SocksHttpBean; + + class ShadowSocksBean; + + class VMessBean; + + class TrojanVLESSBean; + + class NaiveBean; + + class CustomBean; + + class ChainBean; + }; + + class ProxyEntity : public JsonStore { + public: + QString type; + + int id = -1; + int gid = 0; + QSharedPointer bean; + QSharedPointer traffic_data = QSharedPointer( + new traffic::TrafficData("")); + + // Cache + int latency = 0; + QString full_test_report; + + ProxyEntity(fmt::AbstractBean *bean, QString _type); + + [[nodiscard]] QString DisplayLatency() const; + + [[nodiscard]] fmt::ChainBean *ChainBean() const { + return (fmt::ChainBean *) bean.get(); + }; + + [[nodiscard]] fmt::SocksHttpBean *SocksHTTPBean() const { + return (fmt::SocksHttpBean *) bean.get(); + }; + + [[nodiscard]] fmt::ShadowSocksBean *ShadowSocksBean() const { + return (fmt::ShadowSocksBean *) bean.get(); + }; + + [[nodiscard]] fmt::VMessBean *VMessBean() const { + return (fmt::VMessBean *) bean.get(); + }; + + [[nodiscard]] fmt::TrojanVLESSBean *TrojanVLESSBean() const { + return (fmt::TrojanVLESSBean *) bean.get(); + }; + + [[nodiscard]] fmt::NaiveBean *NaiveBean() const { + return (fmt::NaiveBean *) bean.get(); + }; + + [[nodiscard]] fmt::CustomBean *CustomBean() const { + return (fmt::CustomBean *) bean.get(); + }; + + }; +} diff --git a/db/TrafficData.hpp b/db/TrafficData.hpp new file mode 100644 index 0000000..2b3b9ed --- /dev/null +++ b/db/TrafficData.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::traffic { + class TrafficData : public JsonStore { + public: + int id = -1; // ent id + std::string tag; + + long long downlink = 0; + long long uplink = 0; + long long downlink_rate = 0; + long long uplink_rate = 0; + + explicit TrafficData(std::string tag) { + this->tag = std::move(tag); + _add(new configItem("dl", &downlink, itemType::integer64)); + _add(new configItem("ul", &uplink, itemType::integer64)); + }; + + void Reset() { + downlink = 0; + uplink = 0; + downlink_rate = 0; + uplink_rate = 0; + } + + [[nodiscard]] QString DisplaySpeed() const { + return QString("%1↑ %2↓").arg(ReadableSize(uplink_rate), ReadableSize(downlink_rate)); + } + + [[nodiscard]] QString DisplayTraffic() const { + if (downlink + uplink == 0) return ""; + return QString("%1↑ %2↓").arg(ReadableSize(uplink), ReadableSize(downlink)); + } + }; +} diff --git a/db/TrafficLooper.cpp b/db/TrafficLooper.cpp new file mode 100644 index 0000000..c5baf05 --- /dev/null +++ b/db/TrafficLooper.cpp @@ -0,0 +1,120 @@ +#include "TrafficLooper.hpp" + +#include "rpc/gRPC.h" +#include "ui/mainwindow.h" + +#include + +namespace NekoRay::traffic { + + TrafficLooper *trafficLooper = new TrafficLooper; + + std::unique_ptr TrafficLooper::update_stats(TrafficData *item) { +#ifndef NKR_NO_GRPC + auto uplink = NekoRay::rpc::defaultClient->QueryStats(item->tag, "uplink"); + auto downlink = NekoRay::rpc::defaultClient->QueryStats(item->tag, "downlink"); + + item->downlink += downlink; + item->uplink += uplink; + + //? + item->downlink_rate = downlink * 1000 / dataStore->traffic_loop_interval; + item->uplink_rate = uplink * 1000 / dataStore->traffic_loop_interval; + + // return diff + auto ret = std::make_unique(item->tag); + ret->downlink = downlink; + ret->uplink = uplink; + ret->downlink_rate = item->downlink_rate; + ret->uplink_rate = item->uplink_rate; + return ret; +#endif + return nullptr; + } + + QJsonArray TrafficLooper::get_connection_list() { +#ifndef NKR_NO_GRPC + auto str = NekoRay::rpc::defaultClient->ListV2rayConnections(); + QJsonDocument jsonDocument = QJsonDocument::fromJson(str.c_str()); + return jsonDocument.array(); +#else + return QJsonArray{}; +#endif + } + + void TrafficLooper::update_all() { + std::map> updated; // tag to diff + for (const auto &item: items) { + auto data = item.get(); + auto diff = std::move(updated[data->tag]); + // 避免重复查询一个 outbound tag + if (diff == nullptr) { + diff = update_stats(data); + updated[data->tag] = std::move(diff); + } else { + data->uplink += diff->uplink; + data->downlink += diff->downlink; + data->uplink_rate = diff->uplink_rate; + data->downlink_rate = diff->downlink_rate; + } + } + update_stats(bypass); + } + + [[noreturn]] void TrafficLooper::loop() { + while (true) { + auto sleep_ms = dataStore->traffic_loop_interval; + auto user_disabled = sleep_ms == 0; + if (sleep_ms < 500 || sleep_ms > 2000) sleep_ms = 1000; + QThread::msleep(sleep_ms); + if (user_disabled) continue; + + if (!loop_enabled) { + // 停止 + if (looping) { + looping = false; + runOnUiThread([=] { + auto m = GetMainWindow(); + m->refresh_status("STOP"); + }); + } + continue; + } else { + //开始 + if (!looping) { + looping = true; + } + } + + // do update + loop_mutex.lock(); + + update_all(); + + // do conn list update + QJsonArray conn_list; + if (dataStore->connection_statistics) { + conn_list = get_connection_list(); + } + + loop_mutex.unlock(); + + // post to UI + runOnUiThread([=] { + auto m = GetMainWindow(); + if (proxy != nullptr) { + m->refresh_status( + QObject::tr("Proxy: %1\nDirect: %2").arg(proxy->DisplaySpeed(), bypass->DisplaySpeed())); + } + for (const auto &item: items) { + if (item->id < 0) continue; + m->refresh_proxy_list(item->id); + } + if (dataStore->connection_statistics) { + m->refresh_connection_list(conn_list); + } + }); + } + } + +} diff --git a/db/TrafficLooper.hpp b/db/TrafficLooper.hpp new file mode 100644 index 0000000..0002171 --- /dev/null +++ b/db/TrafficLooper.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include "TrafficData.hpp" + +namespace NekoRay::traffic { + class TrafficLooper { + public: + bool loop_enabled = false; + bool looping = false; + QMutex loop_mutex; + + QList> items; + TrafficData *bypass = new TrafficData("bypass"); + TrafficData *proxy = nullptr; + + static std::unique_ptr update_stats(TrafficData *item); + + static QJsonArray get_connection_list(); + + void update_all(); + + [[noreturn]] void loop(); + }; + + extern TrafficLooper *trafficLooper; +} + diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..02f9235 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +tun2socks diff --git a/examples/build-alpine.md b/examples/build-alpine.md new file mode 100644 index 0000000..0856d66 --- /dev/null +++ b/examples/build-alpine.md @@ -0,0 +1,5 @@ +alpine 3.16 + +all use package + +apk add git cmake g++ ninja zxing-cpp-dev yaml-cpp-dev grpc-dev protobuf-dev qt5-qtbase-dev qt5-qtsvg-dev qt5-qttools-dev qt5-qtx11extras-dev c-ares-dev re2-dev diff --git a/examples/netns-root.sh b/examples/netns-root.sh new file mode 100755 index 0000000..2c78086 --- /dev/null +++ b/examples/netns-root.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit +fi + +# add netns +ip netns add nekoray +# ip netns exec nekoray readlink /proc/self/ns/net + +# add lo: lo is not shared +ip -n nekoray addr add 127.0.0.1/8 dev lo +ip -n nekoray link set dev lo up + +# add tun +ip -n nekoray tuntap add tun0 user $USERID mode tun +ip -n nekoray addr add 26.0.0.1/30 dev tun0 +ip -n nekoray link set dev tun0 up +ip -n nekoray route add default dev tun0 + +# set veth to use the socks port +ip link add dev nekoray-ve1 type veth peer name nekoray-ve2 +ip addr add 26.1.0.1/30 dev nekoray-ve1 +ip link set nekoray-ve1 up +ip link set nekoray-ve2 netns nekoray +ip -n nekoray addr add 26.1.0.2/30 dev nekoray-ve2 +ip -n nekoray link set nekoray-ve2 up diff --git a/examples/netns.sh b/examples/netns.sh new file mode 100755 index 0000000..8ae51af --- /dev/null +++ b/examples/netns.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e +set -x + +BASEDIR=$(dirname "$0") + +# netns +[ -f /var/run/netns/nekoray ] || pkexec env USERID=`id -u` sh -c "cd $PWD && $BASEDIR/netns-root.sh" || true + +# run xjasonlyu/tun2socks to provide vpn +firejail --noprofile --netns=nekoray ./tun2socks -device tun0 -proxy socks5://26.1.0.1:2080 -interface nekoray-ve2 -drop-multicast + +# use "firejail --noprofile --netns=nekoray ..." to run your program in VPN diff --git a/examples/readme.txt b/examples/readme.txt new file mode 100644 index 0000000..0af9227 --- /dev/null +++ b/examples/readme.txt @@ -0,0 +1,9 @@ +Linux Only + +此处为配置 VPN 的脚本,仅供参考,使用时要按实际情况替换某些参数(如 socks 端口) + +vpn.sh 配置全局 VPN +ctrl-c 退出后自动删除 VPN + +vpn-netns.sh 配置 netns +分应用代理,用法参考脚本内容 diff --git a/examples/set-cap.sh b/examples/set-cap.sh new file mode 100755 index 0000000..195f26e --- /dev/null +++ b/examples/set-cap.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ] + then echo "Please run as root" + exit +fi + +killall nekoray_core || true +cp nekoray_core /opt/nekoray_core +cp geo* /opt/ +setcap cap_net_admin+ep /opt/nekoray_core +ln -sf /opt/nekoray_core nekoray_core_cap diff --git a/examples/sing-box-vpn.json b/examples/sing-box-vpn.json new file mode 100644 index 0000000..20ee12f --- /dev/null +++ b/examples/sing-box-vpn.json @@ -0,0 +1,43 @@ +{ + "dns": { + "servers": [], + "rules": [], + "strategy": "ipv4_only" + }, + "inbounds": [ + { + "type": "tun", + "interface_name": "nekoray-tun", + "inet4_address": "172.19.0.1/30", + "auto_route": true, + "sniff": false + } + ], + "outbounds": [ + { + "type": "socks", + "tag": "nekoray-socks", + "server": "127.0.0.1", + "server_port": %PORT% + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "network": "udp", + "port": [ + 135, + 137, + 138, + 139, + 5353 + ], + "outbound": "block" + } + ] + } +} \ No newline at end of file diff --git a/examples/vpn-run-root.sh b/examples/vpn-run-root.sh new file mode 100755 index 0000000..dcb9033 --- /dev/null +++ b/examples/vpn-run-root.sh @@ -0,0 +1,71 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit +fi + +[ -z $PORT ] && echo "Please set env PORT" && exit +[ -z $TABLE_FWMARK ] && echo "Please set env TABLE_FWMARK" && exit +[ -z $TUN_NAME ] && echo "Please set env TUN_NAME" && exit +[ -z $USER_ID ] && echo "Please set env USER_ID" && exit +command -v pkill >/dev/null 2>&1 || exit + +BASEDIR=$(dirname "$0") +cd $BASEDIR + +start() { + # add tun (TODO the ip must be the same as matsuri) + ip tuntap add $TUN_NAME mode tun user $USER_ID || return + ip addr add 172.19.0.1/30 dev $TUN_NAME || return + ip link set dev $TUN_NAME up || return + + # set ipv4 rule + ip rule add table $TABLE_FWMARK || return + ip route add table $TABLE_FWMARK default dev $TUN_NAME || return + + # set ipv6 unreachable + ip -6 rule add table $TABLE_FWMARK || return + ip -6 route add table $TABLE_FWMARK unreachable default || return + + # set bypass: fwmark + ip rule add fwmark $TABLE_FWMARK table main || return + ip -6 rule add fwmark $TABLE_FWMARK table main || return + + # set bypass: LAN + for local in $BYPASS_IPS; do + ip rule add to $local table main + done + + if [ ! -z $USE_NEKORAY ]; then + "./nekoray_core" tool protect --protect-listen-path "$PROTECT_LISTEN_PATH" --protect-fwmark $TABLE_FWMARK + else + if [ -z "$PROTECT_LISTEN_PATH" ]; then + "./tun2socks" -device $TUN_NAME -proxy socks5://127.0.0.1:$PORT -interface lo + else + "./tun2socks" -device $TUN_NAME -proxy socks5://127.0.0.1:$PORT -interface lo --protect-listen-path "$PROTECT_LISTEN_PATH" --protect-fwmark $TABLE_FWMARK + rm "$PROTECT_LISTEN_PATH" + fi + fi +} + +stop() { + for local in $BYPASS_IPS; do + ip rule del to $local table main + done + ip rule del table $TABLE_FWMARK + ip rule del fwmark $TABLE_FWMARK + ip route del table $TABLE_FWMARK default + ip -6 rule del table $TABLE_FWMARK + ip -6 rule del fwmark $TABLE_FWMARK + ip -6 route del table $TABLE_FWMARK default + ip link del $TUN_NAME +} + +if [ "$1" != "stop" ]; then + start || true +fi + +stop || true diff --git a/fmt/AbstractBean.cpp b/fmt/AbstractBean.cpp new file mode 100644 index 0000000..047ce90 --- /dev/null +++ b/fmt/AbstractBean.cpp @@ -0,0 +1,36 @@ +#include "AbstractBean.hpp" + +namespace NekoRay::fmt { + AbstractBean::AbstractBean(int version) { + this->version = version; + _add(new configItem("_v", &this->version, itemType::integer)); + _add(new configItem("name", &name, itemType::string)); + _add(new configItem("addr", &serverAddress, itemType::string)); + _add(new configItem("port", &serverPort, itemType::integer)); + } + + QString AbstractBean::ToNekorayShareLink(const QString &type) { + auto b = ToJson(); + QUrl url; + url.setScheme("nekoray"); + url.setHost(type); + url.setFragment(QJsonObject2QString(b, true) + .toUtf8().toBase64(QByteArray::Base64UrlEncoding)); + return url.toString(); + } + + QString AbstractBean::DisplayAddress() { + return ::DisplayAddress(serverAddress, serverPort); + } + + QString AbstractBean::DisplayName() { + if (name.isEmpty()) { + return DisplayAddress(); + } + return name; + } + + QString AbstractBean::DisplayTypeAndName() { + return QString(" [%1] %2").arg(DisplayType(), DisplayName()); + } +} \ No newline at end of file diff --git a/fmt/AbstractBean.hpp b/fmt/AbstractBean.hpp new file mode 100644 index 0000000..c9373b4 --- /dev/null +++ b/fmt/AbstractBean.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::fmt { + + struct CoreObjOutboundBuildResult { + public: + QJsonObject outbound; + QString error; + }; + + struct ExternalBuildResult { + public: + QString program; + QStringList env; + QStringList arguments; + QString error; + }; + + class AbstractBean : public JsonStore { + public: + int version; + QString name = ""; + QString serverAddress = "127.0.0.1"; + int serverPort = 1080; + + explicit AbstractBean(int version); + + QString ToNekorayShareLink(const QString &type); + + [[nodiscard]] virtual QString DisplayAddress(); + + [[nodiscard]] virtual QString DisplayName(); + + virtual QString DisplayType() { return {}; }; + + virtual QString DisplayTypeAndName(); + + virtual bool NeedExternal() { return false; }; + + virtual CoreObjOutboundBuildResult BuildCoreObj() { return {}; }; + + virtual ExternalBuildResult BuildExternal(int mapping_port, int socks_port) { return {}; }; + + virtual QString ToShareLink() { return {}; }; + + virtual QString InsecureHint() { return {}; }; + + }; + + QString DisplayInsecureHint(const QSharedPointer &); + +} diff --git a/fmt/Bean2CoreObj.cpp b/fmt/Bean2CoreObj.cpp new file mode 100644 index 0000000..956c5a1 --- /dev/null +++ b/fmt/Bean2CoreObj.cpp @@ -0,0 +1,175 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#define MAKE_SETTINGS_STREAM_SETTINGS \ +if (!stream->packet_encoding.isEmpty()) settings["packetEncoding"] = stream->packet_encoding; \ +outbound["settings"] = settings; \ +auto streamSettings = stream->BuildStreamSettings(); \ +outbound["streamSettings"] = streamSettings; + +namespace NekoRay::fmt { + + QJsonObject V2rayStreamSettings::BuildStreamSettings() { + QJsonObject streamSettings{ + {"network", network}, + {"security", security}, + }; + + if (network == "ws") { + QJsonObject ws; + if (!path.isEmpty()) ws["path"] = path; + if (!host.isEmpty()) ws["headers"] = QJsonObject{{"Host", host}}; + streamSettings["wsSettings"] = ws; + } else if (network == "h2") { + QJsonObject h2; + if (!path.isEmpty()) h2["path"] = path; + if (!host.isEmpty()) h2["host"] = QList2QJsonArray(host.split(",")); + streamSettings["httpSettings"] = h2; + } else if (network == "grpc") { + QJsonObject grpc; + if (!path.isEmpty()) grpc["serviceName"] = path; + streamSettings["grpcSettings"] = grpc; + } + + if (security == "tls") { + QJsonObject tls; + if (!sni.isEmpty()) tls["serverName"] = sni; + if (allow_insecure || dataStore->skip_cert) tls["allowInsecure"] = true; + if (!certificate.isEmpty()) + tls["certificates"] = QJsonArray{ + QJsonObject{ + {"certificate", certificate}, + }, + }; + streamSettings["tlsSettings"] = tls; + } + + return streamSettings; + } + + CoreObjOutboundBuildResult SocksHttpBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + + QJsonObject outbound; + outbound["protocol"] = socks_http_type == type_HTTP ? "http" : "socks"; + + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + + server["address"] = serverAddress; + server["port"] = serverPort; + + QJsonArray users; + QJsonObject user; + user["user"] = username; + user["pass"] = password; + users.push_back(user); + if (!username.isEmpty() && !password.isEmpty()) server["users"] = users; + + servers.push_back(server); + settings["servers"] = servers; + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult ShadowSocksBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + + QJsonObject outbound; + outbound["protocol"] = "shadowsocks"; + + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + + server["address"] = serverAddress; + server["port"] = serverPort; + server["method"] = method; + server["password"] = password; + + servers.push_back(server); + settings["servers"] = servers; + + if (!plugin.isEmpty()) { + settings["plugin"] = SubStrBefore(plugin, ";"); + settings["pluginOpts"] = SubStrAfter(plugin, ";"); + } + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult VMessBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + QJsonObject outbound{ + {"protocol", "vmess"}, + }; + + QJsonObject settings{ + {"vnext", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"users", QJsonArray{ + QJsonObject{ + {"id", uuid}, + {"alterId", aid}, + {"security", security}, + } + }}, + } + }} + }; + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult TrojanVLESSBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + QJsonObject outbound{ + {"protocol", proxy_type == proxy_VLESS ? "vless" : "trojan"}, + }; + + QJsonObject settings; + if (proxy_type == proxy_VLESS) { + settings = QJsonObject{ + {"vnext", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"users", QJsonArray{ + QJsonObject{ + {"id", password}, + {"encryption", "none"}, + } + }}, + } + }} + }; + } else { + settings = QJsonObject{ + {"servers", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"password", password}, + } + }} + }; + } + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } +} \ No newline at end of file diff --git a/fmt/Bean2External.cpp b/fmt/Bean2External.cpp new file mode 100644 index 0000000..e7516e7 --- /dev/null +++ b/fmt/Bean2External.cpp @@ -0,0 +1,81 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include +#include +#include + +#define WriteTempFile(fn, data) \ +QDir dir; \ +if (!dir.exists("temp")) dir.mkdir("temp"); \ +QFile f(QString("temp/") + fn); \ +bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); \ +if (ok) { \ +f.write(data); \ +} else { \ +result.error = f.errorString(); \ +} \ +f.close(); \ +auto TempFile = QFileInfo(f).absoluteFilePath(); + +namespace NekoRay::fmt { + ExternalBuildResult NaiveBean::BuildExternal(int mapping_port, int socks_port) { + ExternalBuildResult result{dataStore->extraCore->Get("naive")}; + if (result.program.isEmpty()) { + result.error = QObject::tr("Core not found: %1").arg(DisplayType()); + return result; + } + + auto _serverAddress = sni.isEmpty() ? serverAddress : sni; + + result.arguments += "--log"; + result.arguments += "--listen=socks://127.0.0.1:" + Int2String(socks_port); + result.arguments += "--proxy=" + protocol + "://" + + username + ":" + password + "@" + + _serverAddress + ":" + Int2String(mapping_port); + result.arguments += "--host-resolver-rules=MAP " + _serverAddress + " 127.0.0.1"; + if (insecure_concurrency > 0) result.arguments += "--insecure-concurrency=" + Int2String(insecure_concurrency); + if (!extra_headers.isEmpty()) result.arguments += "--extra-headers=" + extra_headers; + if (!certificate.isEmpty()) { + WriteTempFile("naive_" + GetRandomString(10) + ".crt", certificate.toUtf8()); + result.env += "SSL_CERT_FILE=" + TempFile; + } + + return result; + } + + ExternalBuildResult CustomBean::BuildExternal(int mapping_port, int socks_port) { + ExternalBuildResult result{dataStore->extraCore->Get(core)}; + if (result.program.isEmpty()) { + result.error = QObject::tr("Core not found: %1").arg(DisplayType()); + return result; + } + + result.arguments = command; // TODO split? + + for (int i = 0; i < result.arguments.length(); i++) { + auto arg = result.arguments[i]; + if (arg.contains("%mapping_port%")) { + arg = arg.replace("%mapping_port%", Int2String(mapping_port)); + } else if (arg.contains("%socks_port%")) { + arg = arg.replace("%socks_port%", Int2String(socks_port)); + } else { + continue; + } + result.arguments[i] = arg; + } + + if (!config_simple.trimmed().isEmpty()) { + auto config = config_simple; + config = config.replace("%mapping_port%", Int2String(mapping_port)); + config = config.replace("%socks_port%", Int2String(socks_port)); + + WriteTempFile("custom_cfg_" + GetRandomString(10) + ".tmp", config.toUtf8()); + for (int i = 0; i < result.arguments.count(); i++) { + result.arguments[i] = result.arguments[i].replace("%config%", TempFile); + } + } + + return result; + } +} \ No newline at end of file diff --git a/fmt/Bean2Link.cpp b/fmt/Bean2Link.cpp new file mode 100644 index 0000000..d15a0b2 --- /dev/null +++ b/fmt/Bean2Link.cpp @@ -0,0 +1,94 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include + +namespace NekoRay::fmt { + QString SocksHttpBean::ToShareLink() { + QUrl url; + if (socks_http_type == type_HTTP) { // http + if (stream->security == "tls") { + url.setScheme("https"); + } else { + url.setScheme("http"); + } + } else { + url.setScheme(QString("socks%1").arg(socks_http_type)); + } + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + if (!username.isEmpty()) url.setUserName(username); + if (!password.isEmpty()) url.setPassword(password); + url.setHost(serverAddress); + url.setPort(serverPort); + return url.toString(); + } + + QString TrojanVLESSBean::ToShareLink() { + QUrl url; + QUrlQuery query; + url.setScheme(proxy_type == proxy_VLESS ? "vless" : "trojan"); + url.setUserName(password); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + if (!stream->sni.isEmpty()) query.addQueryItem("sni", stream->sni); + query.addQueryItem("security", "tls"); + query.addQueryItem("type", stream->network.replace("h2", "http")); + + if (stream->network == "ws" || stream->network == "h2") { + if (!stream->path.isEmpty()) query.addQueryItem("path", stream->path); + if (!stream->host.isEmpty()) query.addQueryItem("host", stream->host); + } else if (stream->network == "grpc") { + if (!stream->path.isEmpty()) query.addQueryItem("serviceName", stream->path); + } + + url.setQuery(query); + return url.toString(); + } + + QString ShadowSocksBean::ToShareLink() { + QUrl url; + url.setScheme("ss"); + auto username = method + ":" + password; + url.setUserName(username.toUtf8().toBase64(QByteArray::Base64Option::Base64UrlEncoding)); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + QUrlQuery q; + if (!plugin.isEmpty()) q.addQueryItem("plugin", plugin); + if (!q.isEmpty()) url.setQuery(q); + return url.toString(); + } + + QString VMessBean::ToShareLink() { + QJsonObject N{ + {"v", 2}, + {"ps", name}, + {"add", serverAddress}, + {"port", serverPort}, + {"id", uuid}, + {"aid", aid}, + {"net", stream->network}, + {"host", stream->host}, + {"path", stream->path}, + {"type", stream->header_type}, + {"scy", security}, + // TODO header type + {"tls", stream->security == "tls" ? "tls" : ""}, + {"sni", stream->sni}, + }; + return "vmess://" + QJsonObject2QString(N, false).toUtf8().toBase64(); + } + + QString NaiveBean::ToShareLink() { + QUrl url; + url.setScheme("https+naive"); + url.setUserName(username); + url.setPassword(password); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + return url.toString(); + } + +} \ No newline at end of file diff --git a/fmt/ChainBean.hpp b/fmt/ChainBean.hpp new file mode 100644 index 0000000..1eef702 --- /dev/null +++ b/fmt/ChainBean.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::fmt { + class ChainBean : public AbstractBean { + public: + QList list; // in to out + + ChainBean() : AbstractBean(0) { + _add(new configItem("list", &list, itemType::integerList)); + }; + + QString DisplayType() override { return QObject::tr("Chain Proxy"); }; + + QString DisplayAddress() override { return ""; }; + }; +} diff --git a/fmt/CustomBean.hpp b/fmt/CustomBean.hpp new file mode 100644 index 0000000..e3239d7 --- /dev/null +++ b/fmt/CustomBean.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoRay::fmt { + class CustomBean : public AbstractBean { + public: + QString core; + QList command; +// QString config_map; // map: fn to text + QString config_simple; + + CustomBean() : AbstractBean(0) { + _add(new configItem("core", &core, itemType::string)); + _add(new configItem("cmd", &command, itemType::stringList)); +// _add(new configItem("cm", &config_map, itemType::string)); + _add(new configItem("cs", &config_simple, itemType::string)); + }; + + QString DisplayType() override { return core; }; + + bool NeedExternal() override { return true; }; + + ExternalBuildResult BuildExternal(int mapping_port, int socks_port) override; + }; +} \ No newline at end of file diff --git a/fmt/InsecureHint.cpp b/fmt/InsecureHint.cpp new file mode 100644 index 0000000..82703dc --- /dev/null +++ b/fmt/InsecureHint.cpp @@ -0,0 +1,67 @@ +#include "V2RayStreamSettings.hpp" +#include "ShadowSocksBean.hpp" +#include "VMessBean.hpp" +#include "TrojanVLESSBean.hpp" +#include "SocksHttpBean.hpp" + +namespace NekoRay::fmt { + QString DisplayInsecureHint(const QSharedPointer &bean) { + if (!dataStore->insecure_hint) return {}; + auto insecure_hint = bean->InsecureHint(); + auto stream = GetStreamSettings(bean); + if (stream != nullptr) insecure_hint += "\n" + stream->InsecureHint(); + return insecure_hint.trimmed(); + } + + QString V2rayStreamSettings::InsecureHint() const { + if (allow_insecure) { + return QObject::tr( + "The configuration (insecure) can be detected and identified, the transmission is fully visible to the censor and is not resistant to man-in-the-middle tampering with the content of the communication." + ); + } + return {}; + } + + QString ShadowSocksBean::InsecureHint() { + if (method.contains("-poly") || method.contains("-gcm")) { + return {}; + } + return QObject::tr( + "This configuration (Shadowsocks streaming cipher) can be accurately proactively detected and decrypted by censors without requiring a password, and cannot be mitigated by turning on IV replay filters on the server side.\n" + "\n" + "Learn more: https://github.com/net4people/bbs/issues/24" + ); + } + + QString VMessBean::InsecureHint() { + if (security == "none" || security == "zero") { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr( + "This profile is cleartext, don't use it if the server is not in your local network."); + } + } + if (aid > 0) { + return QObject::tr( + "This configuration (VMess MD5 authentication) has been deprecated by upstream because of its questionable resistance to tampering and concealment.\n" + "\n" + "As of January 1, 2022, compatibility with MD5 authentication information will be disabled on the server side by default. Any client using MD5 authentication information will not be able to connect to a server with VMess MD5 authentication information disabled." + ); + } + return {}; + } + + QString TrojanVLESSBean::InsecureHint() { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr("This profile is cleartext, don't use it if the server is not in your local network."); + } + return {}; + } + + QString SocksHttpBean::InsecureHint() { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr("This profile is cleartext, don't use it if the server is not in your local network."); + } + return {}; + } +} + diff --git a/fmt/Link2Bean.cpp b/fmt/Link2Bean.cpp new file mode 100644 index 0000000..47abea2 --- /dev/null +++ b/fmt/Link2Bean.cpp @@ -0,0 +1,147 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include + +namespace NekoRay::fmt { + +#define DECODE_V2RAY_N_1 auto linkN = DecodeB64IfValid(SubStrBefore(SubStrAfter(link, "://"), "#"), QByteArray::Base64Option::Base64UrlEncoding); \ + if (linkN.isEmpty()) return false; \ + auto hasRemarks = link.contains("#"); \ + if (hasRemarks) linkN += "#" + SubStrAfter(link, "#"); \ + auto url = QUrl("https://" + linkN); + + bool SocksHttpBean::TryParseLink(const QString &link) { + if (!SubStrAfter(link, "://").contains(":")) { + // v2rayN shit format + DECODE_V2RAY_N_1 + + if (hasRemarks) name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + } else { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + if (link.startsWith("socks4")) socks_http_type = type_Socks4; + if (link.startsWith("http")) socks_http_type = type_HTTP; + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + if (serverPort == -1) serverPort = socks_http_type == type_HTTP ? 443 : 1080; + + stream->security = GetQueryValue(query, "security", "") == "true" ? "tls" : "none"; + stream->sni = GetQueryValue(query, "sni"); + } + return true; + } + + bool TrojanVLESSBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + password = url.userName(); + if (serverPort == -1) serverPort = 443; + + stream->network = GetQueryValue(query, "type", "tcp").replace("http", "h2"); + stream->security = GetQueryValue(query, "security", "tls"); + auto sni1 = GetQueryValue(query, "sni"); + auto sni2 = GetQueryValue(query, "peer"); + if (!sni1.isEmpty()) stream->sni = sni1; + if (!sni2.isEmpty()) stream->sni = sni2; + if (!query.queryItemValue("allowInsecure").isEmpty()) stream->allow_insecure = true; + + // TODO header kcp quic + if (stream->network == "ws") { + stream->path = GetQueryValue(query, "path", ""); + stream->host = GetQueryValue(query, "host", ""); + } else if (stream->network == "h2") { + stream->path = GetQueryValue(query, "path", ""); + stream->host = GetQueryValue(query, "host", "").replace("|", ","); + } else if (stream->network == "grpc") { + stream->path = GetQueryValue(query, "serviceName", ""); + } + + return !password.isEmpty(); + } + + bool ShadowSocksBean::TryParseLink(const QString &link) { + if (SubStrBefore(link, "#").contains("@")) { + // SS + auto url = QUrl(link); + if (!url.isValid()) return false; + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + auto method_password = DecodeB64IfValid(url.userName(), QByteArray::Base64Option::Base64UrlEncoding); + if (method_password.isEmpty()) return false; + method = SubStrBefore(method_password, ":"); + password = SubStrAfter(method_password, ":"); + auto query = GetQuery(url); + plugin = query.queryItemValue("plugin").replace("simple-obfs;", "obfs-local;"); + } else { + // v2rayN + DECODE_V2RAY_N_1 + + if (hasRemarks) name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + method = url.userName(); + password = url.password(); + } + return true; + } + + bool VMessBean::TryParseLink(const QString &link) { + // V2RayN Format + auto linkN = DecodeB64IfValid(SubStrAfter(link, "vmess://")); + if (!linkN.isEmpty()) { + auto objN = QString2QJsonObject(linkN); + if (objN.isEmpty()) return false; + // REQUIRED + uuid = objN["id"].toString(); + serverAddress = objN["add"].toString(); + serverPort = objN["port"].toVariant().toInt(); + // OPTIONAL + name = objN["ps"].toString(); + aid = objN["aid"].toInt(); + stream->host = objN["host"].toString(); + stream->path = objN["path"].toString(); + stream->sni = objN["sni"].toString(); + auto net = objN["net"].toString().replace("http", "h2"); + if (!net.isEmpty()) stream->network = net; + auto scy = objN["scy"].toString(); + if (!scy.isEmpty()) security = scy; + // TLS (XTLS?) + if (!objN["tls"].toString().isEmpty()) stream->security = "tls"; + // TODO quic & kcp + return true; + } + + // Std Format + return false; + } + + bool NaiveBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + + return !(username.isEmpty() || password.isEmpty()); + } + +} diff --git a/fmt/NaiveBean.hpp b/fmt/NaiveBean.hpp new file mode 100644 index 0000000..25a2642 --- /dev/null +++ b/fmt/NaiveBean.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoRay::fmt { + class NaiveBean : public AbstractBean { + public: + QString username = ""; + QString password = ""; + QString protocol = "https"; + QString extra_headers = ""; + QString sni = ""; + QString certificate = ""; + int insecure_concurrency = 0; + + NaiveBean() : AbstractBean(0) { + _add(new configItem("username", &username, itemType::string)); + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("protocol", &protocol, itemType::string)); + _add(new configItem("extra_headers", &extra_headers, itemType::string)); + _add(new configItem("sni", &sni, itemType::string)); + _add(new configItem("certificate", &certificate, itemType::string)); + _add(new configItem("insecure_concurrency", &insecure_concurrency, itemType::integer)); + }; + + QString DisplayType() override { return "Naive"; }; + + bool NeedExternal() override { return true; }; + + ExternalBuildResult BuildExternal(int mapping_port, int socks_port) override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + }; +} \ No newline at end of file diff --git a/fmt/ShadowSocksBean.hpp b/fmt/ShadowSocksBean.hpp new file mode 100644 index 0000000..6fef724 --- /dev/null +++ b/fmt/ShadowSocksBean.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class ShadowSocksBean : public AbstractBean { + public: + QString method = "aes-128-gcm"; + QString password = ""; + QString plugin = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + ShadowSocksBean() : AbstractBean(0) { + _add(new configItem("method", &method, itemType::string)); + _add(new configItem("pass", &password, itemType::string)); + _add(new configItem("plugin", &plugin, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return "Shadowsocks"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} diff --git a/fmt/SocksHttpBean.hpp b/fmt/SocksHttpBean.hpp new file mode 100644 index 0000000..e35f430 --- /dev/null +++ b/fmt/SocksHttpBean.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class SocksHttpBean : public AbstractBean { + public: + static constexpr int type_HTTP = -80; + static constexpr int type_Socks4 = 4; + static constexpr int type_Socks5 = 5; + + int socks_http_type = type_Socks5; + QString username = ""; + QString password = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + explicit SocksHttpBean(int _socks_http_type) : AbstractBean(0) { + this->socks_http_type = _socks_http_type; + _add(new configItem("v", &socks_http_type, itemType::integer)); + _add(new configItem("username", &username, itemType::string)); + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return socks_http_type == type_HTTP ? "HTTP" : "Socks"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} diff --git a/fmt/TrojanVLESSBean.hpp b/fmt/TrojanVLESSBean.hpp new file mode 100644 index 0000000..6d436dd --- /dev/null +++ b/fmt/TrojanVLESSBean.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class TrojanVLESSBean : public AbstractBean { + public: + static constexpr int proxy_Trojan = 0; + static constexpr int proxy_VLESS = 1; + int proxy_type = proxy_Trojan; + + QString password = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + explicit TrojanVLESSBean(int _proxy_type) : AbstractBean(0) { + proxy_type = _proxy_type; + _add(new configItem("pass", &password, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return proxy_type == proxy_VLESS ? "VLESS" : "Trojan"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} \ No newline at end of file diff --git a/fmt/V2RayStreamSettings.hpp b/fmt/V2RayStreamSettings.hpp new file mode 100644 index 0000000..023ad54 --- /dev/null +++ b/fmt/V2RayStreamSettings.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "AbstractBean.hpp" + +namespace NekoRay::fmt { + class V2rayStreamSettings : public JsonStore { + public: + QString network = "tcp"; + QString security = "none"; + QString packet_encoding = ""; + // ws/h2/grpc + QString path = ""; + QString host = ""; + // ws + int max_early_data = 0; + QString early_data_header_name = ""; + // QUIC & KCP + QString header_type = ""; + // tls + QString sni = ""; + QString certificate = ""; + bool allow_insecure = false; + + V2rayStreamSettings() : JsonStore() { + _add(new configItem("net", &network, itemType::string)); + _add(new configItem("sec", &security, itemType::string)); + _add(new configItem("pac_enc", &packet_encoding, itemType::string)); + _add(new configItem("path", &path, itemType::string)); + _add(new configItem("host", &host, itemType::string)); + _add(new configItem("sni", &sni, itemType::string)); + _add(new configItem("cert", &certificate, itemType::string)); + _add(new configItem("insecure", &allow_insecure, itemType::boolean)); + _add(new configItem("ws_med", &max_early_data, itemType::integer)); + _add(new configItem("ws_edhn", &early_data_header_name, itemType::string)); + _add(new configItem("h_type", &header_type, itemType::string)); + } + + QJsonObject BuildStreamSettings(); + + [[nodiscard]] QString InsecureHint() const; + }; + + inline V2rayStreamSettings *GetStreamSettings(const QSharedPointer &bean) { + if (bean == nullptr) return nullptr; + auto stream_item = bean->_get("stream"); + if (stream_item != nullptr) { + auto stream_store = (NekoRay::JsonStore *) stream_item->ptr; + auto stream = (NekoRay::fmt::V2rayStreamSettings *) stream_store; + return stream; + } + return nullptr; + } +} diff --git a/fmt/VMessBean.hpp b/fmt/VMessBean.hpp new file mode 100644 index 0000000..1091883 --- /dev/null +++ b/fmt/VMessBean.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class VMessBean : public AbstractBean { + public: + QString uuid = ""; + int aid = 0; + QString security = "auto"; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + VMessBean() : AbstractBean(0) { + _add(new configItem("id", &uuid, itemType::string)); + _add(new configItem("aid", &aid, itemType::integer)); + _add(new configItem("sec", &security, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return "VMess"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} + diff --git a/fmt/includes.h b/fmt/includes.h new file mode 100644 index 0000000..426fe4b --- /dev/null +++ b/fmt/includes.h @@ -0,0 +1,9 @@ +#pragma once + +#include "SocksHttpBean.hpp" +#include "ShadowSocksBean.hpp" +#include "ChainBean.hpp" +#include "VMessBean.hpp" +#include "TrojanVLESSBean.hpp" +#include "NaiveBean.hpp" +#include "CustomBean.hpp" diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..164ab83 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,6 @@ +*.log +*.pem +*.json +*.exe +*.dat +/nekoray_core diff --git a/go/auth.go b/go/auth.go new file mode 100644 index 0000000..531b35c --- /dev/null +++ b/go/auth.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Authenticator exposes a function for authenticating requests. +type Authenticator struct { + Token string +} + +// Authenticate checks that a token exists and is valid. It stores the user +// metadata in the returned context and removes the token from the context. +func (a Authenticator) Authenticate(ctx context.Context) (newCtx context.Context, err error) { + auth, err := extractHeader(ctx, "nekoray_auth") + if err != nil { + return ctx, err + } + + if auth != a.Token { + return ctx, status.Error(codes.Unauthenticated, "invalid token") + } + + return purgeHeader(ctx, "nekoray_auth"), nil +} + +func extractHeader(ctx context.Context, header string) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", status.Error(codes.Unauthenticated, "no headers in request") + } + + authHeaders, ok := md[header] + if !ok { + return "", status.Error(codes.Unauthenticated, "no header in request") + } + + if len(authHeaders) != 1 { + return "", status.Error(codes.Unauthenticated, "more than 1 header in request") + } + + return authHeaders[0], nil +} + +func purgeHeader(ctx context.Context, header string) context.Context { + md, _ := metadata.FromIncomingContext(ctx) + mdCopy := md.Copy() + mdCopy[header] = nil + return metadata.NewIncomingContext(ctx, mdCopy) +} diff --git a/go/core_rpc.go b/go/core_rpc.go new file mode 100644 index 0000000..1b95fb3 --- /dev/null +++ b/go/core_rpc.go @@ -0,0 +1,323 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "libcore" + "libcore/device" + "libcore/stun" + "nekoray_core/gen" + "net" + "os" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +var instance *libcore.V2RayInstance + +func setupCore() { + device.IsNekoray = true + libcore.SetConfig("", false, true) + libcore.InitCore("", "", "", nil, ".", "moe.nekoray.pc:bg", true, 50) +} + +func (s *server) Start(ctx context.Context, in *gen.LoadConfigReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + instance = nil + } + }() + + if nekoray_debug { + logrus.Println("Start:", in) + } + + if instance != nil { + err = errors.New("instance already started") + return + } + + instance = libcore.NewV2rayInstance() + + libcore.SetConfig(in.TryDomains, false, true) + + err = instance.LoadConfig(in.CoreConfig) + if err != nil { + return + } + + err = instance.Start() + if err != nil { + return + } + + TunSetV2ray(instance) + + return +} + +func (s *server) SetTun(ctx context.Context, in *gen.SetTunReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + } + }() + + if in.Implementation >= 0 { //Start + err = TunStart(in) + } else { //Stop + TunStop() + } + + return +} + +func (s *server) Stop(ctx context.Context, in *gen.EmptyReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + } + }() + + if instance == nil { + return + } + + TunSetV2ray(nil) + + err = instance.Close() + instance = nil + + return +} + +func (s *server) Exit(ctx context.Context, in *gen.EmptyReq) (out *gen.EmptyResp, _ error) { + out = &gen.EmptyResp{} + + // Connection closed + os.Exit(0) + return +} + +func (s *server) Test(ctx context.Context, in *gen.TestReq) (out *gen.TestResp, _ error) { + var err error + out = &gen.TestResp{Ms: 0} + + defer func() { + if err != nil { + out.Error = err.Error() + } + }() + + if nekoray_debug { + logrus.Println("Test:", in) + } + + if in.Mode == gen.TestMode_UrlTest { + var i *libcore.V2RayInstance + + if in.Config != nil { + // Test instance + i = libcore.NewV2rayInstance() + defer i.Close() + + err = i.LoadConfig(in.Config.CoreConfig) + if err != nil { + return + } + + err = i.Start() + if err != nil { + return + } + } else { + // Test running instance + i = instance + if i == nil { + return + } + } + + // Latency + var t int32 + t, err = libcore.UrlTestV2ray(i, in.Inbound, in.Url, in.Timeout) + out.Ms = t // sn: ms==0 是错误 + } else if in.Mode == gen.TestMode_TcpPing { + startTime := time.Now() + _, err = net.DialTimeout("tcp", in.Address, time.Duration(in.Timeout)*time.Millisecond) + endTime := time.Now() + if err == nil { + out.Ms = int32(endTime.Sub(startTime).Milliseconds()) + } + } else if in.Mode == gen.TestMode_FullTest { + if in.Config == nil { + return + } + + // Test instance + i := libcore.NewV2rayInstance() + defer i.Close() + + err = i.LoadConfig(in.Config.CoreConfig) + if err != nil { + return + } + + err = i.Start() + if err != nil { + return + } + + // Latency + var latency string + if in.FullLatency { + t, _ := libcore.UrlTestV2ray(i, in.Inbound, in.Url, in.Timeout) + out.Ms = t + if t > 0 { + latency = fmt.Sprint(t, "ms") + } else { + latency = "Error" + } + } + + // 入口 IP + var in_ip string + if in.FullInOut { + _in_ip, err := net.ResolveIPAddr("ip", in.InAddress) + if err == nil { + in_ip = _in_ip.String() + } else { + in_ip = err.Error() + } + } + + client := getProxyHttpClient(i) + + // 出口 IP + var out_ip string + if in.FullInOut { + resp, err := client.Get("https://httpbin.org/get") + if err == nil { + v := make(map[string]interface{}) + json.NewDecoder(resp.Body).Decode(&v) + if a, ok := v["origin"]; ok { + if s, ok := a.(string); ok { + out_ip = s + } + } + resp.Body.Close() + } else { + out_ip = "Error" + } + } + + // 下载 + var speed string + if in.FullSpeed { + resp, err := client.Get("http://cachefly.cachefly.net/10mb.test") + if err == nil { + time_start := time.Now() + n, _ := io.Copy(io.Discard, resp.Body) + time_end := time.Now() + + speed = fmt.Sprintf("%.2fMiB/s", (float64(n)/time_end.Sub(time_start).Seconds())/1048576) + resp.Body.Close() + } else { + speed = "Error" + } + } + + // STUN + var stunText string + if in.FullNat { + timeout := time.NewTimer(time.Second * 5) + result := make(chan string, 0) + + go func() { + stunServer := "206.53.159.130:3478" + stunAddr, _ := net.ResolveUDPAddr("udp4", stunServer) + pc, err := i.DialUDP(stunAddr) + if err == nil { + stunClient := stun.NewClientWithConnection(pc) + stunClient.SetServerAddr(stunServer) + nat, host, err, fake := stunClient.Discover() + if err == nil { + if host != nil { + if fake { + result <- fmt.Sprint("No Endpoint", nat) + } else { + result <- fmt.Sprint(nat) + } + } + } else { + result <- "Discover Error" + } + } else { + result <- "DialUDP Error" + } + close(result) + }() + + select { + case <-timeout.C: + stunText = "Timeout" + case r := <-result: + stunText = r + } + } + + fr := make([]string, 0) + if latency != "" { + fr = append(fr, fmt.Sprintf("Latency: %s", latency)) + } + if speed != "" { + fr = append(fr, fmt.Sprintf("Speed: %s", speed)) + } + if in_ip != "" { + fr = append(fr, fmt.Sprintf("In: %s", in_ip)) + } + if out_ip != "" { + fr = append(fr, fmt.Sprintf("Out: %s", out_ip)) + } + if stunText != "" { + fr = append(fr, fmt.Sprintf("NAT: %s", stunText)) + } + + out.FullReport = strings.Join(fr, " / ") + } + + return +} + +func (s *server) QueryStats(ctx context.Context, in *gen.QueryStatsReq) (out *gen.QueryStatsResp, _ error) { + out = &gen.QueryStatsResp{} + if instance != nil { + out.Traffic = instance.QueryStats(in.Tag, in.Direct) + } + return +} + +func (s *server) ListV2RayConnections(ctx context.Context, in *gen.EmptyReq) (*gen.ListV2RayConnectionsResp, error) { + out := &gen.ListV2RayConnectionsResp{ + MatsuriConnectionsJson: libcore.ListV2rayConnections(), + } + return out, nil +} diff --git a/go/gen/libcore.pb.go b/go/gen/libcore.pb.go new file mode 100644 index 0000000..9dfbd26 --- /dev/null +++ b/go/gen/libcore.pb.go @@ -0,0 +1,1181 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.21.4 +// source: libcore.proto + +package gen + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestMode int32 + +const ( + TestMode_TcpPing TestMode = 0 + TestMode_UrlTest TestMode = 1 + TestMode_FullTest TestMode = 2 +) + +// Enum value maps for TestMode. +var ( + TestMode_name = map[int32]string{ + 0: "TcpPing", + 1: "UrlTest", + 2: "FullTest", + } + TestMode_value = map[string]int32{ + "TcpPing": 0, + "UrlTest": 1, + "FullTest": 2, + } +) + +func (x TestMode) Enum() *TestMode { + p := new(TestMode) + *p = x + return p +} + +func (x TestMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TestMode) Descriptor() protoreflect.EnumDescriptor { + return file_libcore_proto_enumTypes[0].Descriptor() +} + +func (TestMode) Type() protoreflect.EnumType { + return &file_libcore_proto_enumTypes[0] +} + +func (x TestMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TestMode.Descriptor instead. +func (TestMode) EnumDescriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{0} +} + +type UpdateAction int32 + +const ( + UpdateAction_Check UpdateAction = 0 + UpdateAction_Download UpdateAction = 1 +) + +// Enum value maps for UpdateAction. +var ( + UpdateAction_name = map[int32]string{ + 0: "Check", + 1: "Download", + } + UpdateAction_value = map[string]int32{ + "Check": 0, + "Download": 1, + } +) + +func (x UpdateAction) Enum() *UpdateAction { + p := new(UpdateAction) + *p = x + return p +} + +func (x UpdateAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UpdateAction) Descriptor() protoreflect.EnumDescriptor { + return file_libcore_proto_enumTypes[1].Descriptor() +} + +func (UpdateAction) Type() protoreflect.EnumType { + return &file_libcore_proto_enumTypes[1] +} + +func (x UpdateAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UpdateAction.Descriptor instead. +func (UpdateAction) EnumDescriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{1} +} + +type EmptyReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyReq) Reset() { + *x = EmptyReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyReq) ProtoMessage() {} + +func (x *EmptyReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmptyReq.ProtoReflect.Descriptor instead. +func (*EmptyReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{0} +} + +type EmptyResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyResp) Reset() { + *x = EmptyResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyResp) ProtoMessage() {} + +func (x *EmptyResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmptyResp.ProtoReflect.Descriptor instead. +func (*EmptyResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{1} +} + +type ErrorResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *ErrorResp) Reset() { + *x = ErrorResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ErrorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ErrorResp) ProtoMessage() {} + +func (x *ErrorResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ErrorResp.ProtoReflect.Descriptor instead. +func (*ErrorResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{2} +} + +func (x *ErrorResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type LoadConfigReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CoreConfig string `protobuf:"bytes,1,opt,name=coreConfig,proto3" json:"coreConfig,omitempty"` + TryDomains string `protobuf:"bytes,2,opt,name=tryDomains,proto3" json:"tryDomains,omitempty"` +} + +func (x *LoadConfigReq) Reset() { + *x = LoadConfigReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoadConfigReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadConfigReq) ProtoMessage() {} + +func (x *LoadConfigReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoadConfigReq.ProtoReflect.Descriptor instead. +func (*LoadConfigReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{3} +} + +func (x *LoadConfigReq) GetCoreConfig() string { + if x != nil { + return x.CoreConfig + } + return "" +} + +func (x *LoadConfigReq) GetTryDomains() string { + if x != nil { + return x.TryDomains + } + return "" +} + +type SetTunReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Mtu int32 `protobuf:"varint,2,opt,name=mtu,proto3" json:"mtu,omitempty"` + Implementation int32 `protobuf:"varint,3,opt,name=implementation,proto3" json:"implementation,omitempty"` + Fakedns bool `protobuf:"varint,4,opt,name=fakedns,proto3" json:"fakedns,omitempty"` +} + +func (x *SetTunReq) Reset() { + *x = SetTunReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetTunReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTunReq) ProtoMessage() {} + +func (x *SetTunReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetTunReq.ProtoReflect.Descriptor instead. +func (*SetTunReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{4} +} + +func (x *SetTunReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SetTunReq) GetMtu() int32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *SetTunReq) GetImplementation() int32 { + if x != nil { + return x.Implementation + } + return 0 +} + +func (x *SetTunReq) GetFakedns() bool { + if x != nil { + return x.Fakedns + } + return false +} + +type TestReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Mode TestMode `protobuf:"varint,1,opt,name=mode,proto3,enum=libcore.TestMode" json:"mode,omitempty"` + Timeout int32 `protobuf:"varint,6,opt,name=timeout,proto3" json:"timeout,omitempty"` + // TcpPing + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + // UrlTest + Config *LoadConfigReq `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + Inbound string `protobuf:"bytes,4,opt,name=inbound,proto3" json:"inbound,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + // FullTest + InAddress string `protobuf:"bytes,7,opt,name=in_address,json=inAddress,proto3" json:"in_address,omitempty"` + FullLatency bool `protobuf:"varint,8,opt,name=full_latency,json=fullLatency,proto3" json:"full_latency,omitempty"` + FullSpeed bool `protobuf:"varint,9,opt,name=full_speed,json=fullSpeed,proto3" json:"full_speed,omitempty"` + FullInOut bool `protobuf:"varint,10,opt,name=full_in_out,json=fullInOut,proto3" json:"full_in_out,omitempty"` + FullNat bool `protobuf:"varint,11,opt,name=full_nat,json=fullNat,proto3" json:"full_nat,omitempty"` +} + +func (x *TestReq) Reset() { + *x = TestReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestReq) ProtoMessage() {} + +func (x *TestReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestReq.ProtoReflect.Descriptor instead. +func (*TestReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{5} +} + +func (x *TestReq) GetMode() TestMode { + if x != nil { + return x.Mode + } + return TestMode_TcpPing +} + +func (x *TestReq) GetTimeout() int32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *TestReq) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *TestReq) GetConfig() *LoadConfigReq { + if x != nil { + return x.Config + } + return nil +} + +func (x *TestReq) GetInbound() string { + if x != nil { + return x.Inbound + } + return "" +} + +func (x *TestReq) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *TestReq) GetInAddress() string { + if x != nil { + return x.InAddress + } + return "" +} + +func (x *TestReq) GetFullLatency() bool { + if x != nil { + return x.FullLatency + } + return false +} + +func (x *TestReq) GetFullSpeed() bool { + if x != nil { + return x.FullSpeed + } + return false +} + +func (x *TestReq) GetFullInOut() bool { + if x != nil { + return x.FullInOut + } + return false +} + +func (x *TestReq) GetFullNat() bool { + if x != nil { + return x.FullNat + } + return false +} + +type TestResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Ms int32 `protobuf:"varint,2,opt,name=ms,proto3" json:"ms,omitempty"` + FullReport string `protobuf:"bytes,3,opt,name=full_report,json=fullReport,proto3" json:"full_report,omitempty"` +} + +func (x *TestResp) Reset() { + *x = TestResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestResp) ProtoMessage() {} + +func (x *TestResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestResp.ProtoReflect.Descriptor instead. +func (*TestResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{6} +} + +func (x *TestResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *TestResp) GetMs() int32 { + if x != nil { + return x.Ms + } + return 0 +} + +func (x *TestResp) GetFullReport() string { + if x != nil { + return x.FullReport + } + return "" +} + +type QueryStatsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Direct string `protobuf:"bytes,2,opt,name=direct,proto3" json:"direct,omitempty"` +} + +func (x *QueryStatsReq) Reset() { + *x = QueryStatsReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryStatsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsReq) ProtoMessage() {} + +func (x *QueryStatsReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsReq.ProtoReflect.Descriptor instead. +func (*QueryStatsReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{7} +} + +func (x *QueryStatsReq) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *QueryStatsReq) GetDirect() string { + if x != nil { + return x.Direct + } + return "" +} + +type QueryStatsResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Traffic int64 `protobuf:"varint,1,opt,name=traffic,proto3" json:"traffic,omitempty"` +} + +func (x *QueryStatsResp) Reset() { + *x = QueryStatsResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryStatsResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsResp) ProtoMessage() {} + +func (x *QueryStatsResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsResp.ProtoReflect.Descriptor instead. +func (*QueryStatsResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{8} +} + +func (x *QueryStatsResp) GetTraffic() int64 { + if x != nil { + return x.Traffic + } + return 0 +} + +type UpdateReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Action UpdateAction `protobuf:"varint,1,opt,name=action,proto3,enum=libcore.UpdateAction" json:"action,omitempty"` +} + +func (x *UpdateReq) Reset() { + *x = UpdateReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateReq) ProtoMessage() {} + +func (x *UpdateReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateReq.ProtoReflect.Descriptor instead. +func (*UpdateReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateReq) GetAction() UpdateAction { + if x != nil { + return x.Action + } + return UpdateAction_Check +} + +type UpdateResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + AssetsName string `protobuf:"bytes,2,opt,name=assets_name,json=assetsName,proto3" json:"assets_name,omitempty"` + DownloadUrl string `protobuf:"bytes,3,opt,name=download_url,json=downloadUrl,proto3" json:"download_url,omitempty"` + ReleaseUrl string `protobuf:"bytes,4,opt,name=release_url,json=releaseUrl,proto3" json:"release_url,omitempty"` + ReleaseNote string `protobuf:"bytes,5,opt,name=release_note,json=releaseNote,proto3" json:"release_note,omitempty"` +} + +func (x *UpdateResp) Reset() { + *x = UpdateResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateResp) ProtoMessage() {} + +func (x *UpdateResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateResp.ProtoReflect.Descriptor instead. +func (*UpdateResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *UpdateResp) GetAssetsName() string { + if x != nil { + return x.AssetsName + } + return "" +} + +func (x *UpdateResp) GetDownloadUrl() string { + if x != nil { + return x.DownloadUrl + } + return "" +} + +func (x *UpdateResp) GetReleaseUrl() string { + if x != nil { + return x.ReleaseUrl + } + return "" +} + +func (x *UpdateResp) GetReleaseNote() string { + if x != nil { + return x.ReleaseNote + } + return "" +} + +type ListV2RayConnectionsResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MatsuriConnectionsJson string `protobuf:"bytes,1,opt,name=matsuri_connections_json,json=matsuriConnectionsJson,proto3" json:"matsuri_connections_json,omitempty"` +} + +func (x *ListV2RayConnectionsResp) Reset() { + *x = ListV2RayConnectionsResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListV2RayConnectionsResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListV2RayConnectionsResp) ProtoMessage() {} + +func (x *ListV2RayConnectionsResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListV2RayConnectionsResp.ProtoReflect.Descriptor instead. +func (*ListV2RayConnectionsResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{11} +} + +func (x *ListV2RayConnectionsResp) GetMatsuriConnectionsJson() string { + if x != nil { + return x.MatsuriConnectionsJson + } + return "" +} + +var File_libcore_proto protoreflect.FileDescriptor + +var file_libcore_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x0a, 0x0a, 0x08, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x52, 0x65, 0x71, 0x22, 0x0b, 0x0a, 0x09, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x21, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x4f, 0x0a, 0x0d, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x72, 0x79, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x79, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x22, 0x73, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, 0x52, + 0x65, 0x71, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6d, 0x70, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x18, 0x0a, 0x07, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x22, 0xdc, 0x02, 0x0a, 0x07, 0x54, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x12, 0x25, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, + 0x65, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4c, 0x6f, 0x61, 0x64, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1d, 0x0a, + 0x0a, 0x69, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, + 0x1d, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x1e, + 0x0a, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x69, 0x6e, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x49, 0x6e, 0x4f, 0x75, 0x74, 0x12, 0x19, + 0x0a, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x74, 0x22, 0x51, 0x0a, 0x08, 0x54, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x6d, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x6d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, + 0x75, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x39, 0x0a, 0x0d, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, + 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x2a, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x61, + 0x66, 0x66, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x72, 0x61, 0x66, + 0x66, 0x69, 0x63, 0x22, 0x3a, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x12, 0x2d, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x15, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0xaa, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, + 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x6f, 0x77, + 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x22, 0x54, 0x0a, 0x18, + 0x4c, 0x69, 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x38, 0x0a, 0x18, 0x6d, 0x61, 0x74, 0x73, + 0x75, 0x72, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x6d, 0x61, 0x74, 0x73, + 0x75, 0x72, 0x69, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4a, 0x73, + 0x6f, 0x6e, 0x2a, 0x32, 0x0a, 0x08, 0x54, 0x65, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, + 0x0a, 0x07, 0x54, 0x63, 0x70, 0x50, 0x69, 0x6e, 0x67, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x72, 0x6c, 0x54, 0x65, 0x73, 0x74, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x46, 0x75, 0x6c, 0x6c, + 0x54, 0x65, 0x73, 0x74, 0x10, 0x02, 0x2a, 0x27, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x10, 0x01, 0x32, + 0x88, 0x04, 0x0a, 0x0e, 0x4c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x45, 0x78, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, + 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, + 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x06, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x35, + 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x1a, + 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x06, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, 0x12, + 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, + 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x04, 0x53, 0x74, 0x6f, + 0x70, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x04, 0x54, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x0a, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, + 0x17, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x14, 0x4c, 0x69, + 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x21, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x12, 0x5a, 0x10, 0x6e, 0x65, + 0x6b, 0x6f, 0x72, 0x61, 0x79, 0x5f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_libcore_proto_rawDescOnce sync.Once + file_libcore_proto_rawDescData = file_libcore_proto_rawDesc +) + +func file_libcore_proto_rawDescGZIP() []byte { + file_libcore_proto_rawDescOnce.Do(func() { + file_libcore_proto_rawDescData = protoimpl.X.CompressGZIP(file_libcore_proto_rawDescData) + }) + return file_libcore_proto_rawDescData +} + +var file_libcore_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_libcore_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_libcore_proto_goTypes = []interface{}{ + (TestMode)(0), // 0: libcore.TestMode + (UpdateAction)(0), // 1: libcore.UpdateAction + (*EmptyReq)(nil), // 2: libcore.EmptyReq + (*EmptyResp)(nil), // 3: libcore.EmptyResp + (*ErrorResp)(nil), // 4: libcore.ErrorResp + (*LoadConfigReq)(nil), // 5: libcore.LoadConfigReq + (*SetTunReq)(nil), // 6: libcore.SetTunReq + (*TestReq)(nil), // 7: libcore.TestReq + (*TestResp)(nil), // 8: libcore.TestResp + (*QueryStatsReq)(nil), // 9: libcore.QueryStatsReq + (*QueryStatsResp)(nil), // 10: libcore.QueryStatsResp + (*UpdateReq)(nil), // 11: libcore.UpdateReq + (*UpdateResp)(nil), // 12: libcore.UpdateResp + (*ListV2RayConnectionsResp)(nil), // 13: libcore.ListV2rayConnectionsResp +} +var file_libcore_proto_depIdxs = []int32{ + 0, // 0: libcore.TestReq.mode:type_name -> libcore.TestMode + 5, // 1: libcore.TestReq.config:type_name -> libcore.LoadConfigReq + 1, // 2: libcore.UpdateReq.action:type_name -> libcore.UpdateAction + 2, // 3: libcore.LibcoreService.Exit:input_type -> libcore.EmptyReq + 2, // 4: libcore.LibcoreService.KeepAlive:input_type -> libcore.EmptyReq + 11, // 5: libcore.LibcoreService.Update:input_type -> libcore.UpdateReq + 5, // 6: libcore.LibcoreService.Start:input_type -> libcore.LoadConfigReq + 6, // 7: libcore.LibcoreService.SetTun:input_type -> libcore.SetTunReq + 2, // 8: libcore.LibcoreService.Stop:input_type -> libcore.EmptyReq + 7, // 9: libcore.LibcoreService.Test:input_type -> libcore.TestReq + 9, // 10: libcore.LibcoreService.QueryStats:input_type -> libcore.QueryStatsReq + 2, // 11: libcore.LibcoreService.ListV2rayConnections:input_type -> libcore.EmptyReq + 3, // 12: libcore.LibcoreService.Exit:output_type -> libcore.EmptyResp + 3, // 13: libcore.LibcoreService.KeepAlive:output_type -> libcore.EmptyResp + 12, // 14: libcore.LibcoreService.Update:output_type -> libcore.UpdateResp + 4, // 15: libcore.LibcoreService.Start:output_type -> libcore.ErrorResp + 4, // 16: libcore.LibcoreService.SetTun:output_type -> libcore.ErrorResp + 4, // 17: libcore.LibcoreService.Stop:output_type -> libcore.ErrorResp + 8, // 18: libcore.LibcoreService.Test:output_type -> libcore.TestResp + 10, // 19: libcore.LibcoreService.QueryStats:output_type -> libcore.QueryStatsResp + 13, // 20: libcore.LibcoreService.ListV2rayConnections:output_type -> libcore.ListV2rayConnectionsResp + 12, // [12:21] is the sub-list for method output_type + 3, // [3:12] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_libcore_proto_init() } +func file_libcore_proto_init() { + if File_libcore_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_libcore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ErrorResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoadConfigReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetTunReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryStatsReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryStatsResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListV2RayConnectionsResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_libcore_proto_rawDesc, + NumEnums: 2, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_libcore_proto_goTypes, + DependencyIndexes: file_libcore_proto_depIdxs, + EnumInfos: file_libcore_proto_enumTypes, + MessageInfos: file_libcore_proto_msgTypes, + }.Build() + File_libcore_proto = out.File + file_libcore_proto_rawDesc = nil + file_libcore_proto_goTypes = nil + file_libcore_proto_depIdxs = nil +} diff --git a/go/gen/libcore.proto b/go/gen/libcore.proto new file mode 100644 index 0000000..e22f1be --- /dev/null +++ b/go/gen/libcore.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package libcore; +option go_package = "nekoray_core/gen"; + +service LibcoreService { + rpc Exit(EmptyReq) returns (EmptyResp) {} + rpc KeepAlive(EmptyReq) returns (EmptyResp) {} + rpc Update(UpdateReq) returns (UpdateResp) {} + // + rpc Start(LoadConfigReq) returns (ErrorResp) {} + rpc SetTun(SetTunReq) returns (ErrorResp) {} + rpc Stop(EmptyReq) returns (ErrorResp) {} + rpc Test(TestReq) returns (TestResp) {} + rpc QueryStats(QueryStatsReq) returns (QueryStatsResp) {} + rpc ListV2rayConnections(EmptyReq) returns (ListV2rayConnectionsResp) {} +} + +message EmptyReq {} + +message EmptyResp {} + +message ErrorResp { + string error = 1; +} + +message LoadConfigReq { + string coreConfig = 1; + string tryDomains = 2; +} + +message SetTunReq { + string name = 1; + int32 mtu = 2; + int32 implementation = 3; + bool fakedns = 4; +} + +enum TestMode { + TcpPing = 0; + UrlTest = 1; + FullTest = 2; +} + +message TestReq { + TestMode mode = 1; + int32 timeout = 6; + // TcpPing + string address = 2; + // UrlTest + LoadConfigReq config = 3; + string inbound = 4; + string url = 5; + // FullTest + string in_address = 7; + bool full_latency = 8; + bool full_speed = 9; + bool full_in_out = 10; + bool full_nat = 11; +} + +message TestResp { + string error = 1; + int32 ms = 2; + string full_report = 3; +} + +message QueryStatsReq{ + string tag = 1; + string direct = 2; +} + +message QueryStatsResp{ + int64 traffic = 1; +} + +enum UpdateAction { + Check = 0; + Download = 1; +} + +message UpdateReq { + UpdateAction action = 1; +} + +message UpdateResp { + string error = 1; + string assets_name = 2; + string download_url = 3; + string release_url = 4; + string release_note = 5; +} + +message ListV2rayConnectionsResp { + string matsuri_connections_json = 1; +} diff --git a/go/gen/libcore_grpc.pb.go b/go/gen/libcore_grpc.pb.go new file mode 100644 index 0000000..92c3843 --- /dev/null +++ b/go/gen/libcore_grpc.pb.go @@ -0,0 +1,395 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.21.4 +// source: libcore.proto + +package gen + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LibcoreServiceClient is the client API for LibcoreService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LibcoreServiceClient interface { + Exit(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) + KeepAlive(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) + Update(ctx context.Context, in *UpdateReq, opts ...grpc.CallOption) (*UpdateResp, error) + // + Start(ctx context.Context, in *LoadConfigReq, opts ...grpc.CallOption) (*ErrorResp, error) + SetTun(ctx context.Context, in *SetTunReq, opts ...grpc.CallOption) (*ErrorResp, error) + Stop(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ErrorResp, error) + Test(ctx context.Context, in *TestReq, opts ...grpc.CallOption) (*TestResp, error) + QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error) + ListV2RayConnections(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ListV2RayConnectionsResp, error) +} + +type libcoreServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLibcoreServiceClient(cc grpc.ClientConnInterface) LibcoreServiceClient { + return &libcoreServiceClient{cc} +} + +func (c *libcoreServiceClient) Exit(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) { + out := new(EmptyResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Exit", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) KeepAlive(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) { + out := new(EmptyResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/KeepAlive", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Update(ctx context.Context, in *UpdateReq, opts ...grpc.CallOption) (*UpdateResp, error) { + out := new(UpdateResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Update", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Start(ctx context.Context, in *LoadConfigReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Start", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) SetTun(ctx context.Context, in *SetTunReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/SetTun", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Stop(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Stop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Test(ctx context.Context, in *TestReq, opts ...grpc.CallOption) (*TestResp, error) { + out := new(TestResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Test", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error) { + out := new(QueryStatsResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/QueryStats", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) ListV2RayConnections(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ListV2RayConnectionsResp, error) { + out := new(ListV2RayConnectionsResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/ListV2rayConnections", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LibcoreServiceServer is the server API for LibcoreService service. +// All implementations must embed UnimplementedLibcoreServiceServer +// for forward compatibility +type LibcoreServiceServer interface { + Exit(context.Context, *EmptyReq) (*EmptyResp, error) + KeepAlive(context.Context, *EmptyReq) (*EmptyResp, error) + Update(context.Context, *UpdateReq) (*UpdateResp, error) + // + Start(context.Context, *LoadConfigReq) (*ErrorResp, error) + SetTun(context.Context, *SetTunReq) (*ErrorResp, error) + Stop(context.Context, *EmptyReq) (*ErrorResp, error) + Test(context.Context, *TestReq) (*TestResp, error) + QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error) + ListV2RayConnections(context.Context, *EmptyReq) (*ListV2RayConnectionsResp, error) + mustEmbedUnimplementedLibcoreServiceServer() +} + +// UnimplementedLibcoreServiceServer must be embedded to have forward compatible implementations. +type UnimplementedLibcoreServiceServer struct { +} + +func (UnimplementedLibcoreServiceServer) Exit(context.Context, *EmptyReq) (*EmptyResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Exit not implemented") +} +func (UnimplementedLibcoreServiceServer) KeepAlive(context.Context, *EmptyReq) (*EmptyResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method KeepAlive not implemented") +} +func (UnimplementedLibcoreServiceServer) Update(context.Context, *UpdateReq) (*UpdateResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedLibcoreServiceServer) Start(context.Context, *LoadConfigReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Start not implemented") +} +func (UnimplementedLibcoreServiceServer) SetTun(context.Context, *SetTunReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetTun not implemented") +} +func (UnimplementedLibcoreServiceServer) Stop(context.Context, *EmptyReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") +} +func (UnimplementedLibcoreServiceServer) Test(context.Context, *TestReq) (*TestResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Test not implemented") +} +func (UnimplementedLibcoreServiceServer) QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented") +} +func (UnimplementedLibcoreServiceServer) ListV2RayConnections(context.Context, *EmptyReq) (*ListV2RayConnectionsResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListV2RayConnections not implemented") +} +func (UnimplementedLibcoreServiceServer) mustEmbedUnimplementedLibcoreServiceServer() {} + +// UnsafeLibcoreServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LibcoreServiceServer will +// result in compilation errors. +type UnsafeLibcoreServiceServer interface { + mustEmbedUnimplementedLibcoreServiceServer() +} + +func RegisterLibcoreServiceServer(s grpc.ServiceRegistrar, srv LibcoreServiceServer) { + s.RegisterService(&LibcoreService_ServiceDesc, srv) +} + +func _LibcoreService_Exit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Exit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Exit", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Exit(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_KeepAlive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).KeepAlive(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/KeepAlive", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).KeepAlive(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Update", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Update(ctx, req.(*UpdateReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoadConfigReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Start(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Start", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Start(ctx, req.(*LoadConfigReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_SetTun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetTunReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).SetTun(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/SetTun", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).SetTun(ctx, req.(*SetTunReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Stop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Stop(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Test_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Test(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Test", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Test(ctx, req.(*TestReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryStatsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).QueryStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/QueryStats", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).QueryStats(ctx, req.(*QueryStatsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_ListV2RayConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).ListV2RayConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/ListV2rayConnections", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).ListV2RayConnections(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +// LibcoreService_ServiceDesc is the grpc.ServiceDesc for LibcoreService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LibcoreService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "libcore.LibcoreService", + HandlerType: (*LibcoreServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Exit", + Handler: _LibcoreService_Exit_Handler, + }, + { + MethodName: "KeepAlive", + Handler: _LibcoreService_KeepAlive_Handler, + }, + { + MethodName: "Update", + Handler: _LibcoreService_Update_Handler, + }, + { + MethodName: "Start", + Handler: _LibcoreService_Start_Handler, + }, + { + MethodName: "SetTun", + Handler: _LibcoreService_SetTun_Handler, + }, + { + MethodName: "Stop", + Handler: _LibcoreService_Stop_Handler, + }, + { + MethodName: "Test", + Handler: _LibcoreService_Test_Handler, + }, + { + MethodName: "QueryStats", + Handler: _LibcoreService_QueryStats_Handler, + }, + { + MethodName: "ListV2rayConnections", + Handler: _LibcoreService_ListV2RayConnections_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "libcore.proto", +} diff --git a/go/gen/update_proto.sh b/go/gen/update_proto.sh new file mode 100644 index 0000000..709ed22 --- /dev/null +++ b/go/gen/update_proto.sh @@ -0,0 +1,3 @@ +protoc -I . --go_out=. --go_opt paths=source_relative --go-grpc_out=. --go-grpc_opt paths=source_relative libcore.proto + +# protoc -I . --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` libcore.proto diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..9d1ceec --- /dev/null +++ b/go/go.mod @@ -0,0 +1,93 @@ +module nekoray_core + +go 1.18 + +require ( + github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 + github.com/jsimonetti/rtnetlink v1.2.0 + github.com/sirupsen/logrus v1.8.1 + github.com/v2fly/v2ray-core/v5 v5.0.0 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + google.golang.org/grpc v1.48.0 + google.golang.org/protobuf v1.28.1 + kernel.org/pub/linux/libs/security/libcap/cap v1.2.64 + libcore v1.0.0 +) + +require ( + github.com/Dreamacro/clash v1.9.0 // indirect + github.com/Dreamacro/go-shadowsocks2 v0.1.7 // indirect + github.com/adrg/xdg v0.4.0 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 // indirect + github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d // indirect + github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb // indirect + github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect + github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/jhump/protoreflect v1.12.0 // indirect + github.com/josharian/native v1.0.0 // indirect + github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8 // indirect + github.com/klauspost/cpuid v1.2.3 // indirect + github.com/klauspost/reedsolomon v1.9.3 // indirect + github.com/lucas-clemente/quic-go v0.28.1 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect + github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect + github.com/mdlayher/netlink v1.6.0 // indirect + github.com/mdlayher/socket v0.1.1 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/mustafaturan/bus v1.0.2 // indirect + github.com/mustafaturan/monoton v1.0.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pion/dtls/v2 v2.0.0-rc.7 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/sctp v1.7.6 // indirect + github.com/pires/go-proxyproto v0.6.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect + github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844 // indirect + github.com/sagernet/libping v0.1.1 // indirect + github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect + github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 // indirect + github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect + github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 // indirect + github.com/xtaci/smux v1.5.16 // indirect + go.starlark.net v0.0.0-20220302181546-5411bad688d1 // indirect + go.uber.org/automaxprocs v1.4.0 // indirect + go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect + golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 // indirect + golang.org/x/mod v0.5.1 // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect + golang.org/x/tools v0.1.9 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gvisor.dev/gvisor v0.0.0 // indirect + inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.64 // indirect +) + +replace libcore v1.0.0 => ../../Matsuri/libcore + +replace github.com/v2fly/v2ray-core/v5 v5.0.0 => ../../v2ray-core + +replace gvisor.dev/gvisor => github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..0232293 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,737 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Dreamacro/clash v1.9.0 h1:IfmPW86Klngu0iQ4LL6Bhxcvtr+QaI7Oppa9qRPX/Q8= +github.com/Dreamacro/clash v1.9.0/go.mod h1:vOzDB9KKD/PirNdSlsH4soMl1xF5lk8SwNQiVY5UacE= +github.com/Dreamacro/go-shadowsocks2 v0.1.7 h1:8CtbE1HoPPMfrQZGXmlluq6dO2lL31W6WRRE8fabc4Q= +github.com/Dreamacro/go-shadowsocks2 v0.1.7/go.mod h1:8p5G4cAj5ZlXwUR+Ww63gfSikr8kvw8uw3TDwLAJpUc= +github.com/FlowerWrong/water v0.0.0-20180301012659-01a4eaa1f6f2/go.mod h1:xrG5L7lq7T2DLnPr2frMnL906CNEoKRwLB+VYFhPq2w= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= +github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI= +github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6ZhUWvbk7PdWVmOaak10o86cqMUYEmn1CZNGEI= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao= +github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE= +github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb h1:zXpN5126w/mhECTkqazBkrOJIMatbPP71aSIDR5UuW4= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb/go.mod h1:F7WkpqJj9t98ePxB/WJGQTIDeOVPuSJ3qdn6JUjg170= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= +github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22 h1:CdVtqYWYGIEuYCbtyx6BVMKOcO0N6lKm99cR1DZubAs= +github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22/go.mod h1:YS1s0XuwU13tHT0WeYeUXUwGk1m8WZvSbK9cx/kY1SE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259/go.mod h1:9Qcha0gTWLw//0VNka1Cbnjvg3pNKGFdAm7E9sBabxE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20210420193930-a4630ec28c79/go.mod h1:Opf9rtYVq0eTyX+aRVmRO9hE8ERAozcdrBxWG9Q6mkQ= +github.com/gopherjs/websocket v0.0.0-20191103002815-9a42957e2b3a/go.mod h1:jd+zY81Fx2lC4bfw58+Rflg1srqmedQjbBUejKOjYNY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= +github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.12.0 h1:1NQ4FpWMgn3by/n1X0fbeKEUxP1wBt7+Oitpv01HR10= +github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink v1.2.0 h1:KlwYLoRXgirTFbh1aVI6MJ7i+R/zJr+JkyhlIW1X3z4= +github.com/jsimonetti/rtnetlink v1.2.0/go.mod h1:RA0RtDj3hv4g6l/Y4B7RubIQkdTDAwXfMW/8bMaZ0FY= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8 h1:QxgFSDEqLP8ZsmVm/Qke0HP6JLV7EB93vtWK7noU1Sw= +github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8/go.mod h1:uL2TcUivilrs0kPsqUwIf8XHAcmkSjsfrzSgAJwS0TI= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= +github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucas-clemente/quic-go v0.28.1 h1:Uo0lvVxWg5la9gflIF9lwa39ONq85Xq2D91YNEIslzU= +github.com/lucas-clemente/quic-go v0.28.1/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= +github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= +github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ= +github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= +github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= +github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 h1:7m/WlWcSROrcK5NxuXaxYD32BZqe/LEEnBrWcH/cOqQ= +github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= +github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= +github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI= +github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1Ws= +github.com/mustafaturan/bus v1.0.2/go.mod h1:h7gfehm8TThv4Dcaa+wDQG7r7j6p74v+7ftr0Rq9i1Q= +github.com/mustafaturan/monoton v0.3.1/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8= +github.com/mustafaturan/monoton v1.0.0 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ= +github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/dtls/v2 v2.0.0-rc.7 h1:LDAIQDt1pcuAIJs7Q2EZ3PSl8MseCFA2nCW0YYSYCx0= +github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4= +github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= +github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= +github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844 h1:o7izBZde2L5foPbQdYisY03y4+6T6UcUXOwR5MyQpUk= +github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844/go.mod h1:2Xj8wyq0y6G6B1gCNTzRcKqo+cyVKatMTNWUmxNYfI4= +github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e h1:Y4avBAtZ59OWvLl6zP9sF62jtMEVRPIH78IQctq9aXQ= +github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI= +github.com/sagernet/libping v0.1.1 h1:uNMN/02fQmRbsgJ0EuxuM/Upq8FrVP4Xj2+LlYviIOs= +github.com/sagernet/libping v0.1.1/go.mod h1:FhmyYM8L32JaKI08noqoS5cK+Gw/Q+4VDnI9WvP6Sp8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 h1:zOjq+1/uLzn/Xo40stbvjIY/yehG0+mfmlsiEmc0xmQ= +github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4/go.mod h1:aI+8yClBW+1uovkHw6HM01YXnYB8vohtB9C83wzx34E= +github.com/seiflotfy/cuckoofilter v0.0.0-20200416141329-862a88987de7/go.mod h1:ET5mVvNjwaGXRgZxO9UZr7X+8eAf87AfIYNwRSp9s4Y= +github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b h1:wHoB6ZYEnIVizebcj419LbN4Tagk7RDFiudRFKyzzmo= +github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20200327133705-caf148ab5e9d/go.mod h1:d3n8NJ6QMRb6I/WAlp4z5ZPAoaeqDmX5NgVZA0mhe+I= +github.com/txthinking/x v0.0.0-20200330144832-5ad2416896a9/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc= +github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08/go.mod h1:KAuQNm+LWQCOFqdBcUgihPzRpVXRKzGbTNhfEfRZ4wY= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs= +github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= +github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20220302181546-5411bad688d1 h1:i0Sz4b+qJi5xwOaFZqZ+RNHkIpaKLDofei/Glt+PMNc= +go.starlark.net v0.0.0-20220302181546-5411bad688d1/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0= +go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb h1:ZrsicilzPCS/Xr8qtBZZLpy4P9TYXAfl49ctG1/5tgw= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.64 h1:E1U4GNGSXEdzQUT+mop0iYawCNXDUU46Y8nfodb+ZY0= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.64/go.mod h1:gtBlgvjXflnxHng9/3bXyXG3XmBYKDt35zu+lNmB+IA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.64 h1:zlw/KoDjEObyddpFcvLiuu8frEvyEwVNc62WZQBp68w= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.64/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/go/grpc.go b/go/grpc.go new file mode 100644 index 0000000..f7f691f --- /dev/null +++ b/go/grpc.go @@ -0,0 +1,223 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "libcore" + "log" + "nekoray_core/gen" + "net" + "net/http" + "os" + "runtime" + "strconv" + "strings" + "time" + _ "unsafe" + + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + v2rayNet "github.com/v2fly/v2ray-core/v5/common/net" + "google.golang.org/grpc" +) + +type server struct { + gen.LibcoreServiceServer +} + +var last time.Time +var nekoray_debug bool + +func (s *server) KeepAlive(ctx context.Context, in *gen.EmptyReq) (*gen.EmptyResp, error) { + last = time.Now() + return &gen.EmptyResp{}, nil +} + +func NekorayCore() { + _token := flag.String("token", "", "") + _port := flag.Int("port", 19810, "") + _debug := flag.Bool("debug", false, "") + flag.CommandLine.Parse(os.Args[2:]) + + nekoray_debug = *_debug + + go func() { + t := time.NewTicker(time.Second * 10) + for { + <-t.C + if last.Add(time.Second * 10).Before(time.Now()) { + fmt.Println("Exit due to inactive") + os.Exit(0) + } + } + }() + + // Libcore + setupCore() + + // GRPC + lis, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(*_port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + token := *_token + if token == "" { + os.Stderr.WriteString("Please set a token: ") + s := bufio.NewScanner(os.Stdin) + if s.Scan() { + token = strings.TrimSpace(s.Text()) + } + } + if token == "" { + fmt.Println("You must set a token") + os.Exit(0) + } + os.Stderr.WriteString("token is set\n") + + auther := Authenticator{ + Token: token, + } + + s := grpc.NewServer( + grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(auther.Authenticate)), + grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(auther.Authenticate)), + ) + gen.RegisterLibcoreServiceServer(s, &server{}) + + log.Printf("neokray grpc server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +// PROXY + +func getProxyHttpClient(_instance *libcore.V2RayInstance) *http.Client { + dailContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := v2rayNet.ParseDestination(fmt.Sprintf("%s:%s", network, addr)) + if err != nil { + return nil, err + } + return _instance.DialContext(ctx, dest) + } + + transport := &http.Transport{ + TLSHandshakeTimeout: time.Second * 3, + ResponseHeaderTimeout: time.Second * 3, + } + if _instance != nil { + transport.DialContext = dailContext + } + + client := &http.Client{ + Transport: transport, + } + + return client +} + +// UPDATE + +var update_download_url, update_download_as string + +func (s *server) Update(ctx context.Context, in *gen.UpdateReq) (*gen.UpdateResp, error) { + ret := &gen.UpdateResp{} + + client := getProxyHttpClient(instance) + + if in.Action == gen.UpdateAction_Check { // Check update + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/MatsuriDayo/nekoray/releases", nil) + resp, err := client.Do(req) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer resp.Body.Close() + + v := []struct { + HtmlUrl string `json:"html_url"` + Assets []struct { + Name string `json:"name"` + BrowserDownloadUrl string `json:"browser_download_url"` + } `json:"assets"` + Prerelease bool `json:"prerelease"` + Body string `json:"body"` + }{} + err = json.NewDecoder(resp.Body).Decode(&v) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + + nowVer := strings.TrimLeft(version_standalone, "nekoray-") + + var search string + if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" { + search = "windows64" + update_download_as = "nekoray.zip" + } else if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { + search = "linux64" + update_download_as = "nekoray.tar.gz" + } else { + ret.Error = "Not official support platform" + return ret, nil + } + + for _, release := range v { + if len(release.Assets) > 0 { + for _, asset := range release.Assets { + if strings.Contains(asset.Name, nowVer) { + return ret, nil // No update + } + if strings.Contains(asset.Name, search) { + if release.Prerelease { + continue + } + update_download_url = asset.BrowserDownloadUrl + ret.AssetsName = asset.Name + ret.DownloadUrl = asset.BrowserDownloadUrl + ret.ReleaseUrl = release.HtmlUrl + ret.ReleaseNote = release.Body + return ret, nil // update + } + } + } + } + } else { // Download update + if update_download_url == "" || update_download_as == "" { + ret.Error = "?" + return ret, nil + } + + req, _ := http.NewRequestWithContext(ctx, "GET", update_download_url, nil) + resp, err := client.Do(req) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer resp.Body.Close() + + f, err := os.OpenFile("../"+update_download_as, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer f.Close() + + _, err = io.Copy(f, resp.Body) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + f.Sync() + } + + return ret, nil +} diff --git a/go/import_extra.go b/go/import_extra.go new file mode 100644 index 0000000..90aaf7e --- /dev/null +++ b/go/import_extra.go @@ -0,0 +1,8 @@ +package main + +import ( + _ "github.com/v2fly/v2ray-core/v5/proxy/vless/inbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vless/outbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/inbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/outbound" +) diff --git a/go/iphlpapi/callback_windows.go b/go/iphlpapi/callback_windows.go new file mode 100644 index 0000000..1709ec5 --- /dev/null +++ b/go/iphlpapi/callback_windows.go @@ -0,0 +1,23 @@ +package iphlpapi + +import ( + "syscall" + "unsafe" +) + +func notifyRouteChange2(family uint16, callback uintptr, callerContext uintptr, initialNotification bool, notificationHandle *syscall.Handle) (ret error) { + var _p0 uint32 + if initialNotification { + _p0 = 1 + } + r0, _, _ := syscall.Syscall6(proc_notifyRouteChange2.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} + +func RegisterNotifyRouteChange2(callback func(callerContext uintptr, row uintptr, notificationType uint32) uintptr, initialNotification bool) (handle syscall.Handle) { + notifyRouteChange2(syscall.AF_UNSPEC, syscall.NewCallback(callback), 0, initialNotification, &handle) + return +} diff --git a/go/iphlpapi/dll_windows.go b/go/iphlpapi/dll_windows.go new file mode 100644 index 0000000..6b9b2d6 --- /dev/null +++ b/go/iphlpapi/dll_windows.go @@ -0,0 +1,14 @@ +package iphlpapi + +import "syscall" + +var ( + proc_getIpForwardTable *syscall.LazyProc + proc_notifyRouteChange2 *syscall.LazyProc +) + +func init() { + iphlpapi := syscall.NewLazyDLL("iphlpapi.dll") + proc_getIpForwardTable = iphlpapi.NewProc("GetIpForwardTable") + proc_notifyRouteChange2 = iphlpapi.NewProc("NotifyRouteChange2") +} diff --git a/go/iphlpapi/route_windows.go b/go/iphlpapi/route_windows.go new file mode 100644 index 0000000..e22b1a2 --- /dev/null +++ b/go/iphlpapi/route_windows.go @@ -0,0 +1,87 @@ +package iphlpapi + +import ( + "fmt" + "net" + "unsafe" +) + +/* +对于路由表,预期的方法是: +查询 0.0.0.0/0 获得原始默认路由 +然后为 vpn 服务器添加默认路由 +之后就根据需要下发vpn路由完事。 +对于0.0.0.0/0 vpn 路由,可以尝试更低的跃点数,也可以尝试分为2个。 +重新连接时可以删除vpn接口的所有非链路路由表。 +路由表格式: +目标网络 uint32 掩码位数 byte低6位 vpn/默认网关 byte 高1位 +*/ + +// 太低的值添加路由时会返回 106 错误 +const routeMetric = 93 + +type RouteRow struct { + ForwardDest [4]byte //目标网络 + ForwardMask [4]byte //掩码 + ForwardPolicy uint32 //ForwardPolicy:0x0 + ForwardNextHop [4]byte //网关 + ForwardIfIndex uint32 // 网卡索引 id + ForwardType uint32 //3 本地接口 4 远端接口 + ForwardProto uint32 //3静态路由 2本地接口 5EGP网关 + ForwardAge uint32 //存在时间 秒 + ForwardNextHopAS uint32 //下一跳自治域号码 0 + ForwardMetric1 uint32 //度量衡(跃点数),根据 ForwardProto 不同意义不同。 + ForwardMetric2 uint32 + ForwardMetric3 uint32 + ForwardMetric4 uint32 + ForwardMetric5 uint32 +} + +func (rr *RouteRow) GetForwardDest() net.IP { + return net.IP(rr.ForwardDest[:]) +} +func (rr *RouteRow) GetForwardMask() net.IP { + return net.IP(rr.ForwardMask[:]) +} +func (rr *RouteRow) GetForwardNextHop() net.IP { + return net.IP(rr.ForwardNextHop[:]) +} + +func GetRoutes() ([]RouteRow, error) { + buf := make([]byte, 4+unsafe.Sizeof(RouteRow{})) + buf_len := uint32(len(buf)) + + proc_getIpForwardTable.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buf_len)), 0) + + var r1 uintptr + for i := 0; i < 5; i++ { + buf = make([]byte, buf_len) + r1, _, _ = proc_getIpForwardTable.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buf_len)), 0) + if r1 == 122 { + continue + } + break + } + + if r1 != 0 { + return nil, fmt.Errorf("Failed to get the routing table, return value:%v", r1) + } + + num := *(*uint32)(unsafe.Pointer(&buf[0])) + routes := make([]RouteRow, num) + sr := uintptr(unsafe.Pointer(&buf[0])) + unsafe.Sizeof(num) + rowSize := unsafe.Sizeof(RouteRow{}) + + // 安全检查 + if len(buf) < int((unsafe.Sizeof(num) + rowSize*uintptr(num))) { + return nil, fmt.Errorf("System error: GetIpForwardTable returns the number is too long, beyond the buffer。") + } + + for i := uint32(0); i < num; i++ { + routes[i] = *((*RouteRow)(unsafe.Pointer(sr + (rowSize * uintptr(i))))) + } + + return routes, nil +} diff --git a/go/main.go b/go/main.go new file mode 100644 index 0000000..80f970d --- /dev/null +++ b/go/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + _ "unsafe" + + "github.com/v2fly/v2ray-core/v5/main/commands" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +//go:linkname build github.com/v2fly/v2ray-core/v5.build +var build string + +var version_v2ray string = "N/A" +var version_standalone string = "N/A" + +func main() { + fmt.Println("V2Ray:", version_v2ray, "Version:", version_standalone) + fmt.Println() + + // nekoray + if len(os.Args) > 1 && os.Args[1] == "nekoray" { + NekorayCore() + return + } + + // toolbox + if len(os.Args) > 1 && os.Args[1] == "tool" { + ToolBox() + return + } + + build = "Matsuridayo/Nekoray" + main_v2ray_v5() +} + +func main_v2ray_v5() { + base.RootCommand.Long = "A unified platform for anti-censorship." + base.RegisterCommand(commands.CmdRun) + base.RegisterCommand(commands.CmdVersion) + base.RegisterCommand(commands.CmdTest) + base.SortLessFunc = runIsTheFirst + base.SortCommands() + base.Execute() +} + +func runIsTheFirst(i, j *base.Command) bool { + left := i.Name() + right := j.Name() + if left == "run" { + return true + } + if right == "run" { + return false + } + return left < right +} diff --git a/go/protect_bindinterface_windows.go b/go/protect_bindinterface_windows.go new file mode 100644 index 0000000..772206a --- /dev/null +++ b/go/protect_bindinterface_windows.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/binary" + "log" + "nekoray_core/iphlpapi" + "net" + "strings" + "sync" + "syscall" + "unsafe" + + "github.com/v2fly/v2ray-core/v5/transport/internet" +) + +// https://docs.microsoft.com/en-us/windows/win32/api/ipmib/ns-ipmib-mib_ipforwardrow +var routes []iphlpapi.RouteRow +var interfaces []net.Interface +var lock sync.Mutex + +func init() { + internet.RegisterListenerController(func(network, address string, fd uintptr) error { + bindInterfaceIndex := getBindInterfaceIndex() + if bindInterfaceIndex != 0 { + if err := bindInterface(fd, bindInterfaceIndex, true, true); err != nil { + log.Println("bind inbound interface", err) + return err + } + } + return nil + }) + internet.RegisterDialerController(func(network, address string, fd uintptr) error { + bindInterfaceIndex := getBindInterfaceIndex() + if bindInterfaceIndex != 0 { + var v4, v6 bool + if strings.HasSuffix(network, "6") { + v4 = false + v6 = true + } else { + v4 = true + v6 = false + } + if err := bindInterface(fd, bindInterfaceIndex, v4, v6); err != nil { + log.Println("bind outbound interface", err) + return err + } + } + return nil + }) + iphlpapi.RegisterNotifyRouteChange2(func(callerContext uintptr, row uintptr, notificationType uint32) uintptr { + updateRoutes() + return 0 + }, true) +} + +func updateRoutes() { + lock.Lock() + defer lock.Unlock() + + var err error + routes, err = iphlpapi.GetRoutes() + if err != nil { + log.Println("warning: GetRoutes failed", err) + } + interfaces, err = net.Interfaces() + if err != nil { + log.Println("warning: Interfaces failed", err) + } +} + +func getBindInterfaceIndex() uint32 { + lock.Lock() + defer lock.Unlock() + + if routes == nil { + log.Println("warning: no routes info") + return 0 + } + if interfaces == nil { + log.Println("warning: no interfaces info") + return 0 + } + + var nextInterface int + for i, intf := range interfaces { + if intf.Name == "nekoray-tun" || intf.Name == "wintun" || intf.Name == "TunMax" { + if len(interfaces) > i+1 { + nextInterface = interfaces[i+1].Index + } + break + } + } + + // Not in VPN mode + if nextInterface == 0 { + return 0 + } + + for _, route := range routes { + // MIB_IPROUTE_TYPE_INDIRECT + if route.ForwardType == 4 { + // MIB_IPPROTO_NETMGMT + if route.ForwardProto == 3 { + if route.GetForwardMask().IsUnspecified() { + return route.ForwardIfIndex + } + } + } + } + + // Default route not found + return uint32(nextInterface) +} + +const ( + IP_UNICAST_IF = 31 // nolint: golint,stylecheck + IPV6_UNICAST_IF = 31 // nolint: golint,stylecheck +) + +func bindInterface(fd uintptr, interfaceIndex uint32, v4, v6 bool) error { + if v4 { + /* MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros. */ + bytes := make([]byte, 4) + binary.BigEndian.PutUint32(bytes, interfaceIndex) + interfaceIndex_v4 := *(*uint32)(unsafe.Pointer(&bytes[0])) + + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(interfaceIndex_v4)); err != nil { + return err + } + } + + if v6 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, int(interfaceIndex)); err != nil { + return err + } + } + + return nil +} diff --git a/go/protect_fwmark_linux.go b/go/protect_fwmark_linux.go new file mode 100644 index 0000000..737033d --- /dev/null +++ b/go/protect_fwmark_linux.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "libcore/protect" + "log" + "strings" + "syscall" + + "github.com/jsimonetti/rtnetlink" + linuxcap "kernel.org/pub/linux/libs/security/libcap/cap" +) + +type fwmarkProtector struct{} + +var rtnetlink_conn *rtnetlink.Conn +var cap_net_admin = 0 + +func (f *fwmarkProtector) Protect(fd int32) bool { + if cap_net_admin == 0 { + str := strings.ToLower(linuxcap.GetProc().String()) + if strings.Contains(str, "cap_net_admin") || str == "=ep" { + cap_net_admin = 1 + } else { + cap_net_admin = -1 + } + } + + // check is in VPN mode + if is_fwmark_exist(514) { + if cap_net_admin == 1 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, 514); err != nil { + log.Println("syscall.SetsockoptInt:", err) + return false + } + } else { + if err := cmsgProtect(int(fd), "./protect"); err != nil { + log.Println("cmsgProtect:", err) + return false + } + } + } + return true +} + +func cmsgProtect(fd int, unixPath string) error { + socket, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + return err + } + defer syscall.Close(socket) + + syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Sec: 3}) + syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Sec: 3}) + + err = syscall.Connect(socket, &syscall.SockaddrUnix{Name: unixPath}) + if err != nil { + return err + } + + err = syscall.Sendmsg(socket, nil, syscall.UnixRights(fd), nil, 0) + if err != nil { + return err + } + + dummy := []byte{1} + n, err := syscall.Read(socket, dummy) + if err != nil { + return err + } + if n != 1 { + return fmt.Errorf("cmsgProtect protect failed") + } + return nil +} + +func is_fwmark_exist(number int) bool { + var err error + + if rtnetlink_conn == nil { + rtnetlink_conn, err = rtnetlink.Dial(nil) + if err != nil { + log.Println(err) + } + return false + } + + rules, err := rtnetlink_conn.Rule.List() + if err != nil { + rtnetlink_conn = nil + return false + } + + for _, rule := range rules { + if rule.Attributes != nil && rule.Attributes.FwMark != nil && uint32(number) == *rule.Attributes.FwMark { + return true + } + } + + return false +} + +func init() { + protect.FdProtector = &fwmarkProtector{} +} diff --git a/go/protect_server/protect_server_linux.go b/go/protect_server/protect_server_linux.go new file mode 100644 index 0000000..737a7ed --- /dev/null +++ b/go/protect_server/protect_server_linux.go @@ -0,0 +1,92 @@ +package protect_server + +import ( + "fmt" + "log" + "net" + "os" + "os/signal" + "reflect" + "syscall" +) + +func getOneFd(socket int) (int, error) { + // recvmsg + buf := make([]byte, syscall.CmsgSpace(4)) + _, _, _, _, err := syscall.Recvmsg(socket, nil, buf, 0) + if err != nil { + return 0, err + } + + // parse control msgs + var msgs []syscall.SocketControlMessage + msgs, err = syscall.ParseSocketControlMessage(buf) + + if len(msgs) != 1 { + return 0, fmt.Errorf("invaild msgs count: %d", len(msgs)) + } + + var fds []int + fds, err = syscall.ParseUnixRights(&msgs[0]) + if len(fds) != 1 { + return 0, fmt.Errorf("invaild fds count: %d", len(fds)) + } + return fds[0], nil +} + +// GetFdFromConn get net.Conn's file descriptor. +func GetFdFromConn(l net.Conn) int { + v := reflect.ValueOf(l) + netFD := reflect.Indirect(reflect.Indirect(v).FieldByName("fd")) + pfd := reflect.Indirect(netFD.FieldByName("pfd")) + fd := int(pfd.FieldByName("Sysfd").Int()) + return fd +} + +func ServeProtect(path string, fwmark int) { + os.Remove(path) + defer os.Remove(path) + + l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"}) + if err != nil { + log.Fatal(err) + } + defer l.Close() + + os.Chmod(path, 0777) + + go func() { + for { + c, err := l.Accept() + if err != nil { + log.Println("Accept:", err) + return + } + + go func() { + socket := GetFdFromConn(c) + defer c.Close() + + fd, err := getOneFd(socket) + if err != nil { + log.Println("getOneFd:", err) + return + } + + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, fwmark); err != nil { + log.Println("syscall.SetsockoptInt:", err) + } + + if err == nil { + c.Write([]byte{1}) + } else { + c.Write([]byte{0}) + } + }() + } + }() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/go/protect_server/protect_server_other.go b/go/protect_server/protect_server_other.go new file mode 100644 index 0000000..f6556c8 --- /dev/null +++ b/go/protect_server/protect_server_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package protect_server + +import "log" + +func ServeProtect(path string, fwmark int) { + log.Println("ServeProtect is not for this platform") +} diff --git a/go/toolbox_linux.go b/go/toolbox_linux.go new file mode 100644 index 0000000..8ce44b9 --- /dev/null +++ b/go/toolbox_linux.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "log" + "nekoray_core/protect_server" + "os" + + "github.com/jsimonetti/rtnetlink" + linuxcap "kernel.org/pub/linux/libs/security/libcap/cap" +) + +func ToolBox() { + // + var protectListenPath string + var protectFwMark int + // + flag.StringVar(&protectListenPath, "protect-listen-path", "", "Set unix protect server listen path (Linux ROOT only)") + flag.IntVar(&protectFwMark, "protect-fwmark", 0, "Set unix protect fwmark (Linux ROOT only)") + flag.CommandLine.Parse(os.Args[3:]) + // + switch os.Args[2] { + case "rule": + { + // Dial a connection to the rtnetlink socket + conn, err := rtnetlink.Dial(nil) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // Request a list of rules + rules, err := conn.Rule.List() + if err != nil { + log.Fatal(err) + } + + for _, rule := range rules { + log.Printf("%+v", rule) + log.Printf("%+v", rule.Attributes) + } + + for _, rule := range rules { + if rule.Attributes.FwMark != nil { + log.Printf("%+v", rule.Attributes) + log.Println(*rule.Attributes.FwMark, *rule.Attributes.Table) + } + } + } + case "cap": + { + set := linuxcap.GetProc() + if set != nil { + log.Println(set) + } + } + case "protect": + { + if protectListenPath == "" { + log.Println("missing protect-listen-path") + return + } + log.Println(protectListenPath, protectFwMark) + protect_server.ServeProtect(protectListenPath, protectFwMark) + } + } +} diff --git a/go/toolbox_other.go b/go/toolbox_other.go new file mode 100644 index 0000000..b155827 --- /dev/null +++ b/go/toolbox_other.go @@ -0,0 +1,6 @@ +//go:build !windows && !linux + +package main + +func ToolBox() { +} diff --git a/go/toolbox_windows.go b/go/toolbox_windows.go new file mode 100644 index 0000000..8716ee3 --- /dev/null +++ b/go/toolbox_windows.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "net" + "os" +) + +func ToolBox() { + switch os.Args[2] { + case "if": + { + intfs, err := net.Interfaces() + if err != nil { + log.Fatalln(err) + } + for _, intf := range intfs { + log.Println(intf) + } + for _, route := range routes { + log.Println(route) + } + log.Println("Upstream:", getBindInterfaceIndex()) + } + } +} diff --git a/go/tun_linux.go b/go/tun_linux.go new file mode 100644 index 0000000..c380957 --- /dev/null +++ b/go/tun_linux.go @@ -0,0 +1,62 @@ +package main + +import ( + "errors" + "libcore" + "nekoray_core/gen" + "sync" + "syscall" + + gvisorTun "gvisor.dev/gvisor/pkg/tcpip/link/tun" +) + +var tun2ray *libcore.Tun2ray +var tun_fd int +var tun_lock sync.Mutex + +func TunStart(config *gen.SetTunReq) (err error) { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + return errors.New("tun aleary started") + } + + tun_fd, err = gvisorTun.Open(config.Name) + if err != nil { + return + } + + tun2ray, err = libcore.NewTun2ray(&libcore.TunConfig{ + FileDescriptor: int32(tun_fd), + MTU: config.Mtu, + V2Ray: instance, // use current if started + Implementation: config.Implementation, + Sniffing: true, + FakeDNS: config.Fakedns, + }) + return +} + +func TunStop() { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + tun2ray.Close() + tun2ray = nil + if tun_fd > 0 { + syscall.Close(tun_fd) + } + tun_fd = 0 + } +} + +func TunSetV2ray(i *libcore.V2RayInstance) { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + tun2ray.SetV2ray(i) + } +} diff --git a/go/tun_stub.go b/go/tun_stub.go new file mode 100644 index 0000000..464cf45 --- /dev/null +++ b/go/tun_stub.go @@ -0,0 +1,19 @@ +//go:build !linux + +package main + +import ( + "errors" + "libcore" + "nekoray_core/gen" +) + +func TunStart(config *gen.SetTunReq) error { + return errors.New("not for this platform") +} + +func TunStop() { +} + +func TunSetV2ray(i *libcore.V2RayInstance) { +} diff --git a/libs/.gitignore b/libs/.gitignore new file mode 100644 index 0000000..f428a32 --- /dev/null +++ b/libs/.gitignore @@ -0,0 +1,2 @@ +deps +downloaded \ No newline at end of file diff --git a/libs/README b/libs/README new file mode 100644 index 0000000..ec17a4a --- /dev/null +++ b/libs/README @@ -0,0 +1,6 @@ +依赖 +libs/deps/* +libs/deps/windows-x64 (vcpkg) +libs/deps/built (prefix) + +全部在项目根目录运行 diff --git a/libs/build_deps_all.sh b/libs/build_deps_all.sh new file mode 100755 index 0000000..d2711b8 --- /dev/null +++ b/libs/build_deps_all.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e + +cd libs + +# libs/deps/... +mkdir -p deps; cd deps +INSTLL_PREFIX=$PWD/built +mkdir -p $INSTLL_PREFIX + +#### yaml-cpp #### +curl -L -o dl.zip https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.zip +unzip dl.zip + +cd yaml-* +mkdir -p build; cd build + +cmake .. -GNinja -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### ZXing #### +curl -L -o dl.zip https://github.com/nu-book/zxing-cpp/archive/refs/tags/v1.3.0.zip +unzip dl.zip + +cd zxing-* +mkdir -p build; cd build + +cmake .. -GNinja -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF -DBUILD_BLACKBOX_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### protobuf #### +git clone --recurse-submodules -b v21.4 --depth 1 --shallow-submodules https://github.com/protocolbuffers/protobuf + +#备注:交叉编译要在 host 也安装 protobuf 并且版本一致,编译安装,同参数,安装到 /usr/local + +mkdir -p protobuf/build +cd protobuf/build + +cmake .. -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -Dprotobuf_MSVC_STATIC_RUNTIME=OFF \ + -Dprotobuf_BUILD_TESTS=OFF \ + -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### clean #### +rm -rf dl.zip yaml-* zxing-* protobuf diff --git a/libs/deploy_common.sh b/libs/deploy_common.sh new file mode 100644 index 0000000..34d8825 --- /dev/null +++ b/libs/deploy_common.sh @@ -0,0 +1,46 @@ +SRC_ROOT="$PWD" +DEST="$PWD/deployment/nekoray" +BUILD="$SRC_ROOT/build" + +mkdir -p $DEST +mkdir -p $BUILD + +export CGO_ENABLED=0 + +#### Go: updater #### +pushd updater +go build -o $DEST -trimpath -ldflags "-w -s" +popd + +#### libcore #### +COMMIT_M=$(cat matsuri_commit.txt) +COMMIT_V=$(cat core_commit.txt) +version_standalone="nekoray-"$(cat nekoray_version.txt) + +pushd .. + +git clone --no-checkout https://github.com/MatsuriDayo/Matsuri.git +git clone --no-checkout https://github.com/MatsuriDayo/v2ray-core.git + +pushd Matsuri +git checkout $COMMIT_M +popd + +pushd v2ray-core +git checkout $COMMIT_V +version_v2ray=$(git log --pretty=format:'%h' -n 1) +popd + +popd + +#### Go: nekoray_core #### +pushd go +go build -o $DEST -trimpath -ldflags "-w -s -X main.version_v2ray=$version_v2ray -X main.version_standalone=$version_standalone" +popd + +#### Download: geoip #### +curl -Lso $DEST/geoip.dat "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/202206042210/geoip.dat" +curl -Lso $DEST/geosite.dat "https://github.com/v2fly/domain-list-community/releases/download/20220604062951/dlc.dat" + +#### copy assets #### +cp assets/* $DEST diff --git a/libs/deploy_linux64.sh b/libs/deploy_linux64.sh new file mode 100755 index 0000000..1859645 --- /dev/null +++ b/libs/deploy_linux64.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +source libs/deploy_common.sh + +#### updater to launcher #### +mv $DEST/updater $DEST/launcher + +#### copy binary #### +cp $BUILD/nekoray $DEST + +#### Download: prebuilt runtime #### +curl -Lso usr.zip https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/20220705-5.15.2-linux64.zip +unzip usr.zip +mv usr $DEST + +#### copy runtime #### +LIB=$SRC_ROOT/libs/deps/built/lib +#cp $LIB/libZXing.so.1 $DEST/usr/lib + +#### pack tar #### +chmod +x $DEST/nekoray $DEST/nekoray_core $DEST/launcher +tar cvzf $SRC_ROOT/deployment/$version_standalone-linux64.tar.gz -C $SRC_ROOT/deployment nekoray +rm -rf $DEST $BUILD diff --git a/libs/deploy_windows64.sh b/libs/deploy_windows64.sh new file mode 100755 index 0000000..898223a --- /dev/null +++ b/libs/deploy_windows64.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +source libs/deploy_common.sh + +#### Go: sing-box #### +pushd $BUILD +curl -Lso sing-box.zip https://github.com/SagerNet/sing-box/archive/64dbac813837bbadfaeec1a6e0d064875a123e5e.zip +unzip sing-box.zip +pushd sing-box-*/cmd/sing-box +go build -o $DEST -trimpath -ldflags "-w -s" +popd +popd + +#### copy exe #### +cp $BUILD/nekoray.exe $DEST + +#### deploy qt & DLL runtime #### +pushd $DEST +windeployqt nekoray.exe --no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --verbose 2 +curl -LSsO https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/libcrypto-1_1-x64.dll +curl -LSsO https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/libssl-1_1-x64.dll +rm -rf translations +popd + +#### pack zip #### +7z a $SRC_ROOT/deployment/$version_standalone-windows64.zip $DEST +cp $BUILD/*.pdb $SRC_ROOT/deployment/ +rm -rf $DEST $BUILD diff --git a/libs/dl.sh b/libs/dl.sh new file mode 100755 index 0000000..a5c9ab1 --- /dev/null +++ b/libs/dl.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +cd libs + +mkdir -p deps; cd ./deps +mkdir -p downloaded; cd ./downloaded; + +NAME=$1 +echo "Downloading: $NAME" +curl -sL $2 -o $NAME; + +cd .. + +for f in $(ls ./downloaded) +do + 7z x -y ./downloaded/$f +done + +# libs/deps/windows-x64/installed/ + +rm -rf downloaded diff --git a/main/Const.hpp b/main/Const.hpp new file mode 100644 index 0000000..d1863c5 --- /dev/null +++ b/main/Const.hpp @@ -0,0 +1,28 @@ +#pragma once + +namespace NekoRay { + namespace DomainMatcher { + enum DomainMatcher { + DEFAULT, + MPH, + }; + } + + namespace SniffingMode { + enum SniffingMode { + DISABLE, + FOR_ROUTING, + FOR_DESTINATION, + }; + } + + namespace SystemProxyMode { + enum SystemProxyMode { + DISABLE, + SYSTEM_PROXY, + VPN, + }; + } + +} + diff --git a/main/GuiUtils.hpp b/main/GuiUtils.hpp new file mode 100644 index 0000000..d4bc784 --- /dev/null +++ b/main/GuiUtils.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +// Dialogs + +inline QWidget *mainwindow; + +#define Dialog_DialogBasicSettings "DialogBasicSettings" +#define Dialog_DialogEditProfile "DialogEditProfile" +#define Dialog_DialogManageGroups "DialogManageGroups" +#define Dialog_DialogManageRoutes "DialogManageRoutes" + +// Utils + +inline QList +CreateActions(QWidget *parent, const QList &texts, const std::function &slot) { + QList acts; + + for (const auto &text: texts) { + acts.push_back(new QAction(text, parent)); //按顺序来 + } + + for (int i = 0; i < acts.size(); i++) { + if (acts[i]->text() == "[Separator]") { + acts[i]->setSeparator(true); + acts[i]->setText(""); + acts[i]->setDisabled(true); + acts[i]->setData(-1); + } else { + acts[i]->setData(i); + QObject::connect(acts[i], &QAction::triggered, parent, [=] { + slot(acts[i]); + }); + } + } + + return acts; +} + +inline QMenu *CreateMenu(QWidget *parent, const QList &texts, const std::function &slot) { + auto menu = new QMenu(parent); + menu->addActions(CreateActions(parent, texts, slot)); + return menu; +} + +#define QRegExpValidator_Number new QRegularExpressionValidator(QRegularExpression("^[0-9]+$") + +// NekoRay Save&Load + +#define P_LOAD_STRING(a) ui->a->setText(bean->a); +#define P_SAVE_STRING(a) bean->a = ui->a->text(); +#define P_SAVE_STRING_QTEXTEDIT(a) bean->a = ui->a->toPlainText(); +#define D_LOAD_STRING(a) ui->a->setText(NekoRay::dataStore->a); +#define D_SAVE_STRING(a) NekoRay::dataStore->a = ui->a->text(); +#define P_C_LOAD_STRING(a) CACHE.a = bean->a; +#define P_C_SAVE_STRING(a) bean->a = CACHE.a; +#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a; +#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a; +#define P_LOAD_INT(a) ui->a->setText(Int2String(bean->a)); ui->a->setValidator(QRegExpValidator_Number, this)); +#define P_SAVE_INT(a) bean->a = ui->a->text().toInt(); +#define D_LOAD_INT(a) ui->a->setText(Int2String(NekoRay::dataStore->a)); ui->a->setValidator(QRegExpValidator_Number, this)); +#define D_SAVE_INT(a) NekoRay::dataStore->a = ui->a->text().toInt(); +#define P_LOAD_COMBO(a) ui->a->setCurrentText(bean->a); +#define P_SAVE_COMBO(a) bean->a = ui->a->currentText(); + +#define D_LOAD_INT_ENABLE(i, e) \ +if (NekoRay::dataStore->i > 0) { \ +ui->e->setChecked(true); \ +ui->i->setText(Int2String(NekoRay::dataStore->i)); \ +} else { \ +ui->e->setChecked(false); \ +ui->i->setText(Int2String(-NekoRay::dataStore->i)); \ +} \ +ui->i->setValidator(QRegExpValidator_Number, this)); +#define D_SAVE_INT_ENABLE(i, e) \ +if (ui->e->isChecked()) { \ +NekoRay::dataStore->i = ui->i->text().toInt(); \ +} else { \ +NekoRay::dataStore->i = -ui->i->text().toInt(); \ +} + +#define C_EDIT_JSON_ALLOW_EMPTY(a) auto editor = new JsonEditor(QString2QJsonObject(CACHE.a), this); \ +auto result = editor->OpenEditor(); \ +CACHE.a = QJsonObject2QString(result, true); \ +if (result.isEmpty()) CACHE.a = ""; \ +editor->deleteLater(); diff --git a/main/NekoRay.cpp b/main/NekoRay.cpp new file mode 100644 index 0000000..d5ac8d0 --- /dev/null +++ b/main/NekoRay.cpp @@ -0,0 +1,320 @@ +#include "NekoRay.hpp" + +#include +#include + +namespace NekoRay { + + DataStore *dataStore = new DataStore(); + + // datastore + + DataStore::DataStore() : JsonStore("groups/nekoray.json") { + _add(new configItem("extraCore", dynamic_cast(extraCore), itemType::jsonStore)); + + _add(new configItem("core_path", &core_path, itemType::string)); + _add(new configItem("user_agent", &user_agent, itemType::string)); + _add(new configItem("test_url", &test_url, itemType::string)); + _add(new configItem("current_group", ¤t_group, itemType::integer)); + _add(new configItem("inbound_address", &inbound_address, itemType::string)); + _add(new configItem("inbound_socks_port", &inbound_socks_port, itemType::integer)); + _add(new configItem("inbound_http_port", &inbound_http_port, itemType::integer)); + _add(new configItem("log_level", &log_level, itemType::string)); + _add(new configItem("remote_dns", &remote_dns, itemType::string)); + _add(new configItem("direct_dns", &direct_dns, itemType::string)); + _add(new configItem("domain_matcher", &domain_matcher, itemType::integer)); + _add(new configItem("domain_strategy", &domain_strategy, itemType::string)); + _add(new configItem("outbound_domain_strategy", &outbound_domain_strategy, itemType::string)); + _add(new configItem("sniffing_mode", &sniffing_mode, itemType::integer)); + _add(new configItem("mux_cool", &mux_cool, itemType::integer)); + _add(new configItem("traffic_loop_interval", &traffic_loop_interval, itemType::integer)); + _add(new configItem("dns_routing", &dns_routing, itemType::boolean)); + _add(new configItem("test_concurrent", &test_concurrent, itemType::integer)); + _add(new configItem("theme", &theme, itemType::string)); + _add(new configItem("custom_inbound", &custom_inbound, itemType::string)); + _add(new configItem("custom_route", &custom_route_global, itemType::string)); + _add(new configItem("v2ray_asset_dir", &v2ray_asset_dir, itemType::string)); + _add(new configItem("sub_use_proxy", &sub_use_proxy, itemType::boolean)); + _add(new configItem("enhance_domain", &enhance_resolve_server_domain, itemType::boolean)); + _add(new configItem("remember_id", &remember_id, itemType::integer)); + _add(new configItem("remember_enable", &remember_enable, itemType::boolean)); + _add(new configItem("start_minimal", &start_minimal, itemType::boolean)); + _add(new configItem("language", &language, itemType::integer)); + _add(new configItem("spmode", &system_proxy_mode, itemType::integer)); + _add(new configItem("insecure_hint", &insecure_hint, itemType::boolean)); + _add(new configItem("skip_cert", &skip_cert, itemType::boolean)); + _add(new configItem("hk_mw", &hotkey_mainwindow, itemType::string)); + _add(new configItem("hk_group", &hotkey_group, itemType::string)); + _add(new configItem("hk_route", &hotkey_route, itemType::string)); + _add(new configItem("fakedns", &fake_dns, itemType::boolean)); + _add(new configItem("active_routing", &active_routing, itemType::string)); + _add(new configItem("mw_size", &mw_size, itemType::string)); + _add(new configItem("conn_stat", &connection_statistics, itemType::boolean)); + } + + void DataStore::UpdateStartedId(int id) { + started_id = id; + if (remember_enable) { + remember_id = id; + Save(); + } else if (remember_id >= 0) { + remember_id = -1919; + Save(); + } + } + + // preset routing + Routing::Routing(int preset) : JsonStore() { + if (preset == 1) { + direct_ip = "geoip:cn\n" + "geoip:private"; + direct_domain = "geosite:cn"; + proxy_ip = ""; + proxy_domain = ""; + block_ip = ""; + block_domain = "geosite:category-ads-all\n" + "domain:appcenter.ms\n" + "domain:app-measurement.com\n" + "domain:firebase.io\n" + "domain:crashlytics.com\n" + "domain:google-analytics.com"; + } + _add(new configItem("direct_ip", &this->direct_ip, itemType::string)); + _add(new configItem("direct_domain", &this->direct_domain, itemType::string)); + _add(new configItem("proxy_ip", &this->proxy_ip, itemType::string)); + _add(new configItem("proxy_domain", &this->proxy_domain, itemType::string)); + _add(new configItem("block_ip", &this->block_ip, itemType::string)); + _add(new configItem("block_domain", &this->block_domain, itemType::string)); + _add(new configItem("custom", &this->custom, itemType::string)); + } + + QString Routing::toString() const { + return QString("[Proxy] %1\n[Proxy] %2\n[Direct] %3\n[Direct] %4\n[Block] %5\n[Block] %6") + .arg(SplitLines(proxy_domain).join(",")) + .arg(SplitLines(proxy_ip).join(",")) + .arg(SplitLines(direct_domain).join(",")) + .arg(SplitLines(direct_ip).join(",")) + .arg(SplitLines(block_domain).join(",")) + .arg(SplitLines(block_ip).join(",")); + } + + QStringList Routing::List() { + QStringList l; + QDir d; + if (d.exists("routes")) { + QDir dr("routes"); + return dr.entryList(QDir::Files); + } + return l; + } + + void Routing::SetToActive(const QString &name) { + dataStore->routing->fn = "routes/" + name; + dataStore->routing->Load(); + dataStore->active_routing = name; + dataStore->Save(); + } + + // NO default extra core + + ExtraCore::ExtraCore() : JsonStore() { + _add(new configItem("core_map", &this->core_map, itemType::string)); + } + + QString ExtraCore::Get(const QString &id) const { + auto obj = QString2QJsonObject(core_map); + for (const auto &c: obj.keys()) { + if (c == id) return obj[id].toString(); + } + return ""; + } + + void ExtraCore::Set(const QString &id, const QString &path) { + auto obj = QString2QJsonObject(core_map); + obj[id] = path; + core_map = QJsonObject2QString(obj, true); + } + + void ExtraCore::Delete(const QString &id) { + auto obj = QString2QJsonObject(core_map); + obj.remove(id); + core_map = QJsonObject2QString(obj, true); + } + + // 添加关联 + void JsonStore::_add(configItem *item) { + _map.insert(item->name, QSharedPointer(item)); + } + + QSharedPointer JsonStore::_get(const QString &name) { + // 直接 [] 会设置一个 nullptr ,所以先判断是否存在 + if (_map.contains(name)) { + return _map[name]; + } + return nullptr; + } + + QJsonObject JsonStore::ToJson() { + QJsonObject object; + for (const auto &_item: _map) { + auto item = _item.get(); + switch (item->type) { + case itemType::string: + object.insert(item->name, *(QString *) item->ptr); + break; + case itemType::integer: + object.insert(item->name, *(int *) item->ptr); + break; + case itemType::integer64: + object.insert(item->name, *(long long *) item->ptr); + break; + case itemType::boolean: + object.insert(item->name, *(bool *) item->ptr); + break; + case itemType::stringList: + object.insert(item->name, QList2QJsonArray(*(QList *) item->ptr)); + break; + case itemType::integerList: + object.insert(item->name, QList2QJsonArray(*(QList *) item->ptr)); + break; + case itemType::jsonStore: + // _add 时应关联对应 JsonStore 的指针 + object.insert(item->name, ((JsonStore *) item->ptr)->ToJson()); + break; + } + } + return object; + } + + QByteArray JsonStore::ToJsonBytes() { + QJsonDocument document; + document.setObject(ToJson()); + return document.toJson(save_control_compact ? QJsonDocument::Compact : QJsonDocument::Indented); + } + + void JsonStore::FromJson(QJsonObject object) { + for (const auto &key: object.keys()) { + if (_map.count(key) == 0) { + if (debug_verbose) { + qDebug() << QString("unknown key\n%1\n%2").arg(key, QJsonObject2QString(object, false)); + } + continue; + } + + auto value = object[key]; + auto item = _map[key].get(); + + if (item == nullptr) + continue; // 故意忽略 + + // 根据类型修改ptr的内容 + switch (item->type) { + case itemType::string: + if (value.type() != QJsonValue::String) { + MessageBoxWarning("错误", "Not a string\n" + key); + continue; + } + *(QString *) item->ptr = value.toString(); + break; + case itemType::integer: + if (value.type() != QJsonValue::Double) { + MessageBoxWarning("错误", "Not a int\n" + key); + continue; + } + *(int *) item->ptr = value.toInt(); + break; + case itemType::integer64: + if (value.type() != QJsonValue::Double) { + MessageBoxWarning("错误", "Not a int64\n" + key); + continue; + } + *(long long *) item->ptr = value.toDouble(); + break; + case itemType::boolean: + if (value.type() != QJsonValue::Bool) { + MessageBoxWarning("错误", "Not a bool\n" + key); + continue; + } + *(bool *) item->ptr = value.toBool(); + break; + case itemType::stringList: + if (value.type() != QJsonValue::Array) { + MessageBoxWarning("错误", "Not a Array\n" + key); + continue; + } + *(QList *) item->ptr = QJsonArray2QListString(value.toArray()); + break; + case itemType::integerList: + if (value.type() != QJsonValue::Array) { + MessageBoxWarning("错误", "Not a Array\n" + key); + continue; + } + *(QList *) item->ptr = QJsonArray2QListInt(value.toArray()); + break; + case itemType::jsonStore: + if (value.type() != QJsonValue::Object) { + MessageBoxWarning("错误", "Not a json object\n" + key); + continue; + } + if (load_control_no_jsonStore) + continue; + ((JsonStore *) item->ptr)->FromJson(value.toObject()); + break; + } + } + + for (const auto &hook: _hooks_after_load) { + hook(); + } + } + + void JsonStore::FromJsonBytes(const QByteArray &data) { + QJsonParseError error{}; + auto document = QJsonDocument::fromJson(data, &error); + + if (error.error != error.NoError) { + if (debug_verbose) qDebug() << "QJsonParseError" << error.errorString(); + return; + } + + FromJson(document.object()); + } + + bool JsonStore::Save() { + for (const auto &hook: _hooks_before_save) { + hook(); + } + + auto save_content = ToJsonBytes(); + auto changed = last_save_content != save_content; + last_save_content = save_content; + + QFile file; + file.setFileName(fn); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + file.write(save_content); + file.close(); + + return changed; + } + + bool JsonStore::Load() { + QFile file; + file.setFileName(fn); + + if (!file.exists() && !load_control_force) + return false; + + bool ok = file.open(QIODevice::ReadOnly); + if (!ok) { + MessageBoxWarning("error", "can not open config " + fn + "\n" + file.errorString()); + } else { + last_save_content = file.readAll(); + FromJsonBytes(last_save_content); + } + + file.close(); + return ok; + } + +} diff --git a/main/NekoRay.hpp b/main/NekoRay.hpp new file mode 100644 index 0000000..c2bf117 --- /dev/null +++ b/main/NekoRay.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "Const.hpp" +#include "NekoRay_Utils.hpp" +#include "NekoRay_ConfigItem.hpp" +#include "NekoRay_DataStore.hpp" diff --git a/main/NekoRay_ConfigItem.hpp b/main/NekoRay_ConfigItem.hpp new file mode 100644 index 0000000..377ffe4 --- /dev/null +++ b/main/NekoRay_ConfigItem.hpp @@ -0,0 +1,63 @@ +// DO NOT INCLUDE THIS + +namespace NekoRay { + // config 工具 + enum itemType { + string, + integer, + integer64, + boolean, + stringList, + integerList, + jsonStore, + }; + + class configItem { + public: + QString name; + void *ptr; + itemType type; + + configItem(QString n, void *p, itemType t) { + name = std::move(n); + ptr = p; + type = t; + } + }; + + // 可格式化对象 + class JsonStore { + public: + QMap> _map; + QList> _hooks_after_load; + QList> _hooks_before_save; + QString fn; + bool debug_verbose = false; + bool load_control_force = false; + bool load_control_no_jsonStore = false; //不加载 json object + bool save_control_compact = false; + QByteArray last_save_content; + + JsonStore() = default; + + explicit JsonStore(QString fileName) { + fn = std::move(fileName); + } + + void _add(configItem *item); + + QSharedPointer _get(const QString &name); + + QJsonObject ToJson(); + + QByteArray ToJsonBytes(); + + void FromJson(QJsonObject object); + + void FromJsonBytes(const QByteArray &data); + + bool Save(); + + bool Load(); + }; +} diff --git a/main/NekoRay_DataStore.hpp b/main/NekoRay_DataStore.hpp new file mode 100644 index 0000000..d81addf --- /dev/null +++ b/main/NekoRay_DataStore.hpp @@ -0,0 +1,122 @@ +// DO NOT INCLUDE THIS + +namespace NekoRay { + + class Routing : public JsonStore { + public: + QString direct_ip; + QString direct_domain; + QString proxy_ip; + QString proxy_domain; + QString block_ip; + QString block_domain; + QString custom = "{\"rules\": []}"; + + explicit Routing(int preset = 0); + + QString toString() const; + + static QStringList List(); + + static void SetToActive(const QString &name); + }; + + class ExtraCore : public JsonStore { + public: + QString core_map; + + explicit ExtraCore(); + + [[nodiscard]] QString Get(const QString &id) const; + + void Set(const QString &id, const QString &path); + + void Delete(const QString &id); + }; + + class DataStore : public JsonStore { + public: + // Running + + QString core_token; + int core_port = 19810; + int started_id = -1919; + + // Saved + + // Misc + QString core_path = "../nekoray_core"; + QString log_level = "warning"; + QString user_agent = "ClashForAndroid/2.5.9.premium"; + bool sub_use_proxy = false; + QString test_url = "http://cp.cloudflare.com/"; + int test_concurrent = 5; + int traffic_loop_interval = 500; + bool connection_statistics = false; + int current_group = 0; //group id + int mux_cool = -8; + QString theme = "0"; + QString v2ray_asset_dir = ""; + int language = 0; + QString mw_size = ""; + + // Security + bool insecure_hint = true; + bool skip_cert = false; + + // Remember + int system_proxy_mode = NekoRay::SystemProxyMode::DISABLE; + int remember_id = -1919; + bool remember_enable = false; + bool start_minimal = false; + + // Socks & HTTP Inbound + QString inbound_address = "127.0.0.1"; + int inbound_socks_port = 2080; + int inbound_http_port = -2081; + QString custom_inbound = "{\"inbounds\": []}"; + + // DNS + QString remote_dns = "https://8.8.8.8/dns-query"; + QString direct_dns = "https+local://223.5.5.5/dns-query"; + bool dns_routing = true; + bool enhance_resolve_server_domain = false; + + // Routing + bool fake_dns = false; + QString domain_strategy = "AsIs"; + QString outbound_domain_strategy = "AsIs"; + int sniffing_mode = SniffingMode::FOR_ROUTING; + int domain_matcher = DomainMatcher::MPH; + QString custom_route_global = "{\"rules\": []}"; + QString active_routing = "Default"; + + // Hotkey + QString hotkey_mainwindow = ""; + QString hotkey_group = ""; + QString hotkey_route = ""; + + // Other Core + ExtraCore *extraCore = new ExtraCore; + + // Running Cache + + Routing *routing = new Routing; + int imported_count = 0; + bool refreshing_group_list = false; + + // Running Flags + + bool flag_use_appdata = false; + bool flag_many = false; + + // + + DataStore(); + + void UpdateStartedId(int id); + }; + + extern DataStore *dataStore; + +} diff --git a/main/NekoRay_Utils.cpp b/main/NekoRay_Utils.cpp new file mode 100644 index 0000000..841ea08 --- /dev/null +++ b/main/NekoRay_Utils.cpp @@ -0,0 +1,103 @@ +#include "NekoRay_Utils.hpp" + +#include "3rdparty/QThreadCreateThread.hpp" +#include "main/GuiUtils.hpp" + +#include + +#include +#include +#include +#include +#include + +QString GetQueryValue(const QUrlQuery &q, const QString &key, const QString &def) { + auto a = q.queryItemValue(key); + if (a.isEmpty()) { + return def; + } + return a; +} + +QString GetRandomString(int randomStringLength) { + std::random_device rd; + std::mt19937 mt(rd()); + + const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + + std::uniform_int_distribution dist(0, possibleCharacters.count() - 1); + + QString randomString; + for (int i = 0; i < randomStringLength; ++i) { + QChar nextChar = possibleCharacters.at(dist(mt)); + randomString.append(nextChar); + } + return randomString; +} + +QByteArray ReadFile(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly); + return file.readAll(); +} + +QString ReadFileText(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly | QFile::Text); + QTextStream stream(&file); + return stream.readAll(); +} + +int MkPort() { + QTcpServer s; + s.listen(); + auto port = s.serverPort(); + s.close(); + return port; +} + +bool IsIpAddress(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol) + return true; + return false; +} + +bool IsIpAddressV4(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv4Protocol) + return true; + return false; +} + +bool IsIpAddressV6(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv6Protocol) + return true; + return false; +} + +int MessageBoxWarning(const QString &title, const QString &text) { + return QMessageBox::warning(nullptr, title, text); +} + +int MessageBoxInfo(const QString &title, const QString &text) { + return QMessageBox::information(nullptr, title, text); +} + +void runOnUiThread(const std::function &callback, QObject *parent) { + // any thread + auto *timer = new QTimer(); + timer->moveToThread(parent == nullptr ? mainwindow->thread() : parent->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() { + // main thread + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void runOnNewThread(const std::function &callback) { + createQThread(callback)->start(); +} diff --git a/main/NekoRay_Utils.hpp b/main/NekoRay_Utils.hpp new file mode 100644 index 0000000..72c139c --- /dev/null +++ b/main/NekoRay_Utils.hpp @@ -0,0 +1,180 @@ +// DO NOT INCLUDE THIS + +#include +#include +#include +#include + +// Dialogs + +inline std::function showLog; +inline std::function showLog_ext; +inline std::function showLog_ext_vt100; +inline std::function dialog_message; + +// Utils + +#define QJSONARRAY_ADD(arr, add) for(const auto &a: (add)) { (arr) += a; } + +inline QString SubStrBefore(QString str, const QString &sub) { + if (!str.contains(sub)) return str; + return str.left(str.indexOf(sub)); +} + +inline QString SubStrAfter(QString str, const QString &sub) { + if (!str.contains(sub)) return str; + return str.right(str.length() - str.indexOf(sub) - sub.length()); +} + +inline QString +DecodeB64IfValid(const QString &input, QByteArray::Base64Option options = QByteArray::Base64Option::Base64Encoding) { + auto result = QByteArray::fromBase64Encoding(input.toUtf8(), + options | QByteArray::Base64Option::AbortOnBase64DecodingErrors); + if (result) { + return result.decoded; + } + return ""; +} + +#define GetQuery(url) QUrlQuery((url).query(QUrl::ComponentFormattingOption::FullyDecoded)); + +QString GetQueryValue(const QUrlQuery &q, const QString &key, const QString &def = ""); + +inline QString Int2String(int i) { + return QVariant(i).toString(); +} + +inline QString Int2String(qint64 i) { + return QVariant(i).toString(); +} + +QString GetRandomString(int randomStringLength); + +// QString >> QJson +inline QJsonObject QString2QJsonObject(const QString &jsonString) { + QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8()); + QJsonObject jsonObject = jsonDocument.object(); + return jsonObject; +} + +// QJson >> QString +inline QString QJsonObject2QString(const QJsonObject &jsonObject, bool compact) { + return QString(QJsonDocument(jsonObject).toJson(compact ? QJsonDocument::Compact : QJsonDocument::Indented)); +} + +template +inline QJsonArray QList2QJsonArray(const QList &list) { + QVariantList list2; + for (auto &item: list) + list2.append(item); + return QJsonArray::fromVariantList(list2); +} + +inline QList QJsonArray2QListInt(const QJsonArray &arr) { + QList list2; + for (auto item: arr) + list2.append(item.toInt()); + return list2; +} + +inline QList QJsonArray2QListString(const QJsonArray &arr) { + QList list2; + for (auto item: arr) + list2.append(item.toString()); + return list2; +} + +inline QString UrlSafe_encode(const QString &s) { + return s.toUtf8().toPercentEncoding().replace(" ", "%20"); +} + +inline bool InRange(unsigned x, unsigned low, unsigned high) { + return (low <= x && x <= high); +} + +inline QStringList SplitLines(const QString &_string) { + return _string.split(QRegularExpression("[\r\n]"), Qt::SplitBehaviorFlags::SkipEmptyParts); +} + +QByteArray ReadFile(const QString &path); + +QString ReadFileText(const QString &path); + +// Net + +int MkPort(); + +// Validators + +bool IsIpAddress(const QString &str); + +bool IsIpAddressV4(const QString &str); + +bool IsIpAddressV6(const QString &str); + +// [2001:4860:4860::8888] -> 2001:4860:4860::8888 +inline QString UnwrapIPV6Host(QString &str) { + return str.replace("[", "").replace("]", ""); +} + +// [2001:4860:4860::8888] or 2001:4860:4860::8888 -> [2001:4860:4860::8888] +inline QString WrapIPV6Host(QString &str) { + if (!IsIpAddressV6(str)) return str; + return "[" + UnwrapIPV6Host(str) + "]"; +} + +inline QString DisplayAddress(QString serverAddress, int serverPort) { + return WrapIPV6Host(serverAddress) + ":" + Int2String(serverPort); +}; + +// Format + +inline QString DisplayTime(long long time, QLocale::FormatType formatType = QLocale::LongFormat) { + QDateTime t; + t.setSecsSinceEpoch(time); + return QLocale().toString(t, formatType); +} + +inline QString ReadableSize(const qint64 &size) { + double sizeAsDouble = size; + static QStringList measures; + if (measures.isEmpty()) + measures << "B" + << "KiB" + << "MiB" + << "GiB" + << "TiB" + << "PiB" + << "EiB" + << "ZiB" + << "YiB"; + QStringListIterator it(measures); + QString measure(it.next()); + while (sizeAsDouble >= 1024.0 && it.hasNext()) { + measure = it.next(); + sizeAsDouble /= 1024.0; + } + return QString::fromLatin1("%1 %2").arg(sizeAsDouble, 0, 'f', 2).arg(measure); +} + +// UI + +int MessageBoxWarning(const QString &title, const QString &text); + +int MessageBoxInfo(const QString &title, const QString &text); + +void runOnUiThread(const std::function &callback, QObject *parent = nullptr); + +void runOnNewThread(const std::function &callback); + +template +inline void connectOnce(EMITTER *emitter, SIGNAL signal, RECEIVER *receiver, ReceiverFunc f, + Qt::ConnectionType connectionType = Qt::AutoConnection) { + auto connection = std::make_shared(); + auto onTriggered = [connection, f](auto... arguments) { + std::invoke(f, arguments...); + QObject::disconnect(*connection); + }; + + *connection = QObject::connect(emitter, signal, receiver, onTriggered, connectionType); +} diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..deec573 --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,125 @@ +#include "ui/mainwindow.h" + +#include +#include +#include +#include +#include + +#include "3rdparty/RunGuard.hpp" +#include "main/NekoRay.hpp" + +#ifdef Q_OS_WIN +#include "sys/windows/MiniDump.h" +#endif + +int main(int argc, char *argv[]) { + // Core dump +#ifdef Q_OS_WIN + Windows_SetCrashHandler(); +#endif + + QApplication a(argc, argv); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); +#endif + QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + + // Clean + QDir::setCurrent(QApplication::applicationDirPath()); + QFile::remove("updater.old"); +#ifndef Q_OS_WIN + if (!QFile::exists("updater")) { + QFile::link("launcher", "updater"); + } +#endif + + // Flags + auto args = QApplication::arguments(); + if (args.contains("-many")) NekoRay::dataStore->flag_many = true; + if (args.contains("-appdata")) NekoRay::dataStore->flag_use_appdata = true; + + // dirs & clean + auto wd = QDir(QApplication::applicationDirPath()); + if (NekoRay::dataStore->flag_use_appdata) { + QApplication::setApplicationName("nekoray"); + wd.setPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + } + if (!wd.exists()) wd.mkdir(wd.absolutePath()); + if (!wd.exists("config")) wd.mkdir("config"); + QDir::setCurrent(wd.absoluteFilePath("config")); + QDir("temp").removeRecursively(); + + // RunGuard + RunGuard guard("nekoray" + wd.absolutePath()); + if (!NekoRay::dataStore->flag_many) { + if (!guard.tryToRun()) { + QMessageBox::warning(nullptr, "NekoRay", QObject::tr("Another program is running.")); + return 0; + } + } + + // icons + QIcon::setFallbackSearchPaths(QStringList{ + ":/nekoray", + ":/icon", + }); + + // icon for no theme + if (QIcon::themeName().isEmpty()) { + QIcon::setThemeName("breeze"); + } + + // Dir + QDir dir; + bool dir_success = true; + if (!dir.exists("profiles")) { + dir_success = dir_success && dir.mkdir("profiles"); + } + if (!dir.exists("groups")) { + dir_success = dir_success && dir.mkdir("groups"); + } + if (!dir.exists("routes")) { + dir_success = dir_success && dir.mkdir("routes"); + } + if (!dir_success) { + QMessageBox::warning(nullptr, "Error", "No permission to write " + dir.absolutePath()); + return 1; + } + + // Load dataStore + auto isLoaded = NekoRay::dataStore->Load(); + if (!isLoaded) { + NekoRay::dataStore->Save(); + } + + // load routing + NekoRay::dataStore->routing->fn = "routes/" + NekoRay::dataStore->active_routing; + isLoaded = NekoRay::dataStore->routing->Load(); + if (!isLoaded) { + NekoRay::dataStore->routing->Save(); + } + + // Translate + QString locale; + switch (NekoRay::dataStore->language) { + case 1: // English + break; + case 2: + locale = "zh_CN"; + break; + default: + locale = QLocale().name(); + } + QTranslator trans; + if (trans.load(":/translations/" + locale + ".qm")) { + QCoreApplication::installTranslator(&trans); + } + QTranslator trans_qt; + if (trans_qt.load(QApplication::applicationDirPath() + "/qtbase_" + locale + ".qm")) { + QCoreApplication::installTranslator(&trans_qt); + } + + MainWindow w; + return QApplication::exec(); +} diff --git a/matsuri_commit.txt b/matsuri_commit.txt new file mode 100644 index 0000000..5fa36e5 --- /dev/null +++ b/matsuri_commit.txt @@ -0,0 +1 @@ +3dd90ce8b7dcb7001f3de42f6bcc8edf0093353a diff --git a/nekoray_version.txt b/nekoray_version.txt new file mode 100644 index 0000000..37dccef --- /dev/null +++ b/nekoray_version.txt @@ -0,0 +1 @@ +1.0-2022-08-04 diff --git a/qv2ray/components/proxy/QvProxyConfigurator.cpp b/qv2ray/components/proxy/QvProxyConfigurator.cpp new file mode 100644 index 0000000..b41aa55 --- /dev/null +++ b/qv2ray/components/proxy/QvProxyConfigurator.cpp @@ -0,0 +1,484 @@ +#ifndef __MINGW32__ + +#include "QvProxyConfigurator.hpp" + +#ifdef Q_OS_WIN +// +#include +// +#include +#include +#include +#include +#endif + +#include +#include + +#include "qv2ray/wrapper.hpp" + +#define QV_MODULE_NAME "SystemProxy" + +#define QSTRN(num) QString::number(num) + +namespace Qv2ray::components::proxy { + + using ProcessArgument = QPair; +#ifdef Q_OS_MACOS + QStringList macOSgetNetworkServices() + { + QProcess p; + p.setProgram("/usr/sbin/networksetup"); + p.setArguments(QStringList{ "-listallnetworkservices" }); + p.start(); + p.waitForStarted(); + p.waitForFinished(); + LOG(p.errorString()); + auto str = p.readAllStandardOutput(); + auto lines = SplitLines(str); + QStringList result; + + // Start from 1 since first line is unneeded. + for (auto i = 1; i < lines.count(); i++) + { + // * means disabled. + if (!lines[i].contains("*")) + { + result << lines[i]; + } + } + + LOG("Found " + QSTRN(result.size()) + " network services: " + result.join(";")); + return result; + } +#endif +#ifdef Q_OS_WIN +#define NO_CONST(expr) const_cast(expr) + // static auto DEFAULT_CONNECTION_NAME = + // NO_CONST(L"DefaultConnectionSettings"); + /// + /// INTERNAL FUNCTION + bool __QueryProxyOptions() + { + INTERNET_PER_CONN_OPTION_LIST List; + INTERNET_PER_CONN_OPTION Option[5]; + // + unsigned long nSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); + Option[0].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL; + Option[1].dwOption = INTERNET_PER_CONN_AUTODISCOVERY_FLAGS; + Option[2].dwOption = INTERNET_PER_CONN_FLAGS; + Option[3].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; + Option[4].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + // + List.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); + List.pszConnection = nullptr; // NO_CONST(DEFAULT_CONNECTION_NAME); + List.dwOptionCount = 5; + List.dwOptionError = 0; + List.pOptions = Option; + + if (!InternetQueryOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &List, &nSize)) + { + LOG("InternetQueryOption failed, GLE=" + QSTRN(GetLastError())); + } + + LOG("System default proxy info:"); + + if (Option[0].Value.pszValue != nullptr) + { + LOG(QString::fromWCharArray(Option[0].Value.pszValue)); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_AUTO_PROXY_URL) == PROXY_TYPE_AUTO_PROXY_URL) + { + LOG("PROXY_TYPE_AUTO_PROXY_URL"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_AUTO_DETECT) == PROXY_TYPE_AUTO_DETECT) + { + LOG("PROXY_TYPE_AUTO_DETECT"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_DIRECT) == PROXY_TYPE_DIRECT) + { + LOG("PROXY_TYPE_DIRECT"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_PROXY) == PROXY_TYPE_PROXY) + { + LOG("PROXY_TYPE_PROXY"); + } + + if (!InternetQueryOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &List, &nSize)) + { + LOG("InternetQueryOption failed,GLE=" + QSTRN(GetLastError())); + } + + if (Option[4].Value.pszValue != nullptr) + { + LOG(QString::fromStdWString(Option[4].Value.pszValue)); + } + + INTERNET_VERSION_INFO Version; + nSize = sizeof(INTERNET_VERSION_INFO); + InternetQueryOption(nullptr, INTERNET_OPTION_VERSION, &Version, &nSize); + + if (Option[0].Value.pszValue != nullptr) + { + GlobalFree(Option[0].Value.pszValue); + } + + if (Option[3].Value.pszValue != nullptr) + { + GlobalFree(Option[3].Value.pszValue); + } + + if (Option[4].Value.pszValue != nullptr) + { + GlobalFree(Option[4].Value.pszValue); + } + + return false; + } + bool __SetProxyOptions(LPWSTR proxy_full_addr, bool isPAC) + { + INTERNET_PER_CONN_OPTION_LIST list; + DWORD dwBufSize = sizeof(list); + // Fill the list structure. + list.dwSize = sizeof(list); + // NULL == LAN, otherwise connectoid name. + list.pszConnection = nullptr; + + if (nullptr == proxy_full_addr) + { + LOG("Clearing system proxy"); + // + list.dwOptionCount = 1; + list.pOptions = new INTERNET_PER_CONN_OPTION[1]; + + // Ensure that the memory was allocated. + if (nullptr == list.pOptions) + { + // Return if the memory wasn't allocated. + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT; + } + else if (isPAC) + { + LOG("Setting system proxy for PAC"); + // + list.dwOptionCount = 2; + list.pOptions = new INTERNET_PER_CONN_OPTION[2]; + + if (nullptr == list.pOptions) + { + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_AUTO_PROXY_URL; + // Set proxy name. + list.pOptions[1].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL; + list.pOptions[1].Value.pszValue = proxy_full_addr; + } + else + { + LOG("Setting system proxy for Global Proxy"); + // + list.dwOptionCount = 2; + list.pOptions = new INTERNET_PER_CONN_OPTION[2]; + + if (nullptr == list.pOptions) + { + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_PROXY; + // Set proxy name. + list.pOptions[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + list.pOptions[1].Value.pszValue = proxy_full_addr; + // Set proxy override. + // list.pOptions[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; + // auto localhost = L"localhost"; + // list.pOptions[2].Value.pszValue = NO_CONST(localhost); + } + + // Set proxy for LAN. + if (!InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize)) + { + LOG("InternetSetOption failed for LAN, GLE=" + QSTRN(GetLastError())); + } + + RASENTRYNAME entry; + entry.dwSize = sizeof(entry); + std::vector entries; + DWORD size = sizeof(entry), count; + LPRASENTRYNAME entryAddr = &entry; + auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + if (ERROR_BUFFER_TOO_SMALL == ret) + { + entries.resize(count); + entries[0].dwSize = sizeof(RASENTRYNAME); + entryAddr = entries.data(); + ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + } + if (ERROR_SUCCESS != ret) + { + LOG("Failed to list entry names"); + return false; + } + + // Set proxy for each connectoid. + for (DWORD i = 0; i < count; ++i) + { + list.pszConnection = entryAddr[i].szEntryName; + if (!InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize)) + { + LOG("InternetSetOption failed for connectoid " + QString::fromWCharArray(list.pszConnection) + ", GLE=" + QSTRN(GetLastError())); + } + } + + delete[] list.pOptions; + InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); + InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); + return true; + } +#endif + + void SetSystemProxy(const QString &address, int httpPort, int socksPort) { + LOG("Setting up System Proxy"); + bool hasHTTP = (httpPort > 0 && httpPort < 65536); + bool hasSOCKS = (socksPort > 0 && socksPort < 65536); + +#ifdef Q_OS_WIN + if (!hasHTTP) + { + LOG("Nothing?"); + return; + } + else + { + LOG("Qv2ray will set system proxy to use HTTP"); + } +#else + if (!hasHTTP && !hasSOCKS) { + LOG("Nothing?"); + return; + } + + if (hasHTTP) { + LOG("Qv2ray will set system proxy to use HTTP"); + } + + if (hasSOCKS) { + LOG("Qv2ray will set system proxy to use SOCKS"); + } +#endif + +#ifdef Q_OS_WIN + QString __a; + const QHostAddress ha(address); + const auto type = ha.protocol(); + if (type == QAbstractSocket::IPv6Protocol) + { + // many software do not recognize IPv6 proxy server string though + const auto str = ha.toString(); // RFC5952 + __a = "[" + str + "]:" + QSTRN(httpPort); + } + else + { + __a = address + ":" + QSTRN(httpPort); + } + + LOG("Windows proxy string: " + __a); + auto proxyStrW = new WCHAR[__a.length() + 1]; + wcscpy(proxyStrW, __a.toStdWString().c_str()); + // + __QueryProxyOptions(); + + if (!__SetProxyOptions(proxyStrW, false)) + { + LOG("Failed to set proxy."); + } + + __QueryProxyOptions(); +#elif defined(Q_OS_LINUX) + QList actions; + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "manual"}}; + // + bool isKDE = qEnvironmentVariable("XDG_SESSION_DESKTOP") == "KDE" || + qEnvironmentVariable("XDG_SESSION_DESKTOP") == "plasma"; + const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + + // + // Configure HTTP Proxies for HTTP, FTP and HTTPS + if (hasHTTP) { + // iterate over protocols... + for (const auto &protocol: QStringList{"http", "ftp", "https"}) { + // for GNOME: + { + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy." + protocol, "host", address}}; + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy." + protocol, "port", QSTRN(httpPort)}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", protocol + "Proxy", // + "http://" + address + " " + QSTRN(httpPort)}}; + } + } + } + + // Configure SOCKS5 Proxies + if (hasSOCKS) { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy.socks", "host", address}}; + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy.socks", "port", QSTRN(socksPort)}}; + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "socksProxy", // + "socks://" + address + " " + QSTRN(socksPort)}}; + } + } + } + // Setting Proxy Mode to Manual + { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "manual"}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "ProxyType", "1"}}; + } + } + + // Notify kioslaves to reload system proxy configuration. + if (isKDE) { + actions << ProcessArgument{"dbus-send", + {"--type=signal", "/KIO/Scheduler", // + "org.kde.KIO.Scheduler.reparseSlaveConfiguration", // + "string:''"}}; + } + // Execute them all! + // + // note: do not use std::all_of / any_of / none_of, + // because those are short-circuit and cannot guarantee atomicity. + QList results; + for (const auto &action: actions) { + // execute and get the code + const auto returnCode = QProcess::execute(action.first, action.second); + // print out the commands and result codes + DEBUG(QString("[%1] Program: %2, Args: %3").arg(returnCode).arg(action.first).arg(action.second.join(";"))); + // give the code back + results << (returnCode == QProcess::NormalExit); + } + + if (results.count(true) != actions.size()) { + LOG("Something wrong when setting proxies."); + } +#else + + for (const auto &service : macOSgetNetworkServices()) + { + LOG("Setting proxy for interface: " + service); + if (hasHTTP) + { + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxy", service, address, QSTRN(httpPort) }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxy", service, address, QSTRN(httpPort) }); + } + + if (hasSOCKS) + { + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxy", service, address, QSTRN(socksPort) }); + } + } + +#endif + } + + void ClearSystemProxy() { + LOG("Clearing System Proxy"); + +#ifdef Q_OS_WIN + if (!__SetProxyOptions(nullptr, false)) + { + LOG("Failed to clear proxy."); + } +#elif defined(Q_OS_LINUX) + QList actions; + const bool isKDE = qEnvironmentVariable("XDG_SESSION_DESKTOP") == "KDE" || + qEnvironmentVariable("XDG_SESSION_DESKTOP") == "plasma"; + const auto configRoot = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + + // Setting System Proxy Mode to: None + { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "none"}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configRoot + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "ProxyType", "0"}}; + } + } + + // Notify kioslaves to reload system proxy configuration. + if (isKDE) { + actions << ProcessArgument{"dbus-send", + {"--type=signal", "/KIO/Scheduler", // + "org.kde.KIO.Scheduler.reparseSlaveConfiguration", // + "string:''"}}; + } + + // Execute the Actions + for (const auto &action: actions) { + // execute and get the code + const auto returnCode = QProcess::execute(action.first, action.second); + // print out the commands and result codes + DEBUG(QString("[%1] Program: %2, Args: %3").arg(returnCode).arg(action.first).arg(action.second.join(";"))); + } + +#else + for (const auto &service : macOSgetNetworkServices()) + { + LOG("Clearing proxy for interface: " + service); + QProcess::execute("/usr/sbin/networksetup", { "-setautoproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxystate", service, "off" }); + } + +#endif + } +} // namespace Qv2ray::components::proxy + +#endif diff --git a/qv2ray/components/proxy/QvProxyConfigurator.hpp b/qv2ray/components/proxy/QvProxyConfigurator.hpp new file mode 100644 index 0000000..a6b0300 --- /dev/null +++ b/qv2ray/components/proxy/QvProxyConfigurator.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +// +namespace Qv2ray::components::proxy +{ + void ClearSystemProxy(); + void SetSystemProxy(const QString &address, int http_port, int socks_port); +} // namespace Qv2ray::components::proxy + +using namespace Qv2ray::components; +using namespace Qv2ray::components::proxy; diff --git a/qv2ray/ui/LogHighlighter.cpp b/qv2ray/ui/LogHighlighter.cpp new file mode 100644 index 0000000..4644878 --- /dev/null +++ b/qv2ray/ui/LogHighlighter.cpp @@ -0,0 +1,148 @@ +#include "LogHighlighter.hpp" + +#define TO_EOL "(([\\s\\S]*)|([\\d\\D]*)|([\\w\\W]*))$" +#define REGEX_IPV6_ADDR \ + R"(\[\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*\])" +#define REGEX_IPV4_ADDR \ + R"((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]))" +#define REGEX_PORT_NUMBER R"(([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])*)" + +namespace Qv2ray::ui { + SyntaxHighlighter::SyntaxHighlighter(bool darkMode, QTextDocument *parent) : QSyntaxHighlighter(parent) { + HighlightingRule rule; + + if (darkMode) { + tcpudpFormat.setForeground(QColor(0, 200, 230)); + ipHostFormat.setForeground(Qt::yellow); + warningFormat.setForeground(QColor(255, 160, 15)); + warningFormat2.setForeground(Qt::cyan); + } else { + ipHostFormat.setForeground(Qt::black); + ipHostFormat.setFontWeight(QFont::Bold); + tcpudpFormat.setForeground(QColor(0, 52, 130)); + warningFormat.setBackground(QColor(255, 160, 15)); + warningFormat.setForeground(Qt::white); + warningFormat2.setForeground(Qt::darkCyan); + } + + dateFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\d\\d\\d\\d/\\d\\d/\\d\\d"); + rule.format = dateFormat; + highlightingRules.append(rule); + // + timeFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\d\\d:\\d\\d:\\d\\d"); + rule.format = timeFormat; + highlightingRules.append(rule); + // + debugFormat.setForeground(Qt::darkGray); + rule.pattern = QRegularExpression("\\[[Dd]ebug\\]" TO_EOL); + rule.format = debugFormat; + highlightingRules.append(rule); + // + infoFormat.setForeground(darkMode ? Qt::lightGray : Qt::darkCyan); + rule.pattern = QRegularExpression("\\[[Ii]nfo\\]" TO_EOL); + rule.format = infoFormat; + highlightingRules.append(rule); + // + + const static QColor darkGreenColor(10, 180, 0); + // + // + acceptedFormat.setForeground(darkGreenColor); + acceptedFormat.setFontItalic(true); + acceptedFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\saccepted\\s"); + rule.format = acceptedFormat; + highlightingRules.append(rule); + // + rejectedFormat.setFontWeight(QFont::Bold); + rejectedFormat.setBackground(Qt::red); + rejectedFormat.setForeground(Qt::white); + rejectedFormat.setFontItalic(true); + rejectedFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\srejected\\s" TO_EOL); + rule.format = rejectedFormat; + highlightingRules.append(rule); + // + v2rayComponentFormat.setForeground(darkMode ? darkGreenColor : Qt::darkYellow); + rule.pattern = QRegularExpression(R"( (\w+\/)+\w+: )"); + rule.format = v2rayComponentFormat; + highlightingRules.append(rule); + // + warningFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\[[Ww]arning\\]" TO_EOL); + rule.format = warningFormat; + highlightingRules.append(rule); + // + warningFormat2.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\[[Ww]arning\\]" TO_EOL); + rule.format = warningFormat2; + highlightingRules.append(rule); + // + failedFormat.setFontWeight(QFont::Bold); + failedFormat.setBackground(Qt::red); + failedFormat.setForeground(Qt::white); + rule.pattern = QRegularExpression("failed"); + rule.format = failedFormat; + highlightingRules.append(rule); + // + qvAppLogFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\[[A-Z]*\\]:"); + rule.format = qvAppLogFormat; + highlightingRules.append(rule); + // + qvAppDebugLogFormat.setForeground(darkMode ? Qt::yellow : Qt::darkYellow); + rule.pattern = QRegularExpression(R"( \[\w+\] )"); + rule.format = qvAppDebugLogFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression("default route"); + rule.format = qvAppDebugLogFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression(">>>>+"); + rule.format = warningFormat; + highlightingRules.append(rule); + + { + // IP IPv6 Host; + rule.pattern = QRegularExpression(REGEX_IPV4_ADDR ":" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression(REGEX_IPV6_ADDR ":" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression( + "([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(/|):" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::PatternOption::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + } + + for (const auto &pattern: {"tcp", "udp"}) { + tcpudpFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression(pattern); + rule.format = tcpudpFormat; + highlightingRules.append(rule); + } + + } + + void SyntaxHighlighter::highlightBlock(const QString &text) { + for (const HighlightingRule &rule: qAsConst(highlightingRules)) { + QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); + + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } + + setCurrentBlockState(0); + } +} // namespace Qv2ray::ui diff --git a/qv2ray/ui/LogHighlighter.hpp b/qv2ray/ui/LogHighlighter.hpp new file mode 100644 index 0000000..a5bbc5d --- /dev/null +++ b/qv2ray/ui/LogHighlighter.hpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +#include +#include + +namespace Qv2ray::ui +{ + class SyntaxHighlighter : public QSyntaxHighlighter + { + Q_OBJECT + + public: + explicit SyntaxHighlighter(bool darkMode, QTextDocument *parent = nullptr); + + protected: + void highlightBlock(const QString &text) override; + + private: + struct HighlightingRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + QVector highlightingRules; + + QTextCharFormat tcpudpFormat; + QTextCharFormat dateFormat; + QTextCharFormat acceptedFormat; + QTextCharFormat rejectedFormat; + QTextCharFormat failedFormat; + QTextCharFormat warningFormat; + QTextCharFormat warningFormat2; + QTextCharFormat infoFormat; + QTextCharFormat debugFormat; + QTextCharFormat timeFormat; + QTextCharFormat ipHostFormat; + QTextCharFormat v2rayComponentFormat; + // + QTextCharFormat qvAppLogFormat; + QTextCharFormat qvAppDebugLogFormat; + }; +} // namespace Qv2ray::ui + +using namespace Qv2ray::ui; diff --git a/qv2ray/ui/QvAutoCompleteTextEdit.cpp b/qv2ray/ui/QvAutoCompleteTextEdit.cpp new file mode 100644 index 0000000..910d5d5 --- /dev/null +++ b/qv2ray/ui/QvAutoCompleteTextEdit.cpp @@ -0,0 +1,167 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "QvAutoCompleteTextEdit.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Qv2ray::ui::widgets +{ + AutoCompleteTextEdit::AutoCompleteTextEdit(const QString &prefix, const QStringList &sourceStrings, QWidget *parent) : QPlainTextEdit(parent) + { + this->prefix = prefix; + this->setLineWrapMode(QPlainTextEdit::NoWrap); + c = new QCompleter(this); + c->setModel(new QStringListModel(sourceStrings, c)); + c->setWidget(this); + c->setCompletionMode(QCompleter::PopupCompletion); + c->setCaseSensitivity(Qt::CaseInsensitive); + QObject::connect(c, QOverload::of(&QCompleter::activated), this, &AutoCompleteTextEdit::insertCompletion); + } + + AutoCompleteTextEdit::~AutoCompleteTextEdit() + { + } + + void AutoCompleteTextEdit::insertCompletion(const QString &completion) + { + QTextCursor tc = textCursor(); + int extra = completion.length() - c->completionPrefix().length(); + tc.movePosition(QTextCursor::Left); + tc.movePosition(QTextCursor::EndOfWord); + tc.insertText(completion.right(extra)); + setTextCursor(tc); + } + + QString AutoCompleteTextEdit::lineUnderCursor() const + { + QTextCursor tc = textCursor(); + tc.select(QTextCursor::LineUnderCursor); + return tc.selectedText(); + } + + QString AutoCompleteTextEdit::wordUnderCursor() const + { + QTextCursor tc = textCursor(); + tc.select(QTextCursor::WordUnderCursor); + return tc.selectedText(); + } + + void AutoCompleteTextEdit::focusInEvent(QFocusEvent *e) + { + if (c) + c->setWidget(this); + + QPlainTextEdit::focusInEvent(e); + } + + void AutoCompleteTextEdit::keyPressEvent(QKeyEvent *e) + { + const bool hasCtrlOrShiftModifier = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::ShiftModifier); + const bool hasOtherModifiers = (e->modifiers() != Qt::NoModifier) && !hasCtrlOrShiftModifier; // has other modifiers + // + const bool isSpace = (e->modifiers().testFlag(Qt::ShiftModifier) || e->modifiers().testFlag(Qt::NoModifier)) // + && e->key() == Qt::Key_Space; + const bool isTab = (e->modifiers().testFlag(Qt::NoModifier) && e->key() == Qt::Key_Tab); + const bool isOtherSpace = e->text() == " "; + // + if (isSpace || isTab || isOtherSpace) + { + QToolTip::showText(this->mapToGlobal(QPoint(0, 0)), tr("You can not input space characters here."), this, QRect{}, 2000); + return; + } + // + if (c && c->popup()->isVisible()) + { + // The following keys are forwarded by the completer to the widget + switch (e->key()) + { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Escape: + case Qt::Key_Tab: + case Qt::Key_Backtab: e->ignore(); return; // let the completer do default behavior + + default: break; + } + } + + QPlainTextEdit::keyPressEvent(e); + + if (!c || (hasCtrlOrShiftModifier && e->text().isEmpty())) + return; + + // if we have other modifiers, or the text is empty, or the line does not start with our prefix. + if (hasOtherModifiers || e->text().isEmpty() || !lineUnderCursor().startsWith(prefix)) + { + c->popup()->hide(); + return; + } + + if (auto word = wordUnderCursor(); word != c->completionPrefix()) + { + c->setCompletionPrefix(word); + c->popup()->setCurrentIndex(c->completionModel()->index(0, 0)); + } + + QRect cr = cursorRect(); + cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); + c->complete(cr); // popup it up! + } +} // namespace Qv2ray::ui::widgets diff --git a/qv2ray/ui/QvAutoCompleteTextEdit.hpp b/qv2ray/ui/QvAutoCompleteTextEdit.hpp new file mode 100644 index 0000000..e2e930c --- /dev/null +++ b/qv2ray/ui/QvAutoCompleteTextEdit.hpp @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +QT_BEGIN_NAMESPACE +class QCompleter; +QT_END_NAMESPACE + +namespace Qv2ray::ui::widgets +{ + class AutoCompleteTextEdit : public QPlainTextEdit + { + Q_OBJECT + + public: + AutoCompleteTextEdit(const QString &prefix, const QStringList &sourceStrings, QWidget *parent = nullptr); + ~AutoCompleteTextEdit(); + + protected: + void keyPressEvent(QKeyEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + + private slots: + void insertCompletion(const QString &completion); + + private: + QString lineUnderCursor() const; + QString wordUnderCursor() const; + + QString prefix; + QCompleter *c = nullptr; + }; +} // namespace Qv2ray::ui::widgets +using namespace Qv2ray::ui::widgets; diff --git a/qv2ray/ui/widgets/common/QJsonModel.cpp b/qv2ray/ui/widgets/common/QJsonModel.cpp new file mode 100644 index 0000000..c633b8c --- /dev/null +++ b/qv2ray/ui/widgets/common/QJsonModel.cpp @@ -0,0 +1,402 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2011 SCHUTZ Sacha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "QJsonModel.hpp" + +#include +#include + +QJsonTreeItem::QJsonTreeItem(QJsonTreeItem *parent) +{ + mParent = parent; +} + +QJsonTreeItem::~QJsonTreeItem() +{ + qDeleteAll(mChilds); +} + +void QJsonTreeItem::appendChild(QJsonTreeItem *item) +{ + mChilds.append(item); +} + +QJsonTreeItem *QJsonTreeItem::child(int row) +{ + return mChilds.value(row); +} + +QJsonTreeItem *QJsonTreeItem::parent() +{ + return mParent; +} + +int QJsonTreeItem::childCount() const +{ + return mChilds.count(); +} + +int QJsonTreeItem::row() const +{ + if (mParent) + return mParent->mChilds.indexOf(const_cast(this)); + + return 0; +} + +void QJsonTreeItem::setKey(const QString &key) +{ + mKey = key; +} + +void QJsonTreeItem::setValue(const QString &value) +{ + mValue = value; +} + +void QJsonTreeItem::setType(const QJsonValue::Type &type) +{ + mType = type; +} + +QString QJsonTreeItem::key() const +{ + return mKey; +} + +QString QJsonTreeItem::value() const +{ + return mValue; +} + +QJsonValue::Type QJsonTreeItem::type() const +{ + return mType; +} + +QJsonTreeItem *QJsonTreeItem::load(const QJsonValue &value, QJsonTreeItem *parent) +{ + QJsonTreeItem *rootItem = new QJsonTreeItem(parent); + rootItem->setKey("root"); + + if (value.isObject()) + { + // Get all QJsonValue childs + for (QString key : value.toObject().keys()) + { + QJsonValue v = value.toObject().value(key); + QJsonTreeItem *child = load(v, rootItem); + child->setKey(key); + child->setType(v.type()); + rootItem->appendChild(child); + } + } + else if (value.isArray()) + { + // Get all QJsonValue childs + int index = 0; + + for (QJsonValue v : value.toArray()) + { + QJsonTreeItem *child = load(v, rootItem); + child->setKey(QString::number(index)); + child->setType(v.type()); + rootItem->appendChild(child); + ++index; + } + } + else + { + rootItem->setValue(value.toVariant().toString()); + rootItem->setType(value.type()); + } + + return rootItem; +} + +//========================================================================= + +QJsonModel::QJsonModel(QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); +} + +QJsonModel::QJsonModel(const QString &fileName, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + load(fileName); +} + +QJsonModel::QJsonModel(QIODevice *device, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + load(device); +} + +QJsonModel::QJsonModel(const QByteArray &json, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + loadJson(json); +} + +QJsonModel::~QJsonModel() +{ + delete mRootItem; +} + +bool QJsonModel::load(const QString &fileName) +{ + QFile file(fileName); + bool success = false; + + if (file.open(QIODevice::ReadOnly)) + { + success = load(&file); + file.close(); + } + else + success = false; + + return success; +} + +bool QJsonModel::load(QIODevice *device) +{ + return loadJson(device->readAll()); +} + +bool QJsonModel::loadJson(const QByteArray &json) +{ + auto const &jdoc = QJsonDocument::fromJson(json); + + if (!jdoc.isNull()) + { + beginResetModel(); + delete mRootItem; + + if (jdoc.isArray()) + { + mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.array())); + mRootItem->setType(QJsonValue::Array); + } + else + { + mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.object())); + mRootItem->setType(QJsonValue::Object); + } + + endResetModel(); + return true; + } + + qDebug() << Q_FUNC_INFO << "cannot load json"; + return false; +} + +QVariant QJsonModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + QJsonTreeItem *item = static_cast(index.internalPointer()); + + if (role == Qt::DisplayRole) + { + if (index.column() == 0) + return QString("%1").arg(item->key()); + + if (index.column() == 1) + return QString("%1").arg(item->value()); + } + else if (Qt::EditRole == role) + { + if (index.column() == 1) + { + return QString("%1").arg(item->value()); + } + } + + return QVariant(); +} + +bool QJsonModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int col = index.column(); + + if (Qt::EditRole == role) + { + if (col == 1) + { + QJsonTreeItem *item = static_cast(index.internalPointer()); + item->setValue(value.toString()); + emit dataChanged(index, index, { Qt::EditRole }); + return true; + } + } + + return false; +} + +QVariant QJsonModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) + { + return mHeaders.value(section); + } + else + return QVariant(); +} + +QModelIndex QJsonModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + QJsonTreeItem *parentItem; + + if (!parent.isValid()) + parentItem = mRootItem; + else + parentItem = static_cast(parent.internalPointer()); + + QJsonTreeItem *childItem = parentItem->child(row); + + if (childItem) + return createIndex(row, column, childItem); + else + return QModelIndex(); +} + +QModelIndex QJsonModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + QJsonTreeItem *childItem = static_cast(index.internalPointer()); + QJsonTreeItem *parentItem = childItem->parent(); + + if (parentItem == mRootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int QJsonModel::rowCount(const QModelIndex &parent) const +{ + QJsonTreeItem *parentItem; + + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = mRootItem; + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->childCount(); +} + +int QJsonModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 2; +} + +Qt::ItemFlags QJsonModel::flags(const QModelIndex &index) const +{ + int col = index.column(); + auto item = static_cast(index.internalPointer()); + auto isArray = QJsonValue::Array == item->type(); + auto isObject = QJsonValue::Object == item->type(); + + if ((col == 1) && !(isArray || isObject)) + { + return Qt::ItemIsEditable | QAbstractItemModel::flags(index); + } + else + { + return QAbstractItemModel::flags(index); + } +} + +QJsonDocument QJsonModel::json() const +{ + auto v = genJson(mRootItem); + QJsonDocument doc; + + if (v.isObject()) + { + doc = QJsonDocument(v.toObject()); + } + else + { + doc = QJsonDocument(v.toArray()); + } + + return doc; +} + +QJsonValue QJsonModel::genJson(QJsonTreeItem *item) const +{ + auto type = item->type(); + int nchild = item->childCount(); + + if (QJsonValue::Object == type) + { + QJsonObject jo; + + for (int i = 0; i < nchild; ++i) + { + auto ch = item->child(i); + auto key = ch->key(); + jo.insert(key, genJson(ch)); + } + + return jo; + } + else if (QJsonValue::Array == type) + { + QJsonArray arr; + + for (int i = 0; i < nchild; ++i) + { + auto ch = item->child(i); + arr.append(genJson(ch)); + } + + return arr; + } + else + { + QJsonValue va(item->value()); + return va; + } +} diff --git a/qv2ray/ui/widgets/common/QJsonModel.hpp b/qv2ray/ui/widgets/common/QJsonModel.hpp new file mode 100644 index 0000000..3b07e7c --- /dev/null +++ b/qv2ray/ui/widgets/common/QJsonModel.hpp @@ -0,0 +1,94 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2011 SCHUTZ Sacha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +class QJsonModel; +class QJsonItem; + +class QJsonTreeItem +{ + public: + QJsonTreeItem(QJsonTreeItem *parent = nullptr); + ~QJsonTreeItem(); + void appendChild(QJsonTreeItem *item); + QJsonTreeItem *child(int row); + QJsonTreeItem *parent(); + int childCount() const; + int row() const; + void setKey(const QString &key); + void setValue(const QString &value); + void setType(const QJsonValue::Type &type); + QString key() const; + QString value() const; + QJsonValue::Type type() const; + + static QJsonTreeItem *load(const QJsonValue &value, QJsonTreeItem *parent = 0); + + protected: + private: + QString mKey; + QString mValue; + QJsonValue::Type mType; + QList mChilds; + QJsonTreeItem *mParent; +}; + +//--------------------------------------------------- + +class QJsonModel : public QAbstractItemModel +{ + Q_OBJECT + public: + explicit QJsonModel(QObject *parent = nullptr); + QJsonModel(const QString &fileName, QObject *parent = nullptr); + QJsonModel(QIODevice *device, QObject *parent = nullptr); + QJsonModel(const QByteArray &json, QObject *parent = nullptr); + ~QJsonModel(); + bool load(const QString &fileName); + bool load(QIODevice *device); + bool loadJson(const QByteArray &json); + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + QJsonDocument json() const; + + private: + QJsonValue genJson(QJsonTreeItem *) const; + + QJsonTreeItem *mRootItem; + QStringList mHeaders; +}; diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.cpp b/qv2ray/ui/widgets/editors/w_JsonEditor.cpp new file mode 100644 index 0000000..06e7edd --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.cpp @@ -0,0 +1,92 @@ +#include "w_JsonEditor.hpp" + +#include "main/NekoRay.hpp" + +JsonEditor::JsonEditor(const QJsonObject& rootObject, QWidget *parent) : QDialog(parent) { + setupUi(this); +// QvMessageBusConnect(JsonEditor); + // + original = rootObject; + final = rootObject; + QString jsonString = JsonToString(rootObject); + + if (VerifyJsonString(jsonString).isEmpty()) { + jsonTree->setModel(&model); + model.loadJson(QJsonDocument(rootObject).toJson()); + } else { + QvMessageBoxWarn(this, tr("Json Contains Syntax Errors"), + tr("Original Json may contain syntax errors. Json tree is disabled.")); + } + + jsonEditor->setText(JsonToString(rootObject)); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); +} + +//QvMessageBusSlotImpl(JsonEditor) +// { +// switch (msg) +// { +// MBShowDefaultImpl; +// MBHideDefaultImpl; +// MBRetranslateDefaultImpl; +// case UPDATE_COLORSCHEME: +// break; +// } +// } + +QJsonObject JsonEditor::OpenEditor() { + int resultCode = this->exec(); + auto string = jsonEditor->toPlainText(); + + while (resultCode == QDialog::Accepted && !VerifyJsonString(string).isEmpty()) { + QvMessageBoxWarn(this, tr("Json Contains Syntax Errors"), + tr("You must correct these errors before continuing.")); + resultCode = this->exec(); + string = jsonEditor->toPlainText(); + } + + return resultCode == QDialog::Accepted ? final : original; +} + +JsonEditor::~JsonEditor() { +} + +void JsonEditor::on_jsonEditor_textChanged() { + auto string = jsonEditor->toPlainText(); + auto VerifyResult = VerifyJsonString(string); + jsonValidateStatus->setText(VerifyResult); + + if (VerifyResult.isEmpty()) { + BLACK(jsonEditor); + final = JsonFromString(string); + model.loadJson(QJsonDocument(final).toJson()); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); + } else { + RED(jsonEditor); + } +} + +void JsonEditor::on_formatJsonBtn_clicked() { + auto string = jsonEditor->toPlainText(); + auto VerifyResult = VerifyJsonString(string); + jsonValidateStatus->setText(VerifyResult); + + if (VerifyResult.isEmpty()) { + BLACK(jsonEditor); + jsonEditor->setPlainText(JsonToString(JsonFromString(string))); + model.loadJson(QJsonDocument(JsonFromString(string)).toJson()); + jsonTree->setModel(&model); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); + } else { + RED(jsonEditor); + QvMessageBoxWarn(this, tr("Syntax Errors"), + tr("Please fix the JSON errors or remove the comments before continue")); + } +} + +void JsonEditor::on_removeCommentsBtn_clicked() { + jsonEditor->setPlainText(JsonToString(JsonFromString(jsonEditor->toPlainText()))); +} diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.hpp b/qv2ray/ui/widgets/editors/w_JsonEditor.hpp new file mode 100644 index 0000000..2f216dd --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "qv2ray/wrapper.hpp" +#include "qv2ray/ui/widgets/common/QJsonModel.hpp" +#include "ui_w_JsonEditor.h" + + +#include + +class JsonEditor + : public QDialog + , private Ui::JsonEditor +{ + Q_OBJECT + + public: + explicit JsonEditor(const QJsonObject& rootObject, QWidget *parent = nullptr); + ~JsonEditor(); + QJsonObject OpenEditor(); + + private slots: + void on_jsonEditor_textChanged(); + + void on_formatJsonBtn_clicked(); + + void on_removeCommentsBtn_clicked(); + + private: + QJsonModel model; + QJsonObject original; + QJsonObject final; +}; diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.ui b/qv2ray/ui/widgets/editors/w_JsonEditor.ui new file mode 100644 index 0000000..51dcd76 --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.ui @@ -0,0 +1,172 @@ + + + JsonEditor + + + Qt::ApplicationModal + + + + 0 + 0 + 889 + 572 + + + + JSON Editor + + + true + + + + + + Qt::Horizontal + + + false + + + + + + + + Monospace + + + + QTextEdit::NoWrap + + + false + + + + + + + Format JSON + + + + + + + Remove All Comments + + + + + + + Json Editor + + + + + + + + + + + Structure Preview + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + 15 + + + true + + + true + + + true + + + 132 + + + 152 + + + + + + + + + + + OK + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + jsonEditor + formatJsonBtn + removeCommentsBtn + jsonTree + + + + + buttonBox + accepted() + JsonEditor + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + JsonEditor + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/qv2ray/utils/HTTPRequestHelper.cpp b/qv2ray/utils/HTTPRequestHelper.cpp new file mode 100644 index 0000000..ce3bd3d --- /dev/null +++ b/qv2ray/utils/HTTPRequestHelper.cpp @@ -0,0 +1,100 @@ +#include "HTTPRequestHelper.hpp" + +#include +#include +#include +#include + +#include "main/NekoRay.hpp" + +#define QV_MODULE_NAME "NetworkCore" + +#include "qv2ray/wrapper.hpp" + +namespace Qv2ray::common::network { + void NetworkRequestHelper::setHeader(QNetworkRequest &request, const QByteArray &key, const QByteArray &value) { + DEBUG("Adding HTTP request header: " + key + ":" + value); + request.setRawHeader(key, value); + } + + void + NetworkRequestHelper::setAccessManagerAttributes(QNetworkRequest &request, QNetworkAccessManager &accessManager) { + + // Use proxy + if (NekoRay::dataStore->sub_use_proxy) { + QNetworkProxy p{QNetworkProxy::Socks5Proxy, "127.0.0.1", + static_cast(NekoRay::dataStore->inbound_socks_port)}; + accessManager.setProxy(p); + if (NekoRay::dataStore->started_id < 0) { + showLog(QObject::tr("Request with proxy but no profile started.")); + } + } + + if (accessManager.proxy().type() == QNetworkProxy::Socks5Proxy) { + DEBUG("Adding HostNameLookupCapability to proxy."); + accessManager.proxy().setCapabilities( + accessManager.proxy().capabilities() | QNetworkProxy::HostNameLookupCapability); + } + + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + // request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true); +#else + // request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); +#endif + + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, NekoRay::dataStore->user_agent); + } + + HTTPResponse NetworkRequestHelper::HttpGet(const QUrl &url) { + QNetworkRequest request; + QNetworkAccessManager accessManager; + request.setUrl(url); + setAccessManagerAttributes(request, accessManager); + auto _reply = accessManager.get(request); + // + { + QEventLoop loop; + QObject::connect(&accessManager, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit); + loop.exec(); + } + // + return HTTPResponse{_reply->error() == QNetworkReply::NetworkError::NoError ? "" : _reply->errorString(), + _reply->readAll(), _reply->rawHeaderPairs()}; + } + + void NetworkRequestHelper::AsyncHttpGet(const QString &url, std::function funcPtr) { + QNetworkRequest request; + request.setUrl(url); + auto accessManagerPtr = new QNetworkAccessManager(); + setAccessManagerAttributes(request, *accessManagerPtr); + auto reply = accessManagerPtr->get(request); + QObject::connect(reply, &QNetworkReply::finished, [=]() { + { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + bool h2Used = reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool(); +#else + bool h2Used = reply->attribute(QNetworkRequest::HTTP2WasUsedAttribute).toBool(); +#endif + if (h2Used) + DEBUG("HTTP/2 was used."); + + if (reply->error() != QNetworkReply::NoError) + LOG("Network error: " + + QString(QMetaEnum::fromType().key(reply->error()))); + + funcPtr(reply->readAll()); + accessManagerPtr->deleteLater(); + } + }); + } + + QString NetworkRequestHelper::GetHeader(const QList> &header, const QString &name) { + for (auto p: header) { + if (QString(p.first).toLower() == name.toLower()) return p.second; + } + return ""; + } + +} // namespace Qv2ray::common::network diff --git a/qv2ray/utils/HTTPRequestHelper.hpp b/qv2ray/utils/HTTPRequestHelper.hpp new file mode 100644 index 0000000..41b9a75 --- /dev/null +++ b/qv2ray/utils/HTTPRequestHelper.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Qv2ray::common::network { + struct HTTPResponse { + QString error; + QByteArray data; + QList> header; + }; + + class NetworkRequestHelper : QObject { + Q_OBJECT + + explicit NetworkRequestHelper(QObject *parent) : QObject(parent) {}; + + ~NetworkRequestHelper() {}; + + public: + static void AsyncHttpGet(const QString &url, std::function funcPtr); + + static HTTPResponse HttpGet(const QUrl &url); + + static QString GetHeader(const QList>& header, const QString& name); + + private: + static void setAccessManagerAttributes(QNetworkRequest &request, QNetworkAccessManager &accessManager); + + static void setHeader(QNetworkRequest &request, const QByteArray &key, const QByteArray &value); + }; +} // namespace Qv2ray::common::network + +using namespace Qv2ray::common::network; diff --git a/qv2ray/wrapper.hpp b/qv2ray/wrapper.hpp new file mode 100644 index 0000000..15c12dc --- /dev/null +++ b/qv2ray/wrapper.hpp @@ -0,0 +1,38 @@ +#pragma once + +// Qv2ray wrapper + +#include + +#define LOG(...) Qv2ray::base::log_internal(__VA_ARGS__) +#define DEBUG(...) Qv2ray::base::log_internal(__VA_ARGS__) +namespace Qv2ray::base { + template + inline void log_internal(T... v) {} +} + +#define JsonToString(a) QJsonObject2QString(a,false) +#define JsonFromString(a) QString2QJsonObject(a) +#define QvMessageBoxWarn(a, b, c) MessageBoxWarning(b,c) + +inline QString VerifyJsonString(const QString &source) { + QJsonParseError error{}; + QJsonDocument doc = QJsonDocument::fromJson(source.toUtf8(), &error); + Q_UNUSED(doc) + + if (error.error == QJsonParseError::NoError) { + return ""; + } else { + //LOG("WARNING: Json parse returns: " + error.errorString()); + return error.errorString(); + } +} + +#define RED(obj) \ + { \ + auto _temp = obj->palette(); \ + _temp.setColor(QPalette::Text, Qt::red); \ + obj->setPalette(_temp); \ + } + +#define BLACK(obj) obj->setPalette(QWidget::palette()); diff --git a/res/icon/dialog-question.svg b/res/icon/dialog-question.svg new file mode 100644 index 0000000..7ff4bc4 --- /dev/null +++ b/res/icon/dialog-question.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/internet-web-browser.svg b/res/icon/internet-web-browser.svg new file mode 100644 index 0000000..138a1a3 --- /dev/null +++ b/res/icon/internet-web-browser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/icon/network-server.svg b/res/icon/network-server.svg new file mode 100644 index 0000000..0806722 --- /dev/null +++ b/res/icon/network-server.svg @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/res/icon/preferences.svg b/res/icon/preferences.svg new file mode 100644 index 0000000..875b939 --- /dev/null +++ b/res/icon/preferences.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/system-run.svg b/res/icon/system-run.svg new file mode 100644 index 0000000..45494f4 --- /dev/null +++ b/res/icon/system-run.svg @@ -0,0 +1,190 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/system-software-update.svg b/res/icon/system-software-update.svg new file mode 100644 index 0000000..e8567fb --- /dev/null +++ b/res/icon/system-software-update.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/neko.qrc b/res/neko.qrc new file mode 100644 index 0000000..aa7701b --- /dev/null +++ b/res/neko.qrc @@ -0,0 +1,16 @@ + + + icon/internet-web-browser.svg + icon/system-run.svg + icon/preferences.svg + icon/network-server.svg + icon/dialog-question.svg + icon/system-software-update.svg + + + nekoray.png + nekoray.css + ../examples/vpn-run-root.sh + ../examples/sing-box-vpn.json + + diff --git a/res/nekoray.css b/res/nekoray.css new file mode 100644 index 0000000..512e281 --- /dev/null +++ b/res/nekoray.css @@ -0,0 +1,3 @@ +QMessageBox { + messagebox-text-interaction-flags: 5; +} diff --git a/res/nekoray.ico b/res/nekoray.ico new file mode 100644 index 0000000..4c355a1 Binary files /dev/null and b/res/nekoray.ico differ diff --git a/res/nekoray.png b/res/nekoray.png new file mode 120000 index 0000000..e73720c --- /dev/null +++ b/res/nekoray.png @@ -0,0 +1 @@ +../assets/nekoray.png \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss.qrc b/res/theme/feiyangqingyun/qss.qrc new file mode 100644 index 0000000..5541c0d --- /dev/null +++ b/res/theme/feiyangqingyun/qss.qrc @@ -0,0 +1,76 @@ + + + qss/flatgray.css + qss/lightblue.css + qss/flatgray/add_bottom.png + qss/flatgray/add_left.png + qss/flatgray/add_right.png + qss/flatgray/add_top.png + qss/flatgray/arrow_bottom.png + qss/flatgray/arrow_left.png + qss/flatgray/arrow_right.png + qss/flatgray/arrow_top.png + qss/flatgray/branch_close.png + qss/flatgray/branch_open.png + qss/flatgray/calendar_nextmonth.png + qss/flatgray/calendar_prevmonth.png + qss/flatgray/checkbox_checked.png + qss/flatgray/checkbox_checked_disable.png + qss/flatgray/checkbox_parcial.png + qss/flatgray/checkbox_parcial_disable.png + qss/flatgray/checkbox_unchecked.png + qss/flatgray/checkbox_unchecked_disable.png + qss/flatgray/menu_checked.png + qss/flatgray/radiobutton_checked.png + qss/flatgray/radiobutton_checked_disable.png + qss/flatgray/radiobutton_unchecked.png + qss/flatgray/radiobutton_unchecked_disable.png + qss/lightblue/add_bottom.png + qss/lightblue/add_left.png + qss/lightblue/add_right.png + qss/lightblue/add_top.png + qss/lightblue/arrow_bottom.png + qss/lightblue/arrow_left.png + qss/lightblue/arrow_right.png + qss/lightblue/arrow_top.png + qss/lightblue/branch_close.png + qss/lightblue/branch_open.png + qss/lightblue/calendar_nextmonth.png + qss/lightblue/calendar_prevmonth.png + qss/lightblue/checkbox_checked.png + qss/lightblue/checkbox_checked_disable.png + qss/lightblue/checkbox_parcial.png + qss/lightblue/checkbox_parcial_disable.png + qss/lightblue/checkbox_unchecked.png + qss/lightblue/checkbox_unchecked_disable.png + qss/lightblue/menu_checked.png + qss/lightblue/radiobutton_checked.png + qss/lightblue/radiobutton_checked_disable.png + qss/lightblue/radiobutton_unchecked.png + qss/lightblue/radiobutton_unchecked_disable.png + qss/blacksoft.css + qss/blacksoft/add_bottom.png + qss/blacksoft/add_left.png + qss/blacksoft/add_right.png + qss/blacksoft/add_top.png + qss/blacksoft/arrow_bottom.png + qss/blacksoft/arrow_left.png + qss/blacksoft/arrow_right.png + qss/blacksoft/arrow_top.png + qss/blacksoft/branch_close.png + qss/blacksoft/branch_open.png + qss/blacksoft/calendar_nextmonth.png + qss/blacksoft/calendar_prevmonth.png + qss/blacksoft/checkbox_checked.png + qss/blacksoft/checkbox_checked_disable.png + qss/blacksoft/checkbox_parcial.png + qss/blacksoft/checkbox_parcial_disable.png + qss/blacksoft/checkbox_unchecked.png + qss/blacksoft/checkbox_unchecked_disable.png + qss/blacksoft/menu_checked.png + qss/blacksoft/radiobutton_checked.png + qss/blacksoft/radiobutton_checked_disable.png + qss/blacksoft/radiobutton_unchecked.png + qss/blacksoft/radiobutton_unchecked_disable.png + + diff --git a/res/theme/feiyangqingyun/qss/blacksoft.css b/res/theme/feiyangqingyun/qss/blacksoft.css new file mode 100644 index 0000000..09a607a --- /dev/null +++ b/res/theme/feiyangqingyun/qss/blacksoft.css @@ -0,0 +1,679 @@ +QPalette{background:#444444;}*{outline:0px;color:#DCDCDC;} + +QGraphicsView{ +border:1px solid #242424; +qproperty-backgroundBrush:#444444; +} + +QWidget[form="true"],QLabel[frameShape="1"]{ +border:1px solid #242424; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#484848; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #DCDCDC; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#DCDCDC; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#AAAAAA; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#DCDCDC; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#AAAAAA; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#DCDCDC; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#AAAAAA; +background-color:#444444; +} + +QWidget[video="true"] QLabel{ +color:#DCDCDC; +border:1px solid #242424; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #AAAAAA; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QLineEdit:read-only{ +background-color:#484848; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #242424; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#AAAAAA; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #242424; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #242424; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #242424; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #242424; +color:#DCDCDC; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#DCDCDC; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/blacksoft/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/blacksoft/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/blacksoft/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/blacksoft/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/blacksoft/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/blacksoft/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/blacksoft/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/blacksoft/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/blacksoft/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/blacksoft/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/blacksoft/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/blacksoft/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/blacksoft/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#242424; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#DCDCDC; +background-color:#484848; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#DCDCDC; +background-color:#484848; +border:1px solid #242424; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/blacksoft/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/blacksoft/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#DCDCDC; +border:0px solid #242424; +background:#646464; +} + +QMenu::separator{ +height:1px; +background:#242424; +} + +QProgressBar{ +min-height:10px; +background:#484848; +border-radius:5px; +text-align:center; +border:1px solid #484848; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#242424; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#484848; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#484848; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#242424; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#484848; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#242424; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#484848; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); +} + +QScrollBar:horizontal{ +background:#484848; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#242424; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#AAAAAA; +} + +QScrollBar::handle:horizontal:pressed{ +background:#AAAAAA; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#484848; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#242424; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#AAAAAA; +} + +QScrollBar::handle:vertical:pressed{ +background:#AAAAAA; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #242424; +selection-background-color:#646464; +selection-color:#DCDCDC; +alternate-background-color:#525252; +gridline-color:#242424; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/blacksoft/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/blacksoft/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#444444; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#DCDCDC; +background:#383838; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#DCDCDC; +background:#525252; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #242424; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #242424; +color:#DCDCDC; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#AAAAAA; +background:#444444; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#242424; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#242424; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#242424; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#242424; +} + +QStatusBar::item{ +border:0px solid #484848; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QToolTip{ +border:0px solid #DCDCDC; +padding:1px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #DCDCDC; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/blacksoft/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/blacksoft/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #DCDCDC; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #242424; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #242424; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#444444; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#444444; +color:#DCDCDC; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#444444; +border-color:#484848; +color:#242424; +} + +/*TextColor:#DCDCDC*/ +/*PanelColor:#444444*/ +/*BorderColor:#242424*/ +/*NormalColorStart:#484848*/ +/*NormalColorEnd:#383838*/ +/*DarkColorStart:#646464*/ +/*DarkColorEnd:#525252*/ +/*HighColor:#AAAAAA*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png b/res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png new file mode 100644 index 0000000..b4a5f14 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_left.png b/res/theme/feiyangqingyun/qss/blacksoft/add_left.png new file mode 100644 index 0000000..165ebd0 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/add_left.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_right.png b/res/theme/feiyangqingyun/qss/blacksoft/add_right.png new file mode 100644 index 0000000..4c79925 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/add_right.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_top.png b/res/theme/feiyangqingyun/qss/blacksoft/add_top.png new file mode 100644 index 0000000..f76300f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/add_top.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/arrow_bottom.png b/res/theme/feiyangqingyun/qss/blacksoft/arrow_bottom.png new file mode 100644 index 0000000..39d7cbc Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/arrow_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/arrow_left.png b/res/theme/feiyangqingyun/qss/blacksoft/arrow_left.png new file mode 100644 index 0000000..1353cb8 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/arrow_left.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/arrow_right.png b/res/theme/feiyangqingyun/qss/blacksoft/arrow_right.png new file mode 100644 index 0000000..0e50d47 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/arrow_right.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png b/res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png new file mode 100644 index 0000000..d2c71e8 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/branch_close.png b/res/theme/feiyangqingyun/qss/blacksoft/branch_close.png new file mode 100644 index 0000000..58a7d13 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/branch_close.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/branch_open.png b/res/theme/feiyangqingyun/qss/blacksoft/branch_open.png new file mode 100644 index 0000000..a072d68 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/branch_open.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/calendar_nextmonth.png b/res/theme/feiyangqingyun/qss/blacksoft/calendar_nextmonth.png new file mode 100644 index 0000000..b06ae31 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/calendar_nextmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png new file mode 100644 index 0000000..46d4d62 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked.png new file mode 100644 index 0000000..b5ba6ef Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png new file mode 100644 index 0000000..f6aab40 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial.png new file mode 100644 index 0000000..cd1645f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial_disable.png new file mode 100644 index 0000000..dd0918f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png new file mode 100644 index 0000000..8a23968 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked_disable.png new file mode 100644 index 0000000..e2a2262 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png b/res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png new file mode 100644 index 0000000..4fca11f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png new file mode 100644 index 0000000..69e499f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked_disable.png new file mode 100644 index 0000000..f098cc5 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked.png new file mode 100644 index 0000000..3f36472 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png new file mode 100644 index 0000000..f729f17 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray.css b/res/theme/feiyangqingyun/qss/flatgray.css new file mode 100644 index 0000000..883e90f --- /dev/null +++ b/res/theme/feiyangqingyun/qss/flatgray.css @@ -0,0 +1,679 @@ +QPalette{background:#FFFFFF;}*{outline:0px;color:#57595B;} + +QGraphicsView{ +border:1px solid #B6B6B6; +qproperty-backgroundBrush:#FFFFFF; +} + +QWidget[form="true"],QLabel[frameShape="1"]{ +border:1px solid #B6B6B6; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#E4E4E4; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #57595B; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#57595B; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#575959; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#57595B; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#575959; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#57595B; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#575959; +background-color:#FFFFFF; +} + +QWidget[video="true"] QLabel{ +color:#57595B; +border:1px solid #B6B6B6; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #575959; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QLineEdit:read-only{ +background-color:#E4E4E4; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #B6B6B6; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#575959; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #B6B6B6; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #B6B6B6; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #B6B6B6; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #B6B6B6; +color:#57595B; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#57595B; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/flatgray/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/flatgray/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/flatgray/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/flatgray/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/flatgray/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/flatgray/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/flatgray/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/flatgray/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/flatgray/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/flatgray/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/flatgray/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/flatgray/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/flatgray/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#B6B6B6; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#57595B; +background-color:#E4E4E4; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#57595B; +background-color:#E4E4E4; +border:1px solid #B6B6B6; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/flatgray/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/flatgray/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#57595B; +border:0px solid #B6B6B6; +background:#F6F6F6; +} + +QMenu::separator{ +height:1px; +background:#B6B6B6; +} + +QProgressBar{ +min-height:10px; +background:#E4E4E4; +border-radius:5px; +text-align:center; +border:1px solid #E4E4E4; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#B6B6B6; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#B6B6B6; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #FFFFFF,stop:0.8 #B6B6B6); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#B6B6B6; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #FFFFFF,stop:0.8 #B6B6B6); +} + +QScrollBar:horizontal{ +background:#E4E4E4; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#B6B6B6; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#575959; +} + +QScrollBar::handle:horizontal:pressed{ +background:#575959; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#E4E4E4; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#B6B6B6; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#575959; +} + +QScrollBar::handle:vertical:pressed{ +background:#575959; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #B6B6B6; +selection-background-color:#F6F6F6; +selection-color:#57595B; +alternate-background-color:#F6F6F6; +gridline-color:#B6B6B6; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/flatgray/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/flatgray/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#FFFFFF; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#57595B; +background:#E4E4E4; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#57595B; +background:#F6F6F6; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #B6B6B6; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #B6B6B6; +color:#57595B; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#575959; +background:#FFFFFF; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#B6B6B6; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#B6B6B6; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#B6B6B6; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#B6B6B6; +} + +QStatusBar::item{ +border:0px solid #E4E4E4; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QToolTip{ +border:0px solid #57595B; +padding:1px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #57595B; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/flatgray/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/flatgray/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #57595B; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #B6B6B6; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #B6B6B6; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#FFFFFF; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#FFFFFF; +color:#57595B; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#FFFFFF; +border-color:#E4E4E4; +color:#B6B6B6; +} + +/*TextColor:#57595B*/ +/*PanelColor:#FFFFFF*/ +/*BorderColor:#B6B6B6*/ +/*NormalColorStart:#E4E4E4*/ +/*NormalColorEnd:#E4E4E4*/ +/*DarkColorStart:#F6F6F6*/ +/*DarkColorEnd:#F6F6F6*/ +/*HighColor:#575959*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_bottom.png b/res/theme/feiyangqingyun/qss/flatgray/add_bottom.png new file mode 100644 index 0000000..868e687 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/add_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_left.png b/res/theme/feiyangqingyun/qss/flatgray/add_left.png new file mode 100644 index 0000000..d9d5127 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/add_left.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_right.png b/res/theme/feiyangqingyun/qss/flatgray/add_right.png new file mode 100644 index 0000000..be8dd1a Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/add_right.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_top.png b/res/theme/feiyangqingyun/qss/flatgray/add_top.png new file mode 100644 index 0000000..ef55ce1 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/add_top.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png b/res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png new file mode 100644 index 0000000..37307b7 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/arrow_left.png b/res/theme/feiyangqingyun/qss/flatgray/arrow_left.png new file mode 100644 index 0000000..aa4312b Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/arrow_left.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/arrow_right.png b/res/theme/feiyangqingyun/qss/flatgray/arrow_right.png new file mode 100644 index 0000000..4db5efa Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/arrow_right.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/arrow_top.png b/res/theme/feiyangqingyun/qss/flatgray/arrow_top.png new file mode 100644 index 0000000..0da1cad Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/arrow_top.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/branch_close.png b/res/theme/feiyangqingyun/qss/flatgray/branch_close.png new file mode 100644 index 0000000..f5b6d34 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/branch_close.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/branch_open.png b/res/theme/feiyangqingyun/qss/flatgray/branch_open.png new file mode 100644 index 0000000..6392c13 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/branch_open.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/calendar_nextmonth.png b/res/theme/feiyangqingyun/qss/flatgray/calendar_nextmonth.png new file mode 100644 index 0000000..6cb40d7 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/calendar_nextmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png new file mode 100644 index 0000000..8a17d0f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked.png new file mode 100644 index 0000000..8808382 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked_disable.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked_disable.png new file mode 100644 index 0000000..9aa0a10 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png new file mode 100644 index 0000000..97376f3 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial_disable.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial_disable.png new file mode 100644 index 0000000..b9b286f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked.png new file mode 100644 index 0000000..50c25e2 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked_disable.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked_disable.png new file mode 100644 index 0000000..7c4c7db Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/menu_checked.png b/res/theme/feiyangqingyun/qss/flatgray/menu_checked.png new file mode 100644 index 0000000..6a1c729 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/menu_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png new file mode 100644 index 0000000..513a41e Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png new file mode 100644 index 0000000..8d16af5 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked.png new file mode 100644 index 0000000..8ef34fb Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked_disable.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked_disable.png new file mode 100644 index 0000000..f70c364 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue.css b/res/theme/feiyangqingyun/qss/lightblue.css new file mode 100644 index 0000000..590189b --- /dev/null +++ b/res/theme/feiyangqingyun/qss/lightblue.css @@ -0,0 +1,679 @@ +QPalette{background:#EAF7FF;}*{outline:0px;color:#386487;} + +QGraphicsView{ +border:1px solid #C0DCF2; +qproperty-backgroundBrush:#EAF7FF; +} + +QWidget[form="true"],QLabel[frameShape="1"],QGraphicsView{ +border:1px solid #C0DCF2; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#DEF0FE; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #386487; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#386487; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#386488; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#386487; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#386488; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#386487; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#386488; +background-color:#EAF7FF; +} + +QWidget[video="true"] QLabel{ +color:#386487; +border:1px solid #C0DCF2; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #386488; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QLineEdit:read-only{ +background-color:#DEF0FE; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #C0DCF2; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#386488; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #C0DCF2; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #C0DCF2; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #C0DCF2; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #C0DCF2; +color:#386487; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#386487; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/lightblue/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/lightblue/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/lightblue/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/lightblue/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/lightblue/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/lightblue/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/lightblue/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/lightblue/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/lightblue/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/lightblue/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/lightblue/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/lightblue/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/lightblue/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#C0DCF2; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#386487; +background-color:#DEF0FE; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#386487; +background-color:#DEF0FE; +border:1px solid #C0DCF2; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/lightblue/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/lightblue/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#386487; +border:0px solid #C0DCF2; +background:#F2F9FF; +} + +QMenu::separator{ +height:1px; +background:#C0DCF2; +} + +QProgressBar{ +min-height:10px; +background:#DEF0FE; +border-radius:5px; +text-align:center; +border:1px solid #DEF0FE; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#C0DCF2; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#C0DCF2; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #EAF7FF,stop:0.8 #C0DCF2); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#C0DCF2; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #EAF7FF,stop:0.8 #C0DCF2); +} + +QScrollBar:horizontal{ +background:#DEF0FE; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#C0DCF2; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#386488; +} + +QScrollBar::handle:horizontal:pressed{ +background:#386488; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#DEF0FE; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#C0DCF2; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#386488; +} + +QScrollBar::handle:vertical:pressed{ +background:#386488; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #C0DCF2; +selection-background-color:#F2F9FF; +selection-color:#386487; +alternate-background-color:#DAEFFF; +gridline-color:#C0DCF2; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/lightblue/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/lightblue/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#EAF7FF; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#386487; +background:#C0DEF6; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#386487; +background:#DAEFFF; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #C0DCF2; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #C0DCF2; +color:#386487; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#386488; +background:#EAF7FF; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#C0DCF2; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#C0DCF2; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#C0DCF2; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#C0DCF2; +} + +QStatusBar::item{ +border:0px solid #DEF0FE; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QToolTip{ +border:0px solid #386487; +padding:1px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #386487; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/lightblue/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/lightblue/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #386487; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #C0DCF2; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #C0DCF2; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#EAF7FF; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#EAF7FF; +color:#386487; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#EAF7FF; +border-color:#DEF0FE; +color:#C0DCF2; +} + +/*TextColor:#386487*/ +/*PanelColor:#EAF7FF*/ +/*BorderColor:#C0DCF2*/ +/*NormalColorStart:#DEF0FE*/ +/*NormalColorEnd:#C0DEF6*/ +/*DarkColorStart:#F2F9FF*/ +/*DarkColorEnd:#DAEFFF*/ +/*HighColor:#386488*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/lightblue/add_bottom.png b/res/theme/feiyangqingyun/qss/lightblue/add_bottom.png new file mode 100644 index 0000000..99eadb9 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/add_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/add_left.png b/res/theme/feiyangqingyun/qss/lightblue/add_left.png new file mode 100644 index 0000000..a7329d3 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/add_left.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/add_right.png b/res/theme/feiyangqingyun/qss/lightblue/add_right.png new file mode 100644 index 0000000..0c0ef6f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/add_right.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/add_top.png b/res/theme/feiyangqingyun/qss/lightblue/add_top.png new file mode 100644 index 0000000..911f5b4 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/add_top.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_bottom.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_bottom.png new file mode 100644 index 0000000..6dc9f23 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/arrow_bottom.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_left.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_left.png new file mode 100644 index 0000000..87171e9 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/arrow_left.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_right.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_right.png new file mode 100644 index 0000000..604def6 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/arrow_right.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_top.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_top.png new file mode 100644 index 0000000..3c02231 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/arrow_top.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/branch_close.png b/res/theme/feiyangqingyun/qss/lightblue/branch_close.png new file mode 100644 index 0000000..73492b3 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/branch_close.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/branch_open.png b/res/theme/feiyangqingyun/qss/lightblue/branch_open.png new file mode 100644 index 0000000..9abd65c Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/branch_open.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png b/res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png new file mode 100644 index 0000000..36a453b Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png new file mode 100644 index 0000000..cce673f Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png new file mode 100644 index 0000000..524d243 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked_disable.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked_disable.png new file mode 100644 index 0000000..de9ece8 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial.png new file mode 100644 index 0000000..1990ead Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png new file mode 100644 index 0000000..cdd3779 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked.png new file mode 100644 index 0000000..0f766a4 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked_disable.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked_disable.png new file mode 100644 index 0000000..25c0cdf Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/menu_checked.png b/res/theme/feiyangqingyun/qss/lightblue/menu_checked.png new file mode 100644 index 0000000..bb52701 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/menu_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png new file mode 100644 index 0000000..cd6561d Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked_disable.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked_disable.png new file mode 100644 index 0000000..27a6896 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked_disable.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png new file mode 100644 index 0000000..8f0b4fc Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png differ diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png new file mode 100644 index 0000000..57c0eb3 Binary files /dev/null and b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png differ diff --git a/rpc/gRPC.cpp b/rpc/gRPC.cpp new file mode 100644 index 0000000..672d7de --- /dev/null +++ b/rpc/gRPC.cpp @@ -0,0 +1,292 @@ +#include "gRPC.h" + +#include +#include + +#ifndef NKR_NO_GRPC + +#include "main/NekoRay.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace QtGrpc { + const char *GrpcAcceptEncodingHeader = "grpc-accept-encoding"; + const char *AcceptEncodingHeader = "accept-encoding"; + const char *TEHeader = "te"; + const char *GrpcStatusHeader = "grpc-status"; + const char *GrpcStatusMessage = "grpc-message"; + const int GrpcMessageSizeHeaderSize = 5; + + class Http2GrpcChannelPrivate : public QObject { + private: + QString url_base; + QThread *thread; + QNetworkAccessManager *nm; + + QString nekoray_auth; + QString serviceName; + + // TODO WTF + // https://github.com/semlanik/qtprotobuf/issues/116 +// setCachingEnabled: 5 bytesDownloaded +// QNetworkReplyImpl: backend error: caching was enabled after some bytes had been written + + // async + QNetworkReply *post(const QString &method, const QString &service, const QByteArray &args) { + QUrl callUrl = url_base + "/" + service + "/" + method; +// qDebug() << "Service call url: " << callUrl; + + QNetworkRequest request(callUrl); + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String{"application/grpc"}); + request.setRawHeader("Cache-Control", "no-store"); + request.setRawHeader(GrpcAcceptEncodingHeader, QByteArray{"identity,deflate,gzip"}); + request.setRawHeader(AcceptEncodingHeader, QByteArray{"identity,gzip"}); + request.setRawHeader(TEHeader, QByteArray{"trailers"}); + request.setAttribute(QNetworkRequest::Http2DirectAttribute, true); + request.setRawHeader("nekoray_auth", nekoray_auth.toLatin1()); + + QByteArray msg(GrpcMessageSizeHeaderSize, '\0'); + *reinterpret_cast(msg.data() + 1) = qToBigEndian((int) args.size()); + msg += args; + // qDebug() << "SEND: " << msg.size(); + + QNetworkReply *networkReply = nm->post(request, msg); + return networkReply; + } + + static QByteArray processReply(QNetworkReply *networkReply, QNetworkReply::NetworkError &statusCode) { + // Check if no network error occured + if (networkReply->error() != QNetworkReply::NoError) { + statusCode = networkReply->error(); + return {}; + } + + // Check if server answer with error + auto errCode = networkReply->rawHeader(GrpcStatusHeader).toInt(); + if (errCode != 0) { + QStringList errstr; + errstr << "grpc-status error code:" << Int2String(errCode) << ", error msg:" + << QLatin1String(networkReply->rawHeader(GrpcStatusMessage)); + showLog(errstr.join(" ")); + statusCode = QNetworkReply::NetworkError::ProtocolUnknownError; + return {}; + } + statusCode = QNetworkReply::NetworkError::NoError; + return networkReply->readAll().mid(GrpcMessageSizeHeaderSize); + } + + QNetworkReply::NetworkError + call(const QString &method, const QString &service, const QByteArray &args, QByteArray &qByteArray, + int timeout_ms) { + QNetworkReply *networkReply = post(method, service, args); + + QTimer *abortTimer = nullptr; + if (timeout_ms > 0) { + abortTimer = new QTimer; + abortTimer->setSingleShot(true); + abortTimer->setInterval(timeout_ms); + connect(abortTimer, &QTimer::timeout, abortTimer, [=]() { + networkReply->abort(); + }); + abortTimer->start(); + } + + { + QEventLoop loop; + QObject::connect(networkReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + } + + if (abortTimer != nullptr) { + abortTimer->stop(); + abortTimer->deleteLater(); + } + + auto grpcStatus = QNetworkReply::NetworkError::ProtocolUnknownError; + qByteArray = processReply(networkReply, grpcStatus); + // qDebug() << __func__ << "RECV: " << qByteArray.toHex() << "grpcStatus" << grpcStatus; + + networkReply->deleteLater(); + return grpcStatus; + } + + public: + Http2GrpcChannelPrivate(const QString &url_, const QString &nekoray_auth_, const QString &serviceName_) { + url_base = "http://" + url_; + nekoray_auth = nekoray_auth_; + serviceName = serviceName_; + // + thread = new QThread; + nm = new QNetworkAccessManager(); + nm->moveToThread(thread); + thread->start(); + } + + QNetworkReply::NetworkError Call(const QString &methodName, + const google::protobuf::Message &req, google::protobuf::Message *rsp, + int timeout_ms = 0) { + std::string reqStr; + req.SerializeToString(&reqStr); + auto requestArray = QByteArray::fromStdString(reqStr); + + QByteArray responseArray; + QNetworkReply::NetworkError err; + QMutex lock; + lock.lock(); + + runOnUiThread([&] { + err = call(methodName, serviceName, requestArray, responseArray, timeout_ms); + lock.unlock(); + }, nm); + + lock.lock(); + lock.unlock(); +// qDebug() << "rsp err" << err; +// qDebug() << "rsp array" << responseArray; + + if (err != QNetworkReply::NetworkError::NoError) { + return err; + } + if (!rsp->ParseFromArray(responseArray.data(), responseArray.size())) { + return QNetworkReply::NetworkError(-114514); + } + return QNetworkReply::NetworkError::NoError; + } + + }; +} + +namespace NekoRay::rpc { + Client::Client(std::function onError, const QString &target, const QString &token) { + this->grpc_channel = std::make_unique(target, token, "libcore.LibcoreService"); + this->onError = std::move(onError); + } + +#define NOT_OK *rpcOK = false; \ + onError( \ + QString("QNetworkReply::NetworkError code: %1\n").arg(status) \ + ); + + void Client::Exit() { + libcore::EmptyReq request; + libcore::EmptyResp reply; + grpc_channel->Call("Exit", request, &reply, 500); + } + + QString Client::Start(bool *rpcOK, const libcore::LoadConfigReq &request) { + libcore::ErrorResp reply; + auto status = grpc_channel->Call("Start", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + QString Client::SetTun(bool *rpcOK, const libcore::SetTunReq &request) { + libcore::ErrorResp reply; + auto status = grpc_channel->Call("SetTun", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + QString Client::Stop(bool *rpcOK) { + libcore::EmptyReq request; + libcore::ErrorResp reply; + auto status = grpc_channel->Call("Stop", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + bool Client::KeepAlive() { + libcore::EmptyReq request; + libcore::EmptyResp reply; + auto status = grpc_channel->Call("KeepAlive", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return true; + } else { + return false; + } + } + + long long Client::QueryStats(const std::string &tag, const std::string &direct) { + libcore::QueryStatsReq request; + request.set_tag(tag); + request.set_direct(direct); + + libcore::QueryStatsResp reply; + auto status = grpc_channel->Call("QueryStats", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return reply.traffic(); + } else { + return 0; + } + } + + std::string Client::ListV2rayConnections() { + libcore::EmptyReq request; + libcore::ListV2rayConnectionsResp reply; + auto status = grpc_channel->Call("ListV2rayConnections", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return reply.matsuri_connections_json(); + } else { + return ""; + } + } + + // + + libcore::TestResp Client::Test(bool *rpcOK, const libcore::TestReq &request) { + libcore::TestResp reply; + auto status = grpc_channel->Call("Test", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return reply; + } else { + NOT_OK + return reply; + } + } + + libcore::UpdateResp Client::Update(bool *rpcOK, const libcore::UpdateReq &request) { + libcore::UpdateResp reply; + auto status = grpc_channel->Call("Update", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return reply; + } else { + NOT_OK + return reply; + } + } +} + +#endif diff --git a/rpc/gRPC.h b/rpc/gRPC.h new file mode 100644 index 0000000..c2e6852 --- /dev/null +++ b/rpc/gRPC.h @@ -0,0 +1,49 @@ +#pragma once + +#ifdef NKR_NO_EXTERNAL +#define NKR_NO_GRPC +#endif + +#ifndef NKR_NO_GRPC + +#include "go/gen/libcore.pb.h" +#include + +namespace QtGrpc { + class Http2GrpcChannelPrivate; +} + +namespace NekoRay::rpc { + class Client { + public: + explicit Client(std::function onError, const QString &target, const QString &token); + + void Exit(); + + bool KeepAlive(); + + // QString returns is error string + + QString Start(bool *rpcOK, const libcore::LoadConfigReq &request); + + QString SetTun(bool *rpcOK, const libcore::SetTunReq &request); + + QString Stop(bool *rpcOK); + + long long QueryStats(const std::string &tag, const std::string &direct); + + std::string ListV2rayConnections(); + + libcore::TestResp Test(bool *rpcOK, const libcore::TestReq &request); + + libcore::UpdateResp Update(bool *rpcOK, const libcore::UpdateReq &request); + + private: + std::unique_ptr grpc_channel; + std::function onError; + }; + + inline Client *defaultClient; +} +#endif + diff --git a/sub/GroupUpdater.cpp b/sub/GroupUpdater.cpp new file mode 100644 index 0000000..45cecb3 --- /dev/null +++ b/sub/GroupUpdater.cpp @@ -0,0 +1,354 @@ +#include "qv2ray/utils/HTTPRequestHelper.hpp" + +#include "db/Database.hpp" +#include "db/ProfileFilter.hpp" +#include "fmt/includes.h" + +#include "GroupUpdater.hpp" + +#include + +#ifndef NKR_NO_EXTERNAL + +#include + +#endif + +#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a + +namespace NekoRay::sub { + + GroupUpdater *groupUpdater = new GroupUpdater; + + void RawUpdater::update(const QString &str) { + // Base64 encoded subscription + if (auto str2 = DecodeB64IfValid(str);!str2.isEmpty()) { + update(str2); + return; + } + + // Clash + if (str.contains("proxies:")) { + updateClash(str); + return; + } + + // Multi line + if (str.count("\n") > 0) { + auto list = str.split("\n"); + for (const auto &str2: list) { + update(str2.trimmed()); + } + return; + } + + QSharedPointer ent; + + // Nekoray format + if (str.startsWith("nekoray://")) { + auto link = QUrl(str); + if (!link.isValid()) return; + ent = ProfileManager::NewProxyEntity(link.host()); + if (ent->bean->version == -114514) return; + auto j = DecodeB64IfValid(link.fragment().toUtf8(), QByteArray::Base64UrlEncoding); + if (j.isEmpty()) return; + ent->bean->FromJsonBytes(j.toUtf8()); + } + + // SOCKS + if (str.startsWith("socks5://") || str.startsWith("socks4://") || + str.startsWith("socks4a://") || str.startsWith("socks://")) { + ent = ProfileManager::NewProxyEntity("socks"); + auto ok = ent->SocksHTTPBean()->TryParseLink(str); + if (!ok) return; + } + + // HTTP + if (str.startsWith("http://") || str.startsWith("https://")) { + ent = ProfileManager::NewProxyEntity("http"); + auto ok = ent->SocksHTTPBean()->TryParseLink(str); + if (!ok) return; + } + + // ShadowSocks + if (str.startsWith("ss://")) { + ent = ProfileManager::NewProxyEntity("shadowsocks"); + auto ok = ent->ShadowSocksBean()->TryParseLink(str); + if (!ok) return; + } + + // VMess + if (str.startsWith("vmess://")) { + ent = ProfileManager::NewProxyEntity("vmess"); + auto ok = ent->VMessBean()->TryParseLink(str); + if (!ok) return; + } + + // VMess + if (str.startsWith("vless://")) { + ent = ProfileManager::NewProxyEntity("vless"); + auto ok = ent->TrojanVLESSBean()->TryParseLink(str); + if (!ok) return; + } + + // Trojan + if (str.startsWith("trojan://")) { + ent = ProfileManager::NewProxyEntity("trojan"); + auto ok = ent->TrojanVLESSBean()->TryParseLink(str); + if (!ok) return; + } + + // Naive + if (str.startsWith("https+naive://")) { + ent = ProfileManager::NewProxyEntity("naive"); + auto ok = ent->NaiveBean()->TryParseLink(str); + if (!ok) return; + } + + // End + if (ent == nullptr) return; + profileManager->AddProfile(ent, gid_add_to); + update_counter++; + } + +#ifndef NKR_NO_EXTERNAL + + QString Node2QString(const YAML::Node &n, const QString &def = "") { + try { + return n.as().c_str(); + } catch (const YAML::Exception &ex) { + return def; + } + } + + int Node2Int(const YAML::Node &n, const int &def = 0) { + try { + return n.as(); + } catch (const YAML::Exception &ex) { + return def; + } + } + + bool Node2Bool(const YAML::Node &n, const bool &def = false) { + try { + return n.as(); + } catch (const YAML::Exception &ex) { + return def; + } + } + +#endif + +// https://github.com/Dreamacro/clash/wiki/configuration + void RawUpdater::updateClash(const QString &str) { +#ifndef NKR_NO_EXTERNAL + try { + auto proxies = YAML::Load(str.toStdString())["proxies"]; + for (auto proxy: proxies) { + auto type = Node2QString(proxy["type"]); + if (type == "ss" || type == "ssr") type = "shadowsocks"; + if (type == "socks5") type = "socks"; + + auto ent = ProfileManager::NewProxyEntity(type); + if (ent->bean->version == -114514) continue; + + // common + ent->bean->name = Node2QString(proxy["name"]); + ent->bean->serverAddress = Node2QString(proxy["server"]); + ent->bean->serverPort = Node2Int(proxy["port"]); + + if (type == "shadowsocks") { + auto bean = ent->ShadowSocksBean(); + bean->method = Node2QString(proxy["cipher"]).replace("dummy", "none"); + bean->password = Node2QString(proxy["password"]); + auto plugin_n = proxy["plugin"]; + auto pluginOpts_n = proxy["plugin-opts"]; + if (plugin_n.IsDefined() && pluginOpts_n.IsDefined()) { + if (Node2QString(plugin_n) == "obfs") { + bean->plugin = "obfs-local;obfs=" + Node2QString(pluginOpts_n["mode"]) + ";obfs-host=" + + Node2QString(pluginOpts_n["host"]); + } + } + auto protocol_n = proxy["protocol"]; + if (protocol_n.IsDefined()) { + continue; // SSR + } + } else if (type == "socks" || type == "http") { + auto bean = ent->SocksHTTPBean(); + bean->password = Node2QString(proxy["username"]); + bean->password = Node2QString(proxy["password"]); + if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + } else if (type == "trojan") { + auto bean = ent->TrojanVLESSBean(); + bean->password = Node2QString(proxy["password"]); + bean->stream->security = "tls"; + bean->stream->network = Node2QString(proxy["network"], "tcp"); + bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + } else if (type == "vmess") { + auto bean = ent->VMessBean(); + bean->uuid = Node2QString(proxy["uuid"]); + bean->aid = Node2Int(proxy["alterId"]); + bean->security = Node2QString(proxy["cipher"]); + bean->stream->network = Node2QString(proxy["network"], "tcp"); + bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); + if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + + auto ws = proxy["ws-opts"]; + if (ws.IsMap()) { + auto headers = ws["headers"]; + for (auto header: headers) { + if (Node2QString(header.first).toLower() == "host") { + bean->stream->host = Node2QString(header.second); + } + } + bean->stream->path = Node2QString(ws["path"]); + bean->stream->max_early_data = Node2Int(proxy["max-early-data"]); + bean->stream->early_data_header_name = Node2QString(ws["early-data-header-name"]); + } + + auto grpc = proxy["grpc-opts"]; + if (grpc.IsMap()) { + bean->stream->path = Node2QString(grpc["grpc-service-name"]); + } + + auto h2 = proxy["h2-opts"]; + if (h2.IsMap()) { + auto hosts = ws["host"]; + for (auto host: hosts) { + bean->stream->host = Node2QString(host); + break; + } + bean->stream->path = Node2QString(h2["path"]); + } + } else { + continue; + } + + profileManager->AddProfile(ent, gid_add_to); + update_counter++; + } + } catch (const YAML::Exception &ex) { + runOnUiThread([=] { + MessageBoxWarning("YAML Exception", ex.what()); + }); + } +#endif + } + + // 不要刷新,下载导入完会自己刷新 + void GroupUpdater::AsyncUpdate(const QString &str, int _sub_gid, + QObject *caller, const std::function &callback) { + if (caller != nullptr && callback != nullptr) { + connectOnce(this, &GroupUpdater::AsyncUpdateCallback, caller, + [=](QObject *receiver) { + if (receiver == caller) callback(); + }); + } + + auto content = str.trimmed(); + bool asURL = false; + + if (_sub_gid < 0 && (content.startsWith("http://") || content.startsWith("https://"))) { + auto items = QStringList{QObject::tr("As Subscription"), QObject::tr("As link")}; + bool ok; + auto a = QInputDialog::getItem(nullptr, + QObject::tr("url detected"), + QObject::tr("%1\nHow to update?").arg(content), + items, 0, false, &ok); + if (!ok) return; + if (items.indexOf(a) == 0) asURL = true; + } + + runOnNewThread([=] { + Update(str, _sub_gid, asURL); + emit AsyncUpdateCallback(caller); + }); + } + + void GroupUpdater::Update(const QString &_str, int _sub_gid, bool _not_sub_as_url) { + // 创建 rawUpdater + NekoRay::dataStore->imported_count = 0; + auto rawUpdater = std::make_unique(); + rawUpdater->gid_add_to = _sub_gid; + + // 准备 + QString sub_user_info; + bool asURL = _sub_gid >= 0 || _not_sub_as_url; // 把 _str 当作 url 处理(下载内容) + auto content = _str.trimmed(); + auto group = profileManager->GetGroup(_sub_gid); + + // 网络请求 + if (asURL) { + auto groupName = group == nullptr ? content : group->name; + showLog(">>>>>>> " + QObject::tr("Requesting subscription: %1").arg(groupName)); + + auto resp = NetworkRequestHelper::HttpGet(content); + if (!resp.error.isEmpty()) { + showLog(">>>>>>> " + QObject::tr("Requesting subscription %1 error: %2") + .arg(groupName, resp.error + "\n" + resp.data)); + return; + } + + content = resp.data; + sub_user_info = NetworkRequestHelper::GetHeader(resp.header, "Subscription-UserInfo"); + } + + QList> in; // 更新前 + QList> out_all; // 更新前 + 更新后 + QList> out; // 更新后 + QList> only_in; // 只在更新前有的 + QList> only_out; // 只在更新后有的 + QList> update_del; // 更新前后都有的,删除更新后多余的 + + // 订阅解析前 + if (group != nullptr) { + in = group->Profiles(); + group->info = sub_user_info; + group->order.clear(); + group->Save(); + } + + // 解析并添加 profile + rawUpdater->update(content); + + if (group != nullptr) { + out_all = group->Profiles(); + + ProfileFilter::OnlyInSrc_ByPointer(out_all, in, out); + ProfileFilter::OnlyInSrc(in, out, only_in); + ProfileFilter::OnlyInSrc(out, in, only_out); + ProfileFilter::Common(in, out, update_del, false, true); + update_del += only_in; + + for (const auto &ent: update_del) { + profileManager->DeleteProfile(ent->id); + } + + QString notice_added; + for (const auto &ent: only_out) { + notice_added += ent->bean->DisplayTypeAndName() + "\n"; + } + QString notice_deleted; + for (const auto &ent: only_in) { + notice_deleted += ent->bean->DisplayTypeAndName() + "\n"; + } + + runOnUiThread([=] { + auto change = "\n" + QObject::tr("Added %1 profiles:\n%2\nDeleted %3 Profiles:\n%4") + .arg(only_out.length()).arg(notice_added) + .arg(only_in.length()).arg(notice_deleted); + if (only_out.length() + only_in.length() == 0) change = QObject::tr("Nothing"); + showLog(">>>>>>> " + QObject::tr("Change of %1:").arg(group->name) + " " + change); + dialog_message("SubUpdater", "finish-dingyue"); + }); + } else { + NekoRay::dataStore->imported_count = rawUpdater->update_counter; + runOnUiThread([=] { + dialog_message("SubUpdater", "finish"); + }); + } + } +} diff --git a/sub/GroupUpdater.hpp b/sub/GroupUpdater.hpp new file mode 100644 index 0000000..2fb9105 --- /dev/null +++ b/sub/GroupUpdater.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace NekoRay::sub { + class RawUpdater { + public: + void updateClash(const QString &str); + + void update(const QString &str); + + int gid_add_to = -1; // 导入到指定组 -1 为当前选中组 + + int update_counter = 0; // 新增了多少个配置 + }; + + class GroupUpdater : public QObject { + Q_OBJECT + + public: + void AsyncUpdate(const QString &str, int _sub_gid = -1, + QObject *caller = nullptr, const std::function &callback = nullptr); + + void Update(const QString &_str, int _sub_gid = -1, bool _not_sub_as_url = false); + + signals: + + void AsyncUpdateCallback(QObject *caller); + }; + + extern GroupUpdater *groupUpdater; +} + diff --git a/sys/AutoRun.cpp b/sys/AutoRun.cpp new file mode 100644 index 0000000..4570388 --- /dev/null +++ b/sys/AutoRun.cpp @@ -0,0 +1,69 @@ +#include "AutoRun.hpp" + +#include + +#ifdef Q_OS_WIN + +#include +#include + +//设置程序自启动 appPath程序路径 +void SetProcessAutoRunSelf(bool enable) { + auto appPath = QApplication::applicationFilePath(); + + QSettings settings("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + QSettings::NativeFormat); + + //以程序名称作为注册表中的键 + //根据键获取对应的值(程序路径) + QFileInfo fInfo(appPath); + QString name = fInfo.baseName(); + QString path = settings.value(name).toString(); + + //如果注册表中的路径和当前程序路径不一样, + //则表示没有设置自启动或自启动程序已经更换了路径 + //toNativeSeparators的意思是将"/"替换为"\" + QString newPath = QDir::toNativeSeparators(appPath); + + if (enable) { + if (path != newPath) { + settings.setValue(name, newPath); + } + } else { + settings.remove(name); + } +} + +bool GetProcessAutoRunSelf() { + auto appPath = QApplication::applicationFilePath(); + + QSettings settings("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + QSettings::NativeFormat); + + //以程序名称作为注册表中的键 + //根据键获取对应的值(程序路径) + QFileInfo fInfo(appPath); + QString name = fInfo.baseName(); + QString path = settings.value(name).toString(); + + //如果注册表中的路径和当前程序路径不一样, + //则表示没有设置自启动或自启动程序已经更换了路径 + //toNativeSeparators的意思是将"/"替换为"\" + QString newPath = QDir::toNativeSeparators(appPath); + return path == newPath; +} + + +#else + +#include + +void SetProcessAutoRunSelf(bool enable) { + QMessageBox::warning(nullptr, "Error", "Autorun is not yet implemented on your platform."); +} + +bool GetProcessAutoRunSelf() { + return false; +} + +#endif diff --git a/sys/AutoRun.hpp b/sys/AutoRun.hpp new file mode 100644 index 0000000..ef92937 --- /dev/null +++ b/sys/AutoRun.hpp @@ -0,0 +1,5 @@ +#pragma once + +void SetProcessAutoRunSelf(bool enable); + +bool GetProcessAutoRunSelf(); diff --git a/sys/ExternalProcess.cpp b/sys/ExternalProcess.cpp new file mode 100644 index 0000000..d6e74c8 --- /dev/null +++ b/sys/ExternalProcess.cpp @@ -0,0 +1,70 @@ +#include "ExternalProcess.hpp" + +namespace NekoRay::sys { + ExternalProcess::ExternalProcess(const QString &tag, + const QString &program, + const QStringList &arguments, + const QStringList &env) + : QProcess() { + this->tag = tag; + this->program = program; + this->arguments = arguments; + this->env = env; + this->running_list = &running_ext; + } + + void ExternalProcess::Start() { + if (started) return; + started = true; + *running_list += this; + + if (show_log) { + connect(this, &QProcess::readyReadStandardOutput, this, + [&]() { + showLog_ext_vt100(readAllStandardOutput().trimmed()); + }); + connect(this, &QProcess::readyReadStandardError, this, + [&]() { + showLog_ext_vt100(readAllStandardError().trimmed()); + }); + } + + connect(this, &QProcess::errorOccurred, this, + [&](QProcess::ProcessError error) { + if (!killed) { + crashed = true; + showLog_ext(tag, "[Error] Crashed:" + QProcess::errorString()); + dialog_message("ExternalProcess", "Crashed"); + } + }); + connect(this, &QProcess::stateChanged, this, + [&](QProcess::ProcessState state) { + if (state == QProcess::NotRunning) { + if (killed) { + showLog_ext(tag, "Stopped"); + } else if (!crashed) { + crashed = true; + Kill(); + showLog_ext(tag, "[Error] Crashed?"); + dialog_message("ExternalProcess", "Crashed"); + } + } + }); + + showLog_ext(tag, "[Starting] " + env.join(" ") + " " + program + " " + arguments.join(" ")); + + QProcess::setEnvironment(env); + QProcess::start(program, arguments); + } + + void ExternalProcess::Kill() { + if (killed) return; + killed = true; + running_list->removeAll(this); + if (!crashed) { + QProcess::kill(); + QProcess::waitForFinished(500); + } + } + +} diff --git a/sys/ExternalProcess.hpp b/sys/ExternalProcess.hpp new file mode 100644 index 0000000..3350ed7 --- /dev/null +++ b/sys/ExternalProcess.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "main/NekoRay.hpp" + +#include + +namespace NekoRay::sys { + class ExternalProcess : public QProcess { + public: + QString tag; + QString program; + QStringList arguments; + QStringList env; + + bool show_log = true; + QList *running_list; + + ExternalProcess(const QString &tag, + const QString &program, + const QStringList &arguments, + const QStringList &env); + + // start & kill is one time + + void Start(); + + void Kill(); + + private: + bool started = false; + bool killed = false; + bool crashed = false; + }; + + // start & kill change this list + inline QList running_ext; +} diff --git a/sys/windows/MiniDump.cpp b/sys/windows/MiniDump.cpp new file mode 100644 index 0000000..58d61e2 --- /dev/null +++ b/sys/windows/MiniDump.cpp @@ -0,0 +1,68 @@ +#include "MiniDump.h" + +#include +#include +#include + +#include +#include +#include +#include + +typedef BOOL( WINAPI *MINIDUMPWRITEDUMP )( +HANDLE hProcess, +DWORD dwPid, +HANDLE hFile, +MINIDUMP_TYPE DumpType, +CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, +CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, +CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam); + +LONG CreateCrashHandler(EXCEPTION_POINTERS *pException) { + QDir::setCurrent(QApplication::applicationDirPath()); + + HMODULE DllHandle = NULL; + DllHandle = LoadLibrary(_T("DBGHELP.DLL")); + + if (DllHandle) { + MINIDUMPWRITEDUMP Dump = (MINIDUMPWRITEDUMP) GetProcAddress(DllHandle, "MiniDumpWriteDump"); + if (Dump) { + //创建 Dump 文件 + QDateTime CurDTime = QDateTime::currentDateTime(); + QString current_date = CurDTime.toString("yyyy_MM_dd_hh_mm_ss"); + //dmp文件的命名 + QString dumpText = "Dump_" + current_date + ".dmp"; + EXCEPTION_RECORD *record = pException->ExceptionRecord; + QString errCode(QString::number(record->ExceptionCode, 16)); + QString errAddr(QString::number((uint) record->ExceptionAddress, 16)); + QString errFlag(QString::number(record->ExceptionFlags, 16)); + QString errPara(QString::number(record->NumberParameters, 16)); + HANDLE DumpHandle = CreateFile((LPCWSTR) dumpText.utf16(), + GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (DumpHandle != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION dumpInfo; + dumpInfo.ExceptionPointers = pException; + dumpInfo.ThreadId = GetCurrentThreadId(); + dumpInfo.ClientPointers = TRUE; + //将dump信息写入dmp文件 + Dump(GetCurrentProcess(), GetCurrentProcessId(), DumpHandle, MiniDumpNormal, &dumpInfo, + NULL, NULL); + CloseHandle(DumpHandle); + } else { + dumpText = ""; + } + //创建消息提示 + QMessageBox::warning(NULL, "Application crashed", + QString("ErrorCode: %1 ErrorAddr:%2 ErrorFlag: %3 ErrorPara: %4\nVersion: %5\nDump file at %6") + .arg(errCode).arg(errAddr).arg(errFlag).arg(errPara) + .arg(NKR_VERSION).arg(dumpText), + QMessageBox::Ok); + return EXCEPTION_EXECUTE_HANDLER; + } + } +} + +void Windows_SetCrashHandler() { + SetErrorMode(SEM_FAILCRITICALERRORS); + SetUnhandledExceptionFilter(CreateCrashHandler); +} diff --git a/sys/windows/MiniDump.h b/sys/windows/MiniDump.h new file mode 100644 index 0000000..ed5257e --- /dev/null +++ b/sys/windows/MiniDump.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __MINGW32__ + +void Windows_SetCrashHandler(){} + +#else + +void Windows_SetCrashHandler(); + +#endif diff --git a/test/test-qt6-build.sh b/test/test-qt6-build.sh new file mode 100644 index 0000000..7c1247b --- /dev/null +++ b/test/test-qt6-build.sh @@ -0,0 +1,5 @@ +rm -rf build-qt6 +mkdir -p build-qt6 +cd build-qt6 +cmake -GNinja -DQT_VERSION_MAJOR=6 .. +ninja diff --git a/translations/translations.qrc b/translations/translations.qrc new file mode 100644 index 0000000..ee9968c --- /dev/null +++ b/translations/translations.qrc @@ -0,0 +1,5 @@ + + + zh_CN.qm + + diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts new file mode 100644 index 0000000..6fb99ef --- /dev/null +++ b/translations/zh_CN.ts @@ -0,0 +1,1185 @@ + + + + + DialogBasicSettings + + Basic Settings + 基本设置 + + + Enable + 启用 + + + HTTP Listen Port + HTTP 监听端口 + + + Listen Address + 监听地址 + + + Socks Listen Port + Socks 监听端口 + + + concurrency + 并发 + + + User Agent + + + + Common + 通用 + + + Style + 样式 + + + Theme + 主题 + + + System + 系统 + + + Subscription + 订阅 + + + Core + 核心 + + + Extra Core + 其他核心 + + + Select + 选择 + + + Edit + 编辑 + + + Custom Inbound + 自定义入站 + + + Asset Location + 资源文件路径 + + + Default: dir of "nekoray-core" + 默认值:和 nekoray_core 同路径 + + + Settings changed + 设置改变 + + + Restart nekoray to take effect. + 重启 nekoray 生效。 + + + Concurrent + 并发 + + + Test URL + 测试 URL + + + Use proxy when updating subscription + 更新订阅时使用代理 + + + Language + 语言 + + + Security + 安全 + + + Insecure hint + 提示不安全的配置 + + + Skip TLS certificate authentication by default + 默认跳过 TLS 证书验证 + + + Traffic statistics refresh rate + 流量统计刷新率 + + + Fast + + + + Slow + + + + Off + 关闭 + + + Add + 添加 + + + Delete + 删除 + + + Please input the core name. + 请输入核心名. + + + Please select the core name. + 请选择核心名. + + + Connection statistics + 连接统计 + + + + DialogEditGroup + + Edit Group + 编辑分组 + + + Type + 类型 + + + Name + 名称 + + + Basic + 基本 + + + Subscription + 订阅 + + + URL + + + + Archive + 归档 + + + Warning + 警告 + + + Please input URL + 请输入 URL + + + Copy profile share links + 复制所有配置的分享链接 + + + Copied + 复制成功 + + + Copy profile share links (Nekoray) + 复制所有配置的分享链接 (Nekoray) + + + + DialogEditProfile + + Edit + 编辑 + + + Common + 通用 + + + Type + 类型 + + + Port + 端口 + + + Address + 地址 + + + Name + 名称 + + + Network + 传输 + + + Security + 传输层安全 + + + Network Settings (%1) + 传输设置 (%1) + + + Security Settings + 安全设置 + + + Allow insecure + 不检查服务器证书(不安全) + + + Certificate + 证书 + + + Custom Json Settings + 自定义 JSON 设置 + + + Not set + 未设置 + + + Already set + 已设置 + + + Path + 路径(Path) + + + Host + 主机(Host) + + + SNI + + + + Custom + 自定义 + + + Packet Encoding + 包编码 + + + Settings + 设置 + + + + DialogHotkey + + Hot key + 热键 + + + Show groups + 显示分组 + + + Show routes + 显示路由 + + + Trigger main window + 显示/隐藏主窗口 + + + + DialogManageGroups + + Groups + 分组 + + + New group + 新建分组 + + + Update all subscriptions + 更新所有订阅 + + + Confirmation + 确认 + + + Update all subscriptions? + 更新所有订阅? + + + + DialogManageRoutes + + Routes + 路由 + + + Outbound Domain Strategy + 出站域名策略 + + + Disable + 禁用 + + + Sniffing Mode + 流量探测 + + + The sniffing result is used for routing + 探测结果用于路由判断 + + + The sniffing result is used for destination + 探测结果用于目标地址 + + + Direct DNS + 直连 DNS + + + Remote DNS + 远程 DNS + + + Enable DNS Routing + 启用 DNS 路由 + + + Domain Strategy + 域名策略 + + + Matcher + 域名匹配器 + + + Block + 阻止 + + + Direct + 直连 + + + Domain + 域名 + + + Proxy + 代理 + + + Preset + 预设 + + + Bypass LAN and China + 绕过局域网和大陆 + + + Global + 全局 + + + IP + + + + Custom + 自定义 + + + Save + 保存 + + + Load + 加载 + + + Cancel + 取消 + + + Remove + 删除 + + + Save routing: %1 + 保存路由: %1 + + + Load routing: %1 + 加载路由: %1 + + + Remove routing: %1 + 删除路由: %1 + + + Mange route set + 管理路由规则 + + + Custom (global) + 自定义 (全局) + + + + EditChain + + EditChain + + + + Select Profile + 选择配置 + + + Traffic order is from top to bottom + 流量顺序是从上到下(最后一个配置为流量的出口) + + + + EditCustom + + EditCustom + + + + Core + 核心 + + + Json + + + + Command + 命令 + + + + EditNaive + + EditNaive + + + + Protocol + 协议 + + + Password + 密码 + + + Extra headers + 附加标头 + + + SNI + + + + Username + 用户名 + + + Certificate + 证书 + + + Insecure concurrency + 不安全并发 + + + + EditShadowSocks + + Plugin Args + 插件参数 + + + Password + 密码 + + + Encryption + 加密 + + + Plugin + 插件 + + + Form + + + + + EditSocksHttp + + Version + 版本 + + + Username + 用户名 + + + Password + 密码 + + + Form + + + + + EditTrojanVLESS + + Password + 密码 + + + + EditVMess + + Security + 加密 + + + EditVMess + + + + Alter Id + + + + UUID + + + + + GroupItem + + Update Subscription + 更新订阅 + + + Edit + 编辑 + + + Basic + 基本 + + + Subscription + 订阅 + + + Confirmation + 确认 + + + Remove + 删除 + + + Remove %1? + 删除 %1 ? + + + Archive + 归档 + + + Update %1? + 更新 %1 ? + + + + JsonEditor + + JSON Editor + + + + Format JSON + + + + Remove All Comments + + + + Json Editor + + + + Structure Preview + + + + OK + + + + Json Contains Syntax Errors + + + + Original Json may contain syntax errors. Json tree is disabled. + + + + You must correct these errors before continuing. + + + + Syntax Errors + + + + Please fix the JSON errors or remove the comments before continue + + + + + MainWindow + + Program + 程序 + + + Preferences + 首选项 + + + Server + 服务器 + + + Ads + 推广 + + + Type + 类型 + + + Address + 地址 + + + Name + 名称 + + + Test Result + 测试结果 + + + Traffic + 流量 + + + System Proxy + 系统代理 + + + Share + 分享 + + + Exit + 退出 + + + Basic Settings + 基本设置 + + + Groups + 分组 + + + Stop + 停止 + + + Routes + 路由 + + + Add profile from clipboard + 从剪切板添加 + + + Debug Info + 调试信息 + + + Copy Link + 复制链接 + + + Clear Test Result + 清理测试结果 + + + Scan QR Code + 扫描 QR Code + + + Disable + 禁用 + + + Error + 错误 + + + Default + 默认 + + + Confirmation + 确认 + + + Settings changed, restart proxy? + 设置已改变,是否重启代理? + + + Imported %1 profile(s) + 导入了 %1 个配置 + + + Running: %1 + 正在运行: %1 + + + None + + + + Unavailable + 不可用 + + + Remove %1 item(s) ? + 删除 %1 个项目? + + + Reset traffic of %1 item(s) ? + 重置 %1 个项目的流量? + + + Config copied + 配置已复制 + + + [%1] test error: %2 + [%1] 测试错误: %2 + + + Clear + 清除 + + + NekoRay + + + + fake + + + + Testing + 正在测试 + + + Http inbound is not enabled, can't set system proxy. + HTTP 入站未启用,无法设置系统代理。 + + + Update + 更新 + + + Document + 文档 + + + Select + 选择 + + + QR Code not found + 未扫描到 QR Code + + + Move + 移动 + + + Log + 日志 + + + Connection + 连接 + + + Status + 状态 + + + Outbound + 出站 + + + Destination + 目标地址 + + + End + 结束 + + + Active + 活动 + + + Start: %1 +End: %2 + 开始: %1 +结束: %2 + + + Starting profile %1 + 正在启动配置 %1 + + + Stopping profile %1 + 正在停止配置 %1 + + + Start with system + 跟随系统启动 + + + Remember last profile + 记住最后的配置 + + + Start minimal + 最小化启动 + + + Move %1 item(s) + 移动 %1 个项目 + + + Profile is insecure: %1 + 配置不安全: %1 + + + Remove Unavailable + 删除不可用 + + + Settings + 设置 + + + Input + 输入 + + + Please enter the items to be tested, separated by commas +1. Latency +2. Download speed +3. In and Out IP +4. NAT type + 请输入要测试的项目,用逗号分隔 +1. 延迟 +2. 下载速度 +3. 入口出口 IP +4. NAT 类型 + + + New profile + 手动输入配置 + + + Start [ Enter ] + 启动 [ Enter ] + + + Delete [ Delete ] + 删除 [ Delete ] + + + Hot key + 热键 + + + QR Code and link + 显示 QR Code 和分享链接 + + + Export V2ray config + 导出 V2ray 配置 + + + QR Code and link (Nekoray) + 显示 QR Code 和分享链接 (Nekoray) + + + Active Routing + 当前路由规则 + + + Active Server + 当前服务器 + + + Load routing and apply: %1 + 加载路由规则并应用: %1 + + + Copy links of selected + 复制选中的分享链接 + + + Copied %1 item(s) + 复制了%1 个项目 + + + New profile from clipboard + 从剪切板添加 + + + Full Test + 完整测试 + + + Current Group + 当前分组 + + + Reset Traffic + 重置流量 + + + Delete Repeat + 删除重复 + + + Select All + 全选 + + + VPN Mode + VPN 模式 + + + Failed to stop VPN process + 停止 VPN 失败 + + + Enable System Proxy + 启用系统代理 + + + Enable VPN + 启用 VPN + + + + ProxyItem + + Remove + 删除 + + + Confirmation + 确认 + + + Remove %1? + 删除 %1 ? + + + + QObject + + As Subscription + 作为订阅 + + + As link + 作为链接 + + + url detected + 检测到 URL + + + %1 +How to update? + %1 +如何处理? + + + Added %1 profiles: +%2 +Deleted %3 Profiles: +%4 + 增加了 %1 个配置: +%2 +删除了 %3 个配置: +%4 + + + Proxy: %1 +Direct: %2 + 代理: %1 +直连: %2 + + + Used: %1 Remain: %2 Expire: %3 + 已用 %1 剩余 %2 过期 %3 + + + Core not found: %1 + 找不到 %1 核心 + + + Update + 更新 + + + No update + 无更新 + + + Open in browser + 浏览器打开 + + + Close + 关闭 + + + Update is ready, restart to install? + 更新已下载好,重启应用? + + + Update found: %1 +Release note: +%2 + 检测到更新: %1 +更新日志: +%2 + + + Unavailable + 不可用 + + + Request with proxy but no profile started. + 即将使用代理请求,但是代理未启动。 + + + Chain Proxy + 链式代理 + + + The configuration (insecure) can be detected and identified, the transmission is fully visible to the censor and is not resistant to man-in-the-middle tampering with the content of the communication. + 该配置 (不安全) 能够被检测识别,传输的内容对审查者完全可见,并且无法抵抗中间人篡改通讯内容. + + + This configuration (Shadowsocks streaming cipher) can be accurately proactively detected and decrypted by censors without requiring a password, and cannot be mitigated by turning on IV replay filters on the server side. + +Learn more: https://github.com/net4people/bbs/issues/24 + 该配置 (Shadowsocks 流式密码) 可以被准确地主动探测、在不需要密码的情况下被审查者解密流量, 且服务端开启 IV 重放过滤器也无法缓解. + +了解更多: https://github.com/net4people/bbs/issues/24 + + + This configuration (VMess MD5 authentication) has been deprecated by upstream because of its questionable resistance to tampering and concealment. + +As of January 1, 2022, compatibility with MD5 authentication information will be disabled on the server side by default. Any client using MD5 authentication information will not be able to connect to a server with VMess MD5 authentication information disabled. + 该配置 (VMess MD5 认证) 抗篡改能力存疑, 隐蔽性存疑, 已被上游废弃. + +自 2022 年 1 月 1 日起, 服务器端将默认禁用对于 MD5 认证信息 的兼容. 任何使用 MD5 认证信息的客户端将无法连接到禁用 VMess MD5 认证信息的服务器端. + + + Requesting subscription: %1 + 正在请求订阅: %1 + + + Requesting subscription %1 error: %2 + 请求订阅 %1 错误: %2 + + + Nothing + + + + Change of %1: + %1 变化: + + + This profile is cleartext, don't use it if the server is not in your local network. + 该配置为明文传输,如果服务器不在本地局域网,请不要使用。 + + + Another program is running. + 另一个 Nekoray 实例正在运行。 + + + Select + 选择 + + + + Qv2ray::ui::widgets::AutoCompleteTextEdit + + You can not input space characters here. + + + + diff --git a/ui/GroupSort.hpp b/ui/GroupSort.hpp new file mode 100644 index 0000000..99e6e05 --- /dev/null +++ b/ui/GroupSort.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace NekoRay { + // implement in mainwindow + namespace GroupSortMethod { + enum GroupSortMethod { + Raw, + ByType, + ByAddress, + ByName, + ByLatency, + ById, + }; + } + + struct GroupSortAction { + GroupSortMethod::GroupSortMethod method = GroupSortMethod::Raw; + bool save_sort = false; //保存到文件 + bool descending = false; //默认升序,开这个就是降序 + }; +} diff --git a/ui/ThemeManager.cpp b/ui/ThemeManager.cpp new file mode 100644 index 0000000..025589d --- /dev/null +++ b/ui/ThemeManager.cpp @@ -0,0 +1,84 @@ +#include "ThemeManager.hpp" + +#include + +ThemeManager *themeManager = new ThemeManager; + +extern QString ReadFileText(const QString &path); + +void ThemeManager::ApplyTheme(const QString &theme) { + auto internal = [=] { + if (this->system_style_name.isEmpty()) { + this->system_style_name = qApp->style()->objectName(); + } + if (this->current_theme == theme) { + return; + } + + bool ok; + auto themeId = theme.toInt(&ok); + + if (ok) { + // System & Built-in + QString qss; + + if (themeId != 0) { + QString path; + std::map replace; + switch (themeId) { + case 1: + path = ":/themes/feiyangqingyun/qss/flatgray.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + case 2: + path = ":/themes/feiyangqingyun/qss/lightblue.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + case 3: + path = ":/themes/feiyangqingyun/qss/blacksoft.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + default: + return; + } + qss = ReadFileText(path); + for (auto const &[a, b]: replace) { + qss = qss.replace(a, b); + } + } + + auto system_style = QStyleFactory::create(this->system_style_name); + + if (themeId == 0) { + // system theme + qApp->setPalette(system_style->standardPalette()); + qApp->setStyle(system_style); + qApp->setStyleSheet(""); + } else { + if (themeId == 1 || themeId == 2 || themeId == 3) { + // feiyangqingyun theme + QString paletteColor = qss.mid(20, 7); + qApp->setPalette(QPalette(paletteColor)); + } else { + // other theme + qApp->setPalette(system_style->standardPalette()); + } + qApp->setStyleSheet(qss); + } + } else { + // QStyleFactory + const auto &_style = QStyleFactory::create(theme); + if (_style != nullptr) { + qApp->setPalette(_style->standardPalette()); + qApp->setStyle(_style); + qApp->setStyleSheet(""); + } + } + + current_theme = theme; + }; + internal(); + + auto nekoray_css = ReadFileText(":nekoray/nekoray.css"); + qApp->setStyleSheet(qApp->styleSheet().append("\n").append(nekoray_css)); +} diff --git a/ui/ThemeManager.hpp b/ui/ThemeManager.hpp new file mode 100644 index 0000000..d902bf6 --- /dev/null +++ b/ui/ThemeManager.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +class ThemeManager { +public: + QString system_style_name = ""; + QString current_theme = "0"; // int: 0:system 1+:builtin string: QStyleFactory + + void ApplyTheme(const QString &theme); +}; + +extern ThemeManager *themeManager; diff --git a/ui/dialog_basic_settings.cpp b/ui/dialog_basic_settings.cpp new file mode 100644 index 0000000..238213f --- /dev/null +++ b/ui/dialog_basic_settings.cpp @@ -0,0 +1,221 @@ +#include "dialog_basic_settings.h" +#include "ui_dialog_basic_settings.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "ui/ThemeManager.hpp" +#include "main/GuiUtils.hpp" +#include "main/NekoRay.hpp" + +#include +#include +#include + +class ExtraCoreWidget : public QWidget { +public: + QString coreName; + + QLabel *label_name; + MyLineEdit *lineEdit_path; + QPushButton *pushButton_pick; + + explicit ExtraCoreWidget(QJsonObject *extraCore, const QString &coreName_, + QWidget *parent = nullptr) + : QWidget(parent) { + coreName = coreName_; + label_name = new QLabel; + label_name->setText(coreName); + lineEdit_path = new MyLineEdit; + lineEdit_path->setText(extraCore->value(coreName).toString()); + pushButton_pick = new QPushButton; + pushButton_pick->setText(QObject::tr("Select")); + auto layout = new QHBoxLayout; + layout->addWidget(label_name); + layout->addWidget(lineEdit_path); + layout->addWidget(pushButton_pick); + setLayout(layout); + setContentsMargins(0, 0, 0, 0); + // + connect(pushButton_pick, &QPushButton::clicked, this, [=] { + auto fn = QFileDialog::getOpenFileName(this, QObject::tr("Select"), QDir::currentPath(), + "", nullptr, QFileDialog::Option::ReadOnly); + if (!fn.isEmpty()) { + lineEdit_path->setText(fn); + } + }); + connect(lineEdit_path, &QLineEdit::textChanged, this, [=](const QString &newTxt) { + extraCore->insert(coreName, newTxt); + }); + } +}; + +DialogBasicSettings::DialogBasicSettings(QWidget *parent) + : QDialog(parent), ui(new Ui::DialogBasicSettings) { + ui->setupUi(this); + + // Common + + ui->socks_ip->setText(NekoRay::dataStore->inbound_address); + ui->log_level->setCurrentText(NekoRay::dataStore->log_level); + ui->connection_statistics->setChecked(NekoRay::dataStore->connection_statistics); + CACHE.custom_inbound = NekoRay::dataStore->custom_inbound; + + if (NekoRay::dataStore->traffic_loop_interval == 500) { + ui->rfsh_r->setCurrentIndex(0); + } else if (NekoRay::dataStore->traffic_loop_interval == 1000) { + ui->rfsh_r->setCurrentIndex(1); + } else { + ui->rfsh_r->setCurrentIndex(2); + } + + D_LOAD_INT(inbound_socks_port) + D_LOAD_INT_ENABLE(inbound_http_port, http_enable) + D_LOAD_INT_ENABLE(mux_cool, mux_cool_enable) + D_LOAD_INT(test_concurrent) + D_LOAD_STRING(test_url) + + connect(ui->custom_inbound_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_inbound) + }); + + // Style + + ui->language->setCurrentIndex(NekoRay::dataStore->language); + connect(ui->language, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + CACHE.needRestart = true; + }); + + int built_in_len = ui->theme->count(); + ui->theme->addItems(QStyleFactory::keys()); + // + bool ok; + auto themeId = NekoRay::dataStore->theme.toInt(&ok); + if (ok) { + ui->theme->setCurrentIndex(themeId); + } else { + ui->theme->setCurrentText(NekoRay::dataStore->theme); + } + // + connect(ui->theme, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + if (index + 1 <= built_in_len) { + themeManager->ApplyTheme(Int2String(index)); + NekoRay::dataStore->theme = Int2String(index); + } else { + themeManager->ApplyTheme(ui->theme->currentText()); + NekoRay::dataStore->theme = ui->theme->currentText(); + } + repaint(); + mainwindow->repaint(); + NekoRay::dataStore->Save(); + }); + + // Subscription + + ui->user_agent->setText(NekoRay::dataStore->user_agent); + ui->sub_use_proxy->setChecked(NekoRay::dataStore->sub_use_proxy); + + // Core + + ui->core_v2ray_asset->setText(NekoRay::dataStore->v2ray_asset_dir); + // + CACHE.extraCore = QString2QJsonObject(NekoRay::dataStore->extraCore->core_map); + if (!CACHE.extraCore.contains("naive")) CACHE.extraCore.insert("naive", ""); + if (!CACHE.extraCore.contains("hysteria")) CACHE.extraCore.insert("hysteria", ""); + // + auto extra_core_layout = ui->extra_core_box->layout(); + for (const auto &s: CACHE.extraCore.keys()) { + extra_core_layout->addWidget(new ExtraCoreWidget(&CACHE.extraCore, s)); + } + + connect(ui->core_v2ray_asset, &QLineEdit::textChanged, this, [=] { + CACHE.needRestart = true; + }); + connect(ui->core_v2ray_asset_pick, &QPushButton::clicked, this, [=] { + auto fn = QFileDialog::getExistingDirectory(this, tr("Select"), QDir::currentPath(), + QFileDialog::Option::ShowDirsOnly | QFileDialog::Option::ReadOnly); + if (!fn.isEmpty()) { + ui->core_v2ray_asset->setText(fn); + } + }); + connect(ui->extra_core_add, &QPushButton::clicked, this, [=] { + bool ok; + auto s = QInputDialog::getText(nullptr, tr("Add"), + tr("Please input the core name."), + QLineEdit::Normal, "", &ok).trimmed(); + if (s.isEmpty() || !ok) return; + if (CACHE.extraCore.contains(s)) return; + extra_core_layout->addWidget(new ExtraCoreWidget(&CACHE.extraCore, s)); + CACHE.extraCore.insert(s, ""); + }); + connect(ui->extra_core_del, &QPushButton::clicked, this, [=] { + bool ok; + auto s = QInputDialog::getItem(nullptr, tr("Delete"), + tr("Please select the core name."), + CACHE.extraCore.keys(), 0, false, &ok); + if (s.isEmpty() || !ok) return; + for (int i = 0; i < extra_core_layout->count(); i++) { + auto item = extra_core_layout->itemAt(i); + auto ecw = dynamic_cast(item->widget()); + if (ecw != nullptr && ecw->coreName == s) { + ecw->deleteLater(); + CACHE.extraCore.remove(s); + return; + } + } + }); + + // Security + + ui->insecure_hint->setChecked(NekoRay::dataStore->insecure_hint); + ui->skip_cert->setChecked(NekoRay::dataStore->skip_cert); +} + +DialogBasicSettings::~DialogBasicSettings() { + delete ui; +} + +void DialogBasicSettings::accept() { + if (CACHE.needRestart) MessageBoxWarning(tr("Settings changed"), tr("Restart nekoray to take effect.")); + + // Common + + NekoRay::dataStore->inbound_address = ui->socks_ip->text(); + NekoRay::dataStore->log_level = ui->log_level->currentText(); + NekoRay::dataStore->connection_statistics = ui->connection_statistics->isChecked(); + NekoRay::dataStore->custom_inbound = CACHE.custom_inbound; + + if (ui->rfsh_r->currentIndex() == 0) { + NekoRay::dataStore->traffic_loop_interval = 500; + } else if (ui->rfsh_r->currentIndex() == 1) { + NekoRay::dataStore->traffic_loop_interval = 1000; + } else { + NekoRay::dataStore->traffic_loop_interval = 0; + } + + D_SAVE_INT(inbound_socks_port) + D_SAVE_INT_ENABLE(inbound_http_port, http_enable) + D_SAVE_INT_ENABLE(mux_cool, mux_cool_enable) + D_SAVE_INT(test_concurrent) + D_SAVE_STRING(test_url) + + // Style + + NekoRay::dataStore->language = ui->language->currentIndex(); + + // Subscription + + NekoRay::dataStore->user_agent = ui->user_agent->text(); + NekoRay::dataStore->sub_use_proxy = ui->sub_use_proxy->isChecked(); + + // Core + + NekoRay::dataStore->v2ray_asset_dir = ui->core_v2ray_asset->text(); + NekoRay::dataStore->extraCore->core_map = QJsonObject2QString(CACHE.extraCore, true); + + // Security + + NekoRay::dataStore->insecure_hint = ui->insecure_hint->isChecked(); + NekoRay::dataStore->skip_cert = ui->skip_cert->isChecked(); + + dialog_message(Dialog_DialogBasicSettings, "UpdateDataStore"); + QDialog::accept(); +} diff --git a/ui/dialog_basic_settings.h b/ui/dialog_basic_settings.h new file mode 100644 index 0000000..a6c69f1 --- /dev/null +++ b/ui/dialog_basic_settings.h @@ -0,0 +1,33 @@ +#ifndef DIALOG_BASIC_SETTINGS_H +#define DIALOG_BASIC_SETTINGS_H + +#include +#include + +namespace Ui { + class DialogBasicSettings; +} + +class DialogBasicSettings : public QDialog { +Q_OBJECT + +public: + explicit DialogBasicSettings(QWidget *parent = nullptr); + + ~DialogBasicSettings(); + +public slots: + + void accept(); + +private: + Ui::DialogBasicSettings *ui; + + struct { + QJsonObject extraCore; + QString custom_inbound; + bool needRestart = false; + } CACHE; +}; + +#endif // DIALOG_BASIC_SETTINGS_H diff --git a/ui/dialog_basic_settings.ui b/ui/dialog_basic_settings.ui new file mode 100644 index 0000000..660d015 --- /dev/null +++ b/ui/dialog_basic_settings.ui @@ -0,0 +1,593 @@ + + + DialogBasicSettings + + + + 0 + 0 + 500 + 400 + + + + + 0 + 0 + + + + Basic Settings + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + 0 + + + + Common + + + + + + + + + + + Listen Address + + + + + + + + + + + + + + + + Custom Inbound + + + + + + + Edit + + + + + + + + + + + + + + + + + Socks Listen Port + + + + + + + + + + + + + + + + HTTP Listen Port + + + + + + + + + + Enable + + + + + + + + + + + + + + + Test URL + + + + + + + + + + Concurrent + + + + + + + + + + + + + + + + + + Loglevel + + + + + + + + debug + + + + + info + + + + + warning + + + + + none + + + + + + + + + + + + + + mux.cool + + + + + + + Enable + + + + + + + concurrency + + + + + + + + + + + + + + + + + + + + Traffic statistics refresh rate + + + + + + + + Fast + + + + + Slow + + + + + Off + + + + + + + + + + + + + + Connection statistics + + + + + + + Enable + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Style + + + + + + + + + 0 + 0 + + + + Theme + + + + + + + + System + + + + + flatgray + + + + + lightblue + + + + + blacksoft + + + + + + + + + + + + + 0 + 0 + + + + Language + + + + + + + + System + + + + + English + + + + + 简体中文 + + + + + + + + + + + Subscription + + + + + + + + + User Agent + + + + + + + Use proxy when updating subscription + + + + + + + + Core + + + + + + V2Ray + + + + + + + + Asset Location + + + + + + + Default: dir of "nekoray-core" + + + + + + + Select + + + + + + + + + + + + Extra Core + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add + + + + + + + Delete + + + + + + + + + + + + + + Security + + + + + + Insecure hint + + + + + + + Skip TLS certificate authentication by default + + + + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + tabWidget + socks_ip + custom_inbound_edit + inbound_socks_port + inbound_http_port + http_enable + test_url + test_concurrent + log_level + mux_cool_enable + mux_cool + rfsh_r + connection_statistics + theme + language + user_agent + sub_use_proxy + core_v2ray_asset + core_v2ray_asset_pick + extra_core_add + extra_core_del + insecure_hint + skip_cert + + + + + buttonBox + accepted() + DialogBasicSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DialogBasicSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/ui/dialog_hotkey.cpp b/ui/dialog_hotkey.cpp new file mode 100644 index 0000000..ea3e31e --- /dev/null +++ b/ui/dialog_hotkey.cpp @@ -0,0 +1,24 @@ +#include "dialog_hotkey.h" +#include "ui_dialog_hotkey.h" + +#include "ui/mainwindow.h" + +DialogHotkey::DialogHotkey(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogHotkey) { + ui->setupUi(this); + ui->show_mainwindow->setKeySequence(NekoRay::dataStore->hotkey_mainwindow); + ui->show_groups->setKeySequence(NekoRay::dataStore->hotkey_group); + ui->show_routes->setKeySequence(NekoRay::dataStore->hotkey_route); + GetMainWindow()->RegisterHotkey(true); +} + +DialogHotkey::~DialogHotkey() { + if (result() == QDialog::Accepted) { + NekoRay::dataStore->hotkey_mainwindow = ui->show_mainwindow->keySequence().toString(); + NekoRay::dataStore->hotkey_group = ui->show_groups->keySequence().toString(); + NekoRay::dataStore->hotkey_route = ui->show_routes->keySequence().toString(); + NekoRay::dataStore->Save(); + } + GetMainWindow()->RegisterHotkey(false); + delete ui; +} diff --git a/ui/dialog_hotkey.h b/ui/dialog_hotkey.h new file mode 100644 index 0000000..6942a98 --- /dev/null +++ b/ui/dialog_hotkey.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "main/NekoRay.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogHotkey; } +QT_END_NAMESPACE + +class DialogHotkey : public QDialog { +Q_OBJECT + +public: + explicit DialogHotkey(QWidget *parent = nullptr); + + ~DialogHotkey() override; + +private: + Ui::DialogHotkey *ui; +}; diff --git a/ui/dialog_hotkey.ui b/ui/dialog_hotkey.ui new file mode 100644 index 0000000..ba055b0 --- /dev/null +++ b/ui/dialog_hotkey.ui @@ -0,0 +1,107 @@ + + + DialogHotkey + + + + 0 + 0 + 400 + 300 + + + + Hot key + + + + + + Trigger main window + + + + + + + + + + Qt::StrongFocus + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Show routes + + + + + + + + + + + + + Show groups + + + + + + + + QtExtKeySequenceEdit + QKeySequenceEdit +
3rdparty/QtExtKeySequenceEdit.h
+
+
+ + buttonBox + show_mainwindow + show_groups + show_routes + + + + + buttonBox + accepted() + DialogHotkey + accept() + + + 258 + 255 + + + 199 + 149 + + + + + buttonBox + rejected() + DialogHotkey + reject() + + + 258 + 255 + + + 199 + 149 + + + + +
diff --git a/ui/dialog_manage_groups.cpp b/ui/dialog_manage_groups.cpp new file mode 100644 index 0000000..d424be6 --- /dev/null +++ b/ui/dialog_manage_groups.cpp @@ -0,0 +1,97 @@ +#include "dialog_manage_groups.h" +#include "ui_dialog_manage_groups.h" + +#include "db/Database.hpp" +#include "sub/GroupUpdater.hpp" +#include "main/GuiUtils.hpp" +#include "ui/widget/GroupItem.h" +#include "ui/edit/dialog_edit_group.h" + +#include +#include +#include + +#define AddGroupToListIfExist(_id) \ +auto __ent = NekoRay::profileManager->GetGroup(_id); \ +if (__ent != nullptr) { \ +auto wI = new QListWidgetItem(); \ +auto w = new GroupItem(this, __ent, wI); \ +wI->setData(114514, _id); \ +ui->listWidget->addItem(wI); \ +ui->listWidget->setItemWidget(wI, w); \ +} + +DialogManageGroups::DialogManageGroups(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogManageGroups) { + ui->setupUi(this); + + for (auto id: NekoRay::profileManager->_groups) { + AddGroupToListIfExist(id) + } + + connect(ui->listWidget, &QListWidget::itemDoubleClicked, this, [=](QListWidgetItem *wI) { + auto w = dynamic_cast(ui->listWidget->itemWidget(wI)); + emit w->edit_clicked(); + }); +} + +DialogManageGroups::~DialogManageGroups() { + delete ui; +} + +void DialogManageGroups::on_add_clicked() { + auto ent = NekoRay::ProfileManager::NewGroup(); + auto dialog = new DialogEditGroup(ent, this); + int ret = dialog->exec(); + dialog->deleteLater(); + + if (ret == QDialog::Accepted) { + NekoRay::profileManager->AddGroup(ent); + AddGroupToListIfExist(ent->id) + dialog_message(Dialog_DialogManageGroups, "refresh-1"); + } +} + +void DialogManageGroups::on_update_all_clicked() { + if (QMessageBox::question(this, tr("Confirmation"), tr("Update all subscriptions?")) + == QMessageBox::StandardButton::Yes) { + for (const auto &gid: NekoRay::profileManager->_groups) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (group == nullptr || group->url.isEmpty()) continue; + _update_one_group(NekoRay::profileManager->_groups.indexOf(gid)); + return; + } + } +} + +void DialogManageGroups::_update_one_group(int _order) { + // calculate next group + int nextOrder = _order; + QSharedPointer nextGroup; + forever { + nextOrder += 1; + if (nextOrder >= NekoRay::profileManager->_groups.length()) break; + auto nextGid = NekoRay::profileManager->_groups[nextOrder]; + nextGroup = NekoRay::profileManager->GetGroup(nextGid); + if (nextGroup == nullptr || nextGroup->url.isEmpty()) continue; + break; + } + + // calculate this group + auto group = NekoRay::profileManager->GetGroup(NekoRay::profileManager->_groups[_order]); + if (group == nullptr) return; + + NekoRay::sub::groupUpdater->AsyncUpdate(group->url, group->id, this, [=] { + // refresh ui + for (int i = 0; i < ui->listWidget->count(); i++) { + auto w = ui->listWidget->itemWidget(ui->listWidget->item(i)); + if (w == nullptr) return; + auto item = dynamic_cast(w); + if (item->ent->id == group->id) { + item->refresh_data(); + } + } + // + if (nextGroup != nullptr) _update_one_group(nextOrder); + }); +} diff --git a/ui/dialog_manage_groups.h b/ui/dialog_manage_groups.h new file mode 100644 index 0000000..01baedb --- /dev/null +++ b/ui/dialog_manage_groups.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include "db/Group.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogManageGroups; } +QT_END_NAMESPACE + +class DialogManageGroups : public QDialog { +Q_OBJECT + +public: + explicit DialogManageGroups(QWidget *parent = nullptr); + + ~DialogManageGroups() override; + +private: + Ui::DialogManageGroups *ui; + + void _update_one_group(int _order); + +private slots: + + void on_add_clicked(); + + void on_update_all_clicked(); +}; diff --git a/ui/dialog_manage_groups.ui b/ui/dialog_manage_groups.ui new file mode 100644 index 0000000..dcba1ff --- /dev/null +++ b/ui/dialog_manage_groups.ui @@ -0,0 +1,55 @@ + + + DialogManageGroups + + + + 0 + 0 + 640 + 480 + + + + Qt::TabFocus + + + Groups + + + + + + Qt::NoFocus + + + + + + + + + Qt::NoFocus + + + New group + + + + + + + Qt::NoFocus + + + Update all subscriptions + + + + + + + + + + diff --git a/ui/dialog_manage_routes.cpp b/ui/dialog_manage_routes.cpp new file mode 100644 index 0000000..f53534f --- /dev/null +++ b/ui/dialog_manage_routes.cpp @@ -0,0 +1,207 @@ +#include "dialog_manage_routes.h" +#include "ui_dialog_manage_routes.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "main/GuiUtils.hpp" + +#include +#include +#include + +#define REFRESH_ACTIVE_ROUTING(a, r) \ +active_routing = a; \ +ui->active_routing->setText("[" + active_routing + "]"); \ +setWindowTitle(title_base + " [" + a + "]"); \ +SetRouteConfig(*r); + +#define SAVE_TO_ROUTING(r) \ +r->direct_ip = directIPTxt->toPlainText(); \ +r->direct_domain = directDomainTxt->toPlainText(); \ +r->proxy_ip = proxyIPTxt->toPlainText(); \ +r->proxy_domain = proxyDomainTxt->toPlainText(); \ +r->block_ip = blockIPTxt->toPlainText(); \ +r->block_domain = blockDomainTxt->toPlainText(); \ +r->custom = CACHE.custom_route; + +DialogManageRoutes::DialogManageRoutes(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogManageRoutes) { + ui->setupUi(this); + title_base = windowTitle(); + + ui->sniffing_mode->setCurrentIndex(NekoRay::dataStore->sniffing_mode); + ui->outbound_domain_strategy->setCurrentText(NekoRay::dataStore->outbound_domain_strategy); + ui->domainMatcherCombo->setCurrentIndex(NekoRay::dataStore->domain_matcher); + ui->domainStrategyCombo->setCurrentText(NekoRay::dataStore->domain_strategy); + ui->fake_dns->setChecked(NekoRay::dataStore->fake_dns); + ui->dns_routing->setChecked(NekoRay::dataStore->dns_routing); + ui->dns_remote->setText(NekoRay::dataStore->remote_dns); + ui->dns_direct->setText(NekoRay::dataStore->direct_dns); + ui->enhance_resolve_server_domain->setChecked(NekoRay::dataStore->enhance_resolve_server_domain); + D_C_LOAD_STRING(custom_route_global) + + connect(ui->custom_route_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_route) + }); + connect(ui->custom_route_global_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_route_global) + }); + + // + builtInSchemesMenu = new QMenu(this); + builtInSchemesMenu->addActions(this->getBuiltInSchemes()); + ui->preset->setMenu(builtInSchemesMenu); + + // + directDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + proxyDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + blockDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + // + directIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + proxyIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + blockIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + // + ui->directTxtLayout->addWidget(directDomainTxt, 0, 0); + ui->proxyTxtLayout->addWidget(proxyDomainTxt, 0, 0); + ui->blockTxtLayout->addWidget(blockDomainTxt, 0, 0); + // + ui->directIPLayout->addWidget(directIPTxt, 0, 0); + ui->proxyIPLayout->addWidget(proxyIPTxt, 0, 0); + ui->blockIPLayout->addWidget(blockIPTxt, 0, 0); + // + REFRESH_ACTIVE_ROUTING(NekoRay::dataStore->active_routing, NekoRay::dataStore->routing) +} + +DialogManageRoutes::~DialogManageRoutes() { + delete ui; +} + +void DialogManageRoutes::accept() { + NekoRay::dataStore->sniffing_mode = ui->sniffing_mode->currentIndex(); + NekoRay::dataStore->domain_matcher = ui->domainMatcherCombo->currentIndex(); + NekoRay::dataStore->domain_strategy = ui->domainStrategyCombo->currentText(); + NekoRay::dataStore->outbound_domain_strategy = ui->outbound_domain_strategy->currentText(); + NekoRay::dataStore->dns_routing = ui->dns_routing->isChecked(); + NekoRay::dataStore->fake_dns = ui->fake_dns->isChecked(); + NekoRay::dataStore->remote_dns = ui->dns_remote->text(); + NekoRay::dataStore->direct_dns = ui->dns_direct->text(); + NekoRay::dataStore->enhance_resolve_server_domain = ui->enhance_resolve_server_domain->isChecked(); + D_C_SAVE_STRING(custom_route_global) + + // + SAVE_TO_ROUTING(NekoRay::dataStore->routing) + NekoRay::dataStore->active_routing = active_routing; + NekoRay::dataStore->routing->fn = "routes/" + NekoRay::dataStore->active_routing; + NekoRay::dataStore->routing->Save(); + + dialog_message(Dialog_DialogManageRoutes, "UpdateDataStore"); + QDialog::accept(); +} + +// built in settings + +QList DialogManageRoutes::getBuiltInSchemes() { + QList list; + list.append(this->schemeToAction(tr("Bypass LAN and China"), routing_cn_lan)); + list.append(this->schemeToAction(tr("Global"), routing_global)); + return list; +} + +QAction *DialogManageRoutes::schemeToAction(const QString &name, const NekoRay::Routing &scheme) { + auto *action = new QAction(this); + action->setText(name); + connect(action, &QAction::triggered, [this, &scheme] { this->SetRouteConfig(scheme); }); + return action; +} + +void DialogManageRoutes::SetRouteConfig(const NekoRay::Routing &conf) { + // + directDomainTxt->setPlainText(conf.direct_domain); + proxyDomainTxt->setPlainText(conf.proxy_domain); + blockDomainTxt->setPlainText(conf.block_domain); + // + blockIPTxt->setPlainText(conf.block_ip); + directIPTxt->setPlainText(conf.direct_ip); + proxyIPTxt->setPlainText(conf.proxy_ip); + // + CACHE.custom_route = conf.custom; +} + +void DialogManageRoutes::on_load_save_clicked() { + auto w = new QDialog; + auto layout = new QVBoxLayout; + w->setLayout(layout); + auto lineEdit = new QLineEdit; + layout->addWidget(lineEdit); + auto list = new QListWidget; + layout->addWidget(list); + for (const auto &name: NekoRay::Routing::List()) { + list->addItem(name); + } + connect(list, &QListWidget::currentTextChanged, lineEdit, &QLineEdit::setText); + auto bottom = new QHBoxLayout; + layout->addLayout(bottom); + auto load = new QPushButton; + load->setText(tr("Load")); + bottom->addWidget(load); + auto save = new QPushButton; + save->setText(tr("Save")); + bottom->addWidget(save); + auto remove = new QPushButton; + remove->setText(tr("Remove")); + bottom->addWidget(remove); + auto cancel = new QPushButton; + cancel->setText(tr("Cancel")); + bottom->addWidget(cancel); + connect(load, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty()) { + auto r = std::make_unique(); + r->load_control_force = true; + r->fn = "routes/" + fn; + if (r->Load()) { + auto btn = QMessageBox::question(nullptr, + "NekoRay", tr("Load routing: %1").arg(fn) + "\n" + r->toString()); + if (btn == QMessageBox::Yes) { + REFRESH_ACTIVE_ROUTING(fn, r) + w->accept(); + } + } + } + }); + connect(save, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty()) { + auto r = std::make_unique(); + SAVE_TO_ROUTING(r) + r->fn = "routes/" + fn; + auto btn = QMessageBox::question(nullptr, "NekoRay", tr("Save routing: %1").arg(fn) + "\n" + r->toString()); + if (btn == QMessageBox::Yes) { + r->Save(); + REFRESH_ACTIVE_ROUTING(fn, r) + w->accept(); + } + } + }); + connect(remove, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty() && NekoRay::Routing::List().length() > 1) { + auto btn = QMessageBox::question(nullptr, "NekoRay", tr("Remove routing: %1").arg(fn)); + if (btn == QMessageBox::Yes) { + QFile f("routes/" + fn); + f.remove(); + if (NekoRay::dataStore->active_routing == fn) { + NekoRay::Routing::SetToActive(NekoRay::Routing::List().first()); + REFRESH_ACTIVE_ROUTING(NekoRay::dataStore->active_routing, NekoRay::dataStore->routing) + } + w->accept(); + } + } + }); + connect(cancel, &QPushButton::clicked, w, &QDialog::accept); + connect(list, &QListWidget::itemDoubleClicked, this, [=](QListWidgetItem *item) { + lineEdit->setText(item->text()); + emit load->clicked(); + }); + w->exec(); + w->deleteLater(); +} diff --git a/ui/dialog_manage_routes.h b/ui/dialog_manage_routes.h new file mode 100644 index 0000000..a07eb5e --- /dev/null +++ b/ui/dialog_manage_routes.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "qv2ray/ui/QvAutoCompleteTextEdit.hpp" +#include "main/NekoRay.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogManageRoutes; } +QT_END_NAMESPACE + +class DialogManageRoutes : public QDialog { +Q_OBJECT + +public: + explicit DialogManageRoutes(QWidget *parent = nullptr); + + ~DialogManageRoutes() override; + +private: + Ui::DialogManageRoutes *ui; + + struct { + QString custom_route; + QString custom_route_global; + } CACHE; + + + QMenu *builtInSchemesMenu; + Qv2ray::ui::widgets::AutoCompleteTextEdit *directDomainTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyDomainTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *blockDomainTxt; + // + Qv2ray::ui::widgets::AutoCompleteTextEdit *directIPTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *blockIPTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyIPTxt; + // + NekoRay::Routing routing_cn_lan = NekoRay::Routing(1); + NekoRay::Routing routing_global = NekoRay::Routing(0); + // + QString title_base; + QString active_routing; +public slots: + + void accept() override; + + QList getBuiltInSchemes(); + + QAction *schemeToAction(const QString &name, const NekoRay::Routing &scheme); + + void SetRouteConfig(const NekoRay::Routing &conf); + + void on_load_save_clicked(); +}; + + diff --git a/ui/dialog_manage_routes.ui b/ui/dialog_manage_routes.ui new file mode 100644 index 0000000..acc1acf --- /dev/null +++ b/ui/dialog_manage_routes.ui @@ -0,0 +1,392 @@ + + + DialogManageRoutes + + + + 0 + 0 + 800 + 600 + + + + Routes + + + + + + + + + + Outbound Domain Strategy + + + + + + + + Disable + + + + + The sniffing result is used for routing + + + + + The sniffing result is used for destination + + + + + + + + Direct DNS + + + + + + + Sniffing Mode + + + + + + + Remote DNS + + + + + + + false + + + + AsIs + + + + + UseIP + + + + + UseIPv4 + + + + + UseIPv6 + + + + + PreferIPv4 + + + + + PreferIPv6 + + + + + + + + + + + + + 使用多个境外 DNS 查询服务器地址,一定程度上可缓解对服务器域名的 DNS 污染,可能有副作用。 + + + 增强域名解析 + + + + + + + + + + + + + + FakeDNS + + + + + + + + + + + + + + + Enable DNS Routing + + + + + + + Qt::Vertical + + + + + + + Domain Strategy + + + + + + + false + + + + AsIs + + + + + IPIfNonMatch + + + + + IPOnDemand + + + + + + + + Matcher + + + + + + + + Original + + + + + Minimal Perfect Hash Matcher + + + + + + + + + + + + Block + + + Qt::AlignCenter + + + + + + + + + + Direct + + + Qt::AlignCenter + + + + + + + Domain + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + Proxy + + + Qt::AlignCenter + + + + + + + + + + + + + IP + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + + + + + + + Preset + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextBesideIcon + + + + + + + Custom + + + + + + + Custom (global) + + + + + + + Mange route set + + + + + + + + 0 + 0 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + sniffing_mode + outbound_domain_strategy + dns_remote + fake_dns + dns_direct + enhance_resolve_server_domain + dns_routing + domainStrategyCombo + domainMatcherCombo + preset + custom_route_edit + + + + + buttonBox + accepted() + DialogManageRoutes + accept() + + + 399 + 574 + + + 399 + 299 + + + + + buttonBox + rejected() + DialogManageRoutes + reject() + + + 399 + 574 + + + 399 + 299 + + + + + diff --git a/ui/edit/dialog_edit_group.cpp b/ui/edit/dialog_edit_group.cpp new file mode 100644 index 0000000..b74cc19 --- /dev/null +++ b/ui/edit/dialog_edit_group.cpp @@ -0,0 +1,66 @@ +#include "dialog_edit_group.h" +#include "ui_dialog_edit_group.h" + +#include "db/Database.hpp" + +#include + +DialogEditGroup::DialogEditGroup(const QSharedPointer &ent, QWidget *parent) : + QDialog(parent), ui(new Ui::DialogEditGroup) { + ui->setupUi(this); + + connect(ui->type, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + ui->cat_sub->setHidden(index == 0); + }); + + ui->name->setText(ent->name); + ui->archive->setChecked(ent->archive); + ui->url->setText(ent->url); + ui->type->setCurrentIndex(ent->url.isEmpty() ? 0 : 1); + ui->type->currentIndexChanged(ui->type->currentIndex()); + ui->copy_links->setVisible(false); + ui->copy_links_nkr->setVisible(false); + if (ent->id >= 0) { // already a group + ui->type->setDisabled(true); + if (!ent->Profiles().isEmpty()) { + ui->copy_links->setVisible(true); + ui->copy_links_nkr->setVisible(true); + } + } + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, [=] { + if (ent->id >= 0) { // already a group + if (!ent->url.isEmpty() && ui->url->text().isEmpty()) { + MessageBoxWarning(tr("Warning"), tr("Please input URL")); + return; + } + } + ent->name = ui->name->text(); + ent->url = ui->url->text(); + ent->archive = ui->archive->isChecked(); + QDialog::accept(); + }); + + connect(ui->copy_links, &QPushButton::clicked, this, [=] { + QStringList links; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (profile->gid != ent->id) continue; + links += profile->bean->ToShareLink(); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied")); + }); + connect(ui->copy_links_nkr, &QPushButton::clicked, this, [=] { + QStringList links; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (profile->gid != ent->id) continue; + links += profile->bean->ToNekorayShareLink(profile->type); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied")); + }); +} + +DialogEditGroup::~DialogEditGroup() { + delete ui; +} diff --git a/ui/edit/dialog_edit_group.h b/ui/edit/dialog_edit_group.h new file mode 100644 index 0000000..b7589bf --- /dev/null +++ b/ui/edit/dialog_edit_group.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "db/Group.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogEditGroup; } +QT_END_NAMESPACE + +class DialogEditGroup : public QDialog { +Q_OBJECT + +public: + explicit DialogEditGroup(const QSharedPointer &ent, QWidget *parent = nullptr); + + ~DialogEditGroup() override; + +private: + Ui::DialogEditGroup *ui; +}; + + diff --git a/ui/edit/dialog_edit_group.ui b/ui/edit/dialog_edit_group.ui new file mode 100644 index 0000000..8bd020a --- /dev/null +++ b/ui/edit/dialog_edit_group.ui @@ -0,0 +1,180 @@ + + + DialogEditGroup + + + + 0 + 0 + 400 + 300 + + + + Edit Group + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Type + + + + + + + + + + Name + + + + + + + + + + Basic + + + + + Subscription + + + + + + + + Archive + + + + + + + + + + + + + 0 + 0 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + URL + + + + + + + + + + + + + + + + + Copy profile share links + + + + + + + Copy profile share links (Nekoray) + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + + + buttonBox + rejected() + DialogEditGroup + reject() + + + 199 + 275 + + + 199 + 149 + + + + +
diff --git a/ui/edit/dialog_edit_profile.cpp b/ui/edit/dialog_edit_profile.cpp new file mode 100644 index 0000000..601b5fb --- /dev/null +++ b/ui/edit/dialog_edit_profile.cpp @@ -0,0 +1,298 @@ +#include "dialog_edit_profile.h" +#include "ui_dialog_edit_profile.h" + +#include "ui/edit/edit_socks_http.h" +#include "ui/edit/edit_shadowsocks.h" +#include "ui/edit/edit_chain.h" +#include "ui/edit/edit_vmess.h" +#include "ui/edit/edit_trojan_vless.h" +#include "ui/edit/edit_naive.h" +#include "ui/edit/edit_custom.h" + +#include "fmt/includes.h" +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "main/GuiUtils.hpp" + +#include + +#define ADJUST_SIZE runOnUiThread([=] { adjustSize(); adjustPosition(mainwindow); }, this); +#define LOAD_TYPE(a) ui->type->addItem(NekoRay::ProfileManager::NewProxyEntity(a)->bean->DisplayType(), a); + +DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, QWidget *parent) + : QDialog(parent), + ui(new Ui::DialogEditProfile) { + // setup UI + ui->setupUi(this); + ui->dialog_layout->setAlignment(ui->left, Qt::AlignTop); + ui->dialog_layout->setAlignment(ui->right_all, Qt::AlignTop); + + // network changed + network_title_base = ui->network_box->title(); + connect(ui->network, &QComboBox::currentTextChanged, this, [=](const QString &txt) { + if (txt == "tcp" || txt == "quic") { + ui->network_box->setVisible(false); + } else { + ui->network_box->setVisible(true); + ui->network_box->setTitle(network_title_base.arg(txt)); + if (txt == "grpc") { + ui->host->setVisible(false); + ui->host_l->setVisible(false); + } else { + ui->host->setVisible(true); + ui->host_l->setVisible(true); + } + } + ADJUST_SIZE + }); + ui->network->removeItem(0); + + // security changed + connect(ui->security, &QComboBox::currentTextChanged, this, [=](const QString &txt) { + if (txt == "tls") { + ui->security_box->setVisible(true); + } else { + ui->security_box->setVisible(false); + } + ADJUST_SIZE + }); + ui->security->removeItem(0); + + // 确定模式和 ent + newEnt = _type != ""; + if (newEnt) { + this->groupId = profileOrGroupId; + this->type = _type; + + // load type to combo box + LOAD_TYPE("socks") + LOAD_TYPE("http") + LOAD_TYPE("shadowsocks"); + LOAD_TYPE("trojan"); + LOAD_TYPE("vmess"); + LOAD_TYPE("vless"); + LOAD_TYPE("naive"); + ui->type->addItem("Hysteria", "hysteria"); + ui->type->addItem(tr("Custom"), "custom"); + LOAD_TYPE("chain"); + + // type changed + connect(ui->type, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + typeSelected(ui->type->itemData(index).toString()); + }); + } else { + this->ent = NekoRay::profileManager->GetProfile(profileOrGroupId); + if (this->ent == nullptr) return; + this->type = ent->type; + ui->type->setVisible(false); + ui->type_l->setVisible(false); + } + + typeSelected(this->type); +} + +DialogEditProfile::~DialogEditProfile() { + delete ui; +} + +void DialogEditProfile::typeSelected(const QString &newType) { + type = newType; + bool validType = true; + + if (type == "socks" || type == "http") { + auto _innerWidget = new EditSocksHttp(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "shadowsocks") { + auto _innerWidget = new EditShadowSocks(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "chain") { + auto _innerWidget = new EditChain(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "vmess") { + auto _innerWidget = new EditVMess(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "trojan" || type == "vless") { + auto _innerWidget = new EditTrojanVLESS(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "naive") { + auto _innerWidget = new EditNaive(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "custom" || type == "hysteria") { + auto _innerWidget = new EditCustom(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + // I don't want to write a settings + if (type == "hysteria") { + _innerWidget->preset_core = type; + _innerWidget->preset_command = "-c %config%"; + _innerWidget->preset_config = "{\n" + " \"server\": \"127.0.0.1:%mapping_port%\",\n" + " \"obfs\": \"fuck me till the daylight\",\n" + " \"up_mbps\": 10,\n" + " \"down_mbps\": 50,\n" + " \"server_name\": \"real.name.com\",\n" + " \"socks5\": {\n" + " \"listen\": \"127.0.0.1:%socks_port%\"\n" + " }\n" + "}"; + type = "custom"; + } + } else { + validType = false; + } + + if (!validType) { + MessageBoxWarning(newType, "Wrong type"); + return; + } + + if (newEnt) { + this->ent = NekoRay::ProfileManager::NewProxyEntity(type); + this->ent->gid = groupId; + } + + // hide some widget + auto notChain = type != "chain"; + ui->address->setVisible(notChain); + ui->address_l->setVisible(notChain); + ui->port->setVisible(notChain); + ui->port_l->setVisible(notChain); + + // 右边 Outbound: settings + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr) { + ui->right_all_w->setVisible(true); + ui->network->setCurrentText(stream->network); + ui->security->setCurrentText(stream->security); + ui->packet_encoding->setCurrentText(stream->packet_encoding); + ui->path->setText(stream->path); + ui->host->setText(stream->host); + ui->sni->setText(stream->sni); + ui->insecure->setChecked(stream->allow_insecure); + CACHE.certificate = stream->certificate; + } else { + ui->right_all_w->setVisible(false); + } + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + ui->custom_box->setVisible(true); + CACHE.custom = *((QString *) custom_item->ptr); + } else { + ui->custom_box->setVisible(false); + } + + // 左边 bean + auto old = ui->bean->layout()->itemAt(0)->widget(); + ui->bean->layout()->removeWidget(old); + innerWidget->layout()->setContentsMargins(0, 0, 0, 0); + ui->bean->layout()->addWidget(innerWidget); + ui->bean->setTitle(ent->bean->DisplayType()); + delete old; + + // 左边 bean inner editor + innerEditor->get_edit_dialog = [&]() { + return (QWidget *) this; + }; + innerEditor->editor_cache_updated = [=] { + editor_cache_updated_impl(); + }; + innerEditor->onStart(ent); + + // 左边 common + ui->name->setText(ent->bean->name); + ui->address->setText(ent->bean->serverAddress); + ui->port->setText(Int2String(ent->bean->serverPort)); + ui->port->setValidator(QRegExpValidator_Number, this)); + + editor_cache_updated_impl(); + ADJUST_SIZE + + // 第一次显示 + if (isHidden()) { + show(); + } +} + +void DialogEditProfile::accept() { + // 左边 + ent->bean->name = ui->name->text(); + ent->bean->serverAddress = ui->address->text(); + ent->bean->serverPort = ui->port->text().toInt(); + + // bean + if (!innerEditor->onEnd()) { + return; + } + + // 右边 + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr) { + stream->network = ui->network->currentText(); + stream->security = ui->security->currentText(); + stream->packet_encoding = ui->packet_encoding->currentText(); + stream->path = ui->path->text(); + stream->host = ui->host->text(); + stream->sni = ui->sni->text(); + stream->allow_insecure = ui->insecure->isChecked(); + stream->certificate = CACHE.certificate; + } + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + *((QString *) custom_item->ptr) = CACHE.custom; + } + + if (newEnt) { + auto ok = NekoRay::profileManager->AddProfile(ent); + if (!ok) { + MessageBoxWarning("???", "id exists"); + } + } else { + ent->Save(); + } + + dialog_message(Dialog_DialogEditProfile, "accept"); + QDialog::accept(); +} + +// cached editor (dialog) + +void DialogEditProfile::editor_cache_updated_impl() { + if (CACHE.certificate.isEmpty()) { + ui->certificate_edit->setText(tr("Not set")); + } else { + ui->certificate_edit->setText(tr("Already set")); + } + if (CACHE.custom.isEmpty()) { + ui->custom_edit->setText(tr("Not set")); + } else { + ui->custom_edit->setText(tr("Already set")); + } + + // CACHE macro + for (auto a: innerEditor->get_editor_cached()) { + if (a.second.isEmpty()) { + a.first->setText(tr("Not set")); + } else { + a.first->setText(tr("Already set")); + } + } +} + +void DialogEditProfile::on_custom_edit_clicked() { + C_EDIT_JSON_ALLOW_EMPTY(custom) + editor_cache_updated_impl(); +} + +void DialogEditProfile::on_certificate_edit_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.certificate, &ok); + if (ok) { + CACHE.certificate = txt; + editor_cache_updated_impl(); + } +} diff --git a/ui/edit/dialog_edit_profile.h b/ui/edit/dialog_edit_profile.h new file mode 100644 index 0000000..ee2a713 --- /dev/null +++ b/ui/edit/dialog_edit_profile.h @@ -0,0 +1,52 @@ +#ifndef DIALOG_EDIT_PROFILE_H +#define DIALOG_EDIT_PROFILE_H + +#include +#include "db/Database.hpp" +#include "profile_editor.h" + +namespace Ui { + class DialogEditProfile; +} + +class DialogEditProfile : public QDialog { +Q_OBJECT + +public: + explicit DialogEditProfile(const QString &_type, int profileOrGroupId, QWidget *parent = nullptr); + + ~DialogEditProfile() override; + +public slots: + + void accept() override; + +private slots: + + void on_custom_edit_clicked(); + + void on_certificate_edit_clicked(); + +private: + Ui::DialogEditProfile *ui; + QWidget *innerWidget{}; + ProfileEditor *innerEditor{}; + + QString type; + int groupId; + bool newEnt = false; + QSharedPointer ent; + + QString network_title_base; + + struct { + QString custom; + QString certificate; + } CACHE; + + void typeSelected(const QString &newType); + + void editor_cache_updated_impl(); +}; + +#endif // DIALOG_EDIT_PROFILE_H diff --git a/ui/edit/dialog_edit_profile.ui b/ui/edit/dialog_edit_profile.ui new file mode 100644 index 0000000..e801b96 --- /dev/null +++ b/ui/edit/dialog_edit_profile.ui @@ -0,0 +1,489 @@ + + + DialogEditProfile + + + + 0 + 0 + 1000 + 600 + + + + + 0 + 0 + + + + Edit + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Common + + + + + + + + + + + + + + + + + Type + + + + + + + Port + + + + + + + Address + + + + + + + Name + + + + + + + + + + + + + + + + 0 + 0 + + + + Bean + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + + + + 0 + 0 + + + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Settings + + + + + + + + + + + + + + none + + + + + tls + + + + + + + + 底层传输方式。必须与服务器一致,否则无法建立连接。 + + + Network + + + + + + + + 0 + 0 + + + + + + + + + + tcp + + + + + ws + + + + + h2 + + + + + grpc + + + + + quic + + + + + + + + 传输层安全。必须与服务器一致,否则无法建立连接。 + + + Security + + + + + + + 包编码,用于实现 UDP FullCone 等特性。需要服务器支持,选错无法连接。不懂请留空。 + + + Packet Encoding + + + + + + + + + + + + + packet + + + + + xudp + + + + + + + + + + + + + + 0 + 0 + + + + Network Settings (%1) + + + + + + http path (ws/http) 或 serviceName (gRPC) + + + Path + + + + + + + + + + http host + + + Host + + + + + + + + + + + + + + 0 + 0 + + + + Security Settings + + + + + + 开启后 V2Ray 不会检查远端主机所提供的 TLS 证书的有效性(安全性相当于明文) + + + Allow insecure + + + + + + + + + PushButton + + + + + + + 服务器名称指示,明文。 + + + SNI + + + + + + + 固定证书 + + + Certificate + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Custom Json Settings + + + + + + Edit + + + + + + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + type + name + address + port + network + security + path + host + insecure + sni + certificate_edit + custom_edit + + + + + buttonBox + accepted() + DialogEditProfile + accept() + + + 151 + 500 + + + 299 + 299 + + + + + buttonBox + rejected() + DialogEditProfile + reject() + + + 151 + 500 + + + 299 + 299 + + + + +
diff --git a/ui/edit/edit_chain.cpp b/ui/edit/edit_chain.cpp new file mode 100644 index 0000000..04c93cc --- /dev/null +++ b/ui/edit/edit_chain.cpp @@ -0,0 +1,56 @@ +#include "edit_chain.h" +#include "ui_edit_chain.h" + +#include "ui/mainwindow.h" +#include "ui/widget/ProxyItem.h" + +#include "db/Database.hpp" +#include "fmt/ChainBean.hpp" + +EditChain::EditChain(QWidget *parent) : QWidget(parent), ui(new Ui::EditChain) { + ui->setupUi(this); +} + +EditChain::~EditChain() { + delete ui; +} + +void EditChain::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->ChainBean(); + + for (auto id: bean->list) { + AddProfileToListIfExist(id); + } +} + +bool EditChain::onEnd() { + auto bean = this->ent->ChainBean(); + + QList idList; + for (int i = 0; i < ui->listWidget->count(); i++) { + idList << ui->listWidget->item(i)->data(114514).toInt(); + } + bean->list = idList; + + return true; +} + +void EditChain::on_select_profile_clicked() { + get_edit_dialog()->hide(); + GetMainWindow()->start_select_mode(this, [=](int id) { + get_edit_dialog()->show(); + AddProfileToListIfExist(id); + }); +} + +void EditChain::AddProfileToListIfExist(int id) { + auto _ent = NekoRay::profileManager->GetProfile(id); + if (_ent != nullptr && _ent->type != "chain") { + auto wI = new QListWidgetItem(); + auto w = new ProxyItem(this, _ent, wI); + wI->setData(114514, id); + ui->listWidget->addItem(wI); + ui->listWidget->setItemWidget(wI, w); + } +} diff --git a/ui/edit/edit_chain.h b/ui/edit/edit_chain.h new file mode 100644 index 0000000..0b93ad6 --- /dev/null +++ b/ui/edit/edit_chain.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "profile_editor.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class EditChain; } +QT_END_NAMESPACE + +class EditChain : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditChain(QWidget *parent = nullptr); + + ~EditChain() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditChain *ui; + QSharedPointer ent; + + void AddProfileToListIfExist(int id); + +private slots: + + void on_select_profile_clicked(); +}; diff --git a/ui/edit/edit_chain.ui b/ui/edit/edit_chain.ui new file mode 100644 index 0000000..49b9669 --- /dev/null +++ b/ui/edit/edit_chain.ui @@ -0,0 +1,57 @@ + + + EditChain + + + + 0 + 0 + 400 + 476 + + + + EditChain + + + + + + Traffic order is from top to bottom + + + + + + + + 0 + 400 + + + + Qt::ActionsContextMenu + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + QListView::Free + + + + + + + Select Profile + + + + + + + + diff --git a/ui/edit/edit_custom.cpp b/ui/edit/edit_custom.cpp new file mode 100644 index 0000000..63a7781 --- /dev/null +++ b/ui/edit/edit_custom.cpp @@ -0,0 +1,66 @@ +#include "edit_custom.h" +#include "ui_edit_custom.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "fmt/CustomBean.hpp" + +EditCustom::EditCustom(QWidget *parent) : + QWidget(parent), ui(new Ui::EditCustom) { + ui->setupUi(this); + ui->config_simple->setPlaceholderText("example:\n" + " server-address: \"127.0.0.1:%mapping_port%\"\n" + " listen-address: \"127.0.0.1\"\n" + " listen-port: %socks_port%\n" + " host: your-domain.com\n" + " sni: your-domain.com\n" + ); +} + +EditCustom::~EditCustom() { + delete ui; +} + +void EditCustom::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->CustomBean(); + + P_LOAD_COMBO(core) + ui->command->setText(bean->command.join(" ")); + P_LOAD_STRING(config_simple) + + // load known core + auto core_map = QString2QJsonObject(NekoRay::dataStore->extraCore->core_map); + for (const auto &key: core_map.keys()) { + ui->core->addItem(key); + } + + if (!preset_core.isEmpty()) { + bean->core = preset_core; + ui->core->setCurrentText(preset_core); + ui->core->setDisabled(true); + ui->command->setText(preset_command); + ui->config_simple->setText(preset_config); + } + if (!bean->core.isEmpty()) { + ui->core->setDisabled(true); + } + +} + +bool EditCustom::onEnd() { + auto bean = this->ent->CustomBean(); + + P_SAVE_COMBO(core) + bean->command = ui->command->text().split(" "); + P_SAVE_STRING_QTEXTEDIT(config_simple) + + return true; +} + +void EditCustom::on_as_json_clicked() { + auto editor = new JsonEditor(QString2QJsonObject(ui->config_simple->toPlainText()), this); + auto result = editor->OpenEditor(); + if (!result.isEmpty()) { + ui->config_simple->setText(QJsonObject2QString(result, false)); + } +} diff --git a/ui/edit/edit_custom.h b/ui/edit/edit_custom.h new file mode 100644 index 0000000..c7d7db1 --- /dev/null +++ b/ui/edit/edit_custom.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditCustom; } +QT_END_NAMESPACE + +class EditCustom : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + QString preset_core; + QString preset_command; + QString preset_config; + + explicit EditCustom(QWidget *parent = nullptr); + + ~EditCustom() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditCustom *ui; + QSharedPointer ent; + +private slots: + + void on_as_json_clicked(); +}; diff --git a/ui/edit/edit_custom.ui b/ui/edit/edit_custom.ui new file mode 100644 index 0000000..3abd7eb --- /dev/null +++ b/ui/edit/edit_custom.ui @@ -0,0 +1,86 @@ + + + EditCustom + + + + 0 + 0 + 400 + 394 + + + + EditCustom + + + + + + + + + 0 + 0 + + + + Core + + + + + + + true + + + + + + + Json + + + + + + + + + + + Command + + + + + + + %config% + + + + + + + + + + 0 + 300 + + + + + + + + core + as_json + command + config_simple + + + + diff --git a/ui/edit/edit_naive.cpp b/ui/edit/edit_naive.cpp new file mode 100644 index 0000000..80660d5 --- /dev/null +++ b/ui/edit/edit_naive.cpp @@ -0,0 +1,67 @@ +#include "edit_naive.h" +#include "ui_edit_naive.h" + +#include "fmt/NaiveBean.hpp" + +#include + +EditNaive::EditNaive(QWidget *parent) : + QWidget(parent), ui(new Ui::EditNaive) { + ui->setupUi(this); +} + +EditNaive::~EditNaive() { + delete ui; +} + +void EditNaive::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->NaiveBean(); + + P_LOAD_STRING(username); + P_LOAD_STRING(password); + P_LOAD_COMBO(protocol); + P_C_LOAD_STRING(extra_headers); + P_LOAD_STRING(sni); + P_C_LOAD_STRING(certificate); + P_LOAD_INT(insecure_concurrency); +} + +bool EditNaive::onEnd() { + auto bean = this->ent->NaiveBean(); + + P_SAVE_STRING(username); + P_SAVE_STRING(password); + P_SAVE_COMBO(protocol); + P_C_SAVE_STRING(extra_headers); + P_SAVE_STRING(sni); + P_C_SAVE_STRING(certificate); + P_SAVE_INT(insecure_concurrency); + + return true; +} + +QList> EditNaive::get_editor_cached() { + return { + {ui->certificate, CACHE.certificate}, + {ui->extra_headers, CACHE.extra_headers}, + }; +} + +void EditNaive::on_certificate_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.certificate, &ok); + if (ok) { + CACHE.certificate = txt; + editor_cache_updated(); + } +} + +void EditNaive::on_extra_headers_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Extra headers"), "", CACHE.extra_headers, &ok); + if (ok) { + CACHE.extra_headers = txt; + editor_cache_updated(); + } +} diff --git a/ui/edit/edit_naive.h b/ui/edit/edit_naive.h new file mode 100644 index 0000000..874cdcd --- /dev/null +++ b/ui/edit/edit_naive.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditNaive; } +QT_END_NAMESPACE + +class EditNaive : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditNaive(QWidget *parent = nullptr); + + ~EditNaive() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + + QList> get_editor_cached() override; + +private: + Ui::EditNaive *ui; + QSharedPointer ent; + + struct { + QString certificate; + QString extra_headers; + } CACHE; + +private slots: + + void on_certificate_clicked(); + + void on_extra_headers_clicked(); +}; diff --git a/ui/edit/edit_naive.ui b/ui/edit/edit_naive.ui new file mode 100644 index 0000000..6486a5f --- /dev/null +++ b/ui/edit/edit_naive.ui @@ -0,0 +1,126 @@ + + + EditNaive + + + + 0 + 0 + 400 + 300 + + + + EditNaive + + + + + + Protocol + + + + + + + Password + + + + + + + + + + + https + + + + + quic + + + + + + + + Extra headers + + + + + + + SNI + + + + + + + PushButton + + + + + + + Username + + + + + + + + + + + + + Certificate + + + + + + + Insecure concurrency + + + + + + + + + + PushButton + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + username + password + protocol + extra_headers + sni + certificate + insecure_concurrency + + + +
diff --git a/ui/edit/edit_shadowsocks.cpp b/ui/edit/edit_shadowsocks.cpp new file mode 100644 index 0000000..76a3846 --- /dev/null +++ b/ui/edit/edit_shadowsocks.cpp @@ -0,0 +1,39 @@ +#include "edit_shadowsocks.h" +#include "ui_edit_shadowsocks.h" + +#include "fmt/ShadowSocksBean.hpp" + +EditShadowSocks::EditShadowSocks(QWidget *parent) : QWidget(parent), + ui(new Ui::EditShadowSocks) { + ui->setupUi(this); +} + +EditShadowSocks::~EditShadowSocks() { + delete ui; +} + +void EditShadowSocks::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->ShadowSocksBean(); + + ui->method->setCurrentText(bean->method); + ui->password->setText(bean->password); + auto ssPlugin = bean->plugin.split(";"); + if (!ssPlugin.empty()) { + ui->plugin->setCurrentText(ssPlugin[0]); + ui->plugin_opts->setText(SubStrAfter(bean->plugin, ";")); + } +} + +bool EditShadowSocks::onEnd() { + auto bean = this->ent->ShadowSocksBean(); + + bean->method = ui->method->currentText(); + bean->password = ui->password->text(); + bean->plugin = ui->plugin->currentText(); + if (!bean->plugin.isEmpty()) { + bean->plugin += ";" + ui->plugin_opts->text(); + } + + return true; +} \ No newline at end of file diff --git a/ui/edit/edit_shadowsocks.h b/ui/edit/edit_shadowsocks.h new file mode 100644 index 0000000..08eb822 --- /dev/null +++ b/ui/edit/edit_shadowsocks.h @@ -0,0 +1,28 @@ +#ifndef EDIT_SHADOWSOCKS_H +#define EDIT_SHADOWSOCKS_H + +#include +#include "profile_editor.h" + +namespace Ui { + class EditShadowSocks; +} + +class EditShadowSocks : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditShadowSocks(QWidget *parent = nullptr); + + ~EditShadowSocks() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditShadowSocks *ui; + QSharedPointer ent; +}; + +#endif // EDIT_SHADOWSOCKS_H diff --git a/ui/edit/edit_shadowsocks.ui b/ui/edit/edit_shadowsocks.ui new file mode 100644 index 0000000..faa8d5c --- /dev/null +++ b/ui/edit/edit_shadowsocks.ui @@ -0,0 +1,192 @@ + + + EditShadowSocks + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + + + Plugin Args + + + + + + + + + + Password + + + + + + + Encryption + + + + + + + + + + + + + obfs-local + + + + + v2ray-plugin + + + + + ssr + + + + + + + + + + + true + + + + aes-128-gcm + + + + + aes-192-gcm + + + + + aes-256-gcm + + + + + chacha20-poly1305 + + + + + xchacha20-poly1305 + + + + + aes-128-ctr + + + + + aes-192-ctr + + + + + aes-256-ctr + + + + + aes-128-cfb + + + + + aes-192-cfb + + + + + aes-256-cfb + + + + + rc4 + + + + + rc4-md5 + + + + + salsa20 + + + + + chacha20 + + + + + chacha20-ietf + + + + + xchacha20 + + + + + none + + + + + + + + Plugin + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + method + password + plugin + plugin_opts + + + +
diff --git a/ui/edit/edit_socks_http.cpp b/ui/edit/edit_socks_http.cpp new file mode 100644 index 0000000..9e03254 --- /dev/null +++ b/ui/edit/edit_socks_http.cpp @@ -0,0 +1,48 @@ +#include "edit_socks_http.h" +#include "ui_edit_socks_http.h" + +#include "fmt/SocksHttpBean.hpp" + +EditSocksHttp::EditSocksHttp(QWidget *parent) : QWidget(parent), + ui(new Ui::EditSocksHttp) { + ui->setupUi(this); +} + +EditSocksHttp::~EditSocksHttp() { + delete ui; +} + +void EditSocksHttp::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->SocksHTTPBean(); + + if (bean->socks_http_type == NekoRay::fmt::SocksHttpBean::type_Socks4) { + ui->version->setCurrentIndex(1); + } else { + ui->version->setCurrentIndex(0); + } + if (bean->socks_http_type == NekoRay::fmt::SocksHttpBean::type_HTTP) { + ui->version->setVisible(false); + ui->version_l->setVisible(false); + } + + ui->username->setText(bean->username); + ui->password->setText(bean->password); +} + +bool EditSocksHttp::onEnd() { + auto bean = this->ent->SocksHTTPBean(); + + if (ui->version->isVisible()) { + if (ui->version->currentIndex() == 1) { + bean->socks_http_type = NekoRay::fmt::SocksHttpBean::type_Socks4; + } else { + bean->socks_http_type = NekoRay::fmt::SocksHttpBean::type_Socks5; + } + } + + bean->username = ui->username->text(); + bean->password = ui->password->text(); + + return true; +} diff --git a/ui/edit/edit_socks_http.h b/ui/edit/edit_socks_http.h new file mode 100644 index 0000000..7440e31 --- /dev/null +++ b/ui/edit/edit_socks_http.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "profile_editor.h" + +namespace Ui { + class EditSocksHttp; +} + +class EditSocksHttp : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditSocksHttp(QWidget *parent = nullptr); + + ~EditSocksHttp() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditSocksHttp *ui; + QSharedPointer ent; +}; diff --git a/ui/edit/edit_socks_http.ui b/ui/edit/edit_socks_http.ui new file mode 100644 index 0000000..060be49 --- /dev/null +++ b/ui/edit/edit_socks_http.ui @@ -0,0 +1,70 @@ + + + EditSocksHttp + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + + + Version + + + + + + + + + + + + + Username + + + + + + + + 5 + + + + + 4 + + + + + + + + Password + + + + + + + version + username + password + + + + diff --git a/ui/edit/edit_trojan_vless.cpp b/ui/edit/edit_trojan_vless.cpp new file mode 100644 index 0000000..bac023e --- /dev/null +++ b/ui/edit/edit_trojan_vless.cpp @@ -0,0 +1,26 @@ +#include "edit_trojan_vless.h" +#include "ui_edit_trojan_vless.h" + +#include "fmt/TrojanVLESSBean.hpp" + +EditTrojanVLESS::EditTrojanVLESS(QWidget *parent) : + QWidget(parent), ui(new Ui::EditTrojanVLESS) { + ui->setupUi(this); +} + +EditTrojanVLESS::~EditTrojanVLESS() { + delete ui; +} + +void EditTrojanVLESS::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->TrojanVLESSBean(); + if (bean->proxy_type == NekoRay::fmt::TrojanVLESSBean::proxy_VLESS) ui->label->setText("UUID"); + ui->password->setText(bean->password); +} + +bool EditTrojanVLESS::onEnd() { + auto bean = this->ent->TrojanVLESSBean(); + bean->password = ui->password->text(); + return true; +} diff --git a/ui/edit/edit_trojan_vless.h b/ui/edit/edit_trojan_vless.h new file mode 100644 index 0000000..486b276 --- /dev/null +++ b/ui/edit/edit_trojan_vless.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditTrojanVLESS; } +QT_END_NAMESPACE + +class EditTrojanVLESS : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditTrojanVLESS(QWidget *parent = nullptr); + + ~EditTrojanVLESS() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditTrojanVLESS *ui; + QSharedPointer ent; +}; + + diff --git a/ui/edit/edit_trojan_vless.ui b/ui/edit/edit_trojan_vless.ui new file mode 100644 index 0000000..89ec992 --- /dev/null +++ b/ui/edit/edit_trojan_vless.ui @@ -0,0 +1,38 @@ + + + EditTrojanVLESS + + + + 0 + 0 + 400 + 300 + + + + + + + + + + Password + + + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + +
diff --git a/ui/edit/edit_vmess.cpp b/ui/edit/edit_vmess.cpp new file mode 100644 index 0000000..1835b10 --- /dev/null +++ b/ui/edit/edit_vmess.cpp @@ -0,0 +1,32 @@ +#include "edit_vmess.h" +#include "ui_edit_vmess.h" + +#include "fmt/VMessBean.hpp" + +EditVMess::EditVMess(QWidget *parent) : + QWidget(parent), ui(new Ui::EditVMess) { + ui->setupUi(this); +} + +EditVMess::~EditVMess() { + delete ui; +} + +void EditVMess::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->VMessBean(); + + ui->uuid->setText(bean->uuid); + ui->aid->setText(Int2String(bean->aid)); + ui->security->setCurrentText(bean->security); +} + +bool EditVMess::onEnd() { + auto bean = this->ent->VMessBean(); + + bean->uuid = ui->uuid->text(); + bean->aid = ui->aid->text().toInt(); + bean->security = ui->security->currentText(); + + return true; +} diff --git a/ui/edit/edit_vmess.h b/ui/edit/edit_vmess.h new file mode 100644 index 0000000..9e0bb20 --- /dev/null +++ b/ui/edit/edit_vmess.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditVMess; } +QT_END_NAMESPACE + +class EditVMess : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditVMess(QWidget *parent = nullptr); + + ~EditVMess() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditVMess *ui; + QSharedPointer ent; +}; + + diff --git a/ui/edit/edit_vmess.ui b/ui/edit/edit_vmess.ui new file mode 100644 index 0000000..ea03ebe --- /dev/null +++ b/ui/edit/edit_vmess.ui @@ -0,0 +1,87 @@ + + + EditVMess + + + + 0 + 0 + 400 + 300 + + + + EditVMess + + + + + + Alter Id + + + + + + + + + + Security + + + + + + + + + + UUID + + + + + + + true + + + + auto + + + + + zero + + + + + none + + + + + chacha20-poly1305 + + + + + aes-128-gcm + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + +
diff --git a/ui/edit/profile_editor.h b/ui/edit/profile_editor.h new file mode 100644 index 0000000..a7cd5c7 --- /dev/null +++ b/ui/edit/profile_editor.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "db/ProxyEntity.hpp" +#include "main/GuiUtils.hpp" + +class ProfileEditor { +public: + virtual void onStart(QSharedPointer ent) = 0; + + virtual bool onEnd() = 0; + + std::function get_edit_dialog; + + // cached editor + + std::function editor_cache_updated; + + virtual QList> get_editor_cached() { return {}; }; +}; diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp new file mode 100644 index 0000000..a74be5e --- /dev/null +++ b/ui/mainwindow.cpp @@ -0,0 +1,1436 @@ +#include "./ui_mainwindow.h" +#include "mainwindow.h" + +#include "db/ProfileFilter.hpp" +#include "db/ConfigBuilder.hpp" +#include "sub/GroupUpdater.hpp" +#include "sys/ExternalProcess.hpp" +#include "sys/AutoRun.hpp" + +#include "ui/ThemeManager.hpp" +#include "ui/edit/dialog_edit_profile.h" +#include "ui/dialog_basic_settings.h" +#include "ui/dialog_manage_groups.h" +#include "ui/dialog_manage_routes.h" +#include "ui/dialog_hotkey.h" + +#include "3rdparty/qrcodegen.hpp" +#include "3rdparty/VT100Parser.hpp" +#include "qv2ray/ui/LogHighlighter.hpp" + +#ifndef NKR_NO_EXTERNAL + +#include "3rdparty/ZxingQtReader.hpp" +#include "qv2ray/components/proxy/QvProxyConfigurator.hpp" + +#endif + +#ifdef Q_OS_WIN + +#include "3rdparty/WinCommander.hpp" + +#else + +#include + +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), ui(new Ui::MainWindow) { + mainwindow = this; + dialog_message = [=](const QString &a, const QString &b) { + runOnUiThread([=] { + dialog_message_impl(a, b); + }); + }; + + // Load Manager + auto isLoaded = NekoRay::profileManager->Load(); + if (!isLoaded) { + auto defaultGroup = NekoRay::ProfileManager::NewGroup(); + defaultGroup->name = tr("Default"); + NekoRay::profileManager->AddGroup(defaultGroup); + } + + // Setup misc UI + themeManager->ApplyTheme(NekoRay::dataStore->theme); + ui->setupUi(this); + title_base = windowTitle(); + connect(ui->menu_start, &QAction::triggered, this, [=]() { neko_start(); }); + connect(ui->menu_stop, &QAction::triggered, this, [=]() { neko_stop(); }); + connect(ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, [=](int from, int to) { + // use tabData to track tab & gid + NekoRay::profileManager->_groups.clear(); + for (int i = 0; i < ui->tabWidget->tabBar()->count(); i++) { + NekoRay::profileManager->_groups += ui->tabWidget->tabBar()->tabData(i).toInt(); + } + NekoRay::profileManager->Save(); + }); + ui->label_running->installEventFilter(this); + ui->label_inbound->installEventFilter(this); + RegisterHotkey(false); + auto last_size = NekoRay::dataStore->mw_size.split("x"); + if (last_size.length() == 2) { + auto w = last_size[0].toInt(); + auto h = last_size[1].toInt(); + if (w > 0 && h > 0) { + resize(w, h); + } + } + + // top bar + ui->toolButton_program->setMenu(ui->menu_program); + ui->toolButton_preferences->setMenu(ui->menu_preferences); + ui->toolButton_server->setMenu(ui->menu_server); + ui->menubar->setVisible(false); + connect(ui->toolButton_document, &QToolButton::clicked, this, + [=] { QDesktopServices::openUrl(QUrl("https://matsuridayo.github.io/")); }); + connect(ui->toolButton_ads, &QToolButton::clicked, this, + [=] { QDesktopServices::openUrl(QUrl("https://matsuricom.github.io/")); }); + connect(ui->toolButton_update, &QToolButton::clicked, this, [=] { CheckUpdate(); }); + + // Setup log UI + new SyntaxHighlighter(false, qvLogDocument); + ui->masterLogBrowser->setDocument(qvLogDocument); + ui->masterLogBrowser->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + { + auto font = ui->masterLogBrowser->font(); + font.setPointSize(9); + ui->masterLogBrowser->setFont(font); + qvLogDocument->setDefaultFont(font); + } + connect(ui->masterLogBrowser->verticalScrollBar(), &QSlider::valueChanged, this, [=](int value) { + if (ui->masterLogBrowser->verticalScrollBar()->maximum() == value) + qvLogAutoScoll = true; + else + qvLogAutoScoll = false; + }); + connect(ui->masterLogBrowser, &QTextBrowser::textChanged, this, [=]() { + if (!qvLogAutoScoll) + return; + auto bar = ui->masterLogBrowser->verticalScrollBar(); + bar->setValue(bar->maximum()); + }); + showLog = [=](const QString &log) { + runOnUiThread([=] { + show_log_impl(log); + }); + }; + showLog_ext = [=](const QString &tag, const QString &log) { + runOnUiThread([=] { + show_log_impl("[" + tag + "] " + log); + }); + }; + showLog_ext_vt100 = [=](const QString &log) { + runOnUiThread([=] { + show_log_impl(cleanVT100String(log)); + }); + }; + + // table UI + ui->proxyListTable->callback_save_order = [=] { + auto group = NekoRay::profileManager->CurrentGroup(); + group->order = ui->proxyListTable->order; + group->Save(); + }; + connect(ui->proxyListTable->horizontalHeader(), &QHeaderView::sectionClicked, this, + [=](int logicalIndex) { + NekoRay::GroupSortAction action; + // 不正确的descending实现 + if (proxy_last_order == logicalIndex) { + action.descending = true; + proxy_last_order = -1; + } else { + proxy_last_order = logicalIndex; + } + action.save_sort = true; + // 表头 + if (logicalIndex == 1) { + action.method = NekoRay::GroupSortMethod::ByType; + } else if (logicalIndex == 2) { + action.method = NekoRay::GroupSortMethod::ByAddress; + } else if (logicalIndex == 3) { + action.method = NekoRay::GroupSortMethod::ByName; + } else if (logicalIndex == 4) { + action.method = NekoRay::GroupSortMethod::ByLatency; + } else if (logicalIndex == 0) { + action.method = NekoRay::GroupSortMethod::ById; + } else { + return; + } + refresh_proxy_list_impl(-1, action); + }); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + + // search box + ui->search->setVisible(false); + connect(shortcut_ctrl_f, &QShortcut::activated, this, [=] { + ui->search->setVisible(true); + ui->search->setFocus(); + }); + connect(shortcut_esc, &QShortcut::activated, this, [=] { + if (ui->search->isVisible()) { + ui->search->setText(""); + ui->search->textChanged(""); + ui->search->setVisible(false); + } + if (select_mode) { + emit profile_selected(-1); + select_mode = false; + refresh_status(); + } + }); + connect(ui->search, &QLineEdit::textChanged, this, [=](const QString &text) { + if (text.isEmpty()) { + for (int i = 0; i < ui->proxyListTable->rowCount(); i++) { + ui->proxyListTable->setRowHidden(i, false); + } + } else { + QList findItem = ui->proxyListTable->findItems(text, Qt::MatchContains); + for (int i = 0; i < ui->proxyListTable->rowCount(); i++) { + ui->proxyListTable->setRowHidden(i, true); + } + for (auto item: findItem) { + if (item != nullptr) ui->proxyListTable->setRowHidden(item->row(), false); + } + } + }); + + // refresh + this->refresh_groups(); + + // Setup Tray + auto icon = QIcon::fromTheme("nekoray"); + auto pixmap = QPixmap("../nekoray.png"); + if (!pixmap.isNull()) icon = QIcon(pixmap); + pixmap = QPixmap("./nekoray.png"); + if (!pixmap.isNull()) icon = QIcon(pixmap); + setWindowIcon(icon); + + tray = new QSystemTrayIcon(this);//初始化托盘对象tray + tray->setIcon(icon);//设定托盘图标,引号内是自定义的png图片路径 + tray->setContextMenu(ui->menu_program);//创建托盘菜单 + tray->show();//让托盘图标显示在系统托盘上 + connect(tray, &QSystemTrayIcon::activated, this, + [=](QSystemTrayIcon::ActivationReason reason) { + switch (reason) { + case QSystemTrayIcon::Trigger: + if (this->isVisible()) { + hide(); + } else { + this->showNormal(); + this->raise(); + this->activateWindow(); + } + break; + default: + break; + } + }); + + // + ui->menu_program_preference->addActions(ui->menu_preferences->actions()); + connect(ui->menu_add_from_clipboard2, &QAction::triggered, ui->menu_add_from_clipboard, &QAction::trigger); + connect(shortcut_ctrl_v, &QShortcut::activated, ui->menu_add_from_clipboard, &QAction::trigger); + // + connect(ui->menu_program, &QMenu::aboutToShow, this, [=]() { + ui->actionRemember_last_proxy->setChecked(NekoRay::dataStore->remember_enable); + ui->actionStart_with_system->setChecked(GetProcessAutoRunSelf()); + ui->actionStart_minimal->setChecked(NekoRay::dataStore->start_minimal); + // active server + for (const auto &old: ui->menuActive_Server->actions()) { + ui->menuActive_Server->removeAction(old); + old->deleteLater(); + } + for (const auto &pf: NekoRay::profileManager->CurrentGroup()->ProfilesWithOrder()) { + auto a = new QAction(pf->bean->DisplayTypeAndName()); + a->setProperty("id", pf->id); + a->setCheckable(true); + if (NekoRay::dataStore->started_id == pf->id) a->setChecked(true); + ui->menuActive_Server->addAction(a); + } + // active routing + for (const auto &old: ui->menuActive_Routing->actions()) { + ui->menuActive_Routing->removeAction(old); + old->deleteLater(); + } + for (const auto &name: NekoRay::Routing::List()) { + auto a = new QAction(name); + a->setCheckable(true); + a->setChecked(name == NekoRay::dataStore->active_routing); + ui->menuActive_Routing->addAction(a); + } + }); + connect(ui->menuActive_Server, &QMenu::triggered, this, [=](QAction *a) { + bool ok; + auto id = a->property("id").toInt(&ok); + if (!ok) return; + if (NekoRay::dataStore->started_id == id) { + neko_stop(); + } else { + neko_start(id); + } + }); + connect(ui->menuActive_Routing, &QMenu::triggered, this, [=](QAction *a) { + auto fn = a->text(); + if (!fn.isEmpty()) { + NekoRay::Routing r; + r.load_control_force = true; + r.fn = "routes/" + fn; + if (r.Load()) { + auto btn = QMessageBox::question(nullptr, "NekoRay", + tr("Load routing and apply: %1").arg(fn) + "\n" + r.toString()); + if (btn == QMessageBox::Yes) { + NekoRay::Routing::SetToActive(fn); + if (NekoRay::dataStore->started_id >= 0) { + neko_start(NekoRay::dataStore->started_id); + } else { + refresh_status(); + } + } + } + } + }); + connect(ui->actionRemember_last_proxy, &QAction::triggered, this, [=](bool checked) { + NekoRay::dataStore->remember_enable = checked; + NekoRay::dataStore->Save(); + }); + connect(ui->actionStart_with_system, &QAction::triggered, this, [=](bool checked) { + SetProcessAutoRunSelf(checked); + }); + connect(ui->actionStart_minimal, &QAction::triggered, this, [=](bool checked) { + NekoRay::dataStore->start_minimal = checked; + NekoRay::dataStore->Save(); + }); + // + connect(ui->checkBox_VPN, &QCheckBox::clicked, this, [=](bool checked) { + neko_set_spmode(checked ? NekoRay::SystemProxyMode::VPN : NekoRay::SystemProxyMode::DISABLE); + }); + connect(ui->menu_spmode, &QMenu::aboutToShow, this, [=]() { + ui->menu_spmode_disabled->setChecked(title_spmode == NekoRay::SystemProxyMode::DISABLE); + ui->menu_spmode_system_proxy->setChecked(title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY); + ui->menu_spmode_vpn->setChecked(title_spmode == NekoRay::SystemProxyMode::VPN); + }); + connect(ui->menu_spmode_system_proxy, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::SYSTEM_PROXY); }); + connect(ui->menu_spmode_vpn, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::VPN); }); + connect(ui->menu_spmode_disabled, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); }); + connect(ui->menu_qr, &QAction::triggered, this, [=]() { display_qr_link(false); }); + connect(ui->menu_qr_nkr, &QAction::triggered, this, [=]() { display_qr_link(true); }); + connect(ui->menu_tcp_ping, &QAction::triggered, this, [=]() { speedtest_current_group(0); }); + connect(ui->menu_url_test, &QAction::triggered, this, [=]() { speedtest_current_group(1); }); + connect(ui->menu_full_test, &QAction::triggered, this, [=]() { speedtest_current_group(2); }); + refresh_status(); + + // Start Core + NekoRay::dataStore->core_token = GetRandomString(32); + NekoRay::dataStore->core_port = MkPort(); + if (NekoRay::dataStore->core_port <= 0) NekoRay::dataStore->core_port = 19810; + + runOnNewThread([=]() { + QString starting_info; + + auto core_path = NekoRay::dataStore->core_path; +#ifdef Q_OS_WIN + if (!core_path.endsWith(".exe")) core_path += ".exe"; +#endif + if (!QFile(core_path).exists()) { + starting_info = "(not found)"; + core_path = ""; + } + + QStringList args; + args.push_back("nekoray"); + args.push_back("-port"); + args.push_back(Int2String(NekoRay::dataStore->core_port)); +#ifdef NKR_DEBUG + args.push_back("-debug"); +#endif + + for (int retry = 0; retry < 10; retry++) { + showLog("Starting nekoray core " + starting_info + "\n"); + core_process = new QProcess; + core_process_show_stderr = false; + connect(core_process, &QProcess::readyReadStandardOutput, this, + [&]() { + showLog(core_process->readAllStandardOutput().trimmed()); + }); + connect(core_process, &QProcess::readyReadStandardError, this, + [&]() { + auto log = core_process->readAllStandardError().trimmed(); + if (core_process_show_stderr) { + showLog(log); + return; + } + if (log.contains("token is set")) { + core_process_show_stderr = true; + } + }); + if (core_path.isEmpty()) break; + if (!NekoRay::dataStore->v2ray_asset_dir.isEmpty()) { + core_process->setEnvironment(QStringList{ + "V2RAY_LOCATION_ASSET=" + NekoRay::dataStore->v2ray_asset_dir + }); + } + core_process->start(core_path, args); + core_process->write((NekoRay::dataStore->core_token + "\n").toUtf8()); + core_process->waitForFinished(-1); + if (core_process_killed) return; + runOnUiThread([=] { neko_stop(true); }); + QThread::sleep(2); + core_process->deleteLater(); + } + }); + + setup_grpc(); + + // Start last + if (NekoRay::dataStore->remember_enable) { + if (NekoRay::dataStore->system_proxy_mode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { + neko_set_spmode(NekoRay::dataStore->system_proxy_mode, false); + } + if (NekoRay::dataStore->remember_id >= 0) { + runOnUiThread([=] { neko_start(NekoRay::dataStore->remember_id); }); + } + } + + if (!NekoRay::dataStore->start_minimal) show(); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (tray->isVisible()) { + hide(); //隐藏窗口 + event->ignore(); //忽略事件 + } +} + +MainWindow::~MainWindow() { + delete ui; +} + +// Group tab manage + +inline int tabIndex2GroupId(int index) { + if (NekoRay::profileManager->_groups.length() <= index) return -1; + return NekoRay::profileManager->_groups[index]; +} + +inline int groupId2TabIndex(int gid) { + for (int key = 0; key < NekoRay::profileManager->_groups.count(); key++) { + if (NekoRay::profileManager->_groups[key] == gid) return key; + } + return 0; +} + +void MainWindow::on_tabWidget_currentChanged(int index) { + if (NekoRay::dataStore->refreshing_group_list) return; + if (tabIndex2GroupId(index) == NekoRay::dataStore->current_group) return; + show_group(tabIndex2GroupId(index)); +} + +void MainWindow::show_group(int gid) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (group == nullptr) { + MessageBoxWarning(tr("Error"), QString("No such group: %1").arg(gid)); + return; + } + if (NekoRay::dataStore->current_group != gid) { + NekoRay::dataStore->current_group = gid; + NekoRay::dataStore->Save(); + } + ui->tabWidget->widget(groupId2TabIndex(gid))->layout()->addWidget(ui->proxyListTable); + refresh_proxy_list(); +} + +// callback + +void MainWindow::dialog_message_impl(const QString &sender, const QString &info) { + if (info.contains("UpdateDataStore")) { + auto changed = NekoRay::dataStore->Save(); + refresh_proxy_list(); + if (changed && NekoRay::dataStore->started_id >= 0 && + QMessageBox::question(this, tr("Confirmation"), tr("Settings changed, restart proxy?") + ) == QMessageBox::StandardButton::Yes) { + neko_start(NekoRay::dataStore->started_id); + } + refresh_status(); + } + if (sender == Dialog_DialogEditProfile) { + if (info == "accept") { + refresh_proxy_list(); + } + } else if (sender == Dialog_DialogManageGroups) { + if (info.startsWith("refresh")) { + this->refresh_groups(); + } + } else if (sender == "SubUpdater") { + // 订阅完毕 + refresh_proxy_list(); + if (!info.contains("dingyue")) { + QMessageBox::information(this, "NekoRay", + tr("Imported %1 profile(s)").arg(NekoRay::dataStore->imported_count)); + } + } else if (sender == "ExternalProcess") { + if (info == "Crashed") { + neko_stop(); + } + } +} + +// top bar & tray menu + +inline bool dialog_is_using = false; + +#define USE_DIALOG(a) if (dialog_is_using) return; \ +dialog_is_using = true; \ +auto dialog = new a(this); \ +dialog->exec(); \ +dialog->deleteLater(); \ +dialog_is_using = false; + +void MainWindow::on_menu_basic_settings_triggered() { + USE_DIALOG(DialogBasicSettings) +} + +void MainWindow::on_menu_manage_groups_triggered() { + USE_DIALOG(DialogManageGroups) +} + +void MainWindow::on_menu_routing_settings_triggered() { + USE_DIALOG(DialogManageRoutes) +} + +void MainWindow::on_menu_hotkey_settings_triggered() { + USE_DIALOG(DialogHotkey) +} + +void MainWindow::on_menu_exit_triggered() { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE, false); + if (title_spmode == NekoRay::SystemProxyMode::VPN) return; + RegisterHotkey(true); + + if (!isMaximized()) { + auto olds = NekoRay::dataStore->mw_size; + auto news = QString("%1x%2").arg(size().width()).arg(size().height()); + if (olds != news) { + NekoRay::dataStore->mw_size = news; + NekoRay::dataStore->Save(); + } + } + + auto last_id = NekoRay::dataStore->started_id; + neko_stop(); + if (NekoRay::dataStore->remember_enable && last_id >= 0) { + NekoRay::dataStore->UpdateStartedId(last_id); + } + + core_process_killed = true; + hide(); + ExitNekorayCore(); + + if (exit_update) { + QDir::setCurrent(QApplication::applicationDirPath()); + QProcess::startDetached("./updater", QStringList{}); + } + qApp->quit(); +} + +void MainWindow::neko_set_spmode(int mode, bool save) { + if (mode != title_spmode) { + // DISABLE + + if (title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { +#ifndef NKR_NO_EXTERNAL + ClearSystemProxy(); +#endif + } else if (title_spmode == NekoRay::SystemProxyMode::VPN) { + if (!StopVPNProcess()) { + refresh_status(); + return; + } + } + + // ENABLE + + if (mode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { +#ifndef NKR_NO_EXTERNAL +#ifdef Q_OS_WIN + if (mode == NekoRay::SystemProxyMode::SYSTEM_PROXY && + !InRange(NekoRay::dataStore->inbound_http_port, 0, 65535)) { + auto btn = QMessageBox::warning(this, "NekoRay", tr("Http inbound is not enabled, can't set system proxy."), + "OK", tr("Settings"), "", 0, 0); + if (btn == 1) { + on_menu_basic_settings_triggered(); + } + return; + } +#endif + SetSystemProxy("127.0.0.1", + NekoRay::dataStore->inbound_http_port, + NekoRay::dataStore->inbound_socks_port); +#endif + } else if (mode == NekoRay::SystemProxyMode::VPN) { + if (!StartVPNProcess()) { + refresh_status(); + return; + } + } + } + + if (save) { + NekoRay::dataStore->system_proxy_mode = mode; + NekoRay::dataStore->Save(); + } + + title_spmode = mode; + refresh_status(); +} + +void MainWindow::refresh_status(const QString &traffic_update) { + // From TrafficLooper + if (!traffic_update.isEmpty()) { + traffic_update_cache = traffic_update; + if (traffic_update == "STOP") { + traffic_update_cache = ""; + } else { + ui->label_speed->setText(traffic_update); + return; + } + } + ui->label_speed->setText(traffic_update_cache); + + // From UI + if (last_test_time.addSecs(2) < QTime::currentTime()) { + ui->label_running->setText(tr("Running: %1").arg(running.isNull() + ? tr("None") + : running->bean->DisplayName().left(50))); + } + // + auto display_http = tr("None"); + if (InRange(NekoRay::dataStore->inbound_http_port, 0, 65535)) { + display_http = DisplayAddress(NekoRay::dataStore->inbound_address, NekoRay::dataStore->inbound_http_port); + } + auto inbound_txt = QString("Socks: %1\nHTTP: %2").arg( + DisplayAddress(NekoRay::dataStore->inbound_address, NekoRay::dataStore->inbound_socks_port), + display_http + ); + ui->label_inbound->setText(inbound_txt); + // + ui->checkBox_VPN->setChecked(title_spmode == NekoRay::SystemProxyMode::VPN); + if (select_mode) ui->label_running->setText("[" + tr("Select") + "]"); + + auto make_title = [=](bool isTray) { + QStringList tt; + if (select_mode) tt << "[" + tr("Select") + "]"; + if (!title_error.isEmpty()) tt << "[" + title_error + "]"; + if (title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { + tt << "[" + tr("System Proxy") + "]"; + } else if (title_spmode == NekoRay::SystemProxyMode::VPN) { + tt << "[" + tr("VPN Mode") + "]"; + } + tt << title_base; + if (!isTray) tt << "(" + QString(NKR_VERSION) + ")"; + if (!NekoRay::dataStore->active_routing.isEmpty() && NekoRay::dataStore->active_routing != "Default") { + tt << "[" + NekoRay::dataStore->active_routing + "]"; + } + if (!running.isNull()) tt << running->bean->DisplayTypeAndName(); + return tt.join(isTray ? "\n" : " "); + }; + + setWindowTitle(make_title(false)); + if (tray != nullptr) tray->setToolTip(make_title(true)); +} + +// table显示 + +// refresh_groups -> show_group -> refresh_proxy_list +void MainWindow::refresh_groups() { + NekoRay::dataStore->refreshing_group_list = true; + + // refresh group? + for (int i = ui->tabWidget->count() - 1; i > 0; i--) { + ui->tabWidget->removeTab(i); + } + + int index = 0; + for (const auto &gid: NekoRay::profileManager->_groups) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (index == 0) { + ui->tabWidget->setTabText(0, group->name); + } else { + auto widget2 = new QWidget(); + auto layout2 = new QVBoxLayout(); + layout2->setContentsMargins(QMargins()); + layout2->setSpacing(0); + widget2->setLayout(layout2); + ui->tabWidget->addTab(widget2, group->name); + } + ui->tabWidget->tabBar()->setTabData(index, gid); + index++; + } + + // show after group changed + if (NekoRay::profileManager->CurrentGroup() == nullptr) { + NekoRay::dataStore->current_group = -1; + ui->tabWidget->setCurrentIndex(groupId2TabIndex(0)); + show_group(NekoRay::profileManager->_groups.count() > 0 ? NekoRay::profileManager->_groups.first() : 0); + } else { + ui->tabWidget->setCurrentIndex(groupId2TabIndex(NekoRay::dataStore->current_group)); + show_group(NekoRay::dataStore->current_group); + } + + NekoRay::dataStore->refreshing_group_list = false; +} + + +void MainWindow::refresh_proxy_list_impl(const int &id, NekoRay::GroupSortAction groupSortAction) { + if (id < 0) { + //这样才能清空数据 + ui->proxyListTable->setRowCount(0); + } + + // 绘制或更新item(s) + int row = -1; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + + row++; + if (id >= 0 && profile->id != id) continue; // update only one item + if (id < 0) { + ui->proxyListTable->insertRow(row); + } else { + // 排序过的,要找 + row = ui->proxyListTable->id2Row[id]; + } + + auto f0 = std::make_unique(); +// auto font = f0->font(); +// font.setPointSize(9); +// f0->setFont(font); + f0->setData(114514, profile->id); + + // C0: is Running + auto f = f0->clone(); + if (profile->id == NekoRay::dataStore->started_id) { + f->setText("✓"); + } else { + f->setText(" "); + } + ui->proxyListTable->setItem(row, 0, f); + + // C1: Type + f = f0->clone(); + f->setText(profile->bean->DisplayType()); + auto insecure_hint = DisplayInsecureHint(profile->bean); + if (!insecure_hint.isEmpty()) { + f->setBackground(Qt::red); + f->setToolTip(insecure_hint); + } + ui->proxyListTable->setItem(row, 1, f); + + // C2: Address+Port + f = f0->clone(); + f->setText(profile->bean->DisplayAddress()); + ui->proxyListTable->setItem(row, 2, f); + + // C3: Name + f = f0->clone(); + f->setText(profile->bean->name); + ui->proxyListTable->setItem(row, 3, f); + + // C4: Test Result + f = f0->clone(); + f->setText(profile->DisplayLatency()); + if (!profile->full_test_report.isEmpty()) f->setText(profile->full_test_report); + ui->proxyListTable->setItem(row, 4, f); + + // C5: Traffic + f = f0->clone(); + f->setText(profile->traffic_data->DisplayTraffic()); + ui->proxyListTable->setItem(row, 5, f); + } + + // 显示排序 + if (id < 0) { + switch (groupSortAction.method) { + case NekoRay::GroupSortMethod::Raw: { + auto group = NekoRay::profileManager->CurrentGroup(); + if (group == nullptr) return; + ui->proxyListTable->order = group->order; + break; + } + case NekoRay::GroupSortMethod::ById: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + if (groupSortAction.descending) { + return a > b; + } else { + return a < b; + } + }); + break; + } + case NekoRay::GroupSortMethod::ByAddress: + case NekoRay::GroupSortMethod::ByName: + case NekoRay::GroupSortMethod::ByType: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + QString ms_a; + QString ms_b; + if (groupSortAction.method == NekoRay::GroupSortMethod::ByType) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->DisplayType(); + ms_b = NekoRay::profileManager->GetProfile(b)->bean->DisplayType(); + } else if (groupSortAction.method == NekoRay::GroupSortMethod::ByName) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->name; + ms_b = NekoRay::profileManager->GetProfile(b)->bean->name; + } else if (groupSortAction.method == NekoRay::GroupSortMethod::ByAddress) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->DisplayAddress(); + ms_b = NekoRay::profileManager->GetProfile(b)->bean->DisplayAddress(); + } + if (groupSortAction.descending) { + return ms_a > ms_b; + } else { + return ms_a < ms_b; + } + }); + break; + } + case NekoRay::GroupSortMethod::ByLatency: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + auto ms_a = NekoRay::profileManager->GetProfile(a)->latency; + auto ms_b = NekoRay::profileManager->GetProfile(b)->latency; + if (ms_a <= 0) ms_a = 114514; + if (ms_b <= 0) ms_b = 114514; + if (groupSortAction.descending) { + return ms_a > ms_b; + } else { + return ms_a < ms_b; + } + }); + break; + } + } + ui->proxyListTable->update_order(groupSortAction.save_sort); + } +} + +// table菜单相关 + +void MainWindow::on_proxyListTable_itemDoubleClicked(QTableWidgetItem *item) { + auto id = item->data(114514).toInt(); + if (select_mode) { + emit profile_selected(id); + select_mode = false; + refresh_status(); + return; + } + auto dialog = new DialogEditProfile("", id, this); + connect(dialog, &QDialog::finished, dialog, &QDialog::deleteLater); +} + +void MainWindow::on_menu_add_from_input_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; + auto dialog = new DialogEditProfile("socks", NekoRay::dataStore->current_group, this); + connect(dialog, &QDialog::finished, dialog, &QDialog::deleteLater); +} + +void MainWindow::on_menu_add_from_clipboard_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; + auto clipboard = QApplication::clipboard()->text(); + NekoRay::sub::groupUpdater->AsyncUpdate(clipboard); +} + +void MainWindow::on_menu_move_triggered() { + auto ents = get_now_selected(); + if (ents.isEmpty()) return; + + auto items = QStringList{}; + for (auto &&group: NekoRay::profileManager->groups) { + items += Int2String(group->id) + " " + group->name; + } + + bool ok; + auto a = QInputDialog::getItem(nullptr, + tr("Move"), + tr("Move %1 item(s)").arg(ents.count()), + items, 0, false, &ok); + if (!ok) return; + auto gid = SubStrBefore(a, " ").toInt(); + for (const auto &ent: ents) { + NekoRay::profileManager->MoveProfile(ent, gid); + } + refresh_proxy_list(); +} + +void MainWindow::on_menu_delete_triggered() { + auto ents = get_now_selected(); + if (ents.count() == 0) return; + if (QMessageBox::question(this, tr("Confirmation"), QString(tr("Remove %1 item(s) ?")).arg(ents.count())) == + QMessageBox::StandardButton::Yes) { + for (const auto &ent: ents) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_reset_traffic_triggered() { + auto ents = get_now_selected(); + if (ents.count() == 0) return; + if (QMessageBox::question(this, + tr("Confirmation"), + QString(tr("Reset traffic of %1 item(s) ?")).arg(ents.count())) + == QMessageBox::StandardButton::Yes) { + for (const auto &ent: ents) { + ent->traffic_data->Reset(); + ent->Save(); + refresh_proxy_list(ent->id); + } + } +} + +void MainWindow::on_menu_profile_debug_info_triggered() { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + auto btn = QMessageBox::information(nullptr, "NekoRay", ents.first()->ToJsonBytes(), "OK", "Edit", "Reload", 0, 0); + if (btn == 1) { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QFileInfo(QString("profiles/%1.json").arg(ents.first()->id)).absoluteFilePath() + )); + } else if (btn == 2) { + ents.first()->Load(); + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_copy_links_triggered() { + auto ents = get_now_selected(); + QStringList links; + for (const auto &ent: ents) { + links += ent->bean->ToShareLink(); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied %1 item(s)").arg(links.length())); +} + +void MainWindow::on_menu_export_config_triggered() { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + auto ent = ents.first(); + auto result = NekoRay::BuildConfig(ent, false); + auto config_core = QJsonObject2QString(result->coreConfig, true); + QApplication::clipboard()->setText(config_core); + MessageBoxWarning(tr("Config copied"), config_core); +} + +void MainWindow::display_qr_link(bool nkrFormat) { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + + auto link = ents.first()->bean->ToShareLink(); + if (nkrFormat) { + link = ents.first()->bean->ToNekorayShareLink(ents.first()->type); + } + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(link.toUtf8().data(), + qrcodegen::QrCode::Ecc::MEDIUM); + qint32 sz = qr.getSize(); + QImage im(sz, sz, QImage::Format_RGB32); + QRgb black = qRgb(0, 0, 0); + QRgb white = qRgb(255, 255, 255); + for (int y = 0; y < sz; y++) + for (int x = 0; x < sz; x++) + im.setPixel(x, y, qr.getModule(x, y) ? black : white); + + class W : public QDialog { + public: + QLabel *l = nullptr; + QPlainTextEdit *l2 = nullptr; + QImage im; + + void set(QLabel *qLabel, QPlainTextEdit *pl, QImage qImage) { + this->l = qLabel; + this->l2 = pl; + this->im = std::move(qImage); + } + + void resizeEvent(QResizeEvent *resizeEvent) override { + auto size = resizeEvent->size(); + auto side = size.height() - 20 - l2->size().height(); + l->setPixmap(QPixmap::fromImage(im.scaled(side, side, Qt::KeepAspectRatio, Qt::FastTransformation), + Qt::MonoOnly)); + l->resize(side, side); + } + }; + + auto w = new W(); + auto l = new QLabel(w); + w->setLayout(new QVBoxLayout); + w->setMinimumSize(256, 256); + l->setMinimumSize(256, 256); + l->setMargin(6); + l->setAlignment(Qt::AlignmentFlag::AlignCenter); + l->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + l->setScaledContents(true); + w->layout()->addWidget(l); + auto l2 = new QPlainTextEdit(w); + l2->setPlainText(link); + l2->setReadOnly(true); + w->layout()->addWidget(l2); + w->set(l, l2, im); + w->setWindowTitle(ents.first()->bean->DisplayTypeAndName()); + QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + sizePolicy.setHeightForWidth(true); + w->setSizePolicy(sizePolicy); + w->exec(); + w->deleteLater(); +} + +void MainWindow::on_menu_scan_qr_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; +#ifndef NKR_NO_EXTERNAL + using namespace ZXingQt; + + hide(); + QThread::sleep(1); + + auto screen = QGuiApplication::primaryScreen(); + auto geom = screen->geometry(); + auto qpx = screen->grabWindow(0, geom.x(), geom.y(), geom.width(), geom.height()); + + show(); + + auto hints = DecodeHints() + .setFormats(BarcodeFormat::QRCode) + .setTryRotate(false) + .setBinarizer(Binarizer::FixedThreshold); + + auto result = ReadBarcode(qpx.toImage(), hints); + const auto &text = result.text(); + if (text.isEmpty()) { + MessageBoxInfo("NekoRay", tr("QR Code not found")); + } else { + NekoRay::sub::groupUpdater->AsyncUpdate(text); + } +#endif +} + +void MainWindow::on_menu_clear_test_result_triggered() { + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + profile->latency = 0; + profile->full_test_report = ""; + } + refresh_proxy_list(); +} + +void MainWindow::on_menu_select_all_triggered() { + ui->proxyListTable->selectAll(); +} + +void MainWindow::on_menu_delete_repeat_triggered() { + QList> out; + QList> out_del; + + NekoRay::ProfileFilter::Uniq(NekoRay::profileManager->CurrentGroup()->Profiles(), out, true, false); + NekoRay::ProfileFilter::OnlyInSrc_ByPointer(NekoRay::profileManager->CurrentGroup()->Profiles(), out, out_del); + + QString remove_display; + for (const auto &ent: out_del) { + remove_display += ent->bean->DisplayTypeAndName() + "\n"; + } + if (out_del.length() > 0 && + QMessageBox::question(this, + tr("Confirmation"), + tr("Remove %1 item(s) ?").arg(out_del.length()) + "\n" + remove_display + ) == QMessageBox::StandardButton::Yes) { + + for (const auto &ent: out_del) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_remove_unavailable_triggered() { + QList> out_del; + + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + if (profile->latency < 0) out_del += profile; + } + + QString remove_display; + for (const auto &ent: out_del) { + remove_display += ent->bean->DisplayTypeAndName() + "\n"; + } + if (out_del.length() > 0 && + QMessageBox::question(this, + tr("Confirmation"), + tr("Remove %1 item(s) ?").arg(out_del.length()) + "\n" + remove_display + ) == QMessageBox::StandardButton::Yes) { + + for (const auto &ent: out_del) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_proxyListTable_customContextMenuRequested(const QPoint &pos) { + ui->menu_server->popup(ui->proxyListTable->viewport()->mapToGlobal(pos)); //弹出菜单 +} + +QMap> MainWindow::get_now_selected() { + auto items = ui->proxyListTable->selectedItems(); + QMap> map; + for (auto item: items) { + auto id = item->data(114514).toInt(); + auto ent = NekoRay::profileManager->GetProfile(id); + if (ent != nullptr) map[id] = ent; + } + return map; +} + +void MainWindow::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_Escape: + // take over by shortcut_esc + break; + case Qt::Key_Return: { + neko_start(); + break; + } + case Qt::Key_Delete: { + on_menu_delete_triggered(); + break; + } + default: + QMainWindow::keyPressEvent(event); + } +} + +// Log + +inline void FastAppendTextDocument(const QString &message, QTextDocument *doc) { + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); + cursor.insertBlock(); + cursor.insertText(message); + cursor.endEditBlock(); +} + +void MainWindow::show_log_impl(const QString &log) { + if (log.trimmed().isEmpty()) return; + + FastAppendTextDocument(log, qvLogDocument); + // qvLogDocument->setPlainText(qvLogDocument->toPlainText() + log); + // From https://gist.github.com/jemyzhang/7130092 + auto maxLines = 200; + auto block = qvLogDocument->begin(); + + while (block.isValid()) { + if (qvLogDocument->blockCount() > maxLines) { + QTextCursor cursor(block); + block = block.next(); + cursor.select(QTextCursor::BlockUnderCursor); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + continue; + } + break; + } +} + +void MainWindow::on_masterLogBrowser_customContextMenuRequested(const QPoint &pos) { + QMenu *menu = ui->masterLogBrowser->createStandardContextMenu(); + + auto sep = new QAction; + sep->setSeparator(true); + menu->addAction(sep); + + auto action_clear = new QAction; + action_clear->setText(tr("Clear")); + action_clear->setData(-1); + connect(action_clear, &QAction::triggered, this, [=] { + qvLogDocument->clear(); + }); + menu->addAction(action_clear); + + menu->exec(ui->masterLogBrowser->viewport()->mapToGlobal(pos)); //弹出菜单 +} + +// eventFilter + +bool MainWindow::eventFilter(QObject *obj, QEvent *event) { + if (event->type() == QEvent::MouseButtonPress) { + auto mouseEvent = dynamic_cast(event); + + if (obj == ui->label_running && mouseEvent->button() == Qt::LeftButton && running != nullptr) { + test_current(); + return true; + } else if (obj == ui->label_inbound && mouseEvent->button() == Qt::LeftButton) { + on_menu_basic_settings_triggered(); + return true; + } + } + return QMainWindow::eventFilter(obj, event); +} + +// profile selector + +void MainWindow::start_select_mode(QObject *context, const std::function &callback) { + select_mode = true; + connectOnce(this, &MainWindow::profile_selected, context, callback); + refresh_status(); +} + +// 连接列表 + +inline QJsonArray last_arr; + +void MainWindow::refresh_connection_list(const QJsonArray &arr) { + if (last_arr == arr) { + return; + } + last_arr = arr; + + ui->tableWidget_conn->setRowCount(0); + + int row = -1; + for (const auto &_item: arr) { + auto item = _item.toObject(); + + row++; + ui->tableWidget_conn->insertRow(row); + + // C0: Status + auto *f = new QTableWidgetItem(""); + f->setData(114514, item["ID"].toInt()); + auto start_t = item["Start"].toInt(); + auto end_t = item["End"].toInt(); + if (end_t > 0) { + f->setText(tr("End")); + } else { + f->setText(tr("Active")); + } + f->setToolTip(tr("Start: %1\nEnd: %2").arg( + DisplayTime(start_t), + end_t > 0 ? DisplayTime(end_t) : "" + )); + ui->tableWidget_conn->setItem(row, 0, f); + + // C1: Outbound + f = f->clone(); + f->setToolTip(""); + f->setText(item["Tag"].toString()); + ui->tableWidget_conn->setItem(row, 1, f); + + // C2: Destination + f = f->clone(); + QString target1 = item["Dest"].toString(); + QString target2 = item["RDest"].toString(); + if (target2.isEmpty() || target1 == target2) { + target2 = ""; + } + f->setText("[" + target1 + "] " + target2); + ui->tableWidget_conn->setItem(row, 2, f); + } +} + + +// Hotkey + +#ifndef NKR_NO_EXTERNAL + +#include + +inline QList> RegisteredHotkey; + +void MainWindow::RegisterHotkey(bool unregister) { + while (!RegisteredHotkey.isEmpty()) { + auto hk = RegisteredHotkey.takeFirst(); + hk->deleteLater(); + } + if (unregister) return; + + QStringList regstr; + regstr += NekoRay::dataStore->hotkey_mainwindow; + regstr += NekoRay::dataStore->hotkey_group; + regstr += NekoRay::dataStore->hotkey_route; + + for (const auto &key: regstr) { + if (key.isEmpty()) continue; + if (regstr.count(key) > 1) return; // Conflict hotkey + } + for (const auto &key: regstr) { + QKeySequence k(key); + if (k.isEmpty()) continue; + auto hk = QSharedPointer(new QHotkey(k, true)); + if (hk->isRegistered()) { + RegisteredHotkey += hk; + connect(hk.get(), &QHotkey::activated, this, [=] { HotkeyEvent(key); }); + } else { + hk->deleteLater(); + } + } +} + +void MainWindow::HotkeyEvent(const QString &key) { + if (key.isEmpty()) return; + runOnUiThread([=] { + if (key == NekoRay::dataStore->hotkey_mainwindow) { + tray->activated(QSystemTrayIcon::ActivationReason::Trigger); + } else if (key == NekoRay::dataStore->hotkey_group) { + on_menu_manage_groups_triggered(); + } else if (key == NekoRay::dataStore->hotkey_route) { + on_menu_routing_settings_triggered(); + } + }); +} + +#else + +void MainWindow::RegisterHotkey(bool unregister) {} + +void MainWindow::HotkeyEvent(const QString &key) {} + +#endif + +// VPN Launcher + +//inline QString mshta_exec_uac = R"(vbscript:CreateObject("Shell.Application").ShellExecute("%1","%2","","runas",1)(window.close))"; + +bool MainWindow::StartVPNProcess() { + // + if (vpn_pid != 0) { + return true; + } +#ifdef Q_OS_WIN + auto configFn = ":/nekoray/vpn/sing-box-vpn.json"; + if (QFile::exists("vpn/sing-box-vpn.json")) configFn = "vpn/sing-box-vpn.json"; + auto config = ReadFileText(configFn).replace("%PORT%", Int2String(NekoRay::dataStore->inbound_socks_port)); +#else + auto protectPath = QDir::currentPath() + "/protect"; + auto configFn = ":/nekoray/vpn/vpn-run-root.sh"; + if (QFile::exists("vpn/vpn-run-root.sh")) configFn = "vpn/vpn-run-root.sh"; + auto config = ReadFileText(configFn) + .replace("$PORT", Int2String(NekoRay::dataStore->inbound_socks_port)) + .replace("$USE_NEKORAY", "1") + .replace("./nekoray_core", QApplication::applicationDirPath() + "/nekoray_core") + .replace("./tun2socks", QApplication::applicationDirPath() + "/tun2socks") + .replace("$PROTECT_LISTEN_PATH", protectPath) + .replace("$TUN_NAME", "nekoray-tun") + .replace("$USER_ID", Int2String((int) getuid())) + .replace("$TABLE_FWMARK", "514"); +#endif + // + QFile file; + file.setFileName(QFileInfo(configFn).fileName()); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + file.write(config.toUtf8()); + file.close(); + auto configPath = QFileInfo(file).absoluteFilePath(); + // +#ifdef Q_OS_WIN + runOnNewThread([=] { + vpn_pid = 1; //TODO get pid? + WinCommander::runProcessElevated(QApplication::applicationDirPath() + "/sing-box.exe", + {"run", "-c", configPath}); // blocking + vpn_pid = 0; + runOnUiThread([=] { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + }); + }); +#else + QFile::remove(protectPath); + if (QFile::exists(protectPath)) { + MessageBoxWarning("Error", "protect cannot be removed"); + return false; + } + // + auto vpn_process = new QProcess; + QProcess::connect(vpn_process, &QProcess::stateChanged, mainwindow, [=](QProcess::ProcessState state) { + if (state == QProcess::NotRunning) { + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + vpn_pid = 0; + vpn_process->deleteLater(); + GetMainWindow()->neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + } + }); + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + watcher = new QFileSystemWatcher({QDir::currentPath()}); + connect(watcher, &QFileSystemWatcher::directoryChanged, this, [=] { + if (!QFile::exists(protectPath)) return; + if (!Tun2rayStartStop(true)) { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + } + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + }); + // + vpn_process->setProcessChannelMode(QProcess::ForwardedChannels); + vpn_process->start("pkexec", {"bash", configPath}); + vpn_process->waitForStarted(); + vpn_pid = vpn_process->processId(); // actually it's pkexec or bash PID +#endif + return true; +} + +bool MainWindow::StopVPNProcess() { + if (vpn_pid != 0) { + bool ok; +#ifdef Q_OS_WIN + auto ret = WinCommander::runProcessElevated("taskkill", {"/IM", "sing-box.exe"}); + ok = ret == 0; +#else + QProcess p; + p.start("pkexec", {"pkill", "-2", "-P", Int2String(vpn_pid)}); + p.waitForFinished(); + ok = p.exitCode() == 0; +#endif + if (ok) { + vpn_pid = 0; + Tun2rayStartStop(false); + } else { + MessageBoxWarning(tr("Error"), tr("Failed to stop VPN process")); + } + return ok; + } + return true; +} diff --git a/ui/mainwindow.h b/ui/mainwindow.h new file mode 100644 index 0000000..f6c3a7d --- /dev/null +++ b/ui/mainwindow.h @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "GroupSort.hpp" + +#include "db/ProxyEntity.hpp" +#include "db/Group.hpp" +#include "main/GuiUtils.hpp" + +class QFileSystemWatcher; + +QT_BEGIN_NAMESPACE +namespace Ui { + class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow { +Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + + ~MainWindow(); + + void refresh_proxy_list(const int &id = -1) { refresh_proxy_list_impl(id, {}); }; + + void show_group(int gid); + + void refresh_groups(); + + void refresh_status(const QString &traffic_update = ""); + + void neko_start(int _id = -1); + + void neko_stop(bool crash = false); + + void neko_set_spmode(int mode, bool save = true); + + void show_log_impl(const QString &log); + + void start_select_mode(QObject *context, const std::function &callback); + + void refresh_connection_list(const QJsonArray &arr); + + void RegisterHotkey(bool unregister); + +signals: + + void profile_selected(int id); + +public slots: + + void on_menu_exit_triggered(); + +private slots: + + void on_masterLogBrowser_customContextMenuRequested(const QPoint &pos); + + void on_menu_basic_settings_triggered(); + + void on_menu_routing_settings_triggered(); + + void on_menu_hotkey_settings_triggered(); + + void on_menu_add_from_input_triggered(); + + void on_menu_add_from_clipboard_triggered(); + + void on_menu_move_triggered(); + + void on_menu_delete_triggered(); + + void on_menu_reset_traffic_triggered(); + + void on_menu_profile_debug_info_triggered(); + + void on_menu_copy_links_triggered(); + + void on_menu_export_config_triggered(); + + void display_qr_link(bool nkrFormat = false); + + void on_menu_scan_qr_triggered(); + + void on_menu_clear_test_result_triggered(); + + void on_menu_manage_groups_triggered(); + + void on_menu_select_all_triggered(); + + void on_menu_delete_repeat_triggered(); + + void on_menu_remove_unavailable_triggered(); + + void on_proxyListTable_itemDoubleClicked(QTableWidgetItem *item); + + void on_proxyListTable_customContextMenuRequested(const QPoint &pos); + + void on_tabWidget_currentChanged(int index); + +private: + Ui::MainWindow *ui; + QSystemTrayIcon *tray; + QShortcut *shortcut_ctrl_f = new QShortcut(QKeySequence("Ctrl+F"), this); + QShortcut *shortcut_ctrl_v = new QShortcut(QKeySequence("Ctrl+V"), this); + QShortcut *shortcut_esc = new QShortcut(QKeySequence("Esc"), this); + // + bool core_process_killed = false; + bool core_process_show_stderr = false; + QProcess *core_process = nullptr; + qint64 vpn_pid = 0; + QFileSystemWatcher *watcher = nullptr; + // + bool qvLogAutoScoll = true; + QTextDocument *qvLogDocument = new QTextDocument(this); + // + QString title_base; + QString title_error; + int title_spmode = NekoRay::SystemProxyMode::DISABLE; + QSharedPointer running; + QString traffic_update_cache; + QTime last_test_time; + // + int proxy_last_order = -1; + bool select_mode = false; + bool exit_update = false; + + QMap> get_now_selected(); + + void dialog_message_impl(const QString &sender, const QString &info); + + void refresh_proxy_list_impl(const int &id = -1, NekoRay::GroupSortAction groupSortAction = {}); + + void keyPressEvent(QKeyEvent *event) override; + + void closeEvent(QCloseEvent *event) override; + + // + + void HotkeyEvent(const QString &key); + + bool StartVPNProcess(); + + bool StopVPNProcess(); + + + // grpc and ... + + static void ExitNekorayCore(); + + void speedtest_current_group(int mode); + + void test_current(); + + void setup_grpc(); + + void CheckUpdate(); + + bool Tun2rayStartStop(bool start); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; +}; + +inline MainWindow *GetMainWindow() { + return (MainWindow *) mainwindow; +} diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui new file mode 100644 index 0000000..6cebde4 --- /dev/null +++ b/ui/mainwindow.ui @@ -0,0 +1,770 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + + 800 + 600 + + + + NekoRay + + + + + + + + + Program + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Preferences + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Server + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Ads + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Document + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Update + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + + 0 + 0 + + + + VPN Mode + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + + + + + + Qt::Vertical + + + + 0 + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + 16 + + + false + + + false + + + 26 + + + + + + + + + Type + + + + + Address + + + + + Name + + + + + Test Result + + + + + Traffic + + + + + + + + + + + + + 0 + + + + Log + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + false + + + + + + + + Connection + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + + Status + + + + + Outbound + + + + + Destination + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 0 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 800 + 32 + + + + + Program + + + + System Proxy + + + + + + + + Preferences + + + + + + Active Server + + + + + + Active Routing + + + + + + + + + + + + + + + + + + + Preferences + + + + + + + + + Server + + + + Share + + + + + + + + + + Current Group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exit + + + + + Basic Settings + + + + + New profile + + + + + Groups + + + + + Start [ Enter ] + + + + + Stop + + + + + Routes + + + + + New profile from clipboard + + + + + Delete [ Delete ] + + + + + Add profile from clipboard + + + + + Debug Info + + + + + QR Code and link + + + + + Copy Link + + + + + Tcp Ping + + + + + Url Test + + + + + Clear Test Result + + + + + Export V2ray config + + + + + Reset Traffic + + + + + Scan QR Code + + + + + true + + + Enable System Proxy + + + + + true + + + Disable + + + + + Delete Repeat + + + + + fake + + + false + + + + + Move + + + + + true + + + Start with system + + + + + true + + + Remember last profile + + + + + true + + + Start minimal + + + + + Remove Unavailable + + + + + Full Test + + + + + Hot key + + + + + Select All + + + + + QR Code and link (Nekoray) + + + + + fake + + + false + + + + + fake + + + false + + + + + Copy links of selected + + + + + true + + + Enable VPN + + + + + + MyTableWidget + QTableWidget +
ui/widget/MyTableWidget.h
+
+
+ + +
diff --git a/ui/mainwindow_grpc.cpp b/ui/mainwindow_grpc.cpp new file mode 100644 index 0000000..f57d3f1 --- /dev/null +++ b/ui/mainwindow_grpc.cpp @@ -0,0 +1,384 @@ +#include "./ui_mainwindow.h" +#include "mainwindow.h" + +#include "db/Database.hpp" +#include "db/ConfigBuilder.hpp" +#include "db/TrafficLooper.hpp" +#include "rpc/gRPC.h" + +#include +#include +#include +#include +#include +#include + +#ifndef NKR_NO_GRPC +using namespace NekoRay::rpc; +#endif + +void MainWindow::setup_grpc() { +#ifndef NKR_NO_GRPC + // Setup Connection + defaultClient = new Client([=](const QString &errStr) { + showLog("gRPC Error: " + errStr); + }, "127.0.0.1:" + Int2String(NekoRay::dataStore->core_port), NekoRay::dataStore->core_token); + auto t = new QTimer(); + connect(t, &QTimer::timeout, this, [=]() { + bool ok = defaultClient->KeepAlive(); + runOnUiThread([=]() { + if (!ok) { + title_error = tr("Error"); + } else { + title_error = ""; + } + refresh_status(); + }); + }); + auto tt = new QThread; + tt->start(); + t->moveToThread(tt); + runOnUiThread([=] { + t->start(2000); + }, t); + + // Looper + runOnNewThread([=] { NekoRay::traffic::trafficLooper->loop(); }); +#endif +} + +// 测速 + +inline bool speedtesting = false; + +void MainWindow::speedtest_current_group(int mode) { +#ifndef NKR_NO_GRPC + if (speedtesting) return; + + QStringList full_test_flags; + if (mode == libcore::FullTest) { + bool ok; + auto s = QInputDialog::getText(nullptr, tr("Input"), + tr("Please enter the items to be tested, separated by commas\n" + "1. Latency\n" + "2. Download speed\n" + "3. In and Out IP\n" + "4. NAT type"), + QLineEdit::Normal, "1,2,3,4", &ok); + full_test_flags = s.trimmed().split(","); + if (!ok) return; + } + + speedtesting = true; + + runOnNewThread([=]() { + auto group = NekoRay::profileManager->CurrentGroup(); + if (group->archive) return; + auto order = ui->proxyListTable->order;//copy + + QList> profiles; + QMutex lock; + QMutex lock2; + int threadN = mode == libcore::FullTest ? 1 : NekoRay::dataStore->test_concurrent; + int threadN_finished = 0; + + // 这个是按照显示的顺序 + for (auto id: order) { + auto profile = NekoRay::profileManager->GetProfile(id); + if (profile != nullptr) profiles += profile; + } + + // Threads + for (int i = 0; i < threadN; i++) { + runOnNewThread([&] { + forever { + // + lock.lock(); + if (profiles.isEmpty()) { + threadN_finished++; + if (threadN == threadN_finished) lock2.unlock(); + lock.unlock(); + return; + } + auto profile = profiles.takeFirst(); + lock.unlock(); + + // + libcore::TestReq req; + req.set_mode((libcore::TestMode) mode); + req.set_timeout(3000); + req.set_url(NekoRay::dataStore->test_url.toStdString()); + + // + QList ext; + + if (mode == libcore::TestMode::UrlTest || mode == libcore::FullTest) { + auto c = NekoRay::BuildConfig(profile, true); + // external test ??? + if (!c->ext.isEmpty()) { + ext = c->ext; + for (auto extC: ext) { + extC->Start(); + } + QThread::msleep(500); + } + // + auto config = new libcore::LoadConfigReq; + config->set_coreconfig(QJsonObject2QString(c->coreConfig, true).toStdString()); + req.set_allocated_config(config); + req.set_in_address(profile->bean->serverAddress.toStdString()); + + req.set_full_latency(full_test_flags.contains("1")); + req.set_full_speed(full_test_flags.contains("2")); + req.set_full_in_out(full_test_flags.contains("3")); + req.set_full_nat(full_test_flags.contains("4")); + } else if (mode == libcore::TcpPing) { + req.set_address(profile->bean->DisplayAddress().toStdString()); + } + + bool rpcOK; + auto result = defaultClient->Test(&rpcOK, req); + for (auto extC: ext) { + extC->Kill(); + extC->deleteLater(); + } + if (!rpcOK) return; + + profile->latency = result.ms(); + if (profile->latency == 0) profile->latency = -1; // sn + profile->full_test_report = result.full_report().c_str(); + + runOnUiThread([=] { + if (!result.error().empty()) { + show_log_impl( + tr("[%1] test error: %2").arg(profile->bean->DisplayTypeAndName(), + result.error().c_str())); + } + refresh_proxy_list(profile->id); + }); + } + }); + } + + // Control + lock2.lock(); + lock2.lock(); + lock2.unlock(); + speedtesting = false; + }); +#endif +} + +void MainWindow::test_current() { +#ifndef NKR_NO_GRPC + last_test_time = QTime::currentTime(); + ui->label_running->setText(tr("Testing")); + + runOnNewThread([=] { + libcore::TestReq req; + req.set_mode(libcore::UrlTest); + req.set_timeout(3000); + req.set_url(NekoRay::dataStore->test_url.toStdString()); + + bool rpcOK; + auto result = defaultClient->Test(&rpcOK, req); + if (!rpcOK) return; + + auto latency = result.ms(); + last_test_time = QTime::currentTime(); + + runOnUiThread([=] { + if (latency <= 0) { + ui->label_running->setText(tr("Test Result") + ": " + tr("Unavailable")); + } else if (latency > 0) { + ui->label_running->setText(tr("Test Result") + ": " + QString("%1 ms").arg(latency)); + } + }); + }); +#endif +} + +void MainWindow::ExitNekorayCore() { +#ifndef NKR_NO_GRPC + NekoRay::rpc::defaultClient->Exit(); +#endif +} + +void MainWindow::neko_start(int _id) { + auto ents = get_now_selected(); + auto ent = (_id < 0 && !ents.isEmpty()) ? ents.first() : NekoRay::profileManager->GetProfile(_id); + if (ent == nullptr) return; + + if (select_mode) { + emit profile_selected(ent->id); + select_mode = false; + refresh_status(); + return; + } + + if (NekoRay::profileManager->GetGroup(ent->gid)->archive) return; + + auto result = NekoRay::BuildConfig(ent, false); + if (!result->error.isEmpty()) { + MessageBoxWarning("BuildConfig return error", result->error); + return; + } + + if (NekoRay::dataStore->started_id >= 0) neko_stop(); + show_log_impl(">>>>>>>> " + tr("Starting profile %1").arg(ent->bean->DisplayTypeAndName())); + auto insecure_hint = DisplayInsecureHint(ent->bean); + if (!insecure_hint.isEmpty()) show_log_impl(">>>>>>>> " + tr("Profile is insecure: %1").arg(insecure_hint)); + +#ifndef NKR_NO_GRPC + libcore::LoadConfigReq req; + req.set_coreconfig(QJsonObject2QString(result->coreConfig, true).toStdString()); + req.set_trydomains(result->tryDomains.join(",").toStdString()); + // + bool rpcOK; + QString error = defaultClient->Start(&rpcOK, req); + if (rpcOK && !error.isEmpty()) { + MessageBoxWarning("LoadConfig return error", error); + return; + } + // + NekoRay::traffic::trafficLooper->proxy = result->outboundStat.get(); + NekoRay::traffic::trafficLooper->items = result->outboundStats; + NekoRay::traffic::trafficLooper->loop_enabled = true; +#endif + + for (auto extC: result->ext) { + extC->Start(); + } + + NekoRay::dataStore->UpdateStartedId(ent->id); + running = ent; + refresh_status(); + refresh_proxy_list(ent->id); +} + +void MainWindow::neko_stop(bool crash) { + auto id = NekoRay::dataStore->started_id; + if (id < 0) return; + show_log_impl(">>>>>>>> " + tr("Stopping profile %1").arg(running->bean->DisplayTypeAndName())); + + while (!NekoRay::sys::running_ext.isEmpty()) { + auto extC = NekoRay::sys::running_ext.takeFirst(); + extC->Kill(); + extC->deleteLater(); + } + +#ifndef NKR_NO_GRPC + NekoRay::traffic::trafficLooper->loop_enabled = false; + NekoRay::traffic::trafficLooper->loop_mutex.lock(); + if (NekoRay::dataStore->traffic_loop_interval != 0) { + NekoRay::traffic::trafficLooper->update_all(); + for (const auto &item: NekoRay::traffic::trafficLooper->items) { + NekoRay::profileManager->GetProfile(item->id)->Save(); + refresh_proxy_list(item->id); + } + } + NekoRay::traffic::trafficLooper->loop_mutex.unlock(); + + if (!crash) { + bool rpcOK; + QString error = defaultClient->Stop(&rpcOK); + if (rpcOK && !error.isEmpty()) { + MessageBoxWarning("Stop return error", error); + return; + } + } +#endif + + NekoRay::dataStore->UpdateStartedId(-1919); + running = nullptr; + refresh_status(); + refresh_proxy_list(id); +} + +void MainWindow::CheckUpdate() { +#ifndef NKR_NO_GRPC + bool ok; + libcore::UpdateReq request; + request.set_action(libcore::UpdateAction::Check); + auto response = NekoRay::rpc::defaultClient->Update(&ok, request); + if (!ok) return; + + auto err = response.error(); + if (!err.empty()) { + runOnUiThread([=] { + MessageBoxWarning(QObject::tr("Update"), err.c_str()); + }); + return; + } + + if (response.release_download_url() == nullptr) { + runOnUiThread([=] { + MessageBoxInfo(QObject::tr("Update"), QObject::tr("No update")); + }); + return; + } + + runOnUiThread([=] { + QMessageBox box(QMessageBox::Question, QObject::tr("Update"), + QObject::tr("Update found: %1\nRelease note:\n%2") + .arg(response.assets_name().c_str(), response.release_note().c_str())); + QAbstractButton *btn1; + if (!NekoRay::dataStore->flag_use_appdata) { + btn1 = box.addButton(QObject::tr("Update"), QMessageBox::AcceptRole); + } + QAbstractButton *btn2 = box.addButton(QObject::tr("Open in browser"), QMessageBox::AcceptRole); + box.addButton(QObject::tr("Close"), QMessageBox::RejectRole); + box.exec(); + + if (btn1 == box.clickedButton()) { + // Download Update + runOnNewThread([=] { + bool ok2; + libcore::UpdateReq request2; + request2.set_action(libcore::UpdateAction::Download); + auto response2 = NekoRay::rpc::defaultClient->Update(&ok2, request2); + runOnUiThread([=] { + if (response2.error().empty()) { + auto q = QMessageBox::question(nullptr, QObject::tr("Update"), + QObject::tr("Update is ready, restart to install?")); + if (q == QMessageBox::StandardButton::Yes) { + this->exit_update = true; + on_menu_exit_triggered(); + } + } else { + MessageBoxWarning(QObject::tr("Update"), response2.error().c_str()); + } + }); + }); + } else if (btn2 == box.clickedButton()) { + QDesktopServices::openUrl(QUrl(response.release_url().c_str())); + } + }); +#endif +} + +bool MainWindow::Tun2rayStartStop(bool start) { +#ifndef NKR_NO_GRPC + // For Linux only currently (check in go) + bool ok; + if (start) { + libcore::SetTunReq req; + req.set_name("nekoray-tun"); + req.set_mtu(1500); + req.set_implementation(0); + req.set_fakedns(NekoRay::dataStore->fake_dns); + auto error = defaultClient->SetTun(&ok, req); + if (!ok) return false; + if (!error.isEmpty()) { + MessageBoxWarning("Error", "Failed to start Tun2ray: " + error); + return false; + } + } else { + libcore::SetTunReq req; + req.set_implementation(-1); + defaultClient->SetTun(&ok, req); + if (!ok) return false; + } +#endif + return true; +} diff --git a/ui/widget/GroupItem.cpp b/ui/widget/GroupItem.cpp new file mode 100644 index 0000000..8feca6f --- /dev/null +++ b/ui/widget/GroupItem.cpp @@ -0,0 +1,120 @@ +#include "GroupItem.h" +#include "ui_GroupItem.h" + +#include "ui/edit/dialog_edit_group.h" +#include "main/GuiUtils.hpp" +#include "sub/GroupUpdater.hpp" + +#include + +QString ParseSubInfo(const QString &info) { + if (info.trimmed().isEmpty()) return ""; + + QString result; + + long long used = 0; + long long total = 0; + long long expire = 0; + + + auto re0m = QRegularExpression("total=([0-9]+)").match(info); + if (re0m.lastCapturedIndex() >= 1) { + total = re0m.captured(1).toLongLong(); + } else { + return ""; + } + auto re1m = QRegularExpression("upload=([0-9]+)").match(info); + if (re1m.lastCapturedIndex() >= 1) { + used += re1m.captured(1).toLongLong(); + } + auto re2m = QRegularExpression("download=([0-9]+)").match(info); + if (re2m.lastCapturedIndex() >= 1) { + used += re2m.captured(1).toLongLong(); + } + auto re3m = QRegularExpression("expire=([0-9]+)").match(info); + if (re3m.lastCapturedIndex() >= 1) { + expire = re3m.captured(1).toLongLong(); + } + + result = QObject::tr("Used: %1 Remain: %2 Expire: %3") + .arg(ReadableSize(used), ReadableSize(total - used), DisplayTime(expire, QLocale::ShortFormat)); + + return result; +} + +GroupItem::GroupItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item) : + QWidget(parent), ui(new Ui::GroupItem) { + ui->setupUi(this); + + this->ent = ent; + this->item = item; + if (ent == nullptr) return; + + connect(this, &GroupItem::edit_clicked, this, &GroupItem::on_edit_clicked); + + refresh_data(); +} + +GroupItem::~GroupItem() { + delete ui; +} + +void GroupItem::refresh_data() { + ui->name->setText(ent->name); + + auto type = ent->url.isEmpty() ? tr("Basic") : tr("Subscription"); + if (ent->archive) type = tr("Archive") + " " + type; + type += " (" + Int2String(ent->Profiles().length()) + ")"; + ui->type->setText(type); + + if (ent->url.isEmpty()) { + ui->url->hide(); + ui->subinfo->hide(); + ui->update_sub->hide(); + } else { + ui->url->setText(ent->url); + auto subinfo = ParseSubInfo(ent->info); + if (subinfo.isEmpty()) { + ui->subinfo->hide(); + } else { + ui->subinfo->show(); + ui->subinfo->setText(subinfo); + } + } + runOnUiThread([=] { + adjustSize(); + item->setSizeHint(sizeHint()); + dynamic_cast(parent())->adjustSize(); + }, this); +} + +void GroupItem::on_update_sub_clicked() { + if (QMessageBox::question(this, tr("Confirmation"), tr("Update %1?").arg(ent->name)) + == QMessageBox::StandardButton::Yes) { + NekoRay::sub::groupUpdater->AsyncUpdate(ent->url, ent->id, this, [=] { + refresh_data(); + }); + } +} + +void GroupItem::on_edit_clicked() { + auto dialog = new DialogEditGroup(ent, this); + int ret = dialog->exec(); + dialog->deleteLater(); + + if (ret == QDialog::Accepted) { + ent->Save(); + refresh_data(); + dialog_message(Dialog_DialogManageGroups, "refresh" + Int2String(ent->id)); + } +} + +void GroupItem::on_remove_clicked() { + if (NekoRay::profileManager->groups.count() == 1) return; + if (QMessageBox::question(this, tr("Confirmation"), tr("Remove %1?").arg(ent->name)) == + QMessageBox::StandardButton::Yes) { + NekoRay::profileManager->DeleteGroup(ent->id); + dialog_message(Dialog_DialogManageGroups, "refresh-1"); + delete item; + } +} diff --git a/ui/widget/GroupItem.h b/ui/widget/GroupItem.h new file mode 100644 index 0000000..8178c4b --- /dev/null +++ b/ui/widget/GroupItem.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "db/Database.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class GroupItem; } +QT_END_NAMESPACE + +class GroupItem : public QWidget { +Q_OBJECT + +public: + explicit GroupItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item); + + ~GroupItem() override; + + void refresh_data(); + + QSharedPointer ent; + QListWidgetItem *item; + +private: + Ui::GroupItem *ui; + +signals: + + void edit_clicked(); + +private slots: + + void on_update_sub_clicked(); + + void on_edit_clicked(); + + void on_remove_clicked(); +}; diff --git a/ui/widget/GroupItem.ui b/ui/widget/GroupItem.ui new file mode 100644 index 0000000..fd2f77f --- /dev/null +++ b/ui/widget/GroupItem.ui @@ -0,0 +1,128 @@ + + + GroupItem + + + + 0 + 0 + 403 + 300 + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + color: rgb(251, 114, 153); + + + Type + + + + + + + + 0 + 0 + + + + Name + + + + + + + + 0 + 0 + + + + Update Subscription + + + + + + + + 0 + 0 + + + + Edit + + + + + + + + 0 + 0 + + + + Remove + + + + + + + + + + 0 + 0 + + + + color: rgb(102, 102, 102); + + + Url + + + + + + + + 0 + 0 + + + + 订阅流量信息 + + + + + + + + diff --git a/ui/widget/MyLineEdit.h b/ui/widget/MyLineEdit.h new file mode 100644 index 0000000..90dee9e --- /dev/null +++ b/ui/widget/MyLineEdit.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class MyLineEdit : public QLineEdit { +public slots: + + explicit MyLineEdit(QWidget *parent = nullptr) : QLineEdit(parent) { + } + + void setText(const QString &s) { + QLineEdit::setText(s); + QLineEdit::home(false); + } +}; diff --git a/ui/widget/MyTableWidget.h b/ui/widget/MyTableWidget.h new file mode 100644 index 0000000..ea7d014 --- /dev/null +++ b/ui/widget/MyTableWidget.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include + +class MyTableWidget : public QTableWidget { +public: + explicit MyTableWidget(QWidget *parent = nullptr) : QTableWidget(parent) { + // 拖拽设置 + this->setDragDropMode(QAbstractItemView::InternalMove); // 内部移动 + this->setDropIndicatorShown(true); // drop位置 提示 + this->setSelectionBehavior(QAbstractItemView::SelectRows); + }; + +// takes and returns the whole row + QList takeRow(int row) { + QList rowItems; + for (int col = 0; col < columnCount(); ++col) { + rowItems << takeItem(row, col); + } + return rowItems; + } + +// sets the whole row + void setRow(int row, const QList &rowItems) { + for (int col = 0; col < columnCount(); ++col) { + setItem(row, col, rowItems.at(col)); + } + } + + QList order; // id sorted + std::function callback_save_order; + std::map id2Row; + + void _save_order(bool saveToFile) { + order.clear(); + id2Row.clear(); + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + order += id; + id2Row[id] = i; + } + if (callback_save_order != nullptr && saveToFile) + callback_save_order(); + } + + void update_order(bool saveToFile) { + if (order.isEmpty()) { + _save_order(false); + return; + } + + // 纠错: order 里面含有不在当前表格控件的 id + bool needSave = false; + auto deleted_profiles = order; + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + deleted_profiles.removeAll(id); + } + for (auto deleted_profile: deleted_profiles) { + needSave = true; + order.removeAll(deleted_profile); + } + + QMap> newRows; + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + auto dst = order.indexOf(id); + if (dst == i) continue; + if (dst == -1) { + // 纠错: 新的profile不需要移动 + needSave = true; + continue; + } + newRows[dst] = takeRow(i); + } + + for (int i = 0; i < this->rowCount(); i++) { + if (!newRows.contains(i)) continue; + setRow(i, newRows[i]); + } + + // Then save the order + _save_order(needSave || saveToFile); + }; + +protected: + +/* + * 2021.7.6 by gy + * 拖拽 继承QTableWidget overwrite dropEvent事件 + * 功能:拖动一行到鼠标落下的位置 + * 注意:DragDropMode相关参数的设置 +*/ + void dropEvent(QDropEvent *event) override { + // 原行号与目标行号的确定 + int row_src, row_dst; + row_src = this->currentRow();// 原行号 可加if + QTableWidgetItem *item = this->itemAt(event->pos());// 获取落点的item + if (item != nullptr) { + // 判断是否为空 + row_dst = item->row();// 不为空 获取其行号 + // 保证鼠标落下的位置 就是拖拽的一行最后所移动到的位置(考虑插入新行 移除原行的上下变化) + row_src = (row_src > row_dst ? row_src + 1 : row_src);// 如果src在dst的下方(行号大),后续插入dst会影响src的行号 + row_dst = (row_src < row_dst ? row_dst + 1 : row_dst);// 如果src在dst的上方(行号小),后续移除src会影响dst的行号 + this->insertRow(row_dst);// 插入一行 + } else { + // 落点没有item 说明拖动到了最下面 + row_dst = this->rowCount();// 获取行总数 + this->insertRow(row_dst);// 在最后新增一行 + } + // 执行移动 并移除原行 + for (int i = 0; i < this->columnCount(); i++) { + // 遍历列 + this->setItem(row_dst, i, this->takeItem(row_src, i));// 每一列item的移动 + } + this->removeRow(row_src);// 删除原行 + + // Then save the order + _save_order(true); + }; +}; diff --git a/ui/widget/ProxyItem.cpp b/ui/widget/ProxyItem.cpp new file mode 100644 index 0000000..b64234a --- /dev/null +++ b/ui/widget/ProxyItem.cpp @@ -0,0 +1,41 @@ +#include "ProxyItem.h" +#include "ui_ProxyItem.h" + +#include + +ProxyItem::ProxyItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item) : + QWidget(parent), ui(new Ui::ProxyItem) { + ui->setupUi(this); + this->item = item; + this->ent = ent; + if (ent == nullptr) return; + + refresh_data(); +} + +ProxyItem::~ProxyItem() { + delete ui; +} + +void ProxyItem::refresh_data() { + ui->type->setText(ent->bean->DisplayType()); + ui->name->setText(ent->bean->DisplayName()); + ui->address->setText(ent->bean->DisplayAddress()); + ui->traffic->setText(ent->traffic_data->DisplayTraffic()); + ui->test_result->setText(ent->DisplayLatency()); + + runOnUiThread([=] { + adjustSize(); + item->setSizeHint(sizeHint()); + dynamic_cast(parent())->adjustSize(); + }, this); +} + +void ProxyItem::on_remove_clicked() { + if (!this->remove_confirm || + QMessageBox::question(this, tr("Confirmation"), tr("Remove %1?").arg(ent->bean->DisplayName())) + == QMessageBox::StandardButton::Yes) { + // TODO do remove (or not) -> callback + delete item; + } +} diff --git a/ui/widget/ProxyItem.h b/ui/widget/ProxyItem.h new file mode 100644 index 0000000..fb3533f --- /dev/null +++ b/ui/widget/ProxyItem.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "db/ProxyEntity.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class ProxyItem; } +QT_END_NAMESPACE + +class ProxyItem : public QWidget { +Q_OBJECT + +public: + explicit ProxyItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item); + + ~ProxyItem() override; + + void refresh_data(); + + QSharedPointer ent; + QListWidgetItem *item; + bool remove_confirm = false; + +private: + Ui::ProxyItem *ui; + +private slots: + + void on_remove_clicked(); +}; diff --git a/ui/widget/ProxyItem.ui b/ui/widget/ProxyItem.ui new file mode 100644 index 0000000..ce7915e --- /dev/null +++ b/ui/widget/ProxyItem.ui @@ -0,0 +1,99 @@ + + + ProxyItem + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + + + + + + + + 名称 + + + + + + + + 0 + 0 + + + + Remove + + + + + + + + + + + color: rgb(102, 102, 102); + + + 地址 + + + + + + + 测试结果 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + color: rgb(251, 114, 153); + + + 类型 + + + + + + + 流量 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + diff --git a/updater/.gitignore b/updater/.gitignore new file mode 100644 index 0000000..7b3c510 --- /dev/null +++ b/updater/.gitignore @@ -0,0 +1,2 @@ +/updater +/launcher diff --git a/updater/go.mod b/updater/go.mod new file mode 100644 index 0000000..8a8b280 --- /dev/null +++ b/updater/go.mod @@ -0,0 +1,11 @@ +module updater + +go 1.18 + +require github.com/codeclysm/extract v2.2.0+incompatible + +require ( + github.com/h2non/filetype v1.1.3 // indirect + github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect + github.com/stretchr/testify v1.7.1 // indirect +) diff --git a/updater/go.sum b/updater/go.sum new file mode 100644 index 0000000..4949ed5 --- /dev/null +++ b/updater/go.sum @@ -0,0 +1,19 @@ +github.com/codeclysm/extract v2.2.0+incompatible h1:q3wyckoA30bhUSiwdQezMqVhwd8+WGE64/GL//LtUhI= +github.com/codeclysm/extract v2.2.0+incompatible/go.mod h1:2nhFMPHiU9At61hz+12bfrlpXSUrOnK+wR+KlGO4Uks= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/juju/errors v0.0.0-20220331221717-b38fca44723b h1:AxFeSQJfcm2O3ov1wqAkTKYFsnMw2g1B4PkYujfAdkY= +github.com/juju/errors v0.0.0-20220331221717-b38fca44723b/go.mod h1:jMGj9DWF/qbo91ODcfJq6z/RYc3FX3taCBZMCcpI4Ls= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/updater/launcher_linux.go b/updater/launcher_linux.go new file mode 100644 index 0000000..0593d8d --- /dev/null +++ b/updater/launcher_linux.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +var local_qt_theme bool + +func Launcher() { + log.Println("Running as launcher") + wd, _ := filepath.Abs(".") + + _debug := flag.Bool("debug", false, "Debug mode") + flag.BoolVar(&local_qt_theme, "theme", false, "Use local QT theme (unstable)") + flag.Parse() + + // Find & symlink some Qt Plugin to enable system theme + tryLinkQtPlugin("styles", !local_qt_theme) + tryLinkQtPlugin("platformthemes", !local_qt_theme) + + cmd := exec.Command("./nekoray", flag.Args()...) + cmd.Env = os.Environ() + ld_env := "LD_LIBRARY_PATH=" + filepath.Join(wd, "./usr/lib") + cmd.Env = append(cmd.Env, ld_env) + log.Println(ld_env, cmd) + + if *_debug { + cmd.Env = append(cmd.Env, "QT_DEBUG_PLUGINS=1") + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Run() + } else { + cmd.Start() + } +} + +func tryLinkQtPlugin(sub string, remove bool) { + wd_plugins_sub := filepath.Join("./usr/plugins", sub) + + if Exist(wd_plugins_sub) { + if remove { + os.RemoveAll(wd_plugins_sub) + } + } else { + if remove { + return + } + + arch := "x86_64" + if runtime.GOARCH == "arm64" { + arch = "aarch64" + } + + paths := []string{ + filepath.Join("/usr/lib/qt5/plugins", sub), + filepath.Join("/usr/lib64/qt5/plugins", sub), + filepath.Join("/usr/lib/"+arch+"-linux-gnu/qt5/plugins", sub), + filepath.Join("/usr/lib/qt/plugins", sub), + } + path := FindExist(paths) + if path == "" { + log.Println("warning:", sub, "not found") + return + } + + err := os.Symlink(path, wd_plugins_sub) + if err != nil { + log.Println("symlink failed:", err.Error()) + } + } +} diff --git a/updater/launcher_windows.go b/updater/launcher_windows.go new file mode 100644 index 0000000..ec63e6b --- /dev/null +++ b/updater/launcher_windows.go @@ -0,0 +1,10 @@ +package main + +import ( + "log" + "runtime" +) + +func Launcher() { + log.Fatalf("launcher is not for your platform", runtime.GOOS) +} diff --git a/updater/main.go b/updater/main.go new file mode 100644 index 0000000..c104cb9 --- /dev/null +++ b/updater/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +func main() { + // update & launcher + exe, err := os.Executable() + if err != nil { + panic(err.Error()) + } + + wd := filepath.Dir(exe) + os.Chdir(wd) + exe = filepath.Base(os.Args[0]) + log.Println("exe:", exe, "exe dir:", wd) + + if strings.HasPrefix(strings.ToLower(exe), "updater") { + if runtime.GOOS == "windows" { + if strings.HasPrefix(strings.ToLower(exe), "updater.old") { + // 2. "updater.old" update files + time.Sleep(time.Second) + Updater() + // 3. start + exec.Command("./nekoray.exe").Start() + } else { + // 1. nekoray stop it self and run "updater.exe" + Copy("./updater.exe", "./updater.old") + exec.Command("./updater.old").Start() + } + } else { + // 1. update files + Updater() + // 2. start + Launcher() + } + return + } else if strings.HasPrefix(strings.ToLower(exe), "launcher") { + Launcher() + return + } + log.Fatalf("wrong name") +} + +func Copy(src string, dst string) { + // Read all content of src to data + data, _ := ioutil.ReadFile(src) + // Write data to dst + ioutil.WriteFile(dst, data, 0644) +} diff --git a/updater/updater.go b/updater/updater.go new file mode 100644 index 0000000..f65615a --- /dev/null +++ b/updater/updater.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/codeclysm/extract" +) + +func Updater() { + pre_cleanup := func() { + if runtime.GOOS == "linux" { + os.RemoveAll("./usr") + } + os.RemoveAll("./nekoray_update") + } + + // update extract + if Exist("./nekoray.zip") { + pre_cleanup() + log.Println("updating from zip") + + f, err := os.Open("./nekoray.zip") + if err != nil { + log.Fatalln(err.Error()) + } + err = extract.Zip(context.Background(), f, "./nekoray_update", nil) + if err != nil { + log.Fatalln(err.Error()) + } + f.Close() + } else if Exist("./nekoray.tar.gz") { + pre_cleanup() + log.Println("updating from tar.gz") + + f, err := os.Open("./nekoray.tar.gz") + if err != nil { + log.Fatalln(err.Error()) + } + err = extract.Gz(context.Background(), f, "./nekoray_update", nil) + if err != nil { + log.Fatalln(err.Error()) + } + f.Close() + } else { + log.Fatalln("no update") + } + + // remove old file + removeAll("./*.dll") + removeAll("./*.dmp") + + // nekoray_list_old := make([]string, 0) + // nekoray_list_new := make([]string, 0) + + // // delete old file from list + // if f, _ := os.Open("./files.json"); f != nil { + // err := json.NewDecoder(f).Decode(&nekoray_list_old) + // if err == nil { + // for _, fn := range nekoray_list_old { + // log.Println("del", fn, os.RemoveAll(fn)) + // } + // } + // f.Close() + // } + + // // walk the new file list + // if os.Chdir("./nekoray_update/nekoray/") == nil { + // filepath.Walk(".", + // func(path string, info os.FileInfo, err error) error { + // if path != "." { + // nekoray_list_new = append([]string{path}, nekoray_list_new...) + // } + // return nil + // }) + // os.Chdir("../../") + + // // store new file list + // if f, _ := os.OpenFile("./files.json", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644); f != nil { + // json.NewEncoder(f).Encode(&nekoray_list_new) + // f.Close() + // } + // } + + // update move + err := Mv("./nekoray_update/nekoray", "./") + if err != nil { + log.Fatalln(err.Error()) + } + + os.RemoveAll("./nekoray_update") + os.RemoveAll("./nekoray.zip") + os.RemoveAll("./nekoray.tar.gz") +} + +func Exist(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func FindExist(paths []string) string { + for _, path := range paths { + if Exist(path) { + return path + } + } + return "" +} + +func Mv(src, dst string) error { + s, err := os.Stat(src) + if err != nil { + return err + } + if s.IsDir() { + es, err := os.ReadDir(src) + if err != nil { + return err + } + for _, e := range es { + err = Mv(filepath.Join(src, e.Name()), filepath.Join(dst, e.Name())) + if err != nil { + return err + } + } + } else { + err = os.MkdirAll(filepath.Dir(dst), 0755) + if err != nil { + return err + } + err = os.Rename(src, dst) + if err != nil { + return err + } + } + return nil +} + +func removeAll(glob string) { + files, _ := filepath.Glob(glob) + if files != nil { + for _, f := range files { + os.Remove(f) + } + } +}