package xmrpayclnt import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "net/url" "sync" "gitlab.com/moneropay/moneropay/v2/pkg/model" ) const callbackIdEntropy = 16 // 128 bits of entropy var callbackIdEnc = base64.URLEncoding type callbackId string func newCallbackId() callbackId { raw := make([]byte, callbackIdEntropy) rand.Read(raw) return callbackId(callbackIdEnc.EncodeToString(raw)) } // Callback receives information about a transaction. // // Whenever MoneroPay sends a POST request to the callback URL specified // in the POST /receive endpoint, the information is sent on C. C and // Complete are closed once the transfer's unlocked amount is >= the // requested amount, or if Close is called. type Callback struct { id callbackId url *url.URL C <-chan model.CallbackResponse Complete <-chan struct{} in chan<- model.CallbackResponse // from http handler kill chan<- struct{} } func newCallback(id callbackId, url *url.URL, done chan callbackId) Callback { c := make(chan model.CallbackResponse) complete := make(chan struct{}) in := make(chan model.CallbackResponse) kill := make(chan struct{}) go func() { defer func() { done <- id // signal to handler <-done // wait for handler to unlink us before closing input channel close(in) close(c) close(complete) }() var info model.CallbackResponse var fresh bool for { if fresh { if info.Complete { return } select { case info = <-in: case c <- info: fresh = false case <-kill: return } } else { select { case info = <-in: fresh = true case <-kill: return } } } }() return Callback{id, url, c, complete, in, kill} } // Close causes the handler to stop listening for MoneroPay's callbacks // relating to this transaction and closes the Callback's channels. func (cb Callback) Close() { close(cb.kill) } func (cb Callback) URL() *url.URL { return cb.url } // CallbackHandler is a HTTP handler that listens for callback POST // requests from the MoneroPay server. type CallbackHandler struct { base *url.URL done chan callbackId mu sync.Mutex // guards below cbs map[callbackId]Callback } func NewCallbackHandler(base *url.URL) *CallbackHandler { done := make(chan callbackId) cbs := make(map[callbackId]Callback) h := &CallbackHandler{base, done, sync.Mutex{}, cbs} go func() { for id := range done { h.mu.Lock() delete(cbs, id) h.mu.Unlock() done <- id // callback can continue cleaning up } }() return h } // ServeHTTP implements http.Handler. func (h CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Decode callback body var info model.CallbackResponse dec := json.NewDecoder(r.Body) if err := dec.Decode(&info); err != nil { lg.Printf("%s: %v\n", r.URL, err) return } // Send to callback goroutine h.mu.Lock() defer h.mu.Unlock() id := callbackId(r.URL.Query().Get("id")) if cb, ok := h.cbs[id]; ok { cb.in <- info } else { lg.Printf("%s: no such callback %q\n", r.URL, id) } } // listen starts listening for callbacks for a new transaction. func (h CallbackHandler) listen() Callback { id := newCallbackId() url := callbackUrl(h.base, id) h.mu.Lock() defer h.mu.Unlock() cb := newCallback(id, url, h.done) h.cbs[id] = cb return cb } func callbackUrl(base *url.URL, id callbackId) *url.URL { url := copyUrl(base) q := url.Query() q.Set("id", string(id)) url.RawQuery = q.Encode() return url } func copyUrl(u *url.URL) *url.URL { cp := *u if u.User != nil { ucp := *u.User cp.User = &ucp } return &cp }