package acme import ( "context" "fmt" "net" "net/url" "strings" "github.com/smallstep/certificates/acme" ) // NewLinker returns a new Directory type. func NewLinker(dns, prefix string) Linker { _, _, err := net.SplitHostPort(dns) if err != nil && strings.Contains(err.Error(), "too many colons in address") { // this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334 // in case a port was appended to this wrong format, we try to extract the port, then check if it's // still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of // these cases, then the input dns is not changed. lastIndex := strings.LastIndex(dns, ":") hostPart, portPart := dns[:lastIndex], dns[lastIndex+1:] if ip := net.ParseIP(hostPart); ip != nil { dns = "[" + hostPart + "]:" + portPart } else if ip := net.ParseIP(dns); ip != nil { dns = "[" + dns + "]" } } return &linker{prefix: prefix, dns: dns} } // Linker interface for generating links for ACME resources. type Linker interface { GetLink(ctx context.Context, typ LinkType, inputs ...string) string GetUnescapedPathSuffix(typ LinkType, provName string, inputs ...string) string LinkOrder(ctx context.Context, o *acme.Order) LinkAccount(ctx context.Context, o *acme.Account) LinkChallenge(ctx context.Context, o *acme.Challenge, azID string) LinkAuthorization(ctx context.Context, o *acme.Authorization) LinkOrdersByAccountID(ctx context.Context, orders []string) } type linkerKey struct{} // NewLinkerContext adds the given linker to the context. func NewLinkerContext(ctx context.Context, v Linker) context.Context { return context.WithValue(ctx, linkerKey{}, v) } // LinkerFromContext returns the current linker from the given context. func LinkerFromContext(ctx context.Context) (v Linker, ok bool) { v, ok = ctx.Value(linkerKey{}).(Linker) return } // MustLinkerFromContext returns the current linker from the given context. It // will panic if it's not in the context. func MustLinkerFromContext(ctx context.Context) Linker { if v, ok := LinkerFromContext(ctx); !ok { panic("acme linker is not the context") } else { return v } } // linker generates ACME links. type linker struct { prefix string dns string } func (l *linker) GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string { switch typ { case NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType: return fmt.Sprintf("/%s/%s", provisionerName, typ) case AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType: return fmt.Sprintf("/%s/%s/%s", provisionerName, typ, inputs[0]) case ChallengeLinkType: return fmt.Sprintf("/%s/%s/%s/%s", provisionerName, typ, inputs[0], inputs[1]) case OrdersByAccountLinkType: return fmt.Sprintf("/%s/%s/%s/orders", provisionerName, AccountLinkType, inputs[0]) case FinalizeLinkType: return fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLinkType, inputs[0]) default: return "" } } // GetLink is a helper for GetLinkExplicit func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string { var ( provName string baseURL = baseURLFromContext(ctx) u = url.URL{} ) if p, err := provisionerFromContext(ctx); err == nil && p != nil { provName = p.GetName() } // Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351 if baseURL != nil { u = *baseURL } u.Path = l.GetUnescapedPathSuffix(typ, provName, inputs...) // If no Scheme is set, then default to https. if u.Scheme == "" { u.Scheme = "https" } // If no Host is set, then use the default (first DNS attr in the ca.json). if u.Host == "" { u.Host = l.dns } u.Path = l.prefix + u.Path return u.String() } // LinkType captures the link type. type LinkType int const ( // NewNonceLinkType new-nonce NewNonceLinkType LinkType = iota // NewAccountLinkType new-account NewAccountLinkType // AccountLinkType account AccountLinkType // OrderLinkType order OrderLinkType // NewOrderLinkType new-order NewOrderLinkType // OrdersByAccountLinkType list of orders owned by account OrdersByAccountLinkType // FinalizeLinkType finalize order FinalizeLinkType // NewAuthzLinkType authz NewAuthzLinkType // AuthzLinkType new-authz AuthzLinkType // ChallengeLinkType challenge ChallengeLinkType // CertificateLinkType certificate CertificateLinkType // DirectoryLinkType directory DirectoryLinkType // RevokeCertLinkType revoke certificate RevokeCertLinkType // KeyChangeLinkType key rollover KeyChangeLinkType ) func (l LinkType) String() string { switch l { case NewNonceLinkType: return "new-nonce" case NewAccountLinkType: return "new-account" case AccountLinkType: return "account" case NewOrderLinkType: return "new-order" case OrderLinkType: return "order" case NewAuthzLinkType: return "new-authz" case AuthzLinkType: return "authz" case ChallengeLinkType: return "challenge" case CertificateLinkType: return "certificate" case DirectoryLinkType: return "directory" case RevokeCertLinkType: return "revoke-cert" case KeyChangeLinkType: return "key-change" default: return fmt.Sprintf("unexpected LinkType '%d'", int(l)) } } // LinkOrder sets the ACME links required by an ACME order. func (l *linker) LinkOrder(ctx context.Context, o *acme.Order) { o.AuthorizationURLs = make([]string, len(o.AuthorizationIDs)) for i, azID := range o.AuthorizationIDs { o.AuthorizationURLs[i] = l.GetLink(ctx, AuthzLinkType, azID) } o.FinalizeURL = l.GetLink(ctx, FinalizeLinkType, o.ID) if o.CertificateID != "" { o.CertificateURL = l.GetLink(ctx, CertificateLinkType, o.CertificateID) } } // LinkAccount sets the ACME links required by an ACME account. func (l *linker) LinkAccount(ctx context.Context, acc *acme.Account) { acc.OrdersURL = l.GetLink(ctx, OrdersByAccountLinkType, acc.ID) } // LinkChallenge sets the ACME links required by an ACME challenge. func (l *linker) LinkChallenge(ctx context.Context, ch *acme.Challenge, azID string) { ch.URL = l.GetLink(ctx, ChallengeLinkType, azID, ch.ID) } // LinkAuthorization sets the ACME links required by an ACME authorization. func (l *linker) LinkAuthorization(ctx context.Context, az *acme.Authorization) { for _, ch := range az.Challenges { l.LinkChallenge(ctx, ch, az.ID) } } // LinkOrdersByAccountID converts each order ID to an ACME link. func (l *linker) LinkOrdersByAccountID(ctx context.Context, orders []string) { for i, id := range orders { orders[i] = l.GetLink(ctx, OrderLinkType, id) } }