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:
@@ -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
|
||||
)
|
||||
@@ -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 表)。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+10255
File diff suppressed because it is too large
Load Diff
Vendored
+24765
File diff suppressed because it is too large
Load Diff
Vendored
+257679
File diff suppressed because it is too large
Load Diff
Vendored
+13425
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user