From b8cc40590c4f0f6ec8befa263f1d20a3b2f865aa Mon Sep 17 00:00:00 2001 From: s3rj1k Date: Sat, 31 Oct 2020 19:42:13 +0200 Subject: [PATCH] Initial public release --- .gitignore | 2 + .golangci.yml | 40 ++ LICENSE | 21 + Makefile | 28 + README.md | 24 + debian/changelog | 11 + debian/compat | 1 + debian/control | 13 + debian/copyright | 5 + debian/nginx-js-challenge.default | 5 + debian/nginx-js-challenge.init | 99 ++++ debian/nginx-js-challenge.lintian-overrides | 6 + debian/nginx-js-challenge.service | 20 + debian/rules | 13 + debian/source/format | 1 + font.go | 33 ++ gc.go | 46 ++ globals.go | 142 +++++ go.mod | 5 + go.sum | 11 + handle.go | 627 ++++++++++++++++++++ hash.go | 16 + init.go | 69 +++ main.go | 81 +++ nginx/js_challenge_include.conf | 20 + nginx/js_challenge_main.conf | 11 + nginx/js_challenge_server.conf | 70 +++ nginx/vhost.conf | 13 + random.go | 15 + revive.toml | 55 ++ templates.go | 224 +++++++ uuid.go | 25 + 32 files changed, 1752 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/nginx-js-challenge.default create mode 100644 debian/nginx-js-challenge.init create mode 100644 debian/nginx-js-challenge.lintian-overrides create mode 100644 debian/nginx-js-challenge.service create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 font.go create mode 100644 gc.go create mode 100644 globals.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handle.go create mode 100644 hash.go create mode 100644 init.go create mode 100644 main.go create mode 100644 nginx/js_challenge_include.conf create mode 100644 nginx/js_challenge_main.conf create mode 100644 nginx/js_challenge_server.conf create mode 100644 nginx/vhost.conf create mode 100644 random.go create mode 100644 revive.toml create mode 100644 templates.go create mode 100644 uuid.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b9525d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +nginx-js-challenge +.idea/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..44a4e46 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,40 @@ +run: + deadline: 5m + skip-dirs: + - vendor/.* +linters-settings: + errcheck: + check-type-assertions: true + govet: + check-shadowing: true + gocyclo: + min-complexity: 50 + maligned: + suggest-new: true + depguard: + list-type: blacklist + include-go-root: true + packages: + - github.com/davecgh/go-spew/spew + misspell: + locale: US + lll: + line-length: 200 + funlen: + lines: 500 + statements: 500 + gocognit: + min-complexity: 80 + unparam: + # call graph construction algorithm (cha, rta). In general, use cha for libraries, + # and rta for programs with main packages. Default is cha. + algo: cha + prealloc: + for-loops: true +linters: + enable-all: true + disable: + - dupl + - godot + - gomnd + - nestif diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc20a78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 s3rj1k + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c57f1bb --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +GO_BIN ?= go +ENV_BIN ?= env +OUT_BIN = nginx-js-challenge + +export PATH := $(PATH):/usr/local/go/bin + +all: clean build + +build: + $(GO_BIN) mod tidy + $(ENV_BIN) CGO_ENABLED=1 GOOS=linux $(GO_BIN) build -ldflags '-s -w -extldflags "-static"' -o $(OUT_BIN) -v + +update: + $(ENV_BIN) GOPROXY=direct GOPRIVATE="github.com/s3rj1k/*" $(GO_BIN) get -u + $(GO_BIN) get -u github.com/golangci/golangci-lint/cmd/golangci-lint + $(GO_BIN) get -u github.com/mgechev/revive + $(GO_BIN) mod tidy + +clean: + $(GO_BIN) clean + rm -f $(OUT_BIN) + +test: + $(GO_BIN) test -failfast ./... + +lint: + golangci-lint run ./... + revive -config revive.toml -exclude ./vendor/... ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d9d553 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Build +## Build binaries +```shell script +make build +``` + +## Build Deb Package +```shell script +apt install make devscripts debhelper build-essential dh-systemd +debuild -us -uc -b +``` + +# Usage +## Start nginx-js-challenge backend +```shell script +./nginx-js-challenge -address=unix:/run/nginx-js-challenge.sock +``` +## Test nginx-js-challenge backend +```shell script +curl --unix-socket /run/nginx-js-challenge.sock http:/example.com +``` + +## Nginx configuration +The ./nginx dir contains the vhost configuration template. diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..16b4161 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,11 @@ +nginx-js-challenge (20191111) UNRELEASED; urgency=low + + * More internal updates + + -- s3rj1k Thu, 31 Oct 2019 12:00:00 +0200 + +nginx-js-challenge (20190610) UNRELEASED; urgency=low + + * Initial release + + -- s3rj1k Mon, 10 Jun 2019 12:00:00 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..bafb1bd --- /dev/null +++ b/debian/control @@ -0,0 +1,13 @@ +Source: nginx-js-challenge +Section: net +Priority: optional +Maintainer: s3rj1k +Build-Depends: debhelper (>= 9), dh-systemd (>= 1.5) +Standards-Version: 3.9.6 + +Package: nginx-js-challenge +Architecture: amd64 +Depends: ${shlibs:Depends}, lsb-base +Suggests: nginx +Description: Nginx JS Challenge + This package contains simple Nginx JS challenge daemon. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..663e5e5 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,5 @@ +This package was debianized by s3rj1k +on Thu, 16 May 2019 15:00:00 +0200. + +Files: * +Copyright: s3rj1k 2019 diff --git a/debian/nginx-js-challenge.default b/debian/nginx-js-challenge.default new file mode 100644 index 0000000..e7cbd9d --- /dev/null +++ b/debian/nginx-js-challenge.default @@ -0,0 +1,5 @@ +# Defaults for nginx-js-challenge initscript +# sourced by /etc/init.d/nginx-js-challenge + +# Additional options that are passed to nginx-js-challenge +DAEMON_ARGS="" diff --git a/debian/nginx-js-challenge.init b/debian/nginx-js-challenge.init new file mode 100644 index 0000000..dc8c53f --- /dev/null +++ b/debian/nginx-js-challenge.init @@ -0,0 +1,99 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: nginx-js-challenge +# Required-Start: $network $remote_fs $local_fs +# Required-Stop: $network $remote_fs $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Stop/Start nginx-js-challenge +### END INIT INFO + +# Author: s3rj1k + +PATH=/sbin:/usr/sbin:/bin:/usr/bin + +if [ -L $0 ]; then + SCRIPTNAME=`/bin/readlink -f $0` +else + SCRIPTNAME=$0 +fi + +NAME="nginx-js-challenge" +DAEMON=${DAEMON:-/usr/sbin/nginx-js-challenge} +PIDFILE=${PIDFILE:-/run/nginx-js-challenge.pid} +DAEMON_ARGS="" + +defaultconfig=`/usr/bin/basename $SCRIPTNAME` +[ -r /etc/default/$defaultconfig ] && . /etc/default/$defaultconfig + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +do_start() +{ + start-stop-daemon --start --quiet --background --no-close --make-pidfile --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS > /var/log/$NAME.log 2>&1 + RETVAL="$?" + return "$RETVAL" +} + +do_stop() +{ + start-stop-daemon --stop --quiet --oknodo --retry=TERM/30/KILL/5 --pidfile $PIDFILE + RETVAL="$?" + rm -f $PIDFILE + return "$RETVAL" +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + restart) + log_daemon_msg "Restarting" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + log_action_msg "Usage: $SCRIPTNAME {start|stop|status|restart}" + exit 3 + ;; +esac + +exit $RETVAL diff --git a/debian/nginx-js-challenge.lintian-overrides b/debian/nginx-js-challenge.lintian-overrides new file mode 100644 index 0000000..614488e --- /dev/null +++ b/debian/nginx-js-challenge.lintian-overrides @@ -0,0 +1,6 @@ +nginx-js-challenge: hardening-no-relro * +nginx-js-challenge: hardening-no-pie * +nginx-js-challenge: binary-without-manpage * +nginx-js-challenge: new-package-should-close-itp-bug +nginx-js-challenge: description-is-pkg-name * +nginx-js-challenge: init.d-script-does-not-implement-required-option * diff --git a/debian/nginx-js-challenge.service b/debian/nginx-js-challenge.service new file mode 100644 index 0000000..2220448 --- /dev/null +++ b/debian/nginx-js-challenge.service @@ -0,0 +1,20 @@ +[Unit] +Description=Nginx JS challenge service +After=network.target + +[Service] +Type=simple + +Environment="DAEMON_ARGS=" +EnvironmentFile=-/etc/default/nginx-js-challenge + +ExecStart=/usr/sbin/nginx-js-challenge -log-date-time=false ${DAEMON_ARGS} + +Restart=on-failure +RestartSec=10 + +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..c32904e --- /dev/null +++ b/debian/rules @@ -0,0 +1,13 @@ +#!/usr/bin/make -f + +%: + dh $@ --with systemd + +override_dh_builddeb: + dh_builddeb -- -Zgzip + +override_dh_auto_install: + dh_install nginx-js-challenge usr/sbin + dh_install nginx/js_challenge_include.conf /usr/share/doc/nginx-js-challenge/ + dh_install nginx/js_challenge_main.conf /usr/share/doc/nginx-js-challenge/ + dh_install nginx/js_challenge_server.conf /usr/share/doc/nginx-js-challenge/ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/font.go b/font.go new file mode 100644 index 0000000..b55d3c1 --- /dev/null +++ b/font.go @@ -0,0 +1,33 @@ +package main + +import ( + "net/url" + "strings" +) + +// isFontInURL checks that web-font is requested in URL. +func isFontInURL(uri string) bool { + u, err := url.Parse(uri) + if err != nil { + return false + } + + value := strings.ToLower(u.Path) + + switch { + case strings.HasSuffix(value, ".eot"): + return true + case strings.HasSuffix(value, ".otf"): + return true + case strings.HasSuffix(value, ".svg"): + return true + case strings.HasSuffix(value, ".ttf"): + return true + case strings.HasSuffix(value, ".woff"): + return true + case strings.HasSuffix(value, ".woff2"): + return true + } + + return false +} diff --git a/gc.go b/gc.go new file mode 100644 index 0000000..e50d2ed --- /dev/null +++ b/gc.go @@ -0,0 +1,46 @@ +package main + +import ( + "net/http" + "sync" + "time" +) + +func cleanDB(db *sync.Map) { + for { + // sleep inside infinite loop + time.Sleep(15 * time.Second) + + // range over db + db.Range(func(key interface{}, val interface{}) bool { + // cast key to string + if id, ok := key.(string); ok { + // cast value to challenge record + if record, ok := val.(challengeDBRecord); ok { + // check expiration time + if record.Expires.Before(time.Now()) { + Debug.Printf( + "%d, Domain:'%s', ID:'%s', %s\n", + http.StatusOK, record.Domain, + id, messageExpiredRecord, + ) + + // check then id is NOT UUID + if !reUUID.MatchString(id) { + Bot.Printf( + "%d, Domain:'%s', Addr:'%s', UA:'%s'\n", + http.StatusTeapot, record.Domain, + record.Address, record.UserAgent, + ) + } + + // delete key + db.Delete(key) + } + } + } + + return true + }) + } +} diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..e9e4112 --- /dev/null +++ b/globals.go @@ -0,0 +1,142 @@ +package main + +import ( + "html/template" + "log" + "net/http" + "regexp" + "sync" + "time" +) + +const ( + // defines maximum number for cryptographic nonce + maxNonce = 250 + + // authentication cookie name + authenticationName = "0dkZynp3NoRHgFUFbf" + // number of seconds for authentication cookie expiration + authenticationExpirationSeconds = 86400 + + // challenge cookie name + challengeName = "Wg9y31L7XZPkl0v4r7" + // number of seconds for challenge hash expiration + challengeExpirationSeconds = 60 + + // challenge response name + responseName = "dJzSKzFqxk327Yr3" + + // number of nanoseconds in second + nanoSecondsInSecond = 1000000000 + + // HTTP code for non-authorized request, used in nginx redirects + unAuthorizedAccess = http.StatusUnauthorized + + // regex for UUIDv4 validation + regExpUUIDv4 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[8,9,a,b][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` + + // URL for JS hash library + defaultJSHashLibraryURL = "https://cdnjs.cloudflare.com/ajax/libs/js-sha1/0.6.0/sha1.min.js" +) + +const ( + messageOnlyGetMethod = "only GET method" + messageOnlyGetOrPostMethod = "only GET or POST method" + messageOnlyPostMethod = "only POST method" + + messageFailedEntropy = "entropy failure" + + messageFailedHTMLRender = "HTML render failure" + messageFailedHTTPResponse = "HTTP response failure" + + messageExpiredChallenge = "expired challenge" + messageInvalidChallenge = "invalid challenge" + messageInvalidResponse = "invalid response" + + messageExpiredRecord = "expired record" + messageUnknownChallenge = "unknown challenge" + + messageEmptyAuthentication = "empty authentication" + messageExpiredAuthentication = "authentication expired" + messageInvalidAuthenticationDomain = "invalid authentication domain" + messageInvalidUserAgent = "invalid authentication user-agent" + messageUnknownAuthentication = "unknown authentication" + messageValidAuthentication = "valid authentication" + + messageAllowOptionsRequest = "allow OPTIONS method" + messageAllowWebFont = "allow web font" +) + +type challengeDBRecord struct { + // Domain defines valid challenge/authentication domain + Domain string + // UserAgent stores UA that originated from HTTP request + UserAgent string + // Expires defines challenge/authentication TTL + Expires time.Time + + // Address stores address that originated from HTTP request + Address string + + // Nonce defines cryptographic nonce for challenge request + Nonce string +} + +// nolint: gochecknoglobals +var ( + // challenge HTML template + challengeTemplate *template.Template + // challenge Lite HTML template + challengeLiteTemplate *template.Template + + // in memory key:value db + db sync.Map + + // mutex for thread-safety + mu sync.Mutex + + // compiled RegExp for UUIDv4 + reUUID *regexp.Regexp + + // IP:PORT or unix socket path + cmdAddress string + // log date/time + cmdLogDateTime bool + // enable debug logging + cmdDebug bool + + // empty favicon.ico + favicon = []byte{ + 000, 000, 001, 000, 001, 000, 016, 016, + 002, 000, 001, 000, 001, 000, 176, 000, + 000, 000, 022, 000, 000, 000, 040, 000, + 000, 000, 016, 000, 000, 000, 032, 000, + 000, 000, 001, 000, 001, 000, 000, 000, + 000, 000, 128, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 255, 255, 255, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 000, 000, + 000, 000, 000, 000, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, 255, 255, + 000, 000, 255, 255, 000, 000, + } + + // Logging levels + Info *log.Logger + Error *log.Logger + Debug *log.Logger + Bot *log.Logger +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8fd40ec --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/s3rj1k/nginx-js-challenge + +go 1.13 + +require golang.org/x/net v0.0.0-20201021035429-f5854403a974 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..22544d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/handle.go b/handle.go new file mode 100644 index 0000000..d666dc8 --- /dev/null +++ b/handle.go @@ -0,0 +1,627 @@ +package main + +import ( + "errors" + "net/http" + "strconv" + "strings" + "syscall" + "time" + + "golang.org/x/net/publicsuffix" +) + +func faviconHandler(w http.ResponseWriter, r *http.Request) { + if _, err := w.Write(favicon); err != nil { + Error.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusInternalServerError, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageFailedHTTPResponse, + ) + } +} + +func challengeHandle(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + renderHandle(w, r) + case http.MethodPost: + validateHandle(w, r) + case http.MethodOptions: + // OPTIONS is needed for CORS to function properly, we allow all OPTIONS requests when specific header is passed + if strings.EqualFold(r.Header.Get("X-Allow-OPTIONS"), "TRUE") { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusAccepted, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageAllowOptionsRequest, + ) + + return + } + + fallthrough + default: + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusMethodNotAllowed, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageOnlyGetOrPostMethod, + ) + + // set default allowed headers + allowHeader := []string{ + http.MethodGet, + http.MethodPost, + } + + if strings.EqualFold(r.Header.Get("X-Allow-OPTIONS"), "TRUE") { + // add OPTIONS to allowed headers + allowHeader = append(allowHeader, http.MethodOptions) + } + + // return proper HTTP error with headers + w.Header().Set( + "Allow", + strings.Join(allowHeader, ", "), + ) + + http.Error(w, messageOnlyGetOrPostMethod, http.StatusMethodNotAllowed) + + return + } +} + +func renderHandle(w http.ResponseWriter, r *http.Request) { + // allow only GET method + if r.Method != http.MethodGet { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusMethodNotAllowed, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageOnlyGetMethod, + ) + + // return proper HTTP error with headers + w.Header().Set("Allow", http.MethodGet) + http.Error(w, messageOnlyGetMethod, http.StatusMethodNotAllowed) + + return + } + + // define domain for a cookie + domain := r.Header.Get("X-Forwarded-Host") + + // compute wildcard domain cookie when appropriate configuration header present + if strings.EqualFold(r.Header.Get("X-TLDPlusOne"), "TRUE") { + if val, err := publicsuffix.EffectiveTLDPlusOne(r.Header.Get("X-Forwarded-Host")); err == nil { + domain = "." + val + } + } + + // clean old invalid cookies + http.SetCookie(w, &http.Cookie{ + Domain: domain, + Name: authenticationName, + Value: "", + Expires: time.Unix(0, 0), + }) + http.SetCookie(w, &http.Cookie{ + Name: authenticationName, + Value: "", + Expires: time.Unix(0, 0), + }) + + // set to True when captcha requested with lite template flag + var isLiteTemplate bool + + // check for lite template header + if strings.EqualFold(r.Header.Get("X-LiteTemplate"), "TRUE") { + isLiteTemplate = true + } + + // generate new nonce + nonce := genNewNonce() + // generate challenge hash + challenge := getStringHash(r.UserAgent(), nonce) + // set how long cookie is valid + challengeTTL := time.Duration(challengeExpirationSeconds * nanoSecondsInSecond) + // generate expire date for challenge hash + expires := time.Now().Add(challengeTTL) + + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Nonce:'%s', Challenge:'%s', TTL:'%s'\n", + http.StatusOK, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), nonce, + challenge, challengeTTL, + ) + + // get JS HASH library URL + jsHashLibraryURL := r.Header.Get("X-JS-Hash-Library-URL") + if len(jsHashLibraryURL) == 0 { + jsHashLibraryURL = defaultJSHashLibraryURL + } + + // populate struct with needed data for template render + data := struct { + MaxNonce string + ChallengeName string + ResponseName string + JSHashLibraryURL string + }{ + MaxNonce: strconv.Itoa(maxNonce), + ChallengeName: challengeName, + ResponseName: responseName, + JSHashLibraryURL: jsHashLibraryURL, + } + + // store challenge hash to db + db.Store(challenge, + challengeDBRecord{ + Domain: domain, + UserAgent: r.UserAgent(), + Expires: expires, + + Address: r.Header.Get("X-Real-IP"), + Nonce: nonce, + }, + ) + + // https://www.fastly.com/blog/clearing-cache-browser + // https://www.w3.org/TR/clear-site-data/ + // Broken on Google Chrome + // w.Header().Set("Clear-Site-Data", `"cache"`) + + // set cookie for wildcard domain cookie, domain starts with '.' + if strings.HasPrefix(domain, ".") { + http.SetCookie(w, &http.Cookie{ + Domain: domain, + Name: challengeName, + Value: challenge, + Expires: expires, + MaxAge: int(expires.Unix() - time.Now().Unix()), + Secure: false, + HttpOnly: false, + SameSite: http.SameSiteNoneMode, + }) + } else { // non-wildcard cookie + http.SetCookie(w, &http.Cookie{ + Name: challengeName, + Value: challenge, + Expires: expires, + MaxAge: int(expires.Unix() - time.Now().Unix()), + Secure: false, + HttpOnly: false, + SameSite: http.SameSiteStrictMode, + }) + } + + var err error + + // render challenge template + if isLiteTemplate { + err = challengeLiteTemplate.Execute(w, data) + } else { + err = challengeTemplate.Execute(w, data) + } + + if err != nil { + // ignore buffer errors + if errors.Is(err, syscall.EPIPE) { + return + } + + Error.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', %s\n", + http.StatusInternalServerError, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), + messageFailedHTMLRender, + ) + + // return proper HTTP error + http.Error(w, messageFailedHTMLRender, http.StatusInternalServerError) + + return + } +} + +func validateHandle(w http.ResponseWriter, r *http.Request) { + // allow only POST method + if r.Method != http.MethodPost { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusMethodNotAllowed, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageOnlyPostMethod, + ) + + // return proper HTTP error with headers + w.Header().Set("Allow", http.MethodPost) + http.Error(w, messageOnlyPostMethod, http.StatusMethodNotAllowed) + + return + } + + // define domain for a cookie + domain := r.Header.Get("X-Forwarded-Host") + + // compute wildcard domain cookie when appropriate configuration header present + if strings.EqualFold(r.Header.Get("X-TLDPlusOne"), "TRUE") { + if val, err := publicsuffix.EffectiveTLDPlusOne(r.Header.Get("X-Forwarded-Host")); err == nil { + domain = "." + val + } + } + + // remove challenge for wildcard domain cookie, domain starts with '.' + if strings.HasPrefix(domain, ".") { + http.SetCookie(w, &http.Cookie{ + Domain: domain, + Name: challengeName, + Value: "", + Expires: time.Unix(0, 0), + }) + } else { // non-wildcard cookie + http.SetCookie(w, &http.Cookie{ + Name: challengeName, + Value: "", + Expires: time.Unix(0, 0), + }) + } + + // get challenge value from request + challenge := r.PostFormValue(challengeName) + // get challenge response value from request + response := r.PostFormValue(responseName) + + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Response:'%s', Challenge:'%s'\n", + http.StatusOK, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), + response, challenge, + ) + + // https://www.fastly.com/blog/clearing-cache-browser + // https://www.w3.org/TR/clear-site-data/ + // Broken on Google Chrome + // w.Header().Set("Clear-Site-Data", `"cache"`) + + // lookup challenge hash in db + val, ok := db.Load(challenge) + if !ok { + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Challenge:'%s', %s\n", + http.StatusSeeOther, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), challenge, + messageUnknownChallenge, + ) + + // redirect to self + http.Redirect(w, r, "/", http.StatusSeeOther) + + return + } + + // check challenge hash record + record, ok := val.(challengeDBRecord) + if !ok { + Error.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Challenge:'%s', %s\n", + http.StatusInternalServerError, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), challenge, + messageUnknownChallenge, + ) + + // return proper HTTP error + http.Error(w, messageUnknownChallenge, http.StatusInternalServerError) + + return + } + + // check that challenge hash is valid for domain + if !strings.EqualFold(domain, record.Domain) { + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Challenge:'%s', %s\n", + http.StatusSeeOther, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), challenge, + messageInvalidChallenge, + ) + + // redirect to self + http.Redirect(w, r, "/", http.StatusSeeOther) + + return + } + + // check challenge hash expiration + if record.Expires.Before(time.Now()) { + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Challenge:'%s', %s\n", + http.StatusSeeOther, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), challenge, + messageExpiredChallenge, + ) + + // redirect to self + http.Redirect(w, r, "/", http.StatusSeeOther) + + return + } + + // validate challenge response + if getStringHash(r.UserAgent(), response) != challenge { + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Challenge:'%s', %s\n", + http.StatusSeeOther, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), challenge, + messageInvalidResponse, + ) + + // redirect to self + http.Redirect(w, r, "/", http.StatusSeeOther) + + return + } + + // generate ID for cookie value + id, err := genUUID() + if err != nil { + Error.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', %s\n", + http.StatusInternalServerError, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), messageFailedEntropy, + ) + + // return proper HTTP error + http.Error(w, messageFailedEntropy, http.StatusInternalServerError) + + return + } + + // set how long cookie is valid + authenticationTTL := time.Duration(authenticationExpirationSeconds * nanoSecondsInSecond) + // generate expire date for authentication hash + expires := time.Now().Add(authenticationTTL) + + Info.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Response:'%s', Challenge:'%s', Auth:'%s', TTL:'%s'\n", + http.StatusOK, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), response, + challenge, id, authenticationTTL, + ) + + // challenge is valid, invalidating used challenge hash + db.Delete(challenge) + + // store challenge authentication hash to db + db.Store(id, + challengeDBRecord{ + Domain: domain, + UserAgent: r.UserAgent(), + Expires: expires, + + Address: r.Header.Get("X-Real-IP"), + }, + ) + + // set cookie for wildcard domain cookie, domain starts with '.' + if strings.HasPrefix(domain, ".") { + http.SetCookie(w, &http.Cookie{ + Domain: domain, + Name: authenticationName, + Value: id, + Expires: expires, + MaxAge: int(expires.Unix() - time.Now().Unix()), + Secure: false, + HttpOnly: false, + SameSite: http.SameSiteNoneMode, + }) + } else { // non-wildcard cookie + http.SetCookie(w, &http.Cookie{ + Name: authenticationName, + Value: id, + Expires: expires, + MaxAge: int(expires.Unix() - time.Now().Unix()), + Secure: false, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + }) + } + + // redirect to self + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func authHandle(w http.ResponseWriter, r *http.Request) { + // allow web font for '@font-face' request from CSS + if strings.EqualFold(r.Header.Get("X-Allow-Web-Font"), "TRUE") && + isFontInURL(r.Header.Get("X-Original-URI")) { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + http.StatusOK, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageAllowWebFont, + ) + + return + } + + // get challenge cookie value from request + auth, err := r.Cookie(authenticationName) + if err != nil || auth == nil { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', UA:'%s', %s\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + r.UserAgent(), messageEmptyAuthentication, + ) + + // return proper HTTP error + http.Error(w, messageEmptyAuthentication, unAuthorizedAccess) + + return + } + + // define domain for a cookie + domain := r.Header.Get("X-Forwarded-Host") + + // compute wildcard domain cookie when appropriate configuration header present + if strings.EqualFold(r.Header.Get("X-TLDPlusOne"), "TRUE") { + if val, err := publicsuffix.EffectiveTLDPlusOne(r.Header.Get("X-Forwarded-Host")); err == nil { + domain = "." + val + } + } + + // lookup cookie value in db + val, ok := db.Load(auth.Value) + if !ok { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageUnknownAuthentication, + ) + + // return proper HTTP error + http.Error(w, messageUnknownAuthentication, unAuthorizedAccess) + + return + } + + // check challenge hash record + record, ok := val.(challengeDBRecord) + if !ok { + Error.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageUnknownAuthentication, + ) + + // return proper HTTP error + http.Error(w, messageUnknownAuthentication, unAuthorizedAccess) + + return + } + + // check that cookie is valid for domain + if !strings.EqualFold(domain, record.Domain) { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s (%s)\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageInvalidAuthenticationDomain, + record.Domain, + ) + + // when we switch form wildcard to none-wildcard cookie we need to clean DB records for this domain + if strings.EqualFold( + strings.TrimPrefix(domain, "."), + strings.TrimPrefix(record.Domain, "."), + ) { + db.Delete(auth.Value) + } + + // return proper HTTP error + http.Error(w, messageInvalidAuthenticationDomain, unAuthorizedAccess) + + return + } + + // check that cookie is valid for UA + if !strings.EqualFold(r.UserAgent(), record.UserAgent) { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s (%s)\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageInvalidUserAgent, record.UserAgent, + ) + + // return proper HTTP error + http.Error(w, messageInvalidUserAgent, unAuthorizedAccess) + + return + } + + // check cookie expiration + if !record.Expires.After(time.Now()) { + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s\n", + unAuthorizedAccess, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageExpiredAuthentication, + ) + + // return proper HTTP error + http.Error(w, messageExpiredAuthentication, unAuthorizedAccess) + } + + Debug.Printf( + "%d, RAddr:'%s', URL:'%s%s', Dom:'%s', UA:'%s', Auth:'%s', %s\n", + http.StatusOK, + r.Header.Get("X-Real-IP"), + r.Header.Get("X-Forwarded-Host"), + r.Header.Get("X-Original-URI"), + domain, r.UserAgent(), auth.Value, + messageValidAuthentication, + ) +} diff --git a/hash.go b/hash.go new file mode 100644 index 0000000..56260f3 --- /dev/null +++ b/hash.go @@ -0,0 +1,16 @@ +package main + +//nolint: gosec +import ( + "crypto/sha1" + "fmt" + "strings" +) + +// getStringHash creates hash as string from input string +func getStringHash(text ...string) string { + b := []byte(strings.Join(text, "")) + h := sha1.Sum(b) // nolint: gosec + + return fmt.Sprintf("%x", h) +} diff --git a/init.go b/init.go new file mode 100644 index 0000000..9b7ab8d --- /dev/null +++ b/init.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "io" + "io/ioutil" + "log" + "math/rand" + "os" + "regexp" + "time" +) + +// nolint: gochecknoinits +func init() { + var err error + + // command line flags + flag.StringVar(&cmdAddress, "address", "unix:/run/nginx-js-challenge.sock", `IP:PORT or Unix Socket path prefixd with "unix:"`) + flag.BoolVar(&cmdLogDateTime, "log-date-time", true, "add date/time to log output") + flag.BoolVar(&cmdDebug, "debug", false, "enable debug logging") + flag.Parse() + + // seed random source + rand.Seed(time.Now().UTC().UnixNano()) + + // define custom log flags + var logFlag int + if cmdLogDateTime { + logFlag = log.Ldate | log.Ltime + } else { + logFlag = 0 + } + // define debug log output + var debugWriter io.Writer + if cmdDebug { + debugWriter = os.Stdout + logFlag |= log.Lshortfile + } else { + debugWriter = ioutil.Discard + } + + // initialize loggers + Info = log.New( + os.Stdout, + "INFO: ", + logFlag, + ) + Error = log.New( + os.Stderr, + "ERROR: ", + logFlag, + ) + Debug = log.New( + debugWriter, + "DEBUG: ", + logFlag, + ) + Bot = log.New( + os.Stdout, + "BOT: ", + logFlag, + ) + + reUUID, err = regexp.Compile(regExpUUIDv4) + if err != nil { + Error.Fatalf("regexp compile error: %s\n", err.Error()) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..491a5d3 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "html/template" + "net" + "net/http" + "os" + "strings" + "syscall" +) + +func main() { + var err error + + // prepare challenge HTML template + challengeTemplate, err = template.New("challenge.html").Parse(challengeHTMLTemplate) + if err != nil { + Error.Fatalf("challenge service template error: %s\n", err.Error()) + } + + // prepare challenge Lite HTML template + challengeLiteTemplate, err = template.New("challenge-lite.html").Parse(challengeLightHTMLTemplate) + if err != nil { + Error.Fatalf("challenge service template error: %s\n", err.Error()) + } + + // create new HTTP mux and define HTTP routes + mux := http.NewServeMux() + mux.HandleFunc("/", challengeHandle) + mux.HandleFunc("/auth", authHandle) + mux.HandleFunc("/favicon.ico", faviconHandler) + + // run DB cleaner to clean expired keys + go cleanDB(&db) + + // define net listner for HTTP serve function + var nl net.Listener + + switch { + case strings.HasPrefix(cmdAddress, "unix:"): + // get socket path + socket := strings.TrimPrefix(cmdAddress, "unix:") + + // remove old Unix socket + if _, err = os.Stat(socket); !os.IsNotExist(err) { + if err = syscall.Unlink(socket); err != nil { + Error.Fatalf("challenge service socket error: %s\n", err.Error()) + } + } + + // listen on unix socket + nl, err = net.Listen("unix", socket) + if err != nil { + Error.Fatalf("challenge service socket error: %s\n", err.Error()) + } + + // close unix socket on exit + defer func() { + if err = nl.Close(); err != nil { + Error.Fatalf("challenge service socket error: %s\n", err.Error()) + } + }() + + // change unix socket permissions + if err = os.Chmod(socket, os.FileMode(0777)); err != nil { + Error.Fatalf("challenge service socket error: %s\n", err.Error()) + } + default: + // listen on TCP + nl, err = net.Listen("tcp", cmdAddress) + if err != nil { + Error.Fatalf("challenge service TCP error: %s\n", err.Error()) + } + } + + // start challenge server + err = http.Serve(nl, mux) + if err != nil { + Error.Fatalf("challenge service start error: %s\n", err.Error()) + } +} diff --git a/nginx/js_challenge_include.conf b/nginx/js_challenge_include.conf new file mode 100644 index 0000000..2fa1d79 --- /dev/null +++ b/nginx/js_challenge_include.conf @@ -0,0 +1,20 @@ +limit_req zone=zone burst=10; +limit_conn perip 10; + +auth_request /auth; +error_page 401 = @js_challenge; + +# proxy_set_header Host $host; +# proxy_set_header User-Agent $http_user_agent; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Host $host; +# proxy_set_header X-Original-URI $request_uri; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Server-IP $server_addr; +# proxy_set_header X-Unprotected-Remote-IP $remote_addr; + +# proxy_pass_header Set-Cookie; + +# proxy_http_version 1.1; + +# proxy_pass https://{{ BACKEND }}; diff --git a/nginx/js_challenge_main.conf b/nginx/js_challenge_main.conf new file mode 100644 index 0000000..18adcf4 --- /dev/null +++ b/nginx/js_challenge_main.conf @@ -0,0 +1,11 @@ +upstream js_challenge_backend { + server unix:/run/nginx-js-challenge.sock; +} + +limit_req_zone $binary_remote_addr zone=zone:10m rate=30r/s; +limit_conn_zone $binary_remote_addr zone=perip:10m; + +geo $auth { + default 0; + 127.0.0.1/32 1; +} diff --git a/nginx/js_challenge_server.conf b/nginx/js_challenge_server.conf new file mode 100644 index 0000000..fb06372 --- /dev/null +++ b/nginx/js_challenge_server.conf @@ -0,0 +1,70 @@ +client_body_timeout 5s; +client_header_timeout 5s; + +location /robots.txt { + default_type text/plain; + + return 200 'User-agent: *\nDisallow: /\n'; +} + +location = /auth { + internal; + + proxy_set_header Content-Length ""; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Allow-OPTIONS "TRUE"; + # proxy_set_header X-Allow-Web-Font "TRUE"; + + proxy_http_version 1.1; + + proxy_pass_request_body off; + proxy_pass http://js_challenge_backend/auth; +} + +location /header.html { + internal; + + default_type text/html; + + root /var/www/html/captcha/; + + try_files /header.html =503; +} + +location /footer.html { + internal; + + default_type text/html; + + root /var/www/html/captcha/; + + try_files /footer.html =503; +} + +location @js_challenge { + internal; + + proxy_hide_header Content-Type; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-LiteTemplate "TRUE"; + # proxy_set_header X-Allow-OPTIONS "TRUE"; + # proxy_set_header X-Allow-Web-Font "TRUE"; + # proxy_set_header X-JS-Hash-Library-URL "https://cdnjs.cloudflare.com/ajax/libs/js-sha1/0.6.0/sha1.min.js"; + + add_header Cache-Control "no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0"; + + # add_before_body /header.html; + # add_after_body /footer.html; + # addition_types "*"; + + proxy_http_version 1.1; + + proxy_pass http://js_challenge; +} diff --git a/nginx/vhost.conf b/nginx/vhost.conf new file mode 100644 index 0000000..e89cd7a --- /dev/null +++ b/nginx/vhost.conf @@ -0,0 +1,13 @@ +include /usr/share/doc/nginx-js-challenge/js_challenge_main.conf; + +server { + listen 80; + server_name domain.net; + + include /usr/share/doc/nginx-js-challenge/js_challenge_server.conf; + + location / { + include /usr/share/doc/nginx-js-challenge/js_challenge_include.conf; + # ... + } +} diff --git a/random.go b/random.go new file mode 100644 index 0000000..c11d764 --- /dev/null +++ b/random.go @@ -0,0 +1,15 @@ +package main + +import ( + "math/rand" + "strconv" +) + +// genNewNonce generates new cryptographic nonce +func genNewNonce() string { + mu.Lock() + number := strconv.Itoa(rand.Intn(maxNonce)) //nolint: gosec + mu.Unlock() + + return number +} diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000..0153141 --- /dev/null +++ b/revive.toml @@ -0,0 +1,55 @@ +ignoreGeneratedHeader = false +severity = "warning" +confidence = 0.8 +errorCode = 0 +warningCode = 0 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +[rule.if-return] +[rule.increment-decrement] +[rule.var-naming] +[rule.var-declaration] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.time-naming] +[rule.unexported-return] +[rule.indent-error-flow] +[rule.errorf] +[rule.empty-block] +[rule.superfluous-else] +[rule.unused-parameter] +[rule.unreachable-code] +[rule.redefines-builtin-id] + +[rule.argument-limit] + arguments = [6] +[rule.confusing-naming] +[rule.get-return] +[rule.modifies-parameter] +[rule.confusing-results] +[rule.deep-exit] +[rule.flag-parameter] +[rule.unnecessary-stmt] +[rule.struct-tag] +[rule.modifies-value-receiver] +[rule.constant-logical-expr] +[rule.bool-literal-in-expr] +[rule.function-result-limit] + arguments = [4] +[rule.range-val-in-closure] +[rule.waitgroup-by-value] +[rule.atomic] +[rule.empty-lines] +[rule.call-to-gc] +[rule.duplicated-imports] +[rule.import-shadowing] +[rule.bare-return] +[rule.unused-receiver] diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..0138efe --- /dev/null +++ b/templates.go @@ -0,0 +1,224 @@ +package main + +/* + - https://projects.lukehaas.me/css-loaders/ + - https://jshint.com/ + + - https://cdnjs.com/libraries/js-sha1 + - https://cdnjs.cloudflare.com/ajax/libs/js-sha1/0.6.0/sha1.min.js + + - https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST + - https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams +*/ + +const challengeHTMLTemplate = ` + + + + + + + Loading... + + + + + + + + + +
+
+
+ + +` + +const challengeLightHTMLTemplate = ` + + + + + +` diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..5bc5f17 --- /dev/null +++ b/uuid.go @@ -0,0 +1,25 @@ +package main + +import ( + "crypto/rand" + "fmt" + "strings" +) + +// genUUID generates UUIDv4 (random). +func genUUID() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", err + } + + // this make sure that the 13th character is "4" + b[6] = (b[6] | 0x40) & 0x4F + // this make sure that the 17th is "8", "9", "a", or "b" + b[8] = (b[8] | 0x80) & 0xBF + + // assemble UUIDv4 + uuid := fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + + return strings.ToLower(uuid), nil +}