mirror of
https://github.com/Maronato/go-finger.git
synced 2025-03-15 00:34:47 +00:00
initial
This commit is contained in:
commit
0a17831dae
15 changed files with 1308 additions and 0 deletions
169
.dockerignore
Normal file
169
.dockerignore
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
# ---> DB
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
results.bin
|
||||||
|
fingers.yml
|
||||||
|
finger
|
||||||
|
|
||||||
|
# ---> Go
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
/goshort
|
||||||
|
|
||||||
|
# ---> Node
|
||||||
|
#
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# >> Git and github
|
||||||
|
.github
|
||||||
|
.git
|
32
.github/workflows/go.yml
vendored
Normal file
32
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
name: Go
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.54
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
run: make test
|
84
.github/workflows/release.yml
vendored
Normal file
84
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
name: Release
|
||||||
|
# Controls when the workflow will run
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildpush:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- # Get the repository's code
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- # https://github.com/vegardit/docker-gitea-act-runner/issues/23
|
||||||
|
name: Fix docker sock permissions
|
||||||
|
run: sudo chmod 666 /var/run/docker.sock
|
||||||
|
- # https://github.com/docker/setup-qemu-action
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- # https://github.com/docker/setup-buildx-action
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- # https://github.com/docker/login-action
|
||||||
|
name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
# Maybe there is a default env var for this?
|
||||||
|
registry: git.maronato.dev
|
||||||
|
username: ${{ github.repository_owner }}}
|
||||||
|
# Ideally, we should only need to set "permissions: package: write", but
|
||||||
|
# Gitea is having issues with that. For now, this is a manually created
|
||||||
|
# token available user-wise, with the "package:write" permission.
|
||||||
|
password: ${{ secrets.PACKAGE_WRITE_TOKEN }}
|
||||||
|
- # https://github.com/docker/metadata-action
|
||||||
|
# Generate tags and labels for the image
|
||||||
|
# according to the commit and the branch
|
||||||
|
name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
# The container image name needs the custom registry in it.
|
||||||
|
# Maybe there is a default env var for this?
|
||||||
|
images: git.maronato.dev/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
- # httos://github.com/actions/cache
|
||||||
|
name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/go/pkg/mod/
|
||||||
|
/tmp/.go-build-cache
|
||||||
|
/tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
- # https://github.com/docker/build-push-action
|
||||||
|
name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.meta.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
- # Temp fix
|
||||||
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
165
.gitignore
vendored
Normal file
165
.gitignore
vendored
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# ---> DB
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
results.bin
|
||||||
|
fingers.yml
|
||||||
|
finger
|
||||||
|
|
||||||
|
# ---> Go
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
/goshort
|
||||||
|
|
||||||
|
# ---> Node
|
||||||
|
#
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
104
.golangci.yml
Normal file
104
.golangci.yml
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
# - cyclop
|
||||||
|
- decorder
|
||||||
|
# - dogsled
|
||||||
|
# - dupl
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errchkjson
|
||||||
|
- errname
|
||||||
|
- errorlint
|
||||||
|
- execinquery
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- gci
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
# - gocognit
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- goerr113
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goheader
|
||||||
|
- goimports
|
||||||
|
- gomnd
|
||||||
|
- gomodguard
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- gosmopolitan
|
||||||
|
- govet
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- ireturn
|
||||||
|
- loggercheck
|
||||||
|
- maintidx
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- musttag
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nosprintfhostport
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- staticcheck
|
||||||
|
- stylecheck
|
||||||
|
- tagalign
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- thelper
|
||||||
|
- tparallel
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
|
# - varnamelen
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
|
- wsl
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
varnamelen:
|
||||||
|
min-name-length: 2
|
||||||
|
ignore-decls:
|
||||||
|
- l *zap.Logger
|
||||||
|
- l *slog.Logger
|
||||||
|
- l slog.Logger
|
||||||
|
- w http.ResponseWriter
|
||||||
|
- r chi.Router
|
||||||
|
- fs *flag.FlagSet
|
||||||
|
- r *http.Request
|
||||||
|
- eg *errgroup.Group
|
||||||
|
- sw *statuswriter.StatusWriter
|
||||||
|
- db *bun.DB
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- style
|
||||||
|
- performance
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
golang 1.21.0
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.tabSize": 2
|
||||||
|
}
|
48
Dockerfile
Normal file
48
Dockerfile
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Load golang image
|
||||||
|
FROM golang:1.21-alpine as builder
|
||||||
|
|
||||||
|
RUN apk add make
|
||||||
|
|
||||||
|
ARG VERSION=undefined
|
||||||
|
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
|
||||||
|
# Set our build environment
|
||||||
|
ENV GOCACHE=/tmp/.go-build-cache
|
||||||
|
# This variable communicates to the service that it's running inside
|
||||||
|
# a docker container.
|
||||||
|
ENV ENV_DOCKER=true
|
||||||
|
|
||||||
|
# Copy dockerignore files
|
||||||
|
COPY .dockerignore ./
|
||||||
|
|
||||||
|
# Install go deps using the cache
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||||
|
go mod download -x
|
||||||
|
|
||||||
|
COPY Makefile ./
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY main.go ./
|
||||||
|
|
||||||
|
# Build it
|
||||||
|
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||||
|
make backend VERSION=$VERSION
|
||||||
|
|
||||||
|
# Now create a new image with just the binary
|
||||||
|
FROM gcr.io/distroless/static-debian11:nonroot
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set our runtime environment
|
||||||
|
ENV ENV_DOCKER=true
|
||||||
|
|
||||||
|
COPY --from=builder /go/src/app/finger /usr/local/bin/finger
|
||||||
|
|
||||||
|
HEALTHCHECK CMD [ "finger", "healthcheck" ]
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT [ "finger" ]
|
||||||
|
CMD [ "serve" ]
|
23
Makefile
Normal file
23
Makefile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
BINARY_NAME=finger
|
||||||
|
VERSION=$(shell git describe --tags --abbrev=0 || echo "undefined")
|
||||||
|
|
||||||
|
all: lint build test
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -ldflags="-X 'main.version=${VERSION}'" -o ${BINARY_NAME} main.go
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
serve:
|
||||||
|
go run main.go serve
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm ${BINARY_NAME}
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
lint-fix:
|
||||||
|
golangci-lint run --fix
|
0
README.md
Normal file
0
README.md
Normal file
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module git.maronato.dev/maronato/finger
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-alpha.3
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||||
|
golang.org/x/sync v0.3.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
14
go.sum
Normal file
14
go.sum
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-alpha.3 h1:fpyiFVEJvxIFljxM4l5ANSk/UGlM1gyU+hPAr9jhB7M=
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-alpha.3/go.mod h1:H/13DK46DKXy7EaIxPhk2Y0EC8aubKm35nBjBe8AAGc=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
570
main.go
Normal file
570
main.go
Normal file
|
@ -0,0 +1,570 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v4"
|
||||||
|
"github.com/peterbourgon/ff/v4/ffhelp"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appName = "finger"
|
||||||
|
|
||||||
|
// Version of the application.
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Run the server
|
||||||
|
if err := Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Allow graceful shutdown
|
||||||
|
trapSignalsCrossPlatform(cancel)
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// Create a logger and add it to the context
|
||||||
|
l := NewLogger(cfg)
|
||||||
|
ctx = WithLogger(ctx, l)
|
||||||
|
|
||||||
|
// Create a new root command
|
||||||
|
subcommands := []*ff.Command{
|
||||||
|
NewServerCmd(cfg),
|
||||||
|
NewHealthcheckCmd(cfg),
|
||||||
|
}
|
||||||
|
cmd := NewRootCmd(cfg, subcommands)
|
||||||
|
|
||||||
|
// Parse and run
|
||||||
|
if err := cmd.ParseAndRun(ctx, os.Args[1:], ff.WithEnvVarPrefix("WF")); err != nil {
|
||||||
|
if errors.Is(err, ff.ErrHelp) || errors.Is(err, ff.ErrNoExec) {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n%s\n", ffhelp.Command(cmd))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("error running command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerCmd(cfg *Config) *ff.Command {
|
||||||
|
return &ff.Command{
|
||||||
|
Name: "serve",
|
||||||
|
Usage: "serve [flags]",
|
||||||
|
ShortHelp: "Start the webfinger server",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
// Parse the webfinger files
|
||||||
|
fingermap, err := ParseFingerFile(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing finger files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(fmt.Sprintf("Loaded %d webfingers", len(fingermap)))
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
if err := StartServer(ctx, cfg, fingermap); err != nil {
|
||||||
|
return fmt.Errorf("error running server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHealthcheckCmd(cfg *Config) *ff.Command {
|
||||||
|
return &ff.Command{
|
||||||
|
Name: "healthcheck",
|
||||||
|
Usage: "healthcheck [flags]",
|
||||||
|
ShortHelp: "Check if the server is running",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
// Create a new client
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 5 * time.Second, //nolint:gomnd // We want to use a constant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request
|
||||||
|
reqURL := url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: net.JoinHostPort(cfg.Host, cfg.Port),
|
||||||
|
Path: "/healthz",
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error sending request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("server returned status %d", resp.StatusCode) //nolint:goerr113 // We want to return an error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerCtxKey struct{}
|
||||||
|
|
||||||
|
// NewLogger creates a new logger with the given debug level.
|
||||||
|
func NewLogger(cfg *Config) *slog.Logger {
|
||||||
|
level := slog.LevelInfo
|
||||||
|
addSource := false
|
||||||
|
|
||||||
|
if cfg.Debug {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
addSource = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return slog.New(
|
||||||
|
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
AddSource: addSource,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggerFromContext(ctx context.Context) *slog.Logger {
|
||||||
|
l, ok := ctx.Value(loggerCtxKey{}).(*slog.Logger)
|
||||||
|
if !ok {
|
||||||
|
panic("logger not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, loggerCtxKey{}, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/caddyserver/caddy/blob/fbb0ecfa322aa7710a3448453fd3ae40f037b8d1/sigtrap.go#L37
|
||||||
|
// trapSignalsCrossPlatform captures SIGINT or interrupt (depending
|
||||||
|
// on the OS), which initiates a graceful shutdown. A second SIGINT
|
||||||
|
// or interrupt will forcefully exit the process immediately.
|
||||||
|
func trapSignalsCrossPlatform(cancel context.CancelFunc) {
|
||||||
|
go func() {
|
||||||
|
shutdown := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGINT)
|
||||||
|
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
<-shutdown
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf("\nForce quit\n") //nolint:forbidigo // We want to print to stdout
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nGracefully shutting down. Press Ctrl+C again to force quit\n") //nolint:forbidigo // We want to print to stdout
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Debug bool
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
urnPath string
|
||||||
|
fingerPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRootCmd parses the command line flags and returns a Config struct.
|
||||||
|
func NewRootCmd(cfg *Config, subcommands []*ff.Command) *ff.Command {
|
||||||
|
fs := ff.NewFlagSet(appName)
|
||||||
|
|
||||||
|
for _, cmd := range subcommands {
|
||||||
|
cmd.Flags = ff.NewFlagSet(cmd.Name).SetParent(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &ff.Command{
|
||||||
|
Name: appName,
|
||||||
|
Usage: fmt.Sprintf("%s <command> [flags]", appName),
|
||||||
|
ShortHelp: fmt.Sprintf("(%s) A webfinger server", version),
|
||||||
|
Flags: fs,
|
||||||
|
Subcommands: subcommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use 0.0.0.0 as the default host if on docker
|
||||||
|
defaultHost := "localhost"
|
||||||
|
if os.Getenv("ENV_DOCKER") == "true" {
|
||||||
|
defaultHost = "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.BoolVar(&cfg.Debug, 'd', "debug", "Enable debug logging")
|
||||||
|
fs.StringVar(&cfg.Host, 'h', "host", defaultHost, "Host to listen on")
|
||||||
|
fs.StringVar(&cfg.Port, 'p', "port", "8080", "Port to listen on")
|
||||||
|
fs.StringVar(&cfg.urnPath, 'u', "urn-file", "urns.yml", "Path to the URNs file")
|
||||||
|
fs.StringVar(&cfg.fingerPath, 'f', "finger-file", "fingers.yml", "Path to the fingers file")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebFinger struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Links []Link `json:"links,omitempty"`
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebFingerMap map[string]*WebFinger
|
||||||
|
|
||||||
|
func ParseFingerFile(ctx context.Context, cfg *Config) (WebFingerMap, error) {
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
urnMap := make(map[string]string)
|
||||||
|
fingerData := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
fingermap := make(WebFingerMap)
|
||||||
|
|
||||||
|
// Read URNs file
|
||||||
|
file, err := os.ReadFile(cfg.urnPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening URNs file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(file, &urnMap); err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The URNs file must be a map of strings to valid URLs
|
||||||
|
for _, v := range urnMap {
|
||||||
|
if _, err := url.Parse(v); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnMap)), slog.Any("data", urnMap))
|
||||||
|
|
||||||
|
// Read webfingers file
|
||||||
|
file, err = os.ReadFile(cfg.fingerPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening fingers file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(file, &fingerData); err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("Fingers file parsed successfully", slog.Int("number", len(fingerData)), slog.Any("data", fingerData))
|
||||||
|
|
||||||
|
// Parse the webfinger file
|
||||||
|
for k, v := range fingerData {
|
||||||
|
resource := k
|
||||||
|
|
||||||
|
// Remove leading acct: if present
|
||||||
|
if len(k) > 5 && resource[:5] == "acct:" {
|
||||||
|
resource = resource[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key must be a URL or email address
|
||||||
|
if _, err := mail.ParseAddress(resource); err != nil {
|
||||||
|
if _, err := url.Parse(resource); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing webfinger key (%s): %w", k, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add acct: back to the key if it is an email address
|
||||||
|
resource = fmt.Sprintf("acct:%s", resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new webfinger
|
||||||
|
webfinger := &WebFinger{
|
||||||
|
Subject: resource,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the fields
|
||||||
|
for field, value := range v {
|
||||||
|
fieldUrn := field
|
||||||
|
|
||||||
|
// If the key is not already an URN, try to find it in the URNs file
|
||||||
|
if _, err := url.Parse(field); err != nil {
|
||||||
|
if _, ok := urnMap[field]; ok {
|
||||||
|
fieldUrn = urnMap[field]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value is a valid URI, add it to the links
|
||||||
|
if _, err := url.Parse(value); err == nil {
|
||||||
|
webfinger.Links = append(webfinger.Links, Link{
|
||||||
|
Rel: fieldUrn,
|
||||||
|
Href: value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Otherwise add it to the properties
|
||||||
|
if webfinger.Properties == nil {
|
||||||
|
webfinger.Properties = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
webfinger.Properties[fieldUrn] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the webfinger to the map
|
||||||
|
fingermap[resource] = webfinger
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("Webfinger map built successfully", slog.Int("number", len(fingermap)), slog.Any("data", fingermap))
|
||||||
|
|
||||||
|
return fingermap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebfingerHandler(_ *Config, webmap WebFingerMap) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
// Only handle GET requests
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
l.Debug("Method not allowed")
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the query params
|
||||||
|
q := r.URL.Query()
|
||||||
|
|
||||||
|
// Get the resource
|
||||||
|
resource := q.Get("resource")
|
||||||
|
if resource == "" {
|
||||||
|
l.Debug("No resource provided")
|
||||||
|
http.Error(w, "No resource provided", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate resource
|
||||||
|
webfinger, ok := webmap[resource]
|
||||||
|
if !ok {
|
||||||
|
l.Debug("Resource not found")
|
||||||
|
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the content type
|
||||||
|
w.Header().Set("Content-Type", "application/jrd+json")
|
||||||
|
|
||||||
|
// Write the response
|
||||||
|
if err := json.NewEncoder(w).Encode(webfinger); err != nil {
|
||||||
|
l.Debug("Error encoding json")
|
||||||
|
http.Error(w, "Error encoding json", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("Webfinger request successful")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HealthCheckHandler(_ *Config) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseWrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapResponseWriter(w http.ResponseWriter) *ResponseWrapper {
|
||||||
|
return &ResponseWrapper{w, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWrapper) WriteHeader(code int) {
|
||||||
|
w.status = code
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWrapper) Status() int {
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWrapper) Write(b []byte) (int, error) {
|
||||||
|
if w.status == 0 {
|
||||||
|
w.status = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := w.ResponseWriter.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error writing response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWrapper) Unwrap() http.ResponseWriter {
|
||||||
|
return w.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Wrap the response writer
|
||||||
|
wrapped := WrapResponseWriter(w)
|
||||||
|
|
||||||
|
// Call the next handler
|
||||||
|
next.ServeHTTP(wrapped, r)
|
||||||
|
|
||||||
|
status := wrapped.Status()
|
||||||
|
|
||||||
|
// Log the request
|
||||||
|
lg := l.With(
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("path", r.URL.Path),
|
||||||
|
slog.Int("status", status),
|
||||||
|
slog.String("remote", r.RemoteAddr),
|
||||||
|
slog.Duration("duration", time.Since(start)),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case status >= http.StatusInternalServerError:
|
||||||
|
lg.Error("Server error")
|
||||||
|
case status >= http.StatusBadRequest:
|
||||||
|
lg.Info("Client error")
|
||||||
|
default:
|
||||||
|
lg.Info("Request completed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReadTimeout is the maximum duration for reading the entire
|
||||||
|
// request, including the body.
|
||||||
|
ReadTimeout = 5 * time.Second
|
||||||
|
// WriteTimeout is the maximum duration before timing out
|
||||||
|
// writes of the response.
|
||||||
|
WriteTimeout = 10 * time.Second
|
||||||
|
// IdleTimeout is the maximum amount of time to wait for the
|
||||||
|
// next request when keep-alives are enabled.
|
||||||
|
IdleTimeout = 30 * time.Second
|
||||||
|
// ReadHeaderTimeout is the amount of time allowed to read
|
||||||
|
// request headers.
|
||||||
|
ReadHeaderTimeout = 2 * time.Second
|
||||||
|
// RequestTimeout is the maximum duration for the entire
|
||||||
|
// request.
|
||||||
|
RequestTimeout = 7 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartServer(ctx context.Context, cfg *Config, webmap WebFingerMap) error {
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
// Create the server mux
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/.well-known/webfinger", WebfingerHandler(cfg, webmap))
|
||||||
|
mux.Handle("/healthz", HealthCheckHandler(cfg))
|
||||||
|
|
||||||
|
// Create a new server
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: net.JoinHostPort(cfg.Host, cfg.Port),
|
||||||
|
BaseContext: func(_ net.Listener) context.Context {
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
Handler: LoggingMiddleware(
|
||||||
|
RecoveryHandler(
|
||||||
|
http.TimeoutHandler(mux, RequestTimeout, "request timed out"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ReadHeaderTimeout: ReadHeaderTimeout,
|
||||||
|
ReadTimeout: ReadTimeout,
|
||||||
|
WriteTimeout: WriteTimeout,
|
||||||
|
IdleTimeout: IdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the errorgroup that will manage the server execution
|
||||||
|
eg, egCtx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
eg.Go(func() error {
|
||||||
|
l.Info("Starting server", slog.String("addr", srv.Addr))
|
||||||
|
|
||||||
|
// Use the global context for the server
|
||||||
|
srv.BaseContext = func(_ net.Listener) context.Context {
|
||||||
|
return egCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.ListenAndServe() //nolint:wrapcheck // We wrap the error in the errgroup
|
||||||
|
})
|
||||||
|
// Gracefully shutdown the server when the context is done
|
||||||
|
eg.Go(func() error {
|
||||||
|
// Wait for the context to be done
|
||||||
|
<-egCtx.Done()
|
||||||
|
|
||||||
|
l.Info("Shutting down server")
|
||||||
|
// Disable the cancel since we don't wan't to force
|
||||||
|
// the server to shutdown if the context is canceled.
|
||||||
|
noCancelCtx := context.WithoutCancel(egCtx)
|
||||||
|
|
||||||
|
return srv.Shutdown(noCancelCtx) //nolint:wrapcheck // We wrap the error in the errgroup
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.RegisterOnShutdown(func() {
|
||||||
|
l.Info("Server shutdown complete")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ignore the error if the context was canceled
|
||||||
|
if err := eg.Wait(); err != nil && ctx.Err() == nil {
|
||||||
|
return fmt.Errorf("server exited with error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecoveryHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
l := LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := recover()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("Panic", slog.Any("error", err))
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
60
main_test.go
Normal file
60
main_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
finger "git.maronato.dev/maronato/finger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkGetWebfinger(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg := &finger.Config{}
|
||||||
|
l := finger.NewLogger(cfg)
|
||||||
|
|
||||||
|
ctx = finger.WithLogger(ctx, l)
|
||||||
|
resource := "acct:user@example.com"
|
||||||
|
webmap := finger.WebFingerMap{
|
||||||
|
resource: {
|
||||||
|
Subject: resource,
|
||||||
|
Links: []finger.Link{
|
||||||
|
{
|
||||||
|
Rel: "http://webfinger.net/rel/avatar",
|
||||||
|
Href: "https://example.com/avatar.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]string{
|
||||||
|
"example": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"acct:other": {
|
||||||
|
Subject: "acct:other",
|
||||||
|
Links: []finger.Link{
|
||||||
|
{
|
||||||
|
Rel: "http://webfinger.net/rel/avatar",
|
||||||
|
Href: "https://example.com/avatar.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]string{
|
||||||
|
"example": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := finger.WebfingerHandler(&finger.Config{}, webmap)
|
||||||
|
|
||||||
|
r, _ := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf("/.well-known/webfinger?resource=%s", resource),
|
||||||
|
http.NoBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
25
urns.yml
Normal file
25
urns.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# maps string keys to best practice fully qualified URNs
|
||||||
|
|
||||||
|
# some references:
|
||||||
|
# http://webfinger.net/rel/
|
||||||
|
# http://www.packetizer.com/webfinger/link_relations.html
|
||||||
|
|
||||||
|
# names of people
|
||||||
|
name: "http://schema.org/name"
|
||||||
|
full_name: "http://schema.org/name"
|
||||||
|
|
||||||
|
# pictures of people
|
||||||
|
avatar: http://webfinger.net/rel/avatar
|
||||||
|
picture: http://webfinger.net/rel/avatar
|
||||||
|
photo: http://webfinger.net/rel/avatar
|
||||||
|
|
||||||
|
# homepages of people
|
||||||
|
profile_page: http://webfinger.net/rel/profile-page
|
||||||
|
profile: http://webfinger.net/rel/profile-page
|
||||||
|
website: http://webfinger.net/rel/profile-page
|
||||||
|
url: http://webfinger.net/rel/profile-page
|
||||||
|
homepage: http://webfinger.net/rel/profile-page
|
||||||
|
|
||||||
|
# OpenID Connect
|
||||||
|
openid: http://openid.net/specs/connect/1.0/issuer
|
||||||
|
open_id: http://openid.net/specs/connect/1.0/issuer
|
Loading…
Add table
Reference in a new issue