diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6cdb363 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/dist +/bin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0553ab9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.mktools/ +/dist +/bin \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..815cbc3 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,10 @@ +project_name: altcha +builds: + - id: altcha + main: ./cmd/altcha + goos: + - linux + goarch: + - amd64 + - arm64 + - "386" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e0fb88 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM reg.cadoles.com/proxy_cache/library/golang:1.23.1 AS build + +RUN apt-get update && apt-get install make + +COPY . /src + +WORKDIR /src + +RUN go mod download && make GORELEASER_ARGS="build --rm-dist --single-target --snapshot" goreleaser + +FROM reg.cadoles.com/proxy_cache/library/busybox + +COPY --from=build /src/dist/altcha_linux_amd64_v1 /app +RUN chown -R 1000:1000 /app + +WORKDIR /app + +CMD ["bin/altcha", "run"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..568f5a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +IMAGE_REPO ?= reg.cadoles.com/cadoles/altcha + +GORELEASER_VERSION ?= v1.13.1 +GORELEASER_ARGS ?= release --snapshot --rm-dist + +ALTCHA_VERSION ?= +GIT_COMMIT := $(shell git rev-parse --short HEAD) +DATE_VERSION := $(shell date +%Y.%-m.%-d) +FULL_VERSION := v$(DATE_VERSION)-$(GIT_COMMIT)$(if $(shell git diff --stat),-dirty,) + +.PHONY: test +test: test-go ## Executing tests + +test-go: + go test -v -race ./... + +build: build-altcha + +build-altcha: ## Build executable + CGO_ENABLED=0 go build \ + -v \ + -ldflags "\ + -X 'main.GitRef=$(shell git rev-parse --short HEAD)' \ + -X 'main.ProjectVersion=$(FULL_VERSION)' \ + -X 'main.BuildDate=$(shell date --utc --rfc-3339=seconds)' \ + " \ + -o ./bin/altcha \ + ./cmd/altcha + +run: + bin/altcha $(ATLCHA_CMD) + +.PHONY: goreleaser +goreleaser: + curl -sfL https://goreleaser.com/static/run | VERSION=$(GORELEASER_VERSION) GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) + +build-image: + docker build \ + -t "$(IMAGE_REPO):latest" \ + . + +release-image: .mktools + @[ ! -z "$(MKT_PROJECT_VERSION)" ] || ( echo "Just downloaded mktools. Please re-run command."; exit 1 ) + docker tag "$(IMAGE_REPO):latest" "$(IMAGE_REPO):$(MKT_PROJECT_VERSION)" + docker tag "$(IMAGE_REPO):latest" "$(IMAGE_REPO):$(MKT_PROJECT_SHORT_VERSION)" + docker push "$(IMAGE_REPO):latest" + docker push "$(IMAGE_REPO):$(MKT_PROJECT_VERSION)" + docker push "$(IMAGE_REPO):$(MKT_PROJECT_SHORT_VERSION)" + +.PHONY: mktools +mktools: + rm -rf .mktools + curl -q https://forge.cadoles.com/Cadoles/mktools/raw/branch/master/install.sh | $(SHELL) + +.mktools: + $(MAKE) mktools + +-include .mktools/*.mk \ No newline at end of file diff --git a/README.md b/README.md index e69de29..f3f9eca 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,21 @@ +# Altcha server + +Serveur de génération de challenges altcha et de validation de la solution + +# Utilisation + +Lancer le serveur +``` +altcha run +``` + +# Variables d'environement +| Nom | Description | Valeur par défaut | Requis | +|---------------------|------------------------------------------------------------------------------|--------------------------|--------| +| ALTCHA_PORT | Port d'écoute du serveur | 3333 | Non | +| ALTCHA_HMAC_KEY | Clé d'encodage des signatures | | Oui | +| ALTCHA_MAX_NUMBER | Nombre d'itération maximum pour résoudre le challenge (défini la difficulté) | 1000000 | Non | +| ALTCHA_ALGORITHM | Algorithme de hashage (valeurs possibles: SHA-1, SHA-256, SHA-512) | SHA-256 | Non | +| ALTCHA_SALT | Forcer le salt du challenge | *Généré automatiquement* | Non | +| ALTCHA_EXPIRE | Temps avant expiration du challenge (en secondes) | 600 | Non | +| ALTCHA_CHECK_EXPIRE | Vérifier si le challenge à expiré | 1 | Non | \ No newline at end of file diff --git a/cmd/altcha/main.go b/cmd/altcha/main.go new file mode 100644 index 0000000..af144e9 --- /dev/null +++ b/cmd/altcha/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "forge.cadoles.com/cadoles/altcha-server/internal/command" +) + +func main() { + command.Main( + command.RunCommand(), + command.GenerateCommand(), + command.SolveCommand(), + command.VerifyCommand(), + ) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0f802f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module forge.cadoles.com/cadoles/altcha-server + +go 1.23.0 + +require ( + github.com/altcha-org/altcha-lib-go v0.1.3 + github.com/caarlos0/env/v11 v11.2.2 + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/render v1.0.3 + github.com/urfave/cli/v2 v2.27.4 + gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88 +) + +require ( + cdr.dev/slog v1.6.1 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cbd9d2c --- /dev/null +++ b/go.sum @@ -0,0 +1,96 @@ +cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4= +cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/altcha-org/altcha-lib-go v0.1.3 h1:eW0T6gs4tqKjCIm5QZwerj++IMx2UHq8lFlrtzfIwGg= +github.com/altcha-org/altcha-lib-go v0.1.3/go.mod h1:I8ESLVWR9C58uvGufB/AJDPhaSU4+4Oh3DLpVtgwDAk= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg= +github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88 h1:dsyRrmhp7fl/YaY1YIzz7lm9qfIFI5KpKNbXwuhTULA= +gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88/go.mod h1:bg+TN16Rq2ygLQbB4VDSHQFNouAEzcy3AAutStehllA= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..6057a65 --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,177 @@ +package api + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "time" + + "forge.cadoles.com/cadoles/altcha-server/internal/client" + "forge.cadoles.com/cadoles/altcha-server/internal/config" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" + "gitlab.com/wpetit/goweb/logger" +) + +type Server struct { + port string + client client.Client +} + +func (s *Server) Run(ctx context.Context) { + r := chi.NewRouter() + + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(corsMiddleware) + r.Use(render.SetContentType(render.ContentTypeJSON)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("root.")) + }) + r.Get("/request", s.requestHandler) + r.Get("/verify", s.submitHandler) + r.Get("/verify-spam-filter", s.submitSpamFilterHandler) + + logger.Info(ctx, "altcha server listening on port "+s.port) + if err := http.ListenAndServe(":"+s.port, r); err != nil { + logger.Error(ctx, err.Error()) + } +} + +func (s *Server) requestHandler(w http.ResponseWriter, r *http.Request) { + challenge, err := s.client.Generate() + + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create challenge : %s", err), http.StatusInternalServerError) + return + } + + writeJSON(w, challenge) +} + +func (s *Server) submitHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + formData := r.FormValue("altcha") + if formData == "" { + http.Error(w, "Atlcha payload missing", http.StatusBadRequest) + return + } + + decodedPayload, err := base64.StdEncoding.DecodeString(formData) + if err != nil { + http.Error(w, "Failed to decode Altcha payload", http.StatusBadRequest) + return + } + + var payload map[string]interface{} + if err := json.Unmarshal(decodedPayload, &payload); err != nil { + http.Error(w, "Failed to parse Altcha payload", http.StatusBadRequest) + return + } + + verified, err := s.client.VerifySolution(payload) + + if err != nil || !verified { + http.Error(w, "Invalid Altcha payload", http.StatusBadRequest) + return + } + + writeJSON(w, map[string]interface{}{ + "success": true, + "data": formData, + }) +} + +func (s *Server) submitSpamFilterHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + formData, err := formToMap(r) + if err != nil { + http.Error(w, "Cannot read form data", http.StatusBadRequest) + } + + payload := r.FormValue("altcha") + if payload == "" { + http.Error(w, "Atlcha payload missing", http.StatusBadRequest) + } + + verified, verificationData, err := s.client.VerifyServerSignature(payload) + if err != nil || !verified { + http.Error(w, "Invalid Altcha payload", http.StatusBadRequest) + return + } + + if verificationData.Verified && verificationData.Expire > time.Now().Unix() { + if verificationData.Classification == "BAD" { + http.Error(w, "Classified as spam", http.StatusBadRequest) + return + } + + if verificationData.FieldsHash != "" { + verified, err := s.client.VerifyFieldsHash(formData, verificationData.Fields, verificationData.FieldsHash) + if err != nil || !verified { + http.Error(w, "Invalid fields hash", http.StatusBadRequest) + return + } + } + + writeJSON(w, map[string]interface{}{ + "success": true, + "data": formData, + "verificationData": verificationData, + }) + return + } + + http.Error(w, "Invalid Altcha payload", http.StatusBadRequest) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "*") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +func writeJSON(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) + } +} + +func formToMap(r *http.Request) (map[string][]string, error) { + if err := r.ParseForm(); err != nil { + return nil, err + } + + return r.Form, nil +} + +func NewServer(cfg config.Config) *Server { + client := *client.NewClient(cfg.HmacKey, cfg.MaxNumber, cfg.Algorithm, cfg.Salt, cfg.Expire, cfg.CheckExpire) + + return &Server { + port: cfg.Port, + client: client, + } +} \ No newline at end of file diff --git a/internal/client/main.go b/internal/client/main.go new file mode 100644 index 0000000..85c92d5 --- /dev/null +++ b/internal/client/main.go @@ -0,0 +1,61 @@ +package client + +import ( + "time" + + "github.com/altcha-org/altcha-lib-go" +) + +type Client struct { + hmacKey string + maxNumber int64 + algorithm altcha.Algorithm + salt string + expire string + checkExpire bool +} + +func NewClient(hmacKey string, maxNumber int64, algorithm string, salt string, expire string, checkExpire bool) *Client { + return &Client { + hmacKey: hmacKey, + maxNumber: maxNumber, + algorithm: altcha.Algorithm(algorithm), + salt: salt, + expire: expire, + checkExpire: checkExpire, + } +} + +func (c *Client) Generate() (altcha.Challenge, error) { + expirationDuration, _ := time.ParseDuration(c.expire+"s") + expiration := time.Now().Add(expirationDuration) + + options := altcha.ChallengeOptions{ + HMACKey: c.hmacKey, + MaxNumber: c.maxNumber, + Algorithm: c.algorithm, + Expires: &expiration, + } + + if len(c.salt) > 0 { + options.Salt = c.salt + } + + return altcha.CreateChallenge(options) +} + +func (c *Client) Solve(challenge string) (*altcha.Solution, error) { + return altcha.SolveChallenge(challenge, c.salt, c.algorithm, int(c.maxNumber), 0, make(<-chan struct{})) +} + +func (c *Client) VerifySolution(payload interface{}) (bool, error) { + return altcha.VerifySolution(payload, c.hmacKey, c.checkExpire) +} + +func (c *Client) VerifyServerSignature(payload interface{}) (bool, altcha.ServerSignatureVerificationData, error) { + return altcha.VerifyServerSignature(payload, c.hmacKey) +} + +func (c *Client) VerifyFieldsHash(formData map[string][]string, fields []string, fieldsHash string) (bool, error) { + return altcha.VerifyFieldsHash(formData, fields, fieldsHash, c.algorithm) +} \ No newline at end of file diff --git a/internal/command/common/flags.go b/internal/command/common/flags.go new file mode 100644 index 0000000..a5ee60e --- /dev/null +++ b/internal/command/common/flags.go @@ -0,0 +1,7 @@ +package common + +import "github.com/urfave/cli/v2" + +func Flags() []cli.Flag { + return []cli.Flag{} +} \ No newline at end of file diff --git a/internal/command/generate.go b/internal/command/generate.go new file mode 100644 index 0000000..00a217b --- /dev/null +++ b/internal/command/generate.go @@ -0,0 +1,40 @@ +package command + +import ( + "fmt" + + "forge.cadoles.com/cadoles/altcha-server/internal/client" + "forge.cadoles.com/cadoles/altcha-server/internal/command/common" + "forge.cadoles.com/cadoles/altcha-server/internal/config" + "github.com/caarlos0/env/v11" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/logger" +) + +func GenerateCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "generate", + Usage: "generate a challenge", + Flags: flags, + Action: func(ctx *cli.Context) error { + cfg := config.Config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + c := client.NewClient(cfg.HmacKey, cfg.MaxNumber, cfg.Algorithm, cfg.Salt, cfg.Expire, cfg.CheckExpire) + + challenge, err := c.Generate() + if err != nil { + logger.Error(ctx.Context, err.Error()) + return err + } + + fmt.Printf("%+v\n", challenge) + + return nil + }, + } +} \ No newline at end of file diff --git a/internal/command/main.go b/internal/command/main.go new file mode 100644 index 0000000..6dd426a --- /dev/null +++ b/internal/command/main.go @@ -0,0 +1,49 @@ +package command + +import ( + "context" + "fmt" + "os" + "sort" + + "github.com/urfave/cli/v2" +) + +func Main(commands ...*cli.Command) { + ctx := context.Background() + + app := &cli.App { + Version: "1", + Name: "altcha-server", + Usage: "create challenges and validate solutions for atlcha captcha", + Commands: commands, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + EnvVars: []string{"ALTCHA_DEBUG"}, + Value: false, + }, + }, + } + + app.ExitErrHandler = func (ctx *cli.Context, err error) { + if err == nil { + return + } + + debug := ctx.Bool("debug") + + if !debug { + fmt.Printf("[ERROR] %v\n", err) + } else { + fmt.Printf("%+v", err) + } + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + if err := app.RunContext(ctx, os.Args); err != nil { + os.Exit(1) + } +} \ No newline at end of file diff --git a/internal/command/run.go b/internal/command/run.go new file mode 100644 index 0000000..830c62c --- /dev/null +++ b/internal/command/run.go @@ -0,0 +1,30 @@ +package command + +import ( + "fmt" + + "forge.cadoles.com/cadoles/altcha-server/internal/api" + "forge.cadoles.com/cadoles/altcha-server/internal/command/common" + "forge.cadoles.com/cadoles/altcha-server/internal/config" + "github.com/caarlos0/env/v11" + "github.com/urfave/cli/v2" +) + +func RunCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "run", + Usage: "run the atlcha api server", + Flags: flags, + Action: func(ctx *cli.Context) error { + cfg := config.Config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + api.NewServer(cfg).Run(ctx.Context) + return nil + }, + } +} \ No newline at end of file diff --git a/internal/command/solve.go b/internal/command/solve.go new file mode 100644 index 0000000..c781742 --- /dev/null +++ b/internal/command/solve.go @@ -0,0 +1,47 @@ +package command + +import ( + "fmt" + + "forge.cadoles.com/cadoles/altcha-server/internal/client" + "forge.cadoles.com/cadoles/altcha-server/internal/command/common" + "forge.cadoles.com/cadoles/altcha-server/internal/config" + "github.com/caarlos0/env/v11" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/logger" +) + +func SolveCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "solve", + Usage: "solve the challenge and return the solution", + Flags: flags, + Args: true, + ArgsUsage: "[CHALLENGE] [SALT]", + Action: func(ctx *cli.Context) error { + cfg := config.Config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + challenge := ctx.Args().Get(0) + salt := ctx.Args().Get(1) + + c := client.NewClient(cfg.HmacKey, cfg.MaxNumber, cfg.Algorithm, salt, cfg.Expire, cfg.CheckExpire) + + + solution, err := c.Solve(challenge) + + if err != nil { + logger.Error(ctx.Context, err.Error()) + return nil + } + + fmt.Printf("%+v\n", solution) + + return nil + }, + } +} \ No newline at end of file diff --git a/internal/command/verify.go b/internal/command/verify.go new file mode 100644 index 0000000..4e9b9a2 --- /dev/null +++ b/internal/command/verify.go @@ -0,0 +1,56 @@ +package command + +import ( + "fmt" + "strconv" + + "forge.cadoles.com/cadoles/altcha-server/internal/client" + "forge.cadoles.com/cadoles/altcha-server/internal/command/common" + "forge.cadoles.com/cadoles/altcha-server/internal/config" + "github.com/altcha-org/altcha-lib-go" + "github.com/caarlos0/env/v11" + "github.com/urfave/cli/v2" +) + +func VerifyCommand() *cli.Command { + flags := common.Flags() + + return &cli.Command{ + Name: "verify", + Usage: "verify the solution", + Flags: flags, + Args: true, + ArgsUsage: "[challenge] [salt] [signature] [solution]", + Action: func(ctx *cli.Context) error { + cfg := config.Config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + challenge := ctx.Args().Get(0) + salt := ctx.Args().Get(1) + signature := ctx.Args().Get(2) + solution, _ := strconv.ParseInt(ctx.Args().Get(3), 10, 64) + + c := client.NewClient(cfg.HmacKey, cfg.MaxNumber, cfg.Algorithm, cfg.Salt, cfg.Expire, cfg.CheckExpire) + + payload := altcha.Payload{ + Algorithm: cfg.Algorithm, + Challenge: challenge, + Number: solution, + Salt: salt, + Signature: signature, + } + + verified, err := c.VerifySolution(payload) + + if err != nil { + return err + } + + fmt.Print(verified) + + return nil + }, + } +} \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3f33206 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,11 @@ +package config + +type Config struct { + Port string `env:"ALTCHA_PORT" envDefault:"3333"` + HmacKey string `env:"ALTCHA_HMAC_KEY"` + MaxNumber int64 `env:"ALTCHA_MAX_NUMBER" envDefault:"1000000"` + Algorithm string `env:"ALTCHA_ALGORITHM" envDefault:"SHA-256"` + Salt string `env:"ALTCHA_SALT"` + Expire string `env:"ALTCHA_EXPIRE" envDefault:"600"` + CheckExpire bool `env:"ALTCHA_CHECK_EXPIRE" envDefault:"1"` +} \ No newline at end of file