211 lines
6.3 KiB
Go
211 lines
6.3 KiB
Go
|
|
/*
|
||
|
|
* Copyright (c) 2026 Kaiy Ragur
|
||
|
|
*
|
||
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
|
|
*/
|
||
|
|
|
||
|
|
package remote
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"context"
|
||
|
|
"crypto/ed25519"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"log"
|
||
|
|
"net"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/neilalexander/yggmail/internal/account"
|
||
|
|
"github.com/neilalexander/yggmail/internal/notify"
|
||
|
|
pb "github.com/neilalexander/yggmail/internal/remote/types"
|
||
|
|
"github.com/neilalexander/yggmail/internal/storage"
|
||
|
|
"github.com/neilalexander/yggmail/internal/utils"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Server struct {
|
||
|
|
log *log.Logger
|
||
|
|
notify Notify
|
||
|
|
queue Queues
|
||
|
|
accMgr *account.Manager
|
||
|
|
storage storage.Storage
|
||
|
|
nodePublickey ed25519.PublicKey
|
||
|
|
validator *ReqValidator
|
||
|
|
}
|
||
|
|
|
||
|
|
type Notify interface {
|
||
|
|
Subscribe(pbk string) chan notify.Event
|
||
|
|
Unsubscribe(pbk string, ch chan notify.Event)
|
||
|
|
NotifyNew(pk string, ev notify.Event) error
|
||
|
|
}
|
||
|
|
|
||
|
|
type Queues interface {
|
||
|
|
QueueForServer(serverHex string) error
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewRemoteServer(nodePublickey ed25519.PublicKey, s storage.Storage, m *account.Manager, l *log.Logger, notify Notify, queue Queues) *Server {
|
||
|
|
return &Server{
|
||
|
|
storage: s,
|
||
|
|
accMgr: m,
|
||
|
|
log: l,
|
||
|
|
notify: notify,
|
||
|
|
queue: queue,
|
||
|
|
nodePublickey: nodePublickey,
|
||
|
|
validator: NewValidator(time.Minute * 3),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (srv *Server) Serve(l net.Listener) error {
|
||
|
|
srv.log.Println("Unified Remote Storage RPC server started (Streaming Mode)")
|
||
|
|
for {
|
||
|
|
conn, err := l.Accept()
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// Serving in new gorutine
|
||
|
|
go srv.handleSession(conn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (srv *Server) handleSession(conn net.Conn) {
|
||
|
|
defer conn.Close()
|
||
|
|
br := bufio.NewReader(conn)
|
||
|
|
for {
|
||
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||
|
|
|
||
|
|
req, reqPayload, err := ReadRequest(br)
|
||
|
|
if err != nil {
|
||
|
|
if err != io.EOF {
|
||
|
|
srv.log.Printf("RPC: Session error: %v", err)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
func() {
|
||
|
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
||
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Minute))
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
resp, respPayload := srv.handleRequest(ctx, req, reqPayload)
|
||
|
|
|
||
|
|
err = SendFrame(conn, resp, respPayload)
|
||
|
|
|
||
|
|
if reqPayload != nil {
|
||
|
|
io.Copy(io.Discard, reqPayload.R)
|
||
|
|
reqPayload.Close()
|
||
|
|
}
|
||
|
|
if respPayload != nil {
|
||
|
|
respPayload.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
srv.log.Printf("RPC: Send error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func RespErr(err error) *pb.Response {
|
||
|
|
return &pb.Response{Success: false, Error: err.Error()}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (srv *Server) handleRequest(ctx context.Context, req *pb.Request, reqPayload *Payload) (resp *pb.Response, respPayload *Payload) {
|
||
|
|
// srv.log.Printf("RPC: Request received from %s", req.AccountPk)
|
||
|
|
sPk, err := utils.DecodeString(req.AccountPk)
|
||
|
|
if err != nil {
|
||
|
|
return RespErr(fmt.Errorf("Error decode key")), respPayload
|
||
|
|
}
|
||
|
|
|
||
|
|
// if ok, err := VerifyRequest(req, sPk); err != nil || !ok {
|
||
|
|
// return nil, fmt.Errorf("401 Unauthorized: Signature mismatch")
|
||
|
|
// }
|
||
|
|
// srv.log.Printf("RPC: Request Verifed from %s", req.AccountPk)
|
||
|
|
ok, err := srv.validator.Validate(req, sPk)
|
||
|
|
if err != nil {
|
||
|
|
return RespErr(fmt.Errorf("401 Unauthorized: Signature mismatch")), respPayload
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
return &pb.Response{Success: true, Error: "duplicate request ignored"}, respPayload
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if this AccountPubKey belongs to a local account we manage
|
||
|
|
acc, _ := srv.accMgr.GetAccount(req.AccountPk)
|
||
|
|
isLocalAccount := (acc != nil)
|
||
|
|
|
||
|
|
_, isPushCall := req.Call.(*pb.Request_MailPush)
|
||
|
|
if !isLocalAccount && !isPushCall {
|
||
|
|
return RespErr(fmt.Errorf("403 Forbidden: Remote management not allowed")), respPayload
|
||
|
|
}
|
||
|
|
|
||
|
|
var rcname string
|
||
|
|
switch call := req.Call.(type) {
|
||
|
|
// --- Mailbox management (Local Account only via Policy) ---
|
||
|
|
case *pb.Request_MboxList:
|
||
|
|
resp, err = srv.handleMailboxList(ctx, req.AccountPk, call.MboxList)
|
||
|
|
rcname = "MboxList"
|
||
|
|
case *pb.Request_MboxCreate:
|
||
|
|
resp, err = srv.handleMailboxCreate(ctx, req.AccountPk, call.MboxCreate)
|
||
|
|
rcname = "MboxCreate"
|
||
|
|
case *pb.Request_MboxDelete:
|
||
|
|
resp, err = srv.handleMailboxDelete(ctx, req.AccountPk, call.MboxDelete)
|
||
|
|
rcname = "MboxDelete"
|
||
|
|
case *pb.Request_MboxRename:
|
||
|
|
resp, err = srv.handleMailboxRename(ctx, req.AccountPk, call.MboxRename)
|
||
|
|
rcname = "MboxRename"
|
||
|
|
case *pb.Request_MboxSub:
|
||
|
|
resp, err = srv.handleMailboxSubscribe(ctx, req.AccountPk, call.MboxSub)
|
||
|
|
rcname = "MboxSub"
|
||
|
|
case *pb.Request_MboxStatus:
|
||
|
|
resp, err = srv.handleMailboxStatus(ctx, req.AccountPk, call.MboxStatus)
|
||
|
|
rcname = "MboxStatus"
|
||
|
|
|
||
|
|
// --- Mail management ---
|
||
|
|
case *pb.Request_MailPush:
|
||
|
|
// handleMailPush has its own logic for Case A (Local) and Case B (Remote Node)
|
||
|
|
resp, err = srv.handleMailPush(ctx, req.AccountPk, call.MailPush, reqPayload)
|
||
|
|
rcname = "MailPush"
|
||
|
|
|
||
|
|
case *pb.Request_MailFetch:
|
||
|
|
resp, respPayload, err = srv.handleMailFetch(ctx, req.AccountPk, call.MailFetch)
|
||
|
|
rcname = "MailFetch"
|
||
|
|
case *pb.Request_MailFlags:
|
||
|
|
resp, err = srv.handleMailFlags(ctx, req.AccountPk, call.MailFlags)
|
||
|
|
rcname = "MailFlags"
|
||
|
|
case *pb.Request_MailMove:
|
||
|
|
resp, err = srv.handleMailMove(ctx, req.AccountPk, call.MailMove)
|
||
|
|
rcname = "MailMove"
|
||
|
|
case *pb.Request_MailDel:
|
||
|
|
resp, err = srv.handleMailDelete(ctx, req.AccountPk, call.MailDel)
|
||
|
|
rcname = "MailDel"
|
||
|
|
case *pb.Request_MailExpunge:
|
||
|
|
resp, err = srv.handleMailExpunge(ctx, req.AccountPk, call.MailExpunge)
|
||
|
|
rcname = "MailExpunge"
|
||
|
|
case *pb.Request_MailIdForSeq:
|
||
|
|
resp, err = srv.handleMailIdForSeq(ctx, req.AccountPk, call.MailIdForSeq)
|
||
|
|
rcname = "MailIdForSeq"
|
||
|
|
case *pb.Request_MailSearch:
|
||
|
|
resp, err = srv.handleMailSearch(ctx, req.AccountPk, call.MailSearch)
|
||
|
|
rcname = "MailSearch"
|
||
|
|
case *pb.Request_MailWatch:
|
||
|
|
resp, err = srv.handleMailWatch(ctx, req.AccountPk, call.MailWatch)
|
||
|
|
|
||
|
|
// --- Queue management (Local Account only via Policy) ---
|
||
|
|
case *pb.Request_QueueInsert:
|
||
|
|
resp, err = srv.handleQueueInsert(ctx, req.AccountPk, call.QueueInsert)
|
||
|
|
rcname = "QueueInsert"
|
||
|
|
|
||
|
|
default:
|
||
|
|
rcname = "Not Implemented"
|
||
|
|
resp = &pb.Response{Success: false, Error: "501 Not Implemented"}
|
||
|
|
}
|
||
|
|
// srv.log.Printf("RPC: %s : Authorized: %t: Publickey: %s :", rcname, isLocalAccount, req.AccountPk)
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
srv.log.Printf("RPC Error: %s %v", rcname, err)
|
||
|
|
resp = &pb.Response{Success: false, Error: err.Error()}
|
||
|
|
}
|
||
|
|
return resp, respPayload
|
||
|
|
}
|