| 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 verified") |
| } 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 |
| } |