Initial commit with a fully working program, lel.
This commit is contained in:
commit
21b2f24986
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
bin/*
|
||||||
|
!bin/.keep
|
||||||
|
.vscode
|
||||||
|
*.test.yaml
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Alexander "Arav" Andreev <me@arav.top>
|
||||||
|
|
||||||
|
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.
|
34
Makefile
Executable file
34
Makefile
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
TARGET=justcaptcha
|
||||||
|
DAEMON_NAME=${TARGET}d
|
||||||
|
|
||||||
|
SYSCTL=${shell which systemctl}
|
||||||
|
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
||||||
|
SYSDDIR=${SYSDDIR_:/%=%}
|
||||||
|
DESTDIR=/
|
||||||
|
|
||||||
|
LDFLAGS=-ldflags "-s -w" -tags osusergo,netgo
|
||||||
|
|
||||||
|
all: ${DAEMON_NAME}
|
||||||
|
|
||||||
|
.PHONY: ${DAEMON_NAME}
|
||||||
|
|
||||||
|
${DAEMON_NAME}:
|
||||||
|
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
|
||||||
|
|
||||||
|
run-test:
|
||||||
|
bin/${DAEMON_NAME} -expire 1m -listen 127.0.0.1:19134
|
||||||
|
|
||||||
|
install:
|
||||||
|
install -Dm 0755 bin/${DAEMON_NAME} ${DESTDIR}usr/bin/${DAEMON_NAME}
|
||||||
|
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||||
|
|
||||||
|
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm ${DESTDIR}usr/bin/${DAEMON_NAME}
|
||||||
|
rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||||
|
|
||||||
|
rm ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
justcaptcha ver. 1.0
|
||||||
|
====================
|
||||||
|
|
||||||
|
A simple CAPTCHA service implementation.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Get a new captcha
|
||||||
|
|
||||||
|
GET /
|
||||||
|
|
||||||
|
It will return an ID of a new captcha in plaintext.
|
||||||
|
|
||||||
|
#### HTTP codes
|
||||||
|
- 200 if created
|
||||||
|
|
||||||
|
### Get an image for a captcha
|
||||||
|
|
||||||
|
GET /:captcha_id/image
|
||||||
|
|
||||||
|
Responds with an image (e.g. in PNG format).
|
||||||
|
|
||||||
|
#### HTTP codes
|
||||||
|
- 200 if exists
|
||||||
|
- 404 if doesn't exist
|
||||||
|
|
||||||
|
### Submit an answer
|
||||||
|
|
||||||
|
POST /:captcha_id
|
||||||
|
|
||||||
|
It takes one form-data parameter `answer=123456`.
|
||||||
|
|
||||||
|
Responds with empty body and one of HTTP codes.
|
||||||
|
|
||||||
|
#### HTTP codes
|
||||||
|
- 200 if solved
|
||||||
|
- 403 if not solved
|
||||||
|
- 404 if doesn't exist
|
||||||
|
|
||||||
|
### Check if captcha is solved
|
||||||
|
|
||||||
|
GET /:captcha_id
|
||||||
|
|
||||||
|
Responds with empty body and one of HTTP codes.
|
||||||
|
|
||||||
|
#### HTTP codes
|
||||||
|
- 200 if solved
|
||||||
|
- 403 if not solved
|
||||||
|
- 404 if doesn't exist
|
30
build/archlinux/PKGBUILD
Normal file
30
build/archlinux/PKGBUILD
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Maintainer: Alexander "Arav" Andreev <me@arav.top>
|
||||||
|
pkgname=justcaptcha-git
|
||||||
|
pkgver=1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Just a CAPTCHA service"
|
||||||
|
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||||
|
url="https://git.arav.top/Arav/justcaptcha"
|
||||||
|
license=('MIT')
|
||||||
|
groups=()
|
||||||
|
depends=()
|
||||||
|
makedepends=('git' 'go')
|
||||||
|
provides=('justcaptcha')
|
||||||
|
conflicts=('justcaptcha')
|
||||||
|
replaces=()
|
||||||
|
backup=()
|
||||||
|
options=()
|
||||||
|
install=
|
||||||
|
source=('justcaptcha-git::git+https://git.arav.top/Arav/justcaptcha.git')
|
||||||
|
noextract=()
|
||||||
|
md5sums=('SKIP')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/$pkgname"
|
||||||
|
make DESTDIR="$pkgdir/"
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir/$pkgname"
|
||||||
|
make DESTDIR="$pkgdir/" install
|
||||||
|
}
|
79
cmd/justcaptchad/main.go
Normal file
79
cmd/justcaptchad/main.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"justcaptcha/internal/captcha"
|
||||||
|
"justcaptcha/internal/handlers"
|
||||||
|
"justcaptcha/pkg/server"
|
||||||
|
"log"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var listenAddress *string = flag.String("listen", "/var/run/captcha/c.sock", "listen address (ip:port|unix_path)")
|
||||||
|
var captchaExpire *time.Duration = flag.Duration("expire", 5*time.Minute, "CAPTCHA expiration in format XX{s,m,h}, e.g. 5m, 300s")
|
||||||
|
var showVersion *bool = flag.Bool("v", false, "show version")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
if *showVersion {
|
||||||
|
fmt.Println("justcaptchad ver. 1.0\nCopyright (c) 2022 Alexander \"Arav\" Andreev <me@arav.top>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !strings.ContainsRune(*listenAddress, ':') {
|
||||||
|
os.Remove(*listenAddress)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
captcha.Init(*captchaExpire)
|
||||||
|
|
||||||
|
hand := handlers.New(*captchaExpire)
|
||||||
|
srv := server.NewHttpServer()
|
||||||
|
|
||||||
|
srv.GET("/", hand.New)
|
||||||
|
srv.POST("/:captcha", hand.Solve)
|
||||||
|
srv.GET("/:captcha", hand.IsSolved)
|
||||||
|
srv.GET("/:captcha/image", hand.Image)
|
||||||
|
|
||||||
|
var network string
|
||||||
|
if !strings.ContainsRune(*listenAddress, ':') {
|
||||||
|
network = "unix"
|
||||||
|
} else {
|
||||||
|
ap, err := netip.ParseAddrPort(*listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ap.IsValid() {
|
||||||
|
log.Fatalln("ip and/or port provided are not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ap.Addr().Is4() {
|
||||||
|
network = "tcp4"
|
||||||
|
} else if ap.Addr().Is6() {
|
||||||
|
network = "tcp6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.Start(network, *listenAddress); err != nil {
|
||||||
|
log.Fatalln("failed to start a server:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doneSignal := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(doneSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
<-doneSignal
|
||||||
|
|
||||||
|
if err := srv.Stop(); err != nil {
|
||||||
|
log.Fatalln("failed to properly shutdown a server:", err)
|
||||||
|
}
|
||||||
|
}
|
14
go.mod
Normal file
14
go.mod
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module justcaptcha
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fogleman/gg v1.3.0
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect
|
||||||
|
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
|
||||||
|
golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
39
init/systemd/justcaptcha.service
Executable file
39
init/systemd/justcaptcha.service
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=justcaptcha
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=on-failure
|
||||||
|
DynamicUser=yes
|
||||||
|
ExecStart=/usr/bin/justcaptchad -expire 5m -listen '/var/run/justcaptcha/j.sock'
|
||||||
|
|
||||||
|
ReadOnlyPaths=/
|
||||||
|
# Set here path to directory where uploads are stored.
|
||||||
|
NoExecPaths=/
|
||||||
|
ExecPaths=/usr/bin/justcaptchad
|
||||||
|
|
||||||
|
RuntimeDirectory=justcaptcha
|
||||||
|
|
||||||
|
AmbientCapabilities=
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
44
internal/captcha/captcha.go
Normal file
44
internal/captcha/captcha.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"image"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxAnswer = 999999
|
||||||
|
)
|
||||||
|
|
||||||
|
var errorNotFound = errors.New("captcha not found")
|
||||||
|
|
||||||
|
type ID string
|
||||||
|
type Answer string
|
||||||
|
|
||||||
|
type ICaptcha interface {
|
||||||
|
GenerateImage() *image.Image
|
||||||
|
GetImage() *image.Image
|
||||||
|
GetAnswer() Answer
|
||||||
|
Solve(answer Answer) bool
|
||||||
|
IsSolved() bool
|
||||||
|
Expire() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Captcha struct {
|
||||||
|
Image image.Image
|
||||||
|
Answer Answer
|
||||||
|
Solved bool
|
||||||
|
ExpireIn time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateAnswer() Answer {
|
||||||
|
ans, _ := rand.Int(rand.Reader, big.NewInt(maxAnswer))
|
||||||
|
return (Answer(ans.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpireDate(expiration time.Duration) time.Time {
|
||||||
|
return time.Now().Add(expiration)
|
||||||
|
}
|
104
internal/captcha/db.go
Normal file
104
internal/captcha/db.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"image"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var expiredScanInterval = 60 * time.Second
|
||||||
|
|
||||||
|
type ICaptchaDB interface {
|
||||||
|
New(data string) (ICaptcha, ID)
|
||||||
|
SetExpiration(expire time.Duration)
|
||||||
|
Image(id ID) (*image.Image, error)
|
||||||
|
Solve(id ID, answer Answer) (bool, error)
|
||||||
|
IsSolved(id ID) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptchaDB struct {
|
||||||
|
DB map[ID]ICaptcha
|
||||||
|
ExpireInterval time.Duration
|
||||||
|
mut sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExpiration stores expire value and starts a goroutine
|
||||||
|
// that checks for expired CAPTCHAs every minute.
|
||||||
|
func (cdb *CaptchaDB) SetExpiration(expire time.Duration) {
|
||||||
|
cdb.ExpireInterval = expire
|
||||||
|
if expire < expiredScanInterval {
|
||||||
|
expiredScanInterval = expire
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
n := time.Now().Second()
|
||||||
|
sleepFor := expiredScanInterval - (time.Duration(n) * time.Second % expiredScanInterval)
|
||||||
|
time.Sleep(sleepFor)
|
||||||
|
|
||||||
|
for id, captcha := range cdb.DB {
|
||||||
|
if time.Now().Sub(captcha.Expire()) <= 0 {
|
||||||
|
delete(cdb.DB, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New accepts an ICaptcha instance, generates an ID and store it in a database.
|
||||||
|
// `data` string is an additional random data used to generate an ID,
|
||||||
|
// e.g. IP-address.
|
||||||
|
func (cdb *CaptchaDB) New(data string, captcha ICaptcha) (ICaptcha, ID) {
|
||||||
|
idHash := sha256.New()
|
||||||
|
|
||||||
|
idHash.Write([]byte(data))
|
||||||
|
idHash.Write([]byte(strconv.FormatInt(time.Now().UnixMicro(), 16)))
|
||||||
|
idHash.Write([]byte(captcha.GetAnswer()))
|
||||||
|
|
||||||
|
id := ID(base64.RawURLEncoding.EncodeToString(idHash.Sum(nil)))
|
||||||
|
|
||||||
|
cdb.mut.Lock()
|
||||||
|
defer cdb.mut.Unlock()
|
||||||
|
cdb.DB[id] = captcha
|
||||||
|
|
||||||
|
return captcha, id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image returns image for a captcha.
|
||||||
|
func (cdb *CaptchaDB) Image(id ID) (*image.Image, error) {
|
||||||
|
if c, ok := cdb.DB[id]; ok {
|
||||||
|
return c.GetImage(), nil
|
||||||
|
}
|
||||||
|
return nil, errorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve compares given answer with a stored one and if failed
|
||||||
|
// deletes a captcha from database.
|
||||||
|
func (cdb *CaptchaDB) Solve(id ID, answer Answer) (bool, error) {
|
||||||
|
cdb.mut.Lock()
|
||||||
|
defer cdb.mut.Unlock()
|
||||||
|
if c, ok := cdb.DB[id]; ok {
|
||||||
|
ok = c.Solve(answer)
|
||||||
|
if !ok {
|
||||||
|
delete(cdb.DB, id)
|
||||||
|
}
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
return false, errorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSolved checks if captcha was solved and removes it
|
||||||
|
// from a database.
|
||||||
|
func (cdb *CaptchaDB) IsSolved(id ID) (bool, error) {
|
||||||
|
cdb.mut.Lock()
|
||||||
|
defer cdb.mut.Unlock()
|
||||||
|
if c, ok := cdb.DB[id]; ok {
|
||||||
|
ok = c.IsSolved()
|
||||||
|
delete(cdb.DB, id)
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
return false, errorNotFound
|
||||||
|
}
|
91
internal/captcha/dwelling_captcha.go
Normal file
91
internal/captcha/dwelling_captcha.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"image"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dwImageWidth = 160
|
||||||
|
dwImageHeight = 40
|
||||||
|
)
|
||||||
|
|
||||||
|
type DwellingCaptcha struct {
|
||||||
|
Captcha
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDwellingCaptcha(expiration time.Duration) *DwellingCaptcha {
|
||||||
|
return &DwellingCaptcha{
|
||||||
|
Captcha: Captcha{
|
||||||
|
Answer: GenerateAnswer(),
|
||||||
|
ExpireIn: ExpireDate(expiration)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) GenerateImage() *image.Image {
|
||||||
|
ctx := gg.NewContext(dwImageWidth, dwImageHeight)
|
||||||
|
|
||||||
|
// fill background
|
||||||
|
ctx.SetRGB255(0x0a, 0x0a, 0x0a)
|
||||||
|
ctx.Clear()
|
||||||
|
|
||||||
|
// draw text
|
||||||
|
ctx.SetRGB255(0xf5, 0xf5, 0xf5)
|
||||||
|
ctx.Scale(4.0, 2.7)
|
||||||
|
ctx.DrawStringAnchored(string(c.GetAnswer()), 20, 5, .5, .5)
|
||||||
|
|
||||||
|
// draw lines and points
|
||||||
|
|
||||||
|
ctx.SetRGB255(0x9f, 0x2b, 0x68)
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
x0, _ := rand.Int(rand.Reader, big.NewInt(int64(ctx.Height())))
|
||||||
|
x1, _ := rand.Int(rand.Reader, big.NewInt(int64(ctx.Height())))
|
||||||
|
ctx.SetLineWidth(2)
|
||||||
|
r, _ := rand.Int(rand.Reader, big.NewInt(int64(4)))
|
||||||
|
ctx.DrawPoint(float64(x0.Int64()), float64(x1.Int64()), float64(r.Int64()))
|
||||||
|
ctx.DrawLine(float64(x0.Int64()), 0, float64(ctx.Height()), float64(x1.Int64()))
|
||||||
|
ctx.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
x0, _ := rand.Int(rand.Reader, big.NewInt(int64(ctx.Height())))
|
||||||
|
x1, _ := rand.Int(rand.Reader, big.NewInt(int64(ctx.Height())))
|
||||||
|
ctx.SetLineWidth(2)
|
||||||
|
ctx.DrawLine(0, float64(x0.Int64()), float64(x1.Int64()), float64(ctx.Height()))
|
||||||
|
r, _ := rand.Int(rand.Reader, big.NewInt(int64(4)))
|
||||||
|
ctx.DrawPoint(float64(x0.Int64()), float64(x1.Int64()), float64(r.Int64()))
|
||||||
|
ctx.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Image = ctx.Image()
|
||||||
|
|
||||||
|
return &c.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) GetImage() *image.Image {
|
||||||
|
if c.Image == nil {
|
||||||
|
return c.GenerateImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) Solve(answer Answer) bool {
|
||||||
|
c.Solved = c.Answer == answer
|
||||||
|
return c.Solved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) GetAnswer() Answer {
|
||||||
|
return c.Answer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) IsSolved() bool {
|
||||||
|
return c.Solved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DwellingCaptcha) Expire() time.Time {
|
||||||
|
return c.ExpireIn
|
||||||
|
}
|
29
internal/captcha/instance.go
Normal file
29
internal/captcha/instance.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var captchaDb CaptchaDB = CaptchaDB{
|
||||||
|
DB: make(map[ID]ICaptcha)}
|
||||||
|
|
||||||
|
func Init(expiration time.Duration) {
|
||||||
|
captchaDb.SetExpiration(expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(data string, captcha ICaptcha) (ICaptcha, ID) {
|
||||||
|
return captchaDb.New(data, captcha)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Image(id ID) (*image.Image, error) {
|
||||||
|
return captchaDb.Image(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Solve(id ID, answer Answer) (bool, error) {
|
||||||
|
return captchaDb.Solve(id, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSolved(id ID) (bool, error) {
|
||||||
|
return captchaDb.IsSolved(id)
|
||||||
|
}
|
76
internal/handlers/handlers.go
Normal file
76
internal/handlers/handlers.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"justcaptcha/internal/captcha"
|
||||||
|
"justcaptcha/pkg/server"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CaptchaHandlers struct {
|
||||||
|
ExpireIn time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(expiration time.Duration) *CaptchaHandlers {
|
||||||
|
return &CaptchaHandlers{
|
||||||
|
ExpireIn: expiration}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CaptchaHandlers) New(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dc := captcha.NewDwellingCaptcha(h.ExpireIn)
|
||||||
|
_, id := captcha.New(r.RemoteAddr, dc)
|
||||||
|
|
||||||
|
fmt.Fprint(w, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CaptchaHandlers) Image(w http.ResponseWriter, r *http.Request) {
|
||||||
|
captchaID := captcha.ID(server.GetURLParam(r, "captcha"))
|
||||||
|
|
||||||
|
captchaImage, err := captcha.Image(captchaID)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if captchaImage == nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Disposition", "inline; filename=\""+string(captchaID)+"\"")
|
||||||
|
|
||||||
|
png.Encode(w, *captchaImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CaptchaHandlers) Solve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
|
||||||
|
captchaID := captcha.ID(server.GetURLParam(r, "captcha"))
|
||||||
|
answer := captcha.Answer(r.FormValue("answer"))
|
||||||
|
|
||||||
|
ok, err := captcha.Solve(captcha.ID(captchaID), answer)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CaptchaHandlers) IsSolved(w http.ResponseWriter, r *http.Request) {
|
||||||
|
captchaID := captcha.ID(server.GetURLParam(r, "captcha"))
|
||||||
|
|
||||||
|
solved, err := captcha.IsSolved(captchaID)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !solved {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
76
pkg/server/http.go
Normal file
76
pkg/server/http.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpServer struct {
|
||||||
|
server *http.Server
|
||||||
|
router *httprouter.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpServer() *HttpServer {
|
||||||
|
r := httprouter.New()
|
||||||
|
return &HttpServer{
|
||||||
|
server: &http.Server{
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
Handler: r,
|
||||||
|
},
|
||||||
|
router: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HttpServer) GET(path string, handler http.HandlerFunc) {
|
||||||
|
s.router.Handler(http.MethodGet, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HttpServer) POST(path string, handler http.HandlerFunc) {
|
||||||
|
s.router.Handler(http.MethodPost, path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HttpServer) SetNotFoundHandler(handler http.HandlerFunc) {
|
||||||
|
s.router.NotFound = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURLParam wrapper around underlying router for getting URL parameters.
|
||||||
|
func GetURLParam(r *http.Request, param string) string {
|
||||||
|
return httprouter.ParamsFromContext(r.Context()).ByName(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HttpServer) Start(network, address string) error {
|
||||||
|
listener, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if listener.Addr().Network() == "unix" {
|
||||||
|
os.Chmod(address, 0777)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HttpServer) Stop() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := s.server.Shutdown(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user