Files
cunicu/pkg/config/file.go
Steffen Vogel 92a7ad2f7f daemon: use per-interface features
Signed-off-by: Steffen Vogel <post@steffenvogel.de>
2022-10-07 18:30:50 +02:00

158 lines
3.2 KiB
Go

package config
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/knadh/koanf/providers/file"
"github.com/stv0g/cunicu/pkg/util/buildinfo"
)
type remoteFileProvider struct {
url *url.URL
etag string
lastModified time.Time
order []string
}
func RemoteFileProvider(u *url.URL) *remoteFileProvider {
return &remoteFileProvider{
url: u,
}
}
func (p *remoteFileProvider) Read() (map[string]interface{}, error) {
return nil, errors.New("this provider does not support parsers")
}
func (p *remoteFileProvider) ReadBytes() ([]byte, error) {
if p.url.Scheme != "https" {
host, _, err := net.SplitHostPort(p.url.Host)
if err != nil {
return nil, fmt.Errorf("failed to split host:port: %w", err)
} else if host != "localhost" && host != "127.0.0.1" && host != "::1" && host != "[::1]" {
return nil, errors.New("remote configuration must be provided via HTTPS")
}
}
client := &http.Client{
Timeout: 5 * time.Second,
}
req := &http.Request{
Method: "GET",
URL: p.url,
Header: http.Header{},
}
req.Header.Set("User-Agent", buildinfo.UserAgent())
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", p.url, err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch: %s: %s", p.url, resp.Status)
}
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
p.order, err = ExtractInterfaceOrder(buf)
if err != nil {
return nil, fmt.Errorf("failed to get interface order: %w", err)
}
p.etag = resp.Header.Get("Etag")
if lm := resp.Header.Get("Last-modified"); lm != "" {
p.lastModified, err = time.Parse(http.TimeFormat, lm)
if err != nil {
return nil, fmt.Errorf("failed to parse Last-Modified header: %w", err)
}
}
return buf, nil
}
func (p *remoteFileProvider) Order() []string {
return p.order
}
func (p *remoteFileProvider) Version() any {
p.hasChanged()
if p.etag != "" {
return p.etag
}
if !p.lastModified.IsZero() {
return p.lastModified.Unix()
}
return nil
}
func (p *remoteFileProvider) hasChanged() (bool, error) {
client := &http.Client{
Timeout: 5 * time.Second,
}
req := &http.Request{
Method: "HEAD",
URL: p.url,
Header: http.Header{},
}
req.Header.Set("User-Agent", buildinfo.UserAgent())
if p.etag != "" {
req.Header.Set("If-None-Match", fmt.Sprintf("\"%s\"", p.etag))
}
if !p.lastModified.IsZero() {
req.Header.Set("If-Modified-Since", p.lastModified.Format(http.TimeFormat))
}
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to fetch %s: %w", p.url, err)
} else if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed to fetch: %s: %s", p.url, resp.Status)
}
return resp.StatusCode == 200, nil
}
type localFileProvider struct {
*file.File
order []string
}
func LocalFileProvider(u *url.URL) *localFileProvider {
return &localFileProvider{
File: file.Provider(u.Path),
}
}
func (p *localFileProvider) ReadBytes() ([]byte, error) {
buf, err := p.File.ReadBytes()
if err == nil {
p.order, err = ExtractInterfaceOrder(buf)
}
return buf, err
}
func (p *localFileProvider) Order() []string {
return p.order
}