mirror of
https://github.com/kopia/kopia.git
synced 2026-03-13 11:46:55 -04:00
* server: when serving HTML UI, prefix the title with string from KOPIA_UI_TITLE_PREFIX envar * kopia-ui: support for multiple repositories + portability This is a major rewrite of the app/ codebase which changes how configuration for repositories is maintained and how it flows through the component hierarchy. Portable mode is enabled by creating 'repositories' subdirectory before launching the app. on macOS: <parent>/KopiaUI.app <parent>/repositories/ On Windows, option #1 - nested directory <parent>\KopiaUI.exe <parent>\repositories\ On Windows, option #2 - parallel directory <parent>\some-dir\KopiaUI.exe <parent>\repositories\ In portable mode, repositories will have 'cache' and 'logs' nested in it.
167 lines
4.0 KiB
Go
167 lines
4.0 KiB
Go
package serverapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/repo/logging"
|
|
)
|
|
|
|
var log = logging.GetContextLoggerFunc("kopia/client")
|
|
|
|
// DefaultUsername is the default username for Kopia server.
|
|
const DefaultUsername = "kopia"
|
|
|
|
// Client provides helper methods for communicating with Kopia API serevr.
|
|
type Client struct {
|
|
options ClientOptions
|
|
}
|
|
|
|
// HTTPClient returns HTTP client that connects to the server.
|
|
func (c *Client) HTTPClient() *http.Client {
|
|
return c.options.HTTPClient
|
|
}
|
|
|
|
// Get sends HTTP GET request and decodes the JSON response into the provided payload structure.
|
|
func (c *Client) Get(ctx context.Context, path string, respPayload interface{}) error {
|
|
req, err := http.NewRequest("GET", c.options.BaseURL+path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.options.LogRequests {
|
|
log(ctx).Debugf("GET %v", c.options.BaseURL+path)
|
|
}
|
|
|
|
if c.options.Username != "" {
|
|
req.SetBasicAuth(c.options.Username, c.options.Password)
|
|
}
|
|
|
|
resp, err := c.options.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close() //nolint:errcheck
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return errors.Errorf("invalid server response: %v", resp.Status)
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(respPayload); err != nil {
|
|
return errors.Wrap(err, "malformed server response")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Post sends HTTP post request with given JSON payload structure and decodes the JSON response into another payload structure.
|
|
func (c *Client) Post(ctx context.Context, path string, reqPayload, respPayload interface{}) error {
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(reqPayload); err != nil {
|
|
return errors.Wrap(err, "unable to encode request")
|
|
}
|
|
|
|
if c.options.LogRequests {
|
|
log(ctx).Infof("POST %v (%v bytes)", c.options.BaseURL+path, buf.Len())
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", c.options.BaseURL+path, &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
if c.options.Username != "" {
|
|
req.SetBasicAuth(c.options.Username, c.options.Password)
|
|
}
|
|
|
|
resp, err := c.options.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close() //nolint:errcheck
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return errors.Errorf("invalid server response: %v", resp.Status)
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(respPayload); err != nil {
|
|
return errors.Wrap(err, "malformed server response")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ClientOptions encapsulates all optional API options.HTTPClient options.
|
|
type ClientOptions struct {
|
|
BaseURL string
|
|
|
|
HTTPClient *http.Client
|
|
|
|
Username string
|
|
Password string
|
|
|
|
TrustedServerCertificateFingerprint string
|
|
|
|
RootCAs *x509.CertPool
|
|
|
|
LogRequests bool
|
|
}
|
|
|
|
// NewClient creates a options.HTTPClient for connecting to Kopia HTTP API.
|
|
// nolint:gocritic
|
|
func NewClient(options ClientOptions) (*Client, error) {
|
|
if options.HTTPClient == nil {
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: options.RootCAs,
|
|
},
|
|
}
|
|
|
|
if f := options.TrustedServerCertificateFingerprint; f != "" {
|
|
if options.RootCAs != nil {
|
|
return nil, errors.Errorf("can't set both RootCAs and TrustedServerCertificateFingerprint")
|
|
}
|
|
|
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
|
transport.TLSClientConfig.VerifyPeerCertificate = verifyPeerCertificate(f)
|
|
}
|
|
|
|
options.HTTPClient = &http.Client{
|
|
Transport: transport,
|
|
}
|
|
}
|
|
|
|
if options.Username == "" {
|
|
options.Username = DefaultUsername
|
|
}
|
|
|
|
options.BaseURL += "/api/v1/"
|
|
|
|
return &Client{options}, nil
|
|
}
|
|
|
|
func verifyPeerCertificate(sha256Fingerprint string) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
for _, c := range rawCerts {
|
|
h := sha256.Sum256(c)
|
|
if hex.EncodeToString(h[:]) == sha256Fingerprint {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return errors.Errorf("can't find certificate matching SHA256 fingerprint %q", sha256Fingerprint)
|
|
}
|
|
}
|