Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add upload token configuration #36

Merged
merged 6 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v3
- run: go vet ./...
- run: go test -v ${{ matrix.env.coverage && '-coverprofile=profile.cov' }} ./...
continue-on-error: ${{ matrix.env.continue-on-error }}
continue-on-error: ${{ matrix.env['continue-on-error'] }}
- uses: shogo82148/actions-goveralls@v1
if: ${{ matrix.env.coverage }}
with:
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/cinode_web_proxy/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func executeWithConfig(ctx context.Context, cfg *config) error {
return httpserver.RunGracefully(ctx,
handler,
httpserver.ListenPort(cfg.port),
httpserver.Logger(log),
)
}

Expand Down
71 changes: 65 additions & 6 deletions pkg/cmd/public_node/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package public_node

import (
"context"
"crypto/sha256"
"crypto/subtle"
"fmt"
"net/http"
"os"
Expand All @@ -41,13 +43,11 @@ func executeWithConfig(ctx context.Context, cfg config) error {
return err
}

log := slog.Default()

log.Info("Server listening for connections",
cfg.log.Info("Server listening for connections",
"address", fmt.Sprintf("http://localhost:%d", cfg.port),
)

log.Info("System info",
cfg.log.Info("System info",
"goos", runtime.GOOS,
"goarch", runtime.GOARCH,
"compiler", runtime.Compiler,
Expand Down Expand Up @@ -76,17 +76,74 @@ func buildHttpHandler(cfg config) (http.Handler, error) {
}

ds := datastore.NewMultiSource(mainDS, time.Hour, additionalDSs...)
return datastore.WebInterface(ds), nil
handler := datastore.WebInterface(
ds,
datastore.WebInterfaceOptionLogger(cfg.log),
)

if cfg.uploadUsername != "" || cfg.uploadPassword != "" {
origHandler := handler
expectedUsernameHash := sha256.Sum256([]byte(cfg.uploadUsername))
expectedPasswordHash := sha256.Sum256([]byte(cfg.uploadPassword))
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead:
// Auth not required, continue without auth check
default:
// Every other method requires token, this is preventive
// since not all methods will be uploads, but it comes from the
// secure-by-default approach.
//
// Also we're comparing hashes instead of their values.
// This, due to properties of a hashing function, reduces attacks
// based on side-channel information, including the length of the
// token. The subtle.ConstantTimeCompare is not really needed here
// but it does not do any harm.
username, password, ok := r.BasicAuth()

var validAuth int = 0
if ok {
validAuth = 1
}

usernameHash := sha256.Sum256([]byte(username))
validAuth &= subtle.ConstantTimeCompare(
expectedUsernameHash[:],
usernameHash[:],
)

passwordHash := sha256.Sum256([]byte(password))
validAuth &= subtle.ConstantTimeCompare(
expectedPasswordHash[:],
passwordHash[:],
)

if validAuth != 1 {
w.WriteHeader(http.StatusForbidden)
return
}
}
origHandler.ServeHTTP(w, r)
})
}

return handler, nil
}

type config struct {
mainDSLocation string
additionalDSLocations []string
port int
log *slog.Logger

uploadUsername string
uploadPassword string
}

func getConfig() config {
cfg := config{}
cfg := config{
log: slog.Default(),
}

cfg.mainDSLocation = os.Getenv("CINODE_MAIN_DATASTORE")
if cfg.mainDSLocation == "" {
Expand All @@ -108,6 +165,8 @@ func getConfig() config {
}

cfg.port = 8080
cfg.uploadUsername = os.Getenv("CINODE_UPLOAD_USERNAME")
cfg.uploadPassword = os.Getenv("CINODE_UPLOAD_PASSWORD")

return cfg
}
44 changes: 44 additions & 0 deletions pkg/cmd/public_node/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/cinode/go/testvectors/testblobs"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"
)

func TestGetConfig(t *testing.T) {
Expand Down Expand Up @@ -81,6 +82,48 @@ func TestBuildHttpHandler(t *testing.T) {
})
})

