feat: implement security, discovery, history, file transfer, web UI (P0-P3)
This commit is contained in:
+90
-17
@@ -1,5 +1,5 @@
|
||||
// Package daemon orchestrates the main clip-sync loop: poll the local clipboard,
|
||||
// broadcast changes to peers, and receive incoming clips via the HTTP server.
|
||||
// broadcast changes to peers, and receive incoming clips via the HTTP(S) server.
|
||||
package daemon
|
||||
|
||||
import (
|
||||
@@ -12,17 +12,23 @@ import (
|
||||
"git.dracodev.net/Projets/clip-sync/internal/clipboard"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/config"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/dedup"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/discovery"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/history"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/notify"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/peer"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/server"
|
||||
)
|
||||
|
||||
// Daemon is the main clip-sync process.
|
||||
type Daemon struct {
|
||||
cfg *config.Config
|
||||
clipboard clipboard.Clipboard
|
||||
filter *dedup.Filter
|
||||
server *server.Server
|
||||
cfg *config.Config
|
||||
clipboard clipboard.Clipboard
|
||||
filter *dedup.Filter
|
||||
server *server.Server
|
||||
broadcaster *peer.Broadcaster
|
||||
history *history.Store
|
||||
origin string
|
||||
discovery discovery.Discovery
|
||||
}
|
||||
|
||||
// New creates a Daemon from the given configuration.
|
||||
@@ -32,7 +38,18 @@ func New(cfg *config.Config) (*Daemon, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newWithClipboard(cfg, cb)
|
||||
origin, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// History is best-effort: if we cannot resolve a path, run without it.
|
||||
hist := (*history.Store)(nil)
|
||||
if p, err := history.DefaultPath(); err == nil {
|
||||
hist = history.New(p, cfg.Daemon.HistorySize)
|
||||
}
|
||||
|
||||
return newDaemon(cfg, cb, origin, hist)
|
||||
}
|
||||
|
||||
// newWithClipboard creates a Daemon with a pre-existing clipboard (used by tests).
|
||||
@@ -42,6 +59,10 @@ func newWithClipboard(cfg *config.Config, cb clipboard.Clipboard) (*Daemon, erro
|
||||
|
||||
// newWithClipboardOrigin creates a Daemon with a pre-existing clipboard and explicit origin.
|
||||
func newWithClipboardOrigin(cfg *config.Config, cb clipboard.Clipboard, origin string) (*Daemon, error) {
|
||||
return newDaemon(cfg, cb, origin, nil)
|
||||
}
|
||||
|
||||
func newDaemon(cfg *config.Config, cb clipboard.Clipboard, origin string, hist *history.Store) (*Daemon, error) {
|
||||
if origin == "" {
|
||||
var err error
|
||||
origin, err = os.Hostname()
|
||||
@@ -51,34 +72,82 @@ func newWithClipboardOrigin(cfg *config.Config, cb clipboard.Clipboard, origin s
|
||||
}
|
||||
|
||||
filter := dedup.NewFilter(origin)
|
||||
broadcaster := peer.NewBroadcaster(cfg.Peers, origin)
|
||||
srv := server.New(cfg.ListenAddr(), cb, filter)
|
||||
broadcaster := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{
|
||||
SharedKey: cfg.Security.SharedKey,
|
||||
TLS: cfg.Security.TLS,
|
||||
CertFile: cfg.Security.CertFile,
|
||||
InsecureSkipVerify: cfg.Security.InsecureSkipVerify,
|
||||
})
|
||||
|
||||
return &Daemon{
|
||||
var notifier notify.Notifier
|
||||
if cfg.Daemon.Notify {
|
||||
notifier = notify.New()
|
||||
}
|
||||
|
||||
srv := server.New(server.Options{
|
||||
Addr: cfg.ListenAddr(),
|
||||
SharedKey: cfg.Security.SharedKey,
|
||||
AllowedOrigins: cfg.Security.AllowedOrigins,
|
||||
MaxBodyBytes: cfg.MaxBodyBytes(),
|
||||
History: hist,
|
||||
Notifier: notifier,
|
||||
ReceiveFiles: cfg.Daemon.ReceiveFiles,
|
||||
ReceiveDir: cfg.ReceiveDir(),
|
||||
}, cb, filter)
|
||||
|
||||
d := &Daemon{
|
||||
cfg: cfg,
|
||||
clipboard: cb,
|
||||
filter: filter,
|
||||
server: srv,
|
||||
broadcaster: broadcaster,
|
||||
}, nil
|
||||
history: hist,
|
||||
origin: origin,
|
||||
}
|
||||
|
||||
if cfg.Daemon.Discovery {
|
||||
d.discovery = discovery.New(origin, cfg.Daemon.Port, broadcaster)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Run starts the daemon loop. It blocks until ctx is cancelled.
|
||||
func (d *Daemon) Run(ctx context.Context) error {
|
||||
// Start the HTTP server in a background goroutine.
|
||||
// Start the HTTP(S) server in a background goroutine.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Printf("clip-sync: listening on %s", d.cfg.ListenAddr())
|
||||
if err := d.server.ListenAndServe(); err != nil {
|
||||
// ListenAndServe returns http.ErrServerClosed on graceful shutdown.
|
||||
if ctx.Err() == nil {
|
||||
log.Printf("clip-sync: server error: %v", err)
|
||||
var err error
|
||||
if d.cfg.Security.TLS {
|
||||
certFile, keyFile := d.cfg.Security.CertFile, d.cfg.Security.KeyFile
|
||||
if certFile == "" {
|
||||
certFile, _ = config.DefaultCertFile()
|
||||
}
|
||||
if keyFile == "" {
|
||||
keyFile, _ = config.DefaultKeyFile()
|
||||
}
|
||||
log.Printf("clip-sync: listening on %s (TLS)", d.cfg.ListenAddr())
|
||||
err = d.server.ListenAndServeTLS(certFile, keyFile)
|
||||
} else {
|
||||
log.Printf("clip-sync: listening on %s", d.cfg.ListenAddr())
|
||||
err = d.server.ListenAndServe()
|
||||
}
|
||||
if err != nil && ctx.Err() == nil {
|
||||
log.Printf("clip-sync: server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start mDNS discovery if enabled.
|
||||
if d.discovery != nil {
|
||||
if err := d.discovery.Start(); err != nil {
|
||||
log.Printf("clip-sync: discovery: %v", err)
|
||||
} else {
|
||||
defer d.discovery.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Main clipboard polling loop.
|
||||
ticker := time.NewTicker(d.cfg.PollInterval())
|
||||
defer ticker.Stop()
|
||||
@@ -111,8 +180,12 @@ func (d *Daemon) Run(ctx context.Context) error {
|
||||
// Record our write so the filter can block echoes.
|
||||
d.filter.MarkWritten(text)
|
||||
|
||||
if d.history != nil {
|
||||
d.history.Append(history.Entry{Text: text, Origin: d.origin, Ts: time.Now().UnixNano()})
|
||||
}
|
||||
|
||||
// Broadcast to all peers.
|
||||
if len(d.cfg.Peers) > 0 {
|
||||
if len(d.cfg.Peers) > 0 || d.discovery != nil {
|
||||
d.broadcaster.Broadcast(text)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user