You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
9.5 KiB
384 lines
9.5 KiB
// Package grpc provides a gRPC client that implements the acl.I interface. |
|
// This allows the relay to use a remote ACL server via gRPC. |
|
package grpc |
|
|
|
import ( |
|
"context" |
|
"time" |
|
|
|
"google.golang.org/grpc" |
|
"google.golang.org/grpc/credentials/insecure" |
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/log" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
acliface "next.orly.dev/pkg/interfaces/acl" |
|
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1" |
|
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1" |
|
) |
|
|
|
// Client implements the acl.I interface via gRPC. |
|
type Client struct { |
|
conn *grpc.ClientConn |
|
client orlyaclv1.ACLServiceClient |
|
ready chan struct{} |
|
mode string |
|
} |
|
|
|
// Verify Client implements acl.I at compile time. |
|
var _ acliface.I = (*Client)(nil) |
|
|
|
// Verify Client implements acl.PolicyChecker at compile time. |
|
var _ acliface.PolicyChecker = (*Client)(nil) |
|
|
|
// ClientConfig holds configuration for the gRPC ACL client. |
|
type ClientConfig struct { |
|
ServerAddress string |
|
ConnectTimeout time.Duration |
|
} |
|
|
|
// New creates a new gRPC ACL client. |
|
func New(ctx context.Context, cfg *ClientConfig) (*Client, error) { |
|
timeout := cfg.ConnectTimeout |
|
if timeout == 0 { |
|
timeout = 10 * time.Second |
|
} |
|
|
|
dialCtx, cancel := context.WithTimeout(ctx, timeout) |
|
defer cancel() |
|
|
|
conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress, |
|
grpc.WithTransportCredentials(insecure.NewCredentials()), |
|
grpc.WithDefaultCallOptions( |
|
grpc.MaxCallRecvMsgSize(16<<20), // 16MB |
|
grpc.MaxCallSendMsgSize(16<<20), // 16MB |
|
), |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
c := &Client{ |
|
conn: conn, |
|
client: orlyaclv1.NewACLServiceClient(conn), |
|
ready: make(chan struct{}), |
|
} |
|
|
|
// Check if server is ready and get mode |
|
go c.waitForReady(ctx) |
|
|
|
return c, nil |
|
} |
|
|
|
func (c *Client) waitForReady(ctx context.Context) { |
|
for { |
|
select { |
|
case <-ctx.Done(): |
|
return |
|
default: |
|
resp, err := c.client.Ready(ctx, &orlyaclv1.Empty{}) |
|
if err == nil && resp.Ready { |
|
// Get mode from server |
|
modeResp, err := c.client.GetMode(ctx, &orlyaclv1.Empty{}) |
|
if err == nil { |
|
c.mode = modeResp.Mode |
|
} |
|
close(c.ready) |
|
log.I.F("gRPC ACL client connected and ready, mode: %s", c.mode) |
|
return |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
} |
|
} |
|
|
|
// Close closes the gRPC connection. |
|
func (c *Client) Close() error { |
|
if c.conn != nil { |
|
return c.conn.Close() |
|
} |
|
return nil |
|
} |
|
|
|
// Ready returns a channel that closes when the client is ready. |
|
func (c *Client) Ready() <-chan struct{} { |
|
return c.ready |
|
} |
|
|
|
// === acl.I Interface Implementation === |
|
|
|
func (c *Client) Configure(cfg ...any) error { |
|
// Configuration is done on the server side |
|
// The client just passes through to the server |
|
return nil |
|
} |
|
|
|
func (c *Client) GetAccessLevel(pub []byte, address string) string { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.GetAccessLevel(ctx, &orlyaclv1.AccessLevelRequest{ |
|
Pubkey: pub, |
|
Address: address, |
|
}) |
|
if chk.E(err) { |
|
return "none" |
|
} |
|
return resp.Level |
|
} |
|
|
|
func (c *Client) GetACLInfo() (name, description, documentation string) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.GetACLInfo(ctx, &orlyaclv1.Empty{}) |
|
if chk.E(err) { |
|
return "", "", "" |
|
} |
|
return resp.Name, resp.Description, resp.Documentation |
|
} |
|
|
|
func (c *Client) Syncer() { |
|
// The syncer runs on the ACL server, not the client |
|
// This is a no-op for the gRPC client |
|
} |
|
|
|
func (c *Client) Type() string { |
|
return c.mode |
|
} |
|
|
|
// === acl.PolicyChecker Interface Implementation === |
|
|
|
func (c *Client) CheckPolicy(ev *event.E) (bool, error) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.CheckPolicy(ctx, &orlyaclv1.PolicyCheckRequest{ |
|
Event: orlydbv1.EventToProto(ev), |
|
}) |
|
if err != nil { |
|
return false, err |
|
} |
|
if resp.Error != "" { |
|
return resp.Allowed, &policyError{msg: resp.Error} |
|
} |
|
return resp.Allowed, nil |
|
} |
|
|
|
// policyError is a simple error type for policy check failures |
|
type policyError struct { |
|
msg string |
|
} |
|
|
|
func (e *policyError) Error() string { |
|
return e.msg |
|
} |
|
|
|
// === Follows ACL Methods === |
|
|
|
// GetThrottleDelay returns the progressive throttle delay for a pubkey. |
|
func (c *Client) GetThrottleDelay(pubkey []byte, ip string) time.Duration { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.GetThrottleDelay(ctx, &orlyaclv1.ThrottleDelayRequest{ |
|
Pubkey: pubkey, |
|
Ip: ip, |
|
}) |
|
if chk.E(err) { |
|
return 0 |
|
} |
|
return time.Duration(resp.DelayMs) * time.Millisecond |
|
} |
|
|
|
// AddFollow adds a pubkey to the followed list. |
|
func (c *Client) AddFollow(pubkey []byte) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.AddFollow(ctx, &orlyaclv1.AddFollowRequest{ |
|
Pubkey: pubkey, |
|
}) |
|
return err |
|
} |
|
|
|
// GetFollowedPubkeys returns all followed pubkeys. |
|
func (c *Client) GetFollowedPubkeys() [][]byte { |
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.GetFollowedPubkeys(ctx, &orlyaclv1.Empty{}) |
|
if chk.E(err) { |
|
return nil |
|
} |
|
return resp.Pubkeys |
|
} |
|
|
|
// GetAdminRelays returns the admin relay URLs. |
|
func (c *Client) GetAdminRelays() []string { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.GetAdminRelays(ctx, &orlyaclv1.Empty{}) |
|
if chk.E(err) { |
|
return nil |
|
} |
|
return resp.Urls |
|
} |
|
|
|
// === Managed ACL Methods === |
|
|
|
// BanPubkey adds a pubkey to the ban list. |
|
func (c *Client) BanPubkey(pubkey, reason string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.BanPubkey(ctx, &orlyaclv1.BanPubkeyRequest{ |
|
Pubkey: pubkey, |
|
Reason: reason, |
|
}) |
|
return err |
|
} |
|
|
|
// UnbanPubkey removes a pubkey from the ban list. |
|
func (c *Client) UnbanPubkey(pubkey string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.UnbanPubkey(ctx, &orlyaclv1.PubkeyRequest{ |
|
Pubkey: pubkey, |
|
}) |
|
return err |
|
} |
|
|
|
// AllowPubkey adds a pubkey to the allow list. |
|
func (c *Client) AllowPubkey(pubkey, reason string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.AllowPubkey(ctx, &orlyaclv1.AllowPubkeyRequest{ |
|
Pubkey: pubkey, |
|
Reason: reason, |
|
}) |
|
return err |
|
} |
|
|
|
// DisallowPubkey removes a pubkey from the allow list. |
|
func (c *Client) DisallowPubkey(pubkey string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.DisallowPubkey(ctx, &orlyaclv1.PubkeyRequest{ |
|
Pubkey: pubkey, |
|
}) |
|
return err |
|
} |
|
|
|
// BlockIP adds an IP to the block list. |
|
func (c *Client) BlockIP(ip, reason string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.BlockIP(ctx, &orlyaclv1.BlockIPRequest{ |
|
Ip: ip, |
|
Reason: reason, |
|
}) |
|
return err |
|
} |
|
|
|
// UnblockIP removes an IP from the block list. |
|
func (c *Client) UnblockIP(ip string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.UnblockIP(ctx, &orlyaclv1.IPRequest{ |
|
Ip: ip, |
|
}) |
|
return err |
|
} |
|
|
|
// UpdatePeerAdmins updates the peer relay identity pubkeys. |
|
func (c *Client) UpdatePeerAdmins(peerPubkeys [][]byte) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.UpdatePeerAdmins(ctx, &orlyaclv1.UpdatePeerAdminsRequest{ |
|
PeerPubkeys: peerPubkeys, |
|
}) |
|
return err |
|
} |
|
|
|
// === Curating ACL Methods === |
|
|
|
// TrustPubkey adds a pubkey to the trusted list. |
|
func (c *Client) TrustPubkey(pubkey, note string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.TrustPubkey(ctx, &orlyaclv1.TrustPubkeyRequest{ |
|
Pubkey: pubkey, |
|
Note: note, |
|
}) |
|
return err |
|
} |
|
|
|
// UntrustPubkey removes a pubkey from the trusted list. |
|
func (c *Client) UntrustPubkey(pubkey string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.UntrustPubkey(ctx, &orlyaclv1.PubkeyRequest{ |
|
Pubkey: pubkey, |
|
}) |
|
return err |
|
} |
|
|
|
// BlacklistPubkey adds a pubkey to the blacklist. |
|
func (c *Client) BlacklistPubkey(pubkey, reason string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.BlacklistPubkey(ctx, &orlyaclv1.BlacklistPubkeyRequest{ |
|
Pubkey: pubkey, |
|
Reason: reason, |
|
}) |
|
return err |
|
} |
|
|
|
// UnblacklistPubkey removes a pubkey from the blacklist. |
|
func (c *Client) UnblacklistPubkey(pubkey string) error { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
_, err := c.client.UnblacklistPubkey(ctx, &orlyaclv1.PubkeyRequest{ |
|
Pubkey: pubkey, |
|
}) |
|
return err |
|
} |
|
|
|
// RateLimitCheck checks if a pubkey/IP can publish. |
|
func (c *Client) RateLimitCheck(pubkey, ip string) (allowed bool, message string, err error) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.RateLimitCheck(ctx, &orlyaclv1.RateLimitCheckRequest{ |
|
Pubkey: pubkey, |
|
Ip: ip, |
|
}) |
|
if err != nil { |
|
return false, "", err |
|
} |
|
return resp.Allowed, resp.Message, nil |
|
} |
|
|
|
// IsCuratingConfigured checks if curating mode is configured. |
|
func (c *Client) IsCuratingConfigured() (bool, error) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
|
|
resp, err := c.client.IsCuratingConfigured(ctx, &orlyaclv1.Empty{}) |
|
if err != nil { |
|
return false, err |
|
} |
|
return resp.Value, nil |
|
}
|
|
|