ui布局自适应
This commit is contained in:
1 parent
98f44890f8
commit
3587129b90
40 files changed
+929
-142
No files matched your search
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "config.hpp"
|
||||
#include "connector_service.hpp"
|
||||
#include "local_certificate.hpp"
|
||||
#include "platform.hpp"
|
||||
|
||||
using connector::ConnectorConfig;
|
||||
@@ -15,6 +16,7 @@ using connector::ConnectorService;
|
||||
@property(nonatomic, strong) NSTextField* statusLabel;
|
||||
@property(nonatomic, strong) NSArray<NSView*>* pathControls;
|
||||
@property(nonatomic, strong) NSButton* connectionButton;
|
||||
@property(nonatomic, strong) NSButton* safariButton;
|
||||
@property(nonatomic, assign) ConnectorService* service;
|
||||
@property(nonatomic, copy) NSString* startupError;
|
||||
@end
|
||||
@@ -72,6 +74,9 @@ using connector::ConnectorService;
|
||||
self.pathControls = @[pathLabel, self.pathField, browse, automatic, save];
|
||||
self.connectionButton = [self button:@"断开连接" frame:NSMakeRect(460, 25, 130, 36) action:@selector(toggleConnection:)];
|
||||
[content addSubview:self.connectionButton];
|
||||
self.safariButton = [self button:@"启用 Safari 支持" frame:NSMakeRect(315, 25, 130, 36) action:@selector(enableSafari:)];
|
||||
self.safariButton.toolTip = @"安装仅用于 127.0.0.1 的本地证书,使 Safari 可以访问 HTTPS 连接器。";
|
||||
[content addSubview:self.safariButton];
|
||||
[self refreshStatus:nil];
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
@@ -82,6 +87,9 @@ using connector::ConnectorService;
|
||||
const BOOL ownsPort = self.service && self.service->running();
|
||||
const BOOL portOccupied = ownsPort || connector::connector_port_in_use();
|
||||
self.connectionButton.title = portOccupied ? @"停止连接" : @"启动连接";
|
||||
const BOOL safariTrusted = connector::local_certificate_trusted();
|
||||
self.safariButton.title = safariTrusted ? @"Safari 支持已启用" : @"启用 Safari 支持";
|
||||
self.safariButton.enabled = !safariTrusted;
|
||||
if (self.startupError.length > 0 && !portOccupied) {
|
||||
self.statusLabel.stringValue = [@"● 连接器未启动 · " stringByAppendingString:self.startupError];
|
||||
self.statusLabel.textColor = NSColor.systemRedColor;
|
||||
@@ -121,6 +129,34 @@ using connector::ConnectorService;
|
||||
[self refreshStatus:nil];
|
||||
}
|
||||
|
||||
- (void)enableSafari:(id)sender {
|
||||
(void)sender;
|
||||
NSAlert* confirmation = [[NSAlert alloc] init];
|
||||
confirmation.messageText = @"启用 Safari 本机连接";
|
||||
confirmation.informativeText = @"连接器会把一个仅用于 127.0.0.1 的本地证书加入当前用户的 SSL 信任列表。系统可能要求你确认。";
|
||||
[confirmation addButtonWithTitle:@"继续"];
|
||||
[confirmation addButtonWithTitle:@"取消"];
|
||||
if ([confirmation runModal] != NSAlertFirstButtonReturn) return;
|
||||
|
||||
std::string error;
|
||||
if (!connector::trust_local_certificate(error)) {
|
||||
NSAlert* alert = [[NSAlert alloc] init];
|
||||
alert.messageText = @"无法启用 Safari 支持";
|
||||
alert.informativeText = [NSString stringWithUTF8String:error.c_str()];
|
||||
[alert runModal];
|
||||
return;
|
||||
}
|
||||
if (self.service && self.service->running() && !self.service->secure_running()) {
|
||||
self.service->stop();
|
||||
}
|
||||
if (self.service && !self.service->running()) {
|
||||
std::string startError;
|
||||
self.service->start(startError);
|
||||
self.startupError = [NSString stringWithUTF8String:startError.c_str()];
|
||||
}
|
||||
[self refreshStatus:nil];
|
||||
}
|
||||
|
||||
- (void)selectSpine:(id)sender {
|
||||
(void)sender;
|
||||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
#include "encoding.hpp"
|
||||
#include "json.hpp"
|
||||
#include "platform.hpp"
|
||||
#ifdef __APPLE__
|
||||
#include "local_certificate.hpp"
|
||||
#endif
|
||||
#include "workspace.hpp"
|
||||
#include "zip.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
@@ -20,6 +24,7 @@ namespace {
|
||||
|
||||
constexpr const char* kVersion = "2.0.0";
|
||||
constexpr unsigned short kPort = 27843;
|
||||
constexpr unsigned short kSecurePort = 27844;
|
||||
std::mutex pairing_mutex;
|
||||
struct Pairing { std::string origin; std::chrono::steady_clock::time_point expires; };
|
||||
std::map<std::string, Pairing> pairing;
|
||||
@@ -125,10 +130,27 @@ std::pair<std::vector<std::uint8_t>, std::string> convert(const Json& payload, c
|
||||
} // namespace
|
||||
|
||||
bool ConnectorService::start(std::string& error) {
|
||||
return server_.start(kPort, [this](const HttpRequest& request) { return handle(request); }, error);
|
||||
if (server_.running()) return true;
|
||||
if (!server_.start(kPort, [this](const HttpRequest& request) { return handle(request); }, error)) return false;
|
||||
#ifdef __APPLE__
|
||||
std::string certificate_error;
|
||||
if (!ensure_local_certificate(certificate_error)) {
|
||||
secure_error_ = certificate_error;
|
||||
std::cerr << "Safari HTTPS certificate error: " << secure_error_ << '\n';
|
||||
} else if (!secure_server_.start(
|
||||
kSecurePort, [this](const HttpRequest& request) { return handle(request); }, secure_error_,
|
||||
local_identity_path())) {
|
||||
// HTTP remains available for existing browsers even if the optional Safari endpoint cannot start.
|
||||
std::cerr << "Safari HTTPS server error: " << secure_error_ << '\n';
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConnectorService::stop() { server_.stop(); }
|
||||
void ConnectorService::stop() {
|
||||
secure_server_.stop();
|
||||
server_.stop();
|
||||
}
|
||||
|
||||
HttpResponse ConnectorService::handle(const HttpRequest& request) {
|
||||
const std::string origin = header(request, "origin");
|
||||
@@ -142,13 +164,21 @@ HttpResponse ConnectorService::handle(const HttpRequest& request) {
|
||||
if (request.method == "GET" && request.path == "/v1/health") {
|
||||
std::lock_guard<std::mutex> lock(config_mutex_);
|
||||
Json::Array versions{Json("3.8"), Json("4.0"), Json("4.1"), Json("4.2"), Json("4.3")};
|
||||
return json_response(200, {
|
||||
Json::Object health = {
|
||||
{"connectorVersion", Json(kVersion)},
|
||||
{"authorized", Json(origin_allowed(origin))},
|
||||
{"spineFound", Json(!find_spine_executable().empty())},
|
||||
{"secureAvailable", Json(secure_server_.running())},
|
||||
{"platform", Json(platform_name())},
|
||||
{"supportedVersions", Json(std::move(versions))},
|
||||
}, origin);
|
||||
};
|
||||
if (!origin_allowed(origin)) {
|
||||
const std::string base = request.secure
|
||||
? "https://127.0.0.1:" + std::to_string(kSecurePort)
|
||||
: "http://127.0.0.1:" + std::to_string(kPort);
|
||||
health["pairUrl"] = Json(base + "/pair?origin=" + url_encode(origin));
|
||||
}
|
||||
return json_response(200, std::move(health), origin);
|
||||
}
|
||||
if (request.method == "GET" && request.path == "/pair") {
|
||||
const auto found = request.query.find("origin");
|
||||
@@ -184,7 +214,10 @@ HttpResponse ConnectorService::handle(const HttpRequest& request) {
|
||||
if (!origin_allowed(origin)) {
|
||||
HttpResponse response = error_response(403, "ORIGIN_NOT_ALLOWED", "需要先授权当前网站使用本地 Spine 连接器", origin);
|
||||
Json body = Json::parse(response.body);
|
||||
body["pairUrl"] = Json("http://127.0.0.1:27843/pair?origin=" + url_encode(origin));
|
||||
const std::string base = request.secure
|
||||
? "https://127.0.0.1:" + std::to_string(kSecurePort)
|
||||
: "http://127.0.0.1:" + std::to_string(kPort);
|
||||
body["pairUrl"] = Json(base + "/pair?origin=" + url_encode(origin));
|
||||
response.body = body.dump();
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,13 @@ class ConnectorService {
|
||||
bool start(std::string& error);
|
||||
void stop();
|
||||
bool running() const { return server_.running(); }
|
||||
bool secure_running() const { return secure_server_.running(); }
|
||||
|
||||
private:
|
||||
HttpResponse handle(const HttpRequest& request);
|
||||
HttpServer server_;
|
||||
HttpServer secure_server_;
|
||||
std::string secure_error_;
|
||||
std::mutex config_mutex_;
|
||||
std::mutex conversion_mutex_;
|
||||
};
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
#include "encoding.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <winsock2.h>
|
||||
@@ -19,6 +26,11 @@ constexpr Socket kInvalidSocket = INVALID_SOCKET;
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __APPLE__
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/SecImportExport.h>
|
||||
#include <Security/SecureTransport.h>
|
||||
#endif
|
||||
using Socket = int;
|
||||
constexpr Socket kInvalidSocket = -1;
|
||||
#endif
|
||||
@@ -37,6 +49,119 @@ void close_socket(Socket socket) {
|
||||
#endif
|
||||
}
|
||||
|
||||
struct ConnectionStream {
|
||||
Socket socket = kInvalidSocket;
|
||||
#ifdef __APPLE__
|
||||
SSLContextRef tls = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
OSStatus socket_read(SSLConnectionRef reference, void* data, std::size_t* length) {
|
||||
const Socket socket = *static_cast<const Socket*>(reference);
|
||||
const long count = recv(socket, static_cast<char*>(data), static_cast<int>(*length), 0);
|
||||
if (count > 0) {
|
||||
*length = static_cast<std::size_t>(count);
|
||||
return noErr;
|
||||
}
|
||||
*length = 0;
|
||||
if (count == 0) return errSSLClosedGraceful;
|
||||
return errno == EAGAIN || errno == EWOULDBLOCK ? errSSLWouldBlock : errSSLInternal;
|
||||
}
|
||||
|
||||
OSStatus socket_write(SSLConnectionRef reference, const void* data, std::size_t* length) {
|
||||
const Socket socket = *static_cast<const Socket*>(reference);
|
||||
const long count = send(socket, static_cast<const char*>(data), static_cast<int>(*length), 0);
|
||||
if (count > 0) {
|
||||
*length = static_cast<std::size_t>(count);
|
||||
return noErr;
|
||||
}
|
||||
*length = 0;
|
||||
return errno == EAGAIN || errno == EWOULDBLOCK ? errSSLWouldBlock : errSSLInternal;
|
||||
}
|
||||
|
||||
void* load_identity(const std::filesystem::path& path, std::string& error) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) {
|
||||
error = "无法读取 HTTPS 身份证书";
|
||||
return nullptr;
|
||||
}
|
||||
const std::string bytes((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
||||
CFDataRef data = CFDataCreate(kCFAllocatorDefault, reinterpret_cast<const UInt8*>(bytes.data()),
|
||||
static_cast<CFIndex>(bytes.size()));
|
||||
const void* keys[] = {kSecImportExportPassphrase};
|
||||
const void* values[] = {CFSTR("spine-connector-local")};
|
||||
CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1,
|
||||
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFArrayRef items = nullptr;
|
||||
const OSStatus status = SecPKCS12Import(data, options, &items);
|
||||
CFRelease(options);
|
||||
CFRelease(data);
|
||||
if (status != errSecSuccess || !items || CFArrayGetCount(items) == 0) {
|
||||
if (items) CFRelease(items);
|
||||
error = "无法读取 HTTPS 身份证书";
|
||||
return nullptr;
|
||||
}
|
||||
CFDictionaryRef item = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(items, 0));
|
||||
CFTypeRef identity = CFDictionaryGetValue(item, kSecImportItemIdentity);
|
||||
if (identity) CFRetain(identity);
|
||||
CFRelease(items);
|
||||
if (!identity) error = "HTTPS 身份证书缺少私钥";
|
||||
return const_cast<void*>(identity);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool open_stream(ConnectionStream& stream, void* identity, std::string& error) {
|
||||
if (!identity) return true;
|
||||
#ifdef __APPLE__
|
||||
SSLContextRef context = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
|
||||
if (!context) {
|
||||
error = "无法创建 HTTPS 服务";
|
||||
return false;
|
||||
}
|
||||
SSLSetIOFuncs(context, socket_read, socket_write);
|
||||
SSLSetConnection(context, &stream.socket);
|
||||
SSLSetProtocolVersionMin(context, kTLSProtocol12);
|
||||
const void* certificates[] = {identity};
|
||||
CFArrayRef certificate_array = CFArrayCreate(kCFAllocatorDefault, certificates, 1, &kCFTypeArrayCallBacks);
|
||||
const OSStatus certificate_status = SSLSetCertificate(context, certificate_array);
|
||||
CFRelease(certificate_array);
|
||||
if (certificate_status != noErr) {
|
||||
CFRelease(context);
|
||||
error = "无法配置 HTTPS 身份证书";
|
||||
return false;
|
||||
}
|
||||
OSStatus status;
|
||||
do {
|
||||
status = SSLHandshake(context);
|
||||
} while (status == errSSLWouldBlock);
|
||||
if (status != noErr) {
|
||||
CFRelease(context);
|
||||
return false;
|
||||
}
|
||||
stream.tls = context;
|
||||
return true;
|
||||
#else
|
||||
(void)stream;
|
||||
error = "当前平台不支持 HTTPS 连接器";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void close_stream(ConnectionStream& stream) {
|
||||
#ifdef __APPLE__
|
||||
if (stream.tls) {
|
||||
SSLClose(stream.tls);
|
||||
CFRelease(stream.tls);
|
||||
stream.tls = nullptr;
|
||||
}
|
||||
#endif
|
||||
if (stream.socket != kInvalidSocket) {
|
||||
close_socket(stream.socket);
|
||||
stream.socket = kInvalidSocket;
|
||||
}
|
||||
}
|
||||
|
||||
std::string lowercase(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return value;
|
||||
@@ -56,21 +181,49 @@ std::string status_text(int status) {
|
||||
}
|
||||
}
|
||||
|
||||
void send_all(Socket socket, const std::string& data) {
|
||||
int read_stream(ConnectionStream& stream, char* buffer, int length) {
|
||||
#ifdef __APPLE__
|
||||
if (stream.tls) {
|
||||
std::size_t received = 0;
|
||||
const OSStatus status = SSLRead(stream.tls, buffer, static_cast<std::size_t>(length), &received);
|
||||
if (status == errSSLClosedGraceful || status == errSSLClosedNoNotify) return static_cast<int>(received);
|
||||
if (status != noErr && status != errSSLWouldBlock) return -1;
|
||||
return static_cast<int>(received);
|
||||
}
|
||||
#endif
|
||||
return recv(stream.socket, buffer, length, 0);
|
||||
}
|
||||
|
||||
void send_all(ConnectionStream& stream, const std::string& data) {
|
||||
std::size_t sent = 0;
|
||||
while (sent < data.size()) {
|
||||
const int count = send(socket, data.data() + sent, static_cast<int>((std::min)(data.size() - sent, static_cast<std::size_t>(1 << 20))), 0);
|
||||
int count;
|
||||
#ifdef __APPLE__
|
||||
if (stream.tls) {
|
||||
std::size_t written = 0;
|
||||
const OSStatus status = SSLWrite(stream.tls, data.data() + sent,
|
||||
(std::min)(data.size() - sent, static_cast<std::size_t>(1 << 20)), &written);
|
||||
if (status != noErr && status != errSSLWouldBlock) return;
|
||||
count = static_cast<int>(written);
|
||||
} else {
|
||||
count = send(stream.socket, data.data() + sent,
|
||||
static_cast<int>((std::min)(data.size() - sent, static_cast<std::size_t>(1 << 20))), 0);
|
||||
}
|
||||
#else
|
||||
count = send(stream.socket, data.data() + sent,
|
||||
static_cast<int>((std::min)(data.size() - sent, static_cast<std::size_t>(1 << 20))), 0);
|
||||
#endif
|
||||
if (count <= 0) return;
|
||||
sent += static_cast<std::size_t>(count);
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_request(Socket socket, HttpRequest& request, int& error_status) {
|
||||
bool parse_request(ConnectionStream& stream, HttpRequest& request, int& error_status) {
|
||||
std::string buffer;
|
||||
char chunk[8192];
|
||||
std::size_t header_end = std::string::npos;
|
||||
while ((header_end = buffer.find("\r\n\r\n")) == std::string::npos) {
|
||||
const int count = recv(socket, chunk, sizeof(chunk), 0);
|
||||
const int count = read_stream(stream, chunk, sizeof(chunk));
|
||||
if (count <= 0) return false;
|
||||
buffer.append(chunk, count);
|
||||
if (buffer.size() > kMaxHeaders) { error_status = 413; return false; }
|
||||
@@ -109,7 +262,7 @@ bool parse_request(Socket socket, HttpRequest& request, int& error_status) {
|
||||
if (content_length > kMaxBody) { error_status = 413; return false; }
|
||||
request.body = buffer.substr(header_end + 4);
|
||||
while (request.body.size() < content_length) {
|
||||
const int count = recv(socket, chunk, sizeof(chunk), 0);
|
||||
const int count = read_stream(stream, chunk, sizeof(chunk));
|
||||
if (count <= 0) return false;
|
||||
request.body.append(chunk, count);
|
||||
}
|
||||
@@ -117,11 +270,20 @@ bool parse_request(Socket socket, HttpRequest& request, int& error_status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void serve_client(Socket client, const RequestHandler& handler) {
|
||||
void serve_client(Socket client, void* identity, const RequestHandler& handler) {
|
||||
ConnectionStream stream{client};
|
||||
std::string tls_error;
|
||||
if (!open_stream(stream, identity, tls_error)) {
|
||||
close_stream(stream);
|
||||
return;
|
||||
}
|
||||
HttpRequest request;
|
||||
int error_status = 400;
|
||||
HttpResponse response;
|
||||
if (parse_request(client, request, error_status)) {
|
||||
if (parse_request(stream, request, error_status)) {
|
||||
#ifdef __APPLE__
|
||||
request.secure = stream.tls != nullptr;
|
||||
#endif
|
||||
try { response = handler(request); }
|
||||
catch (const std::exception& error) {
|
||||
response.status = 500;
|
||||
@@ -138,9 +300,9 @@ void serve_client(Socket client, const RequestHandler& handler) {
|
||||
output << "HTTP/1.1 " << response.status << ' ' << status_text(response.status) << "\r\n";
|
||||
for (const auto& [name, value] : response.headers) output << name << ": " << value << "\r\n";
|
||||
output << "\r\n";
|
||||
send_all(client, output.str());
|
||||
send_all(client, response.body);
|
||||
close_socket(client);
|
||||
send_all(stream, output.str());
|
||||
send_all(stream, response.body);
|
||||
close_stream(stream);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -148,8 +310,21 @@ void serve_client(Socket client, const RequestHandler& handler) {
|
||||
HttpServer::HttpServer() = default;
|
||||
HttpServer::~HttpServer() { stop(); }
|
||||
|
||||
bool HttpServer::start(unsigned short port, RequestHandler handler, std::string& error) {
|
||||
bool HttpServer::start(unsigned short port, RequestHandler handler, std::string& error,
|
||||
const std::filesystem::path& identity) {
|
||||
if (running_) return true;
|
||||
#ifdef __APPLE__
|
||||
if (!identity.empty()) {
|
||||
void* loaded = load_identity(identity, error);
|
||||
if (loaded) tls_identity_ = std::shared_ptr<void>(loaded, [](void* value) { CFRelease(value); });
|
||||
if (!tls_identity_) return false;
|
||||
}
|
||||
#else
|
||||
if (!identity.empty()) {
|
||||
error = "当前平台不支持 HTTPS 连接器";
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
WSADATA data{};
|
||||
if (WSAStartup(MAKEWORD(2, 2), &data) != 0) { error = "无法初始化网络服务"; return false; }
|
||||
@@ -182,7 +357,9 @@ void HttpServer::accept_loop() {
|
||||
if (running_) continue;
|
||||
break;
|
||||
}
|
||||
std::thread(serve_client, client, handler_).detach();
|
||||
std::thread([client, identity = tls_identity_, handler = handler_] {
|
||||
serve_client(client, identity.get(), handler);
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,3 +383,7 @@ void HttpServer::stop() {
|
||||
}
|
||||
|
||||
} // namespace connector
|
||||
|
||||
#ifdef __APPLE__
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace connector {
|
||||
@@ -15,6 +17,7 @@ struct HttpRequest {
|
||||
std::map<std::string, std::string> query;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
bool secure = false;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
@@ -29,7 +32,8 @@ class HttpServer {
|
||||
public:
|
||||
HttpServer();
|
||||
~HttpServer();
|
||||
bool start(unsigned short port, RequestHandler handler, std::string& error);
|
||||
bool start(unsigned short port, RequestHandler handler, std::string& error,
|
||||
const std::filesystem::path& identity = {});
|
||||
void stop();
|
||||
bool running() const { return running_.load(); }
|
||||
|
||||
@@ -37,6 +41,7 @@ class HttpServer {
|
||||
void accept_loop();
|
||||
std::atomic<bool> running_{false};
|
||||
std::intptr_t socket_ = -1;
|
||||
std::shared_ptr<void> tls_identity_;
|
||||
RequestHandler handler_;
|
||||
void* thread_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace connector {
|
||||
|
||||
std::filesystem::path local_identity_path();
|
||||
bool ensure_local_certificate(std::string& error);
|
||||
bool local_certificate_trusted();
|
||||
bool trust_local_certificate(std::string& error);
|
||||
|
||||
} // namespace connector
|
||||
@@ -0,0 +1,178 @@
|
||||
#ifdef __APPLE__
|
||||
|
||||
#include "local_certificate.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "encoding.hpp"
|
||||
#include "platform.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace connector {
|
||||
namespace {
|
||||
|
||||
struct CertificatePaths {
|
||||
std::filesystem::path directory;
|
||||
std::filesystem::path ca_key;
|
||||
std::filesystem::path ca;
|
||||
std::filesystem::path server_key;
|
||||
std::filesystem::path server_certificate;
|
||||
std::filesystem::path identity;
|
||||
std::filesystem::path request;
|
||||
std::filesystem::path extensions;
|
||||
};
|
||||
|
||||
CertificatePaths paths() {
|
||||
const auto directory = config_path().parent_path() / "tls";
|
||||
return {
|
||||
directory,
|
||||
directory / "ca-key.pem",
|
||||
directory / "ca.pem",
|
||||
directory / "server-key.pem",
|
||||
directory / "server.pem",
|
||||
directory / "identity.p12",
|
||||
directory / "server.csr",
|
||||
directory / "server.ext",
|
||||
};
|
||||
}
|
||||
|
||||
std::string process_error(const ProcessResult& result) {
|
||||
std::string message = trim(result.output);
|
||||
return message.empty() ? "OpenSSL 执行失败" : message;
|
||||
}
|
||||
|
||||
bool run_openssl(const std::vector<std::string>& arguments, std::string& error) {
|
||||
const ProcessResult result = run_process("/usr/bin/openssl", arguments, {}, 30);
|
||||
if (result.timed_out) {
|
||||
error = "生成本机 HTTPS 证书超时";
|
||||
return false;
|
||||
}
|
||||
if (result.exit_code != 0) {
|
||||
error = process_error(result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool certificate_is_usable(const std::filesystem::path& certificate) {
|
||||
std::error_code file_error;
|
||||
if (!std::filesystem::is_regular_file(certificate, file_error)) return false;
|
||||
const ProcessResult result = run_process(
|
||||
"/usr/bin/openssl", {"x509", "-checkend", "2592000", "-noout", "-in", certificate.string()}, {}, 10);
|
||||
return !result.timed_out && result.exit_code == 0;
|
||||
}
|
||||
|
||||
bool identity_is_usable(const std::filesystem::path& identity) {
|
||||
std::error_code error;
|
||||
return std::filesystem::is_regular_file(identity, error);
|
||||
}
|
||||
|
||||
void set_private_permissions(const std::filesystem::path& path) {
|
||||
chmod(path.c_str(), 0600);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path local_identity_path() {
|
||||
return paths().identity;
|
||||
}
|
||||
|
||||
bool ensure_local_certificate(std::string& error) {
|
||||
const CertificatePaths files = paths();
|
||||
if (certificate_is_usable(files.server_certificate) && identity_is_usable(files.identity)) {
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
std::filesystem::create_directories(files.directory);
|
||||
chmod(files.directory.c_str(), 0700);
|
||||
for (const auto& path : {files.ca_key, files.ca, files.server_key, files.server_certificate,
|
||||
files.identity, files.request, files.extensions}) {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(path, ignored);
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string dir = files.directory.string();
|
||||
if (!run_openssl({"genrsa", "-out", files.ca_key.string(), "2048"}, error)
|
||||
|| !run_openssl({"req", "-x509", "-new", "-key", files.ca_key.string(), "-sha256", "-days", "3650",
|
||||
"-subj", "/CN=Spine Particle Connector Local CA", "-out", files.ca.string()}, error)
|
||||
|| !run_openssl({"genrsa", "-out", files.server_key.string(), "2048"}, error)
|
||||
|| !run_openssl({"req", "-new", "-key", files.server_key.string(), "-subj", "/CN=127.0.0.1",
|
||||
"-out", files.request.string()}, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream extensions(files.extensions, std::ios::binary | std::ios::trunc);
|
||||
if (!extensions) {
|
||||
error = "无法写入证书配置";
|
||||
return false;
|
||||
}
|
||||
extensions << "subjectAltName=IP:127.0.0.1,DNS:localhost\n"
|
||||
<< "extendedKeyUsage=serverAuth\n"
|
||||
<< "basicConstraints=CA:FALSE\n";
|
||||
}
|
||||
|
||||
if (!run_openssl({"x509", "-req", "-in", files.request.string(), "-CA", files.ca.string(),
|
||||
"-CAkey", files.ca_key.string(), "-CAcreateserial", "-out", files.server_certificate.string(),
|
||||
"-days", "825", "-sha256", "-extfile", files.extensions.string()}, error)
|
||||
|| !run_openssl({"pkcs12", "-export", "-out", files.identity.string(), "-inkey", files.server_key.string(),
|
||||
"-in", files.server_certificate.string(), "-passout", "pass:spine-connector-local"}, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_private_permissions(files.server_key);
|
||||
set_private_permissions(files.identity);
|
||||
{
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(files.ca_key, ignored);
|
||||
}
|
||||
(void)dir;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool local_certificate_trusted() {
|
||||
const CertificatePaths files = paths();
|
||||
const ProcessResult result = run_process(
|
||||
"/usr/bin/security",
|
||||
{"verify-cert", "-c", files.server_certificate.string(), "-n", "127.0.0.1", "-p", "ssl", "-L", "-q"},
|
||||
{}, 10);
|
||||
return !result.timed_out && result.exit_code == 0;
|
||||
}
|
||||
|
||||
bool trust_local_certificate(std::string& error) {
|
||||
const CertificatePaths files = paths();
|
||||
if (!ensure_local_certificate(error)) return false;
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home || !*home) {
|
||||
error = "无法确定当前用户主目录";
|
||||
return false;
|
||||
}
|
||||
const auto keychain = std::filesystem::path(home) / "Library" / "Keychains" / "login.keychain-db";
|
||||
const ProcessResult result = run_process(
|
||||
"/usr/bin/security",
|
||||
{"add-trusted-cert", "-r", "trustRoot", "-p", "ssl", "-k", keychain.string(), files.ca.string()},
|
||||
{}, 120);
|
||||
if (result.timed_out || result.exit_code != 0) {
|
||||
error = result.timed_out ? "等待系统证书授权超时" : process_error(result);
|
||||
return false;
|
||||
}
|
||||
if (!local_certificate_trusted()) {
|
||||
error = "证书已安装,但系统仍未信任它";
|
||||
return false;
|
||||
}
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace connector
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user