2023-02-03 20:55:09 +01:00
|
|
|
// Copyright (C) 2020, 2023 Opsmate, Inc.
|
2020-04-29 17:38:04 +02:00
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
|
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
|
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
//
|
|
|
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
|
|
|
// See the Mozilla Public License for details.
|
|
|
|
|
|
|
|
package loglist
|
|
|
|
|
|
|
|
import (
|
2023-02-03 20:55:09 +01:00
|
|
|
"context"
|
2020-04-29 17:38:04 +02:00
|
|
|
"encoding/json"
|
2023-02-03 21:21:24 +01:00
|
|
|
"errors"
|
2020-04-29 17:38:04 +02:00
|
|
|
"fmt"
|
2023-02-03 20:55:09 +01:00
|
|
|
"io"
|
2020-04-29 17:38:04 +02:00
|
|
|
"net/http"
|
2023-02-03 20:55:09 +01:00
|
|
|
"os"
|
2020-04-29 17:38:04 +02:00
|
|
|
"strings"
|
2023-02-03 21:21:24 +01:00
|
|
|
"time"
|
2020-04-29 17:38:04 +02:00
|
|
|
)
|
|
|
|
|
2023-02-03 21:24:55 +01:00
|
|
|
var UserAgent = "certspotter"
|
|
|
|
|
2023-02-03 21:21:24 +01:00
|
|
|
type ModificationToken struct {
|
|
|
|
etag string
|
|
|
|
modified time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
var ErrNotModified = errors.New("loglist has not been modified")
|
|
|
|
|
|
|
|
func newModificationToken(response *http.Response) *ModificationToken {
|
|
|
|
token := &ModificationToken{
|
|
|
|
etag: response.Header.Get("ETag"),
|
|
|
|
}
|
|
|
|
if t, err := time.Parse(http.TimeFormat, response.Header.Get("Last-Modified")); err == nil {
|
|
|
|
token.modified = t
|
|
|
|
}
|
|
|
|
return token
|
|
|
|
}
|
|
|
|
|
|
|
|
func (token *ModificationToken) setRequestHeaders(request *http.Request) {
|
|
|
|
if token.etag != "" {
|
|
|
|
request.Header.Set("If-None-Match", token.etag)
|
|
|
|
} else if !token.modified.IsZero() {
|
|
|
|
request.Header.Set("If-Modified-Since", token.modified.Format(http.TimeFormat))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:55:09 +01:00
|
|
|
func Load(ctx context.Context, urlOrFile string) (*List, error) {
|
2023-02-03 21:21:24 +01:00
|
|
|
list, _, err := LoadIfModified(ctx, urlOrFile, nil)
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func LoadIfModified(ctx context.Context, urlOrFile string, token *ModificationToken) (*List, *ModificationToken, error) {
|
2020-04-29 17:38:04 +02:00
|
|
|
if strings.HasPrefix(urlOrFile, "https://") {
|
2023-02-03 21:21:24 +01:00
|
|
|
return FetchIfModified(ctx, urlOrFile, token)
|
2020-04-29 17:38:04 +02:00
|
|
|
} else {
|
2023-02-03 21:21:24 +01:00
|
|
|
list, err := ReadFile(urlOrFile)
|
|
|
|
return list, nil, err
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:55:09 +01:00
|
|
|
func Fetch(ctx context.Context, url string) (*List, error) {
|
2023-02-03 21:21:24 +01:00
|
|
|
list, _, err := FetchIfModified(ctx, url, nil)
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func FetchIfModified(ctx context.Context, url string, token *ModificationToken) (*List, *ModificationToken, error) {
|
2023-02-03 20:55:09 +01:00
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
2020-04-29 17:38:04 +02:00
|
|
|
if err != nil {
|
2023-02-03 21:21:24 +01:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
2023-02-03 21:24:55 +01:00
|
|
|
request.Header.Set("User-Agent", UserAgent)
|
2023-02-03 21:21:24 +01:00
|
|
|
if token != nil {
|
|
|
|
token.setRequestHeaders(request)
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
2023-02-03 20:55:09 +01:00
|
|
|
response, err := http.DefaultClient.Do(request)
|
|
|
|
if err != nil {
|
2023-02-03 21:21:24 +01:00
|
|
|
return nil, nil, err
|
2023-02-03 20:55:09 +01:00
|
|
|
}
|
|
|
|
content, err := io.ReadAll(response.Body)
|
2020-04-29 17:38:04 +02:00
|
|
|
response.Body.Close()
|
|
|
|
if err != nil {
|
2023-02-03 21:21:24 +01:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if token != nil && response.StatusCode == http.StatusNotModified {
|
|
|
|
return nil, nil, ErrNotModified
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
|
|
|
if response.StatusCode != 200 {
|
2023-02-03 21:21:24 +01:00
|
|
|
return nil, nil, fmt.Errorf("%s: %s", url, response.Status)
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
2023-02-03 21:21:24 +01:00
|
|
|
list, err := Unmarshal(content)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("error parsing %s: %w", url, err)
|
|
|
|
}
|
|
|
|
return list, newModificationToken(response), err
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func ReadFile(filename string) (*List, error) {
|
2023-02-03 20:55:09 +01:00
|
|
|
content, err := os.ReadFile(filename)
|
2020-04-29 17:38:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-05-01 22:53:56 +02:00
|
|
|
return Unmarshal(content)
|
2020-04-29 17:38:04 +02:00
|
|
|
}
|
|
|
|
|
2021-05-01 22:53:56 +02:00
|
|
|
func Unmarshal(jsonBytes []byte) (*List, error) {
|
2020-04-29 17:38:04 +02:00
|
|
|
list := new(List)
|
|
|
|
if err := json.Unmarshal(jsonBytes, list); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-05-01 22:05:37 +02:00
|
|
|
if err := list.Validate(); err != nil {
|
|
|
|
return nil, fmt.Errorf("Invalid log list: %s", err)
|
|
|
|
}
|
2020-04-29 17:38:04 +02:00
|
|
|
return list, nil
|
|
|
|
}
|