Skip to content

Commit

Permalink
switch admin to use http basic auth for security
Browse files Browse the repository at this point in the history
  • Loading branch information
mleku committed Nov 29, 2024
1 parent 7ffb1c9 commit 6d7f8e7
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 112 deletions.
3 changes: 2 additions & 1 deletion cmd/realy/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ type Config struct {
Listen S `env:"LISTEN" default:"0.0.0.0" usage:"network listen address"`
Port N `env:"PORT" default:"3334" usage:"port to listen on"`
AdminListen S `env:"ADMIN_LISTEN" default:"127.0.0.1" usage:"admin listen address"`
AdminPort N `env:"ADMIN_PORT" default:"3337" usage:"admin listen port"`
AdminUser S `env:"ADMIN_USER" default:"admin" usage:"admin user"`
AdminPass S `env:"ADMIN_PASS" usage:"admin password"`
LogLevel S `env:"LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
DbLogLevel S `env:"DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
AuthRequired bool `env:"AUTH_REQUIRED" default:"false" usage:"requires auth for all access"`
Expand Down
31 changes: 25 additions & 6 deletions cmd/realy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,40 @@ func main() {
debug.SetMemoryLimit(int64(cfg.MemLimit))
var wg sync.WaitGroup
c, cancel := context.Cancel(context.Bg())
storage := ratel.GetBackend(c, &wg, false, units.Gb*166, lol.GetLogLevel(cfg.DbLogLevel),
ratel.DefaultMaxLimit,
cfg.DBSizeLimit, cfg.DBLowWater, cfg.DBHighWater, cfg.GCFrequency*int(time.Second))
storage := ratel.New(
ratel.BackendParams{
Ctx: c,
WG: &wg,
BlockCacheSize: units.Gb * 16,
LogLevel: lol.GetLogLevel(cfg.DbLogLevel),
MaxLimit: ratel.DefaultMaxLimit,
Extra: []int{
cfg.DBSizeLimit,
cfg.DBLowWater,
cfg.DBHighWater,
cfg.GCFrequency * int(time.Second),
},
},
)
r := &app.Relay{Config: cfg, Store: storage}
go app.MonitorResources(c)
var server *realy.Server
if server, err = realy.NewServer(c, cancel, r, cfg.Profile,
ratel.DefaultMaxLimit); chk.E(err) {
if server, err = realy.NewServer(realy.ServerParams{
Ctx: c,
Cancel: cancel,
Rl: r,
DbPath: cfg.Profile,
MaxLimit: ratel.DefaultMaxLimit,
AdminUser: cfg.AdminUser,
AdminPass: cfg.AdminPass}); chk.E(err) {

os.Exit(1)
}
if err != nil {
log.F.F("failed to create server: %v", err)
}
interrupt.AddHandler(func() { server.Shutdown() })
if err = server.Start(cfg.Listen, cfg.Port, cfg.AdminListen, cfg.AdminPort); chk.E(err) {
if err = server.Start(cfg.Listen, cfg.Port); chk.E(err) {
log.F.F("server terminated: %v", err)
}
// cancel()
Expand Down
2 changes: 1 addition & 1 deletion ratel/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func (r *T) Export(c context.T, w io.Writer, pubkeys ...B) {
}
item := it.Item()
b, e := item.ValueCopy(nil)
if e != nil {
if chk.E(e) {
err = nil
continue
}
Expand Down
14 changes: 14 additions & 0 deletions ratel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,27 @@ type T struct {

var _ store.I = (*T)(nil)

type BackendParams struct {
Ctx context.T
WG *sync.WaitGroup
HasL2 bool
BlockCacheSize, LogLevel, MaxLimit int
Extra []int
}

func New(p BackendParams, params ...int) *T {
return GetBackend(p.Ctx, p.WG, p.HasL2, p.BlockCacheSize, p.LogLevel, p.MaxLimit, params...)
}

// GetBackend returns a reasonably configured badger.Backend.
//
// The variadic params correspond to DBSizeLimit, DBLowWater, DBHighWater and
// GCFrequency as an integer multiplier of number of seconds.
//
// Note that the cancel function for the context needs to be managed by the
// caller.
//
// Deprecated: use New instead.
func GetBackend(Ctx context.T, WG *sync.WaitGroup, hasL2 bool,
blockCacheSize, logLevel, maxLimit int, params ...int) (b *T) {
var sizeLimit, lw, hw, freq = 0, 50, 66, 3600
Expand Down
135 changes: 85 additions & 50 deletions realy/admin.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package realy

import (
"crypto/subtle"
"fmt"
"io"
"net/http"
Expand All @@ -9,63 +10,97 @@ import (
"realy.lol/cmd/realy/app"
"realy.lol/context"
"realy.lol/hex"
"realy.lol/sha256"
)

func (s *Server) HandleAdmin(w http.ResponseWriter, r *http.Request) {
log.I.S(r.Header)
switch {
case strings.HasPrefix(r.URL.Path, "/export"):
log.I.F("export of event data requested on admin port")
store := s.relay.Storage(context.Bg())
if strings.Count(r.URL.Path, "/") > 1 {
split := strings.Split(r.URL.Path, "/")
// there should be 3 for a valid path, an empty, "export" and the final parameter
if len(split) != 3 {
fmt.Fprintf(w, "incorrectly formatted export parameter: '%s'",
r.URL.Path)
return
}
switch split[2] {
case "users":
// todo: naughty reaching through interface here lol... but the relay
// implementation does have this feature and another impl may not. Perhaps add
// a new interface for grabbing the relay's allowed list, and rename things to
// be more clear. And add a method for fetching such a relay's allowed writers.
if rl, ok := s.relay.(*app.Relay); ok {
follows := make([]B, 0, len(rl.Followed))
for f := range rl.Followed {
follows = append(follows, B(f))
}
store.Export(s.Ctx, w, follows...)
username, password, ok := r.BasicAuth()
if ok {
log.I.S(username, password)
// Calculate SHA-256 hashes for the provided and expected
// usernames and passwords.
usernameHash := sha256.Sum256(B(username))
passwordHash := sha256.Sum256(B(password))
expectedUsernameHash := sha256.Sum256(B(s.adminUser))
expectedPasswordHash := sha256.Sum256(B(s.adminPass))

// Use the subtle.ConstantTimeCompare() function to check if
// the provided username and password hashes equal the
// expected username and password hashes. ConstantTimeCompare
// will return 1 if the values are equal, or 0 otherwise.
// Importantly, we should to do the work to evaluate both the
// username and password before checking the return values to
// avoid leaking information.
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:],
expectedUsernameHash[:]) == 1
passwordMatch := subtle.ConstantTimeCompare(passwordHash[:],
expectedPasswordHash[:]) == 1

// If the username and password are correct, then call
// the next handler in the chain. Make sure to return
// afterwards, so that none of the code below is run.
if !usernameMatch || !passwordMatch {
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
switch {
case strings.HasPrefix(r.URL.Path, "/export"):
log.I.F("export of event data requested on admin port")
store := s.relay.Storage(context.Bg())
if strings.Count(r.URL.Path, "/") > 1 {
split := strings.Split(r.URL.Path, "/")
// there should be 3 for a valid path, an empty, "export" and the final parameter
if len(split) != 3 {
fmt.Fprintf(w, "incorrectly formatted export parameter: '%s'",
r.URL.Path)
return
}
default:
// this should be a hyphen separated list of hexadecimal pubkey values
var exportPubkeys []B
pubkeys := strings.Split(split[2], "-")
for _, pubkey := range pubkeys {
// check they are valid hex
pk, err := hex.Dec(pubkey)
if err != nil {
log.E.F("invalid public key '%s' in parameters", pubkey)
continue
switch split[2] {
case "users":
// todo: naughty reaching through interface here lol... but the relay
// implementation does have this feature and another impl may not. Perhaps add
// a new interface for grabbing the relay's allowed list, and rename things to
// be more clear. And add a method for fetching such a relay's allowed writers.
if rl, ok := s.relay.(*app.Relay); ok {
follows := make([]B, 0, len(rl.Followed))
for f := range rl.Followed {
follows = append(follows, B(f))
}
store.Export(s.Ctx, w, follows...)
}
default:
// this should be a hyphen separated list of hexadecimal pubkey values
var exportPubkeys []B
pubkeys := strings.Split(split[2], "-")
for _, pubkey := range pubkeys {
// check they are valid hex
pk, err := hex.Dec(pubkey)
if err != nil {
log.E.F("invalid public key '%s' in parameters", pubkey)
continue
}
exportPubkeys = append(exportPubkeys, pk)
}
exportPubkeys = append(exportPubkeys, pk)
store.Export(s.Ctx, w, exportPubkeys...)
}
store.Export(s.Ctx, w, exportPubkeys...)
} else {
store.Export(s.Ctx, w)
}
} else {
store.Export(s.Ctx, w)
case strings.HasPrefix(r.URL.Path, "/import"):
log.I.F("import of event data requested on admin port %s", r.RequestURI)
store := s.relay.Storage(context.Bg())
read := io.LimitReader(r.Body, r.ContentLength)
store.Import(read)
case strings.HasPrefix(r.URL.Path, "/shutdown"):
fmt.Fprintf(w, "shutting down")
defer r.Body.Close()
s.Shutdown()
default:
fmt.Fprintf(w, `todo: make help info page`)
}
case strings.HasPrefix(r.URL.Path, "/import"):
log.I.F("import of event data requested on admin port %s", r.RequestURI)
store := s.relay.Storage(context.Bg())
read := io.LimitReader(r.Body, r.ContentLength)
store.Import(read)
case strings.HasPrefix(r.URL.Path, "/shutdown"):
fmt.Fprintf(w, "shutting down")
defer r.Body.Close()
s.Shutdown()
default:
fmt.Fprintf(w, `todo: make help info page`)
} else {
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
Loading

0 comments on commit 6d7f8e7

Please sign in to comment.