mirror of
https://github.com/certd/certd.git
synced 2026-05-16 05:07:32 +08:00
chore: sdk
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
# Certd OpenAPI SDK 示例
|
||||||
|
|
||||||
|
本目录提供 `/api/v1/cert/get` 接口的多语言 SDK 示例,演示如何封装 `CertdClient`,生成 `x-certd-token`,并按域名或证书 ID 获取证书。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
| 语言 | SDK 类/类型 | 调用示例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Node.js | `sdk/nodejs/certd-client.js` | `sdk/nodejs/get-cert.js` |
|
||||||
|
| Python | `sdk/python/certd_client.py` | `sdk/python/get_cert.py` |
|
||||||
|
| Go | `sdk/go/certd_client.go` | `sdk/go/get_cert.go` |
|
||||||
|
| PHP | `sdk/php/CertdClient.php` | `sdk/php/get_cert.php` |
|
||||||
|
| Java | `sdk/java/CertdClient.java` | `sdk/java/GetCert.java` |
|
||||||
|
|
||||||
|
`CertdClient` 提供以下核心方法:
|
||||||
|
|
||||||
|
- `getSign(content)` / `GetSign(content)`:根据 `content + keySecret` 生成签名
|
||||||
|
- `getToken()` / `GetToken()`:生成 `x-certd-token`
|
||||||
|
- `request(path, body)` / `Request(path, body)`:携带 token 发起 OpenAPI 请求
|
||||||
|
- `getCert(params)` / `GetCert(params)`:调用 `/api/v1/cert/get` 的便捷方法
|
||||||
|
|
||||||
|
## 接口说明
|
||||||
|
|
||||||
|
- 请求地址:`POST /api/v1/cert/get`
|
||||||
|
- 认证方式:请求头传入 `x-certd-token`
|
||||||
|
- `certId` 和 `domains` 至少传一个;两个都传时,服务端优先使用 `certId`
|
||||||
|
- `autoApply=true` 时,如果没有可用证书,会尝试自动创建或触发流水线申请证书
|
||||||
|
- `format` 可选:`pem`、`jks`、`pfx`、`der`、`one`、`p7b`
|
||||||
|
|
||||||
|
## Token 生成规则
|
||||||
|
|
||||||
|
1. 在 OpenKey 页面生成 `keyId` 和 `keySecret`
|
||||||
|
2. 构造 JSON 字符串:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"keyId":"你的 keyId","t":1710000000,"encrypt":false,"signType":"md5"}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 签名:`sign = md5(content + keySecret)`
|
||||||
|
4. Header:`x-certd-token = base64(content) + "." + base64(sign)`
|
||||||
|
|
||||||
|
注意:签名时必须使用和 base64 编码时完全相同的 `content` 字符串。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
所有示例都支持以下环境变量:
|
||||||
|
|
||||||
|
| 变量 | 说明 | 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `CERTD_BASE_URL` | Certd 服务地址 | `http://127.0.0.1:7001` |
|
||||||
|
| `CERTD_KEY_ID` | OpenKey 的 keyId | 必填 |
|
||||||
|
| `CERTD_KEY_SECRET` | OpenKey 的 keySecret | 必填 |
|
||||||
|
| `CERTD_DOMAINS` | 域名列表,多个用英文逗号隔开 | 空 |
|
||||||
|
| `CERTD_CERT_ID` | 证书仓库证书 ID | 空 |
|
||||||
|
| `CERTD_AUTO_APPLY` | 不存在或过期时是否自动申请 | `false` |
|
||||||
|
| `CERTD_FORMAT` | 返回证书格式 | 空,表示返回所有格式 |
|
||||||
|
| `CERTD_ENCRYPT` | 是否要求接口加密返回结果 | `false` |
|
||||||
|
|
||||||
|
## 运行示例
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:CERTD_BASE_URL = "http://127.0.0.1:7001"
|
||||||
|
$env:CERTD_KEY_ID = "your_key_id"
|
||||||
|
$env:CERTD_KEY_SECRET = "your_key_secret"
|
||||||
|
$env:CERTD_DOMAINS = "example.com,*.example.com"
|
||||||
|
$env:CERTD_AUTO_APPLY = "true"
|
||||||
|
$env:CERTD_FORMAT = "pem"
|
||||||
|
|
||||||
|
node sdk\nodejs\get-cert.js
|
||||||
|
python sdk\python\get_cert.py
|
||||||
|
go run sdk\go\certd_client.go sdk\go\get_cert.go
|
||||||
|
php sdk\php\get_cert.php
|
||||||
|
javac sdk\java\CertdClient.java sdk\java\GetCert.java && java -cp sdk\java GetCert
|
||||||
|
```
|
||||||
|
|
||||||
|
如果使用 `CERTD_CERT_ID` 获取证书,可以不传 `CERTD_DOMAINS`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:CERTD_CERT_ID = "1"
|
||||||
|
$env:CERTD_DOMAINS = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## 返回结果
|
||||||
|
|
||||||
|
`CERTD_ENCRYPT=false` 时,示例会直接打印接口返回的 JSON。
|
||||||
|
|
||||||
|
`CERTD_ENCRYPT=true` 时,接口返回内容会被服务端加密;这些示例只演示请求和 token 生成,不包含解密逻辑。
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertdClient struct {
|
||||||
|
KeyID string
|
||||||
|
KeySecret string
|
||||||
|
BaseURL string
|
||||||
|
Encrypt bool
|
||||||
|
SignType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenContent struct {
|
||||||
|
KeyID string `json:"keyId"`
|
||||||
|
T int64 `json:"t"`
|
||||||
|
Encrypt bool `json:"encrypt"`
|
||||||
|
SignType string `json:"signType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCertdClient(keyID, keySecret string) (*CertdClient, error) {
|
||||||
|
if keyID == "" {
|
||||||
|
return nil, fmt.Errorf("keyID is required")
|
||||||
|
}
|
||||||
|
if keySecret == "" {
|
||||||
|
return nil, fmt.Errorf("keySecret is required")
|
||||||
|
}
|
||||||
|
return &CertdClient{
|
||||||
|
KeyID: keyID,
|
||||||
|
KeySecret: keySecret,
|
||||||
|
BaseURL: "http://127.0.0.1:7001",
|
||||||
|
SignType: "md5",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertdClient) GetSign(content string) (string, error) {
|
||||||
|
if c.SignType != "md5" {
|
||||||
|
return "", fmt.Errorf("unsupported signType: %s", c.SignType)
|
||||||
|
}
|
||||||
|
sum := md5.Sum([]byte(content + c.KeySecret))
|
||||||
|
return hex.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertdClient) GetToken() (string, error) {
|
||||||
|
contentBytes, err := json.Marshal(tokenContent{
|
||||||
|
KeyID: c.KeyID,
|
||||||
|
T: time.Now().Unix(),
|
||||||
|
Encrypt: c.Encrypt,
|
||||||
|
SignType: c.SignType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
content := string(contentBytes)
|
||||||
|
sign, err := c.GetSign(content)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(content)) + "." + base64.StdEncoding.EncodeToString([]byte(sign)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertdClient) Request(path string, body any) ([]byte, error) {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token, err := c.GetToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.BaseURL, "/")+path, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("content-type", "application/json")
|
||||||
|
httpReq.Header.Set("x-certd-token", token)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertdClient) GetCert(params any) ([]byte, error) {
|
||||||
|
return c.Request("/api/v1/cert/get", params)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type certGetRequest struct {
|
||||||
|
Domains string `json:"domains,omitempty"`
|
||||||
|
CertID int64 `json:"certId,omitempty"`
|
||||||
|
AutoApply bool `json:"autoApply"`
|
||||||
|
Format string `json:"format,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireEnv(name string) (string, error) {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value == "" {
|
||||||
|
return "", fmt.Errorf("missing environment variable: %s", name)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolEnv(name string, defaultValue bool) bool {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "1", "true", "yes", "y":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
keyID, err := requireEnv("CERTD_KEY_ID")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keySecret, err := requireEnv("CERTD_KEY_SECRET")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewCertdClient(keyID, keySecret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if baseURL := os.Getenv("CERTD_BASE_URL"); baseURL != "" {
|
||||||
|
client.BaseURL = baseURL
|
||||||
|
}
|
||||||
|
client.Encrypt = boolEnv("CERTD_ENCRYPT", false)
|
||||||
|
|
||||||
|
reqBody := certGetRequest{
|
||||||
|
Domains: os.Getenv("CERTD_DOMAINS"),
|
||||||
|
AutoApply: boolEnv("CERTD_AUTO_APPLY", false),
|
||||||
|
Format: os.Getenv("CERTD_FORMAT"),
|
||||||
|
}
|
||||||
|
if certID := os.Getenv("CERTD_CERT_ID"); certID != "" {
|
||||||
|
reqBody.CertID, err = strconv.ParseInt(certID, 10, 64)
|
||||||
|
if err != nil || reqBody.CertID <= 0 {
|
||||||
|
return fmt.Errorf("CERTD_CERT_ID must be a positive integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reqBody.CertID == 0 && reqBody.Domains == "" {
|
||||||
|
return fmt.Errorf("set CERTD_CERT_ID or CERTD_DOMAINS")
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := client.GetCert(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(respBody))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class CertdClient {
|
||||||
|
private final String keyId;
|
||||||
|
private final String keySecret;
|
||||||
|
private final String baseUrl;
|
||||||
|
private final boolean encrypt;
|
||||||
|
private final String signType;
|
||||||
|
|
||||||
|
public CertdClient(String keyId, String keySecret) {
|
||||||
|
this(keyId, keySecret, "http://127.0.0.1:7001", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertdClient(String keyId, String keySecret, String baseUrl, boolean encrypt) {
|
||||||
|
if (isBlank(keyId)) {
|
||||||
|
throw new IllegalArgumentException("keyId is required");
|
||||||
|
}
|
||||||
|
if (isBlank(keySecret)) {
|
||||||
|
throw new IllegalArgumentException("keySecret is required");
|
||||||
|
}
|
||||||
|
this.keyId = keyId;
|
||||||
|
this.keySecret = keySecret;
|
||||||
|
this.baseUrl = trimRightSlash(isBlank(baseUrl) ? "http://127.0.0.1:7001" : baseUrl);
|
||||||
|
this.encrypt = encrypt;
|
||||||
|
this.signType = "md5";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSign(String content) throws Exception {
|
||||||
|
if (!"md5".equals(signType)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported signType: " + signType);
|
||||||
|
}
|
||||||
|
return md5Hex(content + keySecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() throws Exception {
|
||||||
|
String content = "{\"keyId\":\"" + jsonEscape(keyId) + "\",\"t\":" + Instant.now().getEpochSecond()
|
||||||
|
+ ",\"encrypt\":" + encrypt + ",\"signType\":\"" + signType + "\"}";
|
||||||
|
String sign = getSign(content);
|
||||||
|
return base64(content) + "." + base64(sign);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String request(String path, String bodyJson) throws Exception {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + path))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("x-certd-token", getToken())
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
|
||||||
|
}
|
||||||
|
return response.body();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCert(String paramsJson) throws Exception {
|
||||||
|
return request("/api/v1/cert/get", paramsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String jsonEscape(String value) {
|
||||||
|
StringBuilder escaped = new StringBuilder();
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char ch = value.charAt(i);
|
||||||
|
switch (ch) {
|
||||||
|
case '"':
|
||||||
|
escaped.append("\\\"");
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
escaped.append("\\\\");
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
escaped.append("\\b");
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
escaped.append("\\f");
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
escaped.append("\\n");
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
escaped.append("\\r");
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
escaped.append("\\t");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ch < 0x20) {
|
||||||
|
escaped.append(String.format("\\u%04x", (int) ch));
|
||||||
|
} else {
|
||||||
|
escaped.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escaped.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String md5Hex(String value) throws Exception {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (byte b : digest) {
|
||||||
|
hex.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return hex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String base64(String value) {
|
||||||
|
return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimRightSlash(String value) {
|
||||||
|
return value.replaceAll("/+$", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
public class GetCert {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
new GetCert().run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println(e.getMessage());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() throws Exception {
|
||||||
|
CertdClient client = new CertdClient(
|
||||||
|
requireEnv("CERTD_KEY_ID"),
|
||||||
|
requireEnv("CERTD_KEY_SECRET"),
|
||||||
|
env("CERTD_BASE_URL", "http://127.0.0.1:7001"),
|
||||||
|
boolEnv("CERTD_ENCRYPT", false)
|
||||||
|
);
|
||||||
|
|
||||||
|
String certId = System.getenv("CERTD_CERT_ID");
|
||||||
|
String domains = System.getenv("CERTD_DOMAINS");
|
||||||
|
String format = System.getenv("CERTD_FORMAT");
|
||||||
|
|
||||||
|
if (isBlank(certId) && isBlank(domains)) {
|
||||||
|
throw new IllegalArgumentException("Set CERTD_CERT_ID or CERTD_DOMAINS");
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder body = new StringBuilder();
|
||||||
|
body.append("{");
|
||||||
|
boolean hasField = false;
|
||||||
|
if (!isBlank(certId)) {
|
||||||
|
body.append("\"certId\":").append(Long.parseLong(certId));
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
if (!isBlank(domains)) {
|
||||||
|
appendComma(body, hasField);
|
||||||
|
body.append("\"domains\":\"").append(CertdClient.jsonEscape(domains)).append("\"");
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
appendComma(body, hasField);
|
||||||
|
body.append("\"autoApply\":").append(boolEnv("CERTD_AUTO_APPLY", false));
|
||||||
|
hasField = true;
|
||||||
|
if (!isBlank(format)) {
|
||||||
|
appendComma(body, hasField);
|
||||||
|
body.append("\"format\":\"").append(CertdClient.jsonEscape(format)).append("\"");
|
||||||
|
}
|
||||||
|
body.append("}");
|
||||||
|
|
||||||
|
System.out.println(client.getCert(body.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireEnv(String name) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
if (isBlank(value)) {
|
||||||
|
throw new IllegalArgumentException("Missing environment variable: " + name);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String env(String name, String defaultValue) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
return isBlank(value) ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean boolEnv(String name, boolean defaultValue) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
if (isBlank(value)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return value.equalsIgnoreCase("1")
|
||||||
|
|| value.equalsIgnoreCase("true")
|
||||||
|
|| value.equalsIgnoreCase("yes")
|
||||||
|
|| value.equalsIgnoreCase("y");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendComma(StringBuilder builder, boolean hasField) {
|
||||||
|
if (hasField) {
|
||||||
|
builder.append(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
class CertdClient {
|
||||||
|
constructor(keyId, keySecret, options = {}) {
|
||||||
|
if (!keyId) {
|
||||||
|
throw new Error("keyId is required");
|
||||||
|
}
|
||||||
|
if (!keySecret) {
|
||||||
|
throw new Error("keySecret is required");
|
||||||
|
}
|
||||||
|
this.keyId = keyId;
|
||||||
|
this.keySecret = keySecret;
|
||||||
|
this.baseUrl = (options.baseUrl || "http://127.0.0.1:7001").replace(/\/$/, "");
|
||||||
|
this.encrypt = options.encrypt === true;
|
||||||
|
this.signType = options.signType || "md5";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSign(content) {
|
||||||
|
if (this.signType !== "md5") {
|
||||||
|
throw new Error(`Unsupported signType: ${this.signType}`);
|
||||||
|
}
|
||||||
|
return crypto.createHash("md5").update(content + this.keySecret, "utf8").digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(options = {}) {
|
||||||
|
const encrypt = options.encrypt ?? this.encrypt;
|
||||||
|
const content = JSON.stringify({
|
||||||
|
keyId: this.keyId,
|
||||||
|
t: Math.floor(Date.now() / 1000),
|
||||||
|
encrypt,
|
||||||
|
signType: this.signType,
|
||||||
|
});
|
||||||
|
const sign = this.getSign(content);
|
||||||
|
return `${Buffer.from(content, "utf8").toString("base64")}.${Buffer.from(sign, "utf8").toString("base64")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(path, body = {}, options = {}) {
|
||||||
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||||
|
method: options.method || "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-certd-token": this.getToken({ encrypt: options.encrypt }),
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCert(params) {
|
||||||
|
return this.request("/api/v1/cert/get", params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { CertdClient };
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { CertdClient } = require("./certd-client");
|
||||||
|
|
||||||
|
function requireEnv(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boolEnv(name, defaultValue = false) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (value == null || value === "") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return ["1", "true", "yes", "y"].includes(value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new CertdClient(requireEnv("CERTD_KEY_ID"), requireEnv("CERTD_KEY_SECRET"), {
|
||||||
|
baseUrl: process.env.CERTD_BASE_URL,
|
||||||
|
encrypt: boolEnv("CERTD_ENCRYPT"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
autoApply: boolEnv("CERTD_AUTO_APPLY"),
|
||||||
|
};
|
||||||
|
if (process.env.CERTD_CERT_ID) {
|
||||||
|
params.certId = Number(process.env.CERTD_CERT_ID);
|
||||||
|
if (!Number.isInteger(params.certId) || params.certId <= 0) {
|
||||||
|
throw new Error("CERTD_CERT_ID must be a positive integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.env.CERTD_DOMAINS) {
|
||||||
|
params.domains = process.env.CERTD_DOMAINS;
|
||||||
|
}
|
||||||
|
if (process.env.CERTD_FORMAT) {
|
||||||
|
params.format = process.env.CERTD_FORMAT;
|
||||||
|
}
|
||||||
|
if (!params.certId && !params.domains) {
|
||||||
|
throw new Error("Set CERTD_CERT_ID or CERTD_DOMAINS");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await client.getCert(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CertdClient
|
||||||
|
{
|
||||||
|
private string $keyId;
|
||||||
|
private string $keySecret;
|
||||||
|
private string $baseUrl;
|
||||||
|
private bool $encrypt;
|
||||||
|
private string $signType;
|
||||||
|
|
||||||
|
public function __construct(string $keyId, string $keySecret, array $options = [])
|
||||||
|
{
|
||||||
|
if ($keyId === '') {
|
||||||
|
throw new InvalidArgumentException('keyId is required');
|
||||||
|
}
|
||||||
|
if ($keySecret === '') {
|
||||||
|
throw new InvalidArgumentException('keySecret is required');
|
||||||
|
}
|
||||||
|
$this->keyId = $keyId;
|
||||||
|
$this->keySecret = $keySecret;
|
||||||
|
$this->baseUrl = rtrim($options['baseUrl'] ?? 'http://127.0.0.1:7001', '/');
|
||||||
|
$this->encrypt = $options['encrypt'] ?? false;
|
||||||
|
$this->signType = $options['signType'] ?? 'md5';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSign(string $content): string
|
||||||
|
{
|
||||||
|
if ($this->signType !== 'md5') {
|
||||||
|
throw new InvalidArgumentException("Unsupported signType: {$this->signType}");
|
||||||
|
}
|
||||||
|
return md5($content . $this->keySecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getToken(?bool $encrypt = null): string
|
||||||
|
{
|
||||||
|
$content = json_encode([
|
||||||
|
'keyId' => $this->keyId,
|
||||||
|
't' => time(),
|
||||||
|
'encrypt' => $encrypt ?? $this->encrypt,
|
||||||
|
'signType' => $this->signType,
|
||||||
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
$sign = $this->getSign($content);
|
||||||
|
return base64_encode($content) . '.' . base64_encode($sign);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $path, array $body = [], ?bool $encrypt = null): string
|
||||||
|
{
|
||||||
|
if (!function_exists('curl_init')) {
|
||||||
|
throw new RuntimeException('PHP curl extension is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'x-certd-token: ' . $this->getToken($encrypt),
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($this->baseUrl . $path);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 60,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
if ($response === false) {
|
||||||
|
throw new RuntimeException(curl_error($ch));
|
||||||
|
}
|
||||||
|
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($statusCode < 200 || $statusCode >= 300) {
|
||||||
|
throw new RuntimeException("HTTP {$statusCode}: {$response}");
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCert(array $params): string
|
||||||
|
{
|
||||||
|
return $this->request('/api/v1/cert/get', $params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/CertdClient.php';
|
||||||
|
|
||||||
|
function require_env(string $name): string
|
||||||
|
{
|
||||||
|
$value = getenv($name);
|
||||||
|
if ($value === false || $value === '') {
|
||||||
|
throw new RuntimeException("Missing environment variable: {$name}");
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bool_env(string $name, bool $default = false): bool
|
||||||
|
{
|
||||||
|
$value = getenv($name);
|
||||||
|
if ($value === false || $value === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
return in_array(strtolower($value), ['1', 'true', 'yes', 'y'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = new CertdClient(require_env('CERTD_KEY_ID'), require_env('CERTD_KEY_SECRET'), [
|
||||||
|
'baseUrl' => getenv('CERTD_BASE_URL') ?: 'http://127.0.0.1:7001',
|
||||||
|
'encrypt' => bool_env('CERTD_ENCRYPT'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'autoApply' => bool_env('CERTD_AUTO_APPLY'),
|
||||||
|
];
|
||||||
|
if (getenv('CERTD_CERT_ID')) {
|
||||||
|
if (!ctype_digit(getenv('CERTD_CERT_ID'))) {
|
||||||
|
throw new RuntimeException('CERTD_CERT_ID must be a positive integer');
|
||||||
|
}
|
||||||
|
$params['certId'] = intval(getenv('CERTD_CERT_ID'));
|
||||||
|
}
|
||||||
|
if (getenv('CERTD_DOMAINS')) {
|
||||||
|
$params['domains'] = getenv('CERTD_DOMAINS');
|
||||||
|
}
|
||||||
|
if (getenv('CERTD_FORMAT')) {
|
||||||
|
$params['format'] = getenv('CERTD_FORMAT');
|
||||||
|
}
|
||||||
|
if (empty($params['certId']) && empty($params['domains'])) {
|
||||||
|
throw new RuntimeException('Set CERTD_CERT_ID or CERTD_DOMAINS');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $client->getCert($params) . PHP_EOL;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
fwrite(STDERR, $e->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CertdClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key_id: str,
|
||||||
|
key_secret: str,
|
||||||
|
base_url: str = "http://127.0.0.1:7001",
|
||||||
|
encrypt: bool = False,
|
||||||
|
sign_type: str = "md5",
|
||||||
|
) -> None:
|
||||||
|
if not key_id:
|
||||||
|
raise ValueError("key_id is required")
|
||||||
|
if not key_secret:
|
||||||
|
raise ValueError("key_secret is required")
|
||||||
|
self.key_id = key_id
|
||||||
|
self.key_secret = key_secret
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.encrypt = encrypt
|
||||||
|
self.sign_type = sign_type
|
||||||
|
|
||||||
|
def get_sign(self, content: str) -> str:
|
||||||
|
if self.sign_type != "md5":
|
||||||
|
raise ValueError(f"Unsupported sign_type: {self.sign_type}")
|
||||||
|
return hashlib.md5((content + self.key_secret).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def getSign(self, content: str) -> str:
|
||||||
|
return self.get_sign(content)
|
||||||
|
|
||||||
|
def get_token(self, encrypt: Optional[bool] = None) -> str:
|
||||||
|
content = json.dumps(
|
||||||
|
{
|
||||||
|
"keyId": self.key_id,
|
||||||
|
"t": int(time.time()),
|
||||||
|
"encrypt": self.encrypt if encrypt is None else encrypt,
|
||||||
|
"signType": self.sign_type,
|
||||||
|
},
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
sign = self.get_sign(content)
|
||||||
|
return (
|
||||||
|
base64.b64encode(content.encode("utf-8")).decode("ascii")
|
||||||
|
+ "."
|
||||||
|
+ base64.b64encode(sign.encode("utf-8")).decode("ascii")
|
||||||
|
)
|
||||||
|
|
||||||
|
def getToken(self, encrypt: Optional[bool] = None) -> str:
|
||||||
|
return self.get_token(encrypt)
|
||||||
|
|
||||||
|
def request(self, path: str, body: Optional[Dict[str, Any]] = None, encrypt: Optional[bool] = None) -> str:
|
||||||
|
data = json.dumps(body or {}, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{self.base_url}{path}",
|
||||||
|
data=data,
|
||||||
|
method="POST",
|
||||||
|
headers={
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-certd-token": self.get_token(encrypt),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=60) as response:
|
||||||
|
return response.read().decode("utf-8")
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
message = error.read().decode("utf-8", errors="replace")
|
||||||
|
raise RuntimeError(f"HTTP {error.code}: {message}") from error
|
||||||
|
|
||||||
|
def get_cert(self, params: Dict[str, Any]) -> str:
|
||||||
|
return self.request("/api/v1/cert/get", params)
|
||||||
|
|
||||||
|
def getCert(self, params: Dict[str, Any]) -> str:
|
||||||
|
return self.get_cert(params)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from certd_client import CertdClient
|
||||||
|
|
||||||
|
|
||||||
|
def require_env(name: str) -> str:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if not value:
|
||||||
|
raise RuntimeError(f"Missing environment variable: {name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def bool_env(name: str, default: bool = False) -> bool:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return value.lower() in ("1", "true", "yes", "y")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
client = CertdClient(
|
||||||
|
require_env("CERTD_KEY_ID"),
|
||||||
|
require_env("CERTD_KEY_SECRET"),
|
||||||
|
base_url=os.getenv("CERTD_BASE_URL", "http://127.0.0.1:7001"),
|
||||||
|
encrypt=bool_env("CERTD_ENCRYPT"),
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"autoApply": bool_env("CERTD_AUTO_APPLY")}
|
||||||
|
cert_id = os.getenv("CERTD_CERT_ID")
|
||||||
|
domains = os.getenv("CERTD_DOMAINS")
|
||||||
|
cert_format = os.getenv("CERTD_FORMAT")
|
||||||
|
|
||||||
|
if cert_id:
|
||||||
|
params["certId"] = int(cert_id)
|
||||||
|
if domains:
|
||||||
|
params["domains"] = domains
|
||||||
|
if cert_format:
|
||||||
|
params["format"] = cert_format
|
||||||
|
if "certId" not in params and "domains" not in params:
|
||||||
|
raise RuntimeError("Set CERTD_CERT_ID or CERTD_DOMAINS")
|
||||||
|
|
||||||
|
print(client.get_cert(params))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exc:
|
||||||
|
print(str(exc), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
Reference in New Issue
Block a user