feat: support uploading ip addresses certificates

This commit is contained in:
Fu Diwei 2025-12-24 00:29:52 +08:00 committed by RHQYZ
parent 773e4fad36
commit 761454d2e2
11 changed files with 217 additions and 22 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/internal/domain/dtos"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509"
xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key"
)
@ -255,8 +256,8 @@ func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.
}
return &dtos.CertificateValidateCertificateResp{
IsValid: true,
Domains: strings.Join(certX509.DNSNames, ";"),
IsValid: true,
SubjectAltNames: strings.Join(xcertx509.GetSubjectAltNames(certX509), ";"),
}, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/go-acme/lego/v4/certcrypto"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509"
xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key"
)
@ -39,7 +40,7 @@ type Certificate struct {
}
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
c.SubjectAltNames = strings.Join(certX509.DNSNames, ";")
c.SubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";")
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
c.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";")
c.ValidityNotBefore = certX509.NotBefore

View File

@ -21,8 +21,8 @@ type CertificateValidateCertificateReq struct {
}
type CertificateValidateCertificateResp struct {
IsValid bool `json:"isValid"`
Domains string `json:"domains,omitempty"`
IsValid bool `json:"isValid"`
SubjectAltNames string `json:"subjectAltNames,omitempty"`
}
type CertificateValidatePrivateKeyReq struct {

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/certimate-go/certimate/internal/repository"
xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509"
xhttp "github.com/certimate-go/certimate/pkg/utils/http"
xtls "github.com/certimate-go/certimate/pkg/utils/tls"
)
@ -84,7 +85,7 @@ func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeE
ne.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')",
cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(),
cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339),
strings.Join(cert.DNSNames, ";")),
strings.Join(xcertx509.GetSubjectAltNames(cert), ";")),
)
ne.setVariablesOfResult(execCtx, execRes, cert)
@ -164,7 +165,7 @@ func (ne *bizMonitorNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionCon
if certX509 != nil {
vCommonName = certX509.Subject.CommonName
vSubjectAltNames = strings.Join(certX509.DNSNames, ";")
vSubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";")
vNotBefore = certX509.NotBefore
vNotAfter = certX509.NotAfter
vHoursLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours()))

View File

