From af33ece136647355de6cab866cab8c59f30582a5 Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Tue, 27 Mar 2018 16:23:33 -0400 Subject: [PATCH] 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 --- .gitignore | 1 + command/server.go | 11 + http/handler.go | 79 ++++++ http/stub_assets.go | 16 ++ vault/core.go | 17 +- vault/logical_system.go | 114 +++++++++ vault/ui.go | 226 ++++++++++++++++++ vault/ui_test.go | 125 ++++++++++ .../elazarl/go-bindata-assetfs/LICENSE | 23 ++ .../elazarl/go-bindata-assetfs/README.md | 46 ++++ .../elazarl/go-bindata-assetfs/assetfs.go | 167 +++++++++++++ .../elazarl/go-bindata-assetfs/doc.go | 13 + vendor/vendor.json | 6 + 13 files changed, 842 insertions(+), 2 deletions(-) create mode 100644 http/stub_assets.go create mode 100644 vault/ui.go create mode 100644 vault/ui_test.go create mode 100644 vendor/github.com/elazarl/go-bindata-assetfs/LICENSE create mode 100644 vendor/github.com/elazarl/go-bindata-assetfs/README.md create mode 100644 vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go create mode 100644 vendor/github.com/elazarl/go-bindata-assetfs/doc.go diff --git a/.gitignore b/.gitignore index 0d5e304039..7dc579c775 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ tags ui/dist ui/tmp ui/root +http/bindata_assetfs.go # dependencies ui/node_modules diff --git a/command/server.go b/command/server.go index 0241bd6b90..ff9f9fb614 100644 --- a/command/server.go +++ b/command/server.go @@ -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 { diff --git a/http/handler.go b/http/handler.go index 55d1bd458e..2bad9024eb 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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 := ` + + +

Vault UI is not available in this binary. To get Vault UI do one of the following:

+ + + ` + 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. diff --git a/http/stub_assets.go b/http/stub_assets.go new file mode 100644 index 0000000000..c64ac582a5 --- /dev/null +++ b/http/stub_assets.go @@ -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 +} diff --git a/vault/core.go b/vault/core.go index fa9dccedd7..bb3cf5843a 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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 { diff --git a/vault/logical_system.go b/vault/logical_system.go index e80d98963e..cd53a70d36 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -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 /
+ Returns the header value. + POST /
+ Sets the header value for the UI. + DELETE /
+ Clears the header value for UI. + + LIST / + List the headers configured for the UI. + `, + }, "init": { "Initializes or returns the initialization status of the Vault.", ` diff --git a/vault/ui.go b/vault/ui.go new file mode 100644 index 0000000000..1fe1b528ed --- /dev/null +++ b/vault/ui.go @@ -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"` +} diff --git a/vault/ui_test.go b/vault/ui_test.go new file mode 100644 index 0000000000..b1cb2a946c --- /dev/null +++ b/vault/ui_test.go @@ -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)) + } +} diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE b/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE new file mode 100644 index 0000000000..5782c72690 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE @@ -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. diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/README.md b/vendor/github.com/elazarl/go-bindata-assetfs/README.md new file mode 100644 index 0000000000..27ee48f09d --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/README.md @@ -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. diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go b/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go new file mode 100644 index 0000000000..04f6d7a39d --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go @@ -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 + } +} diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/doc.go b/vendor/github.com/elazarl/go-bindata-assetfs/doc.go new file mode 100644 index 0000000000..a664249f34 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/doc.go @@ -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 diff --git a/vendor/vendor.json b/vendor/vendor.json index 749c8f3934..af62bde062 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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",