summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorGrégoire Duchêne <gduchene@awhk.org>2021-03-14 15:41:22 +0000
committerGrégoire Duchêne <gduchene@awhk.org>2021-03-14 15:41:22 +0000
commitce3182d4c3f5fb723a16bece3157a5058b1236f9 (patch)
tree858301133dd6a547e935da356f049ac165261c06 /cmd
parentd10731043bfd7429ebd22c717794c8363f462caf (diff)
Move Twilio authn code into its own package
Diffstat (limited to 'cmd')
-rw-r--r--cmd/fwdsms/config.go2
-rw-r--r--cmd/fwdsms/mailer.go8
-rw-r--r--cmd/fwdsms/mailer_test.go13
-rw-r--r--cmd/fwdsms/main.go20
-rw-r--r--cmd/fwdsms/twilio.go115
-rw-r--r--cmd/fwdsms/twilio_test.go132
6 files changed, 32 insertions, 258 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
- })
-}