mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
637 lines
16 KiB
Go
637 lines
16 KiB
Go
// Copyright 2023 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/klauspost/compress/s2"
|
|
"golang.org/x/crypto/ocsp"
|
|
|
|
"github.com/nats-io/nats-server/v2/server/certidp"
|
|
)
|
|
|
|
const (
|
|
OCSPResponseCacheDefaultDir = "_rc_"
|
|
OCSPResponseCacheDefaultFilename = "cache.json"
|
|
OCSPResponseCacheDefaultTempFilePrefix = "ocsprc-*"
|
|
OCSPResponseCacheMinimumSaveInterval = 1 * time.Second
|
|
OCSPResponseCacheDefaultSaveInterval = 5 * time.Minute
|
|
)
|
|
|
|
type OCSPResponseCacheType int
|
|
|
|
const (
|
|
NONE OCSPResponseCacheType = iota + 1
|
|
LOCAL
|
|
)
|
|
|
|
var OCSPResponseCacheTypeMap = map[string]OCSPResponseCacheType{
|
|
"none": NONE,
|
|
"local": LOCAL,
|
|
}
|
|
|
|
type OCSPResponseCacheConfig struct {
|
|
Type OCSPResponseCacheType
|
|
LocalStore string
|
|
PreserveRevoked bool
|
|
SaveInterval float64
|
|
}
|
|
|
|
func NewOCSPResponseCacheConfig() *OCSPResponseCacheConfig {
|
|
return &OCSPResponseCacheConfig{
|
|
Type: LOCAL,
|
|
LocalStore: OCSPResponseCacheDefaultDir,
|
|
PreserveRevoked: false,
|
|
SaveInterval: OCSPResponseCacheDefaultSaveInterval.Seconds(),
|
|
}
|
|
}
|
|
|
|
type OCSPResponseCacheStats struct {
|
|
Responses int64 `json:"size"`
|
|
Hits int64 `json:"hits"`
|
|
Misses int64 `json:"misses"`
|
|
Revokes int64 `json:"revokes"`
|
|
Goods int64 `json:"goods"`
|
|
Unknowns int64 `json:"unknowns"`
|
|
}
|
|
|
|
type OCSPResponseCacheItem struct {
|
|
Subject string `json:"subject,omitempty"`
|
|
CachedAt time.Time `json:"cached_at"`
|
|
RespStatus certidp.StatusAssertion `json:"resp_status"`
|
|
RespExpires time.Time `json:"resp_expires,omitempty"`
|
|
Resp []byte `json:"resp"`
|
|
}
|
|
|
|
type OCSPResponseCache interface {
|
|
Put(key string, resp *ocsp.Response, subj string, log *certidp.Log)
|
|
Get(key string, log *certidp.Log) []byte
|
|
Delete(key string, miss bool, log *certidp.Log)
|
|
Type() string
|
|
Start(s *Server)
|
|
Stop(s *Server)
|
|
Online() bool
|
|
Config() *OCSPResponseCacheConfig
|
|
Stats() *OCSPResponseCacheStats
|
|
}
|
|
|
|
// NoOpCache is a no-op implementation of OCSPResponseCache
|
|
type NoOpCache struct {
|
|
config *OCSPResponseCacheConfig
|
|
stats *OCSPResponseCacheStats
|
|
online bool
|
|
mu *sync.RWMutex
|
|
}
|
|
|
|
func (c *NoOpCache) Put(_ string, _ *ocsp.Response, _ string, _ *certidp.Log) {}
|
|
|
|
func (c *NoOpCache) Get(_ string, _ *certidp.Log) []byte {
|
|
return nil
|
|
}
|
|
|
|
func (c *NoOpCache) Delete(_ string, _ bool, _ *certidp.Log) {}
|
|
|
|
func (c *NoOpCache) Start(_ *Server) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.stats = &OCSPResponseCacheStats{}
|
|
c.online = true
|
|
}
|
|
|
|
func (c *NoOpCache) Stop(_ *Server) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.online = false
|
|
}
|
|
|
|
func (c *NoOpCache) Online() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.online
|
|
}
|
|
|
|
func (c *NoOpCache) Type() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return "none"
|
|
}
|
|
|
|
func (c *NoOpCache) Config() *OCSPResponseCacheConfig {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.config
|
|
}
|
|
|
|
func (c *NoOpCache) Stats() *OCSPResponseCacheStats {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.stats
|
|
}
|
|
|
|
// LocalCache is a local file implementation of OCSPResponseCache
|
|
type LocalCache struct {
|
|
config *OCSPResponseCacheConfig
|
|
stats *OCSPResponseCacheStats
|
|
online bool
|
|
cache map[string]OCSPResponseCacheItem
|
|
mu *sync.RWMutex
|
|
saveInterval time.Duration
|
|
dirty bool
|
|
timer *time.Timer
|
|
}
|
|
|
|
// Put captures a CA OCSP response to the OCSP peer cache indexed by response fingerprint (a hash)
|
|
func (c *LocalCache) Put(key string, caResp *ocsp.Response, subj string, log *certidp.Log) {
|
|
c.mu.RLock()
|
|
if !c.online || caResp == nil || key == "" {
|
|
c.mu.RUnlock()
|
|
return
|
|
}
|
|
c.mu.RUnlock()
|
|
log.Debugf(certidp.DbgCachingResponse, subj, key)
|
|
rawC, err := c.Compress(caResp.Raw)
|
|
if err != nil {
|
|
log.Errorf(certidp.ErrResponseCompressFail, key, err)
|
|
return
|
|
}
|
|
log.Debugf(certidp.DbgAchievedCompression, float64(len(rawC))/float64(len(caResp.Raw)))
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
// check if we are replacing and do stats
|
|
item, ok := c.cache[key]
|
|
if ok {
|
|
c.adjustStats(-1, item.RespStatus)
|
|
}
|
|
item = OCSPResponseCacheItem{
|
|
Subject: subj,
|
|
CachedAt: time.Now().UTC().Round(time.Second),
|
|
RespStatus: certidp.StatusAssertionIntToVal[caResp.Status],
|
|
RespExpires: caResp.NextUpdate,
|
|
Resp: rawC,
|
|
}
|
|
c.cache[key] = item
|
|
c.adjustStats(1, item.RespStatus)
|
|
c.dirty = true
|
|
}
|
|
|
|
// Get returns a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
|
|
func (c *LocalCache) Get(key string, log *certidp.Log) []byte {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if !c.online || key == "" {
|
|
return nil
|
|
}
|
|
val, ok := c.cache[key]
|
|
if ok {
|
|
atomic.AddInt64(&c.stats.Hits, 1)
|
|
log.Debugf(certidp.DbgCacheHit, key)
|
|
} else {
|
|
atomic.AddInt64(&c.stats.Misses, 1)
|
|
log.Debugf(certidp.DbgCacheMiss, key)
|
|
return nil
|
|
}
|
|
resp, err := c.Decompress(val.Resp)
|
|
if err != nil {
|
|
log.Errorf(certidp.ErrResponseDecompressFail, key, err)
|
|
return nil
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func (c *LocalCache) adjustStatsHitToMiss() {
|
|
atomic.AddInt64(&c.stats.Misses, 1)
|
|
atomic.AddInt64(&c.stats.Hits, -1)
|
|
}
|
|
|
|
func (c *LocalCache) adjustStats(delta int64, rs certidp.StatusAssertion) {
|
|
if delta == 0 {
|
|
return
|
|
}
|
|
atomic.AddInt64(&c.stats.Responses, delta)
|
|
switch rs {
|
|
case ocsp.Good:
|
|
atomic.AddInt64(&c.stats.Goods, delta)
|
|
case ocsp.Revoked:
|
|
atomic.AddInt64(&c.stats.Revokes, delta)
|
|
case ocsp.Unknown:
|
|
atomic.AddInt64(&c.stats.Unknowns, delta)
|
|
}
|
|
}
|
|
|
|
// Delete removes a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
|
|
func (c *LocalCache) Delete(key string, wasMiss bool, log *certidp.Log) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if !c.online || key == "" || c.config == nil {
|
|
return
|
|
}
|
|
item, ok := c.cache[key]
|
|
if !ok {
|
|
return
|
|
}
|
|
if item.RespStatus == ocsp.Revoked && c.config.PreserveRevoked {
|
|
log.Debugf(certidp.DbgPreservedRevocation, key)
|
|
if wasMiss {
|
|
c.adjustStatsHitToMiss()
|
|
}
|
|
return
|
|
}
|
|
log.Debugf(certidp.DbgDeletingCacheResponse, key)
|
|
delete(c.cache, key)
|
|
c.adjustStats(-1, item.RespStatus)
|
|
if wasMiss {
|
|
c.adjustStatsHitToMiss()
|
|
}
|
|
c.dirty = true
|
|
}
|
|
|
|
// Start initializes the configured OCSP peer cache, loads a saved cache from disk (if present), and initializes runtime statistics
|
|
func (c *LocalCache) Start(s *Server) {
|
|
s.Debugf(certidp.DbgStartingCache)
|
|
c.loadCache(s)
|
|
c.initStats()
|
|
c.mu.Lock()
|
|
c.online = true
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *LocalCache) Stop(s *Server) {
|
|
c.mu.Lock()
|
|
s.Debugf(certidp.DbgStoppingCache)
|
|
c.online = false
|
|
c.timer.Stop()
|
|
c.mu.Unlock()
|
|
c.saveCache(s)
|
|
}
|
|
|
|
func (c *LocalCache) Online() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.online
|
|
}
|
|
|
|
func (c *LocalCache) Type() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return "local"
|
|
}
|
|
|
|
func (c *LocalCache) Config() *OCSPResponseCacheConfig {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.config
|
|
}
|
|
|
|
func (c *LocalCache) Stats() *OCSPResponseCacheStats {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if c.stats == nil {
|
|
return nil
|
|
}
|
|
stats := OCSPResponseCacheStats{
|
|
Responses: c.stats.Responses,
|
|
Hits: c.stats.Hits,
|
|
Misses: c.stats.Misses,
|
|
Revokes: c.stats.Revokes,
|
|
Goods: c.stats.Goods,
|
|
Unknowns: c.stats.Unknowns,
|
|
}
|
|
return &stats
|
|
}
|
|
|
|
func (c *LocalCache) initStats() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.stats = &OCSPResponseCacheStats{}
|
|
c.stats.Hits = 0
|
|
c.stats.Misses = 0
|
|
c.stats.Responses = int64(len(c.cache))
|
|
for _, resp := range c.cache {
|
|
switch resp.RespStatus {
|
|
case ocsp.Good:
|
|
c.stats.Goods++
|
|
case ocsp.Revoked:
|
|
c.stats.Revokes++
|
|
case ocsp.Unknown:
|
|
c.stats.Unknowns++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *LocalCache) Compress(buf []byte) ([]byte, error) {
|
|
bodyLen := int64(len(buf))
|
|
var output bytes.Buffer
|
|
writer := s2.NewWriter(&output)
|
|
input := bytes.NewReader(buf[:bodyLen])
|
|
if n, err := io.CopyN(writer, input, bodyLen); err != nil {
|
|
return nil, fmt.Errorf(certidp.ErrCannotWriteCompressed, err)
|
|
} else if n != bodyLen {
|
|
return nil, fmt.Errorf(certidp.ErrTruncatedWrite, n, bodyLen)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return nil, fmt.Errorf(certidp.ErrCannotCloseWriter, err)
|
|
}
|
|
return output.Bytes(), nil
|
|
}
|
|
|
|
func (c *LocalCache) Decompress(buf []byte) ([]byte, error) {
|
|
bodyLen := int64(len(buf))
|
|
input := bytes.NewReader(buf[:bodyLen])
|
|
reader := io.NopCloser(s2.NewReader(input))
|
|
output, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(certidp.ErrCannotReadCompressed, err)
|
|
}
|
|
return output, reader.Close()
|
|
}
|
|
|
|
func (c *LocalCache) loadCache(s *Server) {
|
|
d := s.opts.OCSPCacheConfig.LocalStore
|
|
if d == _EMPTY_ {
|
|
d = OCSPResponseCacheDefaultDir
|
|
}
|
|
f := OCSPResponseCacheDefaultFilename
|
|
store, err := filepath.Abs(path.Join(d, f))
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrLoadCacheFail, err)
|
|
return
|
|
}
|
|
s.Debugf(certidp.DbgLoadingCache, store)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.cache = make(map[string]OCSPResponseCacheItem)
|
|
dat, err := os.ReadFile(store)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
s.Debugf(certidp.DbgNoCacheFound)
|
|
} else {
|
|
s.Warnf(certidp.ErrLoadCacheFail, err)
|
|
}
|
|
return
|
|
}
|
|
err = json.Unmarshal(dat, &c.cache)
|
|
if err != nil {
|
|
// make sure clean cache
|
|
c.cache = make(map[string]OCSPResponseCacheItem)
|
|
s.Warnf(certidp.ErrLoadCacheFail, err)
|
|
c.dirty = true
|
|
return
|
|
}
|
|
c.dirty = false
|
|
}
|
|
|
|
func (c *LocalCache) saveCache(s *Server) {
|
|
c.mu.RLock()
|
|
dirty := c.dirty
|
|
c.mu.RUnlock()
|
|
if !dirty {
|
|
return
|
|
}
|
|
s.Debugf(certidp.DbgCacheDirtySave)
|
|
var d string
|
|
if c.config.LocalStore != _EMPTY_ {
|
|
d = c.config.LocalStore
|
|
} else {
|
|
d = OCSPResponseCacheDefaultDir
|
|
}
|
|
f := OCSPResponseCacheDefaultFilename
|
|
store, err := filepath.Abs(path.Join(d, f))
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
s.Debugf(certidp.DbgSavingCache, store)
|
|
if _, err := os.Stat(d); os.IsNotExist(err) {
|
|
err = os.Mkdir(d, defaultDirPerms)
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
}
|
|
tmp, err := os.CreateTemp(d, OCSPResponseCacheDefaultTempFilePrefix)
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
defer func() {
|
|
tmp.Close()
|
|
os.Remove(tmp.Name())
|
|
}() // clean up any temp files
|
|
|
|
// RW lock here because we're going to snapshot the cache to disk and mark as clean if successful
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
dat, err := json.MarshalIndent(c.cache, "", " ")
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
cacheSize, err := tmp.Write(dat)
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
err = tmp.Sync()
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
err = tmp.Close()
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
// do the final swap and overwrite any old saved peer cache
|
|
err = os.Rename(tmp.Name(), store)
|
|
if err != nil {
|
|
s.Errorf(certidp.ErrSaveCacheFail, err)
|
|
return
|
|
}
|
|
c.dirty = false
|
|
s.Debugf(certidp.DbgCacheSaved, cacheSize)
|
|
}
|
|
|
|
var OCSPResponseCacheUsage = `
|
|
You may enable OCSP peer response cacheing at server configuration root level:
|
|
|
|
(If no TLS blocks are configured with OCSP peer verification, ocsp_cache is ignored.)
|
|
|
|
...
|
|
# short form enables with defaults
|
|
ocsp_cache: true
|
|
|
|
# if false or undefined and one or more TLS blocks are configured with OCSP peer verification, "none" is implied
|
|
|
|
# long form includes settable options
|
|
ocsp_cache {
|
|
|
|
# Cache type <none, local> (default local)
|
|
type: local
|
|
|
|
# Cache file directory for local-type cache (default _rc_ in current working directory)
|
|
local_store: "_rc_"
|
|
|
|
# Ignore cache deletes if cached OCSP response is Revoked status (default false)
|
|
preserve_revoked: false
|
|
|
|
# For local store, interval to save in-memory cache to disk in seconds (default 300 seconds, minimum 1 second)
|
|
save_interval: 300
|
|
}
|
|
...
|
|
|
|
Note: Cache of server's own OCSP response (staple) is enabled using the 'ocsp' configuration option.
|
|
`
|
|
|
|
func (s *Server) initOCSPResponseCache() {
|
|
// No mTLS OCSP or Leaf OCSP enablements, so no need to init cache
|
|
s.mu.RLock()
|
|
if !s.ocspPeerVerify {
|
|
s.mu.RUnlock()
|
|
return
|
|
}
|
|
s.mu.RUnlock()
|
|
so := s.getOpts()
|
|
if so.OCSPCacheConfig == nil {
|
|
so.OCSPCacheConfig = NewOCSPResponseCacheConfig()
|
|
}
|
|
var cc = so.OCSPCacheConfig
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
switch cc.Type {
|
|
case NONE:
|
|
s.ocsprc = &NoOpCache{config: cc, online: true, mu: &sync.RWMutex{}}
|
|
case LOCAL:
|
|
c := &LocalCache{
|
|
config: cc,
|
|
online: false,
|
|
cache: make(map[string]OCSPResponseCacheItem),
|
|
mu: &sync.RWMutex{},
|
|
dirty: false,
|
|
}
|
|
c.saveInterval = time.Duration(cc.SaveInterval) * time.Second
|
|
c.timer = time.AfterFunc(c.saveInterval, func() {
|
|
s.Debugf(certidp.DbgCacheSaveTimerExpired)
|
|
c.saveCache(s)
|
|
c.timer.Reset(c.saveInterval)
|
|
})
|
|
s.ocsprc = c
|
|
default:
|
|
s.Fatalf(certidp.ErrBadCacheTypeConfig, cc.Type)
|
|
}
|
|
}
|
|
|
|
func (s *Server) startOCSPResponseCache() {
|
|
// No mTLS OCSP or Leaf OCSP enablements, so no need to start cache
|
|
s.mu.RLock()
|
|
if !s.ocspPeerVerify || s.ocsprc == nil {
|
|
s.mu.RUnlock()
|
|
return
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Could be heavier operation depending on cache implementation
|
|
s.ocsprc.Start(s)
|
|
if s.ocsprc.Online() {
|
|
s.Noticef(certidp.MsgCacheOnline, s.ocsprc.Type())
|
|
} else {
|
|
s.Noticef(certidp.MsgCacheOffline, s.ocsprc.Type())
|
|
}
|
|
}
|
|
|
|
func (s *Server) stopOCSPResponseCache() {
|
|
s.mu.RLock()
|
|
if s.ocsprc == nil {
|
|
s.mu.RUnlock()
|
|
return
|
|
}
|
|
s.mu.RUnlock()
|
|
s.ocsprc.Stop(s)
|
|
}
|
|
|
|
func parseOCSPResponseCache(v interface{}) (pcfg *OCSPResponseCacheConfig, retError error) {
|
|
var lt token
|
|
defer convertPanicToError(<, &retError)
|
|
tk, v := unwrapValue(v, <)
|
|
cm, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalCacheOptsConfig, v)}
|
|
}
|
|
pcfg = NewOCSPResponseCacheConfig()
|
|
retError = nil
|
|
for mk, mv := range cm {
|
|
// Again, unwrap token value if line check is required.
|
|
tk, mv = unwrapValue(mv, <)
|
|
switch strings.ToLower(mk) {
|
|
case "type":
|
|
cache, ok := mv.(string)
|
|
if !ok {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
|
|
}
|
|
cacheType, exists := OCSPResponseCacheTypeMap[strings.ToLower(cache)]
|
|
if !exists {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrUnknownCacheType, cache)}
|
|
}
|
|
pcfg.Type = cacheType
|
|
case "local_store":
|
|
store, ok := mv.(string)
|
|
if !ok {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
|
|
}
|
|
pcfg.LocalStore = store
|
|
case "preserve_revoked":
|
|
preserve, ok := mv.(bool)
|
|
if !ok {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
|
|
}
|
|
pcfg.PreserveRevoked = preserve
|
|
case "save_interval":
|
|
at := float64(0)
|
|
switch mv := mv.(type) {
|
|
case int64:
|
|
at = float64(mv)
|
|
case float64:
|
|
at = mv
|
|
case string:
|
|
d, err := time.ParseDuration(mv)
|
|
if err != nil {
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
|
|
}
|
|
at = d.Seconds()
|
|
default:
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldTypeConversion, "unexpected type")}
|
|
}
|
|
si := time.Duration(at) * time.Second
|
|
if si < OCSPResponseCacheMinimumSaveInterval {
|
|
si = OCSPResponseCacheMinimumSaveInterval
|
|
}
|
|
pcfg.SaveInterval = si.Seconds()
|
|
default:
|
|
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
|
|
}
|
|
}
|
|
return pcfg, nil
|
|
}
|