// tether-client v0.4: SSE listener with optional WebRTC mesh peer. // // Default flow (SSE only): subscribe to /api/stream, write incoming // clipboard messages to the OS clipboard. Works on Win/Linux/macOS. // // With -rtc: become a WebRTC peer that maintains one RTCPeerConnection // per active browser (mesh, not star). Symmetric "presence" chirps on // the signaling bus let participants discover each other and // auto-upgrade SSE → direct DataChannel. After pairing, clipboard text // flows direct peer-to-peer with DTLS encryption. package main import ( "bufio" "bytes" "crypto/rand" "encoding/hex" "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "runtime" "strings" "sync" "time" "github.com/atotto/clipboard" "github.com/pion/webrtc/v4" ) // peerID — each client instance gets a stable random id for the bus. var peerID = "tether-client-" + randomID(6) func randomID(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) } func defaultLabel(role string) string { return fmt.Sprintf("%s-%s-%s", runtime.GOOS, "sse", role) } // Message envelope (must match server). type Message struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` Signal json.RawMessage `json:"signal,omitempty"` From string `json:"from,omitempty"` To string `json:"to,omitempty"` Role string `json:"role,omitempty"` Source string `json:"source,omitempty"` Room string `json:"room,omitempty"` TS int64 `json:"ts"` } // Signal payload carried in Message.Signal. type SignalPayload struct { Kind string `json:"kind"` // "offer" | "answer" | "ice" SDP *webrtc.SessionDescription `json:"sdp,omitempty"` Candidate *webrtc.ICECandidateInit `json:"candidate,omitempty"` } var ( noClipboard bool myLabel string useRTC bool rtcServer string myRoom string ) func main() { server := flag.String("server", "https://tether.pecord.io", "tether-server base URL") label := flag.String("label", "", "X-Tether-Client label (default: -sse-)") sendText := flag.String("send", "", "send this text and exit (otherwise listen)") roomFlag := flag.String("room", "", "room id to join (or full URL like https://tether.pecord.io/r/)") flag.BoolVar(&noClipboard, "no-clipboard", false, "don't write incoming messages to the OS clipboard") flag.BoolVar(&useRTC, "rtc", false, "act as a WebRTC mesh peer") flag.Parse() // Accept either bare id or full URL myRoom = *roomFlag if strings.Contains(myRoom, "/r/") { parts := strings.SplitN(myRoom, "/r/", 2) if len(parts) == 2 { myRoom = strings.SplitN(parts[1], "/", 2)[0] } } if myRoom == "" { fmt.Fprintln(os.Stderr, "tether-client: -room is required (e.g. -room abc12345 or paste the /r/ URL)") os.Exit(2) } if *label == "" { if *sendText != "" { *label = defaultLabel("sender") } else if useRTC { *label = fmt.Sprintf("%s-rtc-listener", runtime.GOOS) } else { *label = defaultLabel("listener") } } myLabel = *label rtcServer = *server if *sendText != "" { send(*server, *label, *sendText, "clipboard", nil) return } if !noClipboard && clipboard.Unsupported { fmt.Fprintln(os.Stderr, "tether-client: OS clipboard unsupported on this platform; falling back to stdout-only") noClipboard = true } if useRTC { go presenceChirpLoop(*server) } for { if err := listen(*server, *label); err != nil { log.Printf("stream error: %v — reconnecting in 3s", err) time.Sleep(3 * time.Second) } } } func send(server, label, text, msgType string, signal json.RawMessage) { m := Message{Type: msgType, Text: text, Source: label, From: peerID, Signal: signal} postMessage(server, label, m) } func sendMessage(server, label string, m Message) { if m.From == "" { m.From = peerID } if m.Source == "" { m.Source = label } postMessage(server, label, m) } func postMessage(server, label string, m Message) { if m.Room == "" { m.Room = myRoom } body, _ := json.Marshal(m) req, _ := http.NewRequest("POST", server+"/api/send?room="+myRoom, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Tether-Source", label) r, err := http.DefaultClient.Do(req) if err != nil { log.Printf("send: %v", err) return } defer r.Body.Close() if r.StatusCode >= 300 { log.Printf("send: HTTP %d", r.StatusCode) return } if m.Type == "clipboard" && m.Text != "" { fmt.Println("sent.") } } // listen subscribes to the SSE stream and dispatches messages to handlers. func listen(server, label string) error { req, _ := http.NewRequest("GET", server+"/api/stream?room="+myRoom, nil) req.Header.Set("X-Tether-Client", label) r, err := http.DefaultClient.Do(req) if err != nil { return err } defer r.Body.Close() if r.StatusCode != 200 { return fmt.Errorf("HTTP %d", r.StatusCode) } fmt.Fprintf(os.Stderr, "tether-client: connected to %s as %q (rtc=%v, peerID=%s)\n", server, label, useRTC, peerID) sc := bufio.NewScanner(r.Body) sc.Buffer(make([]byte, 1<<20), 1<<20) var ev, data string for sc.Scan() { line := sc.Text() switch { case strings.HasPrefix(line, "event: "): ev = strings.TrimPrefix(line, "event: ") case strings.HasPrefix(line, "data: "): data = strings.TrimPrefix(line, "data: ") case line == "": if data != "" { var m Message if err := json.Unmarshal([]byte(data), &m); err == nil { handleMessage(ev, m) } } ev, data = "", "" } } if err := sc.Err(); err != nil && err != io.EOF { return err } return nil } func handleMessage(ev string, m Message) { // Ignore our own messages (echo). if m.From == peerID { return } switch m.Type { case "", "clipboard": ts := time.UnixMilli(m.TS).Format("15:04:05") fmt.Printf("\n────── %s from %s ──────\n%s\n", ts, m.Source, m.Text) if !noClipboard { if err := clipboard.WriteAll(m.Text); err != nil { fmt.Fprintf(os.Stderr, " ! clipboard write error: %v\n", err) } else { fmt.Fprintln(os.Stderr, " → clipboard updated") } } case "presence": if useRTC { onPresence(m) } case "signal": if useRTC { onSignal(m) } } } // ── WebRTC mesh ───────────────────────────────────────────────────────────── type remotePeer struct { pc *webrtc.PeerConnection dc *webrtc.DataChannel lastSeen time.Time state string // last connection state } var ( peers = make(map[string]*remotePeer) // keyed by remote peerID peersMu sync.Mutex ) func getPeer(id string) (*remotePeer, bool) { peersMu.Lock() defer peersMu.Unlock() p, ok := peers[id] return p, ok } func setPeer(id string, p *remotePeer) { peersMu.Lock() defer peersMu.Unlock() peers[id] = p } func removePeer(id string) { peersMu.Lock() defer peersMu.Unlock() if p, ok := peers[id]; ok { if p.pc != nil { _ = p.pc.Close() } delete(peers, id) } } // presenceChirpLoop broadcasts our own presence every 10s and sweeps stale peers. func presenceChirpLoop(server string) { announce := func() { sendMessage(server, myLabel, Message{ Type: "presence", Role: "peer", From: peerID, }) } announce() t := time.NewTicker(10 * time.Second) sweep := time.NewTicker(15 * time.Second) defer t.Stop() defer sweep.Stop() for { select { case <-t.C: announce() case <-sweep.C: peersMu.Lock() for id, p := range peers { if time.Since(p.lastSeen) > 45*time.Second { fmt.Fprintf(os.Stderr, "rtc: peer %s timed out — tearing down\n", id[:8]) if p.pc != nil { _ = p.pc.Close() } delete(peers, id) } } peersMu.Unlock() } } } // onPresence: a remote participant announced themselves. If we don't yet // have a peer connection to them, create one + send a targeted offer. func onPresence(m Message) { if m.From == "" || m.From == peerID { return } p, exists := getPeer(m.From) if exists { // just refresh lastSeen peersMu.Lock() p.lastSeen = time.Now() peersMu.Unlock() return } // New peer — initiate fmt.Fprintf(os.Stderr, "rtc: discovered new peer %s (role=%s) — sending offer\n", m.From[:min(len(m.From), 8)], m.Role) initiateOffer(m.From) } func initiateOffer(remoteID string) { api := webrtc.NewAPI() pc, err := api.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}, }) if err != nil { log.Printf("rtc: new pc for %s: %v", remoteID[:8], err) return } dc, err := pc.CreateDataChannel("tether", nil) if err != nil { log.Printf("rtc: dc for %s: %v", remoteID[:8], err) _ = pc.Close() return } rp := &remotePeer{pc: pc, dc: dc, lastSeen: time.Now()} setPeer(remoteID, rp) dc.OnOpen(func() { fmt.Fprintf(os.Stderr, "rtc: DataChannel OPEN with %s — P2P live\n", remoteID[:8]) }) dc.OnMessage(func(msg webrtc.DataChannelMessage) { text := string(msg.Data) fmt.Printf("\n[rtc:%s] %s\n", remoteID[:8], text) if !noClipboard { if err := clipboard.WriteAll(text); err == nil { fmt.Fprintln(os.Stderr, " → clipboard updated (via rtc)") } } }) pc.OnICECandidate(func(c *webrtc.ICECandidate) { if c == nil { return } init := c.ToJSON() payload, _ := json.Marshal(SignalPayload{Kind: "ice", Candidate: &init}) sendMessage(rtcServer, myLabel, Message{ Type: "signal", From: peerID, To: remoteID, Signal: payload, }) }) pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { peersMu.Lock() if p, ok := peers[remoteID]; ok { p.state = s.String() } peersMu.Unlock() fmt.Fprintf(os.Stderr, "rtc: %s state=%s\n", remoteID[:8], s) if s == webrtc.PeerConnectionStateFailed || s == webrtc.PeerConnectionStateClosed { removePeer(remoteID) } }) offer, err := pc.CreateOffer(nil) if err != nil { log.Printf("rtc: create offer for %s: %v", remoteID[:8], err) return } if err := pc.SetLocalDescription(offer); err != nil { log.Printf("rtc: set local desc for %s: %v", remoteID[:8], err) return } payload, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer}) sendMessage(rtcServer, myLabel, Message{ Type: "signal", From: peerID, To: remoteID, Signal: payload, }) } // onSignal: routed to the relevant peer connection by `To` field. func onSignal(m Message) { // Only handle signals targeted at us (or untargeted = legacy). if m.To != "" && m.To != peerID { return } var sp SignalPayload if err := json.Unmarshal(m.Signal, &sp); err != nil { return } p, ok := getPeer(m.From) if !ok { // Could be an answer/ice from someone we don't know about yet — // shouldn't happen in normal flow because they wouldn't answer // without first seeing our offer. Drop. return } switch sp.Kind { case "answer": if sp.SDP != nil { if err := p.pc.SetRemoteDescription(*sp.SDP); err != nil { if !strings.Contains(err.Error(), "stable->SetRemote(answer)->stable") { log.Printf("rtc: set remote desc for %s: %v", m.From[:8], err) } } else { fmt.Fprintf(os.Stderr, "rtc: answer applied for %s\n", m.From[:8]) } } case "ice": if sp.Candidate != nil { if err := p.pc.AddICECandidate(*sp.Candidate); err != nil { if !strings.Contains(err.Error(), "remote description is not set") { log.Printf("rtc: add ice for %s: %v", m.From[:8], err) } } } } } func min(a, b int) int { if a < b { return a } return b }