Add AIServer, character art pipeline, and Art package boot integration.

Bring in MaleExplorer assets/shaders, model tooling and Hunyuan updates, plus boot/YooAsset Art package support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 17:06:59 +08:00
co-authored by Cursor
parent b7a0bbcb89
commit 6550b8df4b
121 changed files with 313260 additions and 113 deletions
+42
View File
@@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 3.20)
project(AIServer LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(aiserver
src/main.cpp
src/db.cpp
src/auth.cpp
src/http_ws_server.cpp
src/crypto.cpp
third_party/sqlite3.c
)
target_include_directories(aiserver PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/third_party
)
# SQLite: 关闭部分扩展,加快编译
target_compile_definitions(aiserver PRIVATE
SQLITE_OMIT_LOAD_EXTENSION
SQLITE_THREADSAFE=1
_CRT_SECURE_NO_WARNINGS
)
if(WIN32)
target_link_libraries(aiserver PRIVATE ws2_32 bcrypt)
# sqlite3.c 是 C,MSVC 下关闭部分告警
if(MSVC)
target_compile_options(aiserver PRIVATE /W3 /utf-8)
set_source_files_properties(third_party/sqlite3.c PROPERTIES COMPILE_FLAGS "/W0")
endif()
else()
target_link_libraries(aiserver PRIVATE pthread dl)
endif()
# 运行时默认数据目录:可执行文件旁的 data/
add_custom_command(TARGET aiserver POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:aiserver>/data
)
+43
View File
@@ -0,0 +1,43 @@
# AIServer
C++ 登录 / WebSocket 网关(SQLite),协议兼容客户端 `HotUpdate.Net`。
## 接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/register` | `{"username","password"}` → `{"token","pid"}` |
| POST | `/login` | 同上 |
| GET | `/health` | 健康检查 |
| WS | `/ws?pid={pid}&token={token}` | 无 token → 401;支持 `hello`/`ping` 应用层应答 |
本地默认:`http://127.0.0.1:8080`、`ws://127.0.0.1:8080/ws?...`
生产由 Caddy TLS 反代到本进程(与 `game.xworld.ren` 部署方式一致)。
## 构建(Windows / VS2022)
```powershell
cd D:\UD\AI\AIGame\AIServer
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
```
产物:`build\Release\aiserver.exe`
## 运行
```powershell
cd D:\UD\AI\AIGame\AIServer\build\Release
.\aiserver.exe --host 0.0.0.0 --port 8080 --db ..\..\data\aiserver.db
```
## 客户端联调
Unity 菜单或 Boot 配置:
- HttpBase: `http://127.0.0.1:8080`
- WsUrlTemplate: `ws://127.0.0.1:8080/ws?pid={pid}&token={token}`
## 数据
SQLite 文件默认 `AIServer/data/aiserver.db`(用户表 + token 表)。
+98
View File
@@ -0,0 +1,98 @@
#include "auth.hpp"
#include "crypto.hpp"
#include "json.hpp"
#include <cctype>
#include <ctime>
namespace auth {
namespace {
using nlohmann::json;
bool valid_username(const std::string& u) {
if (u.size() < 2 || u.size() > 64) return false;
for (unsigned char c : u) {
if (!(std::isalnum(c) || c == '_' || c == '-' || c == '.')) return false;
}
return true;
}
bool valid_password(const std::string& p) {
return p.size() >= 3 && p.size() <= 128;
}
std::string make_token(int64_t pid) {
const auto ts = static_cast<int64_t>(std::time(nullptr));
// 兼容线上探测格式:v1.<pid>.<unix>.<random>
return "v1." + std::to_string(pid) + "." + std::to_string(ts) + "." + crypto::random_token_part(24);
}
Result ok_token(int64_t pid, const std::string& token) {
Result r;
r.ok = true;
r.http_status = 200;
r.body = json{{"token", token}, {"pid", pid}}.dump();
return r;
}
Result err(int status, const std::string& code) {
Result r;
r.ok = false;
r.http_status = status;
r.body = json{{"error", code}}.dump();
return r;
}
} // namespace
Result register_user(db::Database& database, const std::string& username, const std::string& password) {
if (!valid_username(username) || !valid_password(password)) return err(400, "invalid_input");
if (database.find_user_by_name(username)) return err(409, "user_exists");
const auto salt = crypto::random_salt_hex(16);
const auto hash = crypto::sha256_hex(salt, password);
if (hash.empty()) return err(500, "hash_failed");
auto pid = database.create_user(username, hash, salt);
if (!pid) {
// unique race
if (database.find_user_by_name(username)) return err(409, "user_exists");
return err(500, "db_error");
}
const auto token = make_token(*pid);
const auto now = static_cast<int64_t>(std::time(nullptr));
const auto exp = now + 86400 * 30; // 30 days
if (!database.store_token(token, *pid, now, exp)) return err(500, "token_store_failed");
return ok_token(*pid, token);
}
Result login_user(db::Database& database, const std::string& username, const std::string& password) {
if (!valid_username(username) || !valid_password(password)) return err(400, "invalid_input");
auto user = database.find_user_by_name(username);
if (!user) return err(401, "invalid_credentials");
const auto hash = crypto::sha256_hex(user->salt, password);
if (hash.empty() || hash != user->password_hash) return err(401, "invalid_credentials");
const auto token = make_token(user->pid);
const auto now = static_cast<int64_t>(std::time(nullptr));
const auto exp = now + 86400 * 30;
database.purge_expired_tokens(now);
if (!database.store_token(token, user->pid, now, exp)) return err(500, "token_store_failed");
return ok_token(user->pid, token);
}
bool validate_ws(db::Database& database, int64_t pid, const std::string& token) {
if (token.empty()) return false;
auto tok_pid = database.pid_for_token(token);
if (!tok_pid) return false;
if (pid > 0 && *tok_pid != pid) return false;
return true;
}
} // namespace auth
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "db.hpp"
#include <string>
namespace auth {
struct Result {
bool ok = false;
int http_status = 500;
std::string body; // JSON
};
Result register_user(db::Database& database, const std::string& username, const std::string& password);
Result login_user(db::Database& database, const std::string& username, const std::string& password);
// Validate token matches pid (pid may be 0 to skip pid check).
bool validate_ws(db::Database& database, int64_t pid, const std::string& token);
} // namespace auth
+142
View File
@@ -0,0 +1,142 @@
#include "crypto.hpp"
#include <array>
#include <cstdio>
#include <random>
#include <sstream>
#include <iomanip>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#else
#include <openssl/sha.h> // fallback not used on Windows path
#endif
namespace crypto {
namespace {
constexpr char kWsMagic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string to_hex(const uint8_t* data, size_t n) {
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < n; ++i) oss << std::setw(2) << static_cast<int>(data[i]);
return oss.str();
}
std::string b64_encode(const uint8_t* data, size_t len) {
static const char* tbl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve(((len + 2) / 3) * 4);
size_t i = 0;
while (i + 2 < len) {
uint32_t n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2];
out.push_back(tbl[(n >> 18) & 63]);
out.push_back(tbl[(n >> 12) & 63]);
out.push_back(tbl[(n >> 6) & 63]);
out.push_back(tbl[n & 63]);
i += 3;
}
if (i < len) {
uint32_t n = data[i] << 16;
out.push_back(tbl[(n >> 18) & 63]);
if (i + 1 < len) {
n |= data[i + 1] << 8;
out.push_back(tbl[(n >> 12) & 63]);
out.push_back(tbl[(n >> 6) & 63]);
out.push_back('=');
} else {
out.push_back(tbl[(n >> 12) & 63]);
out.push_back('=');
out.push_back('=');
}
}
return out;
}
#ifdef _WIN32
bool bcrypt_hash(const wchar_t* alg, const uint8_t* in, size_t in_len, std::vector<uint8_t>& out) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
DWORD hashObjLen = 0, hashLen = 0, cb = 0;
if (BCryptOpenAlgorithmProvider(&hAlg, alg, nullptr, 0) != 0) return false;
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&hashObjLen), sizeof(DWORD), &cb, 0);
BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, reinterpret_cast<PUCHAR>(&hashLen), sizeof(DWORD), &cb, 0);
std::vector<uint8_t> hashObj(hashObjLen);
out.assign(hashLen, 0);
bool ok = BCryptCreateHash(hAlg, &hHash, hashObj.data(), hashObjLen, nullptr, 0, 0) == 0 &&
BCryptHashData(hHash, reinterpret_cast<PUCHAR>(const_cast<uint8_t*>(in)), static_cast<ULONG>(in_len), 0) == 0 &&
BCryptFinishHash(hHash, out.data(), hashLen, 0) == 0;
if (hHash) BCryptDestroyHash(hHash);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
return ok;
}
#endif
} // namespace
std::string sha256_hex(const std::string& data) {
#ifdef _WIN32
std::vector<uint8_t> dig;
if (!bcrypt_hash(BCRYPT_SHA256_ALGORITHM, reinterpret_cast<const uint8_t*>(data.data()), data.size(), dig))
return {};
return to_hex(dig.data(), dig.size());
#else
unsigned char dig[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(data.data()), data.size(), dig);
return to_hex(dig, SHA256_DIGEST_LENGTH);
#endif
}
std::string sha256_hex(const std::string& salt, const std::string& password) {
return sha256_hex(salt + ":" + password);
}
std::string websocket_accept_key(const std::string& client_key) {
std::string src = client_key + kWsMagic;
#ifdef _WIN32
std::vector<uint8_t> dig;
if (!bcrypt_hash(BCRYPT_SHA1_ALGORITHM, reinterpret_cast<const uint8_t*>(src.data()), src.size(), dig))
return {};
return b64_encode(dig.data(), dig.size());
#else
unsigned char dig[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const unsigned char*>(src.data()), src.size(), dig);
return b64_encode(dig, SHA_DIGEST_LENGTH);
#endif
}
std::string random_bytes_hex(size_t nbytes) {
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<int> dist(0, 255);
std::vector<uint8_t> buf(nbytes);
for (auto& b : buf) b = static_cast<uint8_t>(dist(gen));
// url-safe-ish base64 without padding for token part
auto s = b64_encode(buf.data(), buf.size());
for (char& c : s) {
if (c == '+') c = '-';
else if (c == '/') c = '_';
}
while (!s.empty() && s.back() == '=') s.pop_back();
return s;
}
std::string random_token_part(size_t nbytes) { return random_bytes_hex(nbytes); }
std::string random_salt_hex(size_t nbytes) {
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<int> dist(0, 255);
std::vector<uint8_t> buf(nbytes);
for (auto& b : buf) b = static_cast<uint8_t>(dist(gen));
return to_hex(buf.data(), buf.size());
}
} // namespace crypto
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace crypto {
std::string sha256_hex(const std::string& data);
std::string sha256_hex(const std::string& salt, const std::string& password);
// WebSocket accept: Base64(SHA1(key + magic))
std::string websocket_accept_key(const std::string& client_key);
std::string random_token_part(size_t nbytes = 24);
std::string random_salt_hex(size_t nbytes = 16);
} // namespace crypto
+169
View File
@@ -0,0 +1,169 @@
#include "db.hpp"
#include "sqlite3.h"
#include <ctime>
namespace db {
Database::Database(const std::string& path) {
if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) {
err_ = db_ ? sqlite3_errmsg(db_) : "sqlite3_open failed";
if (db_) {
sqlite3_close(db_);
db_ = nullptr;
}
return;
}
exec("PRAGMA journal_mode=WAL;");
exec("PRAGMA foreign_keys=ON;");
const char* schema = R"SQL(
CREATE TABLE IF NOT EXISTS users (
pid INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
pid INTEGER NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY(pid) REFERENCES users(pid) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_tokens_pid ON tokens(pid);
)SQL";
if (!exec(schema)) {
sqlite3_close(db_);
db_ = nullptr;
}
}
Database::~Database() {
if (db_) sqlite3_close(db_);
}
bool Database::exec(const char* sql) {
char* errmsg = nullptr;
if (sqlite3_exec(db_, sql, nullptr, nullptr, &errmsg) != SQLITE_OK) {
err_ = errmsg ? errmsg : "exec failed";
if (errmsg) sqlite3_free(errmsg);
return false;
}
return true;
}
std::optional<int64_t> Database::create_user(const std::string& username, const std::string& password_hash,
const std::string& salt) {
const char* sql =
"INSERT INTO users(username, password_hash, salt, created_at) VALUES(?,?,?,?);";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) {
err_ = sqlite3_errmsg(db_);
return std::nullopt;
}
const auto now = static_cast<int64_t>(std::time(nullptr));
sqlite3_bind_text(st, 1, username.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, password_hash.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, salt.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 4, now);
const int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
err_ = sqlite3_errmsg(db_);
return std::nullopt;
}
return static_cast<int64_t>(sqlite3_last_insert_rowid(db_));
}
std::optional<User> Database::find_user_by_name(const std::string& username) {
const char* sql = "SELECT pid, username, password_hash, salt FROM users WHERE username=? COLLATE NOCASE LIMIT 1;";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) {
err_ = sqlite3_errmsg(db_);
return std::nullopt;
}
sqlite3_bind_text(st, 1, username.c_str(), -1, SQLITE_TRANSIENT);
std::optional<User> u;
if (sqlite3_step(st) == SQLITE_ROW) {
User user;
user.pid = sqlite3_column_int64(st, 0);
user.username = reinterpret_cast<const char*>(sqlite3_column_text(st, 1));
user.password_hash = reinterpret_cast<const char*>(sqlite3_column_text(st, 2));
user.salt = reinterpret_cast<const char*>(sqlite3_column_text(st, 3));
u = std::move(user);
}
sqlite3_finalize(st);
return u;
}
std::optional<User> Database::find_user_by_pid(int64_t pid) {
const char* sql = "SELECT pid, username, password_hash, salt FROM users WHERE pid=? LIMIT 1;";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) {
err_ = sqlite3_errmsg(db_);
return std::nullopt;
}
sqlite3_bind_int64(st, 1, pid);
std::optional<User> u;
if (sqlite3_step(st) == SQLITE_ROW) {
User user;
user.pid = sqlite3_column_int64(st, 0);
user.username = reinterpret_cast<const char*>(sqlite3_column_text(st, 1));
user.password_hash = reinterpret_cast<const char*>(sqlite3_column_text(st, 2));
user.salt = reinterpret_cast<const char*>(sqlite3_column_text(st, 3));
u = std::move(user);
}
sqlite3_finalize(st);
return u;
}
bool Database::store_token(const std::string& token, int64_t pid, int64_t created_at, int64_t expires_at) {
const char* sql = "INSERT OR REPLACE INTO tokens(token, pid, created_at, expires_at) VALUES(?,?,?,?);";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) {
err_ = sqlite3_errmsg(db_);
return false;
}
sqlite3_bind_text(st, 1, token.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 2, pid);
sqlite3_bind_int64(st, 3, created_at);
sqlite3_bind_int64(st, 4, expires_at);
const int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
err_ = sqlite3_errmsg(db_);
return false;
}
return true;
}
std::optional<int64_t> Database::pid_for_token(const std::string& token) {
const char* sql = "SELECT pid, expires_at FROM tokens WHERE token=? LIMIT 1;";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) {
err_ = sqlite3_errmsg(db_);
return std::nullopt;
}
sqlite3_bind_text(st, 1, token.c_str(), -1, SQLITE_TRANSIENT);
std::optional<int64_t> pid;
if (sqlite3_step(st) == SQLITE_ROW) {
const auto expires = sqlite3_column_int64(st, 1);
const auto now = static_cast<int64_t>(std::time(nullptr));
if (expires >= now) pid = sqlite3_column_int64(st, 0);
}
sqlite3_finalize(st);
return pid;
}
void Database::purge_expired_tokens(int64_t now) {
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, "DELETE FROM tokens WHERE expires_at < ?;", -1, &st, nullptr) != SQLITE_OK)
return;
sqlite3_bind_int64(st, 1, now);
sqlite3_step(st);
sqlite3_finalize(st);
}
} // namespace db
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
struct sqlite3;
namespace db {
struct User {
int64_t pid = 0;
std::string username;
std::string password_hash;
std::string salt;
};
class Database {
public:
explicit Database(const std::string& path);
~Database();
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
bool open_ok() const { return db_ != nullptr; }
const std::string& last_error() const { return err_; }
// returns pid on success
std::optional<int64_t> create_user(const std::string& username, const std::string& password_hash,
const std::string& salt);
std::optional<User> find_user_by_name(const std::string& username);
std::optional<User> find_user_by_pid(int64_t pid);
bool store_token(const std::string& token, int64_t pid, int64_t created_at, int64_t expires_at);
std::optional<int64_t> pid_for_token(const std::string& token);
void purge_expired_tokens(int64_t now);
private:
bool exec(const char* sql);
sqlite3* db_ = nullptr;
std::string err_;
};
} // namespace db
+440
View File
@@ -0,0 +1,440 @@
#include "http_ws_server.hpp"
#include "auth.hpp"
#include "crypto.hpp"
#include "json.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
using socket_t = SOCKET;
static constexpr socket_t kInvalid = INVALID_SOCKET;
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using socket_t = int;
static constexpr socket_t kInvalid = -1;
#endif
namespace {
void closesock(socket_t s) {
#ifdef _WIN32
closesocket(s);
#else
close(s);
#endif
}
bool send_all(socket_t s, const char* data, size_t len) {
size_t sent = 0;
while (sent < len) {
#ifdef _WIN32
const int n = ::send(s, data + sent, static_cast<int>(len - sent), 0);
#else
const ssize_t n = ::send(s, data + sent, len - sent, 0);
#endif
if (n <= 0) return false;
sent += static_cast<size_t>(n);
}
return true;
}
bool recv_some(socket_t s, std::string& buf, size_t max_extra = 64 * 1024) {
char tmp[4096];
#ifdef _WIN32
const int n = ::recv(s, tmp, sizeof(tmp), 0);
#else
const ssize_t n = ::recv(s, tmp, sizeof(tmp), 0);
#endif
if (n <= 0) return false;
if (buf.size() + static_cast<size_t>(n) > max_extra) return false;
buf.append(tmp, static_cast<size_t>(n));
return true;
}
std::string to_lower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
struct HttpRequest {
std::string method;
std::string path;
std::string query;
std::map<std::string, std::string> headers;
std::string body;
};
bool parse_request(const std::string& raw, HttpRequest& out) {
const auto pos = raw.find("\r\n\r\n");
if (pos == std::string::npos) return false;
const std::string head = raw.substr(0, pos);
out.body = raw.substr(pos + 4);
std::istringstream iss(head);
std::string line;
if (!std::getline(iss, line)) return false;
if (!line.empty() && line.back() == '\r') line.pop_back();
{
std::istringstream ls(line);
std::string url;
ls >> out.method >> url;
const auto q = url.find('?');
if (q == std::string::npos) {
out.path = url;
} else {
out.path = url.substr(0, q);
out.query = url.substr(q + 1);
}
}
while (std::getline(iss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
const auto c = line.find(':');
if (c == std::string::npos) continue;
auto key = to_lower(line.substr(0, c));
auto val = line.substr(c + 1);
while (!val.empty() && (val.front() == ' ' || val.front() == '\t')) val.erase(val.begin());
out.headers[key] = val;
}
return true;
}
std::map<std::string, std::string> parse_query(const std::string& q) {
std::map<std::string, std::string> m;
size_t i = 0;
while (i < q.size()) {
size_t amp = q.find('&', i);
if (amp == std::string::npos) amp = q.size();
auto part = q.substr(i, amp - i);
const auto eq = part.find('=');
if (eq == std::string::npos) m[part] = "";
else m[part.substr(0, eq)] = part.substr(eq + 1);
i = amp + 1;
}
return m;
}
std::string url_decode(const std::string& s) {
std::string o;
o.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '%' && i + 2 < s.size()) {
const auto hex = s.substr(i + 1, 2);
char* end = nullptr;
const long v = std::strtol(hex.c_str(), &end, 16);
if (end && *end == 0) {
o.push_back(static_cast<char>(v));
i += 2;
continue;
}
} else if (s[i] == '+') {
o.push_back(' ');
continue;
}
o.push_back(s[i]);
}
return o;
}
std::string http_response(int status, const std::string& status_text, const std::string& body,
const std::string& content_type = "application/json; charset=utf-8") {
std::ostringstream oss;
oss << "HTTP/1.1 " << status << " " << status_text << "\r\n";
oss << "Content-Type: " << content_type << "\r\n";
oss << "Content-Length: " << body.size() << "\r\n";
oss << "Connection: close\r\n";
oss << "Access-Control-Allow-Origin: *\r\n";
oss << "\r\n";
oss << body;
return oss.str();
}
bool is_ws_upgrade(const HttpRequest& req) {
auto it = req.headers.find("upgrade");
if (it == req.headers.end()) return false;
return to_lower(it->second).find("websocket") != std::string::npos;
}
bool send_ws_text(socket_t s, const std::string& text) {
// server->client: unmasked
std::vector<uint8_t> frame;
frame.push_back(0x81); // FIN + text
const size_t n = text.size();
if (n < 126) {
frame.push_back(static_cast<uint8_t>(n));
} else if (n <= 0xFFFF) {
frame.push_back(126);
frame.push_back(static_cast<uint8_t>((n >> 8) & 0xFF));
frame.push_back(static_cast<uint8_t>(n & 0xFF));
} else {
frame.push_back(127);
for (int i = 7; i >= 0; --i) frame.push_back(static_cast<uint8_t>((n >> (8 * i)) & 0xFF));
}
frame.insert(frame.end(), text.begin(), text.end());
return send_all(s, reinterpret_cast<const char*>(frame.data()), frame.size());
}
void ws_loop(socket_t s) {
std::vector<uint8_t> buf;
buf.reserve(8192);
while (true) {
char tmp[4096];
#ifdef _WIN32
const int n = ::recv(s, tmp, sizeof(tmp), 0);
#else
const ssize_t n = ::recv(s, tmp, sizeof(tmp), 0);
#endif
if (n <= 0) break;
buf.insert(buf.end(), tmp, tmp + n);
while (buf.size() >= 2) {
const uint8_t b0 = buf[0];
const uint8_t b1 = buf[1];
const bool fin = (b0 & 0x80) != 0;
const uint8_t opcode = b0 & 0x0F;
const bool masked = (b1 & 0x80) != 0;
uint64_t payload_len = b1 & 0x7F;
size_t header = 2;
if (payload_len == 126) {
if (buf.size() < 4) break;
payload_len = (static_cast<uint64_t>(buf[2]) << 8) | buf[3];
header = 4;
} else if (payload_len == 127) {
if (buf.size() < 10) break;
payload_len = 0;
for (int i = 0; i < 8; ++i) payload_len = (payload_len << 8) | buf[2 + i];
header = 10;
}
const size_t mask_len = masked ? 4 : 0;
if (buf.size() < header + mask_len + payload_len) break;
std::string payload;
payload.resize(static_cast<size_t>(payload_len));
const size_t data_off = header + mask_len;
if (masked) {
const uint8_t* mask = buf.data() + header;
for (uint64_t i = 0; i < payload_len; ++i)
payload[static_cast<size_t>(i)] = static_cast<char>(buf[data_off + static_cast<size_t>(i)] ^ mask[i % 4]);
} else {
std::memcpy(payload.data(), buf.data() + data_off, static_cast<size_t>(payload_len));
}
buf.erase(buf.begin(), buf.begin() + static_cast<std::ptrdiff_t>(header + mask_len + payload_len));
if (opcode == 0x8) { // close
return;
}
if (opcode == 0x9) { // ping -> pong
std::vector<uint8_t> pong;
pong.push_back(0x8A);
pong.push_back(static_cast<uint8_t>(payload.size()));
pong.insert(pong.end(), payload.begin(), payload.end());
send_all(s, reinterpret_cast<const char*>(pong.data()), pong.size());
continue;
}
if (opcode == 0x1 && fin) {
// echo hello ack for app-level handshake
try {
auto j = nlohmann::json::parse(payload);
if (j.contains("op") && j["op"] == "hello") {
nlohmann::json ack{{"op", "hello_ack"}, {"ok", true}};
send_ws_text(s, ack.dump());
} else if (j.contains("op") && j["op"] == "ping") {
send_ws_text(s, nlohmann::json{{"op", "pong"}}.dump());
}
} catch (...) {
// ignore non-json
}
}
}
}
}
} // namespace
HttpWsServer::HttpWsServer(db::Database& db, std::string host, uint16_t port)
: db_(db), host_(std::move(host)), port_(port) {}
HttpWsServer::~HttpWsServer() { stop(); }
bool HttpWsServer::start() {
#ifdef _WIN32
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
#endif
listen_sock_ = static_cast<uintptr_t>(::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
auto s = static_cast<socket_t>(listen_sock_);
if (s == kInvalid) return false;
int yes = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(port_);
if (host_ == "0.0.0.0" || host_.empty()) {
addr.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
inet_pton(AF_INET, host_.c_str(), &addr.sin_addr);
}
if (bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
closesock(s);
listen_sock_ = static_cast<uintptr_t>(-1);
return false;
}
if (listen(s, 128) != 0) {
closesock(s);
listen_sock_ = static_cast<uintptr_t>(-1);
return false;
}
running_ = true;
accept_thread_ = std::thread([this] { accept_loop(); });
return true;
}
void HttpWsServer::stop() {
running_ = false;
if (listen_sock_ != static_cast<uintptr_t>(-1)) {
closesock(static_cast<socket_t>(listen_sock_));
listen_sock_ = static_cast<uintptr_t>(-1);
}
if (accept_thread_.joinable()) accept_thread_.join();
#ifdef _WIN32
// WSACleanup 可能被多次调用;此处简单处理
#endif
}
void HttpWsServer::accept_loop() {
auto ls = static_cast<socket_t>(listen_sock_);
while (running_) {
sockaddr_in cli{};
#ifdef _WIN32
int len = sizeof(cli);
#else
socklen_t len = sizeof(cli);
#endif
socket_t cs = ::accept(ls, reinterpret_cast<sockaddr*>(&cli), &len);
if (cs == kInvalid) {
if (!running_) break;
continue;
}
std::thread([this, cs] {
handle_client(static_cast<uintptr_t>(cs));
closesock(cs);
}).detach();
}
}
void HttpWsServer::handle_client(uintptr_t client_sock) {
auto s = static_cast<socket_t>(client_sock);
std::string raw;
// read headers
while (raw.find("\r\n\r\n") == std::string::npos) {
if (!recv_some(s, raw, 256 * 1024)) return;
}
HttpRequest req;
if (!parse_request(raw, req)) {
auto resp = http_response(400, "Bad Request", "{\"error\":\"bad_request\"}");
send_all(s, resp.data(), resp.size());
return;
}
// read remaining body by Content-Length
auto cl_it = req.headers.find("content-length");
size_t content_length = 0;
if (cl_it != req.headers.end()) content_length = static_cast<size_t>(std::strtoull(cl_it->second.c_str(), nullptr, 10));
while (req.body.size() < content_length) {
if (!recv_some(s, raw, 1024 * 1024)) return;
const auto pos = raw.find("\r\n\r\n");
req.body = raw.substr(pos + 4);
}
if (content_length > 0 && req.body.size() > content_length) req.body.resize(content_length);
// WebSocket upgrade
if (req.path == "/ws" && is_ws_upgrade(req)) {
auto q = parse_query(req.query);
const std::string token = url_decode(q.count("token") ? q["token"] : "");
int64_t pid = 0;
if (q.count("pid") && !q["pid"].empty()) pid = std::strtoll(q["pid"].c_str(), nullptr, 10);
if (!auth::validate_ws(db_, pid, token)) {
auto resp = http_response(401, "Unauthorized", "{\"error\":\"unauthorized\"}");
send_all(s, resp.data(), resp.size());
return;
}
auto key_it = req.headers.find("sec-websocket-key");
if (key_it == req.headers.end()) {
auto resp = http_response(400, "Bad Request", "{\"error\":\"missing_ws_key\"}");
send_all(s, resp.data(), resp.size());
return;
}
const auto accept = crypto::websocket_accept_key(key_it->second);
std::ostringstream oss;
oss << "HTTP/1.1 101 Switching Protocols\r\n"
<< "Upgrade: websocket\r\n"
<< "Connection: Upgrade\r\n"
<< "Sec-WebSocket-Accept: " << accept << "\r\n"
<< "\r\n";
const auto resp = oss.str();
if (!send_all(s, resp.data(), resp.size())) return;
std::printf("[ws] open pid=%lld\n", static_cast<long long>(pid));
ws_loop(s);
std::printf("[ws] close pid=%lld\n", static_cast<long long>(pid));
return;
}
// HTTP API
if (req.method == "POST" && (req.path == "/register" || req.path == "/login")) {
std::string username, password;
try {
auto j = nlohmann::json::parse(req.body.empty() ? "{}" : req.body);
username = j.value("username", "");
password = j.value("password", "");
} catch (...) {
auto resp = http_response(400, "Bad Request", "{\"error\":\"invalid_input\"}");
send_all(s, resp.data(), resp.size());
return;
}
auth::Result r =
(req.path == "/register") ? auth::register_user(db_, username, password) : auth::login_user(db_, username, password);
const char* st = r.http_status == 200 ? "OK"
: r.http_status == 400 ? "Bad Request"
: r.http_status == 401 ? "Unauthorized"
: r.http_status == 409 ? "Conflict"
: "Error";
auto resp = http_response(r.http_status, st, r.body);
send_all(s, resp.data(), resp.size());
return;
}
if (req.method == "GET" && req.path == "/health") {
auto resp = http_response(200, "OK", "{\"ok\":true,\"service\":\"aiserver\"}");
send_all(s, resp.data(), resp.size());
return;
}
auto resp = http_response(404, "Not Found", "{\"error\":\"not_found\"}");
send_all(s, resp.data(), resp.size());
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "db.hpp"
#include <atomic>
#include <cstdint>
#include <string>
#include <thread>
class HttpWsServer {
public:
HttpWsServer(db::Database& db, std::string host, uint16_t port);
~HttpWsServer();
bool start();
void stop();
bool running() const { return running_; }
private:
void accept_loop();
void handle_client(uintptr_t client_sock);
db::Database& db_;
std::string host_;
uint16_t port_;
std::atomic<bool> running_{false};
uintptr_t listen_sock_ = static_cast<uintptr_t>(-1);
std::thread accept_thread_;
};
+70
View File
@@ -0,0 +1,70 @@
#include "db.hpp"
#include "http_ws_server.hpp"
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <string>
#include <thread>
namespace fs = std::filesystem;
static void usage(const char* argv0) {
std::printf("Usage: %s [--host 0.0.0.0] [--port 8080] [--db data/aiserver.db] [--daemon]\n", argv0);
}
int main(int argc, char** argv) {
std::string host = "0.0.0.0";
uint16_t port = 8080;
std::string db_path = "data/aiserver.db";
bool daemon = false;
for (int i = 1; i < argc; ++i) {
const std::string a = argv[i];
if (a == "--host" && i + 1 < argc) host = argv[++i];
else if (a == "--port" && i + 1 < argc) port = static_cast<uint16_t>(std::stoi(argv[++i]));
else if (a == "--db" && i + 1 < argc) db_path = argv[++i];
else if (a == "--daemon") daemon = true;
else if (a == "-h" || a == "--help") {
usage(argv[0]);
return 0;
} else {
std::printf("Unknown arg: %s\n", a.c_str());
usage(argv[0]);
return 1;
}
}
try {
fs::create_directories(fs::path(db_path).parent_path());
} catch (...) {
}
db::Database database(db_path);
if (!database.open_ok()) {
std::printf("DB open failed: %s path=%s\n", database.last_error().c_str(), db_path.c_str());
return 1;
}
HttpWsServer server(database, host, port);
if (!server.start()) {
std::printf("Listen failed on %s:%u\n", host.c_str(), static_cast<unsigned>(port));
return 1;
}
std::printf("AIServer listening http://%s:%u (POST /register /login, WS /ws?pid=&token=)\n",
host.c_str(), static_cast<unsigned>(port));
std::printf("DB=%s\n", db_path.c_str());
if (daemon) {
std::printf("daemon mode: running until process killed\n");
while (server.running()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
} else {
std::printf("Press Enter to stop...\n");
std::getchar();
server.stop();
}
return 0;
}
+10255
View File
File diff suppressed because it is too large Load Diff
+24765
View File
File diff suppressed because it is too large Load Diff
+257679
View File
File diff suppressed because it is too large Load Diff
+13425
View File
File diff suppressed because it is too large Load Diff