Skip to content

Commit

Permalink
Merge pull request #5 from wneessen/v0.1.3_reCaptcha
Browse files Browse the repository at this point in the history
v0.1.3: Added reCaptcha v2 support
  • Loading branch information
wneessen authored Aug 11, 2021
2 parents 1aa593d + d531d61 commit c141a90
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 51 deletions.
118 changes: 76 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

[![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/js-mailer)](https://goreportcard.com/report/github.com/wneessen/js-mailer) [![Build Status](https://api.cirrus-ci.com/github/wneessen/js-mailer.svg)](https://cirrus-ci.com/github/wneessen/js-mailer)

JS-Mailer is a simple webservice, that allows JavaScript-based websites to easily send form data, by providing a
simple API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.
JS-Mailer is a simple webservice, that allows JavaScript-based websites to easily send form data, by providing a simple
API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.

## Features

* Single-binary webservice
* Multi-form support
* Multiple recipients per form
Expand All @@ -16,11 +17,12 @@ simple API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.
* Limit form access to specific domains
* Per-form mail server configuration
* hCaptcha support
* reCaptcha v2 support

### Planed features

* [ ] Form field-type validation
* [ ] Form body templates (possibly HTML)
* [ ] gCaptcha support

## Installation

Expand All @@ -35,8 +37,9 @@ There is a ready-to-use Docker image hosted on Github.
```shell
$ docker run -p 8765:8765 -v /etc/js-mailer:/etc/js-mailer ghcr.io/wneessen/js-mailer:main
```

## Configuration

### Server configuration

The server configuration, by default, is searched for in `/etc/js-mailer/js-mailer.json`. The JSON syntax is very basic
Expand All @@ -57,33 +60,50 @@ and comes with sane defaults.
```

* `api (type: struct)`: The struct for the web api configuration
* `bind_addr (type: string)`: The IP address to bind the web service to
* `port (type: uint)`: The port for the webservice to listen on
* `bind_addr (type: string)`: The IP address to bind the web service to
* `port (type: uint)`: The port for the webservice to listen on
* `forms (type: struct)`: The struct for the forms configuration
* `path (type: string)`: The path in which `js-mailer` will look for form configuration JSON files
* `maxlength (type: int64)`: Maximum length in bytes of memory that will be read from the form data HTTP header
* `loglevel (type: string)`: The log level for the web service

### Form configuration
Each form has its own configuration file. The configuration is searched in the forms path and are named by its id.
Again the JSON syntax of the form configuration is very simple, yet flexible.

Each form has its own configuration file. The configuration is searched in the forms path and are named by its id. Again
the JSON syntax of the form configuration is very simple, yet flexible.

```json
{
"id": "test_form",
"secret": "SuperSecretsString",
"recipients": ["[email protected]"],
"recipients": [
"[email protected]"
],
"sender": "[email protected]",
"domains": ["www.example.com", "example.com"],
"domains": [
"www.example.com",
"example.com"
],
"content": {
"subject": "New message through the www.example.com contact form",
"fields": ["name", "email", "message"],
"required_fields": ["name", "email"],
"fields": [
"name",
"email",
"message"
],
"required_fields": [
"name",
"email"
],
"honeypot": "street"
},
"hcaptcha": {
"enabled": true,
"secret_key": "0x01234567890"
"enabled": true,
"secret_key": "0x01234567890"
},
"recaptcha": {
"enabled": true,
"secret_key": "0x01234567890"
},
"server": {
"host": "mail.example.com",
Expand All @@ -95,25 +115,29 @@ Again the JSON syntax of the form configuration is very simple, yet flexible.
}
}
```

* `id (type: string)`: The id of the form (will be looked for in the `formid` parameter of the token request)
* `secret (type: string)`: Secret for the form. This will be used for the token generation
* `recipients (type: []string)`: List of recipients, that should receive the mails with the submitted form data
* `domains (type: []string)`: List of origin domains, that are allowed to use this form
* `content (type: struct)`: The struct for the mail content configuration
* `subject (type: string)`: Subject for the mail notification of the form submission
* `fields (type: []string)`: List of field names that should show up in the mail notification
* `required_fields (type: []string)`: List of field names that are required to submitted
* `honeypot (type: string)`: Name of the honeypot field, that is expected to be empty (Anti-SPAM)
* `subject (type: string)`: Subject for the mail notification of the form submission
* `fields (type: []string)`: List of field names that should show up in the mail notification
* `required_fields (type: []string)`: List of field names that are required to submitted
* `honeypot (type: string)`: Name of the honeypot field, that is expected to be empty (Anti-SPAM)
* `hcaptcha (type: struct)`: The struct for the forms hCaptcha configuration
* `enabled (type: bool)`: Enable hCaptcha challenge-response validation
* `secret_key (type: string)`: Your hCaptcha secret key
* `enabled (type: bool)`: Enable hCaptcha challenge-response validation
* `secret_key (type: string)`: Your hCaptcha secret key
* `recaptcha (type: struct)`: The struct for the forms reCaptcha configuration
* `enabled (type: bool)`: Enable reCaptcha challenge-response validation
* `secret_key (type: string)`: Your reCaptcha secret key
* `server (type: struct)`: The struct for the forms mail server configuration
* `host (type: string)`: Hostname of the sending mail server
* `port (type: uint32)`: Port to connect to on the sending mail server
* `username (type: string)`: Username for the mail server authentication
* `password (type: string)`: Password for the mail server authentication
* `timeout (type: duration)`: Timeout duration for the mail server connection
* `force_tls (type: boolean)`: If set to true, the mail server connection will require mandatory TLS
* `host (type: string)`: Hostname of the sending mail server
* `port (type: uint32)`: Port to connect to on the sending mail server
* `username (type: string)`: Username for the mail server authentication
* `password (type: string)`: Password for the mail server authentication
* `timeout (type: duration)`: Timeout duration for the mail server connection
* `force_tls (type: boolean)`: If set to true, the mail server connection will require mandatory TLS

## Workflow

Expand All @@ -125,36 +149,41 @@ is submitted, the API will then validate that all submitted data is correct and
recipients.

## API responses

The API basically responds with two different types of JSON objects. A `success` response or an `error` response.

### Success response

The succss response JSON struct is very simple:

```json
{
"status_code": 200,
"status": "Ok",
"data": {}
"status_code": 200,
"status": "Ok",
"data": {}
}
```

* `status_code (type: uint32)`: The HTTP status code of the success response
* `status (type: string)`: The HTTP status string of the success response
* `data (type: object)`: An object with abritrary data, based on the type of response

#### Successful token retrieval data object

The `data` object of the success response for a successful token retrieval looks like this:

```json
{
"token": "5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
"form_id": "test_form",
"create_time": 1628670201,
"expire_time": 1628670801,
"url": "https://jsmailer.example.com/api/v1/send/test_form/5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
"enc_type": "multipart/form-data",
"method": "post"
"token": "5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
"form_id": "test_form",
"create_time": 1628670201,
"expire_time": 1628670801,
"url": "https://jsmailer.example.com/api/v1/send/test_form/5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
"enc_type": "multipart/form-data",
"method": "post"
}
```

* `token (type: string)`: The security token of this send request
* `form_id (type: string)`: The form id of the current form (for reference or automatic inclusion via JS)
* `create_time (type: int64)`: The epoch timestamp when the token was created
Expand All @@ -164,29 +193,34 @@ The `data` object of the success response for a successful token retrieval looks
* `method (type: string)`: The method for your form

#### Sent successful data object

The `data` object of the success response for a successfully sent message looks like this:

The API response to a send request (`/api/v1/send/<formid>/<token>`) looks like this:

```json
{
"form_id": "test_form",
"send_time": 1628670331
"form_id": "test_form",
"send_time": 1628670331
}
```

* `form_id (type: string)`: The form id of the current form (for reference)
* `send_time (type: int64)`: The epoch timestamp when the message was sent

### Error response

The error response JSON struct is also very simple:

```json
{
"status_code": 404,
"status": "Not Found",
"error_message": "Validation failed",
"error_data": "Not a valid send URL"
"status_code": 404,
"status": "Not Found",
"error_message": "Validation failed",
"error_data": "Not a valid send URL"
}
```

* `status_code (type: uint32)`: The HTTP status code of the success response
* `status (type: string)`: The HTTP status string of the success response
* `error_message (type: string)`: The general error message why this request failed
Expand Down
11 changes: 11 additions & 0 deletions apirequest/sendform.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ func (a *ApiRequest) SendFormValidate(r *http.Request) (int, error) {
}
}

// Validate reCaptcha if enabled
if formObj.Recaptcha.Enabled {
recapResponse := r.Form.Get("g-recaptcha-response")
if recapResponse == "" {
return 400, fmt.Errorf("missing reCaptcha response")
}
if ok := validation.RecaptchaValidate(recapResponse, formObj.Recaptcha.SecretKey); !ok {
return 400, fmt.Errorf("reCaptcha challenge-response validation failed")
}
}

// Check the token
reqOrigin := r.Header.Get("origin")
if reqOrigin == "" {
Expand Down
4 changes: 4 additions & 0 deletions form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type Form struct {
Enabled bool `fig:"enabled"`
SecretKey string `fig:"secret_key"`
}
Recaptcha struct {
Enabled bool `fig:"enabled"`
SecretKey string `fig:"secret_key"`
}
Content struct {
Subject string
Fields []string
Expand Down
15 changes: 6 additions & 9 deletions validation/hcaptcha.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package validation

import (
"bufio"
"bytes"
"encoding/json"
log "github.com/sirupsen/logrus"
"net/http"
Expand All @@ -15,7 +15,7 @@ type HcaptchaResponseJson struct {
Hostname string `json:"hostname"`
}

// HcaptchaValidate validates the hCaptche challenge against the hCaptcha API
// HcaptchaValidate validates the hCaptcha challenge against the hCaptcha API
func HcaptchaValidate(c, s string) bool {
l := log.WithFields(log.Fields{
"action": "validation.HcaptchaValidate",
Expand All @@ -32,18 +32,15 @@ func HcaptchaValidate(c, s string) bool {
return false
}

var respBody []byte
buf := bufio.NewScanner(httpResp.Body)
for buf.Scan() {
respBody = buf.Bytes()
}
if err = buf.Err(); err != nil {
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, &hcapResp); err != nil {
if err := json.Unmarshal(respBody.Bytes(), &hcapResp); err != nil {
l.Errorf("Failed to unmarshal response JSON: %s", err)
return false
}
Expand Down
51 changes: 51 additions & 0 deletions validation/recaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package validation

import (
"bytes"
"encoding/json"
log "github.com/sirupsen/logrus"
"net/http"
"net/url"
)

// HcaptchaResponseJson reflect the API response from hCaptcha
type RecaptchaResponseJson struct {
Success bool `json:"success"`
ChallengeTimestamp string `json:"challenge_ts"`
Hostname string `json:"hostname"`
}

// RecaptchaValidate validates the reCaptcha challenge against the Google API
func RecaptchaValidate(c, s string) bool {
l := log.WithFields(log.Fields{
"action": "validation.RecaptchaValidate",
})

// 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
}

0 comments on commit c141a90

Please sign in to comment.