Skip to content

Commit

Permalink
refactor: HTTP config (#2278)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2277 

## Description

This PR is a refactor of the HTTP config and is part of the larger
config refactor #2257

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

make test

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored Feb 7, 2024
1 parent 36315e4 commit 216db8f
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 554 deletions.
10 changes: 0 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,16 +395,6 @@ defradb start --tls --pubkeypath ~/path-to-pubkey.key --privkeypath ~/path-to-pr

```

DefraDB also comes with automatic HTTPS for deployments on the public web. To enable HTTPS,
deploy DefraDB to a server with both port 80 and port 443 open. With your domain's DNS A record
pointed to the IP of your server, you can run the database using the following command:
```shell
sudo defradb start --tls --url=your-domain.net [email protected]
```
Note: `sudo` is needed above for the redirection server (to bind port 80).

A valid email address is necessary for the creation of the certificate, and is important to get notifications from the Certificate Authority - in case the certificate is about to expire, etc.

## Supporting CORS

When accessing DefraDB through a frontend interface, you may be confronted with a CORS error. That is because, by default, DefraDB will not have any allowed origins set. To specify which origins should be allowed to access your DefraDB endpoint, you can specify them when starting the database:
Expand Down
33 changes: 12 additions & 21 deletions cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (di *defraInstance) close(ctx context.Context) {
} else {
di.db.Close()
}
if err := di.server.Close(); err != nil {
if err := di.server.Shutdown(ctx); err != nil {
log.FeedbackInfo(
ctx,
"The server could not be closed successfully",
Expand Down Expand Up @@ -259,40 +259,31 @@ func start(ctx context.Context, cfg *config.Config) (*defraInstance, error) {
}
}

sOpt := []func(*httpapi.Server){
serverOpts := []httpapi.ServerOpt{
httpapi.WithAddress(cfg.API.Address),
httpapi.WithRootDir(cfg.Rootdir),
httpapi.WithAllowedOrigins(cfg.API.AllowedOrigins...),
httpapi.WithTLSCertPath(cfg.API.PubKeyPath),
httpapi.WithTLSKeyPath(cfg.API.PrivKeyPath),
}

if cfg.API.TLS {
sOpt = append(
sOpt,
httpapi.WithTLS(),
httpapi.WithSelfSignedCert(cfg.API.PubKeyPath, cfg.API.PrivKeyPath),
httpapi.WithCAEmail(cfg.API.Email),
)
}

var server *httpapi.Server
var handler *httpapi.Handler
if node != nil {
server, err = httpapi.NewServer(node, sOpt...)
handler, err = httpapi.NewHandler(node)
} else {
server, err = httpapi.NewServer(db, sOpt...)
handler, err = httpapi.NewHandler(db)
}
if err != nil {
return nil, errors.Wrap("failed to create http server", err)
return nil, errors.Wrap("failed to create http handler", err)
}
if err := server.Listen(ctx); err != nil {
return nil, errors.Wrap(fmt.Sprintf("failed to listen on TCP address %v", server.Addr), err)
server, err := httpapi.NewServer(handler, serverOpts...)
if err != nil {
return nil, errors.Wrap("failed to create http server", err)
}
// save the address on the config in case the port number was set to random
cfg.API.Address = server.AssignedAddr()

// run the server in a separate goroutine
go func() {
log.FeedbackInfo(ctx, fmt.Sprintf("Providing HTTP API at %s.", cfg.API.AddressToURL()))
if err := server.Run(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.FeedbackErrorE(ctx, "Failed to run the HTTP server", err)
if node != nil {
node.Close()
Expand Down
12 changes: 6 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ func (cfg *Config) paramsPreprocessing() error {
if !filepath.IsAbs(cfg.v.GetString("datastore.badger.path")) {
cfg.v.Set("datastore.badger.path", filepath.Join(cfg.Rootdir, cfg.v.GetString("datastore.badger.path")))
}
if !filepath.IsAbs(cfg.v.GetString("api.privkeypath")) {
cfg.v.Set("api.privkeypath", filepath.Join(cfg.Rootdir, cfg.v.GetString("api.privkeypath")))
privKeyPath := cfg.v.GetString("api.privkeypath")
if privKeyPath != "" && !filepath.IsAbs(privKeyPath) {
cfg.v.Set("api.privkeypath", filepath.Join(cfg.Rootdir, privKeyPath))
}
if !filepath.IsAbs(cfg.v.GetString("api.pubkeypath")) {
cfg.v.Set("api.pubkeypath", filepath.Join(cfg.Rootdir, cfg.v.GetString("api.pubkeypath")))
pubKeyPath := cfg.v.GetString("api.pubkeypath")
if pubKeyPath != "" && !filepath.IsAbs(pubKeyPath) {
cfg.v.Set("api.pubkeypath", filepath.Join(cfg.Rootdir, pubKeyPath))
}

// log.logger configuration as a string
Expand Down Expand Up @@ -303,8 +305,6 @@ func defaultAPIConfig() *APIConfig {
Address: "localhost:9181",
TLS: false,
AllowedOrigins: []string{},
PubKeyPath: "certs/server.key",
PrivKeyPath: "certs/server.crt",
Email: DefaultAPIEmail,
}
}
Expand Down
4 changes: 2 additions & 2 deletions config/configfile_yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ api:
# The list of origins a cross-domain request can be executed from.
# allowed-origins: {{ .API.AllowedOrigins }}
# The path to the public key file. Ignored if domains is set.
pubkeypath: {{ .API.PubKeyPath }}
# pubkeypath: {{ .API.PubKeyPath }}
# The path to the private key file. Ignored if domains is set.
privkeypath: {{ .API.PrivKeyPath }}
# privkeypath: {{ .API.PrivKeyPath }}
# Email address to let the CA (Let's Encrypt) send notifications via email when there are issues (optional).
# email: {{ .API.Email }}

Expand Down
10 changes: 2 additions & 8 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/sourcenetwork/defradb/datastore"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)

// Version is the identifier for the current API version.
Expand Down Expand Up @@ -69,22 +68,17 @@ type Handler struct {
txs *sync.Map
}

func NewHandler(db client.DB, opts ServerOptions) (*Handler, error) {
func NewHandler(db client.DB) (*Handler, error) {
router, err := NewApiRouter()
if err != nil {
return nil, err
}
txs := &sync.Map{}

mux := chi.NewMux()
mux.Use(
middleware.RequestLogger(&logFormatter{}),
middleware.Recoverer,
CorsMiddleware(opts),
)
mux.Route("/api/"+Version, func(r chi.Router) {
r.Use(
ApiMiddleware(db, txs, opts),
ApiMiddleware(db, txs),
TransactionMiddleware,
StoreMiddleware,
)
Expand Down
12 changes: 6 additions & 6 deletions http/handler_ccip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestCCIPGet_WithValidData(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand Down Expand Up @@ -88,7 +88,7 @@ func TestCCIPGet_WithSubscription(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand All @@ -106,7 +106,7 @@ func TestCCIPGet_WithInvalidData(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand Down Expand Up @@ -135,7 +135,7 @@ func TestCCIPPost_WithValidData(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body))
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand Down Expand Up @@ -167,7 +167,7 @@ func TestCCIPPost_WithInvalidGraphQLRequest(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body))
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand All @@ -181,7 +181,7 @@ func TestCCIPPost_WithInvalidBody(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", nil)
rec := httptest.NewRecorder()

handler, err := NewHandler(cdb, ServerOptions{})
handler, err := NewHandler(cdb)
require.NoError(t, err)
handler.ServeHTTP(rec, req)

Expand Down
12 changes: 4 additions & 8 deletions http/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ var (
)

// CorsMiddleware handles cross origin request
func CorsMiddleware(opts ServerOptions) func(http.Handler) http.Handler {
func CorsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
return cors.Handler(cors.Options{
AllowOriginFunc: func(r *http.Request, origin string) bool {
if slices.Contains(opts.AllowedOrigins, "*") {
if slices.Contains(allowedOrigins, "*") {
return true
}
return slices.Contains(opts.AllowedOrigins, strings.ToLower(origin))
return slices.Contains(allowedOrigins, strings.ToLower(origin))
},
AllowedMethods: []string{"GET", "HEAD", "POST", "PATCH", "DELETE"},
AllowedHeaders: []string{"Content-Type"},
Expand All @@ -71,13 +71,9 @@ func CorsMiddleware(opts ServerOptions) func(http.Handler) http.Handler {
}

// ApiMiddleware sets the required context values for all API requests.
func ApiMiddleware(db client.DB, txs *sync.Map, opts ServerOptions) func(http.Handler) http.Handler {
func ApiMiddleware(db client.DB, txs *sync.Map) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if opts.TLS.HasValue() {
rw.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}

ctx := req.Context()
ctx = context.WithValue(ctx, dbContextKey, db)
ctx = context.WithValue(ctx, txsContextKey, txs)
Expand Down
Loading

0 comments on commit 216db8f

Please sign in to comment.