mirror of
https://github.com/certd/certd.git
synced 2026-07-07 04:47:34 +08:00
345 lines
9.4 KiB
YAML
345 lines
9.4 KiB
YAML
name: nginxProxyManager
|
|
icon: logos:nginx
|
|
title: Nginx Proxy Manager 授权
|
|
group: null
|
|
desc: 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。
|
|
setting: null
|
|
sysSetting: null
|
|
type: custom
|
|
disabled: false
|
|
version: 1.0.0
|
|
pluginType: access
|
|
author: samler
|
|
input:
|
|
endpoint:
|
|
title: NPM 地址
|
|
component:
|
|
name: a-input
|
|
allowClear: true
|
|
placeholder: https://npm.example.com
|
|
helper: 请输入 Nginx Proxy Manager 根地址,不要带 /api 后缀。
|
|
required: true
|
|
email:
|
|
title: 邮箱
|
|
component:
|
|
name: a-input
|
|
allowClear: true
|
|
placeholder: admin@example.com
|
|
required: true
|
|
password:
|
|
title: 密码
|
|
component:
|
|
name: a-input-password
|
|
allowClear: true
|
|
placeholder: 请输入密码
|
|
required: true
|
|
encrypt: true
|
|
totpSecret:
|
|
title: TOTP 密钥
|
|
component:
|
|
name: a-input-password
|
|
allowClear: true
|
|
placeholder: Optional base32 TOTP secret
|
|
helper: 当 Nginx Proxy Manager 账号开启 2FA 时必填。
|
|
required: false
|
|
encrypt: true
|
|
ignoreTls:
|
|
title: 忽略无效 TLS
|
|
component:
|
|
name: a-switch
|
|
vModel: checked
|
|
helper: 仅在 Nginx Proxy Manager 使用自签 HTTPS 证书时开启。
|
|
required: false
|
|
testRequest:
|
|
title: 测试
|
|
component:
|
|
name: api-test
|
|
action: TestRequest
|
|
helper: 测试登录并拉取代理主机列表。
|
|
content: |
|
|
const { BaseAccess } = await import("@certd/pipeline");
|
|
const httpsModule = await import("node:https");
|
|
const { URL } = await import("node:url");
|
|
const axiosModule = await import("axios");
|
|
const formDataModule = await import("form-data");
|
|
const { authenticator } = await import("otplib");
|
|
|
|
const https = httpsModule.default ?? httpsModule;
|
|
const axios = axiosModule.default ?? axiosModule;
|
|
const FormData = formDataModule.default ?? formDataModule;
|
|
|
|
function normalizeEndpoint(endpoint) {
|
|
const trimmed = String(endpoint ?? "").trim();
|
|
if (!trimmed) {
|
|
throw new Error("Nginx Proxy Manager 地址不能为空");
|
|
}
|
|
|
|
const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
|
|
return withoutTrailingSlash.endsWith("/api")
|
|
? withoutTrailingSlash.slice(0, -4)
|
|
: withoutTrailingSlash;
|
|
}
|
|
|
|
function buildHttpsAgent(endpoint, ignoreTls) {
|
|
const url = new URL(endpoint);
|
|
if (url.protocol !== "https:") {
|
|
return undefined;
|
|
}
|
|
|
|
return new https.Agent({
|
|
rejectUnauthorized: !ignoreTls,
|
|
});
|
|
}
|
|
|
|
function describeError(error, action) {
|
|
if (axios.isAxiosError(error)) {
|
|
const status = error.response?.status;
|
|
const data = error.response?.data;
|
|
const message =
|
|
data?.error?.message ||
|
|
data?.error ||
|
|
data?.message ||
|
|
(typeof data === "string" ? data : null) ||
|
|
error.message;
|
|
return new Error(`${action} failed${status ? ` (${status})` : ""}: ${message}`);
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return new Error(`${action} failed: ${error.message}`);
|
|
}
|
|
|
|
return new Error(`${action} failed`);
|
|
}
|
|
|
|
class NginxProxyManagerClient {
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.endpoint = normalizeEndpoint(options.endpoint);
|
|
this.apiBaseUrl = `${this.endpoint}/api`;
|
|
this.token = undefined;
|
|
this.tokenPromise = undefined;
|
|
this.httpClient = axios.create({
|
|
baseURL: this.apiBaseUrl,
|
|
timeout: 30000,
|
|
maxBodyLength: Infinity,
|
|
maxContentLength: Infinity,
|
|
httpsAgent: buildHttpsAgent(this.endpoint, options.ignoreTls === true),
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
}
|
|
|
|
async verifyAccess() {
|
|
const proxyHosts = await this.getProxyHosts();
|
|
return {
|
|
proxyHostCount: proxyHosts.length,
|
|
};
|
|
}
|
|
|
|
async getProxyHosts(searchQuery) {
|
|
return await this.requestWithAuth({
|
|
method: "GET",
|
|
url: "/nginx/proxy-hosts",
|
|
params: {
|
|
expand: "certificate",
|
|
...(searchQuery ? { query: searchQuery } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
async getCertificates(searchQuery) {
|
|
return await this.requestWithAuth({
|
|
method: "GET",
|
|
url: "/nginx/certificates",
|
|
params: searchQuery ? { query: searchQuery } : undefined,
|
|
});
|
|
}
|
|
|
|
async getCertificatesWithExpand(searchQuery, expand = []) {
|
|
return await this.requestWithAuth({
|
|
method: "GET",
|
|
url: "/nginx/certificates",
|
|
params: {
|
|
...(searchQuery ? { query: searchQuery } : {}),
|
|
...(expand.length > 0 ? { expand: expand.join(",") } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
async findCustomCertificateByNiceName(niceName) {
|
|
const certificates = await this.getCertificates(niceName);
|
|
return certificates.find((certificate) => {
|
|
return certificate.provider === "other" && certificate.nice_name === niceName;
|
|
});
|
|
}
|
|
|
|
async createCustomCertificate(niceName, domainNames = []) {
|
|
return await this.requestWithAuth({
|
|
method: "POST",
|
|
url: "/nginx/certificates",
|
|
data: {
|
|
provider: "other",
|
|
nice_name: niceName,
|
|
domain_names: domainNames,
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteCertificate(certificateId) {
|
|
await this.requestWithAuth({
|
|
method: "DELETE",
|
|
url: `/nginx/certificates/${certificateId}`,
|
|
});
|
|
}
|
|
|
|
async uploadCertificate(certificateId, payload) {
|
|
const form = new FormData();
|
|
form.append("certificate", Buffer.from(payload.certificate, "utf8"), {
|
|
filename: "fullchain.pem",
|
|
contentType: "application/x-pem-file",
|
|
});
|
|
form.append("certificate_key", Buffer.from(payload.certificateKey, "utf8"), {
|
|
filename: "privkey.pem",
|
|
contentType: "application/x-pem-file",
|
|
});
|
|
if (payload.intermediateCertificate) {
|
|
form.append("intermediate_certificate", Buffer.from(payload.intermediateCertificate, "utf8"), {
|
|
filename: "chain.pem",
|
|
contentType: "application/x-pem-file",
|
|
});
|
|
}
|
|
|
|
await this.requestWithAuth({
|
|
method: "POST",
|
|
url: `/nginx/certificates/${certificateId}/upload`,
|
|
data: form,
|
|
headers: form.getHeaders(),
|
|
});
|
|
}
|
|
|
|
async assignCertificateToProxyHost(hostId, certificateId) {
|
|
await this.requestWithAuth({
|
|
method: "PUT",
|
|
url: `/nginx/proxy-hosts/${hostId}`,
|
|
data: {
|
|
certificate_id: certificateId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async login() {
|
|
if (this.token) {
|
|
return this.token;
|
|
}
|
|
|
|
if (!this.tokenPromise) {
|
|
this.tokenPromise = this.performLogin().finally(() => {
|
|
this.tokenPromise = undefined;
|
|
});
|
|
}
|
|
|
|
this.token = await this.tokenPromise;
|
|
return this.token;
|
|
}
|
|
|
|
async performLogin() {
|
|
const initialLogin = await this.request({
|
|
method: "POST",
|
|
url: "/tokens",
|
|
data: {
|
|
identity: this.options.email,
|
|
secret: this.options.password,
|
|
},
|
|
});
|
|
|
|
if (initialLogin.token) {
|
|
return initialLogin.token;
|
|
}
|
|
|
|
if (!initialLogin.requires_2fa || !initialLogin.challenge_token) {
|
|
throw new Error("登录失败:Nginx Proxy Manager 未返回访问令牌");
|
|
}
|
|
|
|
if (!this.options.totpSecret) {
|
|
throw new Error("登录失败:该 Nginx Proxy Manager 账号启用了 2FA,但未配置 totpSecret");
|
|
}
|
|
|
|
let code;
|
|
try {
|
|
code = authenticator.generate(this.options.totpSecret);
|
|
} catch (error) {
|
|
throw describeError(error, "Generating TOTP code");
|
|
}
|
|
|
|
const completedLogin = await this.request({
|
|
method: "POST",
|
|
url: "/tokens/2fa",
|
|
data: {
|
|
challenge_token: initialLogin.challenge_token,
|
|
code,
|
|
},
|
|
});
|
|
|
|
if (!completedLogin.token) {
|
|
throw new Error("2FA 登录失败:Nginx Proxy Manager 未返回访问令牌");
|
|
}
|
|
|
|
return completedLogin.token;
|
|
}
|
|
|
|
async requestWithAuth(config) {
|
|
const token = await this.login();
|
|
const headers = {
|
|
...(config.headers ?? {}),
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
|
|
return await this.request({
|
|
...config,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
async request(config) {
|
|
const action = `${config.method ?? "GET"} ${config.url ?? "/"}`;
|
|
try {
|
|
const response = await this.httpClient.request(config);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw describeError(error, action);
|
|
}
|
|
}
|
|
}
|
|
|
|
return class NginxProxyManagerAccess extends BaseAccess {
|
|
endpoint = "";
|
|
email = "";
|
|
password = "";
|
|
totpSecret = "";
|
|
ignoreTls = false;
|
|
testRequest = true;
|
|
|
|
createClient() {
|
|
return new NginxProxyManagerClient({
|
|
endpoint: this.endpoint,
|
|
email: this.email,
|
|
password: this.password,
|
|
totpSecret: this.totpSecret || undefined,
|
|
ignoreTls: this.ignoreTls === true,
|
|
});
|
|
}
|
|
|
|
async onTestRequest() {
|
|
const client = this.createClient();
|
|
const result = await client.verifyAccess();
|
|
this.ctx.logger.info(`Nginx Proxy Manager 授权验证成功,找到 ${result.proxyHostCount} 个代理主机`);
|
|
return `成功(${result.proxyHostCount} 个代理主机)`;
|
|
}
|
|
|
|
async getProxyHostList(req = {}) {
|
|
const client = this.createClient();
|
|
return await client.getProxyHosts(req.searchKey);
|
|
}
|
|
};
|