@ -1,12 +1,168 @@
package migrations
import (
"github.com/go-viper/mapstructure/v2"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
tracer := NewTracer("v0.4.11")
tracer.Printf("go ...")
// adapt to new workflow data structure
{
walker := &mWorkflowGraphWalker{}
walker.Define(func(node *mWorkflowNode) (_changed bool, _err error) {
_changed = false
_err = nil
if node.Type != "bizUpload" {
return
}
if node.Data == nil {
return
}
if _, ok := node.Data["config"]; ok {
nodeCfg := node.Data["config"].(map[string]any)
if nodeCfg["domains"] != nil {
nodeCfg["name"] = nodeCfg["domains"]
delete(nodeCfg, "domains")
node.Data["config"] = nodeCfg
_changed = true
return
}
}
return
})
// update collection `workflow`
// - migrate field `graphDraft` / `graphContent`
{
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
if err != nil {
return err
}
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
if record.GetRaw("graphDraft") != nil {
graph := make(map[string]any)
if err := record.UnmarshalJSONField("graphDraft", &graph); err != nil {
return err
}
if _, ok := graph["nodes"]; ok {
nodes := make([]*mWorkflowNode, 0)
if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil {
return err
}
nodesChanged, err := walker.Visit(nodes)
if err != nil {
return err
} else if nodesChanged {
graph["nodes"] = nodes
record.Set("graphDraft", graph)
changed = true
}
}
}
if record.GetRaw("graphContent") != nil {
graph := make(map[string]any)
if err := record.UnmarshalJSONField("graphContent", &graph); err != nil {
return err
}
if _, ok := graph["nodes"]; ok {
nodes := make([]*mWorkflowNode, 0)
if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil {
return err
}
nodesChanged, err := walker.Visit(nodes)
if err != nil {
return err
} else if nodesChanged {
graph["nodes"] = nodes
record.Set("graphContent", graph)
changed = true
}
}
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
// update collection `workflow_run`
// - migrate field `graph`
{
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
if err != nil {
return err
}
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
if record.GetRaw("graph") != nil {
graph := make(map[string]any)
if err := record.UnmarshalJSONField("graph", &graph); err != nil {
return err
}
if _, ok := graph["nodes"]; ok {
nodes := make([]*mWorkflowNode, 0)
if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil {
return err
}
nodesChanged, err := walker.Visit(nodes)
if err != nil {
return err
} else if nodesChanged {
graph["nodes"] = nodes
record.Set("graph", graph)
changed = true
}
}
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
}
// clean old migrations
{
migrations := []string{
@ -23,6 +179,7 @@ func init() {
}
}
tracer.Printf("done")
return nil
}, func(app core.App) error {
return nil

View File

@ -0,0 +1,35 @@
package x509
import (
"crypto/x509"
)
// 返回指定 x509.Certificate 对象的主题替代名称。
//
// 入参:
// - cert: x509.Certificate 对象。
//
// 出参:
// - 主题替代名称的字符串切片。
func GetSubjectAltNames(cert *x509.Certificate) []string {
sans := make([]string, 0)
if cert != nil {
for _, dnsName := range cert.DNSNames {
sans = append(sans, dnsName)
}
for _, ipAddr := range cert.IPAddresses {
sans = append(sans, ipAddr.String())
}
for _, email := range cert.EmailAddresses {
sans = append(sans, email)
}
for _, uri := range cert.URIs {
if uri != nil {
sans = append(sans, uri.String())
}
}
}
return sans
}

View File

@ -48,7 +48,7 @@ export const validateCertificate = async (certificate: string) => {
type RespData = {
isValid: boolean;
domains: string;
subjectAltNames: string;
};
const resp = await pb.send<BaseResponse<RespData>>(`/api/certificates/pre-validate/certificate`, {
method: "POST",

View File

@ -49,12 +49,12 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
const handleSourceChange = (value: string) => {
if (value === initialValues?.source) {
formInst.resetFields(["certificate", "privateKey", "domains"]);
formInst.resetFields(["certificate", "privateKey", "name"]);
} else {
setTimeout(() => {
formInst.setFieldValue("certificate", "");
formInst.setFieldValue("privateKey", "");
formInst.setFieldValue("domains", "");
formInst.setFieldValue("name", "");
}, 0);
}
};
@ -64,8 +64,8 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
const resp = await validateCertificate(value);
formInst.setFields([
{
name: "domains",
value: resp.data.domains,
name: "name",
value: resp.data.subjectAltNames,
},
{
name: "certificate",
@ -75,7 +75,7 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
} catch (e) {
formInst.setFields([
{
name: "domains",
name: "name",
value: "",
},
{
@ -120,8 +120,8 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
</Form.Item>
<Show when={fieldSource === UPLOAD_SOURCE_FORM}>
<Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
<Form.Item name="name" label={t("workflow_node.upload.form.name.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.upload.form.name.placeholder")} readOnly />
</Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate_pem.label")} rules={[formRule]}>
@ -198,9 +198,9 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
return z
.object({
source: z.enum([UPLOAD_SOURCE_FORM, UPLOAD_SOURCE_LOCAL, UPLOAD_SOURCE_URL], t("workflow_node.upload.form.source.placeholder")),
name: z.string().nullish(),
certificate: z.string().nonempty(),
privateKey: z.string().nonempty(),
domains: z.string().nullish(),
})
.superRefine((values, ctx) => {
switch (values.source) {

View File

@ -47,8 +47,8 @@ export const BizUploadNodeRegistry: NodeRegistry = {
{({ field: { value: fieldSource } }) => (
<>
{fieldSource == null || fieldSource === "" || fieldSource === "form" ? (
<Field<string> name="config.domains">
{({ field: { value: fieldDomains } }) => <>{fieldDomains || t("workflow.detail.design.editor.placeholder")}</>}
<Field<string> name="config.name">
{({ field: { value: fieldName } }) => <>{fieldName || t("workflow.detail.design.editor.placeholder")}</>}
</Field>
) : (
<Field<string> name="config.certificate">

View File

@ -145,8 +145,8 @@
"workflow_node.upload.form.source.option.form.label": "Form",
"workflow_node.upload.form.source.option.local.label": "Local path",
"workflow_node.upload.form.source.option.url.label": "URL",
"workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
"workflow_node.upload.form.name.label": "Domains / IP addresses",
"workflow_node.upload.form.name.placholder": "Please select certificate file",
"workflow_node.upload.form.certificate_pem.label": "Certificate (PEM format)",
"workflow_node.upload.form.certificate_pem.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.certificate_path.label": "Certificate file path",

View File

@ -144,8 +144,8 @@
"workflow_node.upload.form.source.option.form.label": "表单",
"workflow_node.upload.form.source.option.local.label": "本地路径",
"workflow_node.upload.form.source.option.url.label": "URL 路径",
"workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.name.label": "证书名称",
"workflow_node.upload.form.name.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate_pem.label": "证书文件PEM 格式)",
"workflow_node.upload.form.certificate_pem.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.certificate_path.label": "证书文件路径",