// Package bridge implements the 3rd-party server bridge (#46): it lets an external // application server talk to MWSE over plain HTTPS (get/post) without speaking // WebSocket, and lets MWSE delegate connection approval to that application // server. // // Three pieces, each independently testable: // // - Inbox : a bounded queue of client->application messages the app drains // by polling an HTTP endpoint. // - HTTPApprover : asks the application "Connect?" for each new client and accepts // only on an explicit approval (fail-closed). // - HTTPTrigger : pushes a suit-notification reply (#44) to the application, // so the app is told the moment a reply arrives instead of polling. package bridge import ( "bytes" "context" "encoding/json" "net/http" "sync" "time" "git.saqut.com/saqut/mwse/internal/notify" ) // Message is one client->application message held in the inbox. type Message struct { From string `json:"from"` Pack any `json:"pack"` At time.Time `json:"at"` } // Inbox is a bounded FIFO of messages awaiting collection by the application // server. It is bounded so a never-polling application cannot make it grow without // limit (oldest messages are dropped first). type Inbox struct { mu sync.Mutex q []Message max int } // NewInbox returns an inbox holding up to max messages (<=0 uses 10000). func NewInbox(max int) *Inbox { if max <= 0 { max = 10000 } return &Inbox{max: max} } // Push appends a message from a client, dropping the oldest if the cap is hit. func (i *Inbox) Push(from string, pack any) { i.mu.Lock() defer i.mu.Unlock() if len(i.q) >= i.max { drop := len(i.q) - i.max + 1 i.q = i.q[drop:] } i.q = append(i.q, Message{From: from, Pack: pack, At: time.Now()}) } // Drain returns and clears all queued messages. func (i *Inbox) Drain() []Message { i.mu.Lock() defer i.mu.Unlock() if len(i.q) == 0 { return nil } out := i.q i.q = nil return out } // Len reports how many messages are queued. func (i *Inbox) Len() int { i.mu.Lock() defer i.mu.Unlock() return len(i.q) } // HTTPApprover delegates connection approval to an application server. For each // new client it POSTs {id, meta} to URL and accepts only if the response is 200 // with a JSON body {"approve": true}. Any error (unreachable app, non-200, // malformed body) denies the connection — fail-closed, matching "approves or it // is rejected". type HTTPApprover struct { URL string Client *http.Client } // NewHTTPApprover builds an approver with a sensible request timeout. func NewHTTPApprover(url string, timeout time.Duration) *HTTPApprover { if timeout <= 0 { timeout = 3 * time.Second } return &HTTPApprover{URL: url, Client: &http.Client{Timeout: timeout}} } // Approve implements ws.Approver. func (a *HTTPApprover) Approve(id string, meta map[string]any) bool { body, _ := json.Marshal(map[string]any{"id": id, "meta": meta}) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, a.URL, bytes.NewReader(body)) if err != nil { return false } req.Header.Set("Content-Type", "application/json") resp, err := a.Client.Do(req) if err != nil { return false } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return false } var out struct { Approve bool `json:"approve"` } if json.NewDecoder(resp.Body).Decode(&out) != nil { return false } return out.Approve } // HTTPTrigger pushes suit-notification replies (#44) to the application server. // It structurally satisfies services.NotifyTrigger. type HTTPTrigger struct { URL string Client *http.Client } // NewHTTPTrigger builds a trigger with a sensible request timeout. func NewHTTPTrigger(url string, timeout time.Duration) *HTTPTrigger { if timeout <= 0 { timeout = 3 * time.Second } return &HTTPTrigger{URL: url, Client: &http.Client{Timeout: timeout}} } // NotifyReplied posts the reply to the application server (best effort). func (t *HTTPTrigger) NotifyReplied(n notify.Notification) { body, _ := json.Marshal(map[string]any{ "trace": n.Trace, "from": n.From, "to": n.To, "reply": n.Reply, }) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, t.URL, bytes.NewReader(body)) if err != nil { return } req.Header.Set("Content-Type", "application/json") if resp, err := t.Client.Do(req); err == nil { resp.Body.Close() } }