├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _examples └── basic │ ├── hosts │ ├── main.go │ └── register_form.html ├── go.mod └── hcaptcha.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kataras 4 | # custom: http://iris-go.com/donate 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: kataras 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Version [e.g. 10] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for hCaptcha 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: kataras 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | go_version: [1.19.x] 17 | steps: 18 | 19 | - name: Set up Go 1.x 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go_version }} 23 | 24 | - name: Check out code into the Go module directory 25 | uses: actions/checkout@v2 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | go.sum 3 | .DS_STORE 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kataras2006@hotmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all read our [Code of Conduct](CODE_OF_CONDUCT.md). 4 | 5 | ## Found a bug? 6 | 7 | Open a new [issue](https://github.com/kataras/hcaptcha/issues/new). 8 | * Write the Operating System and the version of your machine. 9 | * Describe your problem, what did you expect to see and what you see instead. 10 | * If it's a feature request, describe your idea as better as you can. 11 | 12 | ## Code 13 | 14 | 1. Fork the [repository](https://github.com/kataras/hcaptcha). 15 | 2. Make your changes. 16 | 3. Compare & Push the PR from [here](https://github.com/kataras/hcaptcha/compare). 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 Gerasimos Maropoulos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hCaptcha 2 | 3 | [![build status](https://img.shields.io/github/actions/workflow/status/kataras/hcaptcha/ci.yml?style=for-the-badge)](https://github.com/kataras/hcaptcha/actions) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/kataras/hcaptcha) [![godocs](https://img.shields.io/badge/go-%20docs-488AC7.svg?style=for-the-badge)](https://godoc.org/github.com/kataras/hcaptcha) 4 | 5 | The one and only [hCaptcha](https://www.hcaptcha.com/) middleware for Go web servers. 6 | 7 | Inspired by . 8 | 9 | ## Installation 10 | 11 | The only requirement is the [Go Programming Language](https://golang.org/dl). 12 | 13 | ```sh 14 | $ go get -u github.com/kataras/hcaptcha 15 | ``` 16 | 17 | ## Getting Started 18 | 19 | First of all, navigate to , create an account and attach a [new site](https://dashboard.hcaptcha.com/sites) for [development](https://docs.hcaptcha.com/#localdev). 20 | 21 | Import the package: 22 | 23 | ```go 24 | package main 25 | 26 | import "github.com/kataras/hcaptcha" 27 | ``` 28 | 29 | Create a new client: 30 | 31 | ```go 32 | client := hcaptcha.New(your_secret_key) 33 | ``` 34 | 35 | Wrap a page's handler: 36 | 37 | ```go 38 | humanHandler := client.Handler(handler) 39 | ``` 40 | 41 | For a complete example please navigate through [_examples](_examples) directory. 42 | 43 | ## License 44 | 45 | This software is licensed under the [MIT License](LICENSE). 46 | -------------------------------------------------------------------------------- /_examples/basic/hosts: -------------------------------------------------------------------------------- 1 | # https://docs.hcaptcha.com/#localdev 2 | # Add to the end of your hosts file, e.g. on windows: C:/windows/system32/drivers/etc/hosts 3 | 127.0.0.1 yourdomain.com 4 | -------------------------------------------------------------------------------- /_examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/kataras/hcaptcha" 10 | ) 11 | 12 | // Get the following values from: https://dashboard.hcaptcha.com 13 | // Also, check: https://docs.hcaptcha.com/#localdev to test on local environment. 14 | var ( 15 | siteKey = os.Getenv("HCAPTCHA-SITE-KEY") 16 | secretKey = os.Getenv("HCAPTCHA-SECRET-KEY") 17 | ) 18 | 19 | var ( 20 | client = hcaptcha.New(secretKey) /* See `Client.FailureHandler` too. */ 21 | registerForm = template.Must(template.ParseFiles("./register_form.html")) 22 | ) 23 | 24 | func main() { 25 | http.HandleFunc("/", renderForm) 26 | http.HandleFunc("/page", client.HandlerFunc(page) /* See `Client.SiteVerify` to get rid of a wrapper if necessary */) 27 | 28 | fmt.Printf("SiteKey = %s\tSecretKey = %s\nListening on: http://yourdomain.com\n", 29 | siteKey, secretKey) 30 | 31 | http.ListenAndServe(":80", nil) 32 | } 33 | 34 | func page(w http.ResponseWriter, r *http.Request) { 35 | hcaptchaResp, ok := hcaptcha.Get(r) 36 | if !ok { 37 | w.WriteHeader(http.StatusUnauthorized) 38 | fmt.Fprint(w, "Are you a bot?") 39 | return 40 | } 41 | 42 | fmt.Fprintf(w, "Page is inspected by a Human.\nResponse value is: %#+v", hcaptchaResp) 43 | } 44 | 45 | func renderForm(w http.ResponseWriter, r *http.Request) { 46 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 47 | registerForm.Execute(w, map[string]string{ 48 | "SiteKey": siteKey, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /_examples/basic/register_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hCaptcha Demo 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/hcaptcha 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /hcaptcha.go: -------------------------------------------------------------------------------- 1 | package hcaptcha 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | ) 12 | 13 | var ( 14 | // ResponseContextKey is the default request's context key that response of a hcaptcha request is kept. 15 | ResponseContextKey interface{} = "hcaptcha" 16 | // DefaultFailureHandler is the default HTTP handler that is fired on hcaptcha failures. See `Client.FailureHandler`. 17 | DefaultFailureHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) 19 | }) 20 | 21 | // PostMaxMemory is the max memory for a form, defaults to 32MB 22 | PostMaxMemory int64 = 32 << 20 23 | ) 24 | 25 | // Client represents the hcaptcha client. 26 | // It contains the underline HTTPClient which can be modified before API calls. 27 | type Client struct { 28 | HTTPClient *http.Client 29 | 30 | // FailureHandler if specified, fired when user does not complete hcaptcha successfully. 31 | // Failure and error codes information are kept as `Response` type 32 | // at the Request's Context key of "hcaptcha". 33 | // 34 | // Defaults to a handler that writes a status code of 429 (Too Many Requests) 35 | // and without additional information. 36 | FailureHandler http.Handler 37 | 38 | // Optional checks for siteverify 39 | // The user's IP address. 40 | RemoteIP string 41 | // The sitekey you expect to see. 42 | SiteKey string 43 | 44 | secret string 45 | } 46 | 47 | // Response is the hcaptcha JSON response. 48 | type Response struct { 49 | ChallengeTS string `json:"challenge_ts"` 50 | Hostname string `json:"hostname"` 51 | ErrorCodes []string `json:"error-codes,omitempty"` 52 | Success bool `json:"success"` 53 | Credit bool `json:"credit,omitempty"` 54 | } 55 | 56 | // New accepts a hpcatcha secret key and returns a new hcaptcha HTTP Client. 57 | // 58 | // Instructions at: https://docs.hcaptcha.com/. 59 | // 60 | // See its `Handler` and `SiteVerify` for details. 61 | func New(secret string) *Client { 62 | return &Client{ 63 | HTTPClient: http.DefaultClient, 64 | FailureHandler: DefaultFailureHandler, 65 | secret: secret, 66 | } 67 | } 68 | 69 | // Handler is the HTTP route middleware featured hcaptcha validation. 70 | // It calls the `SiteVerify` method and fires the "next" when user completed the hcaptcha successfully, 71 | // 72 | // otherwise it calls the Client's `FailureHandler`. 73 | // 74 | // The hcaptcha's `Response` (which contains any `ErrorCodes`) 75 | // is saved on the Request's Context (see `GetResponseFromContext`). 76 | func (c *Client) Handler(next http.Handler) http.Handler { 77 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | v := c.SiteVerify(r) 79 | r = r.WithContext(context.WithValue(r.Context(), ResponseContextKey, v)) 80 | if v.Success { 81 | next.ServeHTTP(w, r) 82 | return 83 | } 84 | 85 | if c.FailureHandler != nil { 86 | c.FailureHandler.ServeHTTP(w, r) 87 | } 88 | }) 89 | } 90 | 91 | // HandlerFunc same as `Handler` but it accepts and returns a type of `http.HandlerFunc` instead. 92 | func (c *Client) HandlerFunc(next func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 93 | return c.Handler(http.HandlerFunc(next)).ServeHTTP 94 | } 95 | 96 | // responseFormValue = "h-captcha-response" 97 | const apiURL = "https://hcaptcha.com/siteverify" 98 | 99 | // SiteVerify accepts a "r" Request and a secret key (https://dashboard.hcaptcha.com/settings). 100 | // It returns the hcaptcha's `Response`. 101 | // The `response.Success` reports whether the validation passed. 102 | // Any errors are passed through the `response.ErrorCodes` field. 103 | func (c *Client) SiteVerify(r *http.Request) (response Response) { 104 | generatedResponseID, err := getFormValue(r, "h-captcha-response") 105 | if err != nil { 106 | response.ErrorCodes = append(response.ErrorCodes, err.Error()) 107 | return 108 | } 109 | 110 | if generatedResponseID == "" { 111 | response.ErrorCodes = append(response.ErrorCodes, 112 | "form[h-captcha-response] is empty") 113 | return 114 | } 115 | 116 | // Call VerifyToken for verification after extracting token 117 | // Check token before call to maintain backwards compatibility 118 | return c.VerifyToken(generatedResponseID) 119 | } 120 | 121 | // VerifyToken accepts a token and a secret key (https://dashboard.hcaptcha.com/settings). 122 | // It returns the hcaptcha's `Response`. 123 | // The `response.Success` reports whether the validation passed. 124 | // Any errors are passed through the `response.ErrorCodes` field. 125 | // Same as SiteVerify except token is provided by caller instead of being extracted from HTTP request 126 | func (c *Client) VerifyToken(tkn string) (response Response) { 127 | if tkn == "" { 128 | response.ErrorCodes = append(response.ErrorCodes, errors.New("tkn is empty").Error()) 129 | return 130 | } 131 | 132 | values := url.Values{ 133 | "secret": {c.secret}, 134 | "response": {tkn}, 135 | } 136 | 137 | // Add remoteIP if set 138 | if c.RemoteIP != "" { 139 | values.Add("remoteip", c.RemoteIP) 140 | } 141 | 142 | // Add sitekey if set 143 | if c.SiteKey != "" { 144 | values.Add("sitekey", c.SiteKey) 145 | } 146 | 147 | resp, err := c.HTTPClient.PostForm(apiURL, values) 148 | if err != nil { 149 | response.ErrorCodes = append(response.ErrorCodes, err.Error()) 150 | return 151 | } 152 | 153 | body, err := io.ReadAll(resp.Body) 154 | resp.Body.Close() 155 | if err != nil { 156 | response.ErrorCodes = append(response.ErrorCodes, err.Error()) 157 | return 158 | } 159 | 160 | err = json.Unmarshal(body, &response) 161 | if err != nil { 162 | response.ErrorCodes = append(response.ErrorCodes, err.Error()) 163 | return 164 | } 165 | 166 | return 167 | } 168 | 169 | func getFormValue(r *http.Request, key string) (string, error) { 170 | err := r.ParseMultipartForm(PostMaxMemory) 171 | if err != nil && err != http.ErrNotMultipart { 172 | return "", err 173 | } 174 | 175 | if form := r.Form; len(form) > 0 { 176 | return form.Get(key), nil 177 | } 178 | 179 | if form := r.PostForm; len(form) > 0 { 180 | return form.Get(key), nil 181 | } 182 | 183 | if m := r.MultipartForm; m != nil { 184 | if len(m.Value) > 0 { 185 | if values := m.Value[key]; len(values) > 0 { 186 | return values[0], nil 187 | } 188 | } 189 | } 190 | 191 | return "", nil 192 | } 193 | 194 | // Get returns the hcaptcha `Response` of the current "r" request and reports whether was found or not. 195 | func Get(r *http.Request) (Response, bool) { 196 | v := r.Context().Value(ResponseContextKey) 197 | if v != nil { 198 | if response, ok := v.(Response); ok { 199 | return response, true 200 | } 201 | } 202 | 203 | return Response{}, false 204 | } 205 | 206 | // HTMLForm is the default HTML form for clients. 207 | // It's totally optional, use your own code for the best possible result depending on your web application. 208 | // See `ParseForm` and `RenderForm` for more. 209 | var HTMLForm = `
210 | 211 |
212 | 213 |
` 214 | 215 | // ParseForm parses the `HTMLForm` with the necessary parameters and returns 216 | // its result for render. 217 | func ParseForm(dataSiteKey, postActionRelativePath string) string { 218 | return fmt.Sprintf(HTMLForm, postActionRelativePath, dataSiteKey) 219 | } 220 | 221 | // RenderForm writes the `HTMLForm` to "w" response writer. 222 | // See `_examples/basic/register_form.html` example for a custom form instead. 223 | func RenderForm(w http.ResponseWriter, dataSiteKey, postActionRelativePath string) (int, error) { 224 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 225 | return fmt.Fprint(w, ParseForm(dataSiteKey, postActionRelativePath)) 226 | } 227 | --------------------------------------------------------------------------------