Files
aperture/proxy/service.go
2019-12-04 10:58:04 +01:00

160 lines
5.7 KiB
Go

package proxy
import (
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"github.com/lightninglabs/kirin/auth"
"github.com/lightninglabs/kirin/freebie"
)
var (
filePrefix = "!file"
filePrefixHex = filePrefix + "+hex"
filePrefixBase64 = filePrefix + "+base64"
)
// Service generically specifies configuration data for backend services to the
// Kirin proxy.
type Service struct {
// Name is the name of the LSAT-enabled service.
Name string `long:"name" description:"Name of the LSAT-enabled service"`
// TLSCertPath is the optional path to the service's TLS certificate.
TLSCertPath string `long:"tlscertpath" description:"Path to the service's TLS certificate"`
// Address is the service's IP address and port.
Address string `long:"address" description:"service instance rpc address"`
// Protocol is the protocol that should be used to connect to the
// service. Currently supported is http and https.
Protocol string `long:"protocol" description:"service instance protocol"`
// Auth is the authentication level required for this service to be
// accessed. Valid values are "on" for full authentication, "freebie X"
// for X free requests per IP address before authentication is required
// or "off" for no authentication.
Auth auth.Level `long:"auth" description:"required authentication"`
// HostRegexp is a regular expression that is tested against the 'Host'
// HTTP header field to find out if this service should be used.
HostRegexp string `long:"hostregexp" description:"Regular expression to match the host against"`
// PathRegexp is a regular expression that is tested against the path
// of the URL of a request to find out if this service should be used.
PathRegexp string `long:"pathregexp" description:"Regular expression to match the path of the URL against"`
// Headers is a map of strings that defines header name and values that
// should always be passed to the backend service, overwriting any
// headers with the same name that might have been set by the client
// request.
// If the value of a header field starts with the prefix "!file+hex:",
// the rest of the value is treated as a path to a file and the content
// of that file is sent to the backend with each call (hex encoded).
// If the value starts with the prefix "!file+base64:", the content of
// the file is sent encoded as base64.
Headers map[string]string `long:"headers" description:"Header fields to always pass to the service"`
// Capabilities is the list of capabilities authorized for the service
// at the base tier.
Capabilities string `long:"capabilities" description:"A comma-separated list of the service capabilities authorized for the base tier"`
// Constraints is the set of constraints that will take form of caveats.
// They'll be enforced for a service at the base tier. The key should
// correspond to the caveat's condition.
Constraints map[string]string `long:"constraints" description:"The service constraints to enforce at the base tier"`
// AuthWhitelistPaths is an optional list of regular expressions that
// are matched against the path of the URL of a request. If the request
// URL matches any of those regular expressions, the call is treated as
// if Auth was set to "off". This allows certain RPC methods to not
// require an LSAT token. E.g. the path for a gRPC call looks like this:
// /package_name.ServiceName/MethodName
AuthWhitelistPaths []string `long:"authwhitelistpaths" description:"List of regular expressions for paths that don't require authentication'"`
freebieDb freebie.DB
}
// AuthRequired determines the auth level required for a given request.
func (s *Service) AuthRequired(r *http.Request) auth.Level {
// Does the request match any whitelist entry?
for _, pathRegexp := range s.AuthWhitelistPaths {
pathRegexp := regexp.MustCompile(pathRegexp)
if pathRegexp.MatchString(r.URL.Path) {
log.Tracef("Req path [%s] matches whitelist entry "+
"[%s].", r.URL.Path, pathRegexp)
return auth.LevelOff
}
}
// By default we always return the service level auth setting.
return s.Auth
}
// prepareServices prepares the backend service configurations to be used by the
// proxy.
func prepareServices(services []*Service) error {
for _, service := range services {
// Each freebie enabled service gets its own store.
if service.Auth.IsFreebie() {
service.freebieDb = freebie.NewMemIPMaskStore(
service.Auth.FreebieCount(),
)
}
// Replace placeholders/directives in the header fields with the
// actual desired values.
for key, value := range service.Headers {
if !strings.HasPrefix(value, filePrefix) {
continue
}
parts := strings.Split(value, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid header config, " +
"must be '!file+hex:path'")
}
prefix, fileName := parts[0], parts[1]
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
// There are two supported formats to encode the file
// content in: hex and base64.
switch {
case prefix == filePrefixHex:
newValue := hex.EncodeToString(bytes)
service.Headers[key] = newValue
case prefix == filePrefixBase64:
newValue := base64.StdEncoding.EncodeToString(
bytes,
)
service.Headers[key] = newValue
default:
return fmt.Errorf("unsupported file prefix "+
"format %s", value)
}
}
// Make sure all whitelist regular expression entries actually
// compile so we run into an eventual panic during startup and
// not only when the request happens.
for _, entry := range service.AuthWhitelistPaths {
_, err := regexp.Compile(entry)
if err != nil {
return fmt.Errorf("error validating auth "+
"whitelist: %v", err)
}
}
}
return nil
}