package trust import ( "crypto/x509" "errors" "io/ioutil" "net/http" "net/url" "os" "path" "path/filepath" "sync" "time" log "github.com/Sirupsen/logrus" "github.com/docker/libtrust/trustgraph" ) type TrustStore struct { path string caPool *x509.CertPool graph trustgraph.TrustGraph expiration time.Time fetcher *time.Timer fetchTime time.Duration autofetch bool httpClient *http.Client baseEndpoints map[string]*url.URL sync.RWMutex } // defaultFetchtime represents the starting duration to wait between // fetching sections of the graph. Unsuccessful fetches should // increase time between fetching. const defaultFetchtime = 45 * time.Second var baseEndpoints = map[string]string{"official": "https://dvjy3tqbc323p.cloudfront.net/trust/official.json"} func NewTrustStore(path string) (*TrustStore, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err } // Create base graph url map endpoints := map[string]*url.URL{} for name, endpoint := range baseEndpoints { u, err := url.Parse(endpoint) if err != nil { return nil, err } endpoints[name] = u } // Load grant files t := &TrustStore{ path: abspath, caPool: nil, httpClient: &http.Client{}, fetchTime: time.Millisecond, baseEndpoints: endpoints, } err = t.reload() if err != nil { return nil, err } return t, nil } func (t *TrustStore) reload() error { t.Lock() defer t.Unlock() matches, err := filepath.Glob(filepath.Join(t.path, "*.json")) if err != nil { return err } statements := make([]*trustgraph.Statement, len(matches)) for i, match := range matches { f, err := os.Open(match) if err != nil { return err } statements[i], err = trustgraph.LoadStatement(f, nil) if err != nil { f.Close() return err } f.Close() } if len(statements) == 0 { if t.autofetch { log.Debugf("No grants, fetching") t.fetcher = time.AfterFunc(t.fetchTime, t.fetch) } return nil } grants, expiration, err := trustgraph.CollapseStatements(statements, true) if err != nil { return err } t.expiration = expiration t.graph = trustgraph.NewMemoryGraph(grants) log.Debugf("Reloaded graph with %d grants expiring at %s", len(grants), expiration) if t.autofetch { nextFetch := expiration.Sub(time.Now()) if nextFetch < 0 { nextFetch = defaultFetchtime } else { nextFetch = time.Duration(0.8 * (float64)(nextFetch)) } t.fetcher = time.AfterFunc(nextFetch, t.fetch) } return nil } func (t *TrustStore) fetchBaseGraph(u *url.URL) (*trustgraph.Statement, error) { req := &http.Request{ Method: "GET", URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header), Body: nil, Host: u.Host, } resp, err := t.httpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode == 404 { return nil, errors.New("base graph does not exist") } defer resp.Body.Close() return trustgraph.LoadStatement(resp.Body, t.caPool) } // fetch retrieves updated base graphs. This function cannot error, it // should only log errors func (t *TrustStore) fetch() { t.Lock() defer t.Unlock() if t.autofetch && t.fetcher == nil { // Do nothing ?? return } fetchCount := 0 for bg, ep := range t.baseEndpoints { statement, err := t.fetchBaseGraph(ep) if err != nil { log.Infof("Trust graph fetch failed: %s", err) continue } b, err := statement.Bytes() if err != nil { log.Infof("Bad trust graph statement: %s", err) continue } // TODO check if value differs err = ioutil.WriteFile(path.Join(t.path, bg+".json"), b, 0600) if err != nil { log.Infof("Error writing trust graph statement: %s", err) } fetchCount++ } log.Debugf("Fetched %d base graphs at %s", fetchCount, time.Now()) if fetchCount > 0 { go func() { err := t.reload() if err != nil { log.Infof("Reload of trust graph failed: %s", err) } }() t.fetchTime = defaultFetchtime t.fetcher = nil } else if t.autofetch { maxTime := 10 * defaultFetchtime t.fetchTime = time.Duration(1.5 * (float64)(t.fetchTime+time.Second)) if t.fetchTime > maxTime { t.fetchTime = maxTime } t.fetcher = time.AfterFunc(t.fetchTime, t.fetch) } }