Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,15 @@ Key internal components:
## Directory map

- `cmake/` — CMake modules (compiler flags, OpenSSL detection, utilities)
- `include/` — public headers (10 files: requestcontext.h, networkerror.h, responseresult.h, sslconfig.h, proxyconfig.h, taskdata.h, networkrequestmanager.h, networkreply.h, networkrequestevent.h, networkrequestglobal.h)
- `source/` — implementation + private headers (25+ files)
- `include/` — public headers (16 files: requestcontext.h, networkerror.h, responseresult.h, sslconfig.h, proxyconfig.h, taskdata.h, networkrequestmanager.h, networkreply.h, networkrequestevent.h, networkrequestglobal.h, authconfig.h, environment.h, environmentstore.h, oauth2tokencache.h, collectionmodel.h, postmanconverter.h)
- `source/` — implementation + private headers (39+ files)
- Core: `networkrequest.h/.cpp`, `networkcommonrequest.*`, `networkdownloadrequest.*`, `networkuploadrequest.*`, `networkmtdownloadrequest.*`
- State machine: `networkmtdownloadrequest_p.h` (state interface), `networkmtdownloadrequest_states.cpp` (ProbeState, RangeProbeState, MultiDownloadState)
- Strategies: `requeststrategies.h/.cpp` (IRetryStrategy, IRedirectHandler, ISslPolicy)
- Cross-cutting: `qtcompat.h` (Qt version adapters), `progressthrottle.h/.cpp` (progress rate-limiter), `networkrequestregistry.h/.cpp` (self-registering factory)
- Features (v2.3): `environment.cpp` (env substitution), `environmentstore.cpp` (named envs), `oauth2tokencache.cpp` (token cache), `collectionmodel.cpp` (collection tree), `postmanconverter.cpp` (Postman v2.1 I/O)
- Infrastructure: `networkrequestmanager.cpp`, `networkrequestrunnable.*`, `networkaccessmanagerpool.*`, `memorymappedfile.*`, `networkrequestutility.*`, `networkreply.cpp`, `networkcookiejar.*`, `sharedcookiejar.h`
- `test/` — Qt Test unit tests (5 suites: UnitTests, BuilderTests, UtilityTests, DownloaderTests, UiTests)
- `test/` — Qt Test unit tests (7 suites: UnitTests, BuilderTests, AuthTests, CollectionTests, UtilityTests, DownloaderTests, UiTests)
- `samples/networkrequesttool/` — GUI demo
- `samples/networkdownloader/` — download manager app
- `ThirdParty/openssl/` — bundled OpenSSL for Windows
Expand Down
14 changes: 14 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
version 2.3.0 2026-08-05

- Feat: OAuth2 authentication (Client Credentials, Password, Refresh Token) with async token fetch, thread-safe in-memory token cache, and 401 auto-refresh
- Feat: environment variables — named environments (dev/staging/prod) with {{var}} substitution across URL, headers, body, query params, and auth fields; GUI dropdown + Manage Environments dialog
- Feat: response body toolbar — incremental search with highlight navigation, copy to clipboard, save to file, JSON pretty/raw toggle
- Feat: request collections — folder/request tree in left panel, add/remove, persist to collection.json, click to load
- Feat: Postman v2.1 bidirectional import/export (folders, requests, auth round-trip)
- Feat: auth fields serialized in save/load and collection requests (R2)
- Feat: Windows test runner scripts/test.bat (configure + build + ctest)
- Test: 5 new OAuth2 integration tests against local HttpTestServer token endpoint
- Test: UiTests extended with auth save/load round-trip, response search, environment dropdown
- Test: CollectionTests model + Postman round-trip (M0)
- Style: loadFromDisk refactored into reusable loadRequestFromJson (D.1)

version 2.2.1 2026-07-27

- Refactor: consolidate Network*Request classes — eliminate ~250 lines of duplicated code via shared base-class helpers (prepareRequest, evaluateOutcome, handleFailure, collectResponse, disposeReply)
Expand Down
5 changes: 5 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
- **请求优先级队列**:线程饱和时高优先级请求优先执行
- **进度追踪**:下载、上传、批量操作的实时进度更新
- **跨平台**:Windows、Linux、macOS 全支持,含平台特定优化
- **OAuth2 认证**:Client Credentials / Password / Refresh Token,异步 token 获取,线程安全内存缓存,401 自动续期 (v2.3)
- **环境变量**:多命名环境(dev/staging/prod),`{{var}}` 全局替换(URL/Headers/Body/Auth),GUI 下拉切换 + 管理对话框 (v2.3)
- **响应增强**:增量搜索(高亮导航)、一键复制、保存到文件、JSON 格式化/原始切换 (v2.3)
- **请求收藏**:左侧树形面板,文件夹/请求管理,持久化到 collection.json,点击加载 (v2.3)
- **Postman v2.1 支持**:双向导入导出(文件夹、请求、认证字段完整往返)(v2.3)

