118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package twilio
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/sessions"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
|
|
"github.com/eternal-flame-AD/yoake/config"
|
|
"github.com/eternal-flame-AD/yoake/internal/auth"
|
|
"github.com/eternal-flame-AD/yoake/internal/session"
|
|
"github.com/twilio/twilio-go/client"
|
|
)
|
|
|
|
func firstUrlValues(vals ...url.Values) map[string]string {
|
|
res := make(map[string]string)
|
|
for _, val := range vals {
|
|
for k, v := range val {
|
|
res[k] = v[0]
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
const verifySessionName = "twilio-verify"
|
|
|
|
func VerifyMiddleware(prefix string, baseurlS string) echo.MiddlewareFunc {
|
|
baseURL, err := url.Parse(baseurlS)
|
|
if err != nil {
|
|
log.Fatalf("invalid twilio baseurl: %v", baseurlS)
|
|
}
|
|
var basicAuth echo.MiddlewareFunc
|
|
if userpass := baseURL.User.String(); userpass != "" {
|
|
basicAuth = middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
|
ui := url.UserPassword(username, password)
|
|
return subtle.ConstantTimeCompare([]byte(ui.String()), []byte(userpass)) == 1, nil
|
|
})
|
|
}
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
verifySignature := func(c echo.Context) error {
|
|
store := c.Get(session.SessionStoreKeyPrefix + "cookie").(sessions.Store)
|
|
if reqAuth := auth.GetRequestAuth(c); reqAuth.Valid && reqAuth.HasRole(auth.RoleAdmin) {
|
|
return next(c)
|
|
}
|
|
|
|
bypassOk := false
|
|
sess, _ := store.Get(c.Request(), verifySessionName)
|
|
if ts, ok := sess.Values["verified"].(int64); ok && time.Now().Unix() < ts {
|
|
bypassOk = true
|
|
}
|
|
|
|
cleanPath := path.Clean(c.Request().URL.Path)
|
|
if cleanPath == prefix || strings.HasPrefix(cleanPath, prefix+"/") {
|
|
fullReq := c.Request().Clone(c.Request().Context())
|
|
fullReq.URL = baseURL.ResolveReference(c.Request().URL)
|
|
fullReq.URL.User = nil
|
|
if err := TwilioValidate(c, fullReq); err != nil {
|
|
log.Printf("twilio verify failed: %v, url=%s", err, fullReq.URL.String())
|
|
if !bypassOk {
|
|
c.String(http.StatusOK, "We are sorry. Request Validation Failed. This is not your fault.")
|
|
return nil
|
|
}
|
|
} else {
|
|
sess.Values["verified"] = time.Now().Add(5 * time.Minute).Unix()
|
|
sess.Save(c.Request(), c.Response())
|
|
}
|
|
}
|
|
|
|
return next(c)
|
|
}
|
|
if basicAuth != nil {
|
|
return basicAuth(verifySignature)
|
|
}
|
|
return verifySignature
|
|
}
|
|
}
|
|
|
|
func TwilioValidate(c echo.Context, req *http.Request) error {
|
|
conf := config.Config().Twilio
|
|
signature := req.Header.Get("X-Twilio-Signature")
|
|
if signature == "" {
|
|
if conf.SkipVerify {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("no twilio signature present")
|
|
}
|
|
requestValidator := client.NewRequestValidator(conf.AuthToken)
|
|
if req.Method == "POST" {
|
|
query := c.QueryParams()
|
|
form, err := c.FormParams()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !requestValidator.Validate(req.URL.String(), firstUrlValues(form), signature) {
|
|
req.URL.RawQuery = ""
|
|
if !requestValidator.Validate(req.URL.String(), firstUrlValues(form, query), signature) {
|
|
return fmt.Errorf("twilio signature verification failed")
|
|
}
|
|
}
|
|
} else if req.Method == "GET" {
|
|
if !requestValidator.Validate(req.URL.String(), nil, signature) {
|
|
return fmt.Errorf("twilio signature verification failed")
|
|
}
|
|
} else {
|
|
return fmt.Errorf("twilio signature verification failed: unsupported method %s", req.Method)
|
|
}
|
|
|
|
return nil
|
|
}
|