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
|
||||
===========
|
||||
|
||||
A simple CAPTCHA service implementation.
|
||||
A simple CAPTCHA implementation.
|
||||
|
||||
## Service usage
|
||||
|
||||
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.
|
||||
An example using built-in "dwelling" implementation.
|
||||
|
||||
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
|
||||
|
||||
go 1.19
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
)
|
||||
require github.com/fogleman/gg v1.3.0
|
||||
|
||||
require (
|
||||
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/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/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-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