### 📦 示例应用

Expand Down
197 changes: 197 additions & 0 deletions include/authconfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
@Brief: Qt multi-threaded network request module – authentication configuration

MIT License

Copyright (c) 2025 Lucas Wang

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 <QString>
#include <QByteArray>

#pragma pack(push, _CRT_PACKING)

namespace QtNetworkRequest
{
/// Supported authentication types (extensible for future Digest, OAuth2, etc.)
enum class AuthType : int8_t
{
None = 0, // No authentication
Basic = 1, // HTTP Basic Authentication (RFC 7617)
Bearer = 2, // Bearer Token Authentication (RFC 6750)
ApiKey = 3, // Custom API Key in Header or Query Param
OAuth2 = 4, // OAuth 2.0 (client-credentials / password / refresh-token)
};

/// Placement strategy for API Key authentication
enum class ApiKeyPlacement : int8_t
{
Header = 0, // Add as custom request header
QueryParam = 1, // Append to URL query string
};

/// OAuth 2.0 grant types supported by AuthConfig::OAuth2.
/// (Authorization Code + PKCE is intentionally out of scope for v1.)
enum class OAuth2GrantType : int8_t
{
ClientCredentials = 0, // client_credentials
Password = 1, // Resource Owner Password Credentials (ROPC)
RefreshToken = 2, // refresh_token (also used for auto-renew)
};

/// Authentication configuration (value type, read-only after RequestContext build)
struct AuthConfig
{
AuthType type{ AuthType::None };

// --- Basic Auth ---
QString username;
QString password;

// --- Bearer Token ---
/// Raw token value. The library auto-prepends "Bearer " prefix when building the header.
QString token;

// --- API Key ---
QString apiKey; // Header name or query param key
QString apiValue; // Header value or query param value
ApiKeyPlacement apiKeyPlacement{ ApiKeyPlacement::Header };

/// OAuth 2.0 grant configuration (client-credentials / password / refresh-token).
struct OAuth2Config
{
OAuth2GrantType grant{ OAuth2GrantType::ClientCredentials };
QString clientId;
QString clientSecret;
QString scopes; // space-separated scope list
QString tokenUrl; // absolute token endpoint
// Password grant
QString username;
QString password;
// Refresh grant (also used for auto-renew of any grant)
QString refreshToken;
};

// --- OAuth2 ---
OAuth2Config oauth2Config;

// --- Factory methods ---

/// Create a Basic Authentication config.
/// @param user Username (must not be empty)
/// @param pass Password
static AuthConfig basic(const QString &user, const QString &pass)
{
AuthConfig c;
c.type = AuthType::Basic;
c.username = user;
c.password = pass;
return c;
}

/// Create a Bearer Token config.
/// @param tok Raw token string (must not be empty)
static AuthConfig bearer(const QString &tok)
{
AuthConfig c;
c.type = AuthType::Bearer;
c.token = tok;
return c;
}

/// Create an API Key config.
/// @param key Header name or query param key (must not be empty)
/// @param value Header value or query param value (must not be empty)
/// @param placement Where to inject the key (default: Header)
static AuthConfig apiKeyAuth(const QString &key, const QString &value,
ApiKeyPlacement placement = ApiKeyPlacement::Header)
{
AuthConfig c;
c.type = AuthType::ApiKey;
c.apiKey = key;
c.apiValue = value;
c.apiKeyPlacement = placement;
return c;
}

/// Create an OAuth 2.0 config from a grant configuration.
static AuthConfig oauth2(const OAuth2Config &cfg)
{
AuthConfig c;
c.type = AuthType::OAuth2;
c.oauth2Config = cfg;
return c;
}

/// Validate that required credentials are present for the selected auth type.
/// AuthType::None is always considered valid.
bool isValid() const
{
switch (type)
{
case AuthType::Basic: return !username.isEmpty();
case AuthType::Bearer: return !token.isEmpty();
case AuthType::ApiKey: return !apiKey.isEmpty() && !apiValue.isEmpty();
case AuthType::OAuth2:
if (oauth2Config.grant == OAuth2GrantType::Password)
return !oauth2Config.tokenUrl.isEmpty() && !oauth2Config.clientId.isEmpty()
&& !oauth2Config.username.isEmpty() && !oauth2Config.password.isEmpty();
return !oauth2Config.tokenUrl.isEmpty() && !oauth2Config.clientId.isEmpty();
case AuthType::None: return true;
default: return false;
}
}

/// Generate the Authorization header value.
/// For Basic: base64-encoded "user:pass"
/// For Bearer: "Bearer <token>"
/// For others: empty QByteArray
QByteArray authorizationHeaderValue() const
{
switch (type)
{
case AuthType::Basic:
{
QByteArray credentials = (username + u':' + password).toUtf8();
return QByteArrayLiteral("Basic ") + credentials.toBase64();
}
case AuthType::Bearer:
return QByteArrayLiteral("Bearer ") + token.toUtf8();
case AuthType::OAuth2:
// token is populated at runtime by the async OAuth2 token fetch (M2);
// until then it is empty and the header is simply "Bearer ".
return QByteArrayLiteral("Bearer ") + token.toUtf8();
default:
return {};
}
}

/// Header name to use for API Key auth (Header placement).
QByteArray apiKeyHeaderName() const { return apiKey.toUtf8(); }

/// Header value to use for API Key auth (Header placement).
QByteArray apiKeyHeaderValue() const { return apiValue.toUtf8(); }
};
}

