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