🎨 Implement HTTPS network serving (#16912)

* Add use TLS for network serving configuration option

* kernel: Implement TLS certificate generation

* kernel: server: Use https for fixed port proxy when needed

* Allow exporting the CA Certificate file

* Implement import and export of CA Certs
This commit is contained in:
Davide Garberi 2026-01-27 05:59:11 +01:00 committed by GitHub
parent e7621b7a5f
commit 43ea6757d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 759 additions and 10 deletions

View file

@ -1297,6 +1297,15 @@
"about8": "When enabled, the application will be automatically locked when locking the system screen",
"about11": "Network serving",
"about12": "When enabled, other devices in the same LAN will be allowed to access. The application will be closed automatically after modification, please restart manually",
"networkServeTLS": "Enable HTTPS",
"networkServeTLSTip": "When enabled, network connections will be encrypted with TLS using auto-generated self-signed certificates. Browsers will show a security warning that must be accepted. Requires restart",
"exportCACert": "Export CA Certificate",
"exportCACertTip": "Export the CA certificate (ca.crt) file. Install this certificate on client devices to trust the self-signed HTTPS connection",
"exportCABundle": "Export CA Bundle",
"exportCABundleTip": "Export the CA certificate and private key for sharing with other SiYuan devices. All devices using the same CA will be trusted by clients that import it",
"importCABundle": "Import CA Bundle",
"importCABundleTip": "Import a CA bundle from another SiYuan device. After importing, this device will use the same CA, allowing clients to trust certificates from all devices",
"importCABundleSuccess": "CA bundle imported successfully. Please restart the application to apply changes",
"about13": "API token",
"about14": "The token needs to be authenticated when calling the API<br>HTTP request header <code class=\"fn__code\">Authorization: token ${token}</code>",
"about17": "Do not enable proxy when set to <code class='fn__code'>Direct connection</code>",

View file

@ -1297,6 +1297,15 @@
"about8": "启用后将会在系统锁屏时自动锁定应用",
"about11": "网络伺服",
"about12": "启用后将允许同一局域网内的其他设备进行访问。修改后会自动关闭应用,请手动重启",
"networkServeTLS": "启用 HTTPS",
"networkServeTLSTip": "启用后网络连接将使用自动生成的自签名证书进行 TLS 加密。浏览器会显示安全警告,需要手动接受。启用后需重启应用",
"exportCACert": "导出 CA 证书",
"exportCACertTip": "导出 CA 证书ca.crt文件。将此证书安装到客户端设备以信任自签名 HTTPS 连接",
"exportCABundle": "导出 CA 证书包",
"exportCABundleTip": "导出 CA 证书和私钥以便与其他思源设备共享。使用相同 CA 的所有设备将被导入该证书的客户端信任",
"importCABundle": "导入 CA 证书包",
"importCABundleTip": "从另一台思源设备导入 CA 证书包。导入后,此设备将使用相同的 CA允许客户端信任所有设备的证书",
"importCABundleSuccess": "CA 证书包导入成功。请重启应用以应用更改",
"about13": "API token",
"about14": "调用 API 时需要通过该 token 进行鉴权<br>HTTP 请求标头 <code class=\"fn__code\">Authorization: token ${token}</code>",
"about17": "设置为 <code class='fn__code'>直接连接</code> 时不启用代理",

View file

@ -64,6 +64,44 @@ export const about = {
<div class="fn__space"></div>
<input class="b3-switch fn__flex-center" id="networkServe" type="checkbox"${window.siyuan.config.system.networkServe ? " checked" : ""}>
</label>
<label class="b3-label fn__flex">
<div class="fn__flex-1">
${window.siyuan.languages.networkServeTLS}
<div class="b3-label__text">${window.siyuan.languages.networkServeTLSTip}</div>
</div>
<div class="fn__space"></div>
<input class="b3-switch fn__flex-center" id="networkServeTLS" type="checkbox"${window.siyuan.config.system.networkServeTLS ? " checked" : ""}${!window.siyuan.config.system.networkServe ? " disabled" : ""}>
</label>
<div class="fn__flex b3-label config__item${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="exportCACertSection">
<div class="fn__flex-1">
${window.siyuan.languages.exportCACert}
<div class="b3-label__text">${window.siyuan.languages.exportCACertTip}</div>
</div>
<div class="fn__space"></div>
<button class="b3-button b3-button--outline fn__size200 fn__flex-center" id="exportCACert">
<svg><use xlink:href="#iconUpload"></use></svg>${window.siyuan.languages.export}
</button>
</div>
<div class="fn__flex b3-label config__item${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="exportCABundleSection">
<div class="fn__flex-1">
${window.siyuan.languages.exportCABundle}
<div class="b3-label__text">${window.siyuan.languages.exportCABundleTip}</div>
</div>
<div class="fn__space"></div>
<button class="b3-button b3-button--outline fn__size200 fn__flex-center" id="exportCABundle">
<svg><use xlink:href="#iconUpload"></use></svg>${window.siyuan.languages.export}
</button>
</div>
<div class="fn__flex b3-label config__item${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="importCABundleSection">
<div class="fn__flex-1">
${window.siyuan.languages.importCABundle}
<div class="b3-label__text">${window.siyuan.languages.importCABundleTip}</div>
</div>
<div class="fn__space"></div>
<button class="b3-button b3-button--outline fn__size200 fn__flex-center" id="importCABundle">
<svg><use xlink:href="#iconDownload"></use></svg>${window.siyuan.languages.import}
</button>
</div>
<div class="b3-label${(window.siyuan.config.readonly || (isBrowser() && !isInIOS() && !isInAndroid() && !isIPad() && !isInHarmony())) ? " fn__none" : ""}">
<div class="fn__flex">
<div class="fn__flex-1">
@ -102,7 +140,7 @@ export const about = {
<div class="b3-label__text">${window.siyuan.languages.about18}</div>
</div>
<div class="fn__space"></div>
<button data-type="open" data-url="${window.siyuan.config.system.networkServe ? window.siyuan.config.serverAddrs[0] : "http://127.0.0.1:"+ location.port}" class="b3-button b3-button--outline fn__size200 fn__flex-center">
<button data-type="open" data-url="${window.siyuan.config.system.networkServeTLS ? "https" : "http"}://${window.siyuan.config.system.networkServe ? window.siyuan.config.serverAddrs[0] : "127.0.0.1"}:${location.port}" class="b3-button b3-button--outline fn__size200 fn__flex-center">
<svg><use xlink:href="#iconLink"></use></svg>${window.siyuan.languages.about4}
</button>
</div>
@ -379,7 +417,12 @@ ${checkUpdateHTML}
});
});
const networkServeElement = about.element.querySelector("#networkServe") as HTMLInputElement;
const networkServeTLSElement = about.element.querySelector("#networkServeTLS") as HTMLInputElement;
networkServeElement.addEventListener("change", () => {
networkServeTLSElement.disabled = !networkServeElement.checked;
if (!networkServeElement.checked) {
networkServeTLSElement.checked = false;
}
fetchPost("/api/system/setNetworkServe", {networkServe: networkServeElement.checked}, () => {
exportLayout({
errorExit: true,
@ -387,6 +430,60 @@ ${checkUpdateHTML}
});
});
});
networkServeTLSElement.addEventListener("change", () => {
const exportCACertSection = about.element.querySelector("#exportCACertSection");
const exportCABundleSection = about.element.querySelector("#exportCABundleSection");
const importCABundleSection = about.element.querySelector("#importCABundleSection");
if (exportCACertSection && exportCABundleSection && importCABundleSection) {
if (networkServeTLSElement.checked) {
exportCACertSection.classList.remove("fn__none");
exportCABundleSection.classList.remove("fn__none");
importCABundleSection.classList.remove("fn__none");
} else {
exportCACertSection.classList.add("fn__none");
exportCABundleSection.classList.add("fn__none");
importCABundleSection.classList.add("fn__none");
}
}
fetchPost("/api/system/setNetworkServeTLS", {networkServeTLS: networkServeTLSElement.checked}, () => {
exportLayout({
errorExit: true,
cb: exitSiYuan
});
});
});
about.element.querySelector("#exportCACert")?.addEventListener("click", () => {
fetchPost("/api/system/exportTLSCACert", {}, (response) => {
openByMobile(response.data.path);
});
});
about.element.querySelector("#exportCABundle")?.addEventListener("click", () => {
fetchPost("/api/system/exportTLSCABundle", {}, (response) => {
openByMobile(response.data.path);
});
});
about.element.querySelector("#importCABundle")?.addEventListener("click", () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
input.onchange = () => {
if (input.files && input.files[0]) {
const formData = new FormData();
formData.append("file", input.files[0]);
fetch("/api/system/importTLSCABundle", {
method: "POST",
body: formData,
}).then(res => res.json()).then((response) => {
if (response.code === 0) {
showMessage(window.siyuan.languages.importCABundleSuccess);
} else {
showMessage(response.msg, 6000, "error");
}
});
}
};
input.click();
});
const lockScreenModeElement = about.element.querySelector("#lockScreenMode") as HTMLInputElement;
lockScreenModeElement.addEventListener("change", () => {
fetchPost("/api/system/setFollowSystemLockScreen", {lockScreenMode: lockScreenModeElement.checked ? 1 : 0}, () => {

View file

@ -24,10 +24,42 @@ export const initAbout = () => {
<div class="fn__space"></div>
<input class="b3-switch fn__flex-center" id="networkServe" type="checkbox"${window.siyuan.config.system.networkServe ? " checked" : ""}>
</label>
<label class="b3-label fn__flex${window.siyuan.config.readonly ? " fn__none" : ""}">
<div class="fn__flex-1">
${window.siyuan.languages.networkServeTLS}
<div class="b3-label__text">${window.siyuan.languages.networkServeTLSTip}</div>
</div>
<div class="fn__space"></div>
<input class="b3-switch fn__flex-center" id="networkServeTLS" type="checkbox"${window.siyuan.config.system.networkServeTLS ? " checked" : ""}${!window.siyuan.config.system.networkServe ? " disabled" : ""}>
</label>
<div class="b3-label${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="exportCACertSection">
${window.siyuan.languages.exportCACert}
<div class="fn__hr"></div>
<button class="b3-button b3-button--outline fn__block" id="exportCACert">
<svg><use xlink:href="#iconUpload"></use></svg>${window.siyuan.languages.export}
</button>
<div class="b3-label__text">${window.siyuan.languages.exportCACertTip}</div>
</div>
<div class="b3-label${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="exportCABundleSection">
${window.siyuan.languages.exportCABundle}
<div class="fn__hr"></div>
<button class="b3-button b3-button--outline fn__block" id="exportCABundle">
<svg><use xlink:href="#iconUpload"></use></svg>${window.siyuan.languages.export}
</button>
<div class="b3-label__text">${window.siyuan.languages.exportCABundleTip}</div>
</div>
<div class="b3-label${window.siyuan.config.system.networkServeTLS ? "" : " fn__none"}" id="importCABundleSection">
${window.siyuan.languages.importCABundle}
<div class="fn__hr"></div>
<button class="b3-button b3-button--outline fn__block" id="importCABundle">
<svg><use xlink:href="#iconDownload"></use></svg>${window.siyuan.languages.import}
</button>
<div class="b3-label__text">${window.siyuan.languages.importCABundleTip}</div>
</div>
<div class="b3-label">
${window.siyuan.languages.about2}
<div class="fn__hr"></div>
<a target="_blank" href="${window.siyuan.config.system.networkServe ? window.siyuan.config.serverAddrs[0] : "http://127.0.0.1:" + location.port}" class="b3-button b3-button--outline fn__block">
<a target="_blank" href="${window.siyuan.config.system.networkServeTLS ? "https" : "http"}://${window.siyuan.config.system.networkServe ? window.siyuan.config.serverAddrs[0] : "127.0.0.1"}:${location.port}" class="b3-button b3-button--outline fn__block">
<svg><use xlink:href="#iconLink"></use></svg>${window.siyuan.languages.about4}
</a>
<div class="b3-label__text">${window.siyuan.languages.about3.replace("${port}", location.port)}</div>
@ -451,11 +483,67 @@ export const initAbout = () => {
});
});
const networkServeElement = modelMainElement.querySelector("#networkServe") as HTMLInputElement;
const networkServeTLSElement = modelMainElement.querySelector("#networkServeTLS") as HTMLInputElement;
networkServeElement.addEventListener("change", () => {
networkServeTLSElement.disabled = !networkServeElement.checked;
if (!networkServeElement.checked) {
networkServeTLSElement.checked = false;
}
fetchPost("/api/system/setNetworkServe", {networkServe: networkServeElement.checked}, () => {
exitSiYuan();
});
});
networkServeTLSElement.addEventListener("change", () => {
const exportCACertSection = modelMainElement.querySelector("#exportCACertSection");
const exportCABundleSection = modelMainElement.querySelector("#exportCABundleSection");
const importCABundleSection = modelMainElement.querySelector("#importCABundleSection");
if (exportCACertSection && exportCABundleSection && importCABundleSection) {
if (networkServeTLSElement.checked) {
exportCACertSection.classList.remove("fn__none");
exportCABundleSection.classList.remove("fn__none");
importCABundleSection.classList.remove("fn__none");
} else {
exportCACertSection.classList.add("fn__none");
exportCABundleSection.classList.add("fn__none");
importCABundleSection.classList.add("fn__none");
}
}
fetchPost("/api/system/setNetworkServeTLS", {networkServeTLS: networkServeTLSElement.checked}, () => {
exitSiYuan();
});
});
modelMainElement.querySelector("#exportCACert")?.addEventListener("click", () => {
fetchPost("/api/system/exportTLSCACert", {}, (response) => {
openByMobile(response.data.path);
});
});
modelMainElement.querySelector("#exportCABundle")?.addEventListener("click", () => {
fetchPost("/api/system/exportTLSCABundle", {}, (response) => {
openByMobile(response.data.path);
});
});
modelMainElement.querySelector("#importCABundle")?.addEventListener("click", () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
input.onchange = () => {
if (input.files && input.files[0]) {
const formData = new FormData();
formData.append("file", input.files[0]);
fetch("/api/system/importTLSCABundle", {
method: "POST",
body: formData,
}).then(res => res.json()).then((response) => {
if (response.code === 0) {
showMessage(window.siyuan.languages.importCABundleSuccess);
} else {
showMessage(response.msg, 6000, "error");
}
});
}
};
input.click();
});
const tokenElement = modelMainElement.querySelector("#token") as HTMLInputElement;
tokenElement.addEventListener("change", () => {
fetchPost("/api/system/setAPIToken", {token: tokenElement.value}, () => {

View file

@ -1623,6 +1623,10 @@ declare namespace Config {
* Whether to enable network serve (whether to allow connections from other devices)
*/
networkServe: boolean;
/**
* Whether to enable HTTPS for network serve (TLS encryption)
*/
networkServeTLS: boolean;
/**
* The operating system name determined at compile time (obtained using the command `go tool
* dist list`)