diff --git a/_examples/miscellaneous/hcaptcha/hosts b/_examples/miscellaneous/hcaptcha/hosts
new file mode 100644
index 00000000..d11c711b
--- /dev/null
+++ b/_examples/miscellaneous/hcaptcha/hosts
@@ -0,0 +1,3 @@
+# https://docs.hcaptcha.com/#localdev
+# Add to the end of your hosts file, e.g. on windows: C:/windows/system32/drivers/etc/hosts
+127.0.0.1 yourdomain.com
diff --git a/_examples/miscellaneous/hcaptcha/main.go b/_examples/miscellaneous/hcaptcha/main.go
new file mode 100644
index 00000000..87e917ac
--- /dev/null
+++ b/_examples/miscellaneous/hcaptcha/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "os"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/middleware/hcaptcha"
+)
+
+// Get the following values from: https://dashboard.hcaptcha.com
+// Also, check: https://docs.hcaptcha.com/#localdev to test on local environment.
+var (
+ siteKey = os.Getenv("HCAPTCHA-SITE-KEY")
+ secretKey = os.Getenv("HCAPTCHA-SECRET-KEY")
+)
+
+func main() {
+ app := iris.New()
+ app.RegisterView(iris.HTML("./templates", ".html"))
+
+ hCaptcha := hcaptcha.New(secretKey)
+ app.Get("/register", registerForm)
+ app.Post("/register", hCaptcha, register) // See `hcaptcha.SiteVerify` for manual validation too.
+
+ app.Logger().Infof("SiteKey = %s\tSecretKey = %s",
+ siteKey, secretKey)
+
+ // GET: http://yourdomain.com/register
+ app.Listen(":80")
+}
+
+func register(ctx iris.Context) {
+ hcaptchaResp, ok := hcaptcha.Get(ctx)
+ if !ok {
+ ctx.StatusCode(iris.StatusUnauthorized)
+ ctx.WriteString("Are you a bot?")
+ return
+ }
+
+ ctx.Writef("Register action here...action was asked by a Human.\nResponse value is: %#+v", hcaptchaResp)
+}
+
+func registerForm(ctx iris.Context) {
+ ctx.ViewData("SiteKey", siteKey)
+ ctx.View("register_form.html")
+}
diff --git a/_examples/miscellaneous/hcaptcha/templates/register_form.html b/_examples/miscellaneous/hcaptcha/templates/register_form.html
new file mode 100644
index 00000000..26a0cdc0
--- /dev/null
+++ b/_examples/miscellaneous/hcaptcha/templates/register_form.html
@@ -0,0 +1,18 @@
+
+
+
+ hCaptcha Demo
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go
index e5ee0e82..56be5a1f 100644
--- a/core/memstore/memstore.go
+++ b/core/memstore/memstore.go
@@ -21,9 +21,9 @@ type (
}
// Entry is the entry of the context storage Store - .Values()
Entry struct {
- Key string
- ValueRaw interface{}
- immutable bool // if true then it can't change by its caller.
+ Key string `json:"key" msgpack:"key" yaml:"Key" toml:"Value"`
+ ValueRaw interface{} `json:"value" msgpack:"value" yaml:"Value" toml:"Value"`
+ immutable bool // if true then it can't change by its caller.
}
// Store is a collection of key-value entries with immutability capabilities.
diff --git a/core/memstore/memstore_test.go b/core/memstore/memstore_test.go
index 91233c05..686d48e4 100644
--- a/core/memstore/memstore_test.go
+++ b/core/memstore/memstore_test.go
@@ -2,11 +2,14 @@
package memstore
import (
+ "bytes"
+ "encoding/json"
+ "fmt"
"testing"
)
type myTestObject struct {
- name string
+ Name string `json:"name"`
}
func TestMuttable(t *testing.T) {
@@ -15,10 +18,10 @@ func TestMuttable(t *testing.T) {
// slice
p.Set("slice", []myTestObject{{"value 1"}, {"value 2"}})
v := p.Get("slice").([]myTestObject)
- v[0].name = "modified"
+ v[0].Name = "modified"
vv := p.Get("slice").([]myTestObject)
- if vv[0].name != "modified" {
+ if vv[0].Name != "modified" {
t.Fatalf("expected slice to be muttable but caller was not able to change its value")
}
@@ -29,7 +32,7 @@ func TestMuttable(t *testing.T) {
vMap["key 1"] = myTestObject{"modified"}
vvMap := p.Get("map").(map[string]myTestObject)
- if vvMap["key 1"].name != "modified" {
+ if vvMap["key 1"].Name != "modified" {
t.Fatalf("expected map to be muttable but caller was not able to change its value")
}
@@ -38,10 +41,10 @@ func TestMuttable(t *testing.T) {
// we expect pointer here, as we set it.
vObjP := p.Get("objp").(*myTestObject)
- vObjP.name = "modified"
+ vObjP.Name = "modified"
vvObjP := p.Get("objp").(*myTestObject)
- if vvObjP.name != "modified" {
+ if vvObjP.Name != "modified" {
t.Fatalf("expected objp to be muttable but caller was able to change its value")
}
}
@@ -52,10 +55,10 @@ func TestImmutable(t *testing.T) {
// slice
p.SetImmutable("slice", []myTestObject{{"value 1"}, {"value 2"}})
v := p.Get("slice").([]myTestObject)
- v[0].name = "modified"
+ v[0].Name = "modified"
vv := p.Get("slice").([]myTestObject)
- if vv[0].name == "modified" {
+ if vv[0].Name == "modified" {
t.Fatalf("expected slice to be immutable but caller was able to change its value")
}
@@ -65,17 +68,17 @@ func TestImmutable(t *testing.T) {
vMap["key 1"] = myTestObject{"modified"}
vvMap := p.Get("map").(map[string]myTestObject)
- if vvMap["key 1"].name == "modified" {
+ if vvMap["key 1"].Name == "modified" {
t.Fatalf("expected map to be immutable but caller was able to change its value")
}
// object value, it's immutable at all cases.
p.SetImmutable("obj", myTestObject{"value"})
vObj := p.Get("obj").(myTestObject)
- vObj.name = "modified"
+ vObj.Name = "modified"
vvObj := p.Get("obj").(myTestObject)
- if vvObj.name == "modified" {
+ if vvObj.Name == "modified" {
t.Fatalf("expected obj to be immutable but caller was able to change its value")
}
@@ -85,10 +88,10 @@ func TestImmutable(t *testing.T) {
// so it can't be changed by-design
vObjP := p.Get("objp").(myTestObject)
- vObjP.name = "modified"
+ vObjP.Name = "modified"
vvObjP := p.Get("objp").(myTestObject)
- if vvObjP.name == "modified" {
+ if vvObjP.Name == "modified" {
t.Fatalf("expected objp to be immutable but caller was able to change its value")
}
}
@@ -100,13 +103,13 @@ func TestImmutableSetOnlyWithSetImmutable(t *testing.T) {
p.Set("objp", &myTestObject{"modified"})
vObjP := p.Get("objp").(myTestObject)
- if vObjP.name == "modified" {
+ if vObjP.Name == "modified" {
t.Fatalf("caller should not be able to change the immutable entry with a simple `Set`")
}
p.SetImmutable("objp", &myTestObject{"value with SetImmutable"})
vvObjP := p.Get("objp").(myTestObject)
- if vvObjP.name != "value with SetImmutable" {
+ if vvObjP.Name != "value with SetImmutable" {
t.Fatalf("caller should be able to change the immutable entry with a `SetImmutable`")
}
}
@@ -119,3 +122,46 @@ func TestGetInt64Default(t *testing.T) {
t.Fatalf("unexpected value of %d", v)
}
}
+
+func TestJSON(t *testing.T) {
+ var p Store
+
+ p.Set("key1", "value1")
+ p.Set("key2", 2)
+ p.Set("key3", myTestObject{Name: "makis"})
+
+ b, err := json.Marshal(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedJSON := []byte(`[{"key":"key1","value":"value1"},{"key":"key2","value":2},{"key":"key3","value":{"name":"makis"}}]`)
+
+ if !bytes.Equal(b, expectedJSON) {
+ t.Fatalf("expected: %s but got: %s", string(expectedJSON), string(b))
+ }
+
+ var newStore Store
+ if err = json.Unmarshal(b, &newStore); err != nil {
+ t.Fatal(err)
+ }
+
+ for i, v := range newStore {
+ expected, got := p.Get(v.Key), v.ValueRaw
+
+ if ex, g := fmt.Sprintf("%v", expected), fmt.Sprintf("%v", got); ex != g {
+ if _, isMap := got.(map[string]interface{}); isMap {
+ // was struct but converted into map (as expected).
+ b1, _ := json.Marshal(expected)
+ b2, _ := json.Marshal(got)
+
+ if !bytes.Equal(b1, b2) {
+ t.Fatalf("[%d] JSON expected: %s but got: %s", i, string(b1), string(b2))
+ }
+
+ continue
+ }
+ t.Fatalf("[%d] expected: %s but got: %s", i, ex, g)
+ }
+ }
+}
diff --git a/middleware/README.md b/middleware/README.md
index 8a2ff130..c8dfb4f2 100644
--- a/middleware/README.md
+++ b/middleware/README.md
@@ -8,6 +8,7 @@ Builtin Handlers
| [HTTP method override](methodoverride) | [iris/middleware/methodoverride/methodoverride_test.go](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go) |
| [profiling (pprof)](pprof) | [iris/_examples/miscellaneous/pprof](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/pprof) |
| [Google reCAPTCHA](recaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recaptcha) |
+| [hCaptcha](hcaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/hcaptcha) |
| [recovery](recover) | [iris/_examples/miscellaneous/recover](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recover) |
Community made
diff --git a/middleware/hcaptcha/hcaptcha.go b/middleware/hcaptcha/hcaptcha.go
new file mode 100644
index 00000000..fc7ecc3f
--- /dev/null
+++ b/middleware/hcaptcha/hcaptcha.go
@@ -0,0 +1,162 @@
+package hcaptcha
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/kataras/iris/v12/context"
+)
+
+var (
+ // ResponseContextKey is the default request's context key that response of a hcaptcha request is kept.
+ ResponseContextKey string = "iris.hcaptcha"
+ // DefaultFailureHandler is the default HTTP handler that is fired on hcaptcha failures.
+ // See `Client.FailureHandler`.
+ DefaultFailureHandler = func(ctx context.Context) {
+ ctx.StopWithStatus(http.StatusTooManyRequests)
+ }
+)
+
+// Client represents the hcaptcha client.
+type Client struct {
+ // FailureHandler if specified, fired when user does not complete hcaptcha successfully.
+ // Failure and error codes information are kept as `Response` type
+ // at the Request's Context key of "hcaptcha".
+ //
+ // Defaults to a handler that writes a status code of 429 (Too Many Requests)
+ // and without additional information.
+ FailureHandler context.Handler
+
+ secret string
+}
+
+// Option declares an option for the hcaptcha client.
+// See `New` package-level function.
+type Option func(*Client)
+
+// Response is the hcaptcha JSON response.
+type Response struct {
+ ChallengeTS string `json:"challenge_ts"`
+ Hostname string `json:"hostname"`
+ ErrorCodes []string `json:"error-codes,omitempty"`
+ Success bool `json:"success"`
+ Credit bool `json:"credit,omitempty"`
+}
+
+// New accepts a hpcatcha secret key and returns a new hcaptcha HTTP Client.
+//
+// Instructions at: https://docs.hcaptcha.com/.
+//
+// See its `Handler` and `SiteVerify` for details.
+func New(secret string, options ...Option) context.Handler {
+ c := &Client{
+ FailureHandler: DefaultFailureHandler,
+ secret: secret,
+ }
+
+ for _, opt := range options {
+ opt(c)
+ }
+
+ return c.Handler
+}
+
+// Handler is the HTTP route middleware featured hcaptcha validation.
+// It calls the `SiteVerify` method and fires the "next" when user completed the hcaptcha successfully,
+// otherwise it calls the Client's `FailureHandler`.
+// The hcaptcha's `Response` (which contains any `ErrorCodes`)
+// is saved on the Request's Context (see `GetResponseFromContext`).
+func (c *Client) Handler(ctx context.Context) {
+ v := SiteVerify(ctx, c.secret)
+ ctx.Values().Set(ResponseContextKey, v)
+ if v.Success {
+ ctx.Next()
+ return
+ }
+
+ if c.FailureHandler != nil {
+ c.FailureHandler(ctx)
+ }
+}
+
+// responseFormValue = "h-captcha-response"
+const apiURL = "https://hcaptcha.com/siteverify"
+
+// SiteVerify accepts an Iris Context and a secret key (https://dashboard.hcaptcha.com/settings).
+// It returns the hcaptcha's `Response`.
+// The `response.Success` reports whether the validation passed.
+// Any errors are passed through the `response.ErrorCodes` field.
+func SiteVerify(ctx context.Context, secret string) (response Response) {
+ generatedResponseID := ctx.FormValue("h-captcha-response")
+
+ if generatedResponseID == "" {
+ response.ErrorCodes = append(response.ErrorCodes,
+ "form[h-captcha-response] is empty")
+ return
+ }
+
+ resp, err := http.DefaultClient.PostForm(apiURL,
+ url.Values{
+ "secret": {secret},
+ "response": {generatedResponseID},
+ },
+ )
+ if err != nil {
+ response.ErrorCodes = append(response.ErrorCodes, err.Error())
+ return
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ response.ErrorCodes = append(response.ErrorCodes, err.Error())
+ return
+ }
+
+ err = json.Unmarshal(body, &response)
+ if err != nil {
+ response.ErrorCodes = append(response.ErrorCodes, err.Error())
+ return
+ }
+
+ return
+}
+
+// Get returns the hcaptcha `Response` of the current request and reports whether was found or not.
+func Get(ctx context.Context) (Response, bool) {
+ v := ctx.Values().Get(ResponseContextKey)
+ if v != nil {
+ if response, ok := v.(Response); ok {
+ return response, true
+ }
+ }
+
+ return Response{}, false
+}
+
+// Script is the hCaptcha's javascript source file that should be incldued in the HTML head or body.
+const Script = "https://hcaptcha.com/1/api.js"
+
+// HTMLForm is the default HTML form for clients.
+// It's totally optional, use your own code for the best possible result depending on your web application.
+// See `ParseForm` and `RenderForm` for more.
+var HTMLForm = ``
+
+// ParseForm parses the `HTMLForm` with the necessary parameters and returns
+// its result for render.
+func ParseForm(dataSiteKey, postActionRelativePath string) string {
+ return fmt.Sprintf(HTMLForm, postActionRelativePath, Script, dataSiteKey)
+}
+
+// RenderForm writes the `HTMLForm` to "w" response writer.
+// See `_examples/basic/register_form.html` example for a custom form instead.
+func RenderForm(ctx context.Context, dataSiteKey, postActionRelativePath string) (int, error) {
+ return ctx.HTML(ParseForm(dataSiteKey, postActionRelativePath))
+}