diff options
| author | Grégoire Duchêne <gduchene@awhk.org> | 2021-03-14 15:41:22 +0000 |
|---|---|---|
| committer | Grégoire Duchêne <gduchene@awhk.org> | 2021-03-14 15:41:22 +0000 |
| commit | ce3182d4c3f5fb723a16bece3157a5058b1236f9 (patch) | |
| tree | 858301133dd6a547e935da356f049ac165261c06 | |
| parent | d10731043bfd7429ebd22c717794c8363f462caf (diff) | |
Move Twilio authn code into its own package
| -rw-r--r-- | cmd/fwdsms/config.go | 2 | ||||
| -rw-r--r-- | cmd/fwdsms/mailer.go | 8 | ||||
| -rw-r--r-- | cmd/fwdsms/mailer_test.go | 13 | ||||
| -rw-r--r-- | cmd/fwdsms/main.go | 20 | ||||
| -rw-r--r-- | cmd/fwdsms/twilio.go | 115 | ||||
| -rw-r--r-- | cmd/fwdsms/twilio_test.go | 132 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 5 | ||||
| -rw-r--r-- | pkg/twilio/filter.go | 74 | ||||
| -rw-r--r-- | pkg/twilio/filter_test.go | 114 | ||||
| -rw-r--r-- | pkg/twilio/handler.go | 4 | ||||
| -rw-r--r-- | pkg/twilio/sms.go | 34 | ||||
| -rw-r--r-- | pkg/twilio/util.go | 16 |
13 files changed, 277 insertions, 262 deletions
diff --git a/cmd/fwdsms/config.go b/cmd/fwdsms/config.go index 0a50901..4eb0fdf 100644 --- a/cmd/fwdsms/config.go +++ b/cmd/fwdsms/config.go @@ -30,7 +30,7 @@ type SMTP struct { type Twilio struct { Address string `yaml:"address"` - AuthToken string `yaml:"authToken"` + AuthToken []byte `yaml:"authToken"` Endpoint string `yaml:"endpoint"` } diff --git a/cmd/fwdsms/mailer.go b/cmd/fwdsms/mailer.go index b61c981..f8c1db6 100644 --- a/cmd/fwdsms/mailer.go +++ b/cmd/fwdsms/mailer.go @@ -13,6 +13,8 @@ import ( "net/smtp" "text/template" "time" + + "go.awhk.org/fwdsms/pkg/twilio" ) type email struct { @@ -23,7 +25,7 @@ type email struct { type mailer struct { auth smtp.Auth hostname string - sms <-chan SMS + sms <-chan twilio.SMS tmplFrom, tmplTo, tmplMsg *template.Template } @@ -64,7 +66,7 @@ func (m *mailer) sendEmail(e email) error { return c.Quit() } -func (m *mailer) newEmail(sms SMS) email { +func (m *mailer) newEmail(sms twilio.SMS) email { var from, to, msg bytes.Buffer if err := m.tmplFrom.Execute(&from, sms); err != nil { log.Printf("Failed to apply a template: %v.", err) @@ -91,7 +93,7 @@ func (m *mailer) start(ctx context.Context) { } } -func newMailer(cfg *Config, sms <-chan SMS) *mailer { +func newMailer(cfg *Config, sms <-chan twilio.SMS) *mailer { if cfg.Message.From == "" { log.Fatal("Missing From field.") } diff --git a/cmd/fwdsms/mailer_test.go b/cmd/fwdsms/mailer_test.go index b537ffb..e1376c3 100644 --- a/cmd/fwdsms/mailer_test.go +++ b/cmd/fwdsms/mailer_test.go @@ -9,6 +9,8 @@ import ( "time" "github.com/stretchr/testify/assert" + + "go.awhk.org/fwdsms/pkg/twilio" ) func TestMailer_newEmail(t *testing.T) { @@ -19,12 +21,17 @@ func TestMailer_newEmail(t *testing.T) { Subject: "New SMS From {{.From}}", Template: `From: {{.From}} To: {{.To}} -Date: {{.Date.UTC}} +Date: {{.DateReceived.UTC}} -{{.Message}}`, +{{.Body}}`, }}, nil) // Reserved phone numbers, see Ofcom's website. - sms := SMS{time.Unix(0, 0), "+442079460123", "+447700900123", "Hello World!"} + sms := twilio.SMS{ + DateReceived: time.Unix(0, 0), + From: "+442079460123", + To: "+447700900123", + Body: "Hello World!", + } wants := email{ from: "fwdsms@example.com", to: "sms+447700900123@example.com", diff --git a/cmd/fwdsms/main.go b/cmd/fwdsms/main.go index 47af33a..a810be1 100644 --- a/cmd/fwdsms/main.go +++ b/cmd/fwdsms/main.go @@ -14,7 +14,10 @@ import ( "time" "github.com/gorilla/handlers" + "github.com/gorilla/mux" "golang.org/x/sys/unix" + + "go.awhk.org/fwdsms/pkg/twilio" ) var cfgFilename = flag.String("c", "/etc/fwdsms.yaml", "configuration file") @@ -30,10 +33,19 @@ func main() { done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, unix.SIGTERM) - sms := make(chan SMS) - mux := http.NewServeMux() - mux.Handle(cfg.Twilio.Endpoint, handlers.ProxyHeaders(newSMSHandler(cfg, sms))) - srv := http.Server{Handler: mux} + sms := make(chan twilio.SMS) + + r := mux.NewRouter() + r.Path(cfg.Twilio.Endpoint). + Methods(http.MethodPost). + Handler(handlers.ProxyHeaders(&twilio.Filter{ + AuthToken: cfg.Twilio.AuthToken, + Handler: &twilio.SMSTee{ + Chan: sms, + Handler: twilio.EmptyResponseHandler, + }, + })) + srv := http.Server{Handler: r} go func() { var ( l net.Listener diff --git a/cmd/fwdsms/twilio.go b/cmd/fwdsms/twilio.go deleted file mode 100644 index f997465..0000000 --- a/cmd/fwdsms/twilio.go +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-FileCopyrightText: © 2020 Grégoire Duchêne <gduchene@awhk.org> -// SPDX-License-Identifier: ISC - -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "errors" - "fmt" - "hash" - "log" - "net/http" - "sort" - "strings" - "time" -) - -type SMS struct { - Date time.Time - From, To, Message string -} - -type smsHandler struct { - hash hash.Hash - sms chan SMS -} - -var _ http.Handler = &smsHandler{} - -func (h *smsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - resp.Header().Set("Allow", http.MethodPost) - resp.WriteHeader(http.StatusMethodNotAllowed) - return - } - if err := h.checkRequestSignature(req); err != nil { - log.Printf("Failed to check the request signature: %v.", err) - resp.WriteHeader(http.StatusBadRequest) - return - } - - from, ok := req.PostForm["From"] - if !ok || len(from) == 0 { - resp.WriteHeader(http.StatusBadRequest) - return - } - to, ok := req.PostForm["To"] - if !ok || len(to) == 0 { - resp.WriteHeader(http.StatusBadRequest) - return - } - msg, ok := req.PostForm["Body"] - if !ok || len(msg) == 0 { - resp.WriteHeader(http.StatusBadRequest) - return - } - resp.Header().Set("Content-Type", "text/xml") - fmt.Fprintf(resp, "<Response/>") - h.sms <- SMS{time.Now(), from[0], to[0], msg[0]} -} - -func (h *smsHandler) checkRequestSignature(req *http.Request) error { - reqSig, err := func() ([]byte, error) { - h := req.Header.Get("X-Twilio-Signature") - if len(h) == 0 { - return nil, errors.New("missing X-Twilio-Signature header") - } - b, err := base64.StdEncoding.DecodeString(h) - if err != nil { - return nil, errors.New("bad X-Twilio-Signature header") - } - return b, nil - }() - if err != nil { - return err - } - - if err := req.ParseForm(); err != nil { - return err - } - ourSig := func() []byte { - defer h.hash.Reset() - parts := []string{} - for k := range req.PostForm { - parts = append(parts, k) - } - sort.Strings(parts) - for i := range parts { - parts[i] += req.PostForm[parts[i]][0] - } - blob := req.Host + req.URL.Path + strings.Join(parts, "") - if req.URL.Scheme != "" { - blob = fmt.Sprintf("%s://%s", req.URL.Scheme, blob) - } - h.hash.Write([]byte(blob)) - return h.hash.Sum(nil) - }() - - if !hmac.Equal(ourSig, reqSig) { - return errors.New("signature mismatch") - } - return nil -} - -func newSMSHandler(cfg *Config, sms chan SMS) *smsHandler { - if cfg.Twilio.AuthToken == "" { - log.Fatal("Twilio auth token unspecified.") - } - if cfg.Twilio.Endpoint == "" { - log.Fatal("Twilio endpoint unspecified.") - } - return &smsHandler{hmac.New(sha1.New, []byte(cfg.Twilio.AuthToken)), sms} -} diff --git a/cmd/fwdsms/twilio_test.go b/cmd/fwdsms/twilio_test.go deleted file mode 100644 index b17bf06..0000000 --- a/cmd/fwdsms/twilio_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: © 2020 Grégoire Duchêne <gduchene@awhk.org> -// SPDX-License-Identifier: ISC - -package main - -import ( - "io/ioutil" - "net/http" - "net/url" - "strings" - "testing" - "time" - - "github.com/gorilla/handlers" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "go.awhk.org/pipeln" -) - -var handler = newSMSHandler(&Config{ - Twilio: Twilio{ - AuthToken: "token", - Endpoint: "/endpoint", - }, -}, make(chan SMS)) - -func TestSMSHandler_checkRequestSignature_MissingHeader(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "https://example.com/endpoint", nil) - assert.NoError(t, err) - assert.EqualError(t, handler.checkRequestSignature(req), "missing X-Twilio-Signature header") -} - -func TestSMSHandler_checkRequestSignature_SignatureBad(t *testing.T) { - form := url.Values{} - form.Set("foo", "bar") - req, err := http.NewRequest(http.MethodPost, "https://example.com/endpoint", strings.NewReader(form.Encode())) - assert.NoError(t, err) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("X-Twilio-Signature", "a bad signature") - assert.NoError(t, req.ParseForm()) - assert.EqualError(t, handler.checkRequestSignature(req), "bad X-Twilio-Signature header") -} - -func TestSMSHandler_checkRequestSignature_SignatureMismatch(t *testing.T) { - form := url.Values{} - form.Set("foo", "bar") - req, err := http.NewRequest(http.MethodPost, "https://example.com/endpoint", strings.NewReader(form.Encode())) - assert.NoError(t, err) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("X-Twilio-Signature", "DYIRnXpKIjrgAMxc0FD01B55+ag=") - assert.NoError(t, req.ParseForm()) - assert.EqualError(t, handler.checkRequestSignature(req), "signature mismatch") -} - -func TestSMSHandler_checkRequestSignature_SignatureGood(t *testing.T) { - form := url.Values{} - form.Set("foo", "bar") - form.Set("bar", "baz") - req, err := http.NewRequest(http.MethodPost, "/endpoint", strings.NewReader(form.Encode())) - req.Host = "example.com" - req.URL.Scheme = "https" - assert.NoError(t, err) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - // Signature generated with: - // % echo -n "https://example.com/endpointbarbazfoobar" | openssl dgst -binary -hmac "token" -sha1 | base64 - req.Header.Set("X-Twilio-Signature", "NpKVG88Z4y6ayJIxLJrzgEHeEwY=") - assert.NoError(t, req.ParseForm()) - assert.NoError(t, handler.checkRequestSignature(req)) -} - -func TestSMSHandler_EndToEnd(t *testing.T) { - mux := http.NewServeMux() - mux.Handle("/endpoint", handlers.ProxyHeaders(handler)) - srv := http.Server{Handler: mux} - ln := pipeln.New("localhost.test:80") - go srv.Serve(ln) - defer srv.Close() - - client := http.Client{Transport: &http.Transport{Dial: ln.Dial}} - form := url.Values{} - form.Set("From", "Foo") - form.Set("To", "Bar") - form.Set("Body", "Test") - req, err := http.NewRequest(http.MethodPost, "http://localhost.test:80/endpoint", strings.NewReader(form.Encode())) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("X-Forwarded-Scheme", "http") - - t.Run("Bad HTTP Method", func(t *testing.T) { - resp, err := client.Head("http://localhost.test:80/endpoint") - require.NoError(t, err) - assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) - }) - - t.Run("Bad Signature", func(t *testing.T) { - req.Header.Set("X-Twilio-Signature", "DYIRnXpKIjrgAMxc0FD01B55+ag=") - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - }) - - t.Run("Good Signature", func(t *testing.T) { - done := make(chan struct{}) - - go func() { - defer close(done) - select { - case sms := <-handler.sms: - assert.Equal(t, "Foo", sms.From) - assert.Equal(t, "Bar", sms.To) - assert.Equal(t, "Test", sms.Message) - case <-time.After(time.Second): - t.Error("Timed out while waiting on handler.sms.") - } - }() - - // Signature generated with: - // % echo -n "http://localhost.test:80/endpointBodyTestFromFooToBar" | openssl dgst -binary -hmac "token" -sha1 | base64 - req.Header.Set("X-Twilio-Signature", "iiifXqv3dP5j8Oj5eB4RAOm/3tI=") - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "text/xml", resp.Header.Get("Content-Type")) - assert.Equal(t, "<Response/>", string(body)) - <-done - }) -} @@ -4,8 +4,8 @@ go 1.15 require ( github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.7.0 - go.awhk.org/pipeln v0.1.0 golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) @@ -4,14 +4,13 @@ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8S github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.awhk.org/pipeln v0.1.0 h1:lqH2sV2g1mGg207mo5EvKtEFDg30mQ0JoECrhRl5lp8= -go.awhk.org/pipeln v0.1.0/go.mod h1:+yf4v5PiMs/Zr9sQlUUPkPA4jULrBADr+hDuAPjtXZU= golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b h1:lAZ0/chPUDWwjqosYR0X4M490zQhMsiJ4K3DbA7o+3g= golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/pkg/twilio/filter.go b/pkg/twilio/filter.go new file mode 100644 index 0000000..7d5f6b5 --- /dev/null +++ b/pkg/twilio/filter.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: © 2021 Grégoire Duchêne <gduchene@awhk.org> +// SPDX-License-Identifier: ISC + +package twilio + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "errors" + "log" + "net/http" + "sort" + "strings" +) + +var ( + ErrBase64 = errors.New("failed to decode X-Twilio-Signature header") + ErrMissingHeader = errors.New("missing X-Twilio-Signature header") + ErrSignatureMismatch = errors.New("signature mismatch") +) + +type Filter struct { + AuthToken []byte + Handler http.Handler +} + +var _ http.Handler = &Filter{} + +func (th *Filter) CheckRequestSignature(r *http.Request) error { + hdr := r.Header.Get("X-Twilio-Signature") + if len(hdr) == 0 { + return ErrMissingHeader + } + reqSig, err := base64.StdEncoding.DecodeString(hdr) + if err != nil { + return ErrBase64 + } + + // See https://www.twilio.com/docs/usage/security#validating-requests + // for more details. + + parts := []string{} + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + return err + } + for k := range r.PostForm { + parts = append(parts, k) + } + sort.Strings(parts) + for i, k := range parts { + parts[i] += r.PostForm[k][0] + } + } + s := r.URL.String() + strings.Join(parts, "") + h := hmac.New(sha1.New, th.AuthToken) + h.Write([]byte(s)) + ourSig := h.Sum(nil) + + if !hmac.Equal(reqSig, ourSig) { + return ErrSignatureMismatch + } + return nil +} + +func (th *Filter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := th.CheckRequestSignature(r); err != nil { + log.Println("Failed to check Twilio signature:", err) + w.WriteHeader(http.StatusBadRequest) + return + } + th.Handler.ServeHTTP(w, r) +} diff --git a/pkg/twilio/filter_test.go b/pkg/twilio/filter_test.go new file mode 100644 index 0000000..c0c737c --- /dev/null +++ b/pkg/twilio/filter_test.go @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: © 2021 Grégoire Duchêne <gduchene@awhk.org> +// SPDX-License-Identifier: ISC + +package twilio + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFilter_CheckRequestSignature(t *testing.T) { + th := &Filter{[]byte("token"), EmptyResponseHandler} + + t.Run("Good Signature (POST)", func(t *testing.T) { + assert.NoError(t, th.CheckRequestSignature(newRequest(Post))) + }) + + t.Run("Good Signature (GET)", func(t *testing.T) { + assert.NoError(t, th.CheckRequestSignature(newRequest(Get))) + }) + + t.Run("Missing Header", func(t *testing.T) { + r := newRequest(Post) + r.Header.Del("X-Twilio-Signature") + assert.ErrorIs(t, th.CheckRequestSignature(r), ErrMissingHeader) + }) + + t.Run("Bad Base64", func(t *testing.T) { + r := newRequest(Post) + r.Header.Set("X-Twilio-Signature", "Very suspicious Base64 header.") + assert.ErrorIs(t, th.CheckRequestSignature(r), ErrBase64) + }) + + t.Run("Signature Mismatch", func(t *testing.T) { + r := newRequest(Post) + r.Header.Set("X-Twilio-Signature", "dpE7iSS3LEQo72hCT34eBRt3UEI=") + assert.ErrorIs(t, th.CheckRequestSignature(r), ErrSignatureMismatch) + }) +} + +func TestFilter_ServeHTTP(t *testing.T) { + th := &Filter{[]byte("token"), EmptyResponseHandler} + + t.Run("Good Signature (POST)", func(t *testing.T) { + w := httptest.NewRecorder() + th.ServeHTTP(w, newRequest(Post)) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "text/xml", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "<Response/>", w.Body.String()) + }) + + t.Run("Good Signature (GET)", func(t *testing.T) { + w := httptest.NewRecorder() + th.ServeHTTP(w, newRequest(Get)) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "text/xml", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, "<Response/>", w.Body.String()) + }) + + t.Run("Missing Header", func(t *testing.T) { + w := httptest.NewRecorder() + r := newRequest(Post) + r.Header.Del("X-Twilio-Signature") + th.ServeHTTP(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("Bad Base64", func(t *testing.T) { + w := httptest.NewRecorder() + r := newRequest(Post) + r.Header.Set("X-Twilio-Signature", "Very suspicious Base64 header.") + th.ServeHTTP(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("Signature Mismatch", func(t *testing.T) { + w := httptest.NewRecorder() + r := newRequest(Post) + r.Header.Set("X-Twilio-Signature", "dpE7iSS3LEQo72hCT34eBRt3UEI=") + th.ServeHTTP(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +const ( + Get = true + Post = false +) + +// X-Twilio-Signature can be manually generated with: +// % echo -n "${SOME_STRING}" | openssl dgst -binary -hmac ${AUTH_TOKEN} -sha1 | base64 + +func newRequest(get bool) *http.Request { + vals := url.Values{ + "To": {"Bob"}, + "From": {"Alice"}, + "Body": {"A random message."}, + }.Encode() + if get { + r := httptest.NewRequest(http.MethodGet, "https://example.test/endpoint?"+vals, nil) + r.Header.Set("X-Twilio-Signature", "Hh0ReTk/+7Ea38qZ3Xt1/NQx4i4=") + return r + } + rd := strings.NewReader(vals) + r := httptest.NewRequest(http.MethodPost, "https://example.test/endpoint", rd) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r.Header.Set("X-Twilio-Signature", "j61PPnnoUAAsfEnLuwUefOfylf4=") + return r +} diff --git a/pkg/twilio/handler.go b/pkg/twilio/handler.go new file mode 100644 index 0000000..11cb0b8 --- /dev/null +++ b/pkg/twilio/handler.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: © 2021 Grégoire Duchêne <gduchene@awhk.org> +// SPDX-License-Identifier: ISC + +package twilio diff --git a/pkg/twilio/sms.go b/pkg/twilio/sms.go new file mode 100644 index 0000000..0a7dba8 --- /dev/null +++ b/pkg/twilio/sms.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: © 2021 Grégoire Duchêne <gduchene@awhk.org> +// SPDX-License-Identifier: ISC + +package twilio + +import ( + "net/http" + "time" +) + +type SMS struct { + DateReceived time.Time + From, To, Body string +} + +type SMSTee struct { + Chan chan<- SMS + Handler http.Handler +} + +var _ http.Handler = &SMSTee{} + +func (th *SMSTee) ServeHTTP(w http.ResponseWriter, r *http.Request) { + select { + case th.Chan <- SMS{ + DateReceived: time.Now(), + From: r.FormValue("From"), + To: r.FormValue("To"), + Body: r.FormValue("Body"), + }: + th.Handler.ServeHTTP(w, r) + case <-r.Context().Done(): + } +} diff --git a/pkg/twilio/util.go b/pkg/twilio/util.go new file mode 100644 index 0000000..1f1d269 --- /dev/null +++ b/pkg/twilio/util.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: © 2021 Grégoire Duchêne <gduchene@awhk.org> +// SPDX-License-Identifier: ISC + +package twilio + +import ( + "fmt" + "net/http" +) + +// EmptyResponseHandler writes an empty XML response so Twilio knows not +// to do anything after a webhook has been called. +var EmptyResponseHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/xml") + fmt.Fprint(w, "<Response/>") +}) |
