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:
Chris Hoffman 2018-03-27 16:23:33 -04:00 committed by Matthew Irish
parent 7c92bb9b1d
commit e293fe84c3
13 changed files with 842 additions and 2 deletions

1
.gitignore vendored
View File

@ -65,6 +65,7 @@ tags
ui/dist
ui/tmp
ui/root
http/bindata_assetfs.go
# dependencies
ui/node_modules

View File

@ -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 {

View File

@ -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.

16
http/stub_assets.go Normal file
View File

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

View File

@ -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 {

View File

@ -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.",
`

226
vault/ui.go Normal file
View File

@ -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"`
}

125
vault/ui_test.go Normal file
View File

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

23
vendor/github.com/elazarl/go-bindata-assetfs/LICENSE generated vendored Normal file
View File

@ -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.

46
vendor/github.com/elazarl/go-bindata-assetfs/README.md generated vendored Normal file
View File

@ -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.

167
vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go generated vendored Normal file
View File

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

13
vendor/github.com/elazarl/go-bindata-assetfs/doc.go generated vendored Normal file
View File

@ -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

6
vendor/vendor.json vendored
View File

@ -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",