Documentation ¶
Overview ¶
Package caddytls facilitates the management of TLS assets and integrates Let's Encrypt functionality into Caddy with first-class support for creating and renewing certificates automatically. It also implements the tls directive.
This package is meant to be used by Caddy server types. To use the tls directive, a server type must import this package and call RegisterConfigGetter(). The server type must make and keep track of the caddytls.Config structs that this package produces. It must also add tls to its list of directives. When it comes time to make the server instances, the server type can call MakeTLSConfig() to convert a []caddytls.Config to a single tls.Config for use in tls.NewListener(). It is also recommended to call RotateSessionTicketKeys() when starting a new listener.
Index ¶
- Constants
- Variables
- func DeleteOldStapleFiles()
- func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost string) bool
- func HostQualifies(hostname string) bool
- func MakeTLSConfig(configs []*Config) (*tls.Config, error)
- func QualifiesForManagedTLS(c ConfigHolder) bool
- func RegisterConfigGetter(serverType string, fn ConfigGetter)
- func RegisterDNSProvider(name string, provider DNSProviderConstructor)
- func RegisterStorageProvider(name string, provider StorageConstructor)
- func RenewManagedCertificates(allowPrompts bool) (err error)
- func Revoke(host string) error
- func RotateSessionTicketKeys(cfg *tls.Config) chan struct{}
- func SetDefaultTLSParams(config *Config)
- func UpdateOCSPStaples()
- type ACMEClient
- type Certificate
- type ChallengeProvider
- type ClientHelloInfo
- type Config
- func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error)
- func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error)
- func (c *Config) ObtainCert(name string, allowPrompts bool) error
- func (c *Config) RenewCert(name string, allowPrompts bool) error
- func (c *Config) StorageFor(caURL string) (Storage, error)
- type ConfigGetter
- type ConfigHolder
- type DNSProviderConstructor
- type ErrNotExist
- type FileStorage
- func (s *FileStorage) DeleteSite(domain string) error
- func (s *FileStorage) LoadSite(domain string) (*SiteData, error)
- func (s *FileStorage) LoadUser(email string) (*UserData, error)
- func (s *FileStorage) MostRecentUserEmail() string
- func (s *FileStorage) SiteExists(domain string) (bool, error)
- func (s *FileStorage) StoreSite(domain string, data *SiteData) error
- func (s *FileStorage) StoreUser(email string, data *UserData) error
- type Locker
- type OnDemandState
- type SiteData
- type Storage
- type StorageConstructor
- type User
- type UserData
- type Waiter
Constants ¶
const ( // HTTPChallengePort is the officially designated port for // the HTTP challenge according to the ACME spec. HTTPChallengePort = "80" // TLSSNIChallengePort is the officially designated port for // the TLS-SNI challenge according to the ACME spec. TLSSNIChallengePort = "443" // DefaultHTTPAlternatePort is the port on which the ACME // client will open a listener and solve the HTTP challenge. // If this alternate port is used instead of the default // port, then whatever is listening on the default port must // be capable of proxying or forwarding the request to this // alternate port. DefaultHTTPAlternatePort = "5033" // CertCacheInstStorageKey is the name of the key for // accessing the certificate storage on the *caddy.Instance. CertCacheInstStorageKey = "tls_cert_cache" )
const ( // NumTickets is how many tickets to hold and consider // to decrypt TLS sessions. NumTickets = 4 // TicketRotateInterval is how often to generate // new ticket for TLS PFS encryption TicketRotateInterval = 10 * time.Hour )
const ( // RenewInterval is how often to check certificates for renewal. RenewInterval = 12 * time.Hour // RenewDurationBefore is how long before expiration to renew certificates. RenewDurationBefore = (24 * time.Hour) * 30 // RenewDurationBeforeAtStartup is how long before expiration to require // a renewed certificate when the process is first starting up (see #1680). // A wider window between RenewDurationBefore and this value will allow // Caddy to start under duress but hopefully this duration will give it // enough time for the blockage to be relieved. RenewDurationBeforeAtStartup = (24 * time.Hour) * 7 // OCSPInterval is how often to check if OCSP stapling needs updating. OCSPInterval = 1 * time.Hour )
Variables ¶
var ( // DefaultEmail represents the Let's Encrypt account email to use if none provided. DefaultEmail string // Agreed indicates whether user has agreed to the Let's Encrypt SA. Agreed bool // DefaultCAUrl is the default URL to the CA's ACME directory endpoint. // It's very important to set this unless you set it in every Config. DefaultCAUrl string // DefaultKeyType is used as the type of key for new certificates // when no other key type is specified. DefaultKeyType = acme.RSA2048 // DisableHTTPChallenge will disable all HTTP challenges. DisableHTTPChallenge bool // DisableTLSSNIChallenge will disable all TLS-SNI challenges. DisableTLSSNIChallenge bool )
var ClientHelloTelemetry = true
ClientHelloTelemetry determines whether to report TLS ClientHellos to telemetry. Disable if doing it from a different package.
var KnownACMECAs = []string{
"https://acme-v02.api.letsencrypt.org/directory",
}
KnownACMECAs is a list of ACME directory endpoints of known, public, and trusted ACME-compatible certificate authorities.
var SupportedCiphersMap = map[string]uint16{ "ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "ECDHE-ECDSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, "ECDHE-RSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, "ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, "RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, "ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, }
Map of supported ciphers, used only for parsing config.
Note that, at time of writing, HTTP/2 blacklists 276 cipher suites, including all but four of the suites below (the four GCM suites). See https://http2.github.io/http2-spec/#BadCipherSuites
TLS_FALLBACK_SCSV is not in this list because we manually ensure it is always added (even though it is not technically a cipher suite).
This map, like any map, is NOT ORDERED. Do not range over this map.
var SupportedProtocols = map[string]uint16{ "tls1.0": tls.VersionTLS10, "tls1.1": tls.VersionTLS11, "tls1.2": tls.VersionTLS12, }
Map of supported protocols. HTTP/2 only supports TLS 1.2 and higher. If updating this map, also update tlsProtocolStringToMap in caddyhttp/fastcgi/fastcgi.go
Functions ¶
func DeleteOldStapleFiles ¶
func DeleteOldStapleFiles()
DeleteOldStapleFiles deletes cached OCSP staples that have expired. TODO: Should we do this for certificates too?
func HTTPChallengeHandler ¶
HTTPChallengeHandler proxies challenge requests to ACME client if the request path starts with challengeBasePath, if the HTTP challenge is not disabled, and if we are known to be obtaining a certificate for the name. It returns true if it handled the request and no more needs to be done; it returns false if this call was a no-op and the request still needs handling.
func HostQualifies ¶
HostQualifies returns true if the hostname alone appears eligible for automatic HTTPS. For example: localhost, empty hostname, and IP addresses are not eligible because we cannot obtain certificates for those names. Wildcard names are allowed, as long as they conform to CABF requirements (only one wildcard label, and it must be the left-most label).
func MakeTLSConfig ¶
MakeTLSConfig makes a tls.Config from configs. The returned tls.Config is programmed to load the matching caddytls.Config based on the hostname in SNI, but that's all. This is used to create a single TLS configuration for a listener (a group of sites).
func QualifiesForManagedTLS ¶
func QualifiesForManagedTLS(c ConfigHolder) bool
QualifiesForManagedTLS returns true if c qualifies for for managed TLS (but not on-demand TLS specifically). It does NOT check to see if a cert and key already exist for the config. If the return value is true, you should be OK to set c.TLSConfig().Managed to true; then you should check that value in the future instead, because the process of setting up the config may make it look like it doesn't qualify even though it originally did.
func RegisterConfigGetter ¶
func RegisterConfigGetter(serverType string, fn ConfigGetter)
RegisterConfigGetter registers fn as the way to get a Config for server type serverType.
func RegisterDNSProvider ¶
func RegisterDNSProvider(name string, provider DNSProviderConstructor)
RegisterDNSProvider registers provider by name for solving the ACME DNS challenge.
func RegisterStorageProvider ¶
func RegisterStorageProvider(name string, provider StorageConstructor)
RegisterStorageProvider registers provider by name for storing tls data
func RenewManagedCertificates ¶
RenewManagedCertificates renews managed certificates, including ones loaded on-demand.
func Revoke ¶
Revoke revokes the certificate for host via ACME protocol. It assumes the certificate was obtained from the CA at DefaultCAUrl.
func RotateSessionTicketKeys ¶
RotateSessionTicketKeys rotates the TLS session ticket keys on cfg every TicketRotateInterval. It spawns a new goroutine so this function does NOT block. It returns a channel you should close when you are ready to stop the key rotation, like when the server using cfg is no longer running.
func SetDefaultTLSParams ¶
func SetDefaultTLSParams(config *Config)
SetDefaultTLSParams sets the default TLS cipher suites, protocol versions, and server preferences of a server.Config if they were not previously set (it does not overwrite; only fills in missing values).
func UpdateOCSPStaples ¶
func UpdateOCSPStaples()
UpdateOCSPStaples updates the OCSP stapling in all eligible, cached certificates.
OCSP maintenance strives to abide the relevant points on Ryan Sleevi's recommendations for good OCSP support: https://gist.github.com/sleevi/5efe9ef98961ecfb4da8
Types ¶
type ACMEClient ¶
type ACMEClient struct { AllowPrompts bool // contains filtered or unexported fields }
ACMEClient is a wrapper over acme.Client with some custom state attached. It is used to obtain, renew, and revoke certificates with ACME.
func (*ACMEClient) Obtain ¶
func (c *ACMEClient) Obtain(name string) error
Obtain obtains a single certificate for name. It stores the certificate on the disk if successful. This function is safe for concurrent use.
Right now our storage mechanism only supports one name per certificate, so this function (along with Renew and Revoke) only accepts one domain as input. It can be easily modified to support SAN certificates if our storage mechanism is upgraded later.
Callers who have access to a Config value should use the ObtainCert method on that instead of this lower-level method.
func (*ACMEClient) Renew ¶
func (c *ACMEClient) Renew(name string) error
Renew renews the managed certificate for name. It puts the renewed certificate into storage (not the cache). This function is safe for concurrent use.
Callers who have access to a Config value should use the RenewCert method on that instead of this lower-level method.
func (*ACMEClient) Revoke ¶
func (c *ACMEClient) Revoke(name string) error
Revoke revokes the certificate for name and deletes it from storage.
type Certificate ¶
type Certificate struct { tls.Certificate // Names is the list of names this certificate is written for. // The first is the CommonName (if any), the rest are SAN. Names []string // NotAfter is when the certificate expires. NotAfter time.Time // OCSP contains the certificate's parsed OCSP response. OCSP *ocsp.Response // The hex-encoded hash of this cert's chain's bytes. Hash string // contains filtered or unexported fields }
Certificate is a tls.Certificate with associated metadata tacked on. Even if the metadata can be obtained by parsing the certificate, we are more efficient by extracting the metadata onto this struct.
type ChallengeProvider ¶
type ChallengeProvider acme.ChallengeProvider
ChallengeProvider defines an own type that should be used in Caddy plugins over acme.ChallengeProvider. Using acme.ChallengeProvider causes version mismatches with vendored dependencies (see https://github.com/mattfarina/golang-broken-vendor)
acme.ChallengeProvider is an interface that allows the implementation of custom challenge providers. For more details, see: https://godoc.org/github.com/xenolf/lego/acme#ChallengeProvider
type ClientHelloInfo ¶
type ClientHelloInfo struct { Version uint16 `json:"version,omitempty"` CipherSuites []uint16 `json:"cipher_suites,omitempty"` Extensions []uint16 `json:"extensions,omitempty"` CompressionMethods []byte `json:"compression,omitempty"` Curves []tls.CurveID `json:"curves,omitempty"` Points []uint8 `json:"points,omitempty"` // Whether a couple of fields are unknown; if not, the key will encode // differently to reflect that, as opposed to being known empty values. // (some fields may be unknown depending on what package is being used; // i.e. the Go standard lib doesn't expose some things) // (very important to NOT encode these to JSON) ExtensionsUnknown bool `json:"-"` CompressionMethodsUnknown bool `json:"-"` }
ClientHelloInfo is our own version of the standard lib's tls.ClientHelloInfo. As of May 2018, any fields populated by the Go standard library are not guaranteed to have their values in the original order as on the wire.
func (ClientHelloInfo) Key ¶
func (info ClientHelloInfo) Key() string
Key returns a standardized string form of the data in info, useful for identifying duplicates.
type Config ¶
type Config struct { // The hostname or class of hostnames this config is // designated for; can contain wildcard characters // according to RFC 6125 §6.4.3 - this field MUST // be set in order for things to work as expected Hostname string // Whether TLS is enabled Enabled bool // Minimum and maximum protocol versions to allow ProtocolMinVersion uint16 ProtocolMaxVersion uint16 // The list of cipher suites; first should be // TLS_FALLBACK_SCSV to prevent degrade attacks Ciphers []uint16 // Whether to prefer server cipher suites PreferServerCipherSuites bool // The list of preferred curves CurvePreferences []tls.CurveID // Client authentication policy ClientAuth tls.ClientAuthType // List of client CA certificates to allow, if // client authentication is enabled ClientCerts []string // Manual means user provides own certs and keys Manual bool // Managed means config qualifies for implicit, // automatic, managed TLS; as opposed to the user // providing and managing the certificate manually Managed bool // OnDemand means the class of hostnames this // config applies to may obtain and manage // certificates at handshake-time (as opposed // to pre-loaded at startup); OnDemand certs // will be managed the same way as preloaded // ones, however, if an OnDemand cert fails to // renew, it is removed from the in-memory // cache; if this is true, Managed must // necessarily be true OnDemand bool // SelfSigned means that this hostname is // served with a self-signed certificate // that we generated in memory for convenience SelfSigned bool // The endpoint of the directory for the ACME // CA we are to use CAUrl string // The host (ONLY the host, not port) to listen // on if necessary to start a listener to solve // an ACME challenge ListenHost string // The alternate port (ONLY port, not host) to // use for the ACME HTTP challenge; if non-empty, // this port will be used instead of // HTTPChallengePort to spin up a listener for // the HTTP challenge AltHTTPPort string // The alternate port (ONLY port, not host) // to use for the ACME TLS-SNI challenge. // The system must forward TLSSNIChallengePort // to this port for challenge to succeed AltTLSSNIPort string // The string identifier of the DNS provider // to use when solving the ACME DNS challenge DNSProvider string // The email address to use when creating or // using an ACME account (fun fact: if this // is set to "off" then this config will not // qualify for managed TLS) ACMEEmail string // The type of key to use when generating // certificates KeyType acme.KeyType // The storage creator; use StorageFor() to get a guaranteed // non-nil Storage instance. Note, Caddy may call this frequently // so implementors are encouraged to cache any heavy instantiations. StorageProvider string // The state needed to operate on-demand TLS OnDemandState OnDemandState // Add the must staple TLS extension to the CSR generated by lego/acme MustStaple bool // The list of protocols to choose from for Application Layer // Protocol Negotiation (ALPN). ALPN []string // The map of hostname to certificate hash. This is used to complete // handshakes and serve the right certificate given the SNI. Certificates map[string]string // contains filtered or unexported fields }
Config describes how TLS should be configured and used.
func NewConfig ¶
NewConfig returns a new Config with a pointer to the instance's certificate cache. You will usually need to set Other fields on the returned Config for successful practical use.
func (*Config) CacheManagedCertificate ¶
func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error)
CacheManagedCertificate loads the certificate for domain into the cache, from the TLS storage for managed certificates. It returns a copy of the Certificate that was put into the cache.
This method is safe for concurrent use.
func (*Config) GetCertificate ¶
func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error)
GetCertificate gets a certificate to satisfy clientHello. In getting the certificate, it abides the rules and settings defined in the Config that matches clientHello.ServerName. It first checks the in- memory cache, then, if the config enables "OnDemand", it accesses disk, then accesses the network if it must obtain a new certificate via ACME.
This method is safe for use as a tls.Config.GetCertificate callback.
func (*Config) ObtainCert ¶
ObtainCert obtains a certificate for name using c, as long as a certificate does not already exist in storage for that name. The name must qualify and c must be flagged as Managed. This function is a no-op if storage already has a certificate for name.
It only obtains and stores certificates (and their keys), it does not load them into memory. If allowPrompts is true, the user may be shown a prompt.
func (*Config) RenewCert ¶
RenewCert renews the certificate for name using c. It stows the renewed certificate and its assets in storage if successful.
func (*Config) StorageFor ¶
StorageFor obtains a TLS Storage instance for the given CA URL which should be unique for every different ACME CA. If a StorageCreator is set on this Config, it will be used. Otherwise the default file storage implementation is used. When the error is nil, this is guaranteed to return a non-nil Storage instance.
type ConfigGetter ¶
type ConfigGetter func(c *caddy.Controller) *Config
ConfigGetter gets a Config keyed by key.
type ConfigHolder ¶
ConfigHolder is any type that has a Config; it presumably is connected to a hostname and port on which it is serving.
type DNSProviderConstructor ¶
type DNSProviderConstructor func(credentials ...string) (ChallengeProvider, error)
DNSProviderConstructor is a function that takes credentials and returns a type that can solve the ACME DNS challenges.
type ErrNotExist ¶
type ErrNotExist interface { error }
ErrNotExist is returned by Storage implementations when a resource is not found. It is similar to os.ErrNotExist except this is a type, not a variable.
type FileStorage ¶
FileStorage facilitates forming file paths derived from a root directory. It is used to get file paths in a consistent, cross-platform way or persisting ACME assets on the file system.
func (*FileStorage) DeleteSite ¶
func (s *FileStorage) DeleteSite(domain string) error
DeleteSite implements Storage.DeleteSite by deleting just the cert from disk. If it is not present, an instance of ErrNotExist is returned.
func (*FileStorage) LoadSite ¶
func (s *FileStorage) LoadSite(domain string) (*SiteData, error)
LoadSite implements Storage.LoadSite by loading it from disk. If it is not present, an instance of ErrNotExist is returned.
func (*FileStorage) LoadUser ¶
func (s *FileStorage) LoadUser(email string) (*UserData, error)
LoadUser implements Storage.LoadUser by loading it from disk. If it is not present, an instance of ErrNotExist is returned.
func (*FileStorage) MostRecentUserEmail ¶
func (s *FileStorage) MostRecentUserEmail() string
MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the most recently written sub directory in the users' directory. It is named after the email address. This corresponds to the most recent call to StoreUser.
func (*FileStorage) SiteExists ¶
func (s *FileStorage) SiteExists(domain string) (bool, error)
SiteExists implements Storage.SiteExists by checking for the presence of cert and key files.
type Locker ¶
type Locker interface { // TryLock will return immediatedly with or without acquiring the lock. // If a lock could be obtained, (nil, nil) is returned and you may // continue normally. If not (meaning another process is already // working on that name), a Waiter value will be returned upon // which you can Wait() until it is finished, and then return // when it unblocks. If waiting, do not unlock! // // To prevent deadlocks, all implementations (where this concern // is relevant) should put a reasonable expiration on the lock in // case Unlock is unable to be called due to some sort of storage // system failure or crash. TryLock(name string) (Waiter, error) // Unlock unlocks the mutex for name. Only callers of TryLock who // successfully obtained the lock (no Waiter value was returned) // should call this method, and it should be called only after // the obtain/renew and store are finished, even if there was // an error (or a timeout). Unlock should also clean up any // unused resources allocated during TryLock. Unlock(name string) error }
Locker provides support for mutual exclusion
type OnDemandState ¶
type OnDemandState struct { // The number of certificates that have been issued on-demand // by this config. It is only safe to modify this count atomically. // If it reaches MaxObtain, on-demand issuances must fail. ObtainedCount int32 // Set from max_certs in tls config, it specifies the // maximum number of certificates that can be issued. MaxObtain int32 // The url to call to check if an on-demand tls certificate should // be issued. If a request to the URL fails or returns a non 2xx // status on-demand issuances must fail. AskURL *url.URL }
OnDemandState contains some state relevant for providing on-demand TLS.
type SiteData ¶
type SiteData struct { // Cert is the public cert byte array. Cert []byte // Key is the private key byte array. Key []byte // Meta is metadata about the site used by Caddy. Meta []byte }
SiteData contains persisted items pertaining to an individual site.
type Storage ¶
type Storage interface { // SiteExists returns true if this site exists in storage. // Site data is considered present when StoreSite has been called // successfully (without DeleteSite having been called, of course). SiteExists(domain string) (bool, error) // LoadSite obtains the site data from storage for the given domain and // returns it. If data for the domain does not exist, an error value // of type ErrNotExist is returned. For multi-server storage, care // should be taken to make this load atomic to prevent race conditions // that happen with multiple data loads. LoadSite(domain string) (*SiteData, error) // StoreSite persists the given site data for the given domain in // storage. For multi-server storage, care should be taken to make this // call atomic to prevent half-written data on failure of an internal // intermediate storage step. Implementers can trust that at runtime // this function will only be invoked after LockRegister and before // UnlockRegister of the same domain. StoreSite(domain string, data *SiteData) error // DeleteSite deletes the site for the given domain from storage. // Multi-server implementations should attempt to make this atomic. If // the site does not exist, an error value of type ErrNotExist is returned. DeleteSite(domain string) error // LoadUser obtains user data from storage for the given email and // returns it. If data for the email does not exist, an error value // of type ErrNotExist is returned. Multi-server implementations // should take care to make this operation atomic for all loaded // data items. LoadUser(email string) (*UserData, error) // StoreUser persists the given user data for the given email in // storage. Multi-server implementations should take care to make this // operation atomic for all stored data items. StoreUser(email string, data *UserData) error // MostRecentUserEmail provides the most recently used email parameter // in StoreUser. The result is an empty string if there are no // persisted users in storage. MostRecentUserEmail() string // Locker is necessary because synchronizing certificate maintenance // depends on how storage is implemented. Locker }
Storage is an interface abstracting all storage used by Caddy's TLS subsystem. Implementations of this interface store both site and user data.
type StorageConstructor ¶
StorageConstructor is a function type that is used in the Config to instantiate a new Storage instance. This function can return a nil Storage even without an error.
type User ¶
type User struct { Email string Registration *acme.RegistrationResource // contains filtered or unexported fields }
User represents a Let's Encrypt user account.
func (User) GetPrivateKey ¶
func (u User) GetPrivateKey() crypto.PrivateKey
GetPrivateKey gets u's private key.
func (User) GetRegistration ¶
func (u User) GetRegistration() *acme.RegistrationResource
GetRegistration gets u's registration resource.
Source Files ¶
Directories ¶
Path | Synopsis |
---|---|
Package storagetest provides utilities to assist in testing caddytls.Storage implementations.
|
Package storagetest provides utilities to assist in testing caddytls.Storage implementations. |