feat: implement security, discovery, history, file transfer, web UI (P0-P3)
This commit is contained in:
@@ -2,16 +2,16 @@
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// clip-sync [--config path]
|
||||
// clip-sync [flags] # run the daemon
|
||||
// clip-sync history # print local clipboard history
|
||||
// clip-sync --one-shot [text] # send the clipboard (or text) once, then exit
|
||||
//
|
||||
// clip-sync polls the local clipboard every 500 ms and broadcasts any new
|
||||
// text content to all configured peers via HTTP POST /clip. It also runs an
|
||||
// HTTP server on :9137 (configurable) to receive clips from peers and write
|
||||
// them to the local clipboard.
|
||||
// Flags:
|
||||
//
|
||||
// Configuration is read from ~/.config/clip-sync/peers.toml (TOML format).
|
||||
// Environment variables CLIP_SYNC_PORT and CLIP_SYNC_POLL_MS override
|
||||
// the corresponding config values.
|
||||
// --config path config file (default ~/.config/clip-sync/peers.toml)
|
||||
// --version print version and exit
|
||||
// --generate-key generate a shared key and exit
|
||||
// --generate-cert generate a self-signed TLS cert/key and exit
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -21,31 +21,94 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.dracodev.net/Projets/clip-sync/internal/clipboard"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/config"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/daemon"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/history"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/peer"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/security"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/transfer"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
// version is overridden at build time via -ldflags "-X main.version=...".
|
||||
var version = "0.2.0"
|
||||
|
||||
func main() {
|
||||
showVersion := flag.Bool("version", false, "print version and exit")
|
||||
flag.Parse()
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetPrefix("")
|
||||
|
||||
fs := flag.NewFlagSet("clip-sync", flag.ExitOnError)
|
||||
configPath := fs.String("config", "", "config file path")
|
||||
showVersion := fs.Bool("version", false, "print version and exit")
|
||||
generateKey := fs.Bool("generate-key", false, "generate a shared key and exit")
|
||||
generateCert := fs.Bool("generate-cert", false, "generate a self-signed TLS certificate and exit")
|
||||
oneShot := fs.Bool("one-shot", false, "send the clipboard once and exit")
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n clip-sync [flags]\n clip-sync history\n\nFlags:\n")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
|
||||
// Subcommand: history
|
||||
if fs.NArg() > 0 && fs.Arg(0) == "history" {
|
||||
runHistory()
|
||||
return
|
||||
}
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("clip-sync %s\n", version)
|
||||
return
|
||||
}
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetPrefix("")
|
||||
if *generateKey {
|
||||
key, err := security.GenerateKey()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: generate key: %v", err)
|
||||
}
|
||||
fmt.Println(key)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if *generateCert {
|
||||
runGenerateCert()
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: config: %v", err)
|
||||
}
|
||||
|
||||
if *oneShot {
|
||||
runOneShot(cfg, fs.Arg(0))
|
||||
return
|
||||
}
|
||||
|
||||
// Subcommand: send-file <path>
|
||||
if fs.NArg() > 0 && fs.Arg(0) == "send-file" {
|
||||
path := ""
|
||||
if fs.NArg() > 1 {
|
||||
path = fs.Arg(1)
|
||||
}
|
||||
runSendFile(cfg, path)
|
||||
return
|
||||
}
|
||||
|
||||
runDaemon(cfg)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*config.Config, error) {
|
||||
if path != "" {
|
||||
return config.LoadFrom(path)
|
||||
}
|
||||
return config.Load()
|
||||
}
|
||||
|
||||
func runDaemon(cfg *config.Config) {
|
||||
d, err := daemon.New(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: init: %v", err)
|
||||
@@ -54,7 +117,6 @@ func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle graceful shutdown on SIGINT / SIGTERM.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
@@ -70,3 +132,100 @@ func main() {
|
||||
|
||||
log.Println("clip-sync: stopped")
|
||||
}
|
||||
|
||||
func runOneShot(cfg *config.Config, textArg string) {
|
||||
origin, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: hostname: %v", err)
|
||||
}
|
||||
|
||||
b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{
|
||||
SharedKey: cfg.Security.SharedKey,
|
||||
TLS: cfg.Security.TLS,
|
||||
CertFile: cfg.Security.CertFile,
|
||||
InsecureSkipVerify: cfg.Security.InsecureSkipVerify,
|
||||
})
|
||||
|
||||
text := textArg
|
||||
if text == "" {
|
||||
cb, err := clipboard.New()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: clipboard: %v", err)
|
||||
}
|
||||
text, err = cb.Read()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: clipboard read: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
log.Fatal("clip-sync: nothing to send (clipboard is empty)")
|
||||
}
|
||||
|
||||
b.BroadcastSync(text)
|
||||
fmt.Printf("clip-sync: sent %d bytes to %d peer(s)\n", len(text), b.PeerCount())
|
||||
}
|
||||
|
||||
func runSendFile(cfg *config.Config, path string) {
|
||||
if path == "" {
|
||||
log.Fatal("clip-sync: usage: clip-sync send-file <path>")
|
||||
}
|
||||
|
||||
origin, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: hostname: %v", err)
|
||||
}
|
||||
|
||||
f, err := transfer.ReadFile(path, origin)
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: read file: %v", err)
|
||||
}
|
||||
|
||||
b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{
|
||||
SharedKey: cfg.Security.SharedKey,
|
||||
TLS: cfg.Security.TLS,
|
||||
CertFile: cfg.Security.CertFile,
|
||||
InsecureSkipVerify: cfg.Security.InsecureSkipVerify,
|
||||
})
|
||||
|
||||
b.SendFileSync(*f)
|
||||
fmt.Printf("clip-sync: sent %q (%s) to %d peer(s)\n", f.Name, f.Mime, b.PeerCount())
|
||||
}
|
||||
|
||||
func runHistory() {
|
||||
path, err := history.DefaultPath()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: history: %v", err)
|
||||
}
|
||||
entries := history.New(path, 0).List()
|
||||
if len(entries) == 0 {
|
||||
fmt.Println("clip-sync: no history yet")
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
ts := time.Unix(0, e.Ts).Format("2006-01-02 15:04:05")
|
||||
text := strings.ReplaceAll(e.Text, "\n", "\\n")
|
||||
if len(text) > 80 {
|
||||
text = text[:80] + "…"
|
||||
}
|
||||
fmt.Printf("%s [%s] %s\n", ts, e.Origin, text)
|
||||
}
|
||||
}
|
||||
|
||||
func runGenerateCert() {
|
||||
certFile, err := config.DefaultCertFile()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: cert path: %v", err)
|
||||
}
|
||||
keyFile, err := config.DefaultKeyFile()
|
||||
if err != nil {
|
||||
log.Fatalf("clip-sync: key path: %v", err)
|
||||
}
|
||||
host, _ := os.Hostname()
|
||||
|
||||
if err := security.GenerateSelfSignedCert(certFile, keyFile, []string{host, "localhost"}); err != nil {
|
||||
log.Fatalf("clip-sync: generate cert: %v", err)
|
||||
}
|
||||
fmt.Printf("clip-sync: certificate written to %s and %s\n", certFile, keyFile)
|
||||
fmt.Println("Set [security] tls = true and cert_file/key_file in peers.toml, and share these files with your peers.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user