mirror of
https://github.com/dcarrillo/whatismyip.git
synced 2026-07-23 22:45:46 +00:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
04d6927cf5
|
|||
| 49a63ef071 | |||
|
a36a5504fa
|
|||
| 2e32a20f60 | |||
|
3874f6af3f
|
|||
| ff97f2c550 | |||
| 49ffcadb0a | |||
|
210e8c7cdb
|
|||
|
bc27f8193f
|
|||
| 52b6302615 | |||
|
9aa97e62cc
|
|||
|
17c615b93d
|
|||
| 5a0c1c19fc | |||
| c3d113447d | |||
| 473c859163 | |||
| 5f1af4a2b4 | |||
| e8d3a20781 | |||
|
ec1de1fa81
|
|||
|
00749ae529
|
|||
| 9b87d28433 | |||
| 751e5f3885 | |||
|
f3216cdf21
|
|||
| f70f4c6b65 | |||
|
d3849cec02
|
|||
|
00b1661ef9
|
|||
|
10c199109a
|
|||
| b5fe362183 | |||
|
95e7742c56
|
|||
|
680aeefeab
|
|||
| 15136359ae | |||
|
64011f9e99
|
|||
|
f020abc228
|
|||
| 24b05c0015 | |||
|
f2da841307
|
|||
|
5bb5c974dd
|
|||
|
159c30f2f0
|
|||
|
1539ba1987
|
|||
| 4492f77d87 | |||
|
aaf8a3b163
|
|||
|
c37642c6c1
|
|||
|
f3a6f27e99
|
|||
|
f167424e4f
|
|||
|
789cc6939e
|
|||
|
b57beded8f
|
|||
|
d29e238beb
|
|||
|
5d3dcb4b8e
|
|||
| 71a0f37abb | |||
|
c8d6da5ebd
|
|||
| 7caf4ad4a8 | |||
| d13ea29071 | |||
| b11f15ecfe | |||
|
454f65f087
|
|||
|
1988241b98
|
|||
|
901345a337
|
|||
| 0c14419e7e | |||
| db111642d2 | |||
| d5b1373e17 | |||
| ba8a2ec494 | |||
| f8e27bef56 | |||
| 2bbeeb34c5 | |||
| 0090b794ee | |||
| 93f561d6ef | |||
| 9da6d2fec5 | |||
|
8e3d731719
|
|||
|
d5b244dc5f
|
|||
|
d767afd658
|
|||
|
f4fd79737e
|
|||
|
2ab6b29ed5
|
|||
|
55e6cd4816
|
|||
|
a490d5f10e
|
|||
| 994a12da5a | |||
| 91deff4a14 | |||
| 81c3a4fbb0 | |||
| 5b85eef7eb | |||
|
c54cf5a456
|
|||
|
7dfa0a2e6d
|
|||
| 68ef680439 | |||
| bd42f712ea | |||
|
0b31633309
|
|||
|
b5fa3be506
|
|||
|
8783db018b
|
|||
|
e60d1ae5b7
|
|||
|
84a767ade0
|
|||
|
19c72f94a5
|
|||
|
de78dcdf52
|
|||
|
eb200ddd81
|
|||
|
5c4ac4a3ee
|
|||
|
ee328892d6
|
|||
|
04d983d671
|
|||
|
202518d547
|
|||
|
52d14fe78f
|
|||
|
8aadc4fbb6
|
|||
| b6391d8fd1 | |||
|
35fac1bd57
|
|||
|
b13a30c354
|
|||
|
5982683cdd
|
|||
|
1a986a029f
|
|||
|
ed0ddccab5
|
|||
|
20ae50c115
|
|||
|
9763ed0e29
|
|||
|
88691a5149
|
|||
|
6b7fc0bc6a
|
|||
| c5a659ff64 | |||
|
bd06da7b2b
|
|||
|
b61d64a755
|
|||
|
3df794ecc4
|
|||
|
ca1d002974
|
|||
|
1ee7256506
|
|||
|
7c70abf07f
|
|||
|
9070e9a2c2
|
@@ -0,0 +1,52 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '21 21 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- run: |
|
||||
echo "Build"
|
||||
make build
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
+108
-25
@@ -8,59 +8,142 @@ on:
|
||||
- '*'
|
||||
pull_request:
|
||||
|
||||
concurrency: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
make: ["lint", "unit-test", "integration-test"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.18"
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Lint
|
||||
run: make lint
|
||||
- name: ${{ matrix.make }}
|
||||
run: make ${{ matrix.make }}
|
||||
|
||||
- name: Tests
|
||||
run: make test
|
||||
|
||||
deploy:
|
||||
build-binaries:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
strategy:
|
||||
matrix:
|
||||
goosarch: [linux-amd64]
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
run: make build VERSION="$RELEASE_VERSION" GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
BASE="whatismyip-$RELEASE_VERSION-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
cp whatismyip "${BASE}.exe"
|
||||
zip "${BASE}.zip" "${BASE}.exe" LICENSE README.md
|
||||
sha256sum "${BASE}.zip" > "${BASE}.zip.sha256"
|
||||
else
|
||||
tar zcvf "${BASE}.tar.gz" whatismyip LICENSE README.md
|
||||
sha256sum "${BASE}.tar.gz" > "${BASE}.tar.gz.sha256"
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: binary-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: |
|
||||
whatismyip-${{ env.RELEASE_VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.*
|
||||
if-no-files-found: error
|
||||
|
||||
publish-docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Sign in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: ARG_VERSION=${{ env.RELEASE_VERSION }}
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/whatismyip:${{ env.RELEASE_VERSION }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/whatismyip:latest
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-binaries, publish-docker]
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Sign in to dockerhub
|
||||
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: binary-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Deploy image
|
||||
run: make docker-push VERSION=$RELEASE_VERSION
|
||||
|
||||
- name: Build
|
||||
run: make build VERSION=$RELEASE_VERSION
|
||||
|
||||
- name: Prepare release
|
||||
- name: Generate changelog
|
||||
run: |
|
||||
git log $(git describe HEAD~ --tags --abbrev=0)..HEAD --pretty='format:%h - %s <%an>' --no-merges > changelog.txt
|
||||
tar zcvf whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz whatismyip LICENSE README.md
|
||||
sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body_path: changelog.txt
|
||||
files: |
|
||||
whatismyip-${{env.RELEASE_VERSION}}-${{matrix.goosarch}}.tar.gz
|
||||
whatismyip-${{env.RELEASE_VERSION}}-${{matrix.goosarch}}.tar.gz.sha256
|
||||
whatismyip-${{ env.RELEASE_VERSION }}-*.tar.gz
|
||||
whatismyip-${{ env.RELEASE_VERSION }}-*.tar.gz.sha256
|
||||
whatismyip-${{ env.RELEASE_VERSION }}-*.zip
|
||||
whatismyip-${{ env.RELEASE_VERSION }}-*.zip.sha256
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
whatismyip
|
||||
dist/
|
||||
@@ -0,0 +1,72 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- errcheck
|
||||
- errorlint
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
revive:
|
||||
confidence: 0.8
|
||||
severity: warning
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- revive
|
||||
path: internal/metrics/
|
||||
text: "var-naming: avoid package names that conflict with Go standard library package names"
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
@@ -0,0 +1,68 @@
|
||||
# whatismyip
|
||||
|
||||
Single-binary Go service: HTTP/TLS/QUIC server that returns the client's IP, geolocation, ASN, headers, TCP port scan, and DNS discovery. Uses [gin](https://github.com/gin-gonic/gin) + [httprouter](https://github.com/julienschmidt/httprouter). Requires Go >= 1.25.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
make build # CGO_ENABLED=0 -> ./whatismyip (GOOS/GOARCH for cross-compile)
|
||||
make build-all # cross-compile all 5 platforms into dist/
|
||||
make unit-test # go test -count=1 -race -short -cover ./...
|
||||
make integration-test # requires Docker; runs integration-tests/ module
|
||||
make test # unit-test + integration-test
|
||||
make lint # gofmt -l + golangci-lint + shadow (auto-installs tools)
|
||||
make docker-build # local single-arch build (root Dockerfile)
|
||||
make docker-run # builds dev image + runs with test DBs, ports :8080 :8081 :9100
|
||||
```
|
||||
|
||||
**Order and quirks**: `lint -> unit-test -> integration-test`. Integration tests use testcontainers-go (needs Docker). They are a separate Go module with its own `go.mod` — run from the `integration-tests/` directory. They use `test/Dockerfile` (distinct from the root `Dockerfile`).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
cmd/whatismyip.go # entrypoint: parses flags, wires servers, runs
|
||||
├── server/server.go # Manager: Start/Stop/SIGHUP-reload for all server types
|
||||
├── server/tcp.go # plain HTTP
|
||||
├── server/tls.go # TLS/HTTP2
|
||||
├── server/quic.go # HTTP/3 (requires TLS server)
|
||||
├── server/dns.go # micro-DNS server for discovery
|
||||
├── server/prometheus.go # separate metrics endpoint
|
||||
├── router/setup.go # Gin route registration
|
||||
│ ├── router/generic.go # /, /client-port, /all, /json
|
||||
│ ├── router/geo.go # /geo/*, /asn/*
|
||||
│ ├── router/headers.go # /headers, /:header
|
||||
│ ├── router/dns.go # DNS discovery HTTP handler
|
||||
│ ├── router/port_scanner.go # /scan/tcp/:port
|
||||
│ └── router/templates.go # embedded HTML template
|
||||
├── service/geo.go # GeoIP lookup service (MaxMind MMDB)
|
||||
├── service/port_scanner.go
|
||||
├── resolver/setup.go # authoritative micro-DNS server (miekg/dns)
|
||||
├── internal/setting/ # flag parsing + resolver YAML config
|
||||
├── internal/httputils/ # header filtering and formatting
|
||||
├── internal/metrics/ # Prometheus counters/histograms (opt-in)
|
||||
├── internal/validator/uuid/
|
||||
├── internal/core/version.go
|
||||
└── models/geo.go # GeoDB wrapper around oschwald/maxminddb-golang
|
||||
```
|
||||
|
||||
## Key facts
|
||||
|
||||
- **DNS discovery**: enabled via `-resolver test/resolver.yml`. The resolver is an authoritative DNS server answering for a subdomain. Clients prove their DNS provider by resolving `<uuid>.dns.<domain>`.
|
||||
- **Geo**: both `-geoip2-city` and `-geoip2-asn` required together (or omitted). Uses test DBs in `test/`.
|
||||
- **Trusted headers**: `-trusted-header X-Real-IP` for proxy mode. When set without `-trusted-port-header`, client port shows "unknown".
|
||||
- **SIGHUP**: reloads GeoIP databases and restarts all servers without full stop.
|
||||
- **HTTP/3**: requires `-tls-bind`; reuses its port for UDP.
|
||||
- **Prometheus**: separate process/port via `-metrics-bind`. Metrics prefixed `whatismyip_*`.
|
||||
- **Port scan**: enabled by default; disable with `-disable-scan`.
|
||||
- **Embedded template**: `router/templates.go` has the default `home` template. `-template` overrides.
|
||||
- **Version**: injected at build via `-ldflags="-X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'"`.
|
||||
|
||||
## Testing quirks
|
||||
|
||||
- Integration tests need Docker (testcontainers-go) and will take longer. They run against a real containerized build.
|
||||
- Unit tests in `internal/setting/`, `internal/httputils/`, `internal/metrics/`, `internal/validator/uuid/`, `models/`, `router/`, and `service/` use test DBs from `test/`.
|
||||
- The router test reads `test/` resources by relative path — run from repo root.
|
||||
|
||||
## Lint
|
||||
|
||||
Uses `golangci-lint` v2 config (`.golangci.yaml`) with strict `revive` rules and `goimports` as formatter. One exception: `internal/metrics/` gets a pass on `var-naming` (package name conflicts with stdlib `metrics`). Also runs `shadow` and `gofmt -l -d`.
|
||||
+10
-11
@@ -1,21 +1,20 @@
|
||||
FROM golang:1.18-alpine as builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
ARG ARG_VERSION
|
||||
ENV VERSION $ARG_VERSION
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV VERSION=$ARG_VERSION
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ go mod download -x
|
||||
COPY . .
|
||||
|
||||
RUN apk add make git && make build VERSION=$VERSION
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ apk --no-cache add make ca-certificates \
|
||||
&& update-ca-certificates \
|
||||
&& GOOS=$TARGETOS GOARCH=$TARGETARCH make build
|
||||
|
||||
# Build final image
|
||||
FROM scratch
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/whatismyip /usr/bin/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
ENTRYPOINT ["whatismyip"]
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2021 Daniel Carrillo
|
||||
Copyright 2024 Daniel Carrillo
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,63 +1,65 @@
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
VERSION ?= devel-$(shell git rev-parse --short HEAD)
|
||||
DOCKER_URL ?= dcarrillo/whatismyip
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
.PHONY: test
|
||||
test: unit-test integration-test
|
||||
|
||||
.PHONY: unit-test
|
||||
unit-test:
|
||||
go test -race -short -cover ./...
|
||||
go test -count=1 -race -short -cover ./...
|
||||
|
||||
.PHONY: integration-test
|
||||
integration-test:
|
||||
go test ./integration-tests -v
|
||||
cd integration-tests && go test -count=1 -v ./...
|
||||
|
||||
.PHONY: install-tools
|
||||
install-tools:
|
||||
@command golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.45.0; \
|
||||
@if ! command -v golangci-lint &> /dev/null; then \
|
||||
echo "Installing golangci-lint"; \
|
||||
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(GOPATH)/bin; \
|
||||
fi
|
||||
|
||||
@command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@v0.1.10; \
|
||||
@if ! command -v revive &> /dev/null; then \
|
||||
echo "Installing revive..."; \
|
||||
go install github.com/mgechev/revive@latest; \
|
||||
fi
|
||||
|
||||
@if ! command -v shadow &> /dev/null; then \
|
||||
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest; \
|
||||
fi
|
||||
|
||||
@command $(GOPATH)/golines > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go install github.com/segmentio/golines@latest; \
|
||||
fi
|
||||
.PHONY: lint
|
||||
lint: install-tools
|
||||
gofmt -l . && test -z $$(gofmt -l .)
|
||||
golines -l . && test -z $$(golines -l .)
|
||||
golangci-lint run
|
||||
shadow ./...
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd
|
||||
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd
|
||||
|
||||
build-all:
|
||||
@mkdir -p dist
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-linux-amd64
|
||||
GOOS=linux GOARCH=arm64 $(MAKE) build && mv whatismyip dist/whatismyip-linux-arm64
|
||||
GOOS=darwin GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-darwin-amd64
|
||||
GOOS=darwin GOARCH=arm64 $(MAKE) build && mv whatismyip dist/whatismyip-darwin-arm64
|
||||
GOOS=windows GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-windows-amd64.exe
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
docker build --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: docker-build
|
||||
ifneq (,$(findstring devel-,$(VERSION)))
|
||||
@echo "VERSION is set to ${VERSION}, I can't push devel builds"
|
||||
exit 1
|
||||
else
|
||||
docker push ${DOCKER_URL}:${VERSION}
|
||||
docker tag ${DOCKER_URL}:${VERSION} ${DOCKER_URL}:latest
|
||||
docker push ${DOCKER_URL}:latest
|
||||
endif
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: docker-build
|
||||
docker run --tty --interactive --rm \
|
||||
-v ${PWD}/test/GeoIP2-City-Test.mmdb:/tmp/GeoIP2-City-Test.mmdb:ro \
|
||||
-v ${PWD}/test/GeoLite2-ASN-Test.mmdb:/tmp/GeoLite2-ASN-Test.mmdb:ro -p 8080:8080 \
|
||||
${DOCKER_URL}:${VERSION} \
|
||||
-geoip2-city /tmp/GeoIP2-City-Test.mmdb \
|
||||
-geoip2-asn /tmp/GeoLite2-ASN-Test.mmdb \
|
||||
-trusted-header X-Real-IP
|
||||
--publish 8080:8080/tcp \
|
||||
--publish 8081:8081/tcp \
|
||||
--publish 9100:9100/tcp \
|
||||
--publish 8081:8081/udp \
|
||||
--volume ${PWD}/test:/test \
|
||||
${DOCKER_URL}:${VERSION} \
|
||||
-geoip2-city /test/GeoIP2-City-Test.mmdb \
|
||||
-geoip2-asn /test/GeoLite2-ASN-Test.mmdb \
|
||||
-trusted-header X-Real-PortReal-IP \
|
||||
-tls-bind :8081 \
|
||||
-tls-crt /test/server.pem \
|
||||
-tls-key /test/server.key \
|
||||
-enable-http3 \
|
||||
-metrics-bind :9100
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
# What is my IP address
|
||||
|
||||
[](https://github.com/dcarrillo/whatismyip/actions)
|
||||
[](https://github.com/dcarrillo/whatismyip/actions/workflows/codeql-analysis.yml)
|
||||
[](https://goreportcard.com/report/github.com/dcarrillo/whatismyip)
|
||||
[](https://github.com/dcarrillo/whatismyip/releases/)
|
||||
[](./LICENSE)
|
||||
|
||||
- [What is my IP address](#what-is-my-ip-address)
|
||||
- [Features](#features)
|
||||
- [Endpoints](#endpoints)
|
||||
- [Build](#build)
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Run a default TCP server](#run-a-default-tcp-server)
|
||||
- [Run a TLS (HTTP/2) server only](#run-a-tls-http2-server-only)
|
||||
- [Run a default TCP server with a custom template and trust a custom header set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-custom-header-set-by-an-upstream-proxy)
|
||||
- [Download](#download)
|
||||
- [Docker](#docker)
|
||||
- [Run a container locally using test databases](#run-a-container-locally-using-test-databases)
|
||||
- [From Docker Hub](#from-docker-hub)
|
||||
- [Features](#features)
|
||||
- [Endpoints](#endpoints)
|
||||
- [DNS discovery](#dns-discovery)
|
||||
- [Build](#build)
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Run a default TCP server](#run-a-default-tcp-server)
|
||||
- [Run a default TCP server with geo information enabled](#run-a-default-tcp-server-with-geo-information-enabled)
|
||||
- [Run a TLS (HTTP/2) and enable "what is my DNS" with geo information](#run-a-tls-http2-and-enable-what-is-my-dns-with-geo-information)
|
||||
- [Run an HTTP/3 server](#run-an-http3-server)
|
||||
- [Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-pair-of-custom-headers-set-by-an-upstream-proxy)
|
||||
- [Download](#download)
|
||||
- [Docker](#docker)
|
||||
- [Run a container locally using test databases](#run-a-container-locally-using-test-databases)
|
||||
- [From Docker Hub](#from-docker-hub)
|
||||
|
||||
Just another "what is my IP address" service, including geolocation and headers information, written in go with high performance in mind, it uses [gin](https://github.com/gin-gonic/gin) which uses [httprouter](https://github.com/julienschmidt/httprouter) a lightweight high performance HTTP multiplexer.
|
||||
> [!NOTE]
|
||||
> Since version 3.2.0, an optional prometheus metrics endpoint is available.
|
||||
>
|
||||
> Since version 3.0.0, the geodb database is not mandatory; not adding the flags will disable the geo feature.
|
||||
>
|
||||
> Since version 2.3.0, the application includes an optional client [DNS discovery](#dns-discovery) feature.
|
||||
|
||||
Take a look at [ifconfig.es](https://ifconfig.es) a live site using `whatismyip`
|
||||
Just another "what is my IP address" service, including geolocation, TCP open port checking, and headers information. Written in Go with high performance in mind,
|
||||
it uses [gin](https://github.com/gin-gonic/gin) which uses [httprouter](https://github.com/julienschmidt/httprouter), a lightweight high-performance HTTP multiplexer.
|
||||
|
||||
Take a look at [ifconfig.es](https://ifconfig.es), a live site using `whatismyip` with the `DNS discovery` feature enabled.
|
||||
|
||||
Get your public IP easily from the command line:
|
||||
|
||||
@@ -33,21 +44,33 @@ curl -6 ifconfig.es
|
||||
::1
|
||||
```
|
||||
|
||||
Get the IP of your DNS provider:
|
||||
|
||||
```bash
|
||||
curl -L dns.ifconfig.es
|
||||
2a04:e4c0:47::67 (Spain / OPENDNS)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- TLS and HTTP/2.
|
||||
- Can run behind a proxy by trusting a custom header (usually `X-Real-IP`) to figure out the source IP address.
|
||||
- Experimental HTTP/3 support. HTTP/3 requires a TLS server running (`-tls-bind`), as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.
|
||||
- DNS discovery: A best-effort approach to discovering the DNS server that is resolving the client's requests.
|
||||
- Can run behind a proxy by trusting a custom header (usually `X-Real-IP`) to figure out the source IP address. It also supports a custom header to resolve the client port, if the proxy can only add a header for the IP (for example a fixed header from CDNs) the client port is shown as unknown.
|
||||
- IPv4 and IPv6.
|
||||
- Geolocation info including ASN. This feature is possible thanks to [maxmind](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data?lang=en) GeoLite2 databases. In order to use these databases, a license key is needed. Please visit Maxmind site for further instructions and get a free license.
|
||||
- Checking TCP open ports.
|
||||
- High performance.
|
||||
- Self-contained server what can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored.
|
||||
- Prometheus metrics endpoint: Exports metrics (on a separete process/port) for HTTP requests, request duration, geo lookups, port scans, and DNS queries.
|
||||
- Self-contained server that can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored.
|
||||
- HTML templates for the landing page.
|
||||
- Text plain and JSON output.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- https://ifconfig.es/
|
||||
- https://ifconfig.es/json
|
||||
- https://ifconfig.es/client-port
|
||||
- https://ifconfig.es/json (this is the same as `curl -H "Accept: application/json" https://ifconfig.es/`)
|
||||
- https://ifconfig.es/geo
|
||||
- https://ifconfig.es/geo/city
|
||||
- https://ifconfig.es/geo/country
|
||||
@@ -62,66 +85,124 @@ curl -6 ifconfig.es
|
||||
- https://ifconfig.es/all
|
||||
- https://ifconfig.es/headers
|
||||
- https://ifconfig.es/<header_name>
|
||||
- https://ifconfig.es/scan/tcp/<port_number>
|
||||
- https://dns.ifconfig.es
|
||||
|
||||
## DNS discovery
|
||||
|
||||
The DNS discovery works by forcing the client to make a request to `<uuid>.dns.ifconfig.es`. This DNS request is handled by a microdns server
|
||||
included in the `whatismyip` binary. In order to run the discovery server, a configuration file in the following form has to be created:
|
||||
|
||||
```yaml
|
||||
---
|
||||
domain: dns.example.com
|
||||
redirect_port: ":8000"
|
||||
resource_records:
|
||||
- "1800 IN SOA xns.example.com. hostmaster.example.com. 1 10000 2400 604800 1800"
|
||||
- "3600 IN NS xns.example.com."
|
||||
ipv4:
|
||||
- "127.0.0.2"
|
||||
ipv6:
|
||||
- "aaa:aaa:aaa:aaaa::1"
|
||||
```
|
||||
|
||||
The DNS authority for example.com has delegated the subdomain zone `dns.example.com` to the server running the `whatismyip` service.
|
||||
|
||||
The client can request the URL `dns.example.com` by following the redirection `curl -L dns.example.com`.
|
||||
|
||||
To avoid the redirection, you can provide a valid URL, for example, for the real [ifconfig.es](https://ifconfig.es):
|
||||
|
||||
```bash
|
||||
curl $(uuidgen).dns.ifconfig.es
|
||||
|
||||
curl $(cat /proc/sys/kernel/random/uuid).dns.ifconfig.es
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Golang >= 1.17 is required. Previous versions may work.
|
||||
Golang >= 1.25 is required.
|
||||
|
||||
`make build`
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
Usage of ./whatismyip:
|
||||
Usage of whatismyip:
|
||||
-bind string
|
||||
Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080")
|
||||
Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080")
|
||||
-disable-scan
|
||||
Disable TCP port scanning functionality
|
||||
-enable-http3
|
||||
Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.
|
||||
-enable-secure-headers
|
||||
Add sane security-related headers to every response
|
||||
-geoip2-asn string
|
||||
Path to GeoIP2 ASN database
|
||||
Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)
|
||||
-geoip2-city string
|
||||
Path to GeoIP2 city database
|
||||
Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)
|
||||
-metrics-bind string
|
||||
Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint.
|
||||
-resolver string
|
||||
Path to the resolver configuration. It actually enables the resolver for DNS client discovery.
|
||||
-template string
|
||||
Path to template file
|
||||
Path to the template file
|
||||
-tls-bind string
|
||||
Listening address for TLS (see https://pkg.go.dev/net?#Listen)
|
||||
Listening address for TLS (see https://pkg.go.dev/net?#Listen)
|
||||
-tls-crt string
|
||||
When using TLS, path to certificate file
|
||||
When using TLS, path to certificate file
|
||||
-tls-key string
|
||||
When using TLS, path to private key file
|
||||
When using TLS, path to private key file
|
||||
-trusted-header string
|
||||
Trusted request header for remote IP (e.g. X-Real-IP)
|
||||
Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown'
|
||||
-trusted-port-header string
|
||||
Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory
|
||||
-version
|
||||
Output version information and exit
|
||||
Output version information and exit
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Run a default TCP server
|
||||
|
||||
```bash
|
||||
./whatismyip
|
||||
```
|
||||
|
||||
### Run a default TCP server with geo information enabled
|
||||
|
||||
```bash
|
||||
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb
|
||||
```
|
||||
|
||||
### Run a TLS (HTTP/2) server only
|
||||
### Run a TLS (HTTP/2) and enable "what is my DNS" with geo information
|
||||
|
||||
```bash
|
||||
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
|
||||
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key
|
||||
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key \
|
||||
-resolver ./test/resolver.yml
|
||||
```
|
||||
|
||||
### Run a default TCP server with a custom template and trust a custom header set by an upstream proxy
|
||||
### Run an HTTP/3 server
|
||||
|
||||
```bash
|
||||
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
|
||||
-trusted-header X-Real-IP -template mytemplate.tmpl
|
||||
-bind "" -tls-bind :8081 -tls-crt ./test/server.pem -tls-key ./test/server.key -enable-http3
|
||||
```
|
||||
|
||||
### Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy
|
||||
|
||||
```bash
|
||||
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb \
|
||||
-trusted-header X-Real-IP -trusted-port-header X-Real-Port -template mytemplate.tmpl
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
Download latest version from https://github.com/dcarrillo/whatismyip/releases
|
||||
Download the latest version from [github](https://github.com/dcarrillo/whatismyip/releases)
|
||||
|
||||
## Docker
|
||||
|
||||
An ultra-light (~9MB) image is available.
|
||||
An ultra-light (~6MB) image is available on [docker hub](https://hub.docker.com/r/dcarrillo/whatismyip). ~Since version `2.1.2`, the binary is compressed using [upx](https://github.com/upx/upx).~
|
||||
|
||||
### Run a container locally using test databases
|
||||
|
||||
|
||||
+85
-122
@@ -8,159 +8,122 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/httputils"
|
||||
"github.com/dcarrillo/whatismyip/internal/metrics"
|
||||
"github.com/dcarrillo/whatismyip/internal/setting"
|
||||
"github.com/dcarrillo/whatismyip/models"
|
||||
"github.com/dcarrillo/whatismyip/resolver"
|
||||
"github.com/dcarrillo/whatismyip/router"
|
||||
"github.com/dcarrillo/whatismyip/server"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-contrib/secure"
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/unrolled/secure"
|
||||
)
|
||||
|
||||
var (
|
||||
tcpServer *http.Server
|
||||
tlsServer *http.Server
|
||||
engine *gin.Engine
|
||||
)
|
||||
|
||||
func main() {
|
||||
o, err := setting.Setup(os.Args[1:])
|
||||
if err == flag.ErrHelp || err == setting.ErrVersion {
|
||||
fmt.Print(o)
|
||||
os.Exit(0)
|
||||
} else if err != nil {
|
||||
fmt.Print(err)
|
||||
cfg, o, err := setting.Setup(os.Args[1:])
|
||||
if err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) || errors.Is(err, setting.ErrVersion) {
|
||||
fmt.Print(o)
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
|
||||
setupEngine()
|
||||
router.SetupTemplate(engine)
|
||||
router.Setup(engine)
|
||||
|
||||
if setting.App.BindAddress != "" {
|
||||
runTCPServer()
|
||||
}
|
||||
|
||||
if setting.App.TLSAddress != "" {
|
||||
runTLSServer()
|
||||
}
|
||||
|
||||
runHandler()
|
||||
}
|
||||
|
||||
func runHandler() {
|
||||
signalChan := make(chan os.Signal, 3)
|
||||
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
ctx := context.Background()
|
||||
var s os.Signal
|
||||
|
||||
for {
|
||||
s = <-signalChan
|
||||
|
||||
if s == syscall.SIGHUP {
|
||||
models.CloseDBs()
|
||||
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
|
||||
router.SetupTemplate(engine)
|
||||
|
||||
if setting.App.BindAddress != "" {
|
||||
if err := tcpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("TCP server forced to shutdown: %s", err)
|
||||
}
|
||||
runTCPServer()
|
||||
}
|
||||
if setting.App.TLSAddress != "" {
|
||||
if err := tlsServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("TLS server forced to shutdown: %s", err)
|
||||
}
|
||||
runTLSServer()
|
||||
}
|
||||
} else {
|
||||
log.Printf("Shutting down...")
|
||||
if setting.App.BindAddress != "" {
|
||||
if err := tcpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("TCP server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
if setting.App.TLSAddress != "" {
|
||||
if err := tlsServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("TLS server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
models.CloseDBs()
|
||||
break
|
||||
var geoSvc *service.Geo
|
||||
if cfg.GeodbPath.City != "" || cfg.GeodbPath.ASN != "" {
|
||||
if geoSvc, err = service.NewGeo(context.Background(), cfg.GeodbPath.City, cfg.GeodbPath.ASN); err != nil {
|
||||
log.Fatalf("Failed to load geo databases: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runTCPServer() {
|
||||
tcpServer = &http.Server{
|
||||
Addr: setting.App.BindAddress,
|
||||
Handler: engine,
|
||||
ReadTimeout: setting.App.Server.ReadTimeout,
|
||||
WriteTimeout: setting.App.Server.WriteTimeout,
|
||||
servers := []server.Server{}
|
||||
engine := setupEngine(cfg)
|
||||
|
||||
if cfg.Resolver.Domain != "" {
|
||||
store := cache.New(1*time.Minute, 10*time.Minute)
|
||||
var dnsEngine *resolver.Resolver
|
||||
if dnsEngine, err = resolver.Setup(store, resolver.Settings{
|
||||
Domain: cfg.Resolver.Domain,
|
||||
ResourceRecords: cfg.Resolver.ResourceRecords,
|
||||
RedirectPort: cfg.Resolver.RedirectPort,
|
||||
IPv4: cfg.Resolver.Ipv4,
|
||||
IPv6: cfg.Resolver.Ipv6,
|
||||
}); err != nil {
|
||||
log.Fatalf("Invalid resolver configuration: %s", err)
|
||||
}
|
||||
nameServer := server.NewDNSServer(context.Background(), dnsEngine.Handler())
|
||||
servers = append(servers, nameServer)
|
||||
engine.Use(router.GetDNSDiscoveryHandler(store, geoSvc, cfg.Resolver.Domain, cfg.Resolver.RedirectPort))
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("Starting TCP server listening on %s", setting.App.BindAddress)
|
||||
if err := tcpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Stopping TCP server...")
|
||||
}()
|
||||
}
|
||||
rt := router.NewRouter(geoSvc, cfg.TrustedHeader, cfg.TrustedPortHeader, cfg.TemplatePath, cfg.DisableTCPScan)
|
||||
router.SetupTemplate(engine, cfg.TemplatePath)
|
||||
router.Setup(engine, rt)
|
||||
servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler(), cfg))
|
||||
|
||||
func runTLSServer() {
|
||||
tlsServer = &http.Server{
|
||||
Addr: setting.App.TLSAddress,
|
||||
Handler: engine,
|
||||
ReadTimeout: setting.App.Server.ReadTimeout,
|
||||
WriteTimeout: setting.App.Server.WriteTimeout,
|
||||
if cfg.PrometheusAddress != "" {
|
||||
prometheusServer := server.NewPrometheusServer(context.Background(), cfg.PrometheusAddress,
|
||||
server.Timeouts{
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
})
|
||||
servers = append(servers, prometheusServer)
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress)
|
||||
if err := tlsServer.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
|
||||
!errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Stopping TLS server...")
|
||||
}()
|
||||
whatismyip := server.Setup(servers, geoSvc)
|
||||
whatismyip.Run()
|
||||
}
|
||||
|
||||
func setupEngine() {
|
||||
func setupEngine(cfg setting.Settings) *gin.Engine {
|
||||
gin.DisableConsoleColor()
|
||||
if os.Getenv(gin.EnvGinMode) == "" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
engine = gin.New()
|
||||
engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter))
|
||||
engine.Use(gin.Recovery())
|
||||
if setting.App.EnableSecureHeaders {
|
||||
engine.Use(addSecureHeaders())
|
||||
engine := gin.New()
|
||||
engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter), gin.Recovery())
|
||||
if cfg.PrometheusAddress != "" {
|
||||
metrics.Enable()
|
||||
engine.Use(metrics.GinMiddleware())
|
||||
}
|
||||
_ = engine.SetTrustedProxies(nil)
|
||||
engine.TrustedPlatform = setting.App.TrustedHeader
|
||||
}
|
||||
|
||||
func addSecureHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := secure.New(secure.Options{
|
||||
if cfg.EnableSecureHeaders {
|
||||
engine.Use(secure.New(secure.Config{
|
||||
BrowserXssFilter: true,
|
||||
ContentTypeNosniff: true,
|
||||
FrameDeny: true,
|
||||
}).Process(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
_ = engine.SetTrustedProxies(nil)
|
||||
engine.TrustedPlatform = cfg.TrustedHeader
|
||||
|
||||
// Avoid header rewrite if response is a redirection.
|
||||
if status := c.Writer.Status(); status > 300 && status < 399 {
|
||||
c.Abort()
|
||||
return engine
|
||||
}
|
||||
|
||||
func setupHTTPServers(ctx context.Context, handler http.Handler, cfg setting.Settings) []server.Server {
|
||||
var servers []server.Server
|
||||
timeouts := server.Timeouts{
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
}
|
||||
|
||||
if cfg.BindAddress != "" {
|
||||
tcpServer := server.NewTCPServer(ctx, handler, cfg.BindAddress, timeouts)
|
||||
servers = append(servers, tcpServer)
|
||||
}
|
||||
|
||||
if cfg.TLSAddress != "" {
|
||||
tlsServer := server.NewTLSServer(ctx, handler, cfg.TLSAddress, cfg.TLSCrtPath, cfg.TLSKeyPath, timeouts)
|
||||
servers = append(servers, tlsServer)
|
||||
if cfg.EnableHTTP3 {
|
||||
quicServer := server.NewQuicServer(ctx, tlsServer, cfg.TLSAddress, cfg.TLSCrtPath, cfg.TLSKeyPath)
|
||||
servers = append(servers, quicServer)
|
||||
}
|
||||
}
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
module github.com/dcarrillo/whatismyip
|
||||
|
||||
go 1.18
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/oschwald/maxminddb-golang v1.9.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/testcontainers/testcontainers-go v0.12.0
|
||||
github.com/unrolled/secure v1.10.0
|
||||
github.com/gin-contrib/secure v1.1.3
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/containerd/cgroups v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.14+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/moby/sys/mount v0.3.1 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.23 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect
|
||||
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect
|
||||
google.golang.org/grpc v1.45.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
||||
golang.org/x/arch v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
module github.com/dcarrillo/whatismyip/integration-tests
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/dcarrillo/whatismyip => ..
|
||||
|
||||
require (
|
||||
github.com/dcarrillo/whatismyip v0.0.0-00010101000000-000000000000
|
||||
github.com/docker/docker v28.0.4+incompatible
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/gin-gonic/gin v1.12.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.23 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/user v0.1.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
golang.org/x/arch v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,273 @@
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
|
||||
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho=
|
||||
golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
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/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d h1:Jkpk39hlTZOIp3RbfvNX9R8Hv+Sw0X89nlU/xFOErsc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
@@ -4,49 +4,77 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
|
||||
"github.com/dcarrillo/whatismyip/router"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
tc "github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
func buildContainer() testcontainers.ContainerRequest {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
dir := filepath.Dir(filename)
|
||||
func customDialContext() func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(ctx, "udp", "127.0.0.1:53531")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
FromDockerfile: testcontainers.FromDockerfile{
|
||||
Context: "../",
|
||||
Dockerfile: "Dockerfile",
|
||||
},
|
||||
Cmd: []string{
|
||||
"-geoip2-city", "/tmp/GeoIP2-City-Test.mmdb",
|
||||
"-geoip2-asn", "/tmp/GeoLite2-ASN-Test.mmdb",
|
||||
"-bind", ":8000",
|
||||
"-tls-bind", ":8001",
|
||||
"-tls-crt", "/tmp/server.pem",
|
||||
"-tls-key", "/tmp/server.key",
|
||||
"-trusted-header", "X-Real-IP",
|
||||
"-enable-secure-headers",
|
||||
},
|
||||
ExposedPorts: []string{"8000:8000", "8001:8001"},
|
||||
WaitingFor: wait.ForLog("Starting TLS server listening on :8001"),
|
||||
BindMounts: map[string]string{
|
||||
"/tmp/GeoIP2-City-Test.mmdb": filepath.Join(dir, "/../test/GeoIP2-City-Test.mmdb"),
|
||||
"/tmp/GeoLite2-ASN-Test.mmdb": filepath.Join(dir, "/../test/GeoLite2-ASN-Test.mmdb"),
|
||||
"/tmp/server.pem": filepath.Join(dir, "/../test/server.pem"),
|
||||
"/tmp/server.key": filepath.Join(dir, "/../test/server.key"),
|
||||
},
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return req
|
||||
func testWhatIsMyDNS(t *testing.T) {
|
||||
t.Run("RequestDNSDiscovery", func(t *testing.T) {
|
||||
http.DefaultTransport.(*http.Transport).DialContext = customDialContext()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Host = "dns.example.com"
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusFound, resp.StatusCode)
|
||||
u, err := resp.Location()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, validator.IsValid(strings.Split(u.Hostname(), ".")[0]))
|
||||
|
||||
for _, accept := range []string{"application/json", "*/*", "text/html"} {
|
||||
req, err = http.NewRequest("GET", u.String(), nil)
|
||||
req.Host = u.Hostname()
|
||||
req.Header.Set("Accept", accept)
|
||||
assert.NoError(t, err)
|
||||
resp, err = client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if accept == "application/json" {
|
||||
assert.NoError(t, json.Unmarshal(body, &router.DNSJSONResponse{}))
|
||||
} else {
|
||||
ip := strings.Split(string(body), " ")[0]
|
||||
assert.True(t, net.ParseIP(ip) != nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainerIntegration(t *testing.T) {
|
||||
@@ -55,31 +83,270 @@ func TestContainerIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: buildContainer(),
|
||||
Started: true,
|
||||
c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
|
||||
ContainerRequest: tc.ContainerRequest{
|
||||
FromDockerfile: tc.FromDockerfile{
|
||||
Context: "../",
|
||||
Dockerfile: "./test/Dockerfile",
|
||||
PrintBuildLog: true,
|
||||
KeepImage: false,
|
||||
BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) {
|
||||
buildOptions.Target = "test"
|
||||
},
|
||||
},
|
||||
ExposedPorts: []string{
|
||||
"8000:8000",
|
||||
"8001:8001",
|
||||
"8001:8001/udp",
|
||||
"9100:9100",
|
||||
"53531:53/udp",
|
||||
},
|
||||
Cmd: []string{
|
||||
"-geoip2-city", "/GeoIP2-City-Test.mmdb",
|
||||
"-geoip2-asn", "/GeoLite2-ASN-Test.mmdb",
|
||||
"-bind", ":8000",
|
||||
"-tls-bind", ":8001",
|
||||
"-tls-crt", "/server.pem",
|
||||
"-tls-key", "/server.key",
|
||||
"-trusted-header", "X-Real-IP",
|
||||
"-enable-secure-headers",
|
||||
"-enable-http3",
|
||||
"-metrics-bind", ":9100",
|
||||
"-resolver", "/resolver.yml",
|
||||
},
|
||||
Files: []tc.ContainerFile{
|
||||
{
|
||||
HostFilePath: "./../test/GeoIP2-City-Test.mmdb",
|
||||
ContainerFilePath: "/GeoIP2-City-Test.mmdb",
|
||||
},
|
||||
{
|
||||
HostFilePath: "./../test/GeoLite2-ASN-Test.mmdb",
|
||||
ContainerFilePath: "/GeoLite2-ASN-Test.mmdb",
|
||||
},
|
||||
{
|
||||
HostFilePath: "./../test/server.pem",
|
||||
ContainerFilePath: "/server.pem",
|
||||
},
|
||||
{
|
||||
HostFilePath: "./../test/server.key",
|
||||
ContainerFilePath: "/server.key",
|
||||
},
|
||||
{
|
||||
HostFilePath: "./../test/resolver.yml",
|
||||
ContainerFilePath: "/resolver.yml",
|
||||
},
|
||||
},
|
||||
WaitingFor: wait.ForLog("Starting QUIC server"),
|
||||
AutoRemove: true,
|
||||
},
|
||||
Started: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
err := container.Terminate(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := c.Terminate(ctx); err != nil {
|
||||
t.Logf("Failed to terminate container: %s", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
for _, url := range []string{"http://localhost:8000/json", "https://localhost:8001/json"} {
|
||||
resp, _ := http.Get(url)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var dat router.JSONResponse
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(body, &dat))
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
quic bool
|
||||
}{
|
||||
{
|
||||
name: "RequestOverHTTP",
|
||||
url: "http://localhost:8000",
|
||||
quic: false,
|
||||
},
|
||||
{
|
||||
name: "RequestOverHTTPs",
|
||||
url: "https://localhost:8001",
|
||||
quic: false,
|
||||
},
|
||||
{
|
||||
name: "RequestOverUDPWithQuic",
|
||||
url: "https://localhost:8001",
|
||||
quic: true,
|
||||
},
|
||||
}
|
||||
|
||||
testsPortScan := []struct {
|
||||
name string
|
||||
port int
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "RequestOpenPortScan",
|
||||
port: 8000,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "RequestClosedPortScan",
|
||||
port: 65533,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", tt.url, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
var resp *http.Response
|
||||
var body []byte
|
||||
if tt.quic {
|
||||
resp, body, err = doQuicRequest(req)
|
||||
} else {
|
||||
client := &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
if strings.Contains(tt.url, "https://") {
|
||||
assert.Equal(t, `h3=":8001"; ma=2592000`, resp.Header.Get("Alt-Svc"))
|
||||
}
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
assert.NoError(t, json.Unmarshal(body, &router.JSONResponse{}))
|
||||
assert.Equal(t, "DENY", resp.Header.Get("X-Frame-Options"))
|
||||
assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"))
|
||||
assert.Equal(t, "1; mode=block", resp.Header.Get("X-Xss-Protection"))
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range testsPortScan {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:8000/scan/tcp/%d", tt.port), nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Real-IP", "127.0.0.1")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
j := router.JSONScanResponse{}
|
||||
assert.NoError(t, json.Unmarshal(body, &j))
|
||||
assert.Equal(t, tt.want, j.Reachable)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("RequestMetricsEndpoint", func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:9100/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
bodyStr := string(body)
|
||||
|
||||
assert.Contains(t, bodyStr, "whatismyip_http_requests_total")
|
||||
assert.Contains(t, bodyStr, "whatismyip_http_request_duration_seconds")
|
||||
assert.Contains(t, bodyStr, "# HELP")
|
||||
assert.Contains(t, bodyStr, "# TYPE")
|
||||
})
|
||||
|
||||
testWhatIsMyDNS(t)
|
||||
}
|
||||
|
||||
// TODO If other flags like this one are implemented we should think of a better approach
|
||||
func TestContainerIntegrationDisableScan(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skiping integration tests")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
|
||||
ContainerRequest: tc.ContainerRequest{
|
||||
FromDockerfile: tc.FromDockerfile{
|
||||
Context: "../",
|
||||
Dockerfile: "./test/Dockerfile",
|
||||
PrintBuildLog: true,
|
||||
KeepImage: false,
|
||||
BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) {
|
||||
buildOptions.Target = "test"
|
||||
},
|
||||
},
|
||||
ExposedPorts: []string{
|
||||
"8000:8000",
|
||||
},
|
||||
Cmd: []string{
|
||||
"-geoip2-city", "/GeoIP2-City-Test.mmdb",
|
||||
"-geoip2-asn", "/GeoLite2-ASN-Test.mmdb",
|
||||
"-bind", ":8000",
|
||||
"-trusted-header", "X-Real-IP",
|
||||
"-enable-secure-headers",
|
||||
"-disable-scan",
|
||||
},
|
||||
Files: []tc.ContainerFile{
|
||||
{
|
||||
HostFilePath: "./../test/GeoIP2-City-Test.mmdb",
|
||||
ContainerFilePath: "/GeoIP2-City-Test.mmdb",
|
||||
},
|
||||
{
|
||||
HostFilePath: "./../test/GeoLite2-ASN-Test.mmdb",
|
||||
ContainerFilePath: "/GeoLite2-ASN-Test.mmdb",
|
||||
},
|
||||
},
|
||||
WaitingFor: wait.ForLog("Starting TCP server"),
|
||||
AutoRemove: true,
|
||||
},
|
||||
Started: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := c.Terminate(ctx); err != nil {
|
||||
t.Logf("Failed to terminate container: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RequestScanEndpointWithDisabledScan", func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/scan/tcp/8000", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Real-IP", "127.0.0.1")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, body)
|
||||
})
|
||||
}
|
||||
|
||||
func doQuicRequest(req *http.Request) (*http.Response, []byte, error) {
|
||||
roundTripper := &http3.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
defer roundTripper.Close()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripper,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
return resp, body, nil
|
||||
}
|
||||
|
||||
+19
-12
@@ -3,40 +3,47 @@ package httputils
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HeadersToSortedString shorts and dumps http.Header to a string separated by \n
|
||||
// HeadersToSortedString sorts and dumps http.Header to a string separated by \n
|
||||
func HeadersToSortedString(headers http.Header) string {
|
||||
var output string
|
||||
|
||||
keys := make([]string, 0, len(headers))
|
||||
for k := range headers {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var output strings.Builder
|
||||
for _, k := range keys {
|
||||
if len(headers[k]) > 1 {
|
||||
for _, h := range headers[k] {
|
||||
output += k + ": " + h + "\n"
|
||||
}
|
||||
} else {
|
||||
output += k + ": " + headers[k][0] + "\n"
|
||||
for _, h := range headers[k] {
|
||||
output.WriteString(k + ": " + h + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// GetHeadersWithoutTrustedHeaders returns a copy of the request headers with the trusted headers removed
|
||||
func GetHeadersWithoutTrustedHeaders(ctx *gin.Context, trustedHeader, trustedPortHeader string) http.Header {
|
||||
h := ctx.Request.Header.Clone()
|
||||
|
||||
for _, k := range []string{trustedHeader, trustedPortHeader} {
|
||||
delete(h, textproto.CanonicalMIMEHeaderKey(k))
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// GetLogFormatter returns our custom log format
|
||||
func GetLogFormatter(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %d %s \"%s\" \"%s\" \"%s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format("02/Nov/2006:15:04:05 -0700"),
|
||||
param.TimeStamp.Format("02/Jan/2006:15:04:05 -0700"),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
@@ -50,7 +57,7 @@ func GetLogFormatter(param gin.LogFormatterParams) string {
|
||||
)
|
||||
}
|
||||
|
||||
func normalizeLog(log interface{}) interface{} {
|
||||
func normalizeLog(log any) any {
|
||||
switch v := log.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
|
||||
@@ -25,7 +25,7 @@ Header3: Three
|
||||
}
|
||||
|
||||
func TestGetLogFormatter(t *testing.T) {
|
||||
expected := "127.0.0.1 - [01/Nov/0001:00:00:00 +0000] \"GET / HTTP/1.1\" 200 100 1000 local \"golang test 1.0\" \"1.1.1.1, 2.2.2.2\" \"-\"\n"
|
||||
expected := "127.0.0.1 - [15/Jul/2022:10:30:00 +0000] \"GET / HTTP/1.1\" 200 100 1000 local \"golang test 1.0\" \"1.1.1.1, 2.2.2.2\" \"-\"\n"
|
||||
|
||||
h := http.Header{}
|
||||
h.Set("User-Agent", "golang test 1.0")
|
||||
@@ -39,7 +39,7 @@ func TestGetLogFormatter(t *testing.T) {
|
||||
|
||||
p := gin.LogFormatterParams{
|
||||
ClientIP: "127.0.0.1",
|
||||
TimeStamp: time.Time{},
|
||||
TimeStamp: time.Date(2022, time.July, 15, 10, 30, 0, 0, time.UTC),
|
||||
Method: "GET",
|
||||
Path: "/",
|
||||
StatusCode: 200,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
enabled bool
|
||||
initOnce sync.Once
|
||||
requestsTotal *prometheus.CounterVec
|
||||
requestDuration *prometheus.HistogramVec
|
||||
requestsInFlight prometheus.Gauge
|
||||
geoLookups *prometheus.CounterVec
|
||||
portScans prometheus.Counter
|
||||
dnsQueries *prometheus.CounterVec
|
||||
)
|
||||
|
||||
func Enable() {
|
||||
initOnce.Do(func() {
|
||||
enabled = true
|
||||
|
||||
requestsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "whatismyip_http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
},
|
||||
[]string{"method", "path", "status"},
|
||||
)
|
||||
|
||||
requestDuration = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "whatismyip_http_request_duration_seconds",
|
||||
Help: "HTTP request latency in seconds",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
|
||||
requestsInFlight = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "whatismyip_http_requests_in_flight",
|
||||
Help: "Current number of HTTP requests being processed",
|
||||
},
|
||||
)
|
||||
|
||||
geoLookups = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "whatismyip_geo_lookups_total",
|
||||
Help: "Total number of geo lookups",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
|
||||
portScans = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "whatismyip_port_scans_total",
|
||||
Help: "Total number of port scan requests",
|
||||
},
|
||||
)
|
||||
|
||||
dnsQueries = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "whatismyip_dns_queries_total",
|
||||
Help: "Total number of DNS queries",
|
||||
},
|
||||
[]string{"query_type", "rcode"},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func GinMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = "/404" // group 404s
|
||||
}
|
||||
|
||||
requestsInFlight.Inc()
|
||||
defer requestsInFlight.Dec()
|
||||
|
||||
c.Next()
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
status := c.Writer.Status()
|
||||
|
||||
requestsTotal.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%dxx", status/100)).Inc()
|
||||
requestDuration.WithLabelValues(c.Request.Method, path).Observe(duration)
|
||||
}
|
||||
}
|
||||
|
||||
func RecordGeoLookup(lookupType string) {
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
geoLookups.WithLabelValues(lookupType).Inc()
|
||||
}
|
||||
|
||||
func RecordPortScan() {
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
portScans.Inc()
|
||||
}
|
||||
|
||||
func RecordDNSQuery(queryType string, rcode string) {
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
dnsQueries.WithLabelValues(queryType, rcode).Inc()
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisabledMetrics_Middleware(t *testing.T) {
|
||||
if enabled {
|
||||
t.Skip("Skipping disabled test - metrics already enabled")
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
c.Request = httptest.NewRequest("GET", "/test", nil)
|
||||
|
||||
middleware := GinMiddleware()
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
middleware(c)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisabledMetrics_GeoLookup(t *testing.T) {
|
||||
if enabled {
|
||||
t.Skip("Skipping disabled test - metrics already enabled")
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
RecordGeoLookup("city")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisabledMetrics_PortScan(t *testing.T) {
|
||||
if enabled {
|
||||
t.Skip("Skipping disabled test - metrics already enabled")
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
RecordPortScan()
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisabledMetrics_DNSQuery(t *testing.T) {
|
||||
if enabled {
|
||||
t.Skip("Skipping disabled test - metrics already enabled")
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
RecordDNSQuery("A", "NOERROR")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnable(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
assert.True(t, enabled, "Enable() should set enabled to true")
|
||||
assert.NotNil(t, requestsTotal, "requestsTotal should be initialized")
|
||||
assert.NotNil(t, requestDuration, "requestDuration should be initialized")
|
||||
assert.NotNil(t, requestsInFlight, "requestsInFlight should be initialized")
|
||||
assert.NotNil(t, geoLookups, "geoLookups should be initialized")
|
||||
assert.NotNil(t, portScans, "portScans should be initialized")
|
||||
assert.NotNil(t, dnsQueries, "dnsQueries should be initialized")
|
||||
}
|
||||
|
||||
func TestEnableIdempotent(t *testing.T) {
|
||||
Enable()
|
||||
firstRequestsTotal := requestsTotal
|
||||
|
||||
Enable()
|
||||
Enable()
|
||||
|
||||
assert.Equal(t, firstRequestsTotal, requestsTotal, "Enable() should be idempotent")
|
||||
}
|
||||
|
||||
func TestGinMiddleware_StatusCategories(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
testCases := []struct {
|
||||
status int
|
||||
category string
|
||||
}{
|
||||
{200, "2xx"},
|
||||
{201, "2xx"},
|
||||
{301, "3xx"},
|
||||
{404, "4xx"},
|
||||
{500, "5xx"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(GinMiddleware())
|
||||
router.GET("/test-status", func(c *gin.Context) {
|
||||
c.Status(tc.status)
|
||||
})
|
||||
|
||||
initialCount := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/test-status", tc.category))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/test-status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
count := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/test-status", tc.category))
|
||||
assert.Equal(t, initialCount+1, count, "Expected count for category %s to increase by 1", tc.category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinMiddleware_404(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(GinMiddleware())
|
||||
|
||||
initialCount := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/404", "4xx"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/nonexistent-path", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
count := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/404", "4xx"))
|
||||
assert.Equal(t, initialCount+1, count, "Expected count to increase by 1 for empty path (404)")
|
||||
}
|
||||
|
||||
func TestGinMiddleware_RecordsDuration(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(GinMiddleware())
|
||||
router.GET("/test-duration", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/test-duration", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
metric := requestDuration.WithLabelValues("GET", "/test-duration")
|
||||
assert.NotNil(t, metric, "Expected histogram metric to exist")
|
||||
}
|
||||
|
||||
func TestRecordGeoLookup(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
initialCityCount := testutil.ToFloat64(geoLookups.WithLabelValues("city"))
|
||||
initialCountryCount := testutil.ToFloat64(geoLookups.WithLabelValues("asn"))
|
||||
|
||||
RecordGeoLookup("city")
|
||||
RecordGeoLookup("city")
|
||||
RecordGeoLookup("asn")
|
||||
|
||||
cityCount := testutil.ToFloat64(geoLookups.WithLabelValues("city"))
|
||||
assert.Equal(t, initialCityCount+2, cityCount, "Expected city lookups to increase by 2")
|
||||
|
||||
countryCount := testutil.ToFloat64(geoLookups.WithLabelValues("asn"))
|
||||
assert.Equal(t, initialCountryCount+1, countryCount, "Expected country lookups to increase by 1")
|
||||
}
|
||||
|
||||
func TestRecordPortScan(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
initialCount := testutil.ToFloat64(portScans)
|
||||
|
||||
RecordPortScan()
|
||||
RecordPortScan()
|
||||
|
||||
count := testutil.ToFloat64(portScans)
|
||||
assert.Equal(t, initialCount+2, count, "Expected port scans to increase by 2")
|
||||
}
|
||||
|
||||
func TestRecordDNSQuery(t *testing.T) {
|
||||
Enable()
|
||||
|
||||
initialACount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NOERROR"))
|
||||
initialAAAACount := testutil.ToFloat64(dnsQueries.WithLabelValues("AAAA", "NOERROR"))
|
||||
initialNXDOMAINCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NXDOMAIN"))
|
||||
|
||||
RecordDNSQuery("A", "NOERROR")
|
||||
RecordDNSQuery("A", "NOERROR")
|
||||
RecordDNSQuery("AAAA", "NOERROR")
|
||||
RecordDNSQuery("A", "NXDOMAIN")
|
||||
|
||||
aCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NOERROR"))
|
||||
assert.Equal(t, initialACount+2, aCount, "Expected A NOERROR queries to increase by 2")
|
||||
|
||||
aaaaCount := testutil.ToFloat64(dnsQueries.WithLabelValues("AAAA", "NOERROR"))
|
||||
assert.Equal(t, initialAAAACount+1, aaaaCount, "Expected AAAA NOERROR queries to increase by 1")
|
||||
|
||||
nxdomainCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NXDOMAIN"))
|
||||
assert.Equal(t, initialNXDOMAINCount+1, nxdomainCount, "Expected A NXDOMAIN queries to increase by 1")
|
||||
}
|
||||
+103
-39
@@ -9,9 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/core"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type geodbPath struct {
|
||||
type geodbConf struct {
|
||||
City string
|
||||
ASN string
|
||||
}
|
||||
@@ -19,95 +20,158 @@ type serverSettings struct {
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
type settings struct {
|
||||
GeodbPath geodbPath
|
||||
|
||||
type resolver struct {
|
||||
Domain string `yaml:"domain"`
|
||||
ResourceRecords []string `yaml:"resource_records"`
|
||||
RedirectPort string `yaml:"redirect_port,omitempty"`
|
||||
Ipv4 []string `yaml:"ipv4,omitempty"`
|
||||
Ipv6 []string `yaml:"ipv6,omitempty"`
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
GeodbPath geodbConf
|
||||
TemplatePath string
|
||||
BindAddress string
|
||||
TLSAddress string
|
||||
TLSCrtPath string
|
||||
TLSKeyPath string
|
||||
PrometheusAddress string
|
||||
TrustedHeader string
|
||||
TrustedPortHeader string
|
||||
EnableSecureHeaders bool
|
||||
EnableHTTP3 bool
|
||||
DisableTCPScan bool
|
||||
Server serverSettings
|
||||
Resolver resolver
|
||||
version bool
|
||||
}
|
||||
|
||||
const defaultAddress = ":8080"
|
||||
|
||||
// ErrVersion is the custom error triggered when -version flag is passed
|
||||
var ErrVersion = errors.New("setting: version requested")
|
||||
|
||||
// App is the var with the parsed settings
|
||||
var App = settings{
|
||||
// hard-coded for the time being
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup initializes the App object parsing the flags
|
||||
func Setup(args []string) (output string, err error) {
|
||||
func Setup(args []string) (cfg Settings, output string, err error) {
|
||||
flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError)
|
||||
var buf bytes.Buffer
|
||||
var resolverConf string
|
||||
flags.SetOutput(&buf)
|
||||
|
||||
flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database")
|
||||
flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database")
|
||||
flags.StringVar(&App.TemplatePath, "template", "", "Path to template file")
|
||||
cfg.Server = serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
flags.StringVar(&cfg.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)")
|
||||
flags.StringVar(&cfg.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)")
|
||||
flags.StringVar(&cfg.TemplatePath, "template", "", "Path to the template file")
|
||||
flags.StringVar(
|
||||
&App.BindAddress,
|
||||
&resolverConf,
|
||||
"resolver",
|
||||
"",
|
||||
"Path to the resolver configuration. It actually enables the resolver for DNS client discovery.")
|
||||
flags.StringVar(
|
||||
&cfg.BindAddress,
|
||||
"bind",
|
||||
defaultAddress,
|
||||
"Listening address (see https://pkg.go.dev/net?#Listen)",
|
||||
)
|
||||
flags.StringVar(
|
||||
&App.TLSAddress,
|
||||
&cfg.TLSAddress,
|
||||
"tls-bind",
|
||||
"",
|
||||
"Listening address for TLS (see https://pkg.go.dev/net?#Listen)",
|
||||
)
|
||||
flags.StringVar(&App.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file")
|
||||
flags.StringVar(&App.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file")
|
||||
flags.StringVar(&App.TrustedHeader,
|
||||
flags.StringVar(&cfg.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file")
|
||||
flags.StringVar(&cfg.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file")
|
||||
flags.StringVar(
|
||||
&cfg.PrometheusAddress,
|
||||
"metrics-bind",
|
||||
"",
|
||||
"Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint.",
|
||||
)
|
||||
flags.StringVar(
|
||||
&cfg.TrustedHeader,
|
||||
"trusted-header",
|
||||
"",
|
||||
"Trusted request header for remote IP (e.g. X-Real-IP)",
|
||||
"Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown'",
|
||||
)
|
||||
flags.BoolVar(&App.version, "version", false, "Output version information and exit")
|
||||
flags.StringVar(
|
||||
&cfg.TrustedPortHeader,
|
||||
"trusted-port-header",
|
||||
"",
|
||||
"Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory",
|
||||
)
|
||||
flags.BoolVar(&cfg.version, "version", false, "Output version information and exit")
|
||||
flags.BoolVar(
|
||||
&App.EnableSecureHeaders,
|
||||
&cfg.EnableSecureHeaders,
|
||||
"enable-secure-headers",
|
||||
false,
|
||||
"Add sane security-related headers to every response",
|
||||
)
|
||||
flags.BoolVar(
|
||||
&cfg.EnableHTTP3,
|
||||
"enable-http3",
|
||||
false,
|
||||
"Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.",
|
||||
)
|
||||
flags.BoolVar(
|
||||
&cfg.DisableTCPScan,
|
||||
"disable-scan",
|
||||
false,
|
||||
"Disable TCP port scanning functionality",
|
||||
)
|
||||
|
||||
err = flags.Parse(args)
|
||||
if err != nil {
|
||||
return buf.String(), err
|
||||
return cfg, buf.String(), err
|
||||
}
|
||||
|
||||
if App.version {
|
||||
return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion
|
||||
if cfg.version {
|
||||
return cfg, fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion
|
||||
}
|
||||
|
||||
if App.GeodbPath.City == "" || App.GeodbPath.ASN == "" {
|
||||
return "", fmt.Errorf("geoip2-city and geoip2-asn parameters are mandatory")
|
||||
if (cfg.GeodbPath.City != "" && cfg.GeodbPath.ASN == "") || (cfg.GeodbPath.City == "" && cfg.GeodbPath.ASN != "") {
|
||||
return cfg, "", errors.New("both --geoip2-city and --geoip2-asn are mandatory to enable geo information")
|
||||
}
|
||||
|
||||
if (App.TLSAddress != "") && (App.TLSCrtPath == "" || App.TLSKeyPath == "") {
|
||||
return "", fmt.Errorf("In order to use TLS -tls-crt and -tls-key flags are mandatory")
|
||||
if cfg.TrustedPortHeader != "" && cfg.TrustedHeader == "" {
|
||||
return cfg, "", errors.New("trusted-header is mandatory when trusted-port-header is set")
|
||||
}
|
||||
|
||||
if App.TemplatePath != "" {
|
||||
info, err := os.Stat(App.TemplatePath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("%s no such file or directory", App.TemplatePath)
|
||||
if (cfg.TLSAddress != "") && (cfg.TLSCrtPath == "" || cfg.TLSKeyPath == "") {
|
||||
return cfg, "", errors.New("in order to use TLS, the -tls-crt and -tls-key flags are mandatory")
|
||||
}
|
||||
|
||||
if cfg.EnableHTTP3 && cfg.TLSAddress == "" {
|
||||
return cfg, "", errors.New("in order to use HTTP3, the -tls-bind is mandatory")
|
||||
}
|
||||
|
||||
if cfg.TemplatePath != "" {
|
||||
info, err := os.Stat(cfg.TemplatePath)
|
||||
if err != nil {
|
||||
return cfg, "", fmt.Errorf("template path: %w", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return "", fmt.Errorf("%s must be a file", App.TemplatePath)
|
||||
return cfg, "", fmt.Errorf("%s must be a file", cfg.TemplatePath)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
if resolverConf != "" {
|
||||
var err error
|
||||
cfg.Resolver, err = readYAML(resolverConf)
|
||||
if err != nil {
|
||||
return cfg, "", fmt.Errorf("reading resolver configuration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, buf.String(), nil
|
||||
}
|
||||
|
||||
func readYAML(path string) (resolver resolver, err error) {
|
||||
yamlFile, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return resolver, err
|
||||
}
|
||||
return resolver, yaml.Unmarshal(yamlFile, &resolver)
|
||||
}
|
||||
|
||||
@@ -8,95 +8,94 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseMandatoryFlags(t *testing.T) {
|
||||
var mandatoryFlags = []struct {
|
||||
mandatoryFlags := []struct {
|
||||
args []string
|
||||
conf settings
|
||||
}{
|
||||
{
|
||||
[]string{},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{"-geoip2-city", "/city-path"},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{"-geoip2-asn", "/asn-path"},
|
||||
settings{},
|
||||
[]string{
|
||||
"-geoip2-city", "my-city-path",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-geoip2-asn", "my-asn-path",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
[]string{
|
||||
"-tls-bind", ":9000",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-tls-crt", "/crt-path",
|
||||
"-tls-bind", ":9000", "-tls-crt", "/crt-path",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-tls-key", "/key-path",
|
||||
"-tls-bind", ":9000", "-tls-key", "/key-path",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-enable-http3",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-bind", ":8000", "-trusted-port-header", "port-header",
|
||||
},
|
||||
settings{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range mandatoryFlags {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
_, err := Setup(tt.args)
|
||||
assert.NotNil(t, err)
|
||||
_, _, err := Setup(tt.args)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "mandatory")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlags(t *testing.T) {
|
||||
var flags = []struct {
|
||||
flags := []struct {
|
||||
args []string
|
||||
conf settings
|
||||
conf Settings
|
||||
}{
|
||||
{
|
||||
[]string{"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"},
|
||||
settings{
|
||||
GeodbPath: geodbPath{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
[]string{},
|
||||
Settings{
|
||||
BindAddress: ":8080",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-disable-scan"},
|
||||
Settings{
|
||||
BindAddress: ":8080",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
DisableTCPScan: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"-bind", ":8001", "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"},
|
||||
settings{
|
||||
GeodbPath: geodbPath{
|
||||
Settings{
|
||||
GeodbPath: geodbConf{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8001",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
BindAddress: ":8001",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
@@ -108,18 +107,34 @@ func TestParseFlags(t *testing.T) {
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
|
||||
"-tls-crt", "/crt-path", "-tls-key", "/key-path",
|
||||
},
|
||||
settings{
|
||||
GeodbPath: geodbPath{
|
||||
Settings{
|
||||
GeodbPath: geodbConf{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: ":9000",
|
||||
TLSCrtPath: "/crt-path",
|
||||
TLSKeyPath: "/key-path",
|
||||
TrustedHeader: "",
|
||||
EnableSecureHeaders: false,
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: ":9000",
|
||||
TLSCrtPath: "/crt-path",
|
||||
TLSKeyPath: "/key-path",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
|
||||
"-trusted-header", "header", "-trusted-port-header", "port-header",
|
||||
},
|
||||
Settings{
|
||||
GeodbPath: geodbConf{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
BindAddress: ":8080",
|
||||
TrustedHeader: "header",
|
||||
TrustedPortHeader: "port-header",
|
||||
Server: serverSettings{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
@@ -131,16 +146,12 @@ func TestParseFlags(t *testing.T) {
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
|
||||
"-trusted-header", "header", "-enable-secure-headers",
|
||||
},
|
||||
settings{
|
||||
GeodbPath: geodbPath{
|
||||
Settings{
|
||||
GeodbPath: geodbConf{
|
||||
City: "/city-path",
|
||||
ASN: "/asn-path",
|
||||
},
|
||||
TemplatePath: "",
|
||||
BindAddress: ":8080",
|
||||
TLSAddress: "",
|
||||
TLSCrtPath: "",
|
||||
TLSKeyPath: "",
|
||||
TrustedHeader: "header",
|
||||
EnableSecureHeaders: true,
|
||||
Server: serverSettings{
|
||||
@@ -153,19 +164,19 @@ func TestParseFlags(t *testing.T) {
|
||||
|
||||
for _, tt := range flags {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
_, err := Setup(tt.args)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, reflect.DeepEqual(App, tt.conf))
|
||||
cfg, _, err := Setup(tt.args)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, reflect.DeepEqual(cfg, tt.conf))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlagsUsage(t *testing.T) {
|
||||
var usageArgs = []string{"-help", "-h", "--help"}
|
||||
usageArgs := []string{"-help", "-h", "--help"}
|
||||
|
||||
for _, arg := range usageArgs {
|
||||
t.Run(arg, func(t *testing.T) {
|
||||
output, err := Setup([]string{arg})
|
||||
_, output, err := Setup([]string{arg})
|
||||
assert.ErrorIs(t, err, flag.ErrHelp)
|
||||
assert.Contains(t, output, "Usage of")
|
||||
})
|
||||
@@ -173,25 +184,34 @@ func TestParseFlagsUsage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseFlagVersion(t *testing.T) {
|
||||
output, err := Setup([]string{"-version"})
|
||||
_, output, err := Setup([]string{"-version"})
|
||||
assert.ErrorIs(t, err, ErrVersion)
|
||||
assert.Contains(t, output, "whatismyip version")
|
||||
}
|
||||
|
||||
func TestParseFlagTemplate(t *testing.T) {
|
||||
flags := []string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
|
||||
"-template", "/template-path",
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags []string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "Invalid template path",
|
||||
flags: []string{"-template", "/template-path"},
|
||||
errMsg: "no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "Template path is a directory",
|
||||
flags: []string{"-template", "/"},
|
||||
errMsg: "must be a file",
|
||||
},
|
||||
}
|
||||
_, err := Setup(flags)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no such file or directory")
|
||||
|
||||
flags = []string{
|
||||
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
|
||||
"-template", "/",
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, err := Setup(tc.flags)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errMsg)
|
||||
})
|
||||
}
|
||||
_, err = Setup(flags)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must be a file")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func IsValid(u string) bool {
|
||||
_, err := uuid.Parse(u)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
u string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Valid UUID",
|
||||
u: "3b241101-e2bb-4255-8caf-4136c566a964",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid UUID",
|
||||
u: "invalid-uuid",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
u: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.True(t, IsValid(tt.u) == tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
+93
-44
@@ -1,13 +1,14 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
// GeoRecord is the model for City database
|
||||
type GeoRecord struct {
|
||||
Country struct {
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
@@ -26,60 +27,108 @@ type GeoRecord struct {
|
||||
} `maxminddb:"postal"`
|
||||
}
|
||||
|
||||
// ASNRecord is the model for ASN database
|
||||
type ASNRecord struct {
|
||||
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
|
||||
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
|
||||
}
|
||||
|
||||
type geodb struct {
|
||||
city *maxminddb.Reader
|
||||
asn *maxminddb.Reader
|
||||
type GeoDB struct {
|
||||
cityPath string
|
||||
asnPath string
|
||||
City *maxminddb.Reader
|
||||
ASN *maxminddb.Reader
|
||||
}
|
||||
|
||||
var db geodb
|
||||
func Setup(cityPath string, asnPath string) (*GeoDB, error) {
|
||||
city, asn, err := openDatabases(cityPath, asnPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func openMMDB(path string) *maxminddb.Reader {
|
||||
return &GeoDB{
|
||||
cityPath: cityPath,
|
||||
asnPath: asnPath,
|
||||
City: city,
|
||||
ASN: asn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *GeoDB) CloseDBs() error {
|
||||
return closeReaders(db.City, db.ASN)
|
||||
}
|
||||
|
||||
func (db *GeoDB) Reload() error {
|
||||
city, asn, err := openDatabases(db.cityPath, db.asnPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening new databases: %w", err)
|
||||
}
|
||||
|
||||
oldCity, oldASN := db.City, db.ASN
|
||||
db.City, db.ASN = city, asn
|
||||
|
||||
if err := closeReaders(oldCity, oldASN); err != nil {
|
||||
return fmt.Errorf("closing previous databases: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeReaders(city *maxminddb.Reader, asn *maxminddb.Reader) error {
|
||||
var errs []error
|
||||
|
||||
if city != nil {
|
||||
if err := city.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("closing city db: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if asn != nil {
|
||||
if err := asn.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("closing ASN db: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (db *GeoDB) LookupCity(ip net.IP) (*GeoRecord, error) {
|
||||
record := &GeoRecord{}
|
||||
err := db.City.Lookup(ip, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (db *GeoDB) LookupASN(ip net.IP) (*ASNRecord, error) {
|
||||
record := &ASNRecord{}
|
||||
err := db.ASN.Lookup(ip, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func openDatabases(cityPath, asnPath string) (*maxminddb.Reader, *maxminddb.Reader, error) {
|
||||
city, err := openMMDB(cityPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
asn, err := openMMDB(asnPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return city, asn, nil
|
||||
}
|
||||
|
||||
func openMMDB(path string) (*maxminddb.Reader, error) {
|
||||
db, err := maxminddb.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Database %s has been loaded\n", path)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// Setup opens all Geolite2 databases
|
||||
func Setup(cityPath string, asnPath string) {
|
||||
db.city = openMMDB(cityPath)
|
||||
db.asn = openMMDB(asnPath)
|
||||
}
|
||||
|
||||
// CloseDBs unmaps from memory and frees resources to the filesystem
|
||||
func CloseDBs() {
|
||||
log.Printf("Closing dbs...")
|
||||
if err := db.city.Close(); err != nil {
|
||||
log.Printf("Error closing city db: %s", err)
|
||||
}
|
||||
if err := db.asn.Close(); err != nil {
|
||||
log.Printf("Error closing ASN db: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LookUp an IP and get city data
|
||||
func (record *GeoRecord) LookUp(ip net.IP) error {
|
||||
if err := db.city.Lookup(ip, record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookUp an IP and get ASN data
|
||||
func (record *ASNRecord) LookUp(ip net.IP) error {
|
||||
if err := db.asn.Lookup(ip, record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return db, nil
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,10 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModels(t *testing.T) {
|
||||
@@ -59,19 +61,21 @@ func TestModels(t *testing.T) {
|
||||
AutonomousSystemOrganization: "IP-Only",
|
||||
}
|
||||
|
||||
Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
defer CloseDBs()
|
||||
db, err := Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
require.NoError(t, err, fmt.Sprintf("Error setting up db: %s", err))
|
||||
t.Cleanup(func() { assert.NoError(t, db.CloseDBs()) })
|
||||
assert.NotNil(t, db.ASN)
|
||||
assert.NotNil(t, db.City)
|
||||
|
||||
assert.NotNil(t, db.asn)
|
||||
assert.NotNil(t, db.city)
|
||||
|
||||
cityRecord := &GeoRecord{}
|
||||
assert.Nil(t, cityRecord.LookUp(net.ParseIP("81.2.69.192")))
|
||||
cityRecord, err := db.LookupCity(net.ParseIP("81.2.69.192"))
|
||||
require.NoError(t, err, fmt.Sprintf("Error looking up city: %s", err))
|
||||
assert.Equal(t, expectedCity, cityRecord)
|
||||
assert.Error(t, cityRecord.LookUp(net.ParseIP("error")))
|
||||
_, err = db.LookupCity(net.ParseIP("error"))
|
||||
assert.Error(t, err)
|
||||
|
||||
asnRecord := &ASNRecord{}
|
||||
assert.Nil(t, asnRecord.LookUp(net.ParseIP("82.99.17.64")))
|
||||
asnRecord, err := db.LookupASN(net.ParseIP("82.99.17.64"))
|
||||
require.NoError(t, err, fmt.Sprintf("Error looking up asn: %s", err))
|
||||
assert.Equal(t, expectedASN, asnRecord)
|
||||
assert.Error(t, asnRecord.LookUp(net.ParseIP("error")))
|
||||
_, err = db.LookupASN(net.ParseIP("error"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/metrics"
|
||||
"github.com/dcarrillo/whatismyip/internal/validator/uuid"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Domain string
|
||||
ResourceRecords []string
|
||||
RedirectPort string
|
||||
IPv4 []string
|
||||
IPv6 []string
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
handler *dns.ServeMux
|
||||
store *cache.Cache
|
||||
domain string
|
||||
rr []dns.RR
|
||||
ipv4 []net.IP
|
||||
ipv6 []net.IP
|
||||
}
|
||||
|
||||
func ensureDotSuffix(s string) string {
|
||||
if !strings.HasSuffix(s, ".") {
|
||||
return s + "."
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Setup(store *cache.Cache, cfg Settings) (*Resolver, error) {
|
||||
domain := ensureDotSuffix(cfg.Domain)
|
||||
|
||||
rr := make([]dns.RR, 0, len(cfg.ResourceRecords))
|
||||
for _, res := range cfg.ResourceRecords {
|
||||
record, err := dns.NewRR(domain + " " + res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing resource record %q: %w", res, err)
|
||||
}
|
||||
rr = append(rr, record)
|
||||
}
|
||||
|
||||
ipv4, err := parseIPs(cfg.IPv4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing ipv4 addresses: %w", err)
|
||||
}
|
||||
ipv6, err := parseIPs(cfg.IPv6)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing ipv6 addresses: %w", err)
|
||||
}
|
||||
|
||||
resolver := &Resolver{
|
||||
handler: dns.NewServeMux(),
|
||||
store: store,
|
||||
domain: domain,
|
||||
rr: rr,
|
||||
ipv4: ipv4,
|
||||
ipv6: ipv6,
|
||||
}
|
||||
resolver.handler.HandleFunc(resolver.domain, resolver.resolve)
|
||||
resolver.handler.HandleFunc(".", resolver.blackHole)
|
||||
|
||||
return resolver, nil
|
||||
}
|
||||
|
||||
func parseIPs(addresses []string) ([]net.IP, error) {
|
||||
ips := make([]net.IP, 0, len(addresses))
|
||||
for _, address := range addresses {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q", address)
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func (rsv *Resolver) Handler() *dns.ServeMux {
|
||||
return rsv.handler
|
||||
}
|
||||
|
||||
func (rsv *Resolver) blackHole(w dns.ResponseWriter, r *dns.Msg) {
|
||||
msg := startReply(r)
|
||||
msg.SetRcode(r, dns.RcodeRefused)
|
||||
writeMsg(w, msg)
|
||||
logger(w, r.Question[0], msg.Rcode)
|
||||
metrics.RecordDNSQuery(dns.TypeToString[r.Question[0].Qtype], dns.RcodeToString[msg.Rcode])
|
||||
}
|
||||
|
||||
func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) {
|
||||
msg := startReply(r)
|
||||
q := r.Question[0]
|
||||
ip, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
||||
|
||||
for _, res := range rsv.rr {
|
||||
if q.Qtype == res.Header().Rrtype {
|
||||
msg.Answer = append(msg.Answer, dns.Copy(res))
|
||||
writeMsg(w, msg)
|
||||
logger(w, q, msg.Rcode)
|
||||
metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lowerName := strings.ToLower(q.Name) // lowercase because of dns-0x20
|
||||
subDomain := strings.Split(lowerName, ".")[0]
|
||||
switch {
|
||||
case uuid.IsValid(subDomain):
|
||||
msg.SetRcode(r, rsv.getIP(q, msg))
|
||||
// Add fails when the uuid is already registered; keep the first seen
|
||||
// resolver IP for the discovery window
|
||||
_ = rsv.store.Add(subDomain, ip, cache.DefaultExpiration)
|
||||
case lowerName == rsv.domain:
|
||||
msg.SetRcode(r, rsv.getIP(q, msg))
|
||||
default:
|
||||
msg.SetRcode(r, dns.RcodeRefused)
|
||||
}
|
||||
|
||||
writeMsg(w, msg)
|
||||
logger(w, q, msg.Rcode)
|
||||
metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode])
|
||||
}
|
||||
|
||||
func (rsv *Resolver) getIP(question dns.Question, msg *dns.Msg) int {
|
||||
if question.Qtype == dns.TypeA && len(rsv.ipv4) > 0 {
|
||||
for _, ip := range rsv.ipv4 {
|
||||
msg.Answer = append(msg.Answer, &dns.A{
|
||||
Hdr: setHdr(question),
|
||||
A: ip,
|
||||
})
|
||||
}
|
||||
return dns.RcodeSuccess
|
||||
}
|
||||
|
||||
if question.Qtype == dns.TypeAAAA && len(rsv.ipv6) > 0 {
|
||||
for _, ip := range rsv.ipv6 {
|
||||
msg.Answer = append(msg.Answer, &dns.AAAA{
|
||||
Hdr: setHdr(question),
|
||||
AAAA: ip,
|
||||
})
|
||||
}
|
||||
return dns.RcodeSuccess
|
||||
}
|
||||
|
||||
return dns.RcodeRefused
|
||||
}
|
||||
|
||||
func writeMsg(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
if err := w.WriteMsg(msg); err != nil {
|
||||
log.Printf("Failed to write DNS response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setHdr(q dns.Question) dns.RR_Header {
|
||||
return dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: q.Qtype,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
}
|
||||
}
|
||||
|
||||
func startReply(r *dns.Msg) *dns.Msg {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(r)
|
||||
msg.Authoritative = true
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func logger(w dns.ResponseWriter, q dns.Question, code int, err ...string) {
|
||||
emsg := ""
|
||||
if len(err) > 0 {
|
||||
emsg = " - " + strings.Join(err, " ")
|
||||
}
|
||||
ip, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
||||
log.Printf(
|
||||
"DNS %s - %s - %s - %s%s",
|
||||
ip,
|
||||
dns.TypeToString[q.Qtype],
|
||||
q.Name,
|
||||
dns.RcodeToString[code],
|
||||
emsg,
|
||||
)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type DNSJSONResponse struct {
|
||||
DNS dnsData `json:"dns"`
|
||||
}
|
||||
type dnsGeoData struct {
|
||||
Country string `json:"country,omitempty"`
|
||||
AsnOrganization string `json:"provider,omitempty"`
|
||||
}
|
||||
|
||||
type dnsData struct {
|
||||
IP string `json:"ip"`
|
||||
dnsGeoData
|
||||
}
|
||||
|
||||
// TODO
|
||||
// Implement a proper vhost manager instead of using a middleware
|
||||
func GetDNSDiscoveryHandler(store *cache.Cache, geoSvc *service.Geo, domain string, redirectPort string) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
host := hostWithoutPort(ctx.Request.Host)
|
||||
if host != domain && !strings.HasSuffix(host, "."+domain) {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if host == domain && ctx.Request.URL.Path == "/" {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("http://%s.%s%s", uuid.New().String(), domain, redirectPort))
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
handleDNS(ctx, store, geoSvc)
|
||||
ctx.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func hostWithoutPort(host string) string {
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
return h
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func handleDNS(ctx *gin.Context, store *cache.Cache, geoSvc *service.Geo) {
|
||||
d := strings.Split(ctx.Request.Host, ".")[0]
|
||||
if !validator.IsValid(d) {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
v, found := store.Get(d)
|
||||
if !found {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
ipStr, ok := v.(string)
|
||||
if !ok {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
geoResp := dnsGeoData{}
|
||||
if geoSvc != nil {
|
||||
if cityRecord := geoSvc.LookUpCity(ip); cityRecord != nil {
|
||||
geoResp.Country = cityRecord.Country.Names["en"]
|
||||
}
|
||||
if asnRecord := geoSvc.LookUpASN(ip); asnRecord != nil {
|
||||
geoResp.AsnOrganization = asnRecord.AutonomousSystemOrganization
|
||||
}
|
||||
}
|
||||
|
||||
j := DNSJSONResponse{
|
||||
DNS: dnsData{
|
||||
IP: ipStr,
|
||||
dnsGeoData: geoResp,
|
||||
},
|
||||
}
|
||||
|
||||
switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) {
|
||||
case gin.MIMEJSON:
|
||||
ctx.JSON(http.StatusOK, j)
|
||||
default:
|
||||
ctx.String(http.StatusOK, fmt.Sprintf("%s (%s / %s)\n", j.DNS.IP, j.DNS.Country, j.DNS.AsnOrganization))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validator "github.com/dcarrillo/whatismyip/internal/validator/uuid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDNSDiscoveryHandler(t *testing.T) {
|
||||
store := cache.New(cache.NoExpiration, cache.NoExpiration)
|
||||
handler := GetDNSDiscoveryHandler(store, rt.geo, domain, "")
|
||||
|
||||
t.Run("calls next if host does not have domain suffix", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
req.Host = "example.com"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
handler(c)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, testIP.ipv4+"\n", w.Body.String())
|
||||
})
|
||||
|
||||
t.Run("return 404 if there is a path", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/path", nil)
|
||||
req.Host = domain
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
handler(c)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
t.Run("redirects if host is domain", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Host = domain
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
handler(c)
|
||||
|
||||
assert.Equal(t, http.StatusFound, w.Code)
|
||||
r, err := url.Parse(w.Header().Get("Location"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, validator.IsValid(strings.Split(r.Host, ".")[0]))
|
||||
assert.Equal(t, domain, strings.Join(strings.Split(r.Host, ".")[1:], "."))
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleDNS(t *testing.T) {
|
||||
store := cache.New(cache.NoExpiration, cache.NoExpiration)
|
||||
u := uuid.New().String()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subDomain string
|
||||
stored any
|
||||
}{
|
||||
{
|
||||
name: "not found if the subdomain is not a valid uuid",
|
||||
subDomain: "not-uuid",
|
||||
stored: "",
|
||||
},
|
||||
{
|
||||
name: "not found if the ip is not found in the store",
|
||||
subDomain: u,
|
||||
stored: "",
|
||||
},
|
||||
{
|
||||
name: "not found if the ip is in store but is not valid",
|
||||
subDomain: u,
|
||||
stored: "bogus",
|
||||
},
|
||||
{
|
||||
name: "not found if the store contains no string",
|
||||
subDomain: u,
|
||||
stored: 20,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Host = tt.subDomain + "." + domain
|
||||
|
||||
if tt.stored != "" {
|
||||
store.Set(tt.subDomain, tt.stored, cache.DefaultExpiration)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
handleDNS(c, store, rt.geo)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptDNSRequest(t *testing.T) {
|
||||
store := cache.New(cache.NoExpiration, cache.NoExpiration)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accept string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "returns json dns data",
|
||||
accept: "application/json",
|
||||
want: jsonDNSIPv4,
|
||||
},
|
||||
{
|
||||
name: "return plan text dns data",
|
||||
accept: "text/plain",
|
||||
want: plainDNSIPv4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
u := uuid.New().String()
|
||||
req.Host = u + "." + domain
|
||||
req.Header.Add("Accept", tt.accept)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
store.Set(u, testIP.ipv4, cache.DefaultExpiration)
|
||||
handleDNS(c, store, rt.geo)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, tt.want, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
+90
-65
@@ -4,101 +4,126 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/httputils"
|
||||
"github.com/dcarrillo/whatismyip/internal/setting"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const userAgentPattern = `curl|wget|libwww-perl|python|ansible-httpget|HTTPie|WindowsPowerShell|http_request|Go-http-client|^$`
|
||||
type GeoResponse struct {
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Latitude float64 `json:"latitude,omitempty"`
|
||||
Longitude float64 `json:"longitude,omitempty"`
|
||||
PostalCode string `json:"postal_code,omitempty"`
|
||||
TimeZone string `json:"time_zone,omitempty"`
|
||||
ASN uint `json:"asn,omitempty"`
|
||||
ASNOrganization string `json:"asn_organization,omitempty"`
|
||||
}
|
||||
|
||||
// JSONResponse maps data as json
|
||||
type JSONResponse struct {
|
||||
IP string `json:"ip"`
|
||||
IPVersion byte `json:"ip_version"`
|
||||
ClientPort string `json:"client_port"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code"`
|
||||
City string `json:"city"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
ASN uint `json:"asn"`
|
||||
ASNOrganization string `json:"asn_organization"`
|
||||
Host string `json:"host"`
|
||||
Headers http.Header `json:"headers"`
|
||||
IP string `json:"ip"`
|
||||
IPVersion byte `json:"ip_version"`
|
||||
ClientPort string `json:"client_port"`
|
||||
Host string `json:"host"`
|
||||
Headers http.Header `json:"headers"`
|
||||
GeoResponse
|
||||
}
|
||||
|
||||
func getRoot(ctx *gin.Context) {
|
||||
reg := regexp.MustCompile(userAgentPattern)
|
||||
if reg.Match([]byte(ctx.Request.UserAgent())) {
|
||||
ctx.String(http.StatusOK, ctx.ClientIP())
|
||||
} else {
|
||||
func (rt *Router) getRoot(ctx *gin.Context) {
|
||||
switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) {
|
||||
case gin.MIMEHTML:
|
||||
name := "home"
|
||||
if setting.App.TemplatePath != "" {
|
||||
name = filepath.Base(setting.App.TemplatePath)
|
||||
if rt.templatePath != "" {
|
||||
name = filepath.Base(rt.templatePath)
|
||||
}
|
||||
ctx.HTML(http.StatusOK, name, jsonOutput(ctx))
|
||||
ctx.HTML(http.StatusOK, name, rt.jsonOutput(ctx))
|
||||
case gin.MIMEJSON:
|
||||
rt.getJSON(ctx)
|
||||
default:
|
||||
ctx.String(http.StatusOK, ctx.ClientIP()+"\n")
|
||||
}
|
||||
}
|
||||
|
||||
func getClientPortAsString(ctx *gin.Context) {
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
ctx.String(http.StatusOK, port+"\n")
|
||||
func (rt *Router) getClientPort(ctx *gin.Context) string {
|
||||
var port string
|
||||
if rt.trustedPortHeader == "" {
|
||||
if rt.trustedHeader != "" {
|
||||
port = "unknown"
|
||||
} else {
|
||||
_, port, _ = net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
}
|
||||
} else {
|
||||
port = ctx.GetHeader(rt.trustedPortHeader)
|
||||
if port == "" {
|
||||
port = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
return port
|
||||
}
|
||||
|
||||
func getAllAsString(ctx *gin.Context) {
|
||||
output := "IP: " + ctx.ClientIP() + "\n"
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
output += "Client Port: " + port + "\n"
|
||||
func (rt *Router) getClientPortAsString(ctx *gin.Context) {
|
||||
ctx.String(http.StatusOK, rt.getClientPort(ctx)+"\n")
|
||||
}
|
||||
|
||||
r := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
|
||||
if record := r.LookUpCity(); record != nil {
|
||||
output += geoCityRecordToString(record) + "\n"
|
||||
func (rt *Router) getAllAsString(ctx *gin.Context) {
|
||||
ip := net.ParseIP(ctx.ClientIP())
|
||||
|
||||
output := "IP: " + ip.String() + "\n"
|
||||
output += "Client Port: " + rt.getClientPort(ctx) + "\n"
|
||||
|
||||
if rt.geo != nil {
|
||||
if cityRecord := rt.geo.LookUpCity(ip); cityRecord != nil {
|
||||
output += geoCityRecordToString(cityRecord) + "\n"
|
||||
}
|
||||
if asnRecord := rt.geo.LookUpASN(ip); asnRecord != nil {
|
||||
output += geoASNRecordToString(asnRecord) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
if record := r.LookUpASN(); record != nil {
|
||||
output += geoASNRecordToString(record) + "\n"
|
||||
}
|
||||
|
||||
h := ctx.Request.Header
|
||||
h["Host"] = []string{ctx.Request.Host}
|
||||
h := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader)
|
||||
h.Set("Host", ctx.Request.Host)
|
||||
output += httputils.HeadersToSortedString(h)
|
||||
|
||||
ctx.String(http.StatusOK, output)
|
||||
}
|
||||
|
||||
func getJSON(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, jsonOutput(ctx))
|
||||
func (rt *Router) getJSON(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, rt.jsonOutput(ctx))
|
||||
}
|
||||
|
||||
func jsonOutput(ctx *gin.Context) JSONResponse {
|
||||
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
|
||||
asnRecord := ip.LookUpASN()
|
||||
cityRecord := ip.LookUpCity()
|
||||
func (rt *Router) jsonOutput(ctx *gin.Context) JSONResponse {
|
||||
ip := net.ParseIP(ctx.ClientIP())
|
||||
|
||||
var version byte = 4
|
||||
if p := net.ParseIP(ctx.ClientIP()).To4(); p == nil {
|
||||
if p := ip.To4(); p == nil {
|
||||
version = 6
|
||||
}
|
||||
|
||||
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
|
||||
geoResp := GeoResponse{}
|
||||
if rt.geo != nil {
|
||||
if cityRecord := rt.geo.LookUpCity(ip); cityRecord != nil {
|
||||
geoResp.Country = cityRecord.Country.Names["en"]
|
||||
geoResp.CountryCode = cityRecord.Country.ISOCode
|
||||
geoResp.City = cityRecord.City.Names["en"]
|
||||
geoResp.Latitude = cityRecord.Location.Latitude
|
||||
geoResp.Longitude = cityRecord.Location.Longitude
|
||||
geoResp.PostalCode = cityRecord.Postal.Code
|
||||
geoResp.TimeZone = cityRecord.Location.TimeZone
|
||||
}
|
||||
if asnRecord := rt.geo.LookUpASN(ip); asnRecord != nil {
|
||||
geoResp.ASN = asnRecord.AutonomousSystemNumber
|
||||
geoResp.ASNOrganization = asnRecord.AutonomousSystemOrganization
|
||||
}
|
||||
}
|
||||
|
||||
return JSONResponse{
|
||||
IP: ctx.ClientIP(),
|
||||
IPVersion: version,
|
||||
ClientPort: port,
|
||||
Country: cityRecord.Country.Names["en"],
|
||||
CountryCode: cityRecord.Country.ISOCode,
|
||||
City: cityRecord.City.Names["en"],
|
||||
Latitude: cityRecord.Location.Latitude,
|
||||
Longitude: cityRecord.Location.Longitude,
|
||||
PostalCode: cityRecord.Postal.Code,
|
||||
TimeZone: cityRecord.Location.TimeZone,
|
||||
ASN: asnRecord.AutonomousSystemNumber,
|
||||
ASNOrganization: asnRecord.AutonomousSystemOrganization,
|
||||
Host: ctx.Request.Host,
|
||||
Headers: ctx.Request.Header,
|
||||
IP: ip.String(),
|
||||
IPVersion: version,
|
||||
ClientPort: rt.getClientPort(ctx),
|
||||
Host: ctx.Request.Host,
|
||||
Headers: httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader),
|
||||
GeoResponse: geoResp,
|
||||
}
|
||||
}
|
||||
|
||||
+183
-54
@@ -1,39 +1,91 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIP4RootFromCli(t *testing.T) {
|
||||
uas := []string{
|
||||
"",
|
||||
"curl",
|
||||
"wget",
|
||||
"libwww-perl",
|
||||
"python",
|
||||
"ansible-httpget",
|
||||
"HTTPie",
|
||||
"WindowsPowerShell",
|
||||
"http_request",
|
||||
"Go-http-client",
|
||||
func TestRootContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
accepted string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Accept wildcard",
|
||||
accepted: "*/*",
|
||||
expected: contentType.text,
|
||||
},
|
||||
{
|
||||
name: "Bogus accept",
|
||||
accepted: "bogus/plain",
|
||||
expected: contentType.text,
|
||||
},
|
||||
{
|
||||
name: "Accept plain text",
|
||||
accepted: "text/plain",
|
||||
expected: contentType.text,
|
||||
},
|
||||
{
|
||||
name: "Accept json",
|
||||
accepted: "application/json",
|
||||
expected: contentType.json,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
req.Header.Set("Accept", tt.accepted)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Real-IP", testIP.ipv4)
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
for _, ua := range uas {
|
||||
req.Header.Set("User-Agent", ua)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, tt.expected, w.Header().Get("Content-Type"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
func TestGetIP(t *testing.T) {
|
||||
expected := testIP.ipv4 + "\n"
|
||||
tests := []struct {
|
||||
name string
|
||||
accepted string
|
||||
}{
|
||||
{
|
||||
name: "No browser",
|
||||
accepted: "*/*",
|
||||
},
|
||||
{
|
||||
name: "Bogus accept",
|
||||
accepted: "bogus/plain",
|
||||
},
|
||||
{
|
||||
name: "Plain accept",
|
||||
accepted: "text/plain",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
req.Header.Set("Accept", tt.accepted)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, testIP.ipv4, w.Body.String())
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, expected, w.Body.String())
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +100,68 @@ func TestHost(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientPort(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/client-port", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Header.Set("X-Real-IP", testIP.ipv4)
|
||||
type args struct {
|
||||
trustedHeader string
|
||||
trustedPortHeader string
|
||||
headers map[string][]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "No trusted headers set",
|
||||
expected: "1000\n",
|
||||
},
|
||||
{
|
||||
name: "Trusted header only set",
|
||||
args: args{
|
||||
trustedHeader: trustedHeader,
|
||||
},
|
||||
expected: "unknown\n",
|
||||
},
|
||||
{
|
||||
name: "Trusted and port header set but not included in headers",
|
||||
args: args{
|
||||
trustedHeader: trustedHeader,
|
||||
trustedPortHeader: trustedPortHeader,
|
||||
},
|
||||
expected: "unknown\n",
|
||||
},
|
||||
{
|
||||
name: "Trusted and port header set and included in headers",
|
||||
args: args{
|
||||
trustedHeader: trustedHeader,
|
||||
trustedPortHeader: trustedPortHeader,
|
||||
headers: map[string][]string{
|
||||
trustedHeader: {testIP.ipv4},
|
||||
trustedPortHeader: {"1001"},
|
||||
},
|
||||
},
|
||||
expected: "1001\n",
|
||||
},
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
engine := gin.Default()
|
||||
engine.TrustedPlatform = tt.args.trustedHeader
|
||||
r := NewRouter(nil, tt.args.trustedHeader, tt.args.trustedPortHeader, "", false)
|
||||
Setup(engine, r)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "1000\n", w.Body.String())
|
||||
req, _ := http.NewRequest("GET", "/client-port", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Header = tt.args.headers
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, tt.expected, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
@@ -71,36 +175,56 @@ func TestNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
expectedIPv4 := `{"client_port":"1000","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test","headers":{"X-Real-Ip":["81.2.69.192"]}}`
|
||||
expectedIPv6 := `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1000", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}`
|
||||
svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
engine := gin.Default()
|
||||
engine.TrustedPlatform = trustedHeader
|
||||
r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false)
|
||||
Setup(engine, r)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/json", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Host = "test"
|
||||
req.Header.Set("X-Real-IP", testIP.ipv4)
|
||||
type args struct {
|
||||
ip string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
args: args{
|
||||
ip: testIP.ipv4,
|
||||
},
|
||||
expected: jsonIPv4,
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
args: args{
|
||||
ip: testIP.ipv6,
|
||||
},
|
||||
expected: jsonIPv6,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/json", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(tt.args.ip, "1000")
|
||||
req.Host = "test"
|
||||
req.Header.Set(trustedHeader, tt.args.ip)
|
||||
req.Header.Set(trustedPortHeader, "1001")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
|
||||
assert.JSONEq(t, expectedIPv4, w.Body.String())
|
||||
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv6, "1000")
|
||||
req.Host = "test"
|
||||
req.Header.Set("X-Real-IP", testIP.ipv6)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
|
||||
assert.JSONEq(t, expectedIPv6, w.Body.String())
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
|
||||
assert.JSONEq(t, tt.expected, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
expected := `IP: 81.2.69.192
|
||||
Client Port: 1000
|
||||
Client Port: 1001
|
||||
City: London
|
||||
Country: United Kingdom
|
||||
Country Code: GB
|
||||
@@ -114,17 +238,22 @@ ASN Organization:
|
||||
|
||||
Header1: one
|
||||
Host: test
|
||||
X-Real-Ip: 81.2.69.192
|
||||
`
|
||||
svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
engine := gin.Default()
|
||||
engine.TrustedPlatform = trustedHeader
|
||||
r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false)
|
||||
Setup(engine, r)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/all", nil)
|
||||
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
|
||||
req.Host = "test"
|
||||
req.Header.Set("X-Real-IP", testIP.ipv4)
|
||||
req.Header.Set(trustedHeader, testIP.ipv4)
|
||||
req.Header.Set(trustedPortHeader, "1001")
|
||||
req.Header.Set("Header1", "one")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
|
||||
+40
-17
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/models"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -82,32 +81,56 @@ var asnOutput = map[string]asnDataFormatter{
|
||||
},
|
||||
}
|
||||
|
||||
func getGeoAsString(ctx *gin.Context) {
|
||||
field := strings.ToLower(ctx.Params.ByName("field"))
|
||||
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
|
||||
record := ip.LookUpCity()
|
||||
func (rt *Router) getGeoAsString(ctx *gin.Context) {
|
||||
if rt.geo == nil {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
record := rt.geo.LookUpCity(net.ParseIP(ctx.ClientIP()))
|
||||
if record == nil {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
field := strings.ToLower(ctx.Params.ByName("field"))
|
||||
if field == "" {
|
||||
ctx.String(http.StatusOK, geoCityRecordToString(record))
|
||||
} else if g, ok := geoOutput[field]; ok {
|
||||
ctx.String(http.StatusOK, g.format(record))
|
||||
} else {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
g, ok := geoOutput[field]
|
||||
if !ok {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
ctx.String(http.StatusOK, g.format(record))
|
||||
}
|
||||
|
||||
func getASNAsString(ctx *gin.Context) {
|
||||
field := strings.ToLower(ctx.Params.ByName("field"))
|
||||
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
|
||||
record := ip.LookUpASN()
|
||||
func (rt *Router) getASNAsString(ctx *gin.Context) {
|
||||
if rt.geo == nil {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
record := rt.geo.LookUpASN(net.ParseIP(ctx.ClientIP()))
|
||||
if record == nil {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
field := strings.ToLower(ctx.Params.ByName("field"))
|
||||
if field == "" {
|
||||
ctx.String(http.StatusOK, geoASNRecordToString(record))
|
||||
} else if g, ok := asnOutput[field]; ok {
|
||||
ctx.String(http.StatusOK, g.format(record))
|
||||
} else {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
g, ok := asnOutput[field]
|
||||
if !ok {
|
||||
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
ctx.String(http.StatusOK, g.format(record))
|
||||
}
|
||||
|
||||
func geoCityRecordToString(record *models.GeoRecord) string {
|
||||
|
||||
+7
-6
@@ -9,16 +9,17 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getHeadersAsSortedString(ctx *gin.Context) {
|
||||
h := ctx.Request.Header
|
||||
h["Host"] = []string{ctx.Request.Host}
|
||||
|
||||
func (rt *Router) getHeadersAsSortedString(ctx *gin.Context) {
|
||||
h := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader)
|
||||
h.Set("Host", ctx.Request.Host)
|
||||
ctx.String(http.StatusOK, httputils.HeadersToSortedString(h))
|
||||
}
|
||||
|
||||
func getHeaderAsString(ctx *gin.Context) {
|
||||
func (rt *Router) getHeaderAsString(ctx *gin.Context) {
|
||||
headers := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader)
|
||||
|
||||
h := ctx.Params.ByName("header")
|
||||
if v := ctx.GetHeader(h); v != "" {
|
||||
if v := headers.Get(ctx.Params.ByName("header")); v != "" {
|
||||
ctx.String(http.StatusOK, template.HTMLEscapeString(v))
|
||||
} else if strings.ToLower(h) == "host" {
|
||||
ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host))
|
||||
|
||||
+11
-2
@@ -1,10 +1,13 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -26,16 +29,22 @@ Header2: value22
|
||||
Header3: value3
|
||||
Host:
|
||||
`
|
||||
|
||||
svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
engine := gin.Default()
|
||||
engine.TrustedPlatform = trustedHeader
|
||||
r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false)
|
||||
Setup(engine, r)
|
||||
req, _ := http.NewRequest("GET", "/headers", nil)
|
||||
req.Header = map[string][]string{
|
||||
"Header1": {"value1"},
|
||||
"Header2": {"value21", "value22"},
|
||||
"Header3": {"value3"},
|
||||
}
|
||||
req.Header.Set(trustedHeader, "1.1.1.1")
|
||||
req.Header.Set(trustedPortHeader, "1025")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type JSONScanResponse struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (rt *Router) scanTCPPort(ctx *gin.Context) {
|
||||
port, err := strconv.Atoi(ctx.Params.ByName("port"))
|
||||
if err == nil && (port < 1 || port > 65535) {
|
||||
err = fmt.Errorf("%d is not a valid port number", port)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, JSONScanResponse{
|
||||
Reason: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ctx.ClientIP())
|
||||
if ip == nil {
|
||||
ctx.JSON(http.StatusBadRequest, JSONScanResponse{
|
||||
Reason: "client ip could not be determined",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
add := net.TCPAddr{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
}
|
||||
|
||||
scan := service.PortScanner{
|
||||
Address: &add,
|
||||
}
|
||||
|
||||
isOpen, err := scan.IsPortOpen()
|
||||
reason := ""
|
||||
if err != nil {
|
||||
reason = err.Error()
|
||||
}
|
||||
|
||||
response := JSONScanResponse{
|
||||
IP: ctx.ClientIP(),
|
||||
Port: port,
|
||||
Reachable: isOpen,
|
||||
Reason: reason,
|
||||
}
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
+39
-21
@@ -4,31 +4,49 @@ import (
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/setting"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SetupTemplate reads and parses a template from file
|
||||
func SetupTemplate(r *gin.Engine) {
|
||||
if setting.App.TemplatePath == "" {
|
||||
t, _ := template.New("home").Parse(home)
|
||||
r.SetHTMLTemplate(t)
|
||||
} else {
|
||||
log.Printf("Template %s has been loaded", setting.App.TemplatePath)
|
||||
r.LoadHTMLFiles(setting.App.TemplatePath)
|
||||
type Router struct {
|
||||
geo *service.Geo
|
||||
trustedHeader string
|
||||
trustedPortHeader string
|
||||
templatePath string
|
||||
disableScan bool
|
||||
}
|
||||
|
||||
func NewRouter(geo *service.Geo, trustedHeader, trustedPortHeader, templatePath string, disableScan bool) *Router {
|
||||
return &Router{
|
||||
geo: geo,
|
||||
trustedHeader: trustedHeader,
|
||||
trustedPortHeader: trustedPortHeader,
|
||||
templatePath: templatePath,
|
||||
disableScan: disableScan,
|
||||
}
|
||||
}
|
||||
|
||||
// Setup defines the endpoints
|
||||
func Setup(r *gin.Engine) {
|
||||
r.GET("/", getRoot)
|
||||
r.GET("/client-port", getClientPortAsString)
|
||||
r.GET("/geo", getGeoAsString)
|
||||
r.GET("/geo/:field", getGeoAsString)
|
||||
r.GET("/asn", getASNAsString)
|
||||
r.GET("/asn/:field", getASNAsString)
|
||||
r.GET("/headers", getHeadersAsSortedString)
|
||||
r.GET("/all", getAllAsString)
|
||||
r.GET("/json", getJSON)
|
||||
r.GET("/:header", getHeaderAsString)
|
||||
func SetupTemplate(r *gin.Engine, templatePath string) {
|
||||
if templatePath == "" {
|
||||
r.SetHTMLTemplate(template.Must(template.New("home").Parse(home)))
|
||||
} else {
|
||||
log.Printf("Template %s has been loaded", templatePath)
|
||||
r.LoadHTMLFiles(templatePath)
|
||||
}
|
||||
}
|
||||
|
||||
func Setup(r *gin.Engine, rt *Router) {
|
||||
r.GET("/", rt.getRoot)
|
||||
if !rt.disableScan {
|
||||
r.GET("/scan/tcp/:port", rt.scanTCPPort)
|
||||
}
|
||||
r.GET("/client-port", rt.getClientPortAsString)
|
||||
r.GET("/geo", rt.getGeoAsString)
|
||||
r.GET("/geo/:field", rt.getGeoAsString)
|
||||
r.GET("/asn", rt.getASNAsString)
|
||||
r.GET("/asn/:field", rt.getASNAsString)
|
||||
r.GET("/headers", rt.getHeadersAsSortedString)
|
||||
r.GET("/all", rt.getAllAsString)
|
||||
r.GET("/json", rt.getJSON)
|
||||
r.GET("/:header", rt.getHeaderAsString)
|
||||
}
|
||||
|
||||
+18
-5
@@ -1,10 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/models"
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -16,6 +17,7 @@ type testIPs struct {
|
||||
}
|
||||
|
||||
type contentTypes struct {
|
||||
html string
|
||||
text string
|
||||
json string
|
||||
}
|
||||
@@ -29,19 +31,30 @@ var (
|
||||
ipv6ASN: "2a02:a800::1",
|
||||
}
|
||||
contentType = contentTypes{
|
||||
html: "content-type: text/html; charset=utf-8",
|
||||
text: "text/plain; charset=utf-8",
|
||||
json: "application/json; charset=utf-8",
|
||||
}
|
||||
jsonIPv4 = `{"client_port":"1001","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"time_zone":"Europe/London","host":"test", "headers": {}}`
|
||||
jsonIPv6 = `{"asn":3352,"asn_organization":"TELEFONICA DE ESPANA","client_port":"1001","host":"test","ip":"2a02:9000::1","ip_version":6,"headers": {}}`
|
||||
jsonDNSIPv4 = `{"dns":{"ip":"81.2.69.192","country":"United Kingdom"}}`
|
||||
plainDNSIPv4 = "81.2.69.192 (United Kingdom / )\n"
|
||||
)
|
||||
|
||||
const trustedHeader = "X-Real-IP"
|
||||
const (
|
||||
trustedHeader = "X-Real-IP"
|
||||
trustedPortHeader = "X-Real-Port"
|
||||
domain = "dns.example.com"
|
||||
)
|
||||
|
||||
var rt *Router
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
app = gin.Default()
|
||||
app.TrustedPlatform = trustedHeader
|
||||
models.Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
Setup(app)
|
||||
defer models.CloseDBs()
|
||||
svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
rt = NewRouter(svc, trustedHeader, "", "", false)
|
||||
Setup(app, rt)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
+16
-14
@@ -62,20 +62,22 @@ func TestDefaultTemplate(t *testing.T) {
|
||||
|
||||
tmpl, _ := template.New("home").Parse(home)
|
||||
response := JSONResponse{
|
||||
IP: "127.0.0.1",
|
||||
IPVersion: 4,
|
||||
ClientPort: "1000",
|
||||
Country: "A Country",
|
||||
CountryCode: "XX",
|
||||
City: "A City",
|
||||
Latitude: 100,
|
||||
Longitude: -100,
|
||||
PostalCode: "00000",
|
||||
TimeZone: "My/Timezone",
|
||||
ASN: 0,
|
||||
ASNOrganization: "My ISP",
|
||||
Host: "localhost",
|
||||
Headers: req.Header,
|
||||
IP: "127.0.0.1",
|
||||
IPVersion: 4,
|
||||
ClientPort: "1000",
|
||||
Host: "localhost",
|
||||
Headers: req.Header,
|
||||
GeoResponse: GeoResponse{
|
||||
Country: "A Country",
|
||||
CountryCode: "XX",
|
||||
City: "A City",
|
||||
Latitude: 100,
|
||||
Longitude: -100,
|
||||
PostalCode: "00000",
|
||||
TimeZone: "My/Timezone",
|
||||
ASN: 0,
|
||||
ASNOrganization: "My ISP",
|
||||
},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const port = 53
|
||||
|
||||
type DNS struct {
|
||||
server *dns.Server
|
||||
handler *dns.Handler
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewDNSServer(ctx context.Context, handler dns.Handler) *DNS {
|
||||
return &DNS{
|
||||
handler: &handler,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNS) Start() {
|
||||
d.server = &dns.Server{
|
||||
Addr: ":" + strconv.Itoa(port),
|
||||
Net: "udp",
|
||||
Handler: *d.handler,
|
||||
// UDPSize: 65535,
|
||||
// ReusePort: true,
|
||||
}
|
||||
|
||||
log.Printf("Starting DNS server listening on :%d (udp)", port)
|
||||
go func() {
|
||||
if err := d.server.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *DNS) Stop() {
|
||||
log.Print("Stopping DNS server...")
|
||||
ctx, cancel := context.WithTimeout(d.ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := d.server.ShutdownContext(ctx); err != nil {
|
||||
log.Printf("DNS server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type Prometheus struct {
|
||||
server *http.Server
|
||||
ctx context.Context
|
||||
addr string
|
||||
timeouts Timeouts
|
||||
}
|
||||
|
||||
func NewPrometheusServer(ctx context.Context, addr string, timeouts Timeouts) *Prometheus {
|
||||
return &Prometheus{
|
||||
ctx: ctx,
|
||||
addr: addr,
|
||||
timeouts: timeouts,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prometheus) Start() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
p.server = &http.Server{
|
||||
Addr: p.addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: p.timeouts.ReadTimeout,
|
||||
WriteTimeout: p.timeouts.WriteTimeout,
|
||||
}
|
||||
|
||||
log.Printf("Starting Prometheus server listening on %s", p.addr)
|
||||
go func() {
|
||||
if err := p.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Prometheus) Stop() {
|
||||
log.Print("Stopping Prometheus server...")
|
||||
ctx, cancel := context.WithTimeout(p.ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := p.server.Shutdown(ctx); err != nil {
|
||||
log.Printf("Prometheus server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
type Quic struct {
|
||||
server *http3.Server
|
||||
tlsServer *TLS
|
||||
ctx context.Context
|
||||
addr string
|
||||
crtPath string
|
||||
keyPath string
|
||||
}
|
||||
|
||||
func NewQuicServer(ctx context.Context, tlsServer *TLS, addr, crt, key string) *Quic {
|
||||
return &Quic{
|
||||
tlsServer: tlsServer,
|
||||
ctx: ctx,
|
||||
addr: addr,
|
||||
crtPath: crt,
|
||||
keyPath: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Quic) Start() {
|
||||
q.server = &http3.Server{
|
||||
Addr: q.addr,
|
||||
Handler: q.tlsServer.server.Handler,
|
||||
}
|
||||
|
||||
parentHandler := q.tlsServer.server.Handler
|
||||
q.tlsServer.server.Handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if err := q.server.SetQUICHeaders(rw.Header()); err != nil {
|
||||
log.Printf("Failed to set QUIC headers: %s", err)
|
||||
}
|
||||
|
||||
parentHandler.ServeHTTP(rw, req)
|
||||
})
|
||||
|
||||
log.Printf("Starting QUIC server listening on %s (udp)", q.addr)
|
||||
go func() {
|
||||
if err := q.server.ListenAndServeTLS(q.crtPath, q.keyPath); err != nil &&
|
||||
!errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (q *Quic) Stop() {
|
||||
log.Print("Stopping QUIC server...")
|
||||
ctx, cancel := context.WithTimeout(q.ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := q.server.Shutdown(ctx); err != nil {
|
||||
log.Printf("QUIC server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/service"
|
||||
)
|
||||
|
||||
const shutdownTimeout = 10 * time.Second
|
||||
|
||||
type Server interface {
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
servers []Server
|
||||
geoSvc *service.Geo
|
||||
}
|
||||
|
||||
func Setup(servers []Server, geoSvc *service.Geo) *Manager {
|
||||
return &Manager{
|
||||
servers: servers,
|
||||
geoSvc: geoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Run() {
|
||||
m.start()
|
||||
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
for {
|
||||
s := <-signalChan
|
||||
|
||||
if s == syscall.SIGHUP {
|
||||
m.stop()
|
||||
if m.geoSvc != nil {
|
||||
m.geoSvc.Reload()
|
||||
}
|
||||
m.start()
|
||||
} else {
|
||||
log.Print("Shutting down...")
|
||||
m.stop()
|
||||
if m.geoSvc != nil {
|
||||
m.geoSvc.Shutdown()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) start() {
|
||||
for _, s := range m.servers {
|
||||
s.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) stop() {
|
||||
for _, s := range m.servers {
|
||||
s.Stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Timeouts struct {
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
type TCP struct {
|
||||
server *http.Server
|
||||
handler http.Handler
|
||||
ctx context.Context
|
||||
addr string
|
||||
timeouts Timeouts
|
||||
}
|
||||
|
||||
func NewTCPServer(ctx context.Context, handler http.Handler, addr string, timeouts Timeouts) *TCP {
|
||||
return &TCP{
|
||||
handler: handler,
|
||||
ctx: ctx,
|
||||
addr: addr,
|
||||
timeouts: timeouts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCP) Start() {
|
||||
t.server = &http.Server{
|
||||
Addr: t.addr,
|
||||
Handler: t.handler,
|
||||
ReadTimeout: t.timeouts.ReadTimeout,
|
||||
WriteTimeout: t.timeouts.WriteTimeout,
|
||||
}
|
||||
|
||||
log.Printf("Starting TCP server listening on %s", t.addr)
|
||||
go func() {
|
||||
if err := t.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TCP) Stop() {
|
||||
log.Print("Stopping TCP server...")
|
||||
ctx, cancel := context.WithTimeout(t.ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := t.server.Shutdown(ctx); err != nil {
|
||||
log.Printf("TCP server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type TLS struct {
|
||||
server *http.Server
|
||||
handler http.Handler
|
||||
ctx context.Context
|
||||
addr string
|
||||
crtPath string
|
||||
keyPath string
|
||||
timeouts Timeouts
|
||||
}
|
||||
|
||||
func NewTLSServer(ctx context.Context, handler http.Handler, addr, crt, key string, timeouts Timeouts) *TLS {
|
||||
return &TLS{
|
||||
handler: handler,
|
||||
ctx: ctx,
|
||||
addr: addr,
|
||||
crtPath: crt,
|
||||
keyPath: key,
|
||||
timeouts: timeouts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TLS) Start() {
|
||||
t.server = &http.Server{
|
||||
Addr: t.addr,
|
||||
Handler: t.handler,
|
||||
ReadTimeout: t.timeouts.ReadTimeout,
|
||||
WriteTimeout: t.timeouts.WriteTimeout,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
}
|
||||
|
||||
log.Printf("Starting TLS server listening on %s", t.addr)
|
||||
go func() {
|
||||
if err := t.server.ListenAndServeTLS(t.crtPath, t.keyPath); err != nil &&
|
||||
!errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TLS) Stop() {
|
||||
log.Print("Stopping TLS server...")
|
||||
ctx, cancel := context.WithTimeout(t.ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := t.server.Shutdown(ctx); err != nil {
|
||||
log.Printf("TLS server forced to shutdown: %s", err)
|
||||
}
|
||||
}
|
||||
+66
-12
@@ -1,37 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/metrics"
|
||||
"github.com/dcarrillo/whatismyip/models"
|
||||
)
|
||||
|
||||
// Geo defines a base type for lookups
|
||||
type Geo struct {
|
||||
IP net.IP
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
db *models.GeoDB
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// LookUpCity queries the database for city data related to the given IP
|
||||
func (g *Geo) LookUpCity() *models.GeoRecord {
|
||||
record := &models.GeoRecord{}
|
||||
err := record.LookUp(g.IP)
|
||||
func NewGeo(ctx context.Context, cityPath string, asnPath string) (*Geo, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
db, err := models.Setup(cityPath, asnPath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
geo := &Geo{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
db: db,
|
||||
}
|
||||
|
||||
return geo, nil
|
||||
}
|
||||
|
||||
func (g *Geo) LookUpCity(ip net.IP) *models.GeoRecord {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
record, err := g.db.LookupCity(ip)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
metrics.RecordGeoLookup("city")
|
||||
return record
|
||||
}
|
||||
|
||||
// LookUpASN queries the database for ASN data related to the given IP
|
||||
func (g *Geo) LookUpASN() *models.ASNRecord {
|
||||
record := &models.ASNRecord{}
|
||||
err := record.LookUp(g.IP)
|
||||
func (g *Geo) LookUpASN(ip net.IP) *models.ASNRecord {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
record, err := g.db.LookupASN(ip)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
metrics.RecordGeoLookup("asn")
|
||||
return record
|
||||
}
|
||||
|
||||
func (g *Geo) Shutdown() {
|
||||
g.cancel()
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if err := g.db.CloseDBs(); err != nil {
|
||||
log.Printf("Error closing geo databases: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Geo) Reload() {
|
||||
if err := g.ctx.Err(); err != nil {
|
||||
log.Printf("Skipping reload, service is shutting down: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if err := g.db.Reload(); err != nil {
|
||||
log.Printf("Geo database reload failed: %s", err)
|
||||
return
|
||||
}
|
||||
log.Print("Geo database reloaded")
|
||||
}
|
||||
|
||||
+42
-11
@@ -1,36 +1,67 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var geoSvc *Geo
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
models.Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
defer models.CloseDBs()
|
||||
geoSvc, _ = NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestCityLookup(t *testing.T) {
|
||||
ip := Geo{IP: net.ParseIP("error")}
|
||||
c := ip.LookUpCity()
|
||||
c := geoSvc.LookUpCity(net.ParseIP("error"))
|
||||
assert.Nil(t, c)
|
||||
|
||||
ip = Geo{IP: net.ParseIP("1.1.1.1")}
|
||||
c = ip.LookUpCity()
|
||||
c = geoSvc.LookUpCity(net.ParseIP("1.1.1.1"))
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestASNLookup(t *testing.T) {
|
||||
ip := Geo{IP: net.ParseIP("error")}
|
||||
a := ip.LookUpASN()
|
||||
a := geoSvc.LookUpASN(net.ParseIP("error"))
|
||||
assert.Nil(t, a)
|
||||
|
||||
ip = Geo{IP: net.ParseIP("1.1.1.1")}
|
||||
a = ip.LookUpASN()
|
||||
a = geoSvc.LookUpASN(net.ParseIP("1.1.1.1"))
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
|
||||
func TestReloadIsSafeForConcurrentLookups(t *testing.T) {
|
||||
svc, err := NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
|
||||
require.NoError(t, err)
|
||||
defer svc.Shutdown()
|
||||
|
||||
ip := net.ParseIP("81.2.69.192")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
stop := make(chan struct{})
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
svc.LookUpCity(ip)
|
||||
svc.LookUpASN(ip)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for range 100 {
|
||||
svc.Reload()
|
||||
}
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
|
||||
assert.NotNil(t, svc.LookUpCity(ip))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/dcarrillo/whatismyip/internal/metrics"
|
||||
)
|
||||
|
||||
const scannerTimeOut = 3 * time.Second
|
||||
|
||||
type PortScanner struct {
|
||||
Address net.Addr
|
||||
}
|
||||
|
||||
func (p *PortScanner) IsPortOpen() (bool, error) {
|
||||
conn, err := net.DialTimeout(p.Address.Network(), p.Address.String(), scannerTimeOut)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if conn != nil {
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
metrics.RecordPortScan()
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG ARG_VERSION
|
||||
ENV VERSION=$ARG_VERSION
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
FROM builder AS build-test-app
|
||||
RUN CGO_ENABLED=0 \
|
||||
go build -ldflags="-s -w" -o whatismyip ./cmd
|
||||
|
||||
FROM scratch AS test
|
||||
COPY --from=build-test-app /app/whatismyip /usr/bin/
|
||||
ENTRYPOINT ["whatismyip"]
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
domain: dns.example.com
|
||||
redirect_port: ":8000"
|
||||
resource_records:
|
||||
- "1800 IN SOA xns.example.com. hostmaster.example.com. 1 10000 2400 604800 1800"
|
||||
- "3600 IN NS xns.example.com."
|
||||
ipv4:
|
||||
- "127.0.0.2"
|
||||
ipv6:
|
||||
- "aaa:aaa:aaa:aaaa::1"
|
||||
Reference in New Issue
Block a user