From 7b303129b96124b7497acb985aa2b919612ffd7b Mon Sep 17 00:00:00 2001 From: Ramy Aboul Naga Date: Wed, 28 Jun 2017 04:10:34 +0200 Subject: [PATCH] Adds integrity check (#19) (#30) * Adds integrity check (#19) * Replace usage of `flag' with `configure' for `port' arg. Usage: ./main # Default port 8080 ./main --port=1234 # Custom port 1234 --- cmd/bot/config.json.example | 6 ++++ cmd/bot/main.go | 7 ++-- messenger.go | 64 +++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 cmd/bot/config.json.example diff --git a/cmd/bot/config.json.example b/cmd/bot/config.json.example new file mode 100644 index 0000000..264a73c --- /dev/null +++ b/cmd/bot/config.json.example @@ -0,0 +1,6 @@ +{ + "verify-token": "", + "should-verify": false, + "page-token": "", + "app-secret": "" +} diff --git a/cmd/bot/main.go b/cmd/bot/main.go index d3a7aa0..87f4690 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -14,6 +14,8 @@ var ( verifyToken = conf.String("verify-token", "mad-skrilla", "The token used to verify facebook") verify = conf.Bool("should-verify", false, "Whether or not the app should verify itself") pageToken = conf.String("page-token", "not skrilla", "The token that is used to verify the page on facebook") + appSecret = conf.String("app-secret", "", "The app secret from the facebook developer portal") + port = conf.Int("port", 8080, "The port used to serve the messenger bot") ) func main() { @@ -26,6 +28,7 @@ func main() { // Create a new messenger client client := messenger.New(messenger.Options{ Verify: *verify, + AppSecret: *appSecret, VerifyToken: *verifyToken, Token: *pageToken, }) @@ -52,7 +55,7 @@ func main() { fmt.Println("Read at:", m.Watermark().Format(time.UnixDate)) }) - fmt.Println("Serving messenger bot on localhost:8080") + fmt.Printf("Serving messenger bot on localhost:%d\n", *port) - http.ListenAndServe("localhost:8080", client.Handler()) + http.ListenAndServe(fmt.Sprintf("localhost:%d", *port), client.Handler()) } diff --git a/messenger.go b/messenger.go index 18128cf..8011d2d 100644 --- a/messenger.go +++ b/messenger.go @@ -2,10 +2,13 @@ package messenger import ( "bytes" + "crypto/hmac" + "crypto/sha1" "encoding/json" "fmt" "io/ioutil" "net/http" + "strings" "time" ) @@ -24,6 +27,9 @@ type Options struct { // Verify sets whether or not to be in the "verify" mode. Used for // verifying webhooks on the Facebook Developer Portal. Verify bool + // AppSecret is the app secret from the Facebook Developer Portal. Used when + // in the "verify" mode. + AppSecret string // VerifyToken is the token to be used when verifying the webhook. Is set // when the webhook is created. VerifyToken string @@ -64,6 +70,8 @@ type Messenger struct { referralHandlers []ReferralHandler token string verifyHandler func(http.ResponseWriter, *http.Request) + verify bool + appSecret string } // New creates a new Messenger. You pass in Options in order to affect settings. @@ -73,8 +81,10 @@ func New(mo Options) *Messenger { } m := &Messenger{ - mux: mo.Mux, - token: mo.Token, + mux: mo.Mux, + token: mo.Token, + verify: mo.Verify, + appSecret: mo.AppSecret, } if mo.WebhookURL == "" { @@ -240,7 +250,11 @@ func (m *Messenger) handle(w http.ResponseWriter, r *http.Request) { var rec Receive - err := json.NewDecoder(r.Body).Decode(&rec) + // consume a *copy* of the request body + body, _ := ioutil.ReadAll(r.Body) + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + err := json.Unmarshal(body, &rec) if err != nil { fmt.Println("could not decode response:", err) fmt.Fprintln(w, `{status: 'not ok'}`) @@ -251,11 +265,55 @@ func (m *Messenger) handle(w http.ResponseWriter, r *http.Request) { fmt.Println("Object is not page, undefined behaviour. Got", rec.Object) } + if m.verify { + if err := m.checkIntegrity(r); err != nil { + fmt.Println("could not verify request:", err) + fmt.Fprintln(w, `{status: 'not ok'}`) + return + } + } + m.dispatch(rec) fmt.Fprintln(w, `{status: 'ok'}`) } +// checkIntegrity checks the integrity of the requests received +func (m *Messenger) checkIntegrity(r *http.Request) error { + if m.appSecret == "" { + return fmt.Errorf("missing app secret") + } + + sigHeader := "X-Hub-Signature" + sig := strings.SplitN(r.Header.Get(sigHeader), "=", 2) + if len(sig) == 1 { + if sig[0] == "" { + return fmt.Errorf("missing %s header", sigHeader) + } + return fmt.Errorf("malformed %s header: %v", sigHeader, strings.Join(sig, "=")) + } + + checkSHA1 := func(body []byte, hash string) error { + mac := hmac.New(sha1.New, []byte(m.appSecret)) + if mac.Write(body); fmt.Sprintf("%x", mac.Sum(nil)) != hash { + return fmt.Errorf("invalid signature: %s", hash) + } + return nil + } + + body, _ := ioutil.ReadAll(r.Body) + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + sigEnc := strings.ToLower(sig[0]) + sigHash := strings.ToLower(sig[1]) + switch sigEnc { + case "sha1": + return checkSHA1(body, sigHash) + default: + return fmt.Errorf("unknown %s header encoding, expected sha1: %s", sigHeader, sig[0]) + } +} + // dispatch triggers all of the relevant handlers when a webhook event is received. func (m *Messenger) dispatch(r Receive) { for _, entry := range r.Entry {