diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..09dbac8
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/js-mailer.iml b/.idea/js-mailer.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/js-mailer.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..fd6c8d0
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..ba7b38e
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,12 @@
+package api
+
+import (
+ "github.com/ReneKroon/ttlcache/v2"
+ "github.com/wneessen/js-mailer/config"
+)
+
+// Route represents an API route object
+type Route struct {
+ Cache *ttlcache.Cache
+ Config *config.Config
+}
diff --git a/apirequest/form.go b/api/form.go
similarity index 66%
rename from apirequest/form.go
rename to api/form.go
index 070ba73..3740753 100644
--- a/apirequest/form.go
+++ b/api/form.go
@@ -1,4 +1,4 @@
-package apirequest
+package api
import (
"fmt"
@@ -8,20 +8,20 @@ import (
// GetForm gets a form.Form object either from the in-memory cache or if not cached
// yet, from the file system
-func (a *ApiRequest) GetForm(i string) (form.Form, error) {
+func (r *Route) GetForm(i string) (form.Form, error) {
var formObj form.Form
- cacheForm, err := a.Cache.Get(fmt.Sprintf("formObj_%s", i))
+ cacheForm, err := r.Cache.Get(fmt.Sprintf("formObj_%s", i))
if err != nil && err != ttlcache.ErrNotFound {
return formObj, err
}
if cacheForm != nil {
formObj = cacheForm.(form.Form)
} else {
- formObj, err = form.NewForm(a.Config, i)
+ formObj, err = form.NewForm(r.Config, i)
if err != nil {
return formObj, err
}
- if err := a.Cache.Set(fmt.Sprintf("formObj_%s", formObj.Id), formObj); err != nil {
+ if err := r.Cache.Set(fmt.Sprintf("formObj_%s", formObj.Id), formObj); err != nil {
return formObj, err
}
}
diff --git a/api/ping.go b/api/ping.go
new file mode 100644
index 0000000..a957603
--- /dev/null
+++ b/api/ping.go
@@ -0,0 +1,24 @@
+package api
+
+import (
+ "github.com/labstack/echo/v4"
+ "net/http"
+)
+
+// PingResponse reflects the JSON structure for a ping response
+type PingResponse struct {
+ StatusCode int
+ Status string
+ Data interface{}
+}
+
+// Ping is a test route for the API
+func (r *Route) Ping(c echo.Context) error {
+ return c.JSON(http.StatusOK, &PingResponse{
+ StatusCode: http.StatusOK,
+ Status: http.StatusText(http.StatusOK),
+ Data: map[string]string{
+ "Ping": "Pong",
+ },
+ })
+}
diff --git a/api/sendform.go b/api/sendform.go
new file mode 100644
index 0000000..06e83fd
--- /dev/null
+++ b/api/sendform.go
@@ -0,0 +1,84 @@
+package api
+
+import (
+ "fmt"
+ "github.com/wneessen/js-mailer/response"
+ "net/http"
+ "time"
+
+ "github.com/go-mail/mail"
+ "github.com/labstack/echo/v4"
+)
+
+// SentSuccessfull represents confirmation JSON structure for a successfully sent message
+type SentSuccessfull struct {
+ FormId string `json:"form_id"`
+ SendTime int64 `json:"send_time"`
+}
+
+// SendForm handles the HTTP form sending API request
+func (r *Route) SendForm(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ c.Logger().Errorf("no form object found in context")
+ return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
+ }
+
+ // Compose the mail message
+ mailMsg := mail.NewMessage()
+ mailMsg.SetHeader("From", sr.FormObj.Sender)
+ mailMsg.SetHeader("To", sr.FormObj.Recipients...)
+ mailMsg.SetHeader("Subject", sr.FormObj.Content.Subject)
+
+ mailBody := "The following form fields have been transmitted:\n"
+ for _, k := range sr.FormObj.Content.Fields {
+ if v := c.FormValue(k); v != "" {
+ mailBody = fmt.Sprintf("%s\n* %s => %s", mailBody, k, v)
+ }
+ }
+ mailMsg.SetBody("text/plain", mailBody)
+
+ // Send the mail message
+ var serverTimeout time.Duration
+ var err error
+ serverTimeout, err = time.ParseDuration(sr.FormObj.Server.Timeout)
+ if err != nil {
+ c.Logger().Warnf("Could not parse configured server timeout: %s", err)
+ serverTimeout = time.Second * 5
+ }
+ mailDailer := mail.NewDialer(sr.FormObj.Server.Host, sr.FormObj.Server.Port, sr.FormObj.Server.Username,
+ sr.FormObj.Server.Password)
+ mailDailer.Timeout = serverTimeout
+ if sr.FormObj.Server.ForceTLS {
+ mailDailer.StartTLSPolicy = mail.MandatoryStartTLS
+ }
+ mailSender, err := mailDailer.Dial()
+ if err != nil {
+ c.Logger().Errorf("Could not connect to configured mail server: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, &response.ErrorObj{
+ Message: "could not connect to configured mail server",
+ Data: err.Error(),
+ })
+ }
+ defer func() {
+ if err := mailSender.Close(); err != nil {
+ c.Logger().Errorf("Failed to close mail server connection: %s", err)
+ }
+ }()
+ if err := mail.Send(mailSender, mailMsg); err != nil {
+ c.Logger().Errorf("Could not send mail message: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, &response.ErrorObj{
+ Message: "could not send mail message",
+ Data: err.Error(),
+ })
+ }
+
+ return c.JSON(http.StatusOK, response.SuccessResponse{
+ StatusCode: http.StatusOK,
+ Status: http.StatusText(http.StatusOK),
+ Data: &SentSuccessfull{
+ FormId: sr.FormObj.Id,
+ SendTime: time.Now().Unix(),
+ },
+ })
+}
diff --git a/api/sendform_mw.go b/api/sendform_mw.go
new file mode 100644
index 0000000..92c2a14
--- /dev/null
+++ b/api/sendform_mw.go
@@ -0,0 +1,317 @@
+package api
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "github.com/ReneKroon/ttlcache/v2"
+ "github.com/labstack/echo/v4"
+ "github.com/wneessen/js-mailer/form"
+ "github.com/wneessen/js-mailer/response"
+)
+
+// SendFormRequest reflects the structure of the send form request data
+type SendFormRequest struct {
+ FormId string `param:"fid"`
+ FormObj *form.Form
+ Token string `param:"token"`
+ TokenResp *TokenResponse
+}
+
+// CaptchaResponse reflect the API response from various 3rd party captcha services
+type CaptchaResponse struct {
+ Success bool `json:"success"`
+ ChallengeTimestamp string `json:"challenge_ts"`
+ Hostname string `json:"hostname"`
+}
+
+// HcaptchaResponse is the CaptchaResponse for hCaptcha
+type HcaptchaResponse CaptchaResponse
+
+// ReCaptchaResponse is the CaptchaResponse for Google ReCaptcha
+type ReCaptchaResponse CaptchaResponse
+
+// SendFormBindForm is a middleware that validates the provided form data and binds
+// it to a SendFormRequest object
+func (r *Route) SendFormBindForm(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := &SendFormRequest{}
+ if err := c.Bind(sr); err != nil {
+ c.Logger().Errorf("failed to bind request to SendFormRequest object: %s", err)
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
+
+ // Let's retrieve the formObj from cache
+ cacheObj, err := r.Cache.Get(sr.Token)
+ if err == ttlcache.ErrNotFound {
+ return echo.NewHTTPError(http.StatusNotFound, "not a valid send URL")
+ }
+ if err != nil && err != ttlcache.ErrNotFound {
+ c.Logger().Errorf("failed to look up token in cache: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, &response.ErrorObj{
+ Message: "failed to look up token in cache",
+ Data: err.Error(),
+ })
+ }
+ if cacheObj != nil {
+ TokenRespObj := cacheObj.(TokenResponse)
+ sr.TokenResp = &TokenRespObj
+ }
+ if sr.TokenResp != nil && sr.TokenResp.FormId != sr.FormId {
+ c.Logger().Warn("URL form id does not match the cached form object id")
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid form id")
+ }
+ defer func() {
+ if err := r.Cache.Remove(sr.Token); err != nil {
+ c.Logger().Errorf("failed to delete used token from cache: %s", err)
+ }
+ }()
+
+ // Let's try to read formobj from cache
+ formObj, err := r.GetForm(sr.FormId)
+ if err != nil {
+ c.Logger().Errorf("failed get form object: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "form lookup failed")
+ }
+
+ sr.FormObj = &formObj
+ c.Set("formobj", sr)
+
+ return next(c)
+ }
+}
+
+// SendFormReqFields is a middleware that validates that all required fields are set in
+// the SendFormRequest object
+func (r *Route) SendFormReqFields(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "no valid form object found")
+ }
+
+ var invalidFields []string
+ fieldError := make(map[string]string)
+ for _, f := range sr.FormObj.Validation.Fields {
+ v := c.FormValue(f.Name)
+ if f.Required && v == "" {
+ invalidFields = append(invalidFields, f.Name)
+ fieldError[f.Name] = "field is required, but missing"
+ continue
+ }
+
+ switch f.Type {
+ case "text":
+ continue
+ case "email":
+ mailRegExp, err := regexp.Compile("^[a-zA-Z0-9.!#$%&'*+/\\=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
+ if err != nil {
+ c.Logger().Errorf("Failed to compile email comparison regexp: %s", err)
+ continue
+ }
+ if !mailRegExp.Match([]byte(v)) {
+ c.Logger().Debugf("Form field is expected to be of type email but does not match this requirementd: %s", f.Name)
+ invalidFields = append(invalidFields, f.Name)
+ fieldError[f.Name] = "field is expected to be of type email, but does not match"
+ continue
+ }
+ case "number":
+ numRegExp, err := regexp.Compile("^[0-9]+$")
+ if err != nil {
+ c.Logger().Errorf("Failed to compile email comparison regexp: %s", err)
+ continue
+ }
+ if !numRegExp.Match([]byte(v)) {
+ c.Logger().Debugf("Form field is expected to be of type number but does not match this requirementd: %s", f.Name)
+ invalidFields = append(invalidFields, f.Name)
+ fieldError[f.Name] = "field is expected to be of type number, but does not match"
+ continue
+ }
+ case "bool":
+ boolRegExp, err := regexp.Compile("^(?i)(true|false|0|1|on|off)$")
+ if err != nil {
+ c.Logger().Errorf("Failed to compile boolean comparison regexp: %s", err)
+ continue
+ }
+ if !boolRegExp.Match([]byte(v)) {
+ c.Logger().Debugf("Form field is expected to be of type boolean but does not match this requirementd: %s", f.Name)
+ invalidFields = append(invalidFields, f.Name)
+ fieldError[f.Name] = "field is expected to be of type bool, but does not match"
+ continue
+ }
+ case "matchval":
+ if v != f.Value {
+ invalidFields = append(invalidFields, f.Name)
+ fieldError[f.Name] = "field is expected match the configured match value, but isn't"
+ }
+ continue
+ default:
+ continue
+ }
+ }
+ if len(invalidFields) > 0 {
+ c.Logger().Errorf("Form field validation failed: %s", strings.Join(invalidFields, ", "))
+ return echo.NewHTTPError(http.StatusBadRequest, &response.ErrorObj{
+ Message: "fields(s) validation failed",
+ Data: fieldError,
+ })
+ }
+
+ return next(c)
+ }
+}
+
+// SendFormHoneypot is a middleware that checks that a configured honeypot field is not
+// filled with any data
+func (r *Route) SendFormHoneypot(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "no valid form object found")
+ }
+
+ if sr.FormObj.Validation.Honeypot != nil {
+ if c.FormValue(*sr.FormObj.Validation.Honeypot) != "" {
+ c.Logger().Warnf("form includes a honeypot field which is not empty. Denying request")
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid form request data")
+ }
+ }
+
+ return next(c)
+ }
+}
+
+// SendFormHcaptcha is a middleware that checks the form data against hCaptcha
+func (r *Route) SendFormHcaptcha(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "no valid form object found")
+ }
+
+ if sr.FormObj.Validation.Hcaptcha.Enabled {
+ hcapResponse := c.FormValue("h-captcha-response")
+ if hcapResponse == "" {
+ return echo.NewHTTPError(http.StatusBadRequest, "missing hCaptcha response")
+ }
+
+ // Create a HTTP request
+ postData := url.Values{
+ "response": {hcapResponse},
+ "secret": {sr.FormObj.Validation.Hcaptcha.SecretKey},
+ }
+ httpResp, err := http.PostForm("https://hcaptcha.com/siteverify", postData)
+ if err != nil {
+ c.Logger().Errorf("failed to post HTTP request to hCaptcha: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "hCaptcha validation failed")
+ }
+
+ var respBody bytes.Buffer
+ _, err = respBody.ReadFrom(httpResp.Body)
+ if err != nil {
+ c.Logger().Errorf("reading HTTP response body failed: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "hCaptcha validation failed")
+ }
+ if httpResp.StatusCode == http.StatusOK {
+ var hcapResp HcaptchaResponse
+ if err := json.Unmarshal(respBody.Bytes(), &hcapResp); err != nil {
+ c.Logger().Errorf("HTTP repsonse JSON unmarshalling failed: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "hCaptcha validation failed")
+ }
+ if !hcapResp.Success {
+ return echo.NewHTTPError(http.StatusBadRequest,
+ "hCaptcha challenge-response validation failed")
+ }
+ return next(c)
+ }
+
+ return echo.NewHTTPError(http.StatusBadRequest,
+ "hCaptcha challenge-response validation failed")
+ }
+
+ return next(c)
+ }
+}
+
+// SendFormRecaptcha is a middleware that checks the form data against Google ReCaptcha
+func (r *Route) SendFormRecaptcha(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "no valid form object found")
+ }
+
+ if sr.FormObj.Validation.Recaptcha.Enabled {
+ recapResponse := c.FormValue("g-recaptcha-response")
+ if recapResponse == "" {
+ return echo.NewHTTPError(http.StatusBadRequest, "missing reCaptcha response")
+ }
+
+ // Create a HTTP request
+ postData := url.Values{
+ "response": {recapResponse},
+ "secret": {sr.FormObj.Validation.Recaptcha.SecretKey},
+ }
+ httpResp, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", postData)
+ if err != nil {
+ c.Logger().Errorf("failed to post HTTP request to reCaptcha: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "reCaptcha validation failed")
+ }
+
+ var respBody bytes.Buffer
+ _, err = respBody.ReadFrom(httpResp.Body)
+ if err != nil {
+ c.Logger().Errorf("reading HTTP response body failed: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "reCaptcha validation failed")
+ }
+ if httpResp.StatusCode == http.StatusOK {
+ var recapResp ReCaptchaResponse
+ if err := json.Unmarshal(respBody.Bytes(), &recapResp); err != nil {
+ c.Logger().Errorf("HTTP repsonse JSON unmarshalling failed: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "reCaptcha validation failed")
+ }
+ if !recapResp.Success {
+ return echo.NewHTTPError(http.StatusBadRequest,
+ "reCaptcha challenge-response validation failed")
+ }
+ return next(c)
+ }
+
+ return echo.NewHTTPError(http.StatusBadRequest,
+ "reCaptcha challenge-response validation failed")
+ }
+
+ return next(c)
+ }
+}
+
+// SendFormCheckToken is a middleware that checks the form security token
+func (r *Route) SendFormCheckToken(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ sr := c.Get("formobj").(*SendFormRequest)
+ if sr == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "no valid form object found")
+ }
+
+ reqOrigin := c.Request().Header.Get("origin")
+ if reqOrigin == "" {
+ c.Logger().Errorf("no origin domain set in HTTP request")
+ return echo.NewHTTPError(http.StatusUnauthorized, "domain not allowed to access form")
+ }
+ tokenText := fmt.Sprintf("%s_%d_%d_%s_%s", reqOrigin, sr.TokenResp.CreateTime,
+ sr.TokenResp.ExpireTime, sr.FormObj.Id, sr.FormObj.Secret)
+ tokenSha := fmt.Sprintf("%x", sha256.Sum256([]byte(tokenText)))
+ if tokenSha != sr.Token {
+ c.Logger().Errorf("security token does not match")
+ return echo.NewHTTPError(http.StatusUnauthorized, "domain not allowed to access form")
+ }
+
+ return next(c)
+ }
+}
diff --git a/api/token.go b/api/token.go
new file mode 100644
index 0000000..ca199a4
--- /dev/null
+++ b/api/token.go
@@ -0,0 +1,99 @@
+package api
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "github.com/labstack/echo/v4"
+ "github.com/wneessen/js-mailer/response"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// TokenRequest reflects the incoming gettoken request data that for the parameter binding
+type TokenRequest struct {
+ FormId string `query:"formid" form:"formid"`
+}
+
+// TokenResponse reflects the JSON response struct for token request
+type TokenResponse struct {
+ Token string `json:"token"`
+ FormId string `json:"form_id"`
+ CreateTime int64 `json:"create_time,omitempty"`
+ ExpireTime int64 `json:"expire_time,omitempty"`
+ Url string `json:"url"`
+ EncType string `json:"enc_type"`
+ Method string `json:"method"`
+}
+
+// GetToken handles the HTTP token requests and return a TokenResponse on success or
+// an error on failure
+func (r *Route) GetToken(c echo.Context) error {
+ fr := &TokenRequest{}
+ if err := c.Bind(fr); err != nil {
+ c.Logger().Errorf("failed to bind request to TokenRequest object: %s", err)
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
+
+ // Let's try to read formobj from cache
+ formObj, err := r.GetForm(fr.FormId)
+ if err != nil {
+ c.Logger().Errorf("failed to get form object: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, &response.ErrorObj{
+ Message: "failed to get form object",
+ Data: "invalid form id or form configuration broken",
+ })
+ }
+
+ // Let's validate the Origin header
+ isValid := false
+ reqOrigin := c.Request().Header.Get("origin")
+ if reqOrigin == "" {
+ c.Logger().Errorf("no origin domain set in HTTP request header")
+ return echo.NewHTTPError(http.StatusUnauthorized,
+ "domain is not authorized to access the requested form")
+ }
+ for _, d := range formObj.Domains {
+ if reqOrigin == d || reqOrigin == fmt.Sprintf("http://%s", d) ||
+ reqOrigin == fmt.Sprintf("https://%s", d) {
+ isValid = true
+ }
+ }
+ if !isValid {
+ c.Logger().Errorf("domain %q not in allowed domains list for form %s", reqOrigin, formObj.Id)
+ return echo.NewHTTPError(http.StatusUnauthorized,
+ "domain is not authorized to access the requested form")
+ }
+ c.Response().Header().Set("Access-Control-Allow-Origin", reqOrigin)
+
+ // Generate the token
+ reqScheme := "http"
+ if c.Request().Header.Get("X-Forwarded-Proto") == "https" || c.Request().TLS != nil {
+ reqScheme = "https"
+ }
+ nowTime := time.Now()
+ expTime := time.Now().Add(time.Minute * 10)
+ tokenText := fmt.Sprintf("%s_%d_%d_%s_%s", reqOrigin, nowTime.Unix(), expTime.Unix(),
+ formObj.Id, formObj.Secret)
+ tokenSha := fmt.Sprintf("%x", sha256.Sum256([]byte(tokenText)))
+ respToken := TokenResponse{
+ Token: tokenSha,
+ FormId: formObj.Id,
+ CreateTime: nowTime.Unix(),
+ ExpireTime: expTime.Unix(),
+ Url: fmt.Sprintf("%s://%s/api/v1/send/%s/%s", reqScheme,
+ c.Request().Host, url.QueryEscape(formObj.Id), url.QueryEscape(tokenSha)),
+ EncType: "multipart/form-data",
+ Method: "post",
+ }
+ if err := r.Cache.Set(tokenSha, respToken); err != nil {
+ c.Logger().Errorf("Failed to store response token in cache: %s", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
+ }
+
+ return c.JSON(http.StatusOK, &response.SuccessResponse{
+ StatusCode: http.StatusOK,
+ Status: http.StatusText(http.StatusOK),
+ Data: respToken,
+ })
+}
diff --git a/apirequest/apirequest.go b/apirequest/apirequest.go
deleted file mode 100644
index a534c86..0000000
--- a/apirequest/apirequest.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package apirequest
-
-import (
- "github.com/ReneKroon/ttlcache/v2"
- log "github.com/sirupsen/logrus"
- "github.com/wneessen/js-mailer/config"
- "github.com/wneessen/js-mailer/form"
- "github.com/wneessen/js-mailer/response"
- "net/http"
- "strings"
-)
-
-// ApiRequest reflects a new Api request object
-type ApiRequest struct {
- Cache *ttlcache.Cache
- Config *config.Config
- IsHttps bool
- Scheme string
- FormId string
- Token string
- FormObj *form.Form
-}
-
-// RequestHandler handles an incoming HTTP request on the API routes and
-// routes them accordingly to its request type
-func (a *ApiRequest) RequestHandler(w http.ResponseWriter, r *http.Request) {
- l := log.WithFields(log.Fields{
- "action": "apiRequest.RequestHandler",
- })
-
- remoteAddr := r.RemoteAddr
- if r.Header.Get("X-Forwarded-For") != "" {
- remoteAddr = r.Header.Get("X-Forwarded-For")
- }
- if r.Header.Get("X-Real-Ip") != "" {
- remoteAddr = r.Header.Get("X-Real-Ip")
- }
- l.Infof("New request to %s from %s", r.URL.String(), remoteAddr)
-
- a.Scheme = "http"
- if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil {
- a.IsHttps = true
- a.Scheme = "https"
- }
-
- // Set general response header
- w.Header().Set("Access-Control-Allow-Origin", "*")
-
- switch {
- case r.URL.String() == "/api/v1/token":
- a.GetToken(w, r)
- return
- case strings.HasPrefix(r.URL.String(), "/api/v1/send/"):
- code, err := a.SendFormParse(r)
- if err != nil {
- l.Errorf("Failed to parse send request: %s", err)
- response.ErrorJsonData(w, code, "Failed parsing send request", err.Error())
- return
- }
- code, err = a.SendFormValidate(r)
- if err != nil {
- response.ErrorJsonData(w, code, "Validation failed", err.Error())
- return
- }
- a.SendForm(w, r)
- return
- default:
- response.ErrorJson(w, 404, "Unknown API route")
- }
-}
diff --git a/apirequest/gettoken.go b/apirequest/gettoken.go
deleted file mode 100644
index d36aa27..0000000
--- a/apirequest/gettoken.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package apirequest
-
-import (
- "crypto/sha256"
- "fmt"
- log "github.com/sirupsen/logrus"
- "github.com/wneessen/js-mailer/response"
- "net/http"
- "time"
-)
-
-// TokenResponseJson reflects the JSON response struct for token request
-type TokenResponseJson struct {
- Token string `json:"token"`
- FormId string `json:"form_id"`
- CreateTime int64 `json:"create_time,omitempty"`
- ExpireTime int64 `json:"expire_time,omitempty"`
- Url string `json:"url"`
- EncType string `json:"enc_type"`
- Method string `json:"method"`
-}
-
-// GetToken handles the HTTP token requests and return a TokenResponseJson on success or
-// an response.ErrorResponseJson on failure
-func (a *ApiRequest) GetToken(w http.ResponseWriter, r *http.Request) {
- l := log.WithFields(log.Fields{
- "action": "apiRequest.GetToken",
- })
-
- var formId string
- if err := r.ParseMultipartForm(a.Config.Forms.MaxLength); err != nil {
- l.Errorf("Failed to parse form parameters: %s", err)
- response.ErrorJson(w, 500, err.Error())
- return
- }
- formId = r.Form.Get("formid")
- if formId == "" {
- response.ErrorJson(w, 400, "Missing formid")
- return
- }
-
- // Let's try to read formobj from cache
- formObj, err := a.GetForm(formId)
- if err != nil {
- l.Errorf("Failed get formObj: %s", err)
- response.ErrorJson(w, 500, err.Error())
- return
- }
-
- // Let's validate the Origin header
- isValid := false
- reqOrigin := r.Header.Get("origin")
- if reqOrigin == "" {
- l.Errorf("No origin domain set in HTTP request")
- response.ErrorJson(w, 401, "Domain is not allowed to access the requested form")
- return
- }
- for _, d := range formObj.Domains {
- if reqOrigin == d || reqOrigin == fmt.Sprintf("http://%s", d) ||
- reqOrigin == fmt.Sprintf("https://%s", d) {
- isValid = true
- }
- }
- if !isValid {
- l.Errorf("Domain %q not in allowed domains list for form %s", reqOrigin, formObj.Id)
- response.ErrorJson(w, 401, "Domain is not allowed to access the requested form")
- return
- }
- w.Header().Set("Access-Control-Allow-Origin", reqOrigin)
-
- // Generate the token
- nowTime := time.Now()
- expTime := time.Now().Add(time.Minute * 10)
- tokenText := fmt.Sprintf("%s_%d_%d_%s_%s", reqOrigin, nowTime.Unix(), expTime.Unix(), formObj.Id, formObj.Secret)
- tokenSha := fmt.Sprintf("%x", sha256.Sum256([]byte(tokenText)))
- respToken := TokenResponseJson{
- Token: tokenSha,
- FormId: formObj.Id,
- CreateTime: nowTime.Unix(),
- ExpireTime: expTime.Unix(),
- Url: fmt.Sprintf("%s://%s/api/v1/send/%s/%s", a.Scheme, r.Host, formObj.Id, tokenSha),
- EncType: "multipart/form-data",
- Method: "post",
- }
- if err := a.Cache.Set(tokenSha, respToken); err != nil {
- l.Errorf("Failed to store response token in cache: %s", err)
- response.ErrorJson(w, 500, "Internal Server Error")
- return
- }
- response.SuccessJson(w, 200, respToken)
-}
diff --git a/apirequest/sendform.go b/apirequest/sendform.go
deleted file mode 100644
index 090ea48..0000000
--- a/apirequest/sendform.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package apirequest
-
-import (
- "crypto/sha256"
- "fmt"
- "github.com/ReneKroon/ttlcache/v2"
- "github.com/go-mail/mail"
- log "github.com/sirupsen/logrus"
- "github.com/wneessen/js-mailer/response"
- "github.com/wneessen/js-mailer/validation"
- "net/http"
- "strings"
- "time"
-)
-
-// SentSuccessfullJson represents a send confirmation JSON struct
-type SentSuccessfullJson struct {
- FormId string `json:"form_id"`
- SendTime int64 `json:"send_time"`
-}
-
-// SendFormParse parses the coming form data of a send requests and returns an error
-// if data is missing or incorrect
-func (a *ApiRequest) SendFormParse(r *http.Request) (int, error) {
- urlParts := strings.SplitN(r.URL.String(), "/", 6)
- if len(urlParts) != 6 {
- return 404, fmt.Errorf("invalid URL")
- }
- a.FormId = urlParts[4]
- a.Token = urlParts[5]
-
- // Only if the URL is syntatically correct, let's parse the body
- if err := r.ParseMultipartForm(a.Config.Forms.MaxLength); err != nil {
- return 500, err
- }
- return 0, nil
-}
-
-// SendFormValidate validates that all requirement are fulfilled and returns an error
-// if the validation failed
-func (a *ApiRequest) SendFormValidate(r *http.Request) (int, error) {
- l := log.WithFields(log.Fields{
- "action": "apiRequest.SendFormValidate",
- })
-
- // Let's retrieve the formObj from cache
- var tokenRespObj TokenResponseJson
- cacheObj, err := a.Cache.Get(a.Token)
- if err == ttlcache.ErrNotFound {
- return 404, fmt.Errorf("not a valid send URL")
- }
- if err != nil && err != ttlcache.ErrNotFound {
- return 500, fmt.Errorf("failed to look up token in cache: %s", err)
- }
- if cacheObj != nil {
- tokenRespObj = cacheObj.(TokenResponseJson)
- }
- if tokenRespObj.FormId != a.FormId {
- l.Warn("URL form id does not match the cached form object id")
- return 400, fmt.Errorf("invalid form id")
- }
- defer func() {
- if err := a.Cache.Remove(a.Token); err != nil {
- l.Errorf("Failed to delete used token from cache: %s", err)
- }
- }()
-
- // Let's try to read formobj from cache
- formObj, err := a.GetForm(a.FormId)
- if err != nil {
- l.Errorf("Failed get formObj: %s", err)
- return 500, fmt.Errorf("form lookup failed")
- }
- a.FormObj = &formObj
-
- // Make sure all required fields are set
- // Maybe we can build some kind of validator here
- var invalidFields []string
- fieldError := make(map[string]string)
- for _, f := range formObj.Validation.Fields {
- if err := validation.Field(r, &f); err != nil {
- invalidFields = append(invalidFields, f.Name)
- fieldError[f.Name] = err.Error()
- }
- }
- if len(invalidFields) > 0 {
- l.Errorf("Form field validation failed: %s", strings.Join(invalidFields, ", "))
- var errorMsg []string
- for _, f := range invalidFields {
- errorMsg = append(errorMsg, fmt.Sprintf("%s: %s", f, fieldError[f]))
- }
- return 400, fmt.Errorf("field(s) validation failed: %s", strings.Join(errorMsg, ", "))
- }
-
- // Anti-SPAM honeypot handling
- if formObj.Validation.Honeypot != nil {
- if r.Form.Get(*formObj.Validation.Honeypot) != "" {
- l.Warnf("Form includes a honeypot field which is not empty. Denying request")
- return 400, fmt.Errorf("invalid form data")
- }
- }
-
- // Validate hCaptcha if enabled
- if formObj.Validation.Hcaptcha.Enabled {
- hcapResponse := r.Form.Get("h-captcha-response")
- if hcapResponse == "" {
- return 400, fmt.Errorf("missing hCaptcha response")
- }
- if ok := validation.Hcaptcha(hcapResponse, formObj.Validation.Hcaptcha.SecretKey); !ok {
- return 400, fmt.Errorf("hCaptcha challenge-response validation failed")
- }
- }
-
- // Validate reCaptcha if enabled
- if formObj.Validation.Recaptcha.Enabled {
- recapResponse := r.Form.Get("g-recaptcha-response")
- if recapResponse == "" {
- return 400, fmt.Errorf("missing reCaptcha response")
- }
- if ok := validation.Recaptcha(recapResponse, formObj.Validation.Recaptcha.SecretKey); !ok {
- return 400, fmt.Errorf("reCaptcha challenge-response validation failed")
- }
- }
-
- // Check the token
- reqOrigin := r.Header.Get("origin")
- if reqOrigin == "" {
- l.Errorf("No origin domain set in HTTP request")
- return 401, fmt.Errorf("domain not allowed to access form")
- }
- tokenText := fmt.Sprintf("%s_%d_%d_%s_%s", reqOrigin, tokenRespObj.CreateTime, tokenRespObj.ExpireTime,
- formObj.Id, formObj.Secret)
- tokenSha := fmt.Sprintf("%x", sha256.Sum256([]byte(tokenText)))
- if tokenSha != a.Token {
- l.Errorf("No origin domain set in HTTP request")
- return 401, fmt.Errorf("domain not allowed to access form")
- }
-
- return 0, nil
-}
-
-// SendForm handles a send Api request
-func (a *ApiRequest) SendForm(w http.ResponseWriter, r *http.Request) {
- l := log.WithFields(log.Fields{
- "action": "apiRequest.SendForm",
- })
-
- // Compose the mail message
- mailMsg := mail.NewMessage()
- mailMsg.SetHeader("From", a.FormObj.Sender)
- mailMsg.SetHeader("To", a.FormObj.Recipients...)
- mailMsg.SetHeader("Subject", a.FormObj.Content.Subject)
-
- mailBody := "The following form fields have been transmitted:\n"
- for _, k := range a.FormObj.Content.Fields {
- if v := r.Form.Get(k); v != "" {
- mailBody = fmt.Sprintf("%s\n* %s => %s", mailBody, k, v)
- }
- }
- mailMsg.SetBody("text/plain", mailBody)
-
- // Send the mail message
- var serverTimeout time.Duration
- var err error
- serverTimeout, err = time.ParseDuration(a.FormObj.Server.Timeout)
- if err != nil {
- l.Warnf("Could not parse configured server timeout: %s", err)
- serverTimeout = time.Second * 5
- }
- mailDailer := mail.NewDialer(a.FormObj.Server.Host, a.FormObj.Server.Port, a.FormObj.Server.Username,
- a.FormObj.Server.Password)
- mailDailer.Timeout = serverTimeout
- if a.FormObj.Server.ForceTLS {
- mailDailer.StartTLSPolicy = mail.MandatoryStartTLS
- }
- mailSender, err := mailDailer.Dial()
- if err != nil {
- l.Errorf("Could not connect to configured mail server: %s", err)
- response.ErrorJson(w, 500, err.Error())
- return
- }
- defer func() {
- if err := mailSender.Close(); err != nil {
- l.Errorf("Failed to close mail server connection: %s", err)
- }
- }()
- if err := mail.Send(mailSender, mailMsg); err != nil {
- l.Errorf("Could not send mail message: %s", err)
- response.ErrorJson(w, 500, err.Error())
- return
- }
-
- response.SuccessJson(w, 200, &SentSuccessfullJson{
- FormId: a.FormObj.Id,
- SendTime: time.Now().Unix(),
- })
-}
diff --git a/config/config.go b/config/config.go
index 2d8be08..378846b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -6,24 +6,26 @@ import (
log "github.com/sirupsen/logrus"
"os"
"path/filepath"
+ "time"
)
// Config represents the global config object struct
type Config struct {
Loglevel string `fig:"loglevel" default:"debug"`
- Api struct {
- Addr string `fig:"bind_addr"`
- Port uint32 `fig:"port" default:"8765"`
+ Forms struct {
+ Path string `fig:"path" validate:"required"`
}
- Forms struct {
- Path string `fig:"path" validate:"required"`
- MaxLength int64 `fig:"maxlength" default:"1024000"`
+ Server struct {
+ Addr string `fig:"bind_addr"`
+ Port uint32 `fig:"port" default:"8765"`
+ Timeout time.Duration `fig:"timeout" default:"15s"`
+ RequestLimit string `fig:"request_limit" default:"10M"`
}
}
// NewConfig returns a new Config object and fails if the configuration was not found or
// has bad syntax
-func NewConfig() Config {
+func NewConfig() *Config {
l := log.WithFields(log.Fields{
"action": "config.NewConfig",
})
@@ -46,5 +48,5 @@ func NewConfig() Config {
os.Exit(1)
}
- return confObj
+ return &confObj
}
diff --git a/form/form.go b/form/form.go
index b956a5b..a884d30 100644
--- a/form/form.go
+++ b/form/form.go
@@ -4,7 +4,6 @@ import (
"fmt"
"github.com/cyphar/filepath-securejoin"
"github.com/kkyr/fig"
- log "github.com/sirupsen/logrus"
"github.com/wneessen/js-mailer/config"
"os"
)
@@ -46,30 +45,25 @@ type Form struct {
type ValidationField struct {
Name string `fig:"name" validate:"required"`
Type string `fig:"type"`
+ Value string `fig:"value"`
Required bool `fig:"required"`
}
// NewForm returns a new Form object to the caller. It fails with an error when
// the form is question wasn't found or does not fulfill the syntax requirements
func NewForm(c *config.Config, i string) (Form, error) {
- l := log.WithFields(log.Fields{
- "action": "form.NewForm",
- })
formPath, err := securejoin.SecureJoin(c.Forms.Path, fmt.Sprintf("%s.json", i))
if err != nil {
- l.Errorf("Failed to securely join forms path and form id")
- return Form{}, fmt.Errorf("not a valid form id")
+ return Form{}, fmt.Errorf("failed to securely join forms path and form id")
}
_, err = os.Stat(formPath)
if err != nil {
- l.Errorf("Failed to stat form config: %s", err)
- return Form{}, fmt.Errorf("not a valid form id")
+ return Form{}, fmt.Errorf("failed to stat form config: %s", err)
}
var formObj Form
if err := fig.Load(&formObj, fig.File(fmt.Sprintf("%s.json", i)),
fig.Dirs(c.Forms.Path)); err != nil {
- l.Errorf("Failed to read form config: %s", err)
- return Form{}, fmt.Errorf("Not a valid form id")
+ return Form{}, fmt.Errorf("failed to read form config: %s", err)
}
return formObj, nil
diff --git a/go.mod b/go.mod
index 2a8ecee..cf478a2 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,8 @@ require (
github.com/cyphar/filepath-securejoin v0.2.3
github.com/go-mail/mail v2.3.1+incompatible
github.com/kkyr/fig v0.2.0
+ github.com/labstack/echo/v4 v4.5.0
+ github.com/labstack/gommon v0.3.0
github.com/sirupsen/logrus v1.8.1
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
diff --git a/go.sum b/go.sum
index 3c4a2a8..f3822b5 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,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/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/kkyr/fig v0.2.0 h1:t/5yENaBw8ATXbQSWpPqwXLCn6wdhEi6jWXRfUgytZI=
github.com/kkyr/fig v0.2.0/go.mod h1:iqSnedEGFSofGzaB8p34xOhX1ppE1kMulSmJLZ2tNnw=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -16,6 +18,17 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/labstack/echo/v4 v4.5.0 h1:JXk6H5PAw9I3GwizqUHhYyS4f45iyGebR/c1xNCeOCY=
+github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
+github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
@@ -29,12 +42,19 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -44,17 +64,32 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/logging/logging.go b/logging/logging.go
deleted file mode 100644
index 11faf41..0000000
--- a/logging/logging.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package logging
-
-import (
- log "github.com/sirupsen/logrus"
- "strings"
-)
-
-// InitLogging initializes the logging object and sets some sane defaults
-func InitLogging() {
- log.SetLevel(log.InfoLevel)
- log.SetFormatter(&log.TextFormatter{
- DisableLevelTruncation: true,
- DisableColors: false,
- FullTimestamp: true,
- TimestampFormat: "2006-01-02 15:04:05 -0700",
- })
-}
-
-// SetLogLevel sets the correspoding log level for the global logging object
-func SetLogLevel(l string) {
- if l == "" {
- return
- }
- switch strings.ToLower(l) {
- case "debug":
- log.SetLevel(log.DebugLevel)
- case "info":
- log.SetLevel(log.InfoLevel)
- case "warn":
- log.SetLevel(log.WarnLevel)
- case "error":
- log.SetLevel(log.ErrorLevel)
- default:
- log.SetLevel(log.InfoLevel)
- }
-}
diff --git a/main.go b/main.go
index b6375b7..dbc3301 100644
--- a/main.go
+++ b/main.go
@@ -3,61 +3,27 @@ package main
import (
"fmt"
"github.com/ReneKroon/ttlcache/v2"
- log "github.com/sirupsen/logrus"
- "github.com/wneessen/js-mailer/apirequest"
"github.com/wneessen/js-mailer/config"
- "github.com/wneessen/js-mailer/logging"
- "net/http"
+ "github.com/wneessen/js-mailer/server"
"os"
"time"
)
-// VERSION is the global version string contstant
-const VERSION = "0.1.4"
-
-// serve acts as main web service server muxer/handler for incoming HTTP requests
-func serve(c *config.Config) {
- l := log.WithFields(log.Fields{
- "action": "main.serve",
- })
- l.Infof("Starting up js-mailer v%s server API on port %s:%d", VERSION, c.Api.Addr, c.Api.Port)
+func main() {
+ confObj := config.NewConfig()
- // Initialize the cache
cacheObj := ttlcache.NewCache()
- if err := cacheObj.SetTTL(time.Duration(10 * time.Minute)); err != nil {
- l.Errorf("Failed to set TTL on cache object: %s", err)
+ if err := cacheObj.SetTTL(10 * time.Minute); err != nil {
+ _, _ = fmt.Fprintf(os.Stderr, "ERROR: Failed to set default TTL on cache: %s", err)
+ os.Exit(1)
}
defer func() {
- if err := cacheObj.Close(); err != nil {
- l.Errorf("Failed to close cache object: %s", err)
- }
+ _ = cacheObj.Close()
}()
- // Initialize the Api request object
- apiReq := &apirequest.ApiRequest{
+ srv := server.Srv{
Cache: cacheObj,
- Config: c,
- }
- httpMux := http.NewServeMux()
- httpMux.HandleFunc("/", apiReq.RequestHandler)
- httpSrv := &http.Server{
- ReadTimeout: 5 * time.Second,
- WriteTimeout: 5 * time.Second,
- IdleTimeout: 15 * time.Second,
- ReadHeaderTimeout: 5 * time.Second,
- Handler: http.TimeoutHandler(httpMux, time.Second*15, ""),
- Addr: fmt.Sprintf("%s:%d", c.Api.Addr, c.Api.Port),
+ Config: confObj,
}
- if err := httpSrv.ListenAndServe(); err != nil {
- l.Errorf("Failed to start server: %s", err)
- os.Exit(1)
- }
-}
-
-// main is the main function
-func main() {
- logging.InitLogging()
- confObj := config.NewConfig()
- logging.SetLogLevel(confObj.Loglevel)
- serve(&confObj)
+ srv.Start()
}
diff --git a/response/error.go b/response/error.go
index 6d1fd02..3d3393e 100644
--- a/response/error.go
+++ b/response/error.go
@@ -1,56 +1,45 @@
package response
import (
- "encoding/json"
- log "github.com/sirupsen/logrus"
+ "github.com/labstack/echo/v4"
"net/http"
)
-// HttpStatusMsg is a mapping of HTTP status codes to their corresponding status message
-var HttpStatusMsg = map[int]string{
- 200: "Ok",
- 400: "Bad Request",
- 401: "Unauthorized",
- 404: "Not Found",
- 500: "Internal Server Error",
+// ErrorObj is the structure of an error
+type ErrorObj struct {
+ Message string
+ Data interface{}
}
-// ErrorResponseJson reflects the JSON response for a failed request
-type ErrorResponseJson struct {
+// ErrorResponse reflects the default JSON structure of an error response
+type ErrorResponse struct {
StatusCode int `json:"status_code"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
ErrorData interface{} `json:"error_data,omitempty"`
}
-// ErrorJson writes a ErrorResponseJson with no ErrorData to the http.ResponseWriter in case
-// an error response is needed as result to the HTTP request
-func ErrorJson(w http.ResponseWriter, c int, m string) {
- errorJson(w, c, m, nil)
-}
-
-// ErrorJsonData writes a ErrorResponseJson with ErrorData to the http.ResponseWriter in case
-// an error response is needed as result to the HTTP request
-func ErrorJsonData(w http.ResponseWriter, c int, m string, d interface{}) {
- errorJson(w, c, m, d)
-}
-
-// errorJson writes a ErrorResponseJson to the http.ResponseWriter in case
-// an error response is needed as result to the HTTP request
-func errorJson(w http.ResponseWriter, c int, m string, d interface{}) {
- l := log.WithFields(log.Fields{
- "action": "http_error.ErrorJson",
- })
- l.Debugf("Request failed with code %d (%s): %s", c, HttpStatusMsg[c], m)
- errorJson := ErrorResponseJson{
- StatusCode: c,
- Status: HttpStatusMsg[c],
- ErrorMessage: m,
- ErrorData: d,
+// CustomError is a custom error handler for the echo.NewHTTPError function
+func CustomError(err error, c echo.Context) {
+ errResp := &ErrorResponse{
+ StatusCode: http.StatusInternalServerError,
+ }
+ if he, ok := err.(*echo.HTTPError); ok {
+ errResp.StatusCode = he.Code
+ errResp.Status = http.StatusText(errResp.StatusCode)
+ if em, ok := he.Message.(*echo.HTTPError); ok {
+ errResp.ErrorMessage = em.Message.(string)
+ }
+ if em, ok := he.Message.(*ErrorObj); ok {
+ errResp.ErrorMessage = em.Message
+ errResp.ErrorData = em.Data
+ }
+ if em, ok := he.Message.(string); ok {
+ errResp.ErrorMessage = em
+ }
}
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(c)
- if err := json.NewEncoder(w).Encode(errorJson); err != nil {
- l.Errorf("Failed to write error response JSON: %s", err)
+
+ if err := c.JSON(errResp.StatusCode, errResp); err != nil {
+ c.Logger().Errorf("Failed to render error JSON: %s", err)
}
}
diff --git a/response/success.go b/response/success.go
index d08f00e..f2b4b8d 100644
--- a/response/success.go
+++ b/response/success.go
@@ -1,32 +1,8 @@
package response
-import (
- "encoding/json"
- log "github.com/sirupsen/logrus"
- "net/http"
-)
-
-// SuccessResponseJson reflects the HTTP response JSON for a successful request
-type SuccessResponseJson struct {
+// SuccessResponse reflects the default JSON structure of an error response
+type SuccessResponse struct {
StatusCode int `json:"status_code"`
Status string `json:"status"`
- Data interface{} `json:"data"`
-}
-
-// SuccessJson writes a SuccessResponseJson struct to the http.ResponseWriter
-func SuccessJson(w http.ResponseWriter, c int, d interface{}) {
- l := log.WithFields(log.Fields{
- "action": "http_error.ErrorJson",
- })
- l.Debug("Request successfully completed")
- successMsg := SuccessResponseJson{
- StatusCode: c,
- Status: HttpStatusMsg[c],
- Data: d,
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(c)
- if err := json.NewEncoder(w).Encode(successMsg); err != nil {
- l.Errorf("Failed to write success response JSON: %s", err)
- }
+ Data interface{} `json:"data,omitempty"`
}
diff --git a/server/router_api.go b/server/router_api.go
new file mode 100644
index 0000000..1558719
--- /dev/null
+++ b/server/router_api.go
@@ -0,0 +1,22 @@
+package server
+
+import (
+ "github.com/wneessen/js-mailer/api"
+)
+
+// RouterApi registers the JSON API routes with echo
+func (s *Srv) RouterApi() {
+ apiRoute := api.Route{
+ Cache: s.Cache,
+ Config: s.Config,
+ }
+ ag := s.Echo.Group("/api/v1")
+
+ // API routes
+ ag.Add("GET", "/ping", apiRoute.Ping)
+ ag.Add("GET", "/token", apiRoute.GetToken)
+ ag.Add("POST", "/token", apiRoute.GetToken)
+ ag.Add("POST", "/send/:fid/:token", apiRoute.SendForm,
+ apiRoute.SendFormBindForm, apiRoute.SendFormReqFields, apiRoute.SendFormHoneypot,
+ apiRoute.SendFormHcaptcha, apiRoute.SendFormRecaptcha, apiRoute.SendFormCheckToken)
+}
diff --git a/server/server.go b/server/server.go
new file mode 100644
index 0000000..3b35ca1
--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,90 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "github.com/wneessen/js-mailer/response"
+ "net/http"
+ "os"
+ "os/signal"
+ "time"
+
+ "github.com/ReneKroon/ttlcache/v2"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "github.com/labstack/gommon/log"
+ "github.com/wneessen/js-mailer/config"
+)
+
+// VERSION is the global version string contstant
+const VERSION = "0.2.0"
+
+// Srv represents the server object
+type Srv struct {
+ Cache *ttlcache.Cache
+ Config *config.Config
+ Echo *echo.Echo
+}
+
+// Start initalizes and starts the web service
+func (s *Srv) Start() {
+ s.Echo = echo.New()
+
+ // Settings
+ s.Echo.HideBanner = true
+ s.Echo.HidePort = true
+ s.Echo.Debug = s.Config.Loglevel == "debug"
+ s.Echo.Server.ReadTimeout = s.Config.Server.Timeout
+ s.Echo.Server.WriteTimeout = s.Config.Server.Timeout
+ s.Echo.IPExtractor = echo.ExtractIPFromRealIPHeader()
+ s.Echo.HTTPErrorHandler = response.CustomError
+ s.LogLevel()
+
+ // Register routes
+ s.RouterApi()
+
+ // Middlewares
+ s.Echo.Use(middleware.Recover())
+ s.Echo.Use(middleware.Logger())
+ s.Echo.Use(middleware.BodyLimit(s.Config.Server.RequestLimit))
+ s.Echo.Use(middleware.CORS())
+
+ // Start server
+ go func() {
+ s.Echo.Logger.Infof("Starting js-mailer v%s on: %s", VERSION,
+ fmt.Sprintf("%s:%d", s.Config.Server.Addr, s.Config.Server.Port))
+ err := s.Echo.Start(fmt.Sprintf("%s:%d", s.Config.Server.Addr, s.Config.Server.Port))
+ if err != nil && err != http.ErrServerClosed {
+ s.Echo.Logger.Errorf("Failed to start up web service: %s", err)
+ os.Exit(1)
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds.
+ // Use a buffered channel to avoid missing signals as recommended for signal.Notify
+ quitSig := make(chan os.Signal, 1)
+ signal.Notify(quitSig, os.Interrupt)
+ <-quitSig
+ shutdownCtx, ctxCancel := context.WithTimeout(context.Background(), time.Second*5)
+ defer ctxCancel()
+ if err := s.Echo.Shutdown(shutdownCtx); err != nil {
+ s.Echo.Logger.Errorf("Failed to shut down gracefully: %s", err)
+ os.Exit(1)
+ }
+}
+
+// LogLevel sets the log level based on the given loglevel in the config object
+func (s *Srv) LogLevel() {
+ switch s.Config.Loglevel {
+ case "debug":
+ s.Echo.Logger.SetLevel(log.DEBUG)
+ case "info":
+ s.Echo.Logger.SetLevel(log.INFO)
+ case "warn":
+ s.Echo.Logger.SetLevel(log.WARN)
+ case "error":
+ s.Echo.Logger.SetLevel(log.ERROR)
+ default:
+ s.Echo.Logger.SetLevel(log.INFO)
+ }
+}
diff --git a/validation/field.go b/validation/field.go
deleted file mode 100644
index 7754229..0000000
--- a/validation/field.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package validation
-
-import (
- "fmt"
- log "github.com/sirupsen/logrus"
- "github.com/wneessen/js-mailer/form"
- "net/http"
- "regexp"
-)
-
-// Field validates the form field based on its configured type
-func Field(r *http.Request, f *form.ValidationField) error {
- l := log.WithFields(log.Fields{
- "action": "validation.Field",
- "fieldName": f.Name,
- })
-
- if f.Required && r.Form.Get(f.Name) == "" {
- l.Debugf("Form is missing required field: %s", f.Name)
- return fmt.Errorf("field is required, but missing")
- }
-
- switch f.Type {
- case "text":
- return nil
- case "email":
- mailRegExp, err := regexp.Compile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
- if err != nil {
- l.Errorf("Failed to compile email comparison regexp: %s", err)
- return nil
- }
- if !mailRegExp.Match([]byte(r.Form.Get(f.Name))) {
- l.Debugf("Form field is expected to be of type email but does not match this requirementd: %s", f.Name)
- return fmt.Errorf("field is expected to be of type email, but does not match")
- }
- case "number":
- numRegExp, err := regexp.Compile("^[0-9]+$")
- if err != nil {
- l.Errorf("Failed to compile email comparison regexp: %s", err)
- return nil
- }
- if !numRegExp.Match([]byte(r.Form.Get(f.Name))) {
- l.Debugf("Form field is expected to be of type number but does not match this requirementd: %s", f.Name)
- return fmt.Errorf("field is expected to be of type number, but does not match")
- }
- case "bool":
- boolRegExp, err := regexp.Compile("^(?i)(true|false|0|1)$")
- if err != nil {
- l.Errorf("Failed to compile boolean comparison regexp: %s", err)
- return nil
- }
- if !boolRegExp.Match([]byte(r.Form.Get(f.Name))) {
- l.Debugf("Form field is expected to be of type boolean but does not match this requirementd: %s", f.Name)
- return fmt.Errorf("field is expected to be of type bool, but does not match")
- }
- default:
- return nil
- }
-
- return nil
-}
diff --git a/validation/hcaptcha.go b/validation/hcaptcha.go
deleted file mode 100644
index 81003a1..0000000
--- a/validation/hcaptcha.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package validation
-
-import (
- "bytes"
- "encoding/json"
- log "github.com/sirupsen/logrus"
- "net/http"
- "net/url"
-)
-
-// HcaptchaResponseJson reflect the API response from hCaptcha
-type HcaptchaResponseJson struct {
- Success bool `json:"success"`
- ChallengeTimestamp string `json:"challenge_ts"`
- Hostname string `json:"hostname"`
-}
-
-// Hcaptcha validates the hCaptcha challenge against the hCaptcha API
-func Hcaptcha(c, s string) bool {
- l := log.WithFields(log.Fields{
- "action": "validation.Hcaptcha",
- })
-
- // Create a HTTP request
- postData := url.Values{
- "response": {c},
- "secret": {s},
- }
- httpResp, err := http.PostForm("https://hcaptcha.com/siteverify", postData)
- if err != nil {
- l.Errorf("an error occurred creating new HTTP POST request: %v", err)
- return false
- }
-
- var respBody bytes.Buffer
- _, err = respBody.ReadFrom(httpResp.Body)
- if err != nil {
- l.Errorf("Failed to read response body: %s", err)
- return false
- }
- if httpResp.StatusCode == http.StatusOK {
- var hcapResp HcaptchaResponseJson
- if err := json.Unmarshal(respBody.Bytes(), &hcapResp); err != nil {
- l.Errorf("Failed to unmarshal response JSON: %s", err)
- return false
- }
- return hcapResp.Success
- }
-
- return false
-}
diff --git a/validation/recaptcha.go b/validation/recaptcha.go
deleted file mode 100644
index 4bf3a8b..0000000
--- a/validation/recaptcha.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package validation
-
-import (
- "bytes"
- "encoding/json"
- log "github.com/sirupsen/logrus"
- "net/http"
- "net/url"
-)
-
-// RecaptchaResponseJson reflect the API response from hCaptcha
-type RecaptchaResponseJson struct {
- Success bool `json:"success"`
- ChallengeTimestamp string `json:"challenge_ts"`
- Hostname string `json:"hostname"`
-}
-
-// Recaptcha validates the reCaptcha challenge against the Google API
-func Recaptcha(c, s string) bool {
- l := log.WithFields(log.Fields{
- "action": "validation.Recaptcha",
- })
-
- // Create a HTTP request
- postData := url.Values{
- "response": {c},
- "secret": {s},
- }
- httpResp, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", postData)
- if err != nil {
- l.Errorf("an error occurred creating new HTTP POST request: %v", err)
- return false
- }
-
- var respBody bytes.Buffer
- _, err = respBody.ReadFrom(httpResp.Body)
- if err != nil {
- l.Errorf("Failed to read response body: %s", err)
- return false
- }
- if httpResp.StatusCode == http.StatusOK {
- var recapResp RecaptchaResponseJson
- if err := json.Unmarshal(respBody.Bytes(), &recapResp); err != nil {
- l.Errorf("Failed to unmarshal response JSON: %s", err)
- return false
- }
- return recapResp.Success
- }
-
- return false
-}