Skip to content

Commit

Permalink
Implement httphandler in new webhok module. (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
tokuhirom authored Nov 1, 2023
1 parent ba62e0f commit 273a7a0
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 14 deletions.
45 changes: 32 additions & 13 deletions examples/echo_bot_handler/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,64 @@
package main

import (
"fmt"
"log"
"net/http"
"os"

"github.com/line/line-bot-sdk-go/v7/linebot"
"github.com/line/line-bot-sdk-go/v7/linebot/httphandler"
"github.com/line/line-bot-sdk-go/v7/linebot/messaging_api"
"github.com/line/line-bot-sdk-go/v7/linebot/webhook"
)

func main() {
handler, err := httphandler.New(
os.Getenv("CHANNEL_SECRET"),
os.Getenv("CHANNEL_TOKEN"),
handler, err := webhook.NewWebhookHandler(
os.Getenv("LINE_CHANNEL_SECRET"),
)
if err != nil {
log.Fatal(err)
}
bot, err := messaging_api.NewMessagingApiAPI(os.Getenv("LINE_CHANNEL_TOKEN"))

// Setup HTTP Server for receiving requests from LINE platform
handler.HandleEvents(func(events []*linebot.Event, r *http.Request) {
bot, err := handler.NewClient()
handler.HandleEvents(func(req *webhook.CallbackRequest, r *http.Request) {
if err != nil {
log.Print(err)
return
}
for _, event := range events {
if event.Type == linebot.EventTypeMessage {
switch message := event.Message.(type) {
case *linebot.TextMessage:
if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(message.Text)).Do(); err != nil {
log.Println("Handling events...")
for _, event := range req.Events {
log.Printf("/callback called%+v...\n", event)
switch e := event.(type) {
case webhook.MessageEvent:
switch message := e.Message.(type) {
case webhook.TextMessageContent:
_, err = bot.ReplyMessage(
&messaging_api.ReplyMessageRequest{
ReplyToken: e.ReplyToken,
Messages: []messaging_api.MessageInterface{
&messaging_api.TextMessage{
Text: message.Text,
},
},
},
)
if err != nil {
log.Print(err)
}
}
}
}
})
http.Handle("/callback", handler)

// This is just a sample code.
// For actually use, you must support HTTPS by using `ListenAndServeTLS`, reverse proxy or etc.
if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
port := os.Getenv("PORT")
if port == "" {
port = "5000"
}
fmt.Println("http://localhost:" + port + "/")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
74 changes: 74 additions & 0 deletions linebot/webhook/httphandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2016 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package webhook

import (
"errors"
"log"
"net/http"
)

// EventsHandlerFunc type
type EventsHandlerFunc func(*CallbackRequest, *http.Request)

// ErrorHandlerFunc type
type ErrorHandlerFunc func(error, *http.Request)

// WebhookHandler type
type WebhookHandler struct {
channelSecret string

handleEvents EventsHandlerFunc
handleError ErrorHandlerFunc
}

// New returns a new WebhookHandler instance.
func NewWebhookHandler(channelSecret string) (*WebhookHandler, error) {
if channelSecret == "" {
return nil, errors.New("missing channel secret")
}
h := &WebhookHandler{
channelSecret: channelSecret,
}
return h, nil
}

// HandleEvents method
func (wh *WebhookHandler) HandleEvents(f EventsHandlerFunc) {
wh.handleEvents = f
}

// HandleError method
func (wh *WebhookHandler) HandleError(f ErrorHandlerFunc) {
wh.handleError = f
}

func (wh *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
events, err := ParseRequest(wh.channelSecret, r)
if err != nil {
if wh.handleError != nil {
wh.handleError(err, r)
}
if err == ErrInvalidSignature {
log.Printf("linebot webhook request validation error: %v", err)
w.WriteHeader(400)
} else {
log.Printf("linebot internal server error: %v", err)
w.WriteHeader(500)
}
return
}
wh.handleEvents(events, r)
}
127 changes: 127 additions & 0 deletions linebot/webhook/httphandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2016 LINE Corporation
//
// LINE Corporation licenses this file to you under the Apache License,
// version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package webhook

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
)

var testRequestBody = `{
"events": [
{
"replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
"type": "message",
"timestamp": 1462629479859,
"source": {
"type": "user",
"userId": "u206d25c2ea6bd87c17655609a1c37cb8"
},
"message": {
"id": "325708",
"type": "text",
"text": "Hello, world"
}
}
]
}
`

const (
testChannelSecret = "testsecret"
testChannelToken = "testtoken"
)

func TestWebhookHandler(t *testing.T) {
handler, err := NewWebhookHandler(testChannelSecret)
if err != nil {
t.Error(err)
}
handlerFunc := func(req *CallbackRequest, r *http.Request) {
if req == nil {
t.Errorf("events is nil")
}
if r == nil {
t.Errorf("r is nil")
}
}
handler.HandleEvents(handlerFunc)

server := httptest.NewTLSServer(handler)
defer server.Close()
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}

// valid signature
{
body := []byte(testRequestBody)
req, err := http.NewRequest("POST", server.URL, bytes.NewReader(body))
if err != nil {
t.Fatal(err)
}
// generate signature
mac := hmac.New(sha256.New, []byte(testChannelSecret))
mac.Write(body)

req.Header.Set("X-Line-Signature", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
res, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
if res == nil {
t.Fatal("response is nil")
}
if res.StatusCode != http.StatusOK {
t.Errorf("status: %d", res.StatusCode)
}
}

// invalid signature
handler.HandleError(func(err error, r *http.Request) {
if err != ErrInvalidSignature {
t.Errorf("err %v; want %v", err, ErrInvalidSignature)
}
if r == nil {
t.Errorf("r is nil")
}
})
{
body := []byte(testRequestBody)
req, err := http.NewRequest("POST", server.URL, bytes.NewReader(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-LINE-Signature", "invalidSignature")
res, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
if res == nil {
t.Fatal("response is nil")
}
if res.StatusCode != 400 {
t.Errorf("status: %d", 400)
}
}
}
3 changes: 2 additions & 1 deletion linebot/webhook/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
Expand All @@ -27,7 +28,7 @@ func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, erro

var cb CallbackRequest
if err = json.Unmarshal(body, &cb); err != nil {
return nil, err
return nil, fmt.Errorf("failed to unmarshal request body: %w, %s", err, body)
}
return &cb, nil
}
Expand Down

0 comments on commit 273a7a0

Please sign in to comment.