hswaw/smsgw: implement
The SMS gateway service allows consumers to subscribe to SMS messages
received by a Twilio phone number.
This is useful for receiving SMS auth messages.
Change-Id: Ib02a4306ad0d856dd10c7ca9241d9163809e7084
diff --git a/hswaw/smsgw/main.go b/hswaw/smsgw/main.go
new file mode 100644
index 0000000..a0a6a07
--- /dev/null
+++ b/hswaw/smsgw/main.go
@@ -0,0 +1,226 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "code.hackerspace.pl/hscloud/go/mirko"
+ "github.com/golang/glog"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ pb "code.hackerspace.pl/hscloud/hswaw/smsgw/proto"
+)
+
+var (
+ flagTwilioSID string
+ flagTwilioToken string
+ flagTwilioFriendlyPhone string
+
+ flagWebhookListen string
+ flagWebhookPublic string
+)
+
+func init() {
+ flag.Set("logtostderr", "true")
+}
+
+type server struct {
+ dispatcher *dispatcher
+}
+
+func ourPhoneNumber(ctx context.Context, t *twilio, friendly string) (*incomingPhoneNumber, error) {
+ ipn, err := t.getIncomingPhoneNumbers(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, pn := range ipn {
+ if pn.FriendlyName == friendly {
+ return &pn, nil
+ }
+ }
+
+ return nil, fmt.Errorf("requested phone number %q not in list", friendly)
+}
+
+func ensureWebhook(ctx context.Context, t *twilio) {
+ pn, err := ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
+ if err != nil {
+ glog.Exitf("could not get our phone number: %v", err)
+ }
+
+ url := fmt.Sprintf("%ssms", flagWebhookPublic)
+
+ // first setup.
+ if pn.SMSMethod != "POST" || pn.SMSURL != url {
+ glog.Infof("Updating webhook (is %s %q, want %s %q)", pn.SMSMethod, pn.SMSURL, "POST", url)
+ if err := t.updateIncomingPhoneNumberSMSWebhook(ctx, pn.SID, "POST", url); err != nil {
+ glog.Exitf("could not set webhook: %v")
+ }
+
+ // try again to check that it's actually set
+ for {
+ pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
+ if err != nil {
+ glog.Exitf("could not get our phone number: %v", err)
+ }
+ if pn.SMSMethod == "POST" || pn.SMSURL == url {
+ break
+ }
+ glog.Infof("Webhook not yet ready, currently %s %q", pn.SMSMethod, pn.SMSURL)
+ time.Sleep(5 * time.Second)
+ }
+ glog.Infof("Webhook verifier")
+ } else {
+ glog.Infof("Webhook up to date")
+ }
+
+ // now keep checking to make sure that nobody takes over our webhook
+ tick := time.NewTicker(30 * time.Second)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-tick.C:
+ pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
+ if err != nil {
+ glog.Exitf("could not get our phone number: %v", err)
+ }
+ if pn.SMSMethod != "POST" || pn.SMSURL != url {
+ glog.Exitf("Webhook got deconfigured, not %s %q", pn.SMSMethod, pn.SMSURL)
+ }
+ }
+ }
+}
+
+func (s *server) webhookHandler(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ glog.Errorf("webhook body parse error: %v", err)
+ return
+ }
+
+ accountSID := r.PostForm.Get("AccountSid")
+ if accountSID != flagTwilioSID {
+ glog.Errorf("webhook got wrong account sid, got %q, wanted %q", accountSID, flagTwilioSID)
+ return
+ }
+
+ body := r.PostForm.Get("Body")
+ if body == "" {
+ return
+ }
+
+ from := r.PostForm.Get("From")
+
+ glog.Infof("Got SMS from %q, body %q", from, body)
+
+ s.dispatcher.publish(&sms{
+ from: from,
+ body: body,
+ timestamp: time.Now(),
+ })
+
+ w.WriteHeader(200)
+}
+
+func main() {
+ flag.StringVar(&flagTwilioSID, "twilio_sid", "", "Twilio account SID")
+ flag.StringVar(&flagTwilioToken, "twilio_token", "", "Twilio auth token")
+ flag.StringVar(&flagTwilioFriendlyPhone, "twilio_friendly_phone", "", "Twilio friendly phone number")
+
+ flag.StringVar(&flagWebhookListen, "webhook_listen", "127.0.0.1:5000", "Listen address for webhook handler")
+ flag.StringVar(&flagWebhookPublic, "webhook_public", "", "Public address for webhook handler (wg. http://proxy.q3k.org/smsgw/)")
+ flag.Parse()
+
+ if flagTwilioSID == "" || flagTwilioToken == "" {
+ glog.Exitf("twilio_sid and twilio_token must be set")
+ }
+
+ if flagTwilioFriendlyPhone == "" {
+ glog.Exitf("twilio_friendly_phone must be set")
+ }
+
+ if flagWebhookPublic == "" {
+ glog.Exitf("webhook_public must be set")
+ }
+
+ if !strings.HasSuffix(flagWebhookPublic, "/") {
+ flagWebhookPublic += "/"
+ }
+
+ s := &server{
+ dispatcher: newDispatcher(),
+ }
+
+ m := mirko.New()
+ if err := m.Listen(); err != nil {
+ glog.Exitf("Listen(): %v", err)
+ }
+
+ webhookMux := http.NewServeMux()
+ webhookMux.HandleFunc("/sms", s.webhookHandler)
+ webhookSrv := http.Server{
+ Addr: flagWebhookListen,
+ Handler: webhookMux,
+ }
+ go func() {
+ if err := webhookSrv.ListenAndServe(); err != nil {
+ glog.Exitf("webhook ListenAndServe: %v", err)
+ }
+ }()
+
+ t := &twilio{
+ accountSID: flagTwilioSID,
+ accountToken: flagTwilioToken,
+ }
+ go ensureWebhook(m.Context(), t)
+ go s.dispatcher.run(m.Context())
+
+ pb.RegisterSMSGatewayServer(m.GRPC(), s)
+
+ if err := m.Serve(); err != nil {
+ glog.Exitf("Serve(): %v", err)
+ }
+
+ <-m.Done()
+}
+
+func (s *server) Messages(req *pb.MessagesRequest, stream pb.SMSGateway_MessagesServer) error {
+ re := regexp.MustCompile(".*")
+ if req.FilterBody != "" {
+ var err error
+ re, err = regexp.Compile(req.FilterBody)
+ if err != nil {
+ return status.Errorf(codes.InvalidArgument, "filter regexp error: %v", err)
+ }
+ }
+
+ data := make(chan *sms)
+ cancel := make(chan struct{})
+ defer func() {
+ close(cancel)
+ close(data)
+ }()
+
+ s.dispatcher.subscribe(&subscriber{
+ re: re,
+ data: data,
+ cancel: cancel,
+ })
+
+ for d := range data {
+ stream.Send(&pb.MessagesResponse{
+ Sender: d.from,
+ Body: d.body,
+ Timestamp: d.timestamp.UnixNano(),
+ })
+ }
+
+ return nil
+}