51 Commits
1.0.1 ... 2.1.2

Author SHA1 Message Date
c54cf5a456 Add upx compression to Dockerfile 2023-04-29 18:49:21 +02:00
7dfa0a2e6d Update dependencies 2023-04-27 18:21:27 +02:00
68ef680439 Bump github.com/docker/docker (#11)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.17+incompatible to 20.10.24+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.17...v20.10.24)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 19:50:22 +02:00
bd42f712ea Bump github.com/opencontainers/runc from 1.1.3 to 1.1.5 (#10)
Bumps [github.com/opencontainers/runc](https://github.com/opencontainers/runc) from 1.1.3 to 1.1.5.
- [Release notes](https://github.com/opencontainers/runc/releases)
- [Changelog](https://github.com/opencontainers/runc/blob/v1.1.5/CHANGELOG.md)
- [Commits](https://github.com/opencontainers/runc/compare/v1.1.3...v1.1.5)

---
updated-dependencies:
- dependency-name: github.com/opencontainers/runc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-29 18:23:38 +02:00
0b31633309 Remove useless function 2023-03-21 20:19:11 +01:00
b5fa3be506 Move logs about stopping servers to the proper place 2023-03-20 19:27:17 +01:00
8783db018b Use pointers to proper server initializing handling 2023-03-20 17:43:06 +01:00
e60d1ae5b7 Initial server handling refactor (#9) 2023-03-20 16:36:55 +01:00
84a767ade0 chore: Fix README.md typos 2023-03-18 21:14:08 +01:00
19c72f94a5 Add experimental support for HTTP/3 (#8)
* Wait for service in integration tests instead of watching for a string

* Add http3 experimental support
2023-03-18 21:06:51 +01:00
de78dcdf52 Bump dependencies due to minor security issues 2023-03-16 19:59:39 +01:00
eb200ddd81 Fix minor linting issues 2023-03-16 19:59:04 +01:00
5c4ac4a3ee [skip ci] bump action-gh-release to v1 2023-03-10 20:03:32 +01:00
ee328892d6 Bump gin to v1.9.0 2023-03-10 19:30:03 +01:00
04d983d671 Remove redundant ifs 2023-03-10 19:27:10 +01:00
202518d547 Merge branch 'main' of github.com:dcarrillo/whatismyip 2023-02-16 19:27:07 +01:00
52d14fe78f Update dependencies 2023-02-16 19:26:53 +01:00
8aadc4fbb6 Bump golang to 1.20 2023-02-16 19:24:16 +01:00
b6391d8fd1 Bump github.com/containerd/containerd from 1.6.12 to 1.6.18 (#6)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.6.12 to 1.6.18.
2023-02-16 19:21:04 +01:00
35fac1bd57 chore: update linters and go action 2023-01-26 20:47:03 +01:00
b13a30c354 chore: disable go cache 2023-01-26 20:43:13 +01:00
5982683cdd chore: use cache in CI and bump checkout action to v3 2023-01-26 20:38:31 +01:00
1a986a029f Update gin to v1.8.2 (GO-2022-1144) and containerd (CVE-2022-27664) 2022-12-26 19:30:02 +01:00
ed0ddccab5 Update golang.org/x/net (CVE-2022-32149) 2022-11-05 13:37:05 +01:00
20ae50c115 Update golang.org/x/net (CVE-2022-27664) 2022-09-13 20:38:24 +02:00
9763ed0e29 Update to golang 1.19 2022-08-11 20:02:27 +02:00
88691a5149 Update README.md
Add CodeQL badge
2022-07-03 11:57:58 +02:00
6b7fc0bc6a Bump gin and testify to latest versions 2022-07-03 11:39:51 +02:00
c5a659ff64 Create codeql-analysis.yml 2022-07-03 11:29:23 +02:00
bd06da7b2b Bump gin-gonic to 1.8.0 2022-06-03 18:37:22 +02:00
b61d64a755 Moving secure again, this time to github.com/gin-contrib/secure 2022-05-07 15:56:21 +02:00
3df794ecc4 Bump testcontainers-go tp 0.13.0 2022-05-07 13:06:19 +02:00
ca1d002974 Use gin-gonic/contrib/secure instead of deprecated module 2022-05-07 12:51:13 +02:00
1ee7256506 Remove headers set by a trusted proxy from outputs 2022-05-02 18:07:40 +02:00
7c70abf07f Add feature to get the right client port when using a trusted proxy 2022-05-02 17:56:57 +02:00
9070e9a2c2 Use Accept request header instead of user agent to figure out non-browser clients. Funny fact, I borrowed the idea from a fork ('8a3e142cf3/router/generic.go (L33)') 2022-04-30 17:35:12 +02:00
12da27ddab Add optional secure headers to responses 2022-04-02 18:12:31 +02:00
aae2e08240 Bump maxminddb-golang to 1.9.0 2022-04-02 18:07:12 +02:00
e4667bda05 Remove timestamp snapshot from log when releasing 2022-03-19 19:08:02 +01:00
49b28b6536 Update golangci-lint and shadow 2022-03-19 19:02:41 +01:00
edd0713c21 Update modules and bump go version to 1.18 2022-03-19 18:59:15 +01:00
ab46275990 Tidy up the house 2022-01-27 13:17:51 +01:00
f7d0a679c9 Add test for geo model 2021-12-01 18:09:13 +01:00
ac32dc240f Add release badge to README 2021-12-01 13:18:32 +01:00
2d330a99b7 Add CI and go-report badges. Add comments to make golint happy 2021-11-30 17:49:02 +01:00
9b10052cd1 refactor on some tests 2021-11-30 16:32:52 +01:00
def2b0f2f0 Update Gin and testcontainers to latest version 2021-11-30 16:07:31 +01:00
8b5cccd744 Refactor setting module and add tests (#4) 2021-11-29 18:16:27 +01:00
2571e22843 Fix format errors and add gofmt and golines to Makefile 2021-11-21 18:23:36 +01:00
2807e2afc9 Improve client port handling and fix IPv6 client port testing 2021-11-21 12:49:13 +01:00
1aa59f1b0b Fix long lines (#2)
Using [golines](https://github.com/segmentio/golines):
`golines -w .`
2021-11-18 19:26:53 +01:00
27 changed files with 1616 additions and 416 deletions

55
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,55 @@
# 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
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: install go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
- run: |
echo "Build"
make build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -11,19 +11,20 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
make: ["lint", "test"]
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v3
- name: install go - name: install go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version-file: go.mod
cache: true
- name: Lint - name: ${{ matrix.make }}
run: make lint run: make ${{ matrix.make }}
- name: Tests
run: make test
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -33,9 +34,14 @@ jobs:
matrix: matrix:
goosarch: [linux-amd64] goosarch: [linux-amd64]
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: install go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: true
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
@ -51,12 +57,12 @@ jobs:
- name: Prepare release - name: Prepare release
run: | run: |
git log $(git describe HEAD~ --tags --abbrev=0)..HEAD --pretty='format:%h - %s (%cr) <%an>' --no-merges > changelog.txt 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 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 sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256
- name: Release - name: Release
uses: softprops/action-gh-release@v0.1.14 uses: softprops/action-gh-release@v1
with: with:
body_path: changelog.txt body_path: changelog.txt
files: | files: |

View File

@ -1,4 +1,4 @@
FROM golang:1.17-alpine as builder FROM golang:1.20-alpine as builder
ARG ARG_VERSION ARG ARG_VERSION
ENV VERSION $ARG_VERSION ENV VERSION $ARG_VERSION
@ -7,7 +7,8 @@ WORKDIR /app
COPY . . COPY . .
RUN apk add make && make build VERSION=$VERSION RUN apk add make git upx && make build VERSION=$VERSION \
&& upx --best --lzma whatismyip
# Build final image # Build final image
FROM scratch FROM scratch

View File

@ -16,14 +16,20 @@ integration-test:
.PHONY: install-tools .PHONY: install-tools
install-tools: install-tools:
@command golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @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.43.0; \ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin; \
fi fi
@command $(GOPATH)/shadow > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @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.7; \ 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 fi
.PHONY: lint .PHONY: lint
lint: install-tools lint: install-tools
gofmt -l . && test -z $$(gofmt -l .)
golines -l . && test -z $$(golines -l .)
golangci-lint run golangci-lint run
shadow ./... shadow ./...

View File

@ -1,18 +1,25 @@
# What is my IP address # What is my IP address
[![CI](https://github.com/dcarrillo/whatismyip/workflows/CI/badge.svg)](https://github.com/dcarrillo/whatismyip/actions)
[![CodeQL](https://github.com/dcarrillo/whatismyip/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/dcarrillo/whatismyip/actions/workflows/codeql-analysis.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/dcarrillo/whatismyip)](https://goreportcard.com/report/github.com/dcarrillo/whatismyip)
[![GitHub release](https://img.shields.io/github/release/dcarrillo/whatismyip.svg)](https://github.com/dcarrillo/whatismyip/releases/)
[![License Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE)
- [What is my IP address](#what-is-my-ip-address) - [What is my IP address](#what-is-my-ip-address)
- [Features](#features) - [Features](#features)
- [Endpoints](#endpoints) - [Endpoints](#endpoints)
- [Build](#build) - [Build](#build)
- [Usage](#usage) - [Usage](#usage)
- [Examples](#examples) - [Examples](#examples)
- [Run a default TCP server](#run-a-default-tcp-server) - [Run a default TCP server](#run-a-default-tcp-server)
- [Run a TLS server only](#run-a-tls-server-only) - [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) - [Run an HTTP/3 server](#run-an-http3-server)
- [Download](#download) - [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)
- [Docker](#docker) - [Download](#download)
- [Running a container locally using test databases](#running-a-container-locally-using-test-databases) - [Docker](#docker)
- [From Docker Hub](#from-docker-hub) - [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. 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.
@ -30,19 +37,21 @@ curl -6 ifconfig.es
## Features ## Features
- TLS is avaliable. - 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.
- 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. - 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. - 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.
- High performance. - High performance.
- Self-contained server what can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored. - 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. - HTML templates for the landing page.
- Text plain and JSON output. - Text plain and JSON output.
## Endpoints ## Endpoints
- https://ifconfig.es/ - 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
- https://ifconfig.es/geo/city - https://ifconfig.es/geo/city
- https://ifconfig.es/geo/country - https://ifconfig.es/geo/country
@ -60,16 +69,20 @@ curl -6 ifconfig.es
## Build ## Build
Golang >= 1.17 is required. Previous versions may work. Golang >= 1.19 is required.
`make build` `make build`
## Usage ## Usage
```text ```text
Usage of ./whatismyip: Usage of whatismyip:
-bind string -bind string
Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080") Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080")
-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 -geoip2-asn string
Path to GeoIP2 ASN database Path to GeoIP2 ASN database
-geoip2-city string -geoip2-city string
@ -83,7 +96,9 @@ Usage of ./whatismyip:
-tls-key string -tls-key string
When using TLS, path to private key file When using TLS, path to private key file
-trusted-header string -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 -version
Output version information and exit Output version information and exit
``` ```
@ -96,27 +111,36 @@ Usage of ./whatismyip:
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb ./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb
``` ```
### Run a TLS server only ### Run a TLS (HTTP/2) server only
```bash ```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 ./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
``` ```
### 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 ```bash
./whatismyip -geoip2-city ./test/GeoIP2-City-Test.mmdb -geoip2-asn ./test/GeoLite2-ASN-Test.mmdb -trusted-header X-Real-IP -template mytemplate.tmpl ./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 -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
Download latest version from https://github.com/dcarrillo/whatismyip/releases Download the latest version from https://github.com/dcarrillo/whatismyip/releases
## Docker ## Docker
An ultra-light (~9MB) image is available. An ultra-light (~4MB) 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).
### Running a container locally using test databases ### Run a container locally using test databases
`make docker-run` `make docker-run`
@ -127,6 +151,6 @@ docker run --tty --interactive --rm \
-v $PWD/<path to city database>:/tmp/GeoIP2-City-Test.mmdb:ro \ -v $PWD/<path to city database>:/tmp/GeoIP2-City-Test.mmdb:ro \
-v $PWD/<path to ASN database>:/tmp/GeoLite2-ASN-Test.mmdb:ro -p 8080:8080 \ -v $PWD/<path to ASN database>:/tmp/GeoLite2-ASN-Test.mmdb:ro -p 8080:8080 \
dcarrillo/whatismyip:latest \ dcarrillo/whatismyip:latest \
-geoip2-city /tmp/GeoIP2-City-Test.mmdb \ -geoip2-city /tmp/GeoIP2-City-Test.mmdb \
-geoip2-asn /tmp/GeoLite2-ASN-Test.mmdb -geoip2-asn /tmp/GeoLite2-ASN-Test.mmdb
``` ```

View File

@ -2,131 +2,56 @@ package main
import ( import (
"context" "context"
"errors" "flag"
"log" "fmt"
"net/http"
"os" "os"
"os/signal"
"syscall"
"github.com/dcarrillo/whatismyip/internal/httputils" "github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/server"
"github.com/gin-contrib/secure"
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/models"
"github.com/dcarrillo/whatismyip/router" "github.com/dcarrillo/whatismyip/router"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var (
tcpServer *http.Server
tlsServer *http.Server
engine *gin.Engine
)
func main() { func main() {
setting.Setup() 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.Println(err)
os.Exit(1)
}
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN) models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
setupEngine() engine := setupEngine()
router.SetupTemplate(engine) router.SetupTemplate(engine)
router.Setup(engine) router.Setup(engine)
if setting.App.BindAddress != "" { whatismyip := server.Setup(context.Background(), engine.Handler())
runTCPServer() whatismyip.Run()
}
if setting.App.TLSAddress != "" {
runTLSServer()
}
runHandler()
} }
func runHandler() { func setupEngine() *gin.Engine {
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
}
}
}
func runTCPServer() {
tcpServer = &http.Server{
Addr: setting.App.BindAddress,
Handler: engine,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
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...")
}()
}
func runTLSServer() {
tlsServer = &http.Server{
Addr: setting.App.TLSAddress,
Handler: engine,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
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...")
}()
}
func setupEngine() {
gin.DisableConsoleColor() gin.DisableConsoleColor()
if os.Getenv(gin.EnvGinMode) == "" { if os.Getenv(gin.EnvGinMode) == "" {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
engine = gin.New() engine := gin.New()
engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter)) engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter))
engine.Use(gin.Recovery()) engine.Use(gin.Recovery())
if setting.App.EnableSecureHeaders {
engine.Use(secure.New(secure.Config{
BrowserXssFilter: true,
ContentTypeNosniff: true,
FrameDeny: true,
}))
}
_ = engine.SetTrustedProxies(nil) _ = engine.SetTrustedProxies(nil)
engine.TrustedPlatform = setting.App.TrustedHeader engine.TrustedPlatform = setting.App.TrustedHeader
return engine
} }

98
go.mod
View File

@ -1,59 +1,77 @@
module github.com/dcarrillo/whatismyip module github.com/dcarrillo/whatismyip
go 1.17 go 1.20
require ( require (
github.com/gin-gonic/gin v1.7.2-0.20211103141324-89a159bdd92c github.com/gin-contrib/secure v0.0.1
github.com/oschwald/maxminddb-golang v1.8.0 github.com/gin-gonic/gin v1.9.0
github.com/stretchr/testify v1.7.0 github.com/oschwald/maxminddb-golang v1.10.0
github.com/testcontainers/testcontainers-go v0.11.1 github.com/quic-go/quic-go v0.34.0
github.com/stretchr/testify v1.8.2
github.com/testcontainers/testcontainers-go v0.13.0
golang.org/x/net v0.9.0
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.8.16 // indirect github.com/Microsoft/hcsshim v0.9.6 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/bytedance/sonic v1.8.8 // indirect
github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/containerd/containerd v1.5.0-beta.4 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/containerd v1.6.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect github.com/go-playground/validator/v10 v10.12.0 // indirect
github.com/goccy/go-json v0.7.10 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/uuid v1.2.0 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20230426061923-93006964c1fc // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/leodido/go-urn v1.2.3 // indirect
github.com/moby/sys/mount v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect
github.com/moby/sys/mountinfo v0.4.1 // indirect github.com/mattn/go-isatty v0.0.18 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/moby/sys/mount v0.3.3 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.0.0-rc93 // indirect github.com/opencontainers/runc v1.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
go.opencensus.io v0.22.3 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/text v0.3.7 // indirect go.opencensus.io v0.23.0 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect golang.org/x/arch v0.3.0 // indirect
google.golang.org/grpc v1.33.2 // indirect golang.org/x/crypto v0.8.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect golang.org/x/mod v0.10.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.0 // indirect
google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

466
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -4,14 +4,17 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"io/ioutil" "io"
"log" "log"
"net/http" "net/http"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/dcarrillo/whatismyip/router" "github.com/dcarrillo/whatismyip/router"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
@ -19,7 +22,7 @@ import (
func buildContainer() testcontainers.ContainerRequest { func buildContainer() testcontainers.ContainerRequest {
_, filename, _, _ := runtime.Caller(0) _, filename, _, _ := runtime.Caller(0)
dirname := filepath.Dir(filename) dir := filepath.Dir(filename)
req := testcontainers.ContainerRequest{ req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{ FromDockerfile: testcontainers.FromDockerfile{
@ -34,15 +37,25 @@ func buildContainer() testcontainers.ContainerRequest {
"-tls-crt", "/tmp/server.pem", "-tls-crt", "/tmp/server.pem",
"-tls-key", "/tmp/server.key", "-tls-key", "/tmp/server.key",
"-trusted-header", "X-Real-IP", "-trusted-header", "X-Real-IP",
"-enable-secure-headers",
"-enable-http3",
}, },
ExposedPorts: []string{"8000:8000", "8001:8001"}, ExposedPorts: []string{"8000:8000", "8001:8001", "8001:8001/udp"},
WaitingFor: wait.ForLog("Starting TLS server listening on :8001"), WaitingFor: wait.ForHTTP("/geo").
BindMounts: map[string]string{ WithTLS(true, &tls.Config{InsecureSkipVerify: true}).
filepath.Join(dirname, "/../test/GeoIP2-City-Test.mmdb"): "/tmp/GeoIP2-City-Test.mmdb", WithPort("8001"),
filepath.Join(dirname, "/../test/GeoLite2-ASN-Test.mmdb"): "/tmp/GeoLite2-ASN-Test.mmdb", Mounts: testcontainers.Mounts(
filepath.Join(dirname, "/../test/server.pem"): "/tmp/server.pem", testcontainers.BindMount(
filepath.Join(dirname, "/../test/server.key"): "/tmp/server.key", filepath.Join(dir, "/../test/GeoIP2-City-Test.mmdb"),
}, "/tmp/GeoIP2-City-Test.mmdb",
),
testcontainers.BindMount(
filepath.Join(dir, "/../test/GeoLite2-ASN-Test.mmdb"),
"/tmp/GeoLite2-ASN-Test.mmdb",
),
testcontainers.BindMount(filepath.Join(dir, "/../test/server.pem"), "/tmp/server.pem"),
testcontainers.BindMount(filepath.Join(dir, "/../test/server.key"), "/tmp/server.key"),
),
} }
return req return req
@ -62,23 +75,84 @@ func TestContainerIntegration(t *testing.T) {
log.Fatal(err) log.Fatal(err)
} }
defer func() { defer func() {
err := container.Terminate(ctx) err = container.Terminate(ctx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
}() }()
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 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 tests := []struct {
body, err := ioutil.ReadAll(resp.Body) name string
if err != nil { url string
log.Fatal(err) 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,
},
}
assert.NoError(t, json.Unmarshal(body, &dat)) for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", tt.url, nil)
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, _ = client.Do(req)
body, err = io.ReadAll(resp.Body)
if strings.Contains(tt.url, "https://") {
assert.Equal(t, `h3=":8001"; ma=2592000,h3-29=":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"))
})
} }
} }
func doQuicRequest(req *http.Request) (*http.Response, []byte, error) {
roundTripper := &http3.RoundTripper{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
QuicConfig: &quic.Config{},
}
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
}

View File

@ -1,3 +1,4 @@
package core package core
// Version to be defined at build time
var Version = "tobedefined" var Version = "tobedefined"

View File

@ -3,12 +3,15 @@ package httputils
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/textproto"
"sort" "sort"
"strings" "strings"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// HeadersToSortedString shorts and dumps http.Header to a string separated by \n
func HeadersToSortedString(headers http.Header) string { func HeadersToSortedString(headers http.Header) string {
var output string var output string
@ -31,6 +34,18 @@ func HeadersToSortedString(headers http.Header) string {
return output return output
} }
// GetHeadersWithoutTrustedHeaders return a http.Heade object with the original headers except trusted headers
func GetHeadersWithoutTrustedHeaders(ctx *gin.Context) http.Header {
h := ctx.Request.Header
for _, k := range []string{setting.App.TrustedHeader, setting.App.TrustedPortHeader} {
delete(h, textproto.CanonicalMIMEHeaderKey(k))
}
return h
}
// GetLogFormatter returns our custom log format
func GetLogFormatter(param gin.LogFormatterParams) string { func GetLogFormatter(param gin.LogFormatterParams) string {
return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %d %s \"%s\" \"%s\" \"%s\"\n", return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %d %s \"%s\" \"%s\" \"%s\"\n",
param.ClientIP, param.ClientIP,
@ -57,9 +72,8 @@ func normalizeLog(log interface{}) interface{} {
case []string: case []string:
if len(v) == 0 { if len(v) == 0 {
return "-" return "-"
} else {
return strings.Join(v, ", ")
} }
return strings.Join(v, ", ")
} }
return log return log

View File

@ -1,6 +1,8 @@
package setting package setting
import ( import (
"bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@ -18,70 +20,117 @@ type serverSettings struct {
WriteTimeout time.Duration WriteTimeout time.Duration
} }
type settings struct { type settings struct {
GeodbPath geodbPath GeodbPath geodbPath
TemplatePath string TemplatePath string
BindAddress string BindAddress string
TLSAddress string TLSAddress string
TLSCrtPath string TLSCrtPath string
TLSKeyPath string TLSKeyPath string
TrustedHeader string TrustedHeader string
Server serverSettings TrustedPortHeader string
EnableSecureHeaders bool
EnableHTTP3 bool
Server serverSettings
version bool
} }
const defaultAddress = ":8080" const defaultAddress = ":8080"
var App *settings // ErrVersion is the custom error triggered when -version flag is passed
var ErrVersion = errors.New("setting: version requested")
func Setup() { // App is the var with the parsed settings
city := flag.String("geoip2-city", "", "Path to GeoIP2 city database") var App = settings{
asn := flag.String("geoip2-asn", "", "Path to GeoIP2 ASN database") // hard-coded for the time being
template := flag.String("template", "", "Path to template file") Server: serverSettings{
address := flag.String("bind", defaultAddress, "Listening address (see https://pkg.go.dev/net?#Listen)") ReadTimeout: 10 * time.Second,
addressTLS := flag.String("tls-bind", "", "Listening address for TLS (see https://pkg.go.dev/net?#Listen)") WriteTimeout: 10 * time.Second,
tlsCrtPath := flag.String("tls-crt", "", "When using TLS, path to certificate file") },
tlsKeyPath := flag.String("tls-key", "", "When using TLS, path to private key file") }
trustedHeader := flag.String("trusted-header", "", "Trusted request header for remote IP (e.g. X-Real-IP)")
ver := flag.Bool("version", false, "Output version information and exit")
flag.Parse() // Setup initializes the App object parsing the flags
func Setup(args []string) (output string, err error) {
flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)
if *ver { flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database")
fmt.Printf("whatismyip version %s", core.Version) flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database")
os.Exit(0) flags.StringVar(&App.TemplatePath, "template", "", "Path to template file")
flags.StringVar(
&App.BindAddress,
"bind",
defaultAddress,
"Listening address (see https://pkg.go.dev/net?#Listen)",
)
flags.StringVar(
&App.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,
"trusted-header",
"",
"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.StringVar(
&App.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(&App.version, "version", false, "Output version information and exit")
flags.BoolVar(
&App.EnableSecureHeaders,
"enable-secure-headers",
false,
"Add sane security-related headers to every response",
)
flags.BoolVar(
&App.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.",
)
err = flags.Parse(args)
if err != nil {
return buf.String(), err
} }
if *city == "" || *asn == "" { if App.version {
exitWithError("geoip2-city and geoip2-asn parameters are mandatory") return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion
} }
if (*addressTLS != "") && (*tlsCrtPath == "" || *tlsKeyPath == "") { if App.TrustedPortHeader != "" && App.TrustedHeader == "" {
exitWithError("In order to use TLS -tls-crt and -tls-key flags are mandatory") return "", fmt.Errorf("truster-header is mandatory when truster-port-header is set")
} }
if *template != "" { if App.GeodbPath.City == "" || App.GeodbPath.ASN == "" {
info, err := os.Stat(*template) return "", fmt.Errorf("geoip2-city and geoip2-asn parameters are mandatory")
if os.IsNotExist(err) || info.IsDir() { }
exitWithError(*template + " doesn't exist or it's not a file")
if (App.TLSAddress != "") && (App.TLSCrtPath == "" || App.TLSKeyPath == "") {
return "", fmt.Errorf("in order to use TLS, the -tls-crt and -tls-key flags are mandatory")
}
if App.EnableHTTP3 && App.TLSAddress == "" {
return "", fmt.Errorf("in order to use HTTP3, the -tls-bind is mandatory")
}
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 info.IsDir() {
return "", fmt.Errorf("%s must be a file", App.TemplatePath)
} }
} }
App = &settings{ return buf.String(), nil
GeodbPath: geodbPath{City: *city, ASN: *asn},
TemplatePath: *template,
BindAddress: *address,
TLSAddress: *addressTLS,
TLSCrtPath: *tlsCrtPath,
TLSKeyPath: *tlsKeyPath,
TrustedHeader: *trustedHeader,
Server: serverSettings{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
}
}
func exitWithError(error string) {
fmt.Printf("%s\n\n", error)
flag.Usage()
os.Exit(1)
} }

View File

@ -0,0 +1,202 @@
package setting
import (
"flag"
"reflect"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseMandatoryFlags(t *testing.T) {
var mandatoryFlags = []struct {
args []string
}{
{
[]string{},
},
{
[]string{"-geoip2-city", "/city-path"},
},
{
[]string{"-geoip2-asn", "/asn-path"},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
"-tls-crt", "/crt-path",
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
"-tls-key", "/key-path",
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-enable-http3",
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-bind", ":8000",
"-trusted-port-header", "port-header",
},
},
}
for _, tt := range mandatoryFlags {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
_, err := Setup(tt.args)
require.NotNil(t, err)
assert.Contains(t, err.Error(), "mandatory")
})
}
}
func TestParseFlags(t *testing.T) {
var flags = []struct {
args []string
conf settings
}{
{
[]string{"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"},
settings{
GeodbPath: geodbPath{
City: "/city-path",
ASN: "/asn-path",
},
BindAddress: ":8080",
Server: serverSettings{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
},
},
{
[]string{"-bind", ":8001", "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"},
settings{
GeodbPath: geodbPath{
City: "/city-path",
ASN: "/asn-path",
},
BindAddress: ":8001",
Server: serverSettings{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000",
"-tls-crt", "/crt-path", "-tls-key", "/key-path",
},
settings{
GeodbPath: geodbPath{
City: "/city-path",
ASN: "/asn-path",
},
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: geodbPath{
City: "/city-path",
ASN: "/asn-path",
},
BindAddress: ":8080",
TrustedHeader: "header",
TrustedPortHeader: "port-header",
Server: serverSettings{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
},
},
{
[]string{
"-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path",
"-trusted-header", "header", "-enable-secure-headers",
},
settings{
GeodbPath: geodbPath{
City: "/city-path",
ASN: "/asn-path",
},
BindAddress: ":8080",
TrustedHeader: "header",
EnableSecureHeaders: true,
Server: serverSettings{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
},
},
}
for _, tt := range flags {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
_, err := Setup(tt.args)
require.Nil(t, err)
assert.True(t, reflect.DeepEqual(App, tt.conf))
})
}
}
func TestParseFlagsUsage(t *testing.T) {
var usageArgs = []string{"-help", "-h", "--help"}
for _, arg := range usageArgs {
t.Run(arg, func(t *testing.T) {
output, err := Setup([]string{arg})
assert.ErrorIs(t, err, flag.ErrHelp)
assert.Contains(t, output, "Usage of")
})
}
}
func TestParseFlagVersion(t *testing.T) {
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",
}
_, 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", "/",
}
_, err = Setup(flags)
require.Error(t, err)
assert.Contains(t, err.Error(), "must be a file")
}

View File

@ -7,10 +7,7 @@ import (
"github.com/oschwald/maxminddb-golang" "github.com/oschwald/maxminddb-golang"
) )
type Record interface { // GeoRecord is the model for City database
LookUp(ip net.IP)
}
type GeoRecord struct { type GeoRecord struct {
Country struct { Country struct {
ISOCode string `maxminddb:"iso_code"` ISOCode string `maxminddb:"iso_code"`
@ -29,6 +26,7 @@ type GeoRecord struct {
} `maxminddb:"postal"` } `maxminddb:"postal"`
} }
// ASNRecord is the model for ASN database
type ASNRecord struct { type ASNRecord struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"` AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
@ -51,11 +49,13 @@ func openMMDB(path string) *maxminddb.Reader {
return db return db
} }
// Setup opens all Geolite2 databases
func Setup(cityPath string, asnPath string) { func Setup(cityPath string, asnPath string) {
db.city = openMMDB(cityPath) db.city = openMMDB(cityPath)
db.asn = openMMDB(asnPath) db.asn = openMMDB(asnPath)
} }
// CloseDBs unmaps from memory and frees resources to the filesystem
func CloseDBs() { func CloseDBs() {
log.Printf("Closing dbs...") log.Printf("Closing dbs...")
if err := db.city.Close(); err != nil { if err := db.city.Close(); err != nil {
@ -66,20 +66,12 @@ func CloseDBs() {
} }
} }
// LookUp an IP and get city data
func (record *GeoRecord) LookUp(ip net.IP) error { func (record *GeoRecord) LookUp(ip net.IP) error {
err := db.city.Lookup(ip, record) return db.city.Lookup(ip, record)
if err != nil {
return err
}
return nil
} }
// LookUp an IP and get ASN data
func (record *ASNRecord) LookUp(ip net.IP) error { func (record *ASNRecord) LookUp(ip net.IP) error {
err := db.asn.Lookup(ip, record) return db.asn.Lookup(ip, record)
if err != nil {
return err
}
return nil
} }

77
models/geo_test.go Normal file
View File

@ -0,0 +1,77 @@
package models
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestModels(t *testing.T) {
expectedCity := &GeoRecord{
Country: struct {
ISOCode string "maxminddb:\"iso_code\""
Names map[string]string "maxminddb:\"names\""
}{
ISOCode: "GB",
Names: map[string]string{
"de": "Vereinigtes Königreich",
"en": "United Kingdom",
"es": "Reino Unido",
"fr": "Royaume-Uni",
"ja": "イギリス",
"pt-BR": "Reino Unido",
"ru": "Великобритания",
"zh-CN": "英国",
},
},
City: struct {
Names map[string]string "maxminddb:\"names\""
}{
Names: map[string]string{
"de": "London",
"en": "London",
"es": "Londres",
"fr": "Londres",
"ja": "ロンドン",
"pt-BR": "Londres",
"ru": "Лондон",
},
},
Location: struct {
Latitude float64 "maxminddb:\"latitude\""
Longitude float64 "maxminddb:\"longitude\""
TimeZone string "maxminddb:\"time_zone\""
}{
Latitude: 51.5142,
Longitude: -0.0931,
TimeZone: "Europe/London",
},
Postal: struct {
Code string "maxminddb:\"code\""
}{
Code: "",
},
}
expectedASN := &ASNRecord{
AutonomousSystemNumber: 12552,
AutonomousSystemOrganization: "IP-Only",
}
Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
defer CloseDBs()
assert.NotNil(t, db.asn)
assert.NotNil(t, db.city)
cityRecord := &GeoRecord{}
assert.Nil(t, cityRecord.LookUp(net.ParseIP("81.2.69.192")))
assert.Equal(t, expectedCity, cityRecord)
assert.Error(t, cityRecord.LookUp(net.ParseIP("error")))
asnRecord := &ASNRecord{}
assert.Nil(t, asnRecord.LookUp(net.ParseIP("82.99.17.64")))
assert.Equal(t, expectedASN, asnRecord)
assert.Error(t, asnRecord.LookUp(net.ParseIP("error")))
}

View File

@ -4,8 +4,6 @@ import (
"net" "net"
"net/http" "net/http"
"path/filepath" "path/filepath"
"regexp"
"strings"
"github.com/dcarrillo/whatismyip/internal/httputils" "github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/setting"
@ -13,8 +11,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const userAgentPattern = `curl|wget|libwww-perl|python|ansible-httpget|HTTPie|WindowsPowerShell|http_request|Go-http-client|^$` // JSONResponse maps data as json
type JSONResponse struct { type JSONResponse struct {
IP string `json:"ip"` IP string `json:"ip"`
IPVersion byte `json:"ip_version"` IPVersion byte `json:"ip_version"`
@ -33,25 +30,45 @@ type JSONResponse struct {
} }
func getRoot(ctx *gin.Context) { func getRoot(ctx *gin.Context) {
reg := regexp.MustCompile(userAgentPattern) switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) {
if reg.Match([]byte(ctx.Request.UserAgent())) { case gin.MIMEHTML:
ctx.String(http.StatusOK, ctx.ClientIP())
} else {
name := "home" name := "home"
if setting.App.TemplatePath != "" { if setting.App.TemplatePath != "" {
name = filepath.Base(setting.App.TemplatePath) name = filepath.Base(setting.App.TemplatePath)
} }
ctx.HTML(http.StatusOK, name, jsonOutput(ctx)) ctx.HTML(http.StatusOK, name, jsonOutput(ctx))
case gin.MIMEJSON:
getJSON(ctx)
default:
ctx.String(http.StatusOK, ctx.ClientIP()+"\n")
} }
} }
func getClientPort(ctx *gin.Context) string {
var port string
if setting.App.TrustedPortHeader == "" {
if setting.App.TrustedHeader != "" {
port = "unknown"
} else {
_, port, _ = net.SplitHostPort(ctx.Request.RemoteAddr)
}
} else {
port = ctx.GetHeader(setting.App.TrustedPortHeader)
if port == "" {
port = "unknown"
}
}
return port
}
func getClientPortAsString(ctx *gin.Context) { func getClientPortAsString(ctx *gin.Context) {
ctx.String(http.StatusOK, strings.Split(ctx.Request.RemoteAddr, ":")[1]+"\n") ctx.String(http.StatusOK, getClientPort(ctx)+"\n")
} }
func getAllAsString(ctx *gin.Context) { func getAllAsString(ctx *gin.Context) {
output := "IP: " + ctx.ClientIP() + "\n" output := "IP: " + ctx.ClientIP() + "\n"
output += "Client Port: " + strings.Split(ctx.Request.RemoteAddr, ":")[1] + "\n" output += "Client Port: " + getClientPort(ctx) + "\n"
r := service.Geo{IP: net.ParseIP(ctx.ClientIP())} r := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
if record := r.LookUpCity(); record != nil { if record := r.LookUpCity(); record != nil {
@ -62,8 +79,8 @@ func getAllAsString(ctx *gin.Context) {
output += geoASNRecordToString(record) + "\n" output += geoASNRecordToString(record) + "\n"
} }
h := ctx.Request.Header h := httputils.GetHeadersWithoutTrustedHeaders(ctx)
h["Host"] = []string{ctx.Request.Host} h.Set("Host", ctx.Request.Host)
output += httputils.HeadersToSortedString(h) output += httputils.HeadersToSortedString(h)
ctx.String(http.StatusOK, output) ctx.String(http.StatusOK, output)
@ -85,7 +102,7 @@ func jsonOutput(ctx *gin.Context) JSONResponse {
return JSONResponse{ return JSONResponse{
IP: ctx.ClientIP(), IP: ctx.ClientIP(),
IPVersion: version, IPVersion: version,
ClientPort: strings.Split(ctx.Request.RemoteAddr, ":")[1], ClientPort: getClientPort(ctx),
Country: cityRecord.Country.Names["en"], Country: cityRecord.Country.Names["en"],
CountryCode: cityRecord.Country.ISOCode, CountryCode: cityRecord.Country.ISOCode,
City: cityRecord.City.Names["en"], City: cityRecord.City.Names["en"],
@ -96,6 +113,6 @@ func jsonOutput(ctx *gin.Context) JSONResponse {
ASN: asnRecord.AutonomousSystemNumber, ASN: asnRecord.AutonomousSystemNumber,
ASNOrganization: asnRecord.AutonomousSystemOrganization, ASNOrganization: asnRecord.AutonomousSystemOrganization,
Host: ctx.Request.Host, Host: ctx.Request.Host,
Headers: ctx.Request.Header, Headers: httputils.GetHeadersWithoutTrustedHeaders(ctx),
} }
} }

View File

@ -1,27 +1,89 @@
package router package router
import ( import (
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIP4RootFromCli(t *testing.T) { func TestRootContentType(t *testing.T) {
uas := []string{"", "curl", "wget", "libwww-perl", "python", "ansible-httpget", "HTTPie", "WindowsPowerShell", "http_request", "Go-http-client"} 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) w := httptest.NewRecorder()
req.Header.Set("X-Real-IP", testIP.ipv4) app.ServeHTTP(w, req)
for _, ua := range uas { assert.Equal(t, 200, w.Code)
req.Header.Set("User-Agent", ua) assert.Equal(t, tt.expected, w.Header().Get("Content-Type"))
})
}
}
w := httptest.NewRecorder() func TestGetIP(t *testing.T) {
app.ServeHTTP(w, req) 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) w := httptest.NewRecorder()
assert.Equal(t, testIP.ipv4, w.Body.String()) 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"))
})
} }
} }
@ -36,16 +98,76 @@ func TestHost(t *testing.T) {
} }
func TestClientPort(t *testing.T) { func TestClientPort(t *testing.T) {
req, _ := http.NewRequest("GET", "/client-port", nil) type args struct {
req.RemoteAddr = testIP.ipv4 + ":" + "1000" params []string
req.Header.Set("X-Real-IP", testIP.ipv4) 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{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
},
},
expected: "unknown\n",
},
{
name: "Trusted and port header set but not included in headers",
args: args{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
},
expected: "unknown\n",
},
{
name: "Trusted and port header set and included in headers",
args: args{
params: []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
headers: map[string][]string{
trustedHeader: {testIP.ipv4},
trustedPortHeader: {"1001"},
},
},
expected: "1001\n",
},
}
w := httptest.NewRecorder() for _, tt := range tests {
app.ServeHTTP(w, req) _, _ = setting.Setup(tt.args.params)
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/client-port", nil)
req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
req.Header = tt.args.headers
assert.Equal(t, 200, w.Code) w := httptest.NewRecorder()
assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) app.ServeHTTP(w, req)
assert.Equal(t, "1000\n", w.Body.String())
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, tt.expected, w.Body.String())
t.Log(w.Header())
})
}
} }
func TestNotFound(t *testing.T) { func TestNotFound(t *testing.T) {
@ -59,36 +181,59 @@ func TestNotFound(t *testing.T) {
} }
func TestJSON(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"]}}` _, _ = setting.Setup(
expectedIPv6 := `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"9000", "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":""}` []string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
)
req, _ := http.NewRequest("GET", "/json", nil) type args struct {
req.RemoteAddr = testIP.ipv4 + ":" + "1000" ip string
req.Host = "test" }
req.Header.Set("X-Real-IP", testIP.ipv4) 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() w := httptest.NewRecorder()
app.ServeHTTP(w, req) app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
assert.JSONEq(t, expectedIPv4, w.Body.String()) assert.JSONEq(t, tt.expected, w.Body.String())
})
req.RemoteAddr = 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())
} }
func TestAll(t *testing.T) { func TestAll(t *testing.T) {
expected := `IP: 81.2.69.192 expected := `IP: 81.2.69.192
Client Port: 1000 Client Port: 1001
City: London City: London
Country: United Kingdom Country: United Kingdom
Country Code: GB Country Code: GB
@ -102,13 +247,21 @@ ASN Organization:
Header1: one Header1: one
Host: test Host: test
X-Real-Ip: 81.2.69.192
` `
_, _ = setting.Setup(
[]string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
},
)
req, _ := http.NewRequest("GET", "/all", nil) req, _ := http.NewRequest("GET", "/all", nil)
req.RemoteAddr = testIP.ipv4 + ":" + "1000" req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000")
req.Host = "test" 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") req.Header.Set("Header1", "one")
w := httptest.NewRecorder() w := httptest.NewRecorder()

View File

@ -10,15 +10,17 @@ import (
) )
func getHeadersAsSortedString(ctx *gin.Context) { func getHeadersAsSortedString(ctx *gin.Context) {
h := ctx.Request.Header h := httputils.GetHeadersWithoutTrustedHeaders(ctx)
h["Host"] = []string{ctx.Request.Host} h.Set("Host", ctx.Request.Host)
ctx.String(http.StatusOK, httputils.HeadersToSortedString(h)) ctx.String(http.StatusOK, httputils.HeadersToSortedString(h))
} }
func getHeaderAsString(ctx *gin.Context) { func getHeaderAsString(ctx *gin.Context) {
headers := httputils.GetHeadersWithoutTrustedHeaders(ctx)
h := ctx.Params.ByName("header") 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)) ctx.String(http.StatusOK, template.HTMLEscapeString(v))
} else if strings.ToLower(h) == "host" { } else if strings.ToLower(h) == "host" {
ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host)) ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host))

View File

@ -5,6 +5,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -26,11 +27,20 @@ Header2: value22
Header3: value3 Header3: value3
Host: Host:
` `
_, _ = setting.Setup([]string{
"-geoip2-city", "city",
"-geoip2-asn", "asn",
"-trusted-header", trustedHeader,
"-trusted-port-header", trustedPortHeader,
})
req, _ := http.NewRequest("GET", "/headers", nil) req, _ := http.NewRequest("GET", "/headers", nil)
req.Header["Header1"] = []string{"value1"} req.Header = map[string][]string{
req.Header["Header2"] = []string{"value21", "value22"} "Header1": {"value1"},
req.Header["Header3"] = []string{"value3"} "Header2": {"value21", "value22"},
"Header3": {"value3"},
}
req.Header.Set(trustedHeader, "1.1.1.1")
req.Header.Set(trustedPortHeader, "1025")
w := httptest.NewRecorder() w := httptest.NewRecorder()
app.ServeHTTP(w, req) app.ServeHTTP(w, req)

View File

@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetupTemplate reads and parses a template from file
func SetupTemplate(r *gin.Engine) { func SetupTemplate(r *gin.Engine) {
if setting.App.TemplatePath == "" { if setting.App.TemplatePath == "" {
t, _ := template.New("home").Parse(home) t, _ := template.New("home").Parse(home)
@ -18,6 +19,7 @@ func SetupTemplate(r *gin.Engine) {
} }
} }
// Setup defines the endpoints
func Setup(r *gin.Engine) { func Setup(r *gin.Engine) {
r.GET("/", getRoot) r.GET("/", getRoot)
r.GET("/client-port", getClientPortAsString) r.GET("/client-port", getClientPortAsString)

View File

@ -16,6 +16,7 @@ type testIPs struct {
} }
type contentTypes struct { type contentTypes struct {
html string
text string text string
json string json string
} }
@ -29,12 +30,16 @@ var (
ipv6ASN: "2a02:a800::1", ipv6ASN: "2a02:a800::1",
} }
contentType = contentTypes{ contentType = contentTypes{
html: "content-type: text/html; charset=utf-8",
text: "text/plain; charset=utf-8", text: "text/plain; charset=utf-8",
json: "application/json; 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,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test", "headers": {}}`
jsonIPv6 = `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1001", "country":"", "country_code":"", "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":"", "headers": {}}`
) )
const trustedHeader = "X-Real-IP" const trustedHeader = "X-Real-IP"
const trustedPortHeader = "X-Real-Port"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
app = gin.Default() app = gin.Default()

View File

@ -54,10 +54,11 @@ const expectedHome = `
func TestDefaultTemplate(t *testing.T) { func TestDefaultTemplate(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil) req, _ := http.NewRequest("GET", "/", nil)
req.Header = map[string][]string{
req.Header["Header1"] = []string{"value1"} "Header1": {"value1"},
req.Header["Header2"] = []string{"value21", "value22"} "Header2": {"value21", "value22"},
req.Header["Header3"] = []string{"value3"} "Header3": {"value3"},
}
tmpl, _ := template.New("home").Parse(home) tmpl, _ := template.New("home").Parse(home)
response := JSONResponse{ response := JSONResponse{

54
server/quic_server.go Normal file
View File

@ -0,0 +1,54 @@
package server
import (
"context"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/quic-go/quic-go/http3"
)
type QuicServer struct {
server *http3.Server
tlsServer *TLSServer
ctx context.Context
}
func NewQuicServer(ctx context.Context, tlsServer *TLSServer) *QuicServer {
return &QuicServer{
tlsServer: tlsServer,
ctx: ctx,
}
}
func (q *QuicServer) Start() {
q.server = &http3.Server{
Addr: setting.App.TLSAddress,
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.Fatal(err)
}
parentHandler.ServeHTTP(rw, req)
})
log.Printf("Starting QUIC server listening on %s (udp)", setting.App.TLSAddress)
go func() {
if err := q.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
err.Error() != "quic: Server closed" {
log.Fatal(err)
}
}()
}
func (q *QuicServer) Stop() {
log.Printf("Stopping QUIC server...")
if err := q.server.Close(); err != nil {
log.Printf("QUIC server forced to shutdown")
}
}

96
server/server.go Normal file
View File

@ -0,0 +1,96 @@
package server
import (
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/models"
"golang.org/x/net/context"
)
type Server interface {
Start()
Stop()
}
type Factory struct {
tcpServer *TCPServer
tlsServer *TLSServer
quicServer *QuicServer
}
func Setup(ctx context.Context, handler http.Handler) *Factory {
var tcpServer *TCPServer
var tlsServer *TLSServer
var quicServer *QuicServer
if setting.App.BindAddress != "" {
tcpServer = NewTCPServer(ctx, &handler)
}
if setting.App.TLSAddress != "" {
tlsServer = NewTLSServer(ctx, &handler)
if setting.App.EnableHTTP3 {
quicServer = NewQuicServer(ctx, tlsServer)
}
}
return &Factory{
tcpServer: tcpServer,
tlsServer: tlsServer,
quicServer: quicServer,
}
}
func (f *Factory) Run() {
f.start()
signalChan := make(chan os.Signal, 3)
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
var s os.Signal
for {
s = <-signalChan
if s == syscall.SIGHUP {
f.stop()
models.CloseDBs()
models.Setup(setting.App.GeodbPath.City, setting.App.GeodbPath.ASN)
f.start()
} else {
log.Printf("Shutting down...")
f.stop()
models.CloseDBs()
break
}
}
}
func (f *Factory) start() {
if f.tcpServer != nil {
f.tcpServer.Start()
}
if f.tlsServer != nil {
f.tlsServer.Start()
if f.quicServer != nil {
f.quicServer.Start()
}
}
}
func (f *Factory) stop() {
if f.tcpServer != nil {
f.tcpServer.Stop()
}
if f.tlsServer != nil {
if f.quicServer != nil {
f.quicServer.Stop()
}
f.tlsServer.Stop()
}
}

46
server/tcp_server.go Normal file
View File

@ -0,0 +1,46 @@
package server
import (
"context"
"errors"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TCPServer struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTCPServer(ctx context.Context, handler *http.Handler) *TCPServer {
return &TCPServer{
handler: handler,
ctx: ctx,
}
}
func (t *TCPServer) Start() {
t.server = &http.Server{
Addr: setting.App.BindAddress,
Handler: *t.handler,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
log.Printf("Starting TCP server listening on %s", setting.App.BindAddress)
go func() {
if err := t.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
func (t *TCPServer) Stop() {
log.Printf("Stopping TCP server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TCP server forced to shutdown: %s", err)
}
}

47
server/tls_server.go Normal file
View File

@ -0,0 +1,47 @@
package server
import (
"context"
"errors"
"log"
"net/http"
"github.com/dcarrillo/whatismyip/internal/setting"
)
type TLSServer struct {
server *http.Server
handler *http.Handler
ctx context.Context
}
func NewTLSServer(ctx context.Context, handler *http.Handler) *TLSServer {
return &TLSServer{
handler: handler,
ctx: ctx,
}
}
func (t *TLSServer) Start() {
t.server = &http.Server{
Addr: setting.App.TLSAddress,
Handler: *t.handler,
ReadTimeout: setting.App.Server.ReadTimeout,
WriteTimeout: setting.App.Server.WriteTimeout,
}
log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress)
go func() {
if err := t.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
func (t *TLSServer) Stop() {
log.Printf("Stopping TLS server...")
if err := t.server.Shutdown(t.ctx); err != nil {
log.Printf("TLS server forced to shutdown: %s", err)
}
}

View File

@ -7,10 +7,12 @@ import (
"github.com/dcarrillo/whatismyip/models" "github.com/dcarrillo/whatismyip/models"
) )
// Geo defines a base type for lookups
type Geo struct { type Geo struct {
IP net.IP IP net.IP
} }
// LookUpCity queries the database for city data related to the given IP
func (g *Geo) LookUpCity() *models.GeoRecord { func (g *Geo) LookUpCity() *models.GeoRecord {
record := &models.GeoRecord{} record := &models.GeoRecord{}
err := record.LookUp(g.IP) err := record.LookUp(g.IP)
@ -22,6 +24,7 @@ func (g *Geo) LookUpCity() *models.GeoRecord {
return record return record
} }
// LookUpASN queries the database for ASN data related to the given IP
func (g *Geo) LookUpASN() *models.ASNRecord { func (g *Geo) LookUpASN() *models.ASNRecord {
record := &models.ASNRecord{} record := &models.ASNRecord{}
err := record.LookUp(g.IP) err := record.LookUp(g.IP)