#pragma pack(pop)
98 changes: 98 additions & 0 deletions include/collectionmodel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
@Brief: Qt multi-threaded network request module – request Collection model

MIT License

Copyright (c) 2025 Lucas Wang

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 <QString>
#include <QJsonObject>
#include <QList>
#include "networkrequestglobal.h"

namespace QtNetworkRequest
{
enum class CollectionItemType
{
Folder,
Request
};

/// A node in a Collection tree. Folders contain children; Requests carry a
/// `requestJson` object whose fields match the on-disk single-request schema
/// (method/url/body/bodyType/rawType/timestamp/params[]/headers[]/auth).
struct CollectionItem
{
CollectionItemType type{ CollectionItemType::Folder };
QString id; // stable id for tree mapping / lookup
QString name;
QJsonObject requestJson; // for Request items
QList<CollectionItem> children; // for Folder items
};

/// In-memory Collection (folders + requests) with JSON (de)serialization.
/// Backs both the GUI tree and the Postman v2.1 converter.
class NETWORK_EXPORT Collection
{
public:
Collection() = default;
explicit Collection(const QString &name) : m_name(name) {}

const QString &name() const { return m_name; }
void setName(const QString &name) { m_name = name; }

const CollectionItem &root() const { return m_root; }
CollectionItem &root() { return m_root; }

/// Load/save the whole collection from/to a JSON file.
bool load(const QString &path);
bool save(const QString &path) const;

/// Mutators return the new item's id (empty string on failure).
QString addFolder(const QString &parentId, const QString &name);
QString addRequest(const QString &parentId, const QString &name,
const QJsonObject &requestJson);
/// Remove the item with the given id anywhere in the tree.
void removeItem(const QString &id);

/// Lookup by id (navigates from root). Returns nullptr if not found.
CollectionItem *findById(const QString &id);
const CollectionItem *findById(const QString &id) const;

/// Generate a stable unique id (UUID without braces).
static QString genId();

/// (De)serialize a single item (shared by Collection + PostmanConverter).
static QJsonObject toJson(const CollectionItem &item);
static CollectionItem fromJson(const QJsonObject &obj, bool *ok = nullptr);

private:
CollectionItem *findById(CollectionItem &node, const QString &id);
const CollectionItem *findById(const CollectionItem &node, const QString &id) const;
bool removeFrom(CollectionItem &parent, const QString &id);

QString m_name;
CollectionItem m_root{ CollectionItemType::Folder, QString(), QStringLiteral("root") };
};
}
46 changes: 46 additions & 0 deletions include/environment.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
@Brief: Qt multi-threaded network request module – environment variable substitution

MIT License

Copyright (c) 2025 Lucas Wang

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 <QString>
#include <QMap>
#include "requestcontext.h" // RequestContext (authConfig, headers, ...)
#include "authconfig.h"
#include "networkrequestglobal.h"

namespace QtNetworkRequest
{
/// Replace all `{{key}}` occurrences in `text` using `vars`.
/// Unknown keys are left intact (Postman-style, so misconfiguration is visible).
/// A single pass is performed — values are NOT recursively substituted.
NETWORK_EXPORT QString substituteEnv(const QString &text, const QMap<QString, QString> &vars);

/// Substitute `{{var}}` across a whole RequestContext: url, header values, body,
/// query params, and the relevant AuthConfig fields (R3). `binaryBody` is never
/// touched, because env substitution must not corrupt binary payloads.
NETWORK_EXPORT void applyEnvironment(RequestContext &ctx, const QMap<QString, QString> &vars);
}
Loading
Loading