Removed code for a standalone executable. go min ver was downgraded to 1.12.
This commit is contained in:
parent
4d9daca977
commit
ce1df27e3a
33
Makefile
33
Makefile
@ -1,33 +0,0 @@
|
|||||||
PACKAGE_NAME=justcaptcha
|
|
||||||
TARGET=${PACKAGE_NAME}d
|
|
||||||
|
|
||||||
SYSCTL=${shell which systemctl}
|
|
||||||
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
|
||||||
SYSDDIR=${SYSDDIR_:/%=%}
|
|
||||||
DESTDIR=/
|
|
||||||
|
|
||||||
VERSION=2.0.2
|
|
||||||
|
|
||||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
|
|
||||||
|
|
||||||
all: ${TARGET}
|
|
||||||
|
|
||||||
.PHONY: ${TARGET}
|
|
||||||
|
|
||||||
${TARGET}:
|
|
||||||
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
|
|
||||||
|
|
||||||
run:
|
|
||||||
bin/${TARGET} -expiry 1m -listen 127.0.0.1:19134
|
|
||||||
|
|
||||||
install:
|
|
||||||
install -Dm 0755 bin/${TARGET} ${DESTDIR}usr/bin/${TARGET}
|
|
||||||
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${PACKAGE_NAME}/LICENSE
|
|
||||||
|
|
||||||
install -Dm 0644 init/systemd.service ${DESTDIR}${SYSDDIR}/${PACKAGE_NAME}.service
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
rm ${DESTDIR}usr/bin/${TARGET}
|
|
||||||
rm ${DESTDIR}usr/share/licenses/${PACKAGE_NAME}/LICENSE
|
|
||||||
|
|
||||||
rm ${DESTDIR}${SYSDDIR}/${PACKAGE_NAME}.service
|
|
100
README.md
100
README.md
@ -1,105 +1,9 @@
|
|||||||
justcaptcha
|
justcaptcha
|
||||||
===========
|
===========
|
||||||
|
|
||||||
A simple CAPTCHA service implementation.
|
A simple CAPTCHA implementation.
|
||||||
|
|
||||||
## Service usage
|
An example using built-in "dwelling" implementation.
|
||||||
|
|
||||||
justcaptchad -expiry 10m -listen /var/run/justcaptcha/sock
|
|
||||||
|
|
||||||
`-expiry` takes time for CAPTCHA to be valid for in format X{s,m,h}.
|
|
||||||
|
|
||||||
`-listen` is `ip:port` or `/path/to/unix.sock` to listen on.
|
|
||||||
|
|
||||||
## Service HTTP API
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
All error codes are returned with an error message in `text/plain`.
|
|
||||||
|
|
||||||
### Create a new CAPTCHA
|
|
||||||
|
|
||||||
POST /
|
|
||||||
|
|
||||||
It will return an ID of a new CAPTCHA in `text/plain`.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `201` if created (always being created)
|
|
||||||
|
|
||||||
### Get an image for a CAPTCHA
|
|
||||||
|
|
||||||
GET /:captcha_id/image?style=
|
|
||||||
|
|
||||||
Responds with an image in JPEG format (`image/jpeg`).
|
|
||||||
|
|
||||||
An optional URL parameter `style=` set a name of a CAPTCHA style if it is
|
|
||||||
supported by CAPTCHA implementation. E.g. `style=dark`.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `200` if exists
|
|
||||||
- `404` if doesn't exist
|
|
||||||
- `500` if for some reason an image wasn't created
|
|
||||||
|
|
||||||
### Submit an answer
|
|
||||||
|
|
||||||
POST /:captcha_id
|
|
||||||
|
|
||||||
Accepts `application/x-www-form-urlencoded` content type.
|
|
||||||
|
|
||||||
It takes one parameter `answer=123456`.
|
|
||||||
|
|
||||||
Responds with an empty body and one of the HTTP codes.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `202` if solved
|
|
||||||
- `403` if not solved
|
|
||||||
|
|
||||||
### Check if captcha is solved
|
|
||||||
|
|
||||||
GET /:captcha_id?remove
|
|
||||||
|
|
||||||
Responds with an empty body and one of the HTTP codes.
|
|
||||||
|
|
||||||
If an optional `remove` parameter without a value supplied CAPTCHA will be
|
|
||||||
removed without checking and a HTTP code `204` will be sent. Otherwise, a `403`
|
|
||||||
HTTP code will be sent if it is not solved.
|
|
||||||
|
|
||||||
A `remove` parameter was added because browsers will print an error in a console
|
|
||||||
if HTTP code is not within `2xx`, so to keep a console clean you can provide
|
|
||||||
this parameter.
|
|
||||||
|
|
||||||
This can be useful to remove an unused CAPTCHA from a DB without waiting for it
|
|
||||||
to be expired. E.g. when a visitor requests for a new CAPTCHA or leaving a page.
|
|
||||||
|
|
||||||
#### HTTP codes
|
|
||||||
- `204` if solved
|
|
||||||
- `403` if not solved
|
|
||||||
|
|
||||||
### Example of interaction
|
|
||||||
|
|
||||||
First a client makes a POST request with empty body to create a new CAPTCHA and obtains an ID for it.
|
|
||||||
|
|
||||||
POST /
|
|
||||||
|
|
||||||
As a result we get an ID `n60f2K9JiD5c4qX9MYe90A54nT0nnJrtgfhAjfaWtBg`.
|
|
||||||
|
|
||||||
Then a client requests an image for a new CAPTCHA. E.g. with a dark style.
|
|
||||||
|
|
||||||
GET /n60f2K9JiD5c4qX9MYe90A54nT0nnJrtgfhAjfaWtBg/image?style=dark
|
|
||||||
|
|
||||||
Then a client submits an answer for a CAPTCHA.
|
|
||||||
|
|
||||||
POST 'answer=198807' /n60f2K9JiD5c4qX9MYe90A54nT0nnJrtgfhAjfaWtBg
|
|
||||||
|
|
||||||
And if answer was correct a client gets a HTTP code 202. Or 403 otherwise.
|
|
||||||
|
|
||||||
Then a server checks if CAPTCHA was solved with following request.
|
|
||||||
|
|
||||||
GET /n60f2K9JiD5c4qX9MYe90A54nT0nnJrtgfhAjfaWtBg
|
|
||||||
|
|
||||||
## Library usage
|
|
||||||
|
|
||||||
A simple example using built-in dwelling CAPTCHA implementation.
|
|
||||||
|
|
||||||
Create a new CAPTCHA:
|
Create a new CAPTCHA:
|
||||||
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
|
||||||
pkgname=justcaptcha
|
|
||||||
pkgver=2.0.2
|
|
||||||
pkgrel=2
|
|
||||||
pkgdesc="Just a standalone simple CAPTCHA service"
|
|
||||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
|
||||||
url="https://git.arav.su/Arav/justcaptcha"
|
|
||||||
license=('MIT')
|
|
||||||
makedepends=('go')
|
|
||||||
provides=('justcaptcha')
|
|
||||||
conflicts=('justcaptcha')
|
|
||||||
source=("https://git.arav.su/Arav/justcaptcha/archive/v${pkgver}.tar.gz")
|
|
||||||
md5sums=('SKIP')
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
make VERSION=$pkgver DESTDIR="$pkgdir/"
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
make VERSION=$pkgver DESTDIR="$pkgdir/" install
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.arav.su/Arav/justcaptcha/internal/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var version string
|
|
||||||
|
|
||||||
var listenAddress *string = flag.String("listen", "/var/run/justcaptcha/sock", "listen address (ip:port|unix_path)")
|
|
||||||
var captchaExpiry *time.Duration = flag.Duration("expiry", 10*time.Minute, "CAPTCHA expiry 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.", version, "\nCopyright (c) 2022 Alexander \"Arav\" Andreev <me@arav.su>")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hand := http.NewCaptchaHandlers(*captchaExpiry)
|
|
||||||
srv := http.NewHttpServer()
|
|
||||||
|
|
||||||
srv.POST("/", 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"
|
|
||||||
defer os.Remove(*listenAddress)
|
|
||||||
} else {
|
|
||||||
ap, err := netip.ParseAddrPort(*listenAddress)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
7
go.mod
7
go.mod
@ -1,11 +1,8 @@
|
|||||||
module git.arav.su/Arav/justcaptcha
|
module git.arav.su/Arav/justcaptcha
|
||||||
|
|
||||||
go 1.19
|
go 1.12
|
||||||
|
|
||||||
require (
|
require github.com/fogleman/gg v1.3.0
|
||||||
github.com/fogleman/gg v1.3.0
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -2,8 +2,6 @@ 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/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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=A simple CAPTCHA service for your website
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Restart=on-failure
|
|
||||||
DynamicUser=yes
|
|
||||||
ExecStart=/usr/bin/justcaptchad -expiry 10m -listen /var/run/justcaptcha/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
|
|
@ -1,78 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image/jpeg"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.arav.su/Arav/justcaptcha/pkg/captcha"
|
|
||||||
"git.arav.su/Arav/justcaptcha/pkg/captcha/inmemdb"
|
|
||||||
"git.arav.su/Arav/justcaptcha/pkg/dwcaptcha"
|
|
||||||
)
|
|
||||||
|
|
||||||
const errMsgWrongAnswer = "An answer provided was wrong"
|
|
||||||
const errMsgImageNotFound = "cannot get an image for a non-existing CAPTCHA"
|
|
||||||
|
|
||||||
type CaptchaHandlers struct {
|
|
||||||
expiry time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaHandlers(expiry time.Duration) *CaptchaHandlers {
|
|
||||||
inmemdb.SetExpiry(expiry)
|
|
||||||
return &CaptchaHandlers{expiry: expiry}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CaptchaHandlers) New(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dc := dwcaptcha.NewDwellingCaptcha(h.expiry)
|
|
||||||
_, id := inmemdb.New(r.RemoteAddr, dc)
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
fmt.Fprint(w, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CaptchaHandlers) Image(w http.ResponseWriter, r *http.Request) {
|
|
||||||
captchaID := captcha.ID(getURLParam(r, "captcha"))
|
|
||||||
captchaStyle := r.URL.Query().Get("style")
|
|
||||||
|
|
||||||
captchaImage := inmemdb.Image(captchaID, captchaStyle)
|
|
||||||
if captchaImage == nil {
|
|
||||||
http.Error(w, errMsgImageNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Disposition", "inline; filename=\""+string(captchaID)+"\"")
|
|
||||||
|
|
||||||
jpeg.Encode(w, *captchaImage, &jpeg.Options{Quality: 20})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CaptchaHandlers) Solve(w http.ResponseWriter, r *http.Request) {
|
|
||||||
captchaID := captcha.ID(getURLParam(r, "captcha"))
|
|
||||||
|
|
||||||
r.ParseForm()
|
|
||||||
answer := captcha.Answer(r.FormValue("answer"))
|
|
||||||
|
|
||||||
if ok := inmemdb.Solve(captchaID, answer); !ok {
|
|
||||||
http.Error(w, errMsgWrongAnswer, http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CaptchaHandlers) IsSolved(w http.ResponseWriter, r *http.Request) {
|
|
||||||
captchaID := captcha.ID(getURLParam(r, "captcha"))
|
|
||||||
isJustRemove := r.URL.Query().Has("remove")
|
|
||||||
|
|
||||||
if isJustRemove {
|
|
||||||
inmemdb.Remove(captchaID)
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if solved := inmemdb.IsSolved(captchaID); !solved {
|
|
||||||
http.Error(w, errMsgWrongAnswer, http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
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