Adding upstream version 1.0.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
25c29c6190
commit
cd7c72beb5
20 changed files with 1539 additions and 0 deletions
23
.forgejo/workflows/test.yml
Normal file
23
.forgejo/workflows/test.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: docker-global-bookworm
|
||||
container:
|
||||
image: 'code.forgejo.org/oci/node:20-bookworm'
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: golangci-lint
|
||||
uses: https://github.com/golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.60.3 # renovate: datasource=go depName=golangci-lint packageName=github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
- name: test
|
||||
run: go test -v ./...
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Generated test captchas
|
||||
capgen/*.png
|
||||
capgen/*.wav
|
||||
|
||||
# Programs
|
||||
capgen/capgen
|
||||
cangensounds/cangensounds
|
||||
capexample/capexample
|
27
.golangci.yaml
Normal file
27
.golangci.yaml
Normal file
|
@ -0,0 +1,27 @@
|
|||
linters:
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
fast: false
|
||||
enable:
|
||||
- bidichk
|
||||
- dupl
|
||||
- errcheck
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- tenv
|
||||
- testifylint
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- unparam
|
||||
- wastedassign
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2011-2014 Dmitry Chestnykh <dmitry@codingrobots.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
220
README.md
Normal file
220
README.md
Normal file
|
@ -0,0 +1,220 @@
|
|||
Package captcha
|
||||
=====================
|
||||
|
||||
This is a fork of https://github.com/dchest/captcha
|
||||
|
||||
**:warning: Warning: this captcha can be broken by advanced OCR captcha breaking algorithms.**
|
||||
|
||||
import "code.forgejo.org/go-chi/captcha"
|
||||
|
||||
Package captcha implements generation and verification of image CAPTCHAs.
|
||||
|
||||
A captcha solution is the sequence of digits 0-9 with the defined length.
|
||||
There is one captcha representations: image.
|
||||
|
||||
An image representation is a PNG-encoded image with the solution printed on
|
||||
it in such a way that makes it hard for computers to solve it using OCR.
|
||||
|
||||
This package doesn't require external files or libraries to generate captcha
|
||||
representations; it is self-contained.
|
||||
|
||||
To make captchas one-time, the package includes a memory storage that stores
|
||||
captcha ids, their solutions, and expiration time. Used captchas are removed
|
||||
from the store immediately after calling Verify or VerifyString, while
|
||||
unused captchas (user loaded a page with captcha, but didn't submit the
|
||||
form) are collected automatically after the predefined expiration time.
|
||||
Developers can also provide custom store (for example, which saves captcha
|
||||
ids and solutions in database) by implementing Store interface and
|
||||
registering the object with SetCustomStore.
|
||||
|
||||
Captchas are created by calling New, which returns the captcha id. Their
|
||||
representations, though, are created on-the-fly by calling WriteImage function.
|
||||
Created representations are not stored anywhere, but subsequent calls to these
|
||||
functions with the same id will write the same captcha solution. Reload function
|
||||
will create a new different solution for the provided captcha, allowing users to
|
||||
"reload" captcha if they can't solve the displayed one without reloading the
|
||||
whole page. Verify and VerifyString are used to verify that the given solution
|
||||
is the right one for the given captcha id.
|
||||
|
||||
Server provides an http.Handler which can serve image representations of
|
||||
captchas automatically from the URL. It can also be used to reload captchas.
|
||||
Refer to Server function documentation for details, or take a look at the
|
||||
example in "capexample" subdirectory.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||

|
||||
|
||||
Constants
|
||||
---------
|
||||
|
||||
``` go
|
||||
const (
|
||||
// Default number of digits in captcha solution.
|
||||
DefaultLen = 6
|
||||
// The number of captchas created that triggers garbage collection used
|
||||
// by default store.
|
||||
CollectNum = 100
|
||||
// Expiration time of captchas used by default store.
|
||||
Expiration = 10 * time.Minute
|
||||
)
|
||||
```
|
||||
|
||||
``` go
|
||||
const (
|
||||
// Standard width and height of a captcha image.
|
||||
StdWidth = 240
|
||||
StdHeight = 80
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Variables
|
||||
---------
|
||||
|
||||
``` go
|
||||
var (
|
||||
ErrNotFound = errors.New("captcha: id not found")
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
### func New
|
||||
|
||||
func New() string
|
||||
|
||||
New creates a new captcha with the standard length, saves it in the internal
|
||||
storage and returns its id.
|
||||
|
||||
### func NewLen
|
||||
|
||||
func NewLen(length int) (id string)
|
||||
|
||||
NewLen is just like New, but accepts length of a captcha solution as the
|
||||
argument.
|
||||
|
||||
### func RandomDigits
|
||||
|
||||
func RandomDigits(length int) (b []byte)
|
||||
|
||||
RandomDigits returns a byte slice of the given length containing
|
||||
pseudorandom numbers in range 0-9. The slice can be used as a captcha
|
||||
solution.
|
||||
|
||||
### func Reload
|
||||
|
||||
func Reload(id string) bool
|
||||
|
||||
Reload generates and remembers new digits for the given captcha id. This
|
||||
function returns false if there is no captcha with the given id.
|
||||
|
||||
After calling this function, the image presented to a user must be refreshed to
|
||||
show the new captcha representation (WriteImage will write the new one).
|
||||
|
||||
### func Server
|
||||
|
||||
func Server(imgWidth, imgHeight int) http.Handler
|
||||
|
||||
Server returns a handler that serves HTTP requests with image or
|
||||
representations of captchas. Image dimensions are accepted as
|
||||
arguments. The server decides which captcha to serve based on the last URL
|
||||
path component: file name part must contain a captcha id, file extension —
|
||||
its format (PNG or WAV).
|
||||
|
||||
For example, for file name "LBm5vMjHDtdUfaWYXiQX.png" it serves an image captcha
|
||||
with id "LBm5vMjHDtdUfaWYXiQX".
|
||||
|
||||
To reload captcha (get a different solution for the same captcha id), append
|
||||
"?reload=x" to URL, where x may be anything (for example, current time or a
|
||||
random number to make browsers refetch an image instead of loading it from
|
||||
cache).
|
||||
|
||||
### func SetCustomStore
|
||||
|
||||
func SetCustomStore(s Store)
|
||||
|
||||
SetCustomStore sets custom storage for captchas, replacing the default
|
||||
memory store. This function must be called before generating any captchas.
|
||||
|
||||
### func Verify
|
||||
|
||||
func Verify(id string, digits []byte) bool
|
||||
|
||||
Verify returns true if the given digits are the ones that were used to
|
||||
create the given captcha id.
|
||||
|
||||
The function deletes the captcha with the given id from the internal
|
||||
storage, so that the same captcha can't be verified anymore.
|
||||
|
||||
### func VerifyString
|
||||
|
||||
func VerifyString(id string, digits string) bool
|
||||
|
||||
VerifyString is like Verify, but accepts a string of digits. It removes
|
||||
spaces and commas from the string, but any other characters, apart from
|
||||
digits and listed above, will cause the function to return false.
|
||||
|
||||
### func WriteImage
|
||||
|
||||
func WriteImage(w io.Writer, id string, width, height int) error
|
||||
|
||||
WriteImage writes PNG-encoded image representation of the captcha with the
|
||||
given id. The image will have the given width and height.
|
||||
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
``` go
|
||||
type Image struct {
|
||||
*image.Paletted
|
||||
// contains unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### func NewImage
|
||||
|
||||
func NewImage(id string, digits []byte, width, height int) *Image
|
||||
|
||||
NewImage returns a new captcha image of the given width and height with the
|
||||
given digits, where each digit must be in range 0-9.
|
||||
|
||||
### func (*Image) WriteTo
|
||||
|
||||
func (m *Image) WriteTo(w io.Writer) (int64, error)
|
||||
|
||||
WriteTo writes captcha image in PNG format into the given writer.
|
||||
|
||||
``` go
|
||||
type Store interface {
|
||||
// Set sets the digits for the captcha id.
|
||||
Set(id string, digits []byte)
|
||||
|
||||
// Get returns stored digits for the captcha id. Clear indicates
|
||||
// whether the captcha must be deleted from the store.
|
||||
Get(id string, clear bool) (digits []byte)
|
||||
}
|
||||
```
|
||||
|
||||
An object implementing Store interface can be registered with SetCustomStore
|
||||
function to handle storage and retrieval of captcha ids and solutions for
|
||||
them, replacing the default memory store.
|
||||
|
||||
It is the responsibility of an object to delete expired and used captchas
|
||||
when necessary (for example, the default memory store collects them in Set
|
||||
method after the certain amount of captchas has been stored.)
|
||||
|
||||
### func NewMemoryStore
|
||||
|
||||
func NewMemoryStore(collectNum int, expiration time.Duration) Store
|
||||
|
||||
NewMemoryStore returns a new standard memory store for captchas with the
|
||||
given collection threshold and expiration time in seconds. The returned
|
||||
store must be registered with SetCustomStore to replace the default one.
|
83
capexample/main.go
Normal file
83
capexample/main.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// example of HTTP server that uses the captcha package.
|
||||
//
|
||||
//nolint:forbidigo
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"code.forgejo.org/go-chi/captcha"
|
||||
)
|
||||
|
||||
var formTemplate = template.Must(template.New("example").Parse(formTemplateSrc))
|
||||
|
||||
func showFormHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
d := struct {
|
||||
CaptchaID string
|
||||
}{
|
||||
captcha.New(),
|
||||
}
|
||||
if err := formTemplate.Execute(w, &d); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func processFormHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if !captcha.VerifyString(r.FormValue("captchaId"), r.FormValue("captchaSolution")) {
|
||||
_, _ = io.WriteString(w, "Wrong captcha solution! No robots allowed!\n")
|
||||
} else {
|
||||
_, _ = io.WriteString(w, "Great job, human! You solved the captcha.\n")
|
||||
}
|
||||
_, _ = io.WriteString(w, "<br><a href='/'>Try another one</a>")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", showFormHandler)
|
||||
http.HandleFunc("/process", processFormHandler)
|
||||
http.Handle("/captcha/", captcha.Server(captcha.StdWidth, captcha.StdHeight))
|
||||
fmt.Println("Server is at localhost:8666")
|
||||
if err := http.ListenAndServe("localhost:8666", nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const formTemplateSrc = `<!doctype html>
|
||||
<head><title>Captcha Example</title></head>
|
||||
<body>
|
||||
<script>
|
||||
function setSrcQuery(e, q) {
|
||||
var src = e.src;
|
||||
var p = src.indexOf('?');
|
||||
if (p >= 0) {
|
||||
src = src.substr(0, p);
|
||||
}
|
||||
e.src = src + "?" + q
|
||||
}
|
||||
|
||||
function reload() {
|
||||
setSrcQuery(document.getElementById('image'), "reload=" + (new Date()).getTime());
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<form action="/process" method=post>
|
||||
<p>Type the numbers you see in the picture below:</p>
|
||||
<p><img style="transform: scaleX(-1)" id=image src="/captcha/{{.CaptchaID}}.png" alt="Captcha image"></p>
|
||||
<a href="#" onclick="reload()">Reload</a>
|
||||
<input type=hidden name=captchaId value="{{.CaptchaID}}"><br>
|
||||
<input name=captchaSolution>
|
||||
<input type=submit value=Submit>
|
||||
</form>
|
||||
`
|
BIN
capgen/example.png
Normal file
BIN
capgen/example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
capgen/example.wav
Normal file
BIN
capgen/example.wav
Normal file
Binary file not shown.
46
capgen/main.go
Normal file
46
capgen/main.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// capgen is an utility to test captcha generation.
|
||||
//
|
||||
//nolint:forbidigo
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.forgejo.org/go-chi/captcha"
|
||||
)
|
||||
|
||||
var (
|
||||
flagLen = flag.Int("len", captcha.DefaultLen, "length of captcha")
|
||||
flagImgW = flag.Int("width", captcha.StdWidth, "image captcha width")
|
||||
flagImgH = flag.Int("height", captcha.StdHeight, "image captcha height")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: capgen [flags] filename\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
fname := flag.Arg(0)
|
||||
if fname == "" {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
f, err := os.Create(fname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := captcha.RandomDigits(*flagLen)
|
||||
if _, err := captcha.NewImage("", d, *flagImgW, *flagImgH).WriteTo(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(d)
|
||||
}
|
153
captcha.go
Normal file
153
captcha.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package captcha implements generation and verification of image and audio
|
||||
// CAPTCHAs.
|
||||
//
|
||||
// A captcha solution is the sequence of digits 0-9 with the defined length.
|
||||
// There are two captcha representations: image and audio.
|
||||
//
|
||||
// An image representation is a PNG-encoded image with the solution printed on
|
||||
// it in such a way that makes it hard for computers to solve it using OCR.
|
||||
//
|
||||
// An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound with
|
||||
// the spoken solution (currently in English, Russian, Chinese, and Japanese).
|
||||
// To make it hard for computers to solve audio captcha, the voice that
|
||||
// pronounces numbers has random speed and pitch, and there is a randomly
|
||||
// generated background noise mixed into the sound.
|
||||
//
|
||||
// This package doesn't require external files or libraries to generate captcha
|
||||
// representations; it is self-contained.
|
||||
//
|
||||
// To make captchas one-time, the package includes a memory storage that stores
|
||||
// captcha ids, their solutions, and expiration time. Used captchas are removed
|
||||
// from the store immediately after calling Verify or VerifyString, while
|
||||
// unused captchas (user loaded a page with captcha, but didn't submit the
|
||||
// form) are collected automatically after the predefined expiration time.
|
||||
// Developers can also provide custom store (for example, which saves captcha
|
||||
// ids and solutions in database) by implementing Store interface and
|
||||
// registering the object with SetCustomStore.
|
||||
//
|
||||
// Captchas are created by calling New, which returns the captcha id. Their
|
||||
// representations, though, are created on-the-fly by calling WriteImage or
|
||||
// WriteAudio functions. Created representations are not stored anywhere, but
|
||||
// subsequent calls to these functions with the same id will write the same
|
||||
// captcha solution. Reload function will create a new different solution for
|
||||
// the provided captcha, allowing users to "reload" captcha if they can't solve
|
||||
// the displayed one without reloading the whole page. Verify and VerifyString
|
||||
// are used to verify that the given solution is the right one for the given
|
||||
// captcha id.
|
||||
//
|
||||
// Server provides an http.Handler which can serve image and audio
|
||||
// representations of captchas automatically from the URL. It can also be used
|
||||
// to reload captchas. Refer to Server function documentation for details, or
|
||||
// take a look at the example in "capexample" subdirectory.
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default number of digits in captcha solution.
|
||||
DefaultLen = 6
|
||||
// The number of captchas created that triggers garbage collection used
|
||||
// by default store.
|
||||
CollectNum = 100
|
||||
// Expiration time of captchas used by default store.
|
||||
Expiration = 10 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("captcha: id not found")
|
||||
// globalStore is a shared storage for captchas, generated by New function.
|
||||
globalStore = NewMemoryStore(CollectNum, Expiration)
|
||||
)
|
||||
|
||||
// SetCustomStore sets custom storage for captchas, replacing the default
|
||||
// memory store. This function must be called before generating any captchas.
|
||||
func SetCustomStore(s Store) {
|
||||
globalStore = s
|
||||
}
|
||||
|
||||
// New creates a new captcha with the standard length, saves it in the internal
|
||||
// storage and returns its id.
|
||||
func New() string {
|
||||
return NewLen(DefaultLen)
|
||||
}
|
||||
|
||||
// NewLen is just like New, but accepts length of a captcha solution as the
|
||||
// argument.
|
||||
func NewLen(length int) (id string) {
|
||||
id = randomID()
|
||||
globalStore.Set(id, RandomDigits(length))
|
||||
return
|
||||
}
|
||||
|
||||
// Reload generates and remembers new digits for the given captcha id. This
|
||||
// function returns false if there is no captcha with the given id.
|
||||
//
|
||||
// After calling this function, the image or audio presented to a user must be
|
||||
// refreshed to show the new captcha representation (WriteImage and WriteAudio
|
||||
// will write the new one).
|
||||
func Reload(id string) bool {
|
||||
old := globalStore.Get(id, false)
|
||||
if old == nil {
|
||||
return false
|
||||
}
|
||||
globalStore.Set(id, RandomDigits(len(old)))
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteImage writes PNG-encoded image representation of the captcha with the
|
||||
// given id. The image will have the given width and height.
|
||||
func WriteImage(w io.Writer, id string, width, height int) error {
|
||||
d := globalStore.Get(id, false)
|
||||
if d == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
_, err := NewImage(id, d, width, height).WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify returns true if the given digits are the ones that were used to
|
||||
// create the given captcha id.
|
||||
//
|
||||
// The function deletes the captcha with the given id from the internal
|
||||
// storage, so that the same captcha can't be verified anymore.
|
||||
func Verify(id string, digits []byte) bool {
|
||||
if len(digits) == 0 {
|
||||
return false
|
||||
}
|
||||
reald := globalStore.Get(id, true)
|
||||
if reald == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(digits, reald)
|
||||
}
|
||||
|
||||
// VerifyString is like Verify, but accepts a string of digits. It removes
|
||||
// spaces and commas from the string, but any other characters, apart from
|
||||
// digits and listed above, will cause the function to return false.
|
||||
func VerifyString(id string, digits string) bool {
|
||||
if digits == "" {
|
||||
return false
|
||||
}
|
||||
ns := make([]byte, len(digits))
|
||||
for i := range ns {
|
||||
d := digits[i]
|
||||
switch {
|
||||
case '0' <= d && d <= '9':
|
||||
ns[i] = d - '0'
|
||||
case d == ' ' || d == ',':
|
||||
// ignore
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return Verify(id, ns)
|
||||
}
|
52
captcha_test.go
Normal file
52
captcha_test.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c := New()
|
||||
if c == "" {
|
||||
t.Errorf("expected id, got empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
id := New()
|
||||
if Verify(id, []byte{0, 0}) {
|
||||
t.Errorf("verified wrong captcha")
|
||||
}
|
||||
id = New()
|
||||
d := globalStore.Get(id, false) // cheating
|
||||
if !Verify(id, d) {
|
||||
t.Errorf("proper captcha not verified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReload(t *testing.T) {
|
||||
id := New()
|
||||
d1 := globalStore.Get(id, false) // cheating
|
||||
Reload(id)
|
||||
d2 := globalStore.Get(id, false) // cheating again
|
||||
if bytes.Equal(d1, d2) {
|
||||
t.Errorf("reload didn't work: %v = %v", d1, d2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomDigits(t *testing.T) {
|
||||
d1 := RandomDigits(10)
|
||||
for _, v := range d1 {
|
||||
if v > 9 {
|
||||
t.Errorf("digits not in range 0-9: %v", d1)
|
||||
}
|
||||
}
|
||||
d2 := RandomDigits(10)
|
||||
if bytes.Equal(d1, d2) {
|
||||
t.Errorf("digits seem to be not random")
|
||||
}
|
||||
}
|
215
font.go
Normal file
215
font.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
const (
|
||||
fontWidth = 11
|
||||
fontHeight = 18
|
||||
blackChar = 1
|
||||
)
|
||||
|
||||
//nolint:dupl
|
||||
var font = [][]byte{
|
||||
{ // 0
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 1
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
},
|
||||
{ // 2
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
},
|
||||
{ // 3
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
},
|
||||
{ // 5
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 6
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0,
|
||||
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 7
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
},
|
||||
{ // 8
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 9
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
},
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module code.forgejo.org/go-chi/captcha
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.4
|
278
image.go
Normal file
278
image.go
Normal file
|
@ -0,0 +1,278 @@
|
|||
// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
mathRand "math/rand/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// Standard width and height of a captcha image.
|
||||
StdWidth = 240
|
||||
StdHeight = 80
|
||||
// Maximum absolute skew factor of a single digit.
|
||||
maxSkew = 0.7
|
||||
// Number of background circles.
|
||||
circleCount = 20
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
*image.Paletted
|
||||
numWidth int
|
||||
numHeight int
|
||||
dotSize int
|
||||
rng *mathRand.Rand
|
||||
}
|
||||
|
||||
// Int returns a pseudorandom int in the half-open interval [from, to].
|
||||
func (m *Image) Int(from, to int) int {
|
||||
return from + m.rng.IntN(to-from+1)
|
||||
}
|
||||
|
||||
// Float returns a pseudorandom float64 in the half-open interval [from, to).
|
||||
func (m *Image) Float(from, to float64) float64 {
|
||||
return from + (to-from)*m.rng.Float64()
|
||||
}
|
||||
|
||||
// NewImage returns a new captcha image of the given width and height with the
|
||||
// given digits, where each digit must be in range 0-9.
|
||||
func NewImage(id string, digits []byte, width, height int) *Image {
|
||||
m := new(Image)
|
||||
|
||||
// Initialize PRNG.
|
||||
m.rng = mathRand.New(mathRand.NewChaCha8(deriveSeed(imageSeedPurpose, id, digits)))
|
||||
|
||||
m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), m.getRandomPalette())
|
||||
m.calculateSizes(width, height, len(digits))
|
||||
// Randomly position captcha inside the image.
|
||||
maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
|
||||
maxy := height - m.numHeight - m.dotSize*2
|
||||
var border int
|
||||
if width > height {
|
||||
border = height / 5
|
||||
} else {
|
||||
border = width / 5
|
||||
}
|
||||
x := m.Int(border, maxx-border)
|
||||
y := m.Int(border, maxy-border)
|
||||
// Draw digits.
|
||||
for _, n := range digits {
|
||||
m.drawDigit(font[n], x, y)
|
||||
x += m.numWidth + m.dotSize
|
||||
}
|
||||
// Draw strike-through line.
|
||||
m.strikeThrough()
|
||||
// Apply wave distortion.
|
||||
m.distort(m.Float(5, 10), m.Float(100, 200))
|
||||
// Fill image with random circles.
|
||||
m.fillWithCircles(circleCount, m.dotSize)
|
||||
// Flip the image horizontally.
|
||||
m.flipX()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Image) getRandomPalette() color.Palette {
|
||||
p := make([]color.Color, circleCount+1)
|
||||
// Transparent color.
|
||||
p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
|
||||
// Primary color.
|
||||
prim := color.RGBA{
|
||||
uint8(m.rng.IntN(129)),
|
||||
uint8(m.rng.IntN(129)),
|
||||
uint8(m.rng.IntN(129)),
|
||||
0xFF,
|
||||
}
|
||||
p[1] = prim
|
||||
// Circle colors.
|
||||
for i := 2; i <= circleCount; i++ {
|
||||
p[i] = m.randomBrightness(prim, 255)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// encodedPNG encodes an image to PNG and returns
|
||||
// the result as a byte slice.
|
||||
func (m *Image) encodedPNG() []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, m.Paletted); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// WriteTo writes captcha image in PNG format into the given writer.
|
||||
func (m *Image) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(m.encodedPNG())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
func (m *Image) calculateSizes(width, height, ncount int) {
|
||||
// Goal: fit all digits inside the image.
|
||||
var border int
|
||||
if width > height {
|
||||
border = height / 4
|
||||
} else {
|
||||
border = width / 4
|
||||
}
|
||||
// Convert everything to floats for calculations.
|
||||
w := float64(width - border*2)
|
||||
h := float64(height - border*2)
|
||||
// fw takes into account 1-dot spacing between digits.
|
||||
fw := float64(fontWidth + 1)
|
||||
fh := float64(fontHeight)
|
||||
nc := float64(ncount)
|
||||
// Calculate the width of a single digit taking into account only the
|
||||
// width of the image.
|
||||
nw := w / nc
|
||||
// Calculate the height of a digit from this width.
|
||||
nh := nw * fh / fw
|
||||
// Digit too high?
|
||||
if nh > h {
|
||||
// Fit digits based on height.
|
||||
nh = h
|
||||
nw = fw / fh * nh
|
||||
}
|
||||
// Calculate dot size.
|
||||
m.dotSize = int(nh / fh)
|
||||
if m.dotSize < 1 {
|
||||
m.dotSize = 1
|
||||
}
|
||||
// Save everything, making the actual width smaller by 1 dot to account
|
||||
// for spacing between digits.
|
||||
m.numWidth = int(nw) - m.dotSize
|
||||
m.numHeight = int(nh)
|
||||
}
|
||||
|
||||
func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
|
||||
for x := fromX; x <= toX; x++ {
|
||||
m.SetColorIndex(x, y, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
|
||||
f := 1 - radius
|
||||
dfx := 1
|
||||
dfy := -2 * radius
|
||||
xo := 0
|
||||
yo := radius
|
||||
|
||||
m.SetColorIndex(x, y+radius, colorIdx)
|
||||
m.SetColorIndex(x, y-radius, colorIdx)
|
||||
m.drawHorizLine(x-radius, x+radius, y, colorIdx)
|
||||
|
||||
for xo < yo {
|
||||
if f >= 0 {
|
||||
yo--
|
||||
dfy += 2
|
||||
f += dfy
|
||||
}
|
||||
xo++
|
||||
dfx += 2
|
||||
f += dfx
|
||||
m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
|
||||
m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
|
||||
m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
|
||||
m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) fillWithCircles(n, maxradius int) {
|
||||
maxx := m.Bounds().Max.X
|
||||
maxy := m.Bounds().Max.Y
|
||||
for i := 0; i < n; i++ {
|
||||
colorIdx := uint8(m.Int(1, circleCount-1))
|
||||
r := m.Int(1, maxradius)
|
||||
m.drawCircle(m.Int(r, maxx-r), m.Int(r, maxy-r), r, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) strikeThrough() {
|
||||
maxx := m.Bounds().Max.X
|
||||
maxy := m.Bounds().Max.Y
|
||||
y := m.Int(maxy/3, maxy-maxy/3)
|
||||
amplitude := m.Float(5, 20)
|
||||
period := m.Float(80, 180)
|
||||
dx := 2.0 * math.Pi / period
|
||||
for x := 0; x < maxx; x++ {
|
||||
xo := amplitude * math.Cos(float64(y)*dx)
|
||||
yo := amplitude * math.Sin(float64(x)*dx)
|
||||
for yn := 0; yn < m.dotSize; yn++ {
|
||||
r := m.rng.IntN(m.dotSize + 1)
|
||||
m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) drawDigit(digit []byte, x, y int) {
|
||||
skf := m.Float(-maxSkew, maxSkew)
|
||||
xs := float64(x)
|
||||
r := m.dotSize / 2
|
||||
y += m.Int(-r, r)
|
||||
for yo := 0; yo < fontHeight; yo++ {
|
||||
for xo := 0; xo < fontWidth; xo++ {
|
||||
if digit[yo*fontWidth+xo] != blackChar {
|
||||
continue
|
||||
}
|
||||
m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
|
||||
}
|
||||
xs += skf
|
||||
x = int(xs)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) distort(amplude float64, period float64) {
|
||||
w := m.Bounds().Max.X
|
||||
h := m.Bounds().Max.Y
|
||||
|
||||
oldm := m.Paletted
|
||||
newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
|
||||
|
||||
dx := 2.0 * math.Pi / period
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
xo := amplude * math.Sin(float64(y)*dx)
|
||||
yo := amplude * math.Cos(float64(x)*dx)
|
||||
newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
|
||||
}
|
||||
}
|
||||
m.Paletted = newm
|
||||
}
|
||||
|
||||
func (m *Image) flipX() {
|
||||
w := m.Bounds().Max.X
|
||||
h := m.Bounds().Max.Y
|
||||
|
||||
src := m.Paletted
|
||||
dst := image.NewPaletted(image.Rect(0, 0, w, h), src.Palette)
|
||||
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
xo := w - x - 1
|
||||
dst.SetColorIndex(x, y, src.ColorIndexAt(xo, y))
|
||||
}
|
||||
}
|
||||
m.Paletted = dst
|
||||
}
|
||||
|
||||
func (m *Image) randomBrightness(c color.RGBA, maxB uint8) color.RGBA {
|
||||
minc := min(min(c.R, c.G), c.B)
|
||||
maxc := max(max(c.R, c.G), c.B)
|
||||
if maxc > maxB {
|
||||
return c
|
||||
}
|
||||
n := m.rng.IntN(int(maxB-maxc)) - int(minc)
|
||||
return color.RGBA{
|
||||
uint8(int(c.R) + n),
|
||||
uint8(int(c.G) + n),
|
||||
uint8(int(c.B) + n),
|
||||
c.A,
|
||||
}
|
||||
}
|
17
image_test.go
Normal file
17
image_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkNewImage(b *testing.B) {
|
||||
b.StopTimer()
|
||||
d := RandomDigits(DefaultLen)
|
||||
id := randomID()
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewImage(id, d, StdWidth, StdHeight)
|
||||
}
|
||||
}
|
109
random.go
Normal file
109
random.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
)
|
||||
|
||||
// idLen is a length of captcha id string.
|
||||
// (20 bytes of 62-letter alphabet give ~119 bits.)
|
||||
const idLen = 20
|
||||
|
||||
// idChars are characters allowed in captcha id.
|
||||
var idChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
// rngKey is a secret key used to deterministically derive seeds for
|
||||
// PRNGs used in image. Generated once during initialization.
|
||||
var rngKey [32]byte
|
||||
|
||||
func init() {
|
||||
if _, err := io.ReadFull(rand.Reader, rngKey[:]); err != nil {
|
||||
panic("captcha: error reading random source: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Purposes for seed derivation. The goal is to make deterministic PRNG produce
|
||||
// different outputs for images and audio by using different derived seeds.
|
||||
const (
|
||||
imageSeedPurpose = 0x01
|
||||
audioSeedPurpose = 0x02
|
||||
)
|
||||
|
||||
// deriveSeed returns a 16-byte PRNG seed from rngKey, purpose, id and digits.
|
||||
// Same purpose, id and digits will result in the same derived seed for this
|
||||
// instance of running application.
|
||||
//
|
||||
// out = HMAC(rngKey, purpose || id || 0x00 || digits)
|
||||
func deriveSeed(purpose byte, id string, digits []byte) (out [32]byte) {
|
||||
var buf [sha256.Size]byte
|
||||
|
||||
h := hmac.New(sha256.New, rngKey[:])
|
||||
h.Write([]byte{purpose})
|
||||
h.Write([]byte(id))
|
||||
h.Write([]byte{0})
|
||||
h.Write(digits)
|
||||
|
||||
sum := h.Sum(buf[:0])
|
||||
copy(out[:], sum)
|
||||
return
|
||||
}
|
||||
|
||||
// RandomDigits returns a byte slice of the given length containing
|
||||
// pseudorandom numbers in range 0-9. The slice can be used as a captcha
|
||||
// solution.
|
||||
func RandomDigits(length int) []byte {
|
||||
return randomBytesMod(length, 10)
|
||||
}
|
||||
|
||||
// randomBytes returns a byte slice of the given length read from CSPRNG.
|
||||
func randomBytes(length int) (b []byte) {
|
||||
b = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
panic("captcha: error reading random source: " + err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// randomBytesMod returns a byte slice of the given length, where each byte is
|
||||
// a random number modulo mod.
|
||||
func randomBytesMod(length int, mod byte) (b []byte) {
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
if mod == 0 {
|
||||
panic("captcha: bad mod argument for randomBytesMod")
|
||||
}
|
||||
|
||||
maxrb := 255 - byte(256%int(mod))
|
||||
b = make([]byte, length)
|
||||
i := 0
|
||||
for {
|
||||
r := randomBytes(length + (length / 4))
|
||||
for _, c := range r {
|
||||
if c > maxrb {
|
||||
// Skip this number to avoid modulo bias.
|
||||
continue
|
||||
}
|
||||
b[i] = c % mod
|
||||
i++
|
||||
if i == length {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// randomID returns a new random id string.
|
||||
func randomID() string {
|
||||
b := randomBytesMod(idLen, byte(len(idChars)))
|
||||
for i, c := range b {
|
||||
b[i] = idChars[c]
|
||||
}
|
||||
return string(b)
|
||||
}
|
4
renovate.json
Normal file
4
renovate.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["go-chi/renovate-config"]
|
||||
}
|
84
server.go
Normal file
84
server.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type captchaHandler struct {
|
||||
imgWidth int
|
||||
imgHeight int
|
||||
}
|
||||
|
||||
// Server returns a handler that serves HTTP requests with image
|
||||
// representations of captchas. Image dimensions are accepted as
|
||||
// arguments. The server decides which captcha to serve based on the last URL
|
||||
// path component: file name part must contain a captcha id, file extension —
|
||||
// its format (PNG or WAV).
|
||||
//
|
||||
// For example, for file name "LBm5vMjHDtdUfaWYXiQX.png" it serves an image captcha
|
||||
// with id "LBm5vMjHDtdUfaWYXiQX", and for "LBm5vMjHDtdUfaWYXiQX.wav" it serves the
|
||||
// same captcha in audio format.
|
||||
//
|
||||
// To serve a captcha as a downloadable file, the URL must be constructed in
|
||||
// such a way as if the file to serve is in the "download" subdirectory:
|
||||
// "/download/LBm5vMjHDtdUfaWYXiQX.wav".
|
||||
//
|
||||
// To reload captcha (get a different solution for the same captcha id), append
|
||||
// "?reload=x" to URL, where x may be anything (for example, current time or a
|
||||
// random number to make browsers refetch an image instead of loading it from
|
||||
// cache).
|
||||
//
|
||||
// By default, the Server serves audio in English language. To serve audio
|
||||
// captcha in one of the other supported languages, append "lang" value, for
|
||||
// example, "?lang=ru".
|
||||
func Server(imgWidth, imgHeight int) http.Handler {
|
||||
return &captchaHandler{imgWidth, imgHeight}
|
||||
}
|
||||
|
||||
func (h *captchaHandler) serve(w http.ResponseWriter, r *http.Request, id, ext string, download bool) error {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
var content bytes.Buffer
|
||||
switch ext {
|
||||
case ".png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
if err := WriteImage(&content, id, h.imgWidth, h.imgHeight); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
default:
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if download {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
dir, file := path.Split(r.URL.Path)
|
||||
ext := path.Ext(file)
|
||||
id := file[:len(file)-len(ext)]
|
||||
if ext == "" || id == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.FormValue("reload") != "" {
|
||||
Reload(id)
|
||||
}
|
||||
download := path.Base(dir) == "download"
|
||||
if h.serve(w, r, id, ext, download) == ErrNotFound {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
// Ignore other errors.
|
||||
}
|
117
store.go
Normal file
117
store.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An object implementing Store interface can be registered with SetCustomStore
|
||||
// function to handle storage and retrieval of captcha ids and solutions for
|
||||
// them, replacing the default memory store.
|
||||
//
|
||||
// It is the responsibility of an object to delete expired and used captchas
|
||||
// when necessary (for example, the default memory store collects them in Set
|
||||
// method after the certain amount of captchas has been stored.)
|
||||
type Store interface {
|
||||
// Set sets the digits for the captcha id.
|
||||
Set(id string, digits []byte)
|
||||
|
||||
// Get returns stored digits for the captcha id. Clear indicates
|
||||
// whether the captcha must be deleted from the store.
|
||||
Get(id string, clear bool) (digits []byte)
|
||||
}
|
||||
|
||||
// expValue stores timestamp and id of captchas. It is used in the list inside
|
||||
// memoryStore for indexing generated captchas by timestamp to enable garbage
|
||||
// collection of expired captchas.
|
||||
type idByTimeValue struct {
|
||||
timestamp time.Time
|
||||
id string
|
||||
}
|
||||
|
||||
// memoryStore is an internal store for captcha ids and their values.
|
||||
type memoryStore struct {
|
||||
sync.RWMutex
|
||||
digitsByID map[string][]byte
|
||||
idByTime *list.List
|
||||
// Number of items stored since last collection.
|
||||
numStored int
|
||||
// Number of saved items that triggers collection.
|
||||
collectNum int
|
||||
// Expiration time of captchas.
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
// NewMemoryStore returns a new standard memory store for captchas with the
|
||||
// given collection threshold and expiration time (duration). The returned
|
||||
// store must be registered with SetCustomStore to replace the default one.
|
||||
func NewMemoryStore(collectNum int, expiration time.Duration) Store {
|
||||
s := new(memoryStore)
|
||||
s.digitsByID = make(map[string][]byte)
|
||||
s.idByTime = list.New()
|
||||
s.collectNum = collectNum
|
||||
s.expiration = expiration
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *memoryStore) Set(id string, digits []byte) {
|
||||
s.Lock()
|
||||
s.digitsByID[id] = digits
|
||||
s.idByTime.PushBack(idByTimeValue{time.Now(), id})
|
||||
s.numStored++
|
||||
if s.numStored <= s.collectNum {
|
||||
s.Unlock()
|
||||
return
|
||||
}
|
||||
s.Unlock()
|
||||
go s.collect()
|
||||
}
|
||||
|
||||
func (s *memoryStore) Get(id string, clear bool) (digits []byte) {
|
||||
if !clear {
|
||||
// When we don't need to clear captcha, acquire read lock.
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
} else {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
}
|
||||
digits, ok := s.digitsByID[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if clear {
|
||||
delete(s.digitsByID, id)
|
||||
// XXX(dchest) Index (s.idByTime) will be cleaned when
|
||||
// collecting expired captchas. Can't clean it here, because
|
||||
// we don't store reference to expValue in the map.
|
||||
// Maybe store it?
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *memoryStore) collect() {
|
||||
now := time.Now()
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.numStored = 0
|
||||
for e := s.idByTime.Front(); e != nil; {
|
||||
ev, ok := e.Value.(idByTimeValue)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ev.timestamp.Add(s.expiration).Before(now) {
|
||||
delete(s.digitsByID, ev.id)
|
||||
next := e.Next()
|
||||
s.idByTime.Remove(e)
|
||||
e = next
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
79
store_test.go
Normal file
79
store_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetGet(t *testing.T) {
|
||||
s := NewMemoryStore(CollectNum, Expiration)
|
||||
id := "captcha id"
|
||||
d := RandomDigits(10)
|
||||
s.Set(id, d)
|
||||
d2 := s.Get(id, false)
|
||||
if d2 == nil || !bytes.Equal(d, d2) {
|
||||
t.Errorf("saved %v, getDigits returned got %v", d, d2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClear(t *testing.T) {
|
||||
s := NewMemoryStore(CollectNum, Expiration)
|
||||
id := "captcha id"
|
||||
d := RandomDigits(10)
|
||||
s.Set(id, d)
|
||||
d2 := s.Get(id, true)
|
||||
if d2 == nil || !bytes.Equal(d, d2) {
|
||||
t.Errorf("saved %v, getDigitsClear returned got %v", d, d2)
|
||||
}
|
||||
d2 = s.Get(id, false)
|
||||
if d2 != nil {
|
||||
t.Errorf("getDigitClear didn't clear (%q=%v)", id, d2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollect(t *testing.T) {
|
||||
// TODO(dchest): can't test automatic collection when saving, because
|
||||
// it's currently launched in a different goroutine.
|
||||
s := NewMemoryStore(10, -1)
|
||||
// create 10 ids
|
||||
ids := make([]string, 10)
|
||||
d := RandomDigits(10)
|
||||
for i := range ids {
|
||||
ids[i] = randomID()
|
||||
s.Set(ids[i], d)
|
||||
}
|
||||
s.(*memoryStore).collect()
|
||||
// Must be already collected
|
||||
nc := 0
|
||||
for i := range ids {
|
||||
d2 := s.Get(ids[i], false)
|
||||
if d2 != nil {
|
||||
t.Errorf("%d: not collected", i)
|
||||
nc++
|
||||
}
|
||||
}
|
||||
if nc > 0 {
|
||||
t.Errorf("= not collected %d out of %d captchas", nc, len(ids))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSetCollect(b *testing.B) {
|
||||
b.StopTimer()
|
||||
d := RandomDigits(10)
|
||||
s := NewMemoryStore(9999, -1)
|
||||
ids := make([]string, 1000)
|
||||
for i := range ids {
|
||||
ids[i] = randomID()
|
||||
}
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < 1000; j++ {
|
||||
s.Set(ids[j], d)
|
||||
}
|
||||
s.(*memoryStore).collect()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue