Skip to content

Commit

Permalink
Merge pull request #2 from ccamel/add-recaptcha-support
Browse files Browse the repository at this point in the history
Add reCaptcha support
  • Loading branch information
rchakode authored Jun 4, 2020
2 parents 162ee58 + 17082f3 commit cb46b06
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode
.idea
hugo-mx-gateway
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Regardless of the deployment platform (Google App Engine, Kubernetes, Docker), t
* `CONTACT_REPLY_BCC_EMAIL`: Sets an email address for bcc copy of the email sent to the user. This is useful for tracking and follow up.
* `DEMO_URL`: Specific for demo forms, it can be used to set the URL of the demo site that will be included to the user reply email (e.g. `https://demo.example.com/`).
* `ALLOWED_ORIGINS`: Set a list of comma-separated domains that the `hugo-mx-gateway` App shoudl trust. This is for security reason to filter requests. Only requests with an `Origin` header belonging to the defined origins will be accepted, through it's only required that the request has a valid `Referer` header. It's expected in the future to these request filtering and admission rules.
* `RECAPTCHA_PRIVATE_KEY` (optional): The [reCaptcha](https://www.google.com/recaptcha/intro/v3.html) private key.
* `TEMPLATE_DEMO_REQUEST_REPLY` (optional): Specify the path of the template to reply a demo request. The default templare is `templates/template_reply_demo_request.html`
* `TEMPLATE_CONTACT_REQUEST_REPLY` (optional): Specify the path of the template to reply a contact request. The default templare is `templates/template_reply_contact_request.html`.

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module hugo-mx-gateway
go 1.13

require (
github.com/dpapathanasiou/go-recaptcha v0.0.0-20190121160230-be5090b17804
github.com/gorilla/mux v1.7.4
github.com/sirupsen/logrus v1.2.0
github.com/spf13/viper v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dpapathanasiou/go-recaptcha v0.0.0-20190121160230-be5090b17804 h1:gFnPvL9HX+Nrb4M2AwzFYqcwGStxYZpuDpFAqpViBG4=
github.com/dpapathanasiou/go-recaptcha v0.0.0-20190121160230-be5090b17804/go.mod h1:eovtlS/D2AGk8vy2a9sO4XzOyHMHb8jM+WPsf9pkgFo=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
Expand Down
18 changes: 9 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import (
)

type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
Name string
Method string
Pattern string
Handler http.Handler
}

type Routes []Route
Expand All @@ -40,13 +40,15 @@ var routes = Routes{
"SendMail",
"POST",
"/sendmail",
SendMail,
MuxSecAllowedDomainsHandler(
MuxSecReCaptchaHandler(
http.HandlerFunc(SendMail))),
},
Route{
"Healthz",
"GET",
"/healthz",
Healthz,
http.HandlerFunc(Healthz),
},
}

Expand All @@ -67,9 +69,7 @@ func MuxLoggerHandler(inner http.Handler, name string) http.Handler {
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = MuxLoggerHandler(handler, route.Name)
handler := MuxLoggerHandler(route.Handler, route.Name)

router.
Methods(route.Method).
Expand Down
10 changes: 8 additions & 2 deletions samples/hugo-partial-contact-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<fieldset>
<legend>Please fill in the form to submit your request</legend>
<form action="https://contact-request-endpoint/" method="post">
<!-- uncomment this div block when enabling reCaptcha
<script src="https://www.google.com/recaptcha/api.js"></script>
-->
<div class="form-item">
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Mr. Smith" />
Expand All @@ -15,7 +18,7 @@
<label for="organization">Organization</label>
<input type="text" name="organization" id="organization" placeholder="Company, Inc." />
</div>
{{ if in .Params.tags "contact" }}
{{ if in .Params.tags "contact" }}
<div class="form-item">
<label for="subject">Subject</label>
<input type="text" name="subject" id="subject" value="" placeholder="Need help or expertise?" />
Expand All @@ -25,12 +28,15 @@
<label for="message">Message</label>
<textarea rows="6" name="message" id="message" placeholder="Please add details concerning your request."></textarea>
</div>
{{ else }}
{{ else }}
<div class="form-item">
<input type="hidden" name="subject" id="subject" value="Your Access to Product Demo!" />
<input type="hidden" name="target" id="target" value="demo" />
</div>
{{ end }}
<!-- uncomment the below div when enabling reCaptcha
<div class="g-recaptcha" data-sitekey="{{.Site.Params.reCaptchaPrivateKey}}"></div>
-->
<input class="button" type="submit" value="Submit">
</form>
</fieldset>
Expand Down
92 changes: 66 additions & 26 deletions sendmail.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Copyright 2020 Rodrigue Chakode and contributors.
Licensed 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
Expand All @@ -28,6 +28,7 @@ import (
"net/url"
"strings"

"github.com/dpapathanasiou/go-recaptcha"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -149,32 +150,71 @@ func (m *SendMailRequest) ParseTemplate(templateFileName string, data interface{
return nil
}

// SendMail handles HTTP request to send email
func SendMail(httpResp http.ResponseWriter, httpReq *http.Request) {
// MuxSecAllowedDomainsHandler is a security middleware which controls allowed domains.
func MuxSecAllowedDomainsHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
allowedDomains := strings.Split(viper.GetString("ALLOWED_ORIGINS"), ",")
allowedOrigins := make(map[string]bool)

for _, domain := range allowedDomains {
domainTrimmed := strings.TrimSpace(domain)
allowedOrigins[fmt.Sprintf("http://%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("https://%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("http://www.%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("https://www.%s", domainTrimmed)] = true
}

allowedDomains := strings.Split(viper.GetString("ALLOWED_ORIGINS"), ",")
allowedOrigins := make(map[string]bool)
for _, domain := range allowedDomains {
domainTrimmed := strings.TrimSpace(domain)
allowedOrigins[fmt.Sprintf("http://%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("https://%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("http://www.%s", domainTrimmed)] = true
allowedOrigins[fmt.Sprintf("https://www.%s", domainTrimmed)] = true
}
if len(httpReq.Header["Origin"]) == 0 || len(httpReq.Header["Referer"]) == 0 {
rawHeader, _ := json.Marshal(httpReq.Header)
log.Infoln("request with unexpected headers", string(rawHeader))
httpResp.WriteHeader(http.StatusForbidden)
return
}
if len(r.Header["Origin"]) == 0 || len(r.Header["Referer"]) == 0 {
rawHeader, _ := json.Marshal(r.Header)
log.Infoln("request with unexpected headers", string(rawHeader))
w.WriteHeader(http.StatusForbidden)
return
}

reqOrigin := httpReq.Header["Origin"][0]
if _, domainFound := allowedOrigins[reqOrigin]; !domainFound {
log.Errorln("not allowed origin", reqOrigin)
httpResp.WriteHeader(http.StatusForbidden)
return
}
reqOrigin := r.Header["Origin"][0]
if _, domainFound := allowedOrigins[reqOrigin]; !domainFound {
log.Errorln("not allowed origin", reqOrigin)
w.WriteHeader(http.StatusForbidden)
return
}

next.ServeHTTP(w, r)
})
}

// MuxSecReCaptchaHandler is a security middleware which verifies the challenge code from
// the reCaptcha human verification system (provided by Google).
func MuxSecReCaptchaHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recaptchaResponse, found := r.Form["g-recaptcha-response"]

if found {
remoteIp, _, _ := net.SplitHostPort(r.RemoteAddr)
recaptchaPrivateKey := viper.GetString("RECAPTCHA_PRIVATE_KEY")

recaptcha.Init(recaptchaPrivateKey)

result, err := recaptcha.Confirm(remoteIp, recaptchaResponse[0])
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorln("reCaptcha server error")
w.WriteHeader(http.StatusForbidden)
return
}

if !result {
w.WriteHeader(http.StatusForbidden)
return
}
}

next.ServeHTTP(w, r)
})
}

// SendMail handles HTTP request to send email
func SendMail(httpResp http.ResponseWriter, httpReq *http.Request) {
httpReq.ParseForm()

contactRequest := ContactRequest{
Expand Down Expand Up @@ -227,12 +267,12 @@ func SendMail(httpResp http.ResponseWriter, httpReq *http.Request) {

replyTplFile := ""
if contactRequest.RequestTarget == "demo" {
replyTplFile = viper.GetString("TEMPLATE_DEMO_REQUEST_REPLY");
replyTplFile = viper.GetString("TEMPLATE_DEMO_REQUEST_REPLY")
if replyTplFile == "" {
replyTplFile = "./templates/template_reply_demo_request.html"
}
} else {
replyTplFile = viper.GetString("TEMPLATE_CONTACT_REQUEST_REPLY");
replyTplFile = viper.GetString("TEMPLATE_CONTACT_REQUEST_REPLY")
if replyTplFile == "" {
replyTplFile = "./templates/template_reply_contact_request.html"
}
Expand Down
4 changes: 4 additions & 0 deletions vendor/github.com/dpapathanasiou/go-recaptcha/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions vendor/github.com/dpapathanasiou/go-recaptcha/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions vendor/github.com/dpapathanasiou/go-recaptcha/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions vendor/github.com/dpapathanasiou/go-recaptcha/recaptcha.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# github.com/dpapathanasiou/go-recaptcha v0.0.0-20190121160230-be5090b17804
github.com/dpapathanasiou/go-recaptcha
# github.com/fsnotify/fsnotify v1.4.7
github.com/fsnotify/fsnotify
# github.com/gorilla/mux v1.7.4
Expand Down

0 comments on commit cb46b06

Please sign in to comment.