OSS: Adding UI handlers and configurable headers (#390)
* adding UI handlers and UI header configuration * forcing specific static headers * properly getting UI config value from config/environment * fixing formatting in stub UI text * use http.Header * case-insensitive X-Vault header check * fixing var name * wrap both stubbed and real UI in header handler * adding test for >1 keys
This commit is contained in:
parent
7c92bb9b1d
commit
e293fe84c3
|
@ -65,6 +65,7 @@ tags
|
|||
ui/dist
|
||||
ui/tmp
|
||||
ui/root
|
||||
http/bindata_assetfs.go
|
||||
|
||||
# dependencies
|
||||
ui/node_modules
|
||||
|
|
|
@ -452,6 +452,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
ClusterName: config.ClusterName,
|
||||
CacheSize: config.CacheSize,
|
||||
PluginDirectory: config.PluginDirectory,
|
||||
EnableUI: config.EnableUI,
|
||||
EnableRaw: config.EnableRawEndpoint,
|
||||
}
|
||||
if c.flagDev {
|
||||
|
@ -607,6 +608,16 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
coreConfig.ClusterAddr = u.String()
|
||||
}
|
||||
|
||||
// Override the UI enabling config by the environment variable
|
||||
if enableUI := os.Getenv("VAULT_UI"); enableUI != "" {
|
||||
var err error
|
||||
coreConfig.EnableUI, err = strconv.ParseBool(enableUI)
|
||||
if err != nil {
|
||||
c.UI.Output("Error parsing the environment variable VAULT_UI")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the core
|
||||
core, newCoreError := vault.NewCore(coreConfig)
|
||||
if newCoreError != nil {
|
||||
|
|
|
@ -6,9 +6,11 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
|
@ -54,6 +56,9 @@ const (
|
|||
|
||||
var (
|
||||
ReplicationStaleReadTimeout = 2 * time.Second
|
||||
|
||||
// Set to false by stub_asset if the ui build tag isn't enabled
|
||||
uiBuiltIn = true
|
||||
)
|
||||
|
||||
// Handler returns an http.Handler for the API. This can be used on
|
||||
|
@ -82,6 +87,14 @@ func Handler(core *vault.Core) http.Handler {
|
|||
}
|
||||
mux.Handle("/v1/sys/", handleRequestForwarding(core, handleLogical(core, false, nil)))
|
||||
mux.Handle("/v1/", handleRequestForwarding(core, handleLogical(core, false, nil)))
|
||||
if core.UIEnabled() == true {
|
||||
if uiBuiltIn {
|
||||
mux.Handle("/ui/", http.StripPrefix("/ui/", handleUIHeaders(core, handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))))
|
||||
} else {
|
||||
mux.Handle("/ui/", handleUIHeaders(core, handleUIStub()))
|
||||
}
|
||||
mux.Handle("/", handleRootRedirect())
|
||||
}
|
||||
|
||||
// Wrap the handler in another handler to trigger all help paths.
|
||||
helpWrappedHandler := wrapHelpHandler(mux, core)
|
||||
|
@ -145,6 +158,72 @@ func stripPrefix(prefix, path string) (string, bool) {
|
|||
return path, true
|
||||
}
|
||||
|
||||
func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
header := w.Header()
|
||||
|
||||
userHeaders, err := core.UIHeaders()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if userHeaders != nil {
|
||||
for k := range userHeaders {
|
||||
v := userHeaders.Get(k)
|
||||
header.Set(k, v)
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func handleUI(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
h.ServeHTTP(w, req)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func handleUIStub() http.Handler {
|
||||
stubHTML := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<p>Vault UI is not available in this binary. To get Vault UI do one of the following:</p>
|
||||
<ul>
|
||||
<li><a href="https://www.vaultproject.io/downloads.html">Download an official release</a></li>
|
||||
<li>Run <code>make release</code> to create your own release binaries.
|
||||
<li>Run <code>make dev-ui</code> to create a development binary with the UI.
|
||||
</ul>
|
||||
</html>
|
||||
`
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte(stubHTML))
|
||||
})
|
||||
}
|
||||
|
||||
func handleRootRedirect() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/ui/", 307)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
type UIAssetWrapper struct {
|
||||
FileSystem *assetfs.AssetFS
|
||||
}
|
||||
|
||||
func (fs *UIAssetWrapper) Open(name string) (http.File, error) {
|
||||
file, err := fs.FileSystem.Open(name)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
// serve index.html instead of 404ing
|
||||
if err == os.ErrNotExist {
|
||||
return fs.FileSystem.Open("index.html")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseRequest(r *http.Request, w http.ResponseWriter, out interface{}) error {
|
||||
// Limit the maximum number of bytes to MaxRequestSize to protect
|
||||
// against an indefinite amount of data being read.
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// +build !ui
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
uiBuiltIn = false
|
||||
}
|
||||
|
||||
// assetFS is a stub for building Vault without a UI.
|
||||
func assetFS() *assetfs.AssetFS {
|
||||
return nil
|
||||
}
|
|
@ -363,8 +363,8 @@ type Core struct {
|
|||
replicationState *uint32
|
||||
activeNodeReplicationState *uint32
|
||||
|
||||
// uiEnabled indicates whether Vault Web UI is enabled or not
|
||||
uiEnabled bool
|
||||
// uiConfig contains UI configuration
|
||||
uiConfig *UIConfig
|
||||
|
||||
// rawEnabled indicates whether the Raw endpoint is enabled
|
||||
rawEnabled bool
|
||||
|
@ -620,6 +620,9 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
|||
}
|
||||
c.auditBackends = auditBackends
|
||||
|
||||
uiStoragePrefix := systemBarrierPrefix + "ui"
|
||||
c.uiConfig = NewUIConfig(conf.EnableUI, physical.NewView(c.physical, uiStoragePrefix), NewBarrierView(c.barrier, uiStoragePrefix))
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
@ -1510,6 +1513,16 @@ func (c *Core) StepDown(req *logical.Request) (retErr error) {
|
|||
return retErr
|
||||
}
|
||||
|
||||
// UIEnabled returns if the UI is enabled
|
||||
func (c *Core) UIEnabled() bool {
|
||||
return c.uiConfig.Enabled()
|
||||
}
|
||||
|
||||
// UIHeaders returns configured UI headers
|
||||
func (c *Core) UIHeaders() (http.Header, error) {
|
||||
return c.uiConfig.Headers(context.Background())
|
||||
}
|
||||
|
||||
// sealInternal is an internal method used to seal the vault. It does not do
|
||||
// any authorization checking. The stateLock must be held prior to calling.
|
||||
func (c *Core) sealInternal(keepLock bool) error {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -77,6 +78,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
|||
"rotate",
|
||||
"config/cors",
|
||||
"config/auditing/*",
|
||||
"config/ui/headers/*",
|
||||
"plugins/catalog/*",
|
||||
"revoke-prefix/*",
|
||||
"revoke-force/*",
|
||||
|
@ -148,6 +150,41 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
|||
HelpSynopsis: strings.TrimSpace(sysHelp["config/cors"][1]),
|
||||
},
|
||||
|
||||
&framework.Path{
|
||||
Pattern: "config/ui/headers/" + framework.GenericNameRegex("header"),
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"header": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The name of the header.",
|
||||
},
|
||||
"values": &framework.FieldSchema{
|
||||
Type: framework.TypeStringSlice,
|
||||
Description: "The values to set the header.",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.handleConfigUIHeadersRead,
|
||||
logical.UpdateOperation: b.handleConfigUIHeadersUpdate,
|
||||
logical.DeleteOperation: b.handleConfigUIHeadersDelete,
|
||||
},
|
||||
|
||||
HelpDescription: strings.TrimSpace(sysHelp["config/ui/headers"][0]),
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["config/ui/headers"][1]),
|
||||
},
|
||||
|
||||
&framework.Path{
|
||||
Pattern: "config/ui/headers/$",
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ListOperation: b.handleConfigUIHeadersList,
|
||||
},
|
||||
|
||||
HelpDescription: strings.TrimSpace(sysHelp["config/ui/headers"][0]),
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["config/ui/headers"][1]),
|
||||
},
|
||||
|
||||
&framework.Path{
|
||||
Pattern: "capabilities$",
|
||||
|
||||
|
@ -2699,6 +2736,68 @@ func (b *SystemBackend) handleDisableAudit(ctx context.Context, req *logical.Req
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleConfigUIHeadersRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
header := data.Get("header").(string)
|
||||
|
||||
value, err := b.Core.uiConfig.GetHeader(ctx, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"value": value,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleConfigUIHeadersList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
headers, err := b.Core.uiConfig.HeaderKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return logical.ListResponse(headers), nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleConfigUIHeadersUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
header := data.Get("header").(string)
|
||||
values := data.Get("values").([]string)
|
||||
if header == "" || len(values) == 0 {
|
||||
return logical.ErrorResponse("header and values must be specified"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(header), "x-vault-") {
|
||||
return logical.ErrorResponse("X-Vault headers cannot be set"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Translate the list of values to the valid header string
|
||||
value := http.Header{
|
||||
header: values,
|
||||
}
|
||||
err := b.Core.uiConfig.SetHeader(ctx, header, value.Get(header))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleConfigUIHeadersDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
header := data.Get("header").(string)
|
||||
err := b.Core.uiConfig.DeleteHeader(ctx, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// handleRawRead is used to read directly from the barrier
|
||||
func (b *SystemBackend) handleRawRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
path := data.Get("path").(string)
|
||||
|
@ -3332,6 +3431,21 @@ This path responds to the following HTTP methods.
|
|||
Clears the CORS configuration and disables acceptance of CORS requests.
|
||||
`,
|
||||
},
|
||||
"config/ui/headers": {
|
||||
"Configures response headers that should be returned from the UI.",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
GET /<header>
|
||||
Returns the header value.
|
||||
POST /<header>
|
||||
Sets the header value for the UI.
|
||||
DELETE /<header>
|
||||
Clears the header value for UI.
|
||||
|
||||
LIST /
|
||||
List the headers configured for the UI.
|
||||
`,
|
||||
},
|
||||
"init": {
|
||||
"Initializes or returns the initialization status of the Vault.",
|
||||
`
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
)
|
||||
|
||||
const (
|
||||
uiConfigKey = "config"
|
||||
uiConfigPlaintextKey = "config_plaintext"
|
||||
)
|
||||
|
||||
var (
|
||||
staticHeaders = http.Header{
|
||||
"Content-Security-Policy": {
|
||||
"default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// UIConfig contains UI configuration. This takes both a physical view and a barrier view
|
||||
// because it is stored in both plaintext and encrypted to allow for getting the header
|
||||
// values before the barrier is unsealed
|
||||
type UIConfig struct {
|
||||
l sync.RWMutex
|
||||
physicalStorage physical.Backend
|
||||
barrierStorage logical.Storage
|
||||
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewUIConfig creates a new UI config
|
||||
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
|
||||
return &UIConfig{
|
||||
physicalStorage: physicalStorage,
|
||||
barrierStorage: barrierStorage,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled returns if the UI is enabled
|
||||
func (c *UIConfig) Enabled() bool {
|
||||
c.l.RLock()
|
||||
defer c.l.RUnlock()
|
||||
return c.enabled
|
||||
}
|
||||
|
||||
// Headers returns the response headers that should be returned in the UI
|
||||
func (c *UIConfig) Headers(ctx context.Context) (http.Header, error) {
|
||||
c.l.RLock()
|
||||
defer c.l.RUnlock()
|
||||
|
||||
config, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := make(http.Header)
|
||||
if config != nil {
|
||||
headers = config.Headers
|
||||
}
|
||||
|
||||
for k := range staticHeaders {
|
||||
v := staticHeaders.Get(k)
|
||||
headers.Set(k, v)
|
||||
}
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
// HeaderKeys returns the list of the configured headers
|
||||
func (c *UIConfig) HeaderKeys(ctx context.Context) ([]string, error) {
|
||||
c.l.RLock()
|
||||
defer c.l.RUnlock()
|
||||
|
||||
config, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var keys []string
|
||||
for k := range config.Headers {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// GetHeader retrieves the configured value for the given header
|
||||
func (c *UIConfig) GetHeader(ctx context.Context, header string) (string, error) {
|
||||
c.l.RLock()
|
||||
defer c.l.RUnlock()
|
||||
|
||||
config, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if config == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
value := config.Headers.Get(header)
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// SetHeader sets the value for the given header
|
||||
func (c *UIConfig) SetHeader(ctx context.Context, header, value string) error {
|
||||
if val := staticHeaders.Get(header); val != "" {
|
||||
return fmt.Errorf("the header %s is not settable", header)
|
||||
}
|
||||
|
||||
c.l.Lock()
|
||||
defer c.l.Unlock()
|
||||
|
||||
config, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
config = &uiConfigEntry{
|
||||
Headers: http.Header{
|
||||
header: {value},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
config.Headers.Set(header, value)
|
||||
}
|
||||
return c.save(ctx, config)
|
||||
}
|
||||
|
||||
// DeleteHeader deletes the header configuration for the given header
|
||||
func (c *UIConfig) DeleteHeader(ctx context.Context, header string) error {
|
||||
c.l.Lock()
|
||||
defer c.l.Unlock()
|
||||
|
||||
config, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
config.Headers.Del(header)
|
||||
return c.save(ctx, config)
|
||||
}
|
||||
|
||||
func (c *UIConfig) get(ctx context.Context) (*uiConfigEntry, error) {
|
||||
// Read plaintext always to ensure in sync with barrier value
|
||||
plaintextConfigRaw, err := c.physicalStorage.Get(ctx, uiConfigPlaintextKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configRaw, err := c.barrierStorage.Get(ctx, uiConfigKey)
|
||||
if err == nil {
|
||||
if configRaw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
config := new(uiConfigEntry)
|
||||
if err := json.Unmarshal(configRaw.Value, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check that plaintext value matches barrier value, if not sync values
|
||||
if plaintextConfigRaw == nil || bytes.Compare(plaintextConfigRaw.Value, configRaw.Value) != 0 {
|
||||
if err := c.save(ctx, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Respond with error if not sealed
|
||||
if !strings.Contains(err.Error(), ErrBarrierSealed.Error()) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Respond with plaintext value
|
||||
if configRaw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
config := new(uiConfigEntry)
|
||||
if err := json.Unmarshal(plaintextConfigRaw.Value, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *UIConfig) save(ctx context.Context, config *uiConfigEntry) error {
|
||||
if len(config.Headers) == 0 {
|
||||
if err := c.physicalStorage.Delete(ctx, uiConfigPlaintextKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.barrierStorage.Delete(ctx, uiConfigKey)
|
||||
}
|
||||
|
||||
configRaw, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry := &physical.Entry{
|
||||
Key: uiConfigPlaintextKey,
|
||||
Value: configRaw,
|
||||
}
|
||||
if err := c.physicalStorage.Put(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
barrEntry := &logical.StorageEntry{
|
||||
Key: uiConfigKey,
|
||||
Value: configRaw,
|
||||
}
|
||||
return c.barrierStorage.Put(ctx, barrEntry)
|
||||
}
|
||||
|
||||
type uiConfigEntry struct {
|
||||
Headers http.Header `json:"headers"`
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
|
||||
"github.com/hashicorp/vault/helper/logformat"
|
||||
"github.com/hashicorp/vault/physical/inmem"
|
||||
log "github.com/mgutz/logxi/v1"
|
||||
)
|
||||
|
||||
func TestConfig_Enabled(t *testing.T) {
|
||||
logger := logformat.NewVaultLogger(log.LevelTrace)
|
||||
phys, err := inmem.NewTransactionalInmem(nil, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logl := &logical.InmemStorage{}
|
||||
|
||||
config := NewUIConfig(true, phys, logl)
|
||||
if !config.Enabled() {
|
||||
t.Fatal("ui should be enabled")
|
||||
}
|
||||
|
||||
config = NewUIConfig(false, phys, logl)
|
||||
if config.Enabled() {
|
||||
t.Fatal("ui should not be enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Headers(t *testing.T) {
|
||||
logger := logformat.NewVaultLogger(log.LevelTrace)
|
||||
phys, err := inmem.NewTransactionalInmem(nil, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logl := &logical.InmemStorage{}
|
||||
|
||||
config := NewUIConfig(true, phys, logl)
|
||||
headers, err := config.Headers(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(headers) != len(staticHeaders) {
|
||||
t.Fatalf("expected %d headers, got %d", len(staticHeaders), len(headers))
|
||||
}
|
||||
|
||||
head, err := config.GetHeader(context.Background(), "Test-Header")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if head != "" {
|
||||
t.Fatal("header returned found, should not be found")
|
||||
}
|
||||
err = config.SetHeader(context.Background(), "Test-Header", "123")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
head, err = config.GetHeader(context.Background(), "Test-Header")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if head == "" {
|
||||
t.Fatal("header not found when it should be")
|
||||
}
|
||||
if head != "123" {
|
||||
t.Fatalf("expected: %s, got: %s", "123", head)
|
||||
}
|
||||
|
||||
head, err = config.GetHeader(context.Background(), "tEST-hEADER")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if head == "" {
|
||||
t.Fatal("header not found when it should be")
|
||||
}
|
||||
if head != "123" {
|
||||
t.Fatalf("expected: %s, got: %s", "123", head)
|
||||
}
|
||||
|
||||
keys, err := config.HeaderKeys(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("expected 1 key, got %d", len(keys))
|
||||
}
|
||||
|
||||
err = config.SetHeader(context.Background(), "Test-Header-2", "321")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
keys, err = config.HeaderKeys(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("expected 1 key, got %d", len(keys))
|
||||
}
|
||||
err = config.DeleteHeader(context.Background(), "Test-Header-2")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
err = config.DeleteHeader(context.Background(), "Test-Header")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
head, err = config.GetHeader(context.Background(), "Test-Header")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if head != "" {
|
||||
t.Fatal("header returned found, should not be found")
|
||||
}
|
||||
keys, err = config.HeaderKeys(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 0 {
|
||||
t.Fatalf("expected 0 key, got %d", len(keys))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2014, Elazar Leibovich
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,46 @@
|
|||
# go-bindata-assetfs
|
||||
|
||||
Serve embedded files from [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) with `net/http`.
|
||||
|
||||
[GoDoc](http://godoc.org/github.com/elazarl/go-bindata-assetfs)
|
||||
|
||||
### Installation
|
||||
|
||||
Install with
|
||||
|
||||
$ go get github.com/jteeuwen/go-bindata/...
|
||||
$ go get github.com/elazarl/go-bindata-assetfs/...
|
||||
|
||||
### Creating embedded data
|
||||
|
||||
Usage is identical to [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) usage,
|
||||
instead of running `go-bindata` run `go-bindata-assetfs`.
|
||||
|
||||
The tool will create a `bindata_assetfs.go` file, which contains the embedded data.
|
||||
|
||||
A typical use case is
|
||||
|
||||
$ go-bindata-assetfs data/...
|
||||
|
||||
### Using assetFS in your code
|
||||
|
||||
The generated file provides an `assetFS()` function that returns a `http.Filesystem`
|
||||
wrapping the embedded files. What you usually want to do is:
|
||||
|
||||
http.Handle("/", http.FileServer(assetFS()))
|
||||
|
||||
This would run an HTTP server serving the embedded files.
|
||||
|
||||
## Without running binary tool
|
||||
|
||||
You can always just run the `go-bindata` tool, and then
|
||||
|
||||
use
|
||||
|
||||
import "github.com/elazarl/go-bindata-assetfs"
|
||||
...
|
||||
http.Handle("/",
|
||||
http.FileServer(
|
||||
&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "data"}))
|
||||
|
||||
to serve files embedded from the `data` directory.
|
|
@ -0,0 +1,167 @@
|
|||
package assetfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultFileTimestamp = time.Now()
|
||||
)
|
||||
|
||||
// FakeFile implements os.FileInfo interface for a given path and size
|
||||
type FakeFile struct {
|
||||
// Path is the path of this file
|
||||
Path string
|
||||
// Dir marks of the path is a directory
|
||||
Dir bool
|
||||
// Len is the length of the fake file, zero if it is a directory
|
||||
Len int64
|
||||
// Timestamp is the ModTime of this file
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (f *FakeFile) Name() string {
|
||||
_, name := filepath.Split(f.Path)
|
||||
return name
|
||||
}
|
||||
|
||||
func (f *FakeFile) Mode() os.FileMode {
|
||||
mode := os.FileMode(0644)
|
||||
if f.Dir {
|
||||
return mode | os.ModeDir
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func (f *FakeFile) ModTime() time.Time {
|
||||
return f.Timestamp
|
||||
}
|
||||
|
||||
func (f *FakeFile) Size() int64 {
|
||||
return f.Len
|
||||
}
|
||||
|
||||
func (f *FakeFile) IsDir() bool {
|
||||
return f.Mode().IsDir()
|
||||
}
|
||||
|
||||
func (f *FakeFile) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssetFile implements http.File interface for a no-directory file with content
|
||||
type AssetFile struct {
|
||||
*bytes.Reader
|
||||
io.Closer
|
||||
FakeFile
|
||||
}
|
||||
|
||||
func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
|
||||
if timestamp.IsZero() {
|
||||
timestamp = defaultFileTimestamp
|
||||
}
|
||||
return &AssetFile{
|
||||
bytes.NewReader(content),
|
||||
ioutil.NopCloser(nil),
|
||||
FakeFile{name, false, int64(len(content)), timestamp}}
|
||||
}
|
||||
|
||||
func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return nil, errors.New("not a directory")
|
||||
}
|
||||
|
||||
func (f *AssetFile) Size() int64 {
|
||||
return f.FakeFile.Size()
|
||||
}
|
||||
|
||||
func (f *AssetFile) Stat() (os.FileInfo, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// AssetDirectory implements http.File interface for a directory
|
||||
type AssetDirectory struct {
|
||||
AssetFile
|
||||
ChildrenRead int
|
||||
Children []os.FileInfo
|
||||
}
|
||||
|
||||
func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
|
||||
fileinfos := make([]os.FileInfo, 0, len(children))
|
||||
for _, child := range children {
|
||||
_, err := fs.AssetDir(filepath.Join(name, child))
|
||||
fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}})
|
||||
}
|
||||
return &AssetDirectory{
|
||||
AssetFile{
|
||||
bytes.NewReader(nil),
|
||||
ioutil.NopCloser(nil),
|
||||
FakeFile{name, true, 0, time.Time{}},
|
||||
},
|
||||
0,
|
||||
fileinfos}
|
||||
}
|
||||
|
||||
func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
|
||||
if count <= 0 {
|
||||
return f.Children, nil
|
||||
}
|
||||
if f.ChildrenRead+count > len(f.Children) {
|
||||
count = len(f.Children) - f.ChildrenRead
|
||||
}
|
||||
rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
|
||||
f.ChildrenRead += count
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
func (f *AssetDirectory) Stat() (os.FileInfo, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// AssetFS implements http.FileSystem, allowing
|
||||
// embedded files to be served from net/http package.
|
||||
type AssetFS struct {
|
||||
// Asset should return content of file in path if exists
|
||||
Asset func(path string) ([]byte, error)
|
||||
// AssetDir should return list of files in the path
|
||||
AssetDir func(path string) ([]string, error)
|
||||
// AssetInfo should return the info of file in path if exists
|
||||
AssetInfo func(path string) (os.FileInfo, error)
|
||||
// Prefix would be prepended to http requests
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (fs *AssetFS) Open(name string) (http.File, error) {
|
||||
name = path.Join(fs.Prefix, name)
|
||||
if len(name) > 0 && name[0] == '/' {
|
||||
name = name[1:]
|
||||
}
|
||||
if b, err := fs.Asset(name); err == nil {
|
||||
timestamp := defaultFileTimestamp
|
||||
if fs.AssetInfo != nil {
|
||||
if info, err := fs.AssetInfo(name); err == nil {
|
||||
timestamp = info.ModTime()
|
||||
}
|
||||
}
|
||||
return NewAssetFile(name, b, timestamp), nil
|
||||
}
|
||||
if children, err := fs.AssetDir(name); err == nil {
|
||||
return NewAssetDirectory(name, children, fs), nil
|
||||
} else {
|
||||
// If the error is not found, return an error that will
|
||||
// result in a 404 error. Otherwise the server returns
|
||||
// a 500 error for files not found.
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// assetfs allows packages to serve static content embedded
|
||||
// with the go-bindata tool with the standard net/http package.
|
||||
//
|
||||
// See https://github.com/jteeuwen/go-bindata for more information
|
||||
// about embedding binary data with go-bindata.
|
||||
//
|
||||
// Usage example, after running
|
||||
// $ go-bindata data/...
|
||||
// use:
|
||||
// http.Handle("/",
|
||||
// http.FileServer(
|
||||
// &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "data"}))
|
||||
package assetfs
|
|
@ -864,6 +864,12 @@
|
|||
"revision": "bb3d318650d48840a39aa21a027c6630e198e626",
|
||||
"revisionTime": "2017-11-10T20:55:13Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "7DxViusFRJ7UPH0jZqYatwDrOkY=",
|
||||
"path": "github.com/elazarl/go-bindata-assetfs",
|
||||
"revision": "38087fe4dafb822e541b3f7955075cc1c30bd294",
|
||||
"revisionTime": "2018-02-23T16:03:09Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "5BP5xofo0GoFi6FtgqFFbmHyUKI=",
|
||||
"path": "github.com/fatih/structs",
|
||||
|
|
Loading…
Reference in New Issue