t.Run("Upload auth", func(t *testing.T) {

const VALID_USERNAME = "Alice"
const INVALID_USERNAME = "Bob"
const VALID_PASSWORD = "secret"
const INVALID_PASSWORD = "plaintext"

h, err := buildHttpHandler(config{
mainDSLocation: t.TempDir(),
additionalDSLocations: []string{
t.TempDir(),
t.TempDir(),
t.TempDir(),
},
uploadUsername: VALID_USERNAME,
uploadPassword: VALID_PASSWORD,
})
require.NoError(t, err)
require.NotNil(t, h)

server := httptest.NewServer(h)
defer server.Close()

err = testblobs.DynamicLink.Put(server.URL)
require.ErrorContains(t, err, "403")

err = testblobs.DynamicLink.PutWithAuth(server.URL, VALID_USERNAME, VALID_PASSWORD)
require.NoError(t, err)

err = testblobs.DynamicLink.PutWithAuth(server.URL, VALID_USERNAME, INVALID_PASSWORD)
require.ErrorContains(t, err, "403")

err = testblobs.DynamicLink.PutWithAuth(server.URL, INVALID_USERNAME, VALID_PASSWORD)
require.ErrorContains(t, err, "403")

err = testblobs.DynamicLink.PutWithAuth(server.URL, INVALID_USERNAME, INVALID_PASSWORD)
require.ErrorContains(t, err, "403")

_, err = testblobs.DynamicLink.Get(server.URL)
require.NoError(t, err)
})

t.Run("invalid main datastore", func(t *testing.T) {
h, err := buildHttpHandler(config{
mainDSLocation: "",
Expand Down Expand Up @@ -108,6 +151,7 @@ func TestExecuteWithConfig(t *testing.T) {
}()
err := executeWithConfig(ctx, config{
mainDSLocation: "memory://",
log: slog.Default(),
})
require.NoError(t, err)
})
Expand Down
32 changes: 29 additions & 3 deletions pkg/datastore/webinterface.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/http"

"github.com/cinode/go/pkg/common"
"golang.org/x/exp/slog"
)

var (
Expand All @@ -32,15 +33,32 @@ var (

// WebInterface provides simple web interface for given Datastore
type webInterface struct {
ds DS
ds DS
log *slog.Logger
}

type webInterfaceOption func(i *webInterface)

func WebInterfaceOptionLogger(log *slog.Logger) webInterfaceOption {
return func(i *webInterface) { i.log = log }
}

// WebInterface returns http handler representing web interface to given
// Datastore instance
func WebInterface(ds DS) http.Handler {
return &webInterface{
func WebInterface(ds DS, opts ...webInterfaceOption) http.Handler {
ret := &webInterface{
ds: ds,
}

for _, o := range opts {
o(ret)
}

if ret.log == nil {
ret.log = slog.Default()
}

return ret
}

func (i *webInterface) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -103,6 +121,14 @@ func (i *webInterface) checkErr(err error, w http.ResponseWriter, r *http.Reques
return false
}

i.log.Error(
"Internal error happened while processing the request", err,
slog.Group("req",
slog.String("remoteAddr", r.RemoteAddr),
slog.String("method", r.Method),
slog.String("url", r.URL.String()),
),
)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return false
}
Expand Down
17 changes: 7 additions & 10 deletions pkg/datastore/webinterface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,17 @@ import (

"github.com/cinode/go/pkg/common"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"
)

// import (
// "bytes"
// "io"
// "mime/multipart"
// "net/http"
// "net/http/httptest"
// "testing"
// )

func testServer(t *testing.T) string {
log := slog.New(slog.NewTextHandler(io.Discard))

// Test web interface and web connector
server := httptest.NewServer(WebInterface(InMemory()))
server := httptest.NewServer(WebInterface(
InMemory(),
WebInterfaceOptionLogger(log),
))
t.Cleanup(func() { server.Close() })
return server.URL + "/"
}
Expand Down
8 changes: 8 additions & 0 deletions testvectors/testblobs/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type TestBlob struct {
}

func (s *TestBlob) Put(baseUrl string) error {
return s.PutWithAuth(baseUrl, "", "")
}

func (s *TestBlob) PutWithAuth(baseUrl, username, password string) error {
finalUrl, err := url.JoinPath(baseUrl, base58.Encode(s.BlobName))
if err != nil {
return err
Expand All @@ -48,6 +52,10 @@ func (s *TestBlob) Put(baseUrl string) error {
return err
}

if username != "" || password != "" {
req.SetBasicAuth(username, password)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
Expand Down
Loading