mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 09:21:56 +08:00
refactor code
This commit is contained in:
parent
75326b1ddd
commit
9cdc59b272
@ -3,6 +3,7 @@ package domain
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Value any
|
||||
@ -11,6 +12,7 @@ type (
|
||||
ComparisonOperator string
|
||||
LogicalOperator string
|
||||
ValueType string
|
||||
ExprType string
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,6 +31,12 @@ const (
|
||||
Number ValueType = "number"
|
||||
String ValueType = "string"
|
||||
Boolean ValueType = "boolean"
|
||||
|
||||
ConstExprType ExprType = "const"
|
||||
VarExprType ExprType = "var"
|
||||
CompareExprType ExprType = "compare"
|
||||
LogicalExprType ExprType = "logical"
|
||||
NotExprType ExprType = "not"
|
||||
)
|
||||
|
||||
type EvalResult struct {
|
||||
@ -40,14 +48,40 @@ func (e *EvalResult) GetFloat64() (float64, error) {
|
||||
if e.Type != Number {
|
||||
return 0, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
switch v := e.Value.(type) {
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case float64:
|
||||
return v, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported type: %T", v)
|
||||
|
||||
stringValue, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("value is not a string: %v", e.Value)
|
||||
}
|
||||
|
||||
floatValue, err := strconv.ParseFloat(stringValue, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse float64: %v", err)
|
||||
}
|
||||
return floatValue, nil
|
||||
}
|
||||
|
||||
func (e *EvalResult) GetBool() (bool, error) {
|
||||
if e.Type != Boolean {
|
||||
return false, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
|
||||
strValue, ok := e.Value.(string)
|
||||
if ok {
|
||||
if strValue == "true" {
|
||||
return true, nil
|
||||
} else if strValue == "false" {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||
}
|
||||
|
||||
boolValue, ok := e.Value.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
|
||||
}
|
||||
|
||||
return boolValue, nil
|
||||
}
|
||||
|
||||
func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {
|
||||
@ -232,9 +266,17 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {
|
||||
}
|
||||
switch e.Type {
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(bool) && other.Value.(bool),
|
||||
Value: left && right,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
@ -247,9 +289,17 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {
|
||||
}
|
||||
switch e.Type {
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(bool) || other.Value.(bool),
|
||||
Value: left || right,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
@ -260,9 +310,13 @@ func (e *EvalResult) Not() (*EvalResult, error) {
|
||||
if e.Type != Boolean {
|
||||
return nil, fmt.Errorf("type mismatch: %s", e.Type)
|
||||
}
|
||||
boolValue, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: !e.Value.(bool),
|
||||
Value: !boolValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -272,9 +326,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) {
|
||||
}
|
||||
switch e.Type {
|
||||
case Boolean:
|
||||
left, err := e.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := other.GetBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EvalResult{
|
||||
Type: Boolean,
|
||||
Value: e.Value.(bool) == other.Value.(bool),
|
||||
Value: left == right,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", e.Type)
|
||||
@ -282,17 +344,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) {
|
||||
}
|
||||
|
||||
type Expr interface {
|
||||
GetType() string
|
||||
GetType() ExprType
|
||||
Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||
}
|
||||
|
||||
type ConstExpr struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
Value Value `json:"value"`
|
||||
ValueType ValueType `json:"valueType"`
|
||||
}
|
||||
|
||||
func (c ConstExpr) GetType() string { return c.Type }
|
||||
func (c ConstExpr) GetType() ExprType { return c.Type }
|
||||
|
||||
func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
return &EvalResult{
|
||||
@ -302,11 +364,11 @@ func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error
|
||||
}
|
||||
|
||||
type VarExpr struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
Selector WorkflowNodeIOValueSelector `json:"selector"`
|
||||
}
|
||||
|
||||
func (v VarExpr) GetType() string { return v.Type }
|
||||
func (v VarExpr) GetType() ExprType { return v.Type }
|
||||
|
||||
func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
if v.Selector.Id == "" {
|
||||
@ -330,13 +392,13 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||
}
|
||||
|
||||
type CompareExpr struct {
|
||||
Type string `json:"type"` // compare
|
||||
Type ExprType `json:"type"` // compare
|
||||
Op ComparisonOperator `json:"op"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (c CompareExpr) GetType() string { return c.Type }
|
||||
func (c CompareExpr) GetType() ExprType { return c.Type }
|
||||
|
||||
func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := c.Left.Eval(variables)
|
||||
@ -369,13 +431,13 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err
|
||||
}
|
||||
|
||||
type LogicalExpr struct {
|
||||
Type string `json:"type"` // logical
|
||||
Type ExprType `json:"type"` // logical
|
||||
Op LogicalOperator `json:"op"`
|
||||
Left Expr `json:"left"`
|
||||
Right Expr `json:"right"`
|
||||
}
|
||||
|
||||
func (l LogicalExpr) GetType() string { return l.Type }
|
||||
func (l LogicalExpr) GetType() ExprType { return l.Type }
|
||||
|
||||
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
left, err := l.Left.Eval(variables)
|
||||
@ -398,11 +460,11 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, err
|
||||
}
|
||||
|
||||
type NotExpr struct {
|
||||
Type string `json:"type"` // not
|
||||
Type ExprType `json:"type"` // not
|
||||
Expr Expr `json:"expr"`
|
||||
}
|
||||
|
||||
func (n NotExpr) GetType() string { return n.Type }
|
||||
func (n NotExpr) GetType() ExprType { return n.Type }
|
||||
|
||||
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
|
||||
inner, err := n.Expr.Eval(variables)
|
||||
@ -413,7 +475,7 @@ func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error)
|
||||
}
|
||||
|
||||
type rawExpr struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
}
|
||||
|
||||
func MarshalExpr(e Expr) ([]byte, error) {
|
||||
@ -427,31 +489,31 @@ func UnmarshalExpr(data []byte) (Expr, error) {
|
||||
}
|
||||
|
||||
switch typ.Type {
|
||||
case "const":
|
||||
case ConstExprType:
|
||||
var e ConstExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case "var":
|
||||
case VarExprType:
|
||||
var e VarExpr
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
case "compare":
|
||||
case CompareExprType:
|
||||
var e CompareExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToCompareExpr()
|
||||
case "logical":
|
||||
case LogicalExprType:
|
||||
var e LogicalExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.ToLogicalExpr()
|
||||
case "not":
|
||||
case NotExprType:
|
||||
var e NotExprRaw
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, err
|
||||
@ -463,7 +525,7 @@ func UnmarshalExpr(data []byte) (Expr, error) {
|
||||
}
|
||||
|
||||
type CompareExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
Op ComparisonOperator `json:"op"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
@ -487,7 +549,7 @@ func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) {
|
||||
}
|
||||
|
||||
type LogicalExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
Op LogicalOperator `json:"op"`
|
||||
Left json.RawMessage `json:"left"`
|
||||
Right json.RawMessage `json:"right"`
|
||||
@ -511,7 +573,7 @@ func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
|
||||
}
|
||||
|
||||
type NotExprRaw struct {
|
||||
Type string `json:"type"`
|
||||
Type ExprType `json:"type"`
|
||||
Expr json.RawMessage `json:"expr"`
|
||||
}
|
||||
|
||||
|
@ -88,8 +88,10 @@ type WorkflowNodeConfigForCondition struct {
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForInspect struct {
|
||||
Host string `json:"host"` // 主机
|
||||
Domain string `json:"domain"` // 域名
|
||||
Port string `json:"port"` // 端口
|
||||
Path string `json:"path"` // 路径
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
@ -134,9 +136,14 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect {
|
||||
host := maputil.GetString(n.Config, "host")
|
||||
if host == "" {
|
||||
return WorkflowNodeConfigForInspect{}
|
||||
}
|
||||
|
||||
domain := maputil.GetString(n.Config, "domain")
|
||||
if domain == "" {
|
||||
return WorkflowNodeConfigForInspect{}
|
||||
domain = host
|
||||
}
|
||||
|
||||
port := maputil.GetString(n.Config, "port")
|
||||
@ -144,9 +151,13 @@ func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect {
|
||||
port = "443"
|
||||
}
|
||||
|
||||
path := maputil.GetString(n.Config, "path")
|
||||
|
||||
return WorkflowNodeConfigForInspect{
|
||||
Domain: domain,
|
||||
Port: port,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,8 +100,8 @@ func (n *applyNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 添加中间结果
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
|
||||
n.outputs[outputCertificateValidatedKey] = "true"
|
||||
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24))
|
||||
|
||||
n.logger.Info("apply completed")
|
||||
|
||||
@ -147,8 +147,8 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||
if expirationTime > renewalInterval {
|
||||
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24)
|
||||
n.outputs[outputCertificateValidatedKey] = "true"
|
||||
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24))
|
||||
|
||||
return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
|
||||
}
|
||||
|
6
internal/workflow/node-processor/const.go
Normal file
6
internal/workflow/node-processor/const.go
Normal file
@ -0,0 +1,6 @@
|
||||
package nodeprocessor
|
||||
|
||||
const (
|
||||
outputCertificateValidatedKey = "certificate.validated"
|
||||
outputCertificateDaysLeftKey = "certificate.daysLeft"
|
||||
)
|
@ -3,9 +3,12 @@ package nodeprocessor
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
@ -26,13 +29,13 @@ func NewInspectNode(node *domain.WorkflowNode) *inspectNode {
|
||||
}
|
||||
|
||||
func (n *inspectNode) Process(ctx context.Context) error {
|
||||
n.logger.Info("enter inspect website certificate node ...")
|
||||
n.logger.Info("entering inspect certificate node...")
|
||||
|
||||
nodeConfig := n.node.GetConfigForInspect()
|
||||
|
||||
err := n.inspect(ctx, nodeConfig)
|
||||
if err != nil {
|
||||
n.logger.Warn("inspect website certificate failed: " + err.Error())
|
||||
n.logger.Warn("inspect certificate failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@ -40,18 +43,35 @@ func (n *inspectNode) Process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error {
|
||||
// 定义重试参数
|
||||
maxRetries := 3
|
||||
retryInterval := 2 * time.Second
|
||||
|
||||
var cert *tls.Certificate
|
||||
var lastError error
|
||||
var certInfo *x509.Certificate
|
||||
|
||||
domainWithPort := nodeConfig.Domain + ":" + nodeConfig.Port
|
||||
host := nodeConfig.Host
|
||||
|
||||
port := nodeConfig.Port
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
|
||||
domain := nodeConfig.Domain
|
||||
if domain == "" {
|
||||
domain = host
|
||||
}
|
||||
|
||||
path := nodeConfig.Path
|
||||
if path != "" && !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
targetAddr := fmt.Sprintf("%s:%s", host, port)
|
||||
n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain))
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, domainWithPort))
|
||||
n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@ -60,30 +80,65 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod
|
||||
}
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: domain, // Set SNI to domain for proper certificate selection
|
||||
},
|
||||
ForceAttemptHTTP2: false,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", domainWithPort, &tls.Config{
|
||||
InsecureSkipVerify: true, // Allow self-signed certificates
|
||||
})
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 15 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr)
|
||||
if path != "" {
|
||||
urlStr = urlStr + path
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil)
|
||||
if err != nil {
|
||||
lastError = fmt.Errorf("failed to connect to %s: %w", domainWithPort, err)
|
||||
lastError = fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
if domain != host {
|
||||
req.Host = domain
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "CertificateValidator/1.0")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
lastError = fmt.Errorf("HTTP request failed: %w", err)
|
||||
n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get certificate information
|
||||
certInfo := conn.ConnectionState().PeerCertificates[0]
|
||||
conn.Close()
|
||||
|
||||
// Certificate information retrieved successfully
|
||||
cert = &tls.Certificate{
|
||||
Certificate: [][]byte{certInfo.Raw},
|
||||
Leaf: certInfo,
|
||||
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
||||
resp.Body.Close()
|
||||
lastError = fmt.Errorf("no TLS certificates received in HTTP response")
|
||||
n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
certInfo = resp.TLS.PeerCertificates[0]
|
||||
resp.Body.Close()
|
||||
|
||||
lastError = nil
|
||||
n.logger.Info(fmt.Sprintf("Successfully retrieved certificate information for %s", domainWithPort))
|
||||
n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr))
|
||||
break
|
||||
}
|
||||
|
||||
@ -91,69 +146,46 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod
|
||||
return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError)
|
||||
}
|
||||
|
||||
certInfo := cert.Leaf
|
||||
if certInfo == nil {
|
||||
outputs := map[string]any{
|
||||
outputCertificateValidatedKey: "false",
|
||||
outputCertificateDaysLeftKey: "0",
|
||||
}
|
||||
n.setOutputs(outputs)
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
isValid := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)
|
||||
isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)
|
||||
|
||||
// Check domain matching
|
||||
domainMatch := false
|
||||
if len(certInfo.DNSNames) > 0 {
|
||||
for _, dnsName := range certInfo.DNSNames {
|
||||
if matchDomain(nodeConfig.Domain, dnsName) {
|
||||
domainMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if matchDomain(nodeConfig.Domain, certInfo.Subject.CommonName) {
|
||||
domainMatch = true
|
||||
domainMatch := true
|
||||
if err := certInfo.VerifyHostname(domain); err != nil {
|
||||
domainMatch = false
|
||||
}
|
||||
|
||||
isValid = isValid && domainMatch
|
||||
isValid := isValidTime && domainMatch
|
||||
|
||||
daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24)
|
||||
|
||||
// Set node outputs
|
||||
outputs := map[string]any{
|
||||
"certificate.validated": isValid,
|
||||
"certificate.daysLeft": daysRemaining,
|
||||
isValidStr := "false"
|
||||
if isValid {
|
||||
isValidStr = "true"
|
||||
}
|
||||
|
||||
outputs := map[string]any{
|
||||
outputCertificateValidatedKey: isValidStr,
|
||||
outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)),
|
||||
}
|
||||
|
||||
n.setOutputs(outputs)
|
||||
|
||||
n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d",
|
||||
targetAddr, domain, isValidStr, int(daysRemaining)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *inspectNode) setOutputs(outputs map[string]any) {
|
||||
n.outputs = outputs
|
||||
}
|
||||
|
||||
func matchDomain(requestDomain, certDomain string) bool {
|
||||
if requestDomain == certDomain {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(certDomain) > 2 && certDomain[0] == '*' && certDomain[1] == '.' {
|
||||
|
||||
wildcardSuffix := certDomain[1:]
|
||||
requestDomainLen := len(requestDomain)
|
||||
suffixLen := len(wildcardSuffix)
|
||||
|
||||
if requestDomainLen > suffixLen && requestDomain[requestDomainLen-suffixLen:] == wildcardSuffix {
|
||||
remainingPart := requestDomain[:requestDomainLen-suffixLen]
|
||||
if len(remainingPart) > 0 && !contains(remainingPart, '.') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func contains(s string, c byte) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -69,8 +69,8 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
|
||||
n.outputs[outputCertificateValidatedKey] = "true"
|
||||
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24))
|
||||
|
||||
n.logger.Info("upload completed")
|
||||
|
||||
@ -91,8 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl
|
||||
|
||||
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id)
|
||||
if lastCertificate != nil {
|
||||
n.outputs["certificate.validated"] = true
|
||||
n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
|
||||
n.outputs[outputCertificateValidatedKey] = "true"
|
||||
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(lastCertificate.ExpireAt).Hours()/24))
|
||||
return true, "the certificate has already been uploaded"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd";
|
||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||
import AddNode from "./AddNode";
|
||||
import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
|
||||
import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow";
|
||||
import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow";
|
||||
import { produce } from "immer";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
@ -32,7 +32,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
const selectors = condition.leftSelector.split("#");
|
||||
const t = selectors[2] as WorkflowNodeIoValueType;
|
||||
const left: Expr = {
|
||||
type: "var",
|
||||
type: ExprType.Var,
|
||||
selector: {
|
||||
id: selectors[0],
|
||||
name: selectors[1],
|
||||
@ -40,27 +40,10 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
},
|
||||
};
|
||||
|
||||
let value: Value = condition.rightValue;
|
||||
switch (t) {
|
||||
case "boolean":
|
||||
if (value === "true") {
|
||||
value = true;
|
||||
} else if (value === "false") {
|
||||
value = false;
|
||||
}
|
||||
break;
|
||||
case "number":
|
||||
value = parseInt(value as string);
|
||||
break;
|
||||
case "string":
|
||||
value = value as string;
|
||||
break;
|
||||
}
|
||||
|
||||
const right: Expr = { type: "const", value: value, valueType: t };
|
||||
const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t };
|
||||
|
||||
return {
|
||||
type: "compare",
|
||||
type: ExprType.Compare,
|
||||
op: condition.operator,
|
||||
left,
|
||||
right,
|
||||
@ -77,7 +60,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
|
||||
for (let i = 1; i < values.conditions.length; i++) {
|
||||
expr = {
|
||||
type: "logical",
|
||||
type: ExprType.Logical,
|
||||
op: values.logicalOperator,
|
||||
left: expr,
|
||||
right: createComparisonExpr(values.conditions[i]),
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
WorkflowNode,
|
||||
workflowNodeIOOptions,
|
||||
WorkflowNodeIoValueType,
|
||||
ExprType,
|
||||
} from "@/domain/workflow";
|
||||
import { FormInstance } from "antd";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
@ -58,7 +59,7 @@ const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
||||
rightValue: "",
|
||||
},
|
||||
],
|
||||
logicalOperator: "and",
|
||||
logicalOperator: LogicalOperator.And,
|
||||
};
|
||||
};
|
||||
|
||||
@ -67,10 +68,10 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => {
|
||||
if (!expr) return initFormModel();
|
||||
|
||||
const conditions: ConditionItem[] = [];
|
||||
let logicalOp: LogicalOperator = "and";
|
||||
let logicalOp: LogicalOperator = LogicalOperator.And;
|
||||
|
||||
const extractComparisons = (expr: Expr): void => {
|
||||
if (expr.type === "compare") {
|
||||
if (expr.type === ExprType.Compare) {
|
||||
// 确保左侧是变量,右侧是常量
|
||||
if (isVarExpr(expr.left) && isConstExpr(expr.right)) {
|
||||
conditions.push({
|
||||
@ -79,7 +80,7 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => {
|
||||
rightValue: String(expr.right.value),
|
||||
});
|
||||
}
|
||||
} else if (expr.type === "logical") {
|
||||
} else if (expr.type === ExprType.Logical) {
|
||||
logicalOp = expr.op;
|
||||
extractComparisons(expr.left);
|
||||
extractComparisons(expr.right);
|
||||
@ -304,25 +305,18 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
|
||||
const type = typeStr as WorkflowNodeIoValueType;
|
||||
|
||||
const left: Expr = {
|
||||
type: "var",
|
||||
type: ExprType.Var,
|
||||
selector: { id, name, type },
|
||||
};
|
||||
|
||||
let rightValue: any = condition.rightValue;
|
||||
if (type === "number") {
|
||||
rightValue = Number(condition.rightValue);
|
||||
} else if (type === "boolean") {
|
||||
rightValue = condition.rightValue === "true";
|
||||
}
|
||||
|
||||
const right: Expr = {
|
||||
type: "const",
|
||||
value: rightValue,
|
||||
type: ExprType.Const,
|
||||
value: condition.rightValue,
|
||||
valueType: type,
|
||||
};
|
||||
|
||||
return {
|
||||
type: "compare",
|
||||
type: ExprType.Compare,
|
||||
op: condition.operator,
|
||||
left,
|
||||
right,
|
||||
@ -339,7 +333,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
|
||||
|
||||
for (let i = 1; i < values.conditions.length; i++) {
|
||||
expr = {
|
||||
type: "logical",
|
||||
type: ExprType.Logical,
|
||||
op: values.logicalOperator,
|
||||
left: expr,
|
||||
right: createComparisonExpr(values.conditions[i]),
|
||||
|
@ -39,7 +39,7 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => {
|
||||
const config = (node.config as WorkflowNodeConfigForInspect) ?? {};
|
||||
return (
|
||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||
<Typography.Text className="truncate">{config.domain ?? ""}</Typography.Text>
|
||||
<Typography.Text className="truncate">{config.host ?? ""}</Typography.Text>
|
||||
</Flex>
|
||||
);
|
||||
}, [node]);
|
||||
|
@ -7,7 +7,7 @@ import { z } from "zod";
|
||||
import { type WorkflowNodeConfigForInspect } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
|
||||
import { validDomainName, validPortNumber } from "@/utils/validators";
|
||||
import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators";
|
||||
|
||||
type InspectNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForInspect>;
|
||||
|
||||
@ -29,6 +29,8 @@ const initFormModel = (): InspectNodeConfigFormFieldValues => {
|
||||
return {
|
||||
domain: "",
|
||||
port: "443",
|
||||
path: "",
|
||||
host: "",
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,12 +39,14 @@ const InspectNodeConfigForm = forwardRef<InspectNodeConfigFormInstance, InspectN
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
domain: z.string().refine((val) => validDomainName(val), {
|
||||
message: t("workflow_node.inspect.form.domain.placeholder"),
|
||||
host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), {
|
||||
message: t("workflow_node.inspect.form.host.placeholder"),
|
||||
}),
|
||||
domain: z.string().optional(),
|
||||
port: z.string().refine((val) => validPortNumber(val), {
|
||||
message: t("workflow_node.inspect.form.port.placeholder"),
|
||||
}),
|
||||
path: z.string().optional(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
@ -70,13 +74,21 @@ const InspectNodeConfigForm = forwardRef<InspectNodeConfigFormInstance, InspectN
|
||||
|
||||
return (
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item name="domain" label={t("workflow_node.inspect.form.domain.label")} rules={[formRule]}>
|
||||
<Input variant="filled" placeholder={t("workflow_node.inspect.form.domain.placeholder")} />
|
||||
<Form.Item name="host" label={t("workflow_node.inspect.form.host.label")} rules={[formRule]}>
|
||||
<Input variant="filled" placeholder={t("workflow_node.inspect.form.host.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="port" label={t("workflow_node.inspect.form.port.label")} rules={[formRule]}>
|
||||
<Input variant="filled" placeholder={t("workflow_node.inspect.form.port.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="domain" label={t("workflow_node.inspect.form.domain.label")} rules={[formRule]}>
|
||||
<Input variant="filled" placeholder={t("workflow_node.inspect.form.domain.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="path" label={t("workflow_node.inspect.form.path.label")} rules={[formRule]}>
|
||||
<Input variant="filled" placeholder={t("workflow_node.inspect.form.path.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = n
|
||||
name: "certificate",
|
||||
type: "certificate",
|
||||
required: true,
|
||||
label: "证书",
|
||||
label: i18n.t("workflow.variables.certificate.label"),
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -82,7 +82,7 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
|
||||
name: "certificate",
|
||||
type: "certificate",
|
||||
required: true,
|
||||
label: "证书",
|
||||
label: i18n.t("workflow.variables.certificate.label"),
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -93,7 +93,7 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
|
||||
name: "certificate",
|
||||
type: "certificate",
|
||||
required: true,
|
||||
label: "证书",
|
||||
label: i18n.t("workflow.variables.certificate.label"),
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -104,7 +104,7 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
|
||||
name: "certificate",
|
||||
type: "certificate",
|
||||
required: true,
|
||||
label: "证书",
|
||||
label: i18n.t("workflow.variables.certificate.label"),
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -161,6 +161,8 @@ export type WorkflowNodeConfigForUpload = {
|
||||
export type WorkflowNodeConfigForInspect = {
|
||||
domain: string;
|
||||
port: string;
|
||||
host: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForDeploy = {
|
||||
@ -200,14 +202,20 @@ export type WorkflowNodeIO = {
|
||||
valueSelector?: WorkflowNodeIOValueSelector;
|
||||
};
|
||||
|
||||
export const VALUE_TYPES = Object.freeze({
|
||||
STRING: "string",
|
||||
NUMBER: "number",
|
||||
BOOLEAN: "boolean",
|
||||
} as const);
|
||||
|
||||
export type WorkflowNodeIoValueType = (typeof VALUE_TYPES)[keyof typeof VALUE_TYPES];
|
||||
|
||||
export type WorkflowNodeIOValueSelector = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: WorkflowNodeIoValueType;
|
||||
};
|
||||
|
||||
export type WorkflowNodeIoValueType = "string" | "number" | "boolean";
|
||||
|
||||
type WorkflowNodeIOOptions = {
|
||||
label: string;
|
||||
value: string;
|
||||
@ -224,12 +232,12 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => {
|
||||
switch (output.type) {
|
||||
case "certificate":
|
||||
rs.options.push({
|
||||
label: `${node.name} - ${output.label} - 是否有效`,
|
||||
label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.is_validated.label")}`,
|
||||
value: `${node.id}#${output.name}.validated#boolean`,
|
||||
});
|
||||
|
||||
rs.options.push({
|
||||
label: `${node.name} - ${output.label} - 剩余天数`,
|
||||
label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.days_left.label")}`,
|
||||
value: `${node.id}#${output.name}.daysLeft#number`,
|
||||
});
|
||||
break;
|
||||
@ -254,22 +262,34 @@ export type Value = string | number | boolean;
|
||||
|
||||
export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is";
|
||||
|
||||
export type LogicalOperator = "and" | "or" | "not";
|
||||
export enum LogicalOperator {
|
||||
And = "and",
|
||||
Or = "or",
|
||||
Not = "not",
|
||||
}
|
||||
|
||||
export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType };
|
||||
export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector };
|
||||
export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr };
|
||||
export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr };
|
||||
export type NotExpr = { type: "not"; expr: Expr };
|
||||
export enum ExprType {
|
||||
Const = "const",
|
||||
Var = "var",
|
||||
Compare = "compare",
|
||||
Logical = "logical",
|
||||
Not = "not",
|
||||
}
|
||||
|
||||
export type ConstExpr = { type: ExprType.Const; value: string; valueType: WorkflowNodeIoValueType };
|
||||
export type VarExpr = { type: ExprType.Var; selector: WorkflowNodeIOValueSelector };
|
||||
export type CompareExpr = { type: ExprType.Compare; op: ComparisonOperator; left: Expr; right: Expr };
|
||||
export type LogicalExpr = { type: ExprType.Logical; op: LogicalOperator; left: Expr; right: Expr };
|
||||
export type NotExpr = { type: ExprType.Not; expr: Expr };
|
||||
|
||||
export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr;
|
||||
|
||||
export const isConstExpr = (expr: Expr): expr is ConstExpr => {
|
||||
return expr.type === "const";
|
||||
return expr.type === ExprType.Const;
|
||||
};
|
||||
|
||||
export const isVarExpr = (expr: Expr): expr is VarExpr => {
|
||||
return expr.type === "var";
|
||||
return expr.type === ExprType.Var;
|
||||
};
|
||||
|
||||
// #endregion
|
||||
|
@ -53,5 +53,9 @@
|
||||
"workflow.detail.orchestration.action.run": "Run",
|
||||
"workflow.detail.orchestration.action.run.confirm": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?",
|
||||
"workflow.detail.orchestration.action.run.prompt": "Running... Please check the history later",
|
||||
"workflow.detail.runs.tab": "History runs"
|
||||
"workflow.detail.runs.tab": "History runs",
|
||||
|
||||
"workflow.variables.is_validated.label": "Is valid",
|
||||
"workflow.variables.days_left.label": "Days left",
|
||||
"workflow.variables.certificate.label": "Certificate"
|
||||
}
|
||||
|
@ -806,6 +806,10 @@
|
||||
"workflow_node.inspect.form.domain.placeholder": "Please enter domain name",
|
||||
"workflow_node.inspect.form.port.label": "Port",
|
||||
"workflow_node.inspect.form.port.placeholder": "Please enter port",
|
||||
"workflow_node.inspect.form.host.label": "Host",
|
||||
"workflow_node.inspect.form.host.placeholder": "Please enter host",
|
||||
"workflow_node.inspect.form.path.label": "Path",
|
||||
"workflow_node.inspect.form.path.placeholder": "Please enter path",
|
||||
|
||||
"workflow_node.notify.label": "Notification",
|
||||
"workflow_node.notify.form.subject.label": "Subject",
|
||||
|
@ -53,5 +53,9 @@
|
||||
"workflow.detail.orchestration.action.run": "执行",
|
||||
"workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
|
||||
"workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史",
|
||||
"workflow.detail.runs.tab": "执行历史"
|
||||
"workflow.detail.runs.tab": "执行历史",
|
||||
|
||||
"workflow.variables.is_validated.label": "是否有效",
|
||||
"workflow.variables.days_left.label": "剩余天数",
|
||||
"workflow.variables.certificate.label": "证书"
|
||||
}
|
||||
|
@ -805,6 +805,10 @@
|
||||
"workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名",
|
||||
"workflow_node.inspect.form.port.label": "端口号",
|
||||
"workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号",
|
||||
"workflow_node.inspect.form.host.label": "Host",
|
||||
"workflow_node.inspect.form.host.placeholder": "请输入 Host",
|
||||
"workflow_node.inspect.form.path.label": "Path",
|
||||
"workflow_node.inspect.form.path.placeholder": "请输入 Path",
|
||||
|
||||
"workflow_node.notify.label": "推送通知",
|
||||
"workflow_node.notify.form.subject.label": "通知主题",
|
||||
|
Loading…
x
Reference in New Issue
Block a user