implement auth and a simple admin-follows whitelist
Some checks failed
Go / build (push) Has been cancelled

This commit is contained in:
2025-09-07 19:08:29 +01:00
parent f5a8c094e4
commit 5edb7a3b09
27 changed files with 458 additions and 117 deletions

View File

@@ -1 +1,50 @@
package app
import (
"encoders.orly/envelopes/authenvelope"
"encoders.orly/envelopes/okenvelope"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"protocol.orly/auth"
)
func (l *Listener) HandleAuth(b []byte) (err error) {
var rem []byte
env := authenvelope.NewResponse()
if rem, err = env.Unmarshal(b); chk.E(err) {
return
}
if len(rem) > 0 {
log.I.F("extra '%s'", rem)
}
var valid bool
if valid, err = auth.Validate(
env.Event, l.challenge.Load(),
l.ServiceURL(l.req),
); err != nil {
e := err.Error()
if err = Ok.Error(l, env, e); chk.E(err) {
return
}
return
} else if !valid {
if err = Ok.Error(
l, env, "auth response event is invalid",
); chk.E(err) {
return
}
return
} else {
if err = okenvelope.NewFrom(
env.Event.ID, true,
).Write(l); chk.E(err) {
return
}
log.D.F(
"%s authed to pubkey,%0x", l.remote,
env.Event.Pubkey,
)
l.authedPubkey.Store(env.Event.Pubkey)
}
return
}

View File

