Commit db47d88c authored by Janoš Guljaš's avatar Janoš Guljaš Committed by GitHub

api gateway mode (#675)

parent dfeca7c2
......@@ -41,6 +41,7 @@ const (
optionNamePaymentThreshold = "payment-threshold"
optionNamePaymentTolerance = "payment-tolerance"
optionNameResolverEndpoints = "resolver-options"
optionNameGatewayMode = "gateway-mode"
)
func init() {
......@@ -184,4 +185,5 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Uint64(optionNamePaymentThreshold, 100000, "threshold in BZZ where you expect to get paid from your peers")
cmd.Flags().Uint64(optionNamePaymentTolerance, 10000, "excess debt above payment threshold in BZZ where you disconnect from your peer")
cmd.Flags().StringSlice(optionNameResolverEndpoints, []string{}, "resolver connection string, see help for format")
cmd.Flags().Bool(optionNameGatewayMode, false, "disable a set of sensitive features in the api")
}
......@@ -161,6 +161,7 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
PaymentThreshold: c.config.GetUint64(optionNamePaymentThreshold),
PaymentTolerance: c.config.GetUint64(optionNamePaymentTolerance),
ResolverConnectionCfgs: resolverCfgs,
GatewayMode: c.config.GetBool(optionNameGatewayMode),
})
if err != nil {
return err
......
......@@ -156,7 +156,7 @@ func newTestServer(t *testing.T, storer storage.Storer) *url.URL {
t.Helper()
logger := logging.New(ioutil.Discard, 0)
store := statestore.NewStateStore()
s := api.New(tags.NewTags(store, logger), storer, nil, nil, logger, nil)
s := api.New(tags.NewTags(store, logger), storer, nil, logger, nil, api.Options{})
ts := httptest.NewServer(s)
srvUrl, err := url.Parse(ts.URL)
if err != nil {
......
......@@ -69,6 +69,8 @@ paths:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/ReferenceResponse'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -169,6 +171,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/Status'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -227,6 +231,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/ReferenceResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -301,6 +307,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/ReferenceResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -367,6 +375,8 @@ paths:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/NewTagResponse'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -393,6 +403,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/NewTagResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......@@ -413,6 +425,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/responses/204'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'404':
$ref: 'SwarmCommon.yaml#/components/responses/404'
'500':
......@@ -444,6 +458,8 @@ paths:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Status'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'404':
$ref: 'SwarmCommon.yaml#/components/responses/404'
'500':
......@@ -472,6 +488,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/Response'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'404':
$ref: 'SwarmCommon.yaml#/components/responses/404'
default:
......@@ -489,6 +507,8 @@ paths:
$ref: 'SwarmCommon.yaml#/components/schemas/Response'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/400'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'404':
$ref: 'SwarmCommon.yaml#/components/responses/404'
default:
......@@ -504,12 +524,14 @@ paths:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/PinningState'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
'/pinning/chunks/':
'/pinning/chunks':
get:
summary: Get list of pinned chunks
tags:
......@@ -521,6 +543,8 @@ paths:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/BzzChunksPinned'
'403':
$ref: 'SwarmCommon.yaml#/components/responses/403'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
......
......@@ -228,6 +228,12 @@ components:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'403':
description: Forbidden
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'404':
description: Not Found
content:
......
......@@ -39,31 +39,36 @@ type Service interface {
}
type server struct {
Tags *tags.Tags
Storer storage.Storer
Resolver resolver.Interface
CORSAllowedOrigins []string
Logger logging.Logger
Tracer *tracing.Tracer
Tags *tags.Tags
Storer storage.Storer
Resolver resolver.Interface
Logger logging.Logger
Tracer *tracing.Tracer
Options
http.Handler
metrics metrics
}
type Options struct {
CORSAllowedOrigins []string
GatewayMode bool
}
const (
// TargetsRecoveryHeader defines the Header for Recovery targets in Global Pinning
TargetsRecoveryHeader = "swarm-recovery-targets"
)
// New will create a and initialize a new API service.
func New(tags *tags.Tags, storer storage.Storer, resolver resolver.Interface, corsAllowedOrigins []string, logger logging.Logger, tracer *tracing.Tracer) Service {
func New(tags *tags.Tags, storer storage.Storer, resolver resolver.Interface, logger logging.Logger, tracer *tracing.Tracer, o Options) Service {
s := &server{
Tags: tags,
Storer: storer,
Resolver: resolver,
CORSAllowedOrigins: corsAllowedOrigins,
Logger: logger,
Tracer: tracer,
metrics: newMetrics(),
Tags: tags,
Storer: storer,
Resolver: resolver,
Options: o,
Logger: logger,
Tracer: tracer,
metrics: newMetrics(),
}
s.setupRouting()
......
......@@ -14,7 +14,6 @@ import (
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/pingpong"
"github.com/ethersphere/bee/pkg/resolver"
resolverMock "github.com/ethersphere/bee/pkg/resolver/mock"
"github.com/ethersphere/bee/pkg/storage"
......@@ -24,11 +23,11 @@ import (
)
type testServerOptions struct {
Pingpong pingpong.Interface
Storer storage.Storer
Resolver resolver.Interface
Tags *tags.Tags
Logger logging.Logger
Storer storage.Storer
Resolver resolver.Interface
Tags *tags.Tags
GatewayMode bool
Logger logging.Logger
}
func newTestServer(t *testing.T, o testServerOptions) *http.Client {
......@@ -38,7 +37,9 @@ func newTestServer(t *testing.T, o testServerOptions) *http.Client {
if o.Resolver == nil {
o.Resolver = resolverMock.NewResolver()
}
s := api.New(o.Tags, o.Storer, o.Resolver, nil, o.Logger, nil)
s := api.New(o.Tags, o.Storer, o.Resolver, o.Logger, nil, api.Options{
GatewayMode: o.GatewayMode,
})
ts := httptest.NewServer(s)
t.Cleanup(ts.Close)
......@@ -115,7 +116,7 @@ func TestParseName(t *testing.T) {
}))
}
s := api.New(nil, nil, tC.res, nil, tC.log, nil).(*api.Server)
s := api.New(nil, nil, tC.res, tC.log, nil, api.Options{}).(*api.Server)
t.Run(tC.desc, func(t *testing.T) {
got, err := s.ResolveNameOrAddress(tC.name)
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package api_test
import (
"io/ioutil"
"net/http"
"testing"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/storage/mock"
"github.com/ethersphere/bee/pkg/tags"
)
func TestGatewayMode(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
client := newTestServer(t, testServerOptions{
Storer: mock.NewStorer(),
Tags: tags.NewTags(statestore.NewStateStore(), logger),
Logger: logger,
GatewayMode: true,
})
forbiddenResponseOption := jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: http.StatusText(http.StatusForbidden),
Code: http.StatusForbidden,
})
t.Run("pinning endpoints", func(t *testing.T) {
path := "/pinning/chunks/0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
jsonhttptest.Request(t, client, http.MethodGet, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodPost, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodDelete, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodGet, "/pinning/chunks", http.StatusForbidden, forbiddenResponseOption)
})
t.Run("tags endpoints", func(t *testing.T) {
path := "/tags/42"
jsonhttptest.Request(t, client, http.MethodGet, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodDelete, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodPatch, path, http.StatusForbidden, forbiddenResponseOption)
jsonhttptest.Request(t, client, http.MethodGet, "/tags", http.StatusForbidden, forbiddenResponseOption)
})
t.Run("pinning", func(t *testing.T) {
headerOption := jsonhttptest.WithRequestHeader(api.SwarmPinHeader, "true")
forbiddenResponseOption := jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: "pinning is disabled",
Code: http.StatusForbidden,
})
jsonhttptest.Request(t, client, http.MethodPost, "/chunks/0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c", http.StatusOK) // should work without pinning
jsonhttptest.Request(t, client, http.MethodPost, "/chunks/0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c", http.StatusForbidden, forbiddenResponseOption, headerOption)
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", http.StatusOK) // should work without pinning
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", http.StatusForbidden, forbiddenResponseOption, headerOption)
jsonhttptest.Request(t, client, http.MethodPost, "/files", http.StatusForbidden, forbiddenResponseOption, headerOption)
jsonhttptest.Request(t, client, http.MethodPost, "/dirs", http.StatusForbidden, forbiddenResponseOption, headerOption)
})
t.Run("encryption", func(t *testing.T) {
headerOption := jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, "true")
forbiddenResponseOption := jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: "encryption is disabled",
Code: http.StatusForbidden,
})
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", http.StatusOK) // should work without pinning
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", http.StatusForbidden, forbiddenResponseOption, headerOption)
jsonhttptest.Request(t, client, http.MethodPost, "/files", http.StatusForbidden, forbiddenResponseOption, headerOption)
jsonhttptest.Request(t, client, http.MethodPost, "/dirs", http.StatusForbidden, forbiddenResponseOption, headerOption)
})
}
......@@ -7,6 +7,7 @@ package api
import (
"fmt"
"net/http"
"strings"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/logging"
......@@ -66,29 +67,41 @@ func (s *server) setupRouting() {
"GET": http.HandlerFunc(s.bzzDownloadHandler),
})
handle(router, "/tags", jsonhttp.MethodHandler{
"POST": web.ChainHandlers(
jsonhttp.NewMaxBodyBytesHandler(1024),
web.FinalHandlerFunc(s.createTag),
),
})
handle(router, "/tags/{id}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.getTag),
"DELETE": http.HandlerFunc(s.deleteTag),
"PATCH": web.ChainHandlers(
jsonhttp.NewMaxBodyBytesHandler(1024),
web.FinalHandlerFunc(s.doneSplit),
),
})
handle(router, "/tags", web.ChainHandlers(
s.gatewayModeForbidEndpointHandler,
web.FinalHandler(jsonhttp.MethodHandler{
"POST": web.ChainHandlers(
jsonhttp.NewMaxBodyBytesHandler(1024),
web.FinalHandlerFunc(s.createTag),
),
})),
)
handle(router, "/tags/{id}", web.ChainHandlers(
s.gatewayModeForbidEndpointHandler,
web.FinalHandler(jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.getTag),
"DELETE": http.HandlerFunc(s.deleteTag),
"PATCH": web.ChainHandlers(
jsonhttp.NewMaxBodyBytesHandler(1024),
web.FinalHandlerFunc(s.doneSplit),
),
})),
)
handle(router, "/pinning/chunks/{address}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.getPinnedChunk),
"POST": http.HandlerFunc(s.pinChunk),
"DELETE": http.HandlerFunc(s.unpinChunk),
})
handle(router, "/pinning/chunks", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.listPinnedChunks),
})
handle(router, "/pinning/chunks/{address}", web.ChainHandlers(
s.gatewayModeForbidEndpointHandler,
web.FinalHandler(jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.getPinnedChunk),
"POST": http.HandlerFunc(s.pinChunk),
"DELETE": http.HandlerFunc(s.unpinChunk),
})),
)
handle(router, "/pinning/chunks", web.ChainHandlers(
s.gatewayModeForbidEndpointHandler,
web.FinalHandler(jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.listPinnedChunks),
})),
)
s.Handler = web.ChainHandlers(
logging.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, "api access"),
......@@ -107,6 +120,7 @@ func (s *server) setupRouting() {
h.ServeHTTP(w, r)
})
},
s.gatewayModeForbidHeadersHandler,
web.FinalHandler(router),
)
}
......@@ -119,3 +133,32 @@ func containsOrigin(s string, l []string) (ok bool) {
}
return false
}
func (s *server) gatewayModeForbidEndpointHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.GatewayMode {
s.Logger.Tracef("gateway mode: forbidden %s", r.URL.String())
jsonhttp.Forbidden(w, nil)
return
}
h.ServeHTTP(w, r)
})
}
func (s *server) gatewayModeForbidHeadersHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.GatewayMode {
if strings.ToLower(r.Header.Get(SwarmPinHeader)) == "true" {
s.Logger.Tracef("gateway mode: forbidden pinning %s", r.URL.String())
jsonhttp.Forbidden(w, "pinning is disabled")
return
}
if strings.ToLower(r.Header.Get(SwarmEncryptHeader)) == "true" {
s.Logger.Tracef("gateway mode: forbidden encryption %s", r.URL.String())
jsonhttp.Forbidden(w, "encryption is disabled")
return
}
}
h.ServeHTTP(w, r)
})
}
......@@ -92,6 +92,7 @@ type Options struct {
PaymentThreshold uint64
PaymentTolerance uint64
ResolverConnectionCfgs []*resolver.ConnectionConfig
GatewayMode bool
}
func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, swarmPrivateKey *ecdsa.PrivateKey, networkID uint64, logger logging.Logger, o Options) (*Bee, error) {
......@@ -301,7 +302,10 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
var apiService api.Service
if o.APIAddr != "" {
// API server
apiService = api.New(tagg, ns, multiResolver, o.CORSAllowedOrigins, logger, tracer)
apiService = api.New(tagg, ns, multiResolver, logger, tracer, api.Options{
CORSAllowedOrigins: o.CORSAllowedOrigins,
GatewayMode: o.GatewayMode,
})
apiListener, err := net.Listen("tcp", o.APIAddr)
if err != nil {
return nil, fmt.Errorf("api listener: %w", err)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment