blob: 3095c0025f36e11c3486cf0cb9066300963064b2 [file] [log] [blame]
Sergiusz Bazanski325e9472019-09-27 02:49:47 +02001package main
2
3import (
4 "context"
5 "flag"
6 "fmt"
7 "net/http"
8 "regexp"
9 "strings"
10 "time"
11
12 "code.hackerspace.pl/hscloud/go/mirko"
13 "github.com/golang/glog"
14 "google.golang.org/grpc/codes"
15 "google.golang.org/grpc/status"
16
17 pb "code.hackerspace.pl/hscloud/hswaw/smsgw/proto"
18)
19
20var (
21 flagTwilioSID string
22 flagTwilioToken string
23 flagTwilioFriendlyPhone string
24
25 flagWebhookListen string
26 flagWebhookPublic string
27)
28
29func init() {
30 flag.Set("logtostderr", "true")
31}
32
33type server struct {
34 dispatcher *dispatcher
35}
36
37func ourPhoneNumber(ctx context.Context, t *twilio, friendly string) (*incomingPhoneNumber, error) {
38 ipn, err := t.getIncomingPhoneNumbers(ctx)
39 if err != nil {
40 return nil, err
41 }
42
43 for _, pn := range ipn {
44 if pn.FriendlyName == friendly {
45 return &pn, nil
46 }
47 }
48
49 return nil, fmt.Errorf("requested phone number %q not in list", friendly)
50}
51
52func ensureWebhook(ctx context.Context, t *twilio) {
53 pn, err := ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
54 if err != nil {
55 glog.Exitf("could not get our phone number: %v", err)
56 }
57
58 url := fmt.Sprintf("%ssms", flagWebhookPublic)
59
60 // first setup.
61 if pn.SMSMethod != "POST" || pn.SMSURL != url {
62 glog.Infof("Updating webhook (is %s %q, want %s %q)", pn.SMSMethod, pn.SMSURL, "POST", url)
63 if err := t.updateIncomingPhoneNumberSMSWebhook(ctx, pn.SID, "POST", url); err != nil {
64 glog.Exitf("could not set webhook: %v")
65 }
66
67 // try again to check that it's actually set
68 for {
69 pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
70 if err != nil {
71 glog.Exitf("could not get our phone number: %v", err)
72 }
73 if pn.SMSMethod == "POST" || pn.SMSURL == url {
74 break
75 }
76 glog.Infof("Webhook not yet ready, currently %s %q", pn.SMSMethod, pn.SMSURL)
77 time.Sleep(5 * time.Second)
78 }
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020079 glog.Infof("Webhook verified")
Sergiusz Bazanski325e9472019-09-27 02:49:47 +020080 } else {
81 glog.Infof("Webhook up to date")
82 }
83
84 // now keep checking to make sure that nobody takes over our webhook
85 tick := time.NewTicker(30 * time.Second)
86 for {
87 select {
88 case <-ctx.Done():
89 return
90 case <-tick.C:
91 pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
92 if err != nil {
93 glog.Exitf("could not get our phone number: %v", err)
94 }
95 if pn.SMSMethod != "POST" || pn.SMSURL != url {
96 glog.Exitf("Webhook got deconfigured, not %s %q", pn.SMSMethod, pn.SMSURL)
97 }
98 }
99 }
100}
101
102func (s *server) webhookHandler(w http.ResponseWriter, r *http.Request) {
103 if err := r.ParseForm(); err != nil {
104 glog.Errorf("webhook body parse error: %v", err)
105 return
106 }
107
108 accountSID := r.PostForm.Get("AccountSid")
109 if accountSID != flagTwilioSID {
110 glog.Errorf("webhook got wrong account sid, got %q, wanted %q", accountSID, flagTwilioSID)
111 return
112 }
113
114 body := r.PostForm.Get("Body")
115 if body == "" {
116 return
117 }
118
119 from := r.PostForm.Get("From")
120
121 glog.Infof("Got SMS from %q, body %q", from, body)
122
123 s.dispatcher.publish(&sms{
124 from: from,
125 body: body,
126 timestamp: time.Now(),
127 })
128
129 w.WriteHeader(200)
130}
131
132func main() {
133 flag.StringVar(&flagTwilioSID, "twilio_sid", "", "Twilio account SID")
134 flag.StringVar(&flagTwilioToken, "twilio_token", "", "Twilio auth token")
135 flag.StringVar(&flagTwilioFriendlyPhone, "twilio_friendly_phone", "", "Twilio friendly phone number")
136
137 flag.StringVar(&flagWebhookListen, "webhook_listen", "127.0.0.1:5000", "Listen address for webhook handler")
138 flag.StringVar(&flagWebhookPublic, "webhook_public", "", "Public address for webhook handler (wg. http://proxy.q3k.org/smsgw/)")
139 flag.Parse()
140
141 if flagTwilioSID == "" || flagTwilioToken == "" {
142 glog.Exitf("twilio_sid and twilio_token must be set")
143 }
144
145 if flagTwilioFriendlyPhone == "" {
146 glog.Exitf("twilio_friendly_phone must be set")
147 }
148
149 if flagWebhookPublic == "" {
150 glog.Exitf("webhook_public must be set")
151 }
152
153 if !strings.HasSuffix(flagWebhookPublic, "/") {
154 flagWebhookPublic += "/"
155 }
156
157 s := &server{
158 dispatcher: newDispatcher(),
159 }
160
161 m := mirko.New()
162 if err := m.Listen(); err != nil {
163 glog.Exitf("Listen(): %v", err)
164 }
165
166 webhookMux := http.NewServeMux()
167 webhookMux.HandleFunc("/sms", s.webhookHandler)
168 webhookSrv := http.Server{
169 Addr: flagWebhookListen,
170 Handler: webhookMux,
171 }
172 go func() {
173 if err := webhookSrv.ListenAndServe(); err != nil {
174 glog.Exitf("webhook ListenAndServe: %v", err)
175 }
176 }()
177
178 t := &twilio{
179 accountSID: flagTwilioSID,
180 accountToken: flagTwilioToken,
181 }
182 go ensureWebhook(m.Context(), t)
183 go s.dispatcher.run(m.Context())
184
185 pb.RegisterSMSGatewayServer(m.GRPC(), s)
186
187 if err := m.Serve(); err != nil {
188 glog.Exitf("Serve(): %v", err)
189 }
190
191 <-m.Done()
192}
193
194func (s *server) Messages(req *pb.MessagesRequest, stream pb.SMSGateway_MessagesServer) error {
195 re := regexp.MustCompile(".*")
196 if req.FilterBody != "" {
197 var err error
198 re, err = regexp.Compile(req.FilterBody)
199 if err != nil {
200 return status.Errorf(codes.InvalidArgument, "filter regexp error: %v", err)
201 }
202 }
203
204 data := make(chan *sms)
205 cancel := make(chan struct{})
206 defer func() {
207 close(cancel)
208 close(data)
209 }()
210
211 s.dispatcher.subscribe(&subscriber{
212 re: re,
213 data: data,
214 cancel: cancel,
215 })
216
217 for d := range data {
218 stream.Send(&pb.MessagesResponse{
219 Sender: d.from,
220 Body: d.body,
221 Timestamp: d.timestamp.UnixNano(),
222 })
223 }
224
225 return nil
226}