@@ -5,8 +5,11 @@ import (
"strings"
acl "acl.orly"
"encoders.orly/envelopes/authenvelope"
"encoders.orly/envelopes/eventenvelope"
"encoders.orly/envelopes/okenvelope"
"encoders.orly/kind"
"encoders.orly/reason"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
utils "utils.orly"
@@ -53,6 +56,51 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
}
return
}
// // send a challenge to the client to auth if an ACL is active and not authed
// if acl.Registry.Active.Load() != "none" && l.authedPubkey.Load() == nil {
// log.D.F("sending challenge to %s", l.remote)
// if err = authenvelope.NewChallengeWith(l.challenge.Load()).
// Write(l); chk.E(err) {
// // return
// }
// // ACL is enabled so return and wait for auth
// // return
// }
// check permissions of user
accessLevel := acl.Registry.GetAccessLevel(l.authedPubkey.Load())
switch accessLevel {
case "none":
log.D.F("handle event: sending CLOSED to %s", l.remote)
if err = okenvelope.NewFrom(
env.Id(), false,
reason.AuthRequired.F("auth required for write access"),
).Write(l); chk.E(err) {
// return
}
log.D.F("handle event: sending challenge to %s", l.remote)
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
Write(l); chk.E(err) {
return
}
return
case "read":
log.D.F("handle event: sending CLOSED to %s", l.remote)
if err = okenvelope.NewFrom(
env.Id(), false,
reason.AuthRequired.F("auth required for write access"),
).Write(l); chk.E(err) {
// return
}
log.D.F("handle event: sending challenge to %s", l.remote)
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
Write(l); chk.E(err) {
// return
}
return
default:
// user has write access or better, continue
log.D.F("user has %s access", accessLevel)
}
// if the event is a delete, process the delete
if env.E.Kind == kind.EventDeletion.K {
l.HandleDelete(env)

View File

@@ -38,6 +38,7 @@ func (l *Listener) HandleMessage(msg []byte, remote string) {
err = l.HandleClose(rem)
case authenvelope.L:
log.D.F("authenvelope: %s", rem)
err = l.HandleAuth(rem)
default:
err = errorf.E("unknown envelope type %s\n%s", t, rem)
}

View File

@@ -44,6 +44,23 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
// relayinfo.ProtectedEvents,
// relayinfo.RelayListMetadata,
)
if s.Config.ACLMode != "none" {
supportedNIPs = relayinfo.GetList(
relayinfo.BasicProtocol,
relayinfo.Authentication,
// relayinfo.EncryptedDirectMessage,
// relayinfo.EventDeletion,
relayinfo.RelayInformationDocument,
// relayinfo.GenericTagQueries,
// relayinfo.NostrMarketplace,
// relayinfo.EventTreatment,
// relayinfo.CommandResults,
// relayinfo.ParameterizedReplaceableEvents,
// relayinfo.ExpirationTimestamp,
// relayinfo.ProtectedEvents,
// relayinfo.RelayListMetadata,
)
}
sort.Sort(supportedNIPs)
log.T.Ln("supported NIPs", supportedNIPs)
info = &relayinfo.T{
@@ -52,9 +69,9 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
Nips: supportedNIPs,
Software: version.URL,
Version: version.V,
Limitation: relayinfo.Limits{
// AuthRequired: s.C.AuthRequired,
// RestrictedWrites: s.C.AuthRequired,
Limitation: relayinfo.Limits{
AuthRequired: s.Config.ACLMode != "none",
RestrictedWrites: s.Config.ACLMode != "none",
},
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
}

View File

@@ -3,12 +3,16 @@ package app
import (
"errors"
acl "acl.orly"
"encoders.orly/envelopes/authenvelope"
"encoders.orly/envelopes/closedenvelope"
"encoders.orly/envelopes/eoseenvelope"
"encoders.orly/envelopes/eventenvelope"
"encoders.orly/envelopes/okenvelope"
"encoders.orly/envelopes/reqenvelope"
"encoders.orly/event"
"encoders.orly/filter"
"encoders.orly/reason"
"encoders.orly/tag"
"github.com/dgraph-io/badger/v4"
"lol.mleku.dev/chk"
@@ -28,6 +32,50 @@ func (l *Listener) HandleReq(msg []byte) (
if len(rem) > 0 {
log.I.F("extra '%s'", rem)
}
// // send a challenge to the client to auth if an ACL is active and not authed
// if acl.Registry.Active.Load() != "none" && l.authedPubkey.Load() == nil {
// log.D.F("sending challenge to %s", l.remote)
// if err = authenvelope.NewChallengeWith(l.challenge.Load()).
// Write(l); chk.E(err) {
// // return
// }
// log.D.F("sending CLOSED to %s", l.remote)
// if err = closedenvelope.NewFrom(
// env.Subscription, reason.AuthRequired.F("auth required for access"),
// ).Write(l); chk.E(err) {
// return
// }
// // ACL is enabled so return and wait for auth
// // return
// }
// send a challenge to the client to auth if an ACL is active
if acl.Registry.Active.Load() != "none" {
// log.D.F("sending CLOSED to %s", l.remote)
// if err = closedenvelope.NewFrom(
// env.Subscription, reason.AuthRequired.F("auth required for access"),
// ).Write(l); chk.E(err) {
// // return
// }
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
Write(l); chk.E(err) {
// return
}
}
// check permissions of user
accessLevel := acl.Registry.GetAccessLevel(l.authedPubkey.Load())
switch accessLevel {
case "none":
if err = okenvelope.NewFrom(
env.Subscription, false,
reason.AuthRequired.F("user not authed or has no read access"),
).Write(l); chk.E(err) {
return
}
return
default:
// user has read access or better, continue
log.D.F("user has %s access", accessLevel)
}
var events event.S
for _, f := range *env.Filters {
if pointers.Present(f.Limit) {

View File

@@ -2,10 +2,12 @@ package app
import (
"context"
"crypto/rand"
"net/http"
"strings"
"time"
"encoders.orly/hex"
"github.com/coder/websocket"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
@@ -63,7 +65,11 @@ whitelist:
Server: s,
conn: conn,
remote: remote,
req: r,
}
chal := make([]byte, 32)
rand.Read(chal)
listener.challenge.Store([]byte(hex.Enc(chal)))
ticker := time.NewTicker(DefaultPingWait)
go s.Pinger(ctx, conn, ticker, cancel)
defer func() {

View File

@@ -2,16 +2,21 @@ package app
import (
"context"
"net/http"
"github.com/coder/websocket"
"lol.mleku.dev/chk"
"utils.orly/atomic"
)
type Listener struct {
*Server
conn *websocket.Conn
ctx context.Context
remote string
conn *websocket.Conn
ctx context.Context
remote string
req *http.Request
challenge atomic.Bytes
authedPubkey atomic.Bytes
}
func (l *Listener) Write(p []byte) (n int, err error) {

View File

@@ -10,7 +10,7 @@ import (
// parameters to generate formatted messages and return errors if any issues
// occur during processing.
type OK func(
l *Listener, env *eventenvelope.Submission, format string, params ...any,
l *Listener, env eventenvelope.I, format string, params ...any,
) (err error)
// OKs provides a collection of handler functions for managing different types
@@ -36,7 +36,7 @@ type OKs struct {
// inputs.
var Ok = OKs{
Ok: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -44,7 +44,7 @@ var Ok = OKs{
).Write(l)
},
AuthRequired: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -52,7 +52,7 @@ var Ok = OKs{
).Write(l)
},
PoW: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -60,7 +60,7 @@ var Ok = OKs{
).Write(l)
},
Duplicate: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -68,7 +68,7 @@ var Ok = OKs{
).Write(l)
},
Blocked: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -76,7 +76,7 @@ var Ok = OKs{
).Write(l)
},
RateLimited: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -84,7 +84,7 @@ var Ok = OKs{
).Write(l)
},
Invalid: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -92,7 +92,7 @@ var Ok = OKs{
).Write(l)
},
Error: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -100,7 +100,7 @@ var Ok = OKs{
).Write(l)
},
Unsupported: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(
@@ -108,7 +108,7 @@ var Ok = OKs{
).Write(l)
},
Restricted: func(
l *Listener, env *eventenvelope.Submission, format string,
l *Listener, env eventenvelope.I, format string,
params ...any,
) (err error) {
return okenvelope.NewFrom(

View File

@@ -4,8 +4,11 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"database.orly"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/app/config"
"protocol.orly/publish"
@@ -39,3 +42,33 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
}
func (s *Server) ServiceURL(req *http.Request) (st string) {
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
host = req.Host
}
proto := req.Header.Get("X-Forwarded-Proto")
if proto == "" {
if host == "localhost" {
proto = "ws"
} else if strings.Contains(host, ":") {
// has a port number
proto = "ws"
} else if _, err := strconv.Atoi(
strings.ReplaceAll(
host, ".",
"",
),
); chk.E(err) {
// it's a naked IP
proto = "ws"
} else {
proto = "wss"
}
} else if proto == "https" {
proto = "wss"
} else if proto == "http" {
proto = "ws"
}
return proto + "://" + host
}