Files
kopia/internal/serverapi/client.go
Jarek Kowalski 057c2789d8 Kopia UI: support for multiple repositories + portability (#398)
* 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.
2020-04-04 17:18:37 -07:00

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)
}
}