forked from hswaw/hscloud
Sergiusz Bazanski
6f773e0004
This productionizes smsgw. We also add some jsonnet machinery to provide a unified service for Go micro/mirkoservices. This machinery provides all the nice stuff: - a deployment - a service for all your types of pots - TLS certificates for HSPKI We also update and test hspki for a new name scheme. Change-Id: I292d00f858144903cbc8fe0c1c26eb1180d636bc
226 lines
5.1 KiB
Go
226 lines
5.1 KiB
Go
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
|
|
}
|