mirror of
https://github.com/casjaysdevdocker/ifconfig
synced 2025-01-18 00:34:19 -05:00
🗃️ Committing everything that changed 🗃️
This commit is contained in:
parent
40ba713617
commit
13c8d27e11
@ -1,5 +1,9 @@
|
||||
Dockerfile
|
||||
Dockerfile.geoip.gitignore
|
||||
# Files to ignore
|
||||
.gitkeep
|
||||
.gitignore
|
||||
node_modules/**
|
||||
.node_modules/**
|
||||
**/.gitkeep
|
||||
**/.gitignore
|
||||
**/node_modules/**
|
||||
**/.node_modules/**
|
||||
|
130
Dockerfile
130
Dockerfile
@ -1,38 +1,110 @@
|
||||
# Build
|
||||
FROM golang:1.15-buster AS build
|
||||
WORKDIR /go/src/github.com/mpolden/echoip
|
||||
COPY . .
|
||||
FROM golang:1.15-buster AS src
|
||||
|
||||
# Must build without cgo because libc is unavailable in runtime image
|
||||
RUN apt update && apt install -yy git && \
|
||||
git clone -q https://github.com/mpolden/echoip /go/src/github.com/mpolden/echoip && \
|
||||
cd /go/src/github.com/mpolden/echoip
|
||||
|
||||
WORKDIR /go/src/github.com/mpolden/echoip
|
||||
ENV GO111MODULE=on CGO_ENABLED=0
|
||||
RUN make
|
||||
|
||||
# Run
|
||||
FROM casjaysdevdocker/alpine:latest as run
|
||||
COPY --from=build /go/bin/echoip /opt/echoip/
|
||||
COPY ./html /opt/echoip/html
|
||||
COPY ./bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
FROM casjaysdevdocker/alpine:latest AS build
|
||||
|
||||
FROM run
|
||||
ARG BUILD_DATE="$(date +'%Y-%m-%d %H:%M')"
|
||||
ARG ALPINE_VERSION="v3.16"
|
||||
|
||||
LABEL \
|
||||
org.label-schema.name="ifconfig" \
|
||||
org.label-schema.description="Sow ip information" \
|
||||
org.label-schema.url="https://hub.docker.com/r/casjaysdevdocker/ifconfig" \
|
||||
org.label-schema.vcs-url="https://github.com/casjaysdevdocker/ifconfig" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.version=$BUILD_DATE \
|
||||
org.label-schema.vcs-ref=$BUILD_DATE \
|
||||
org.label-schema.license="WTFPL" \
|
||||
org.label-schema.vcs-type="Git" \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.vendor="CasjaysDev" \
|
||||
maintainer="CasjaysDev <docker-admin@casjaysdev.com>"
|
||||
ARG DEFAULT_DATA_DIR="/usr/local/share/template-files/data" \
|
||||
DEFAULT_CONF_DIR="/usr/local/share/template-files/config" \
|
||||
DEFAULT_TEMPLATE_DIR="/usr/local/share/template-files/defaults"
|
||||
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/echoip
|
||||
VOLUME /opt/echoip/html
|
||||
ARG PACK_LIST="bash"
|
||||
|
||||
HEALTHCHECK --start-period=1m --interval=10m --timeout=3s CMD ["/usr/local/bin/docker-entrypoint.sh", "healthcheck"]
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
ENV=ENV=~/.bashrc \
|
||||
TZ="America/New_York" \
|
||||
SHELL="/bin/sh" \
|
||||
TERM="xterm-256color" \
|
||||
TIMEZONE="${TZ:-$TIMEZONE}" \
|
||||
HOSTNAME="casjaysdev-ifconfig"
|
||||
|
||||
COPY ./rootfs/. /
|
||||
COPY --from=src /go/bin/echoip /opt/echoip/
|
||||
|
||||
RUN set -ex; \
|
||||
rm -Rf "/etc/apk/repositories"; \
|
||||
mkdir -p "${DEFAULT_DATA_DIR}" "${DEFAULT_CONF_DIR}" "${DEFAULT_TEMPLATE_DIR}"; \
|
||||
echo "http://dl-cdn.alpinelinux.org/alpine/${ALPINE_VERSION}/main" >>"/etc/apk/repositories"; \
|
||||
echo "http://dl-cdn.alpinelinux.org/alpine/${ALPINE_VERSION}/community" >>"/etc/apk/repositories"; \
|
||||
if [ "${ALPINE_VERSION}" = "edge" ]; then echo "http://dl-cdn.alpinelinux.org/alpine/${ALPINE_VERSION}/testing" >>"/etc/apk/repositories" ; fi ; \
|
||||
apk update --update-cache && apk add --no-cache ${PACK_LIST}
|
||||
|
||||
RUN cp -Rf /usr/local/share/template-files/data/. /opt/echoip/ && \
|
||||
ln -sf /opt/echoip/echoip /usr/local/bin/echoip
|
||||
|
||||
RUN echo 'Running cleanup' ; \
|
||||
rm -Rf /usr/share/doc/* /usr/share/info/* /tmp/* /var/tmp/* ; \
|
||||
rm -Rf /usr/local/bin/.gitkeep /usr/local/bin/.gitkeep /config /data /var/cache/apk/* ; \
|
||||
rm -rf /lib/systemd/system/multi-user.target.wants/* ; \
|
||||
rm -rf /etc/systemd/system/*.wants/* ; \
|
||||
rm -rf /lib/systemd/system/local-fs.target.wants/* ; \
|
||||
rm -rf /lib/systemd/system/sockets.target.wants/*udev* ; \
|
||||
rm -rf /lib/systemd/system/sockets.target.wants/*initctl* ; \
|
||||
rm -rf /lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* ; \
|
||||
rm -rf /lib/systemd/system/systemd-update-utmp* ; \
|
||||
if [ -d "/lib/systemd/system/sysinit.target.wants" ]; then cd "/lib/systemd/system/sysinit.target.wants" && rm $(ls | grep -v systemd-tmpfiles-setup) ; fi
|
||||
|
||||
FROM scratch
|
||||
|
||||
ARG \
|
||||
SERVICE_PORT="8080" \
|
||||
EXPOSE_PORTS="8080" \
|
||||
PHP_SERVER="ifconfig" \
|
||||
NODE_VERSION="system" \
|
||||
NODE_MANAGER="system" \
|
||||
BUILD_VERSION="latest" \
|
||||
LICENSE="MIT" \
|
||||
IMAGE_NAME="ifconfig" \
|
||||
BUILD_DATE="Mon Nov 14 12:26:35 PM EST 2022" \
|
||||
TIMEZONE="America/New_York"
|
||||
|
||||
LABEL maintainer="CasjaysDev <docker-admin@casjaysdev.com>" \
|
||||
org.opencontainers.image.vendor="CasjaysDev" \
|
||||
org.opencontainers.image.authors="CasjaysDev" \
|
||||
org.opencontainers.image.vcs-type="Git" \
|
||||
org.opencontainers.image.name="${IMAGE_NAME}" \
|
||||
org.opencontainers.image.base.name="${IMAGE_NAME}" \
|
||||
org.opencontainers.image.license="${LICENSE}" \
|
||||
org.opencontainers.image.vcs-ref="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.build-date="${BUILD_DATE}" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.schema-version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.url="https://hub.docker.com/r/casjaysdevdocker/${IMAGE_NAME}" \
|
||||
org.opencontainers.image.vcs-url="https://github.com/casjaysdevdocker/${IMAGE_NAME}" \
|
||||
org.opencontainers.image.url.source="https://github.com/casjaysdevdocker/${IMAGE_NAME}" \
|
||||
org.opencontainers.image.documentation="https://hub.docker.com/r/casjaysdevdocker/${IMAGE_NAME}" \
|
||||
org.opencontainers.image.description="Containerized version of ${IMAGE_NAME}" \
|
||||
com.github.containers.toolbox="false"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
ENV=~/.bashrc \
|
||||
SHELL="/bin/bash" \
|
||||
PORT="${SERVICE_PORT}" \
|
||||
TERM="xterm-256color" \
|
||||
PHP_SERVER="${PHP_SERVER}" \
|
||||
CONTAINER_NAME="${IMAGE_NAME}" \
|
||||
TZ="${TZ:-America/New_York}" \
|
||||
TIMEZONE="${TZ:-$TIMEZONE}" \
|
||||
HOSTNAME="casjaysdev-${IMAGE_NAME}"
|
||||
|
||||
COPY --from=build /. /
|
||||
|
||||
USER root
|
||||
WORKDIR /root
|
||||
|
||||
VOLUME [ "/config","/data" ]
|
||||
|
||||
EXPOSE $EXPOSE_PORTS
|
||||
|
||||
#CMD [ "" ]
|
||||
ENTRYPOINT [ "tini", "-p", "SIGTERM", "--", "/usr/local/bin/entrypoint.sh" ]
|
||||
HEALTHCHECK --start-period=1m --interval=2m --timeout=3s CMD [ "/usr/local/bin/entrypoint.sh", "healthcheck" ]
|
||||
|
25
LICENSE
25
LICENSE
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2012-2020, Martin Polden
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
13
LICENSE.md
Normal file
13
LICENSE.md
Normal file
@ -0,0 +1,13 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2022 casjay <git-admin@casjaysdev.com>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
1. You just DO WHAT THE FUCK YOU WANT TO.
|
77
Makefile
77
Makefile
@ -1,77 +0,0 @@
|
||||
DOCKER ?= docker
|
||||
DOCKER_IMAGE ?= mpolden/echoip
|
||||
OS := $(shell uname)
|
||||
ifeq ($(OS),Linux)
|
||||
TAR_OPTS := --wildcards
|
||||
endif
|
||||
XGOARCH := $(shell uname -m)
|
||||
XGOOS := linux
|
||||
XBIN := $(XGOOS)_$(XGOARCH)/echoip
|
||||
|
||||
all: lint test install
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
check-fmt:
|
||||
bash -c "diff --line-format='%L' <(echo -n) <(gofmt -d -s .)"
|
||||
|
||||
lint: check-fmt vet
|
||||
|
||||
install:
|
||||
go install ./...
|
||||
|
||||
databases := GeoLite2-City GeoLite2-Country GeoLite2-ASN
|
||||
|
||||
$(databases):
|
||||
ifndef GEOIP_LICENSE_KEY
|
||||
$(error GEOIP_LICENSE_KEY must be set. Please see https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/)
|
||||
endif
|
||||
mkdir -p data
|
||||
@curl -fsSL -m 30 "https://download.maxmind.com/app/geoip_download?edition_id=$@&license_key=$(GEOIP_LICENSE_KEY)&suffix=tar.gz" | tar $(TAR_OPTS) --strip-components=1 -C $(CURDIR)/data -xzf - '*.mmdb'
|
||||
test ! -f data/GeoLite2-City.mmdb || mv data/GeoLite2-City.mmdb data/city.mmdb
|
||||
test ! -f data/GeoLite2-Country.mmdb || mv data/GeoLite2-Country.mmdb data/country.mmdb
|
||||
test ! -f data/GeoLite2-ASN.mmdb || mv data/GeoLite2-ASN.mmdb data/asn.mmdb
|
||||
|
||||
geoip-download: $(databases)
|
||||
|
||||
# Create an environment to build multiarch containers (https://github.com/docker/buildx/)
|
||||
docker-multiarch-builder:
|
||||
DOCKER_BUILDKIT=1 $(DOCKER) build -o . git://github.com/docker/buildx
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
mv buildx ~/.docker/cli-plugins/docker-buildx
|
||||
$(DOCKER) buildx create --name multiarch-builder --node multiarch-builder --driver docker-container --use
|
||||
$(DOCKER) run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
docker-build:
|
||||
$(DOCKER) build -t $(DOCKER_IMAGE) .
|
||||
|
||||
docker-login:
|
||||
@echo "$(DOCKER_PASSWORD)" | $(DOCKER) login -u "$(DOCKER_USERNAME)" --password-stdin
|
||||
|
||||
docker-test:
|
||||
$(eval CONTAINER=$(shell $(DOCKER) run --rm --detach --publish-all $(DOCKER_IMAGE)))
|
||||
$(eval DOCKER_PORT=$(shell $(DOCKER) port $(CONTAINER) | cut -d ":" -f 2))
|
||||
curl -fsS -m 5 localhost:$(DOCKER_PORT) > /dev/null; $(DOCKER) stop $(CONTAINER)
|
||||
|
||||
docker-push: docker-test docker-login
|
||||
$(DOCKER) push $(DOCKER_IMAGE)
|
||||
|
||||
docker-pushx: docker-multiarch-builder docker-test docker-login
|
||||
$(DOCKER) buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t $(DOCKER_IMAGE) --push .
|
||||
|
||||
xinstall:
|
||||
env GOOS=$(XGOOS) GOARCH=$(XGOARCH) go install ./...
|
||||
|
||||
publish:
|
||||
ifndef DEST_PATH
|
||||
$(error DEST_PATH must be set when publishing)
|
||||
endif
|
||||
rsync -a $(GOPATH)/bin/$(XBIN) $(DEST_PATH)/$(XBIN)
|
||||
@sha256sum $(GOPATH)/bin/$(XBIN)
|
||||
|
||||
run:
|
||||
go run cmd/echoip/main.go -a data/asn.mmdb -c data/city.mmdb -f data/country.mmdb -H x-forwarded-for -r -s -p
|
131
README.md
131
README.md
@ -1,131 +1,36 @@
|
||||
# echoip
|
||||
## 👋 Welcome to ifconfig 🚀
|
||||
|
||||
![Build Status](https://github.com/mpolden/echoip/workflows/ci/badge.svg)
|
||||
|
||||
A simple service for looking up your IP address. This is the code that powers
|
||||
<https://ifconfig.co>.
|
||||
|
||||
## Usage
|
||||
|
||||
Just the business, please:
|
||||
Description
|
||||
|
||||
|
||||
## Install my system scripts
|
||||
|
||||
```shell
|
||||
$ curl ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ http ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ wget -qO- ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ fetch -qo- https://ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ bat -print=b ifconfig.co/ip
|
||||
127.0.0.1
|
||||
sudo bash -c "$(curl -q -LSsf "https://github.com/systemmgr/installer/raw/main/install.sh")"
|
||||
sudo systemmgr --config && sudo systemmgr install scripts
|
||||
```
|
||||
|
||||
Country and city lookup:
|
||||
## Get source files
|
||||
|
||||
```shell
|
||||
$ curl ifconfig.co/country
|
||||
Elbonia
|
||||
|
||||
$ curl ifconfig.co/country-iso
|
||||
EB
|
||||
|
||||
$ curl ifconfig.co/city
|
||||
Bornyasherk
|
||||
|
||||
$ curl ifconfig.co/asn
|
||||
AS59795
|
||||
dockermgr download src ifconfig
|
||||
```
|
||||
|
||||
As JSON:
|
||||
OR
|
||||
|
||||
```shell
|
||||
$ curl -H 'Accept: application/json' ifconfig.co # or curl ifconfig.co/json
|
||||
{
|
||||
"city": "Bornyasherk",
|
||||
"country": "Elbonia",
|
||||
"country_iso": "EB",
|
||||
"ip": "127.0.0.1",
|
||||
"ip_decimal": 2130706433,
|
||||
"asn": "AS59795",
|
||||
"asn_org": "Hosting4Real"
|
||||
}
|
||||
git clone "https://github.com/casjaysdevdocker/ifconfig" "$HOME/Projects/github/casjaysdevdocker/ifconfig"
|
||||
```
|
||||
|
||||
Port testing:
|
||||
## Build container
|
||||
|
||||
```shell
|
||||
$ curl ifconfig.co/port/80
|
||||
{
|
||||
"ip": "127.0.0.1",
|
||||
"port": 80,
|
||||
"reachable": false
|
||||
}
|
||||
cd "$HOME/Projects/github/casjaysdevdocker/ifconfig"
|
||||
buildx
|
||||
```
|
||||
|
||||
Pass the appropriate flag (usually `-4` and `-6`) to your client to switch
|
||||
between IPv4 and IPv6 lookup.
|
||||
## Authors
|
||||
|
||||
## Features
|
||||
|
||||
* Easy to remember domain name
|
||||
* Fast
|
||||
* Supports IPv6
|
||||
* Supports HTTPS
|
||||
* Supports common command-line clients (e.g. `curl`, `httpie`, `ht`, `wget` and `fetch`)
|
||||
* JSON output
|
||||
* ASN, country and city lookup using the MaxMind GeoIP database
|
||||
* Port testing
|
||||
* All endpoints (except `/port`) can return information about a custom IP address specified via `?ip=` query parameter
|
||||
* Open source under the [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause)
|
||||
|
||||
## Why?
|
||||
|
||||
* To scratch an itch
|
||||
* An excuse to use Go for something
|
||||
* Faster than ifconfig.me and has IPv6 support
|
||||
|
||||
## Building
|
||||
|
||||
Compiling requires the [Golang compiler](https://golang.org/) to be installed.
|
||||
This package can be installed with:
|
||||
|
||||
`go install github.com/mpolden/echoip/...@latest`
|
||||
|
||||
For more information on building a Go project, see the [official Go
|
||||
documentation](https://golang.org/doc/code.html).
|
||||
|
||||
## Docker image
|
||||
|
||||
A Docker image is available on [Docker
|
||||
Hub](https://hub.docker.com/r/casjay/ifconfig), which can be downloaded with:
|
||||
|
||||
`docker pull casjay/ifconfig`
|
||||
|
||||
### Server Options
|
||||
|
||||
```shell
|
||||
$ echoip -h
|
||||
Usage of echoip:
|
||||
-C int
|
||||
Size of response cache. Set to 0 to disable
|
||||
-H value
|
||||
Header to trust for remote IP, if present (e.g. X-Real-IP)
|
||||
-a string
|
||||
Path to GeoIP ASN database
|
||||
-c string
|
||||
Path to GeoIP city database
|
||||
-f string
|
||||
Path to GeoIP country database
|
||||
-l string
|
||||
Listening address (default ":8080")
|
||||
-p Enable port lookup
|
||||
-r Perform reverse hostname lookups
|
||||
-t string
|
||||
Path to template directory (default "html")
|
||||
```
|
||||
🤖 casjay: [Github](https://github.com/casjay) [Docker](https://hub.docker.com/r/casjay) 🤖
|
||||
📽 dockermgr: [Github](https://github.com/dockermgr) [Docker](https://hub.docker.com/r/dockermgr) 📽
|
||||
⛵ CasjaysDev Docker: [Github](https://github.com/casjaysdevdocker) [Docker](https://hub.docker.com/r/casjaysdevdocker) ⛵
|
||||
|
@ -1,36 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Set bash options
|
||||
[ -n "$DEBUG" ] && set -x
|
||||
set -o pipefail
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
GEOIP="-a /data/GeoLite2-ASN.mmdb -c /data/GeoLite2-City.mmdb -f /data/GeoLite2-Country.mmdb"
|
||||
OPTS="-H x-forwarded-for -r -s -p"
|
||||
CONFIG="-t /config/web"
|
||||
|
||||
export TZ="${TZ:-America/New_York}"
|
||||
export HOSTNAME="${HOSTNAME:-casjaysdev-ifconfig}"
|
||||
|
||||
[ -n "${TZ}" ] && echo "${TZ}" >/etc/timezone
|
||||
[ -n "${HOSTNAME}" ] && echo "${HOSTNAME}" >/etc/hostname
|
||||
[ -n "${HOSTNAME}" ] && echo "127.0.0.1 $HOSTNAME localhost" >/etc/hosts
|
||||
[ -f "/usr/share/zoneinfo/${TZ}" ] && ln -sf "/usr/share/zoneinfo/${TZ}" "/etc/localtime"
|
||||
|
||||
[ -f "/config/env" ] && . /config/env
|
||||
|
||||
case $1 in
|
||||
bash | sh | shell)
|
||||
exec /bin/bash -l
|
||||
;;
|
||||
healthcheck)
|
||||
curl -q -LSsf -I --fail http://localhost:8080/json || exit 10
|
||||
;;
|
||||
*)
|
||||
if [ -f /opt/echoip/echoip ]; then
|
||||
echo "starting echoip"
|
||||
/opt/echoip/echoip $GEOIP $OPTS $CONFIG
|
||||
else
|
||||
echo "echoip not found"
|
||||
exit 10
|
||||
fi
|
||||
;;
|
||||
esac
|
@ -1,93 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/mpolden/echoip/http"
|
||||
"github.com/mpolden/echoip/iputil"
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
)
|
||||
|
||||
type multiValueFlag []string
|
||||
|
||||
func (f *multiValueFlag) String() string {
|
||||
vs := ""
|
||||
for i, v := range *f {
|
||||
vs += v
|
||||
if i < len(*f)-1 {
|
||||
vs += ", "
|
||||
}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
func (f *multiValueFlag) Set(v string) error {
|
||||
*f = append(*f, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.SetPrefix("echoip: ")
|
||||
log.SetFlags(log.Lshortfile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
countryFile := flag.String("f", "", "Path to GeoIP country database")
|
||||
cityFile := flag.String("c", "", "Path to GeoIP city database")
|
||||
asnFile := flag.String("a", "", "Path to GeoIP ASN database")
|
||||
listen := flag.String("l", ":8080", "Listening address")
|
||||
reverseLookup := flag.Bool("r", false, "Perform reverse hostname lookups")
|
||||
portLookup := flag.Bool("p", false, "Enable port lookup")
|
||||
template := flag.String("t", "html", "Path to template dir")
|
||||
cacheSize := flag.Int("C", 0, "Size of response cache. Set to 0 to disable")
|
||||
profile := flag.Bool("P", false, "Enables profiling handlers")
|
||||
sponsor := flag.Bool("s", false, "Show sponsor logo")
|
||||
var headers multiValueFlag
|
||||
flag.Var(&headers, "H", "Header to trust for remote IP, if present (e.g. X-Real-IP)")
|
||||
flag.Parse()
|
||||
if len(flag.Args()) != 0 {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
r, err := geo.Open(*countryFile, *cityFile, *asnFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cache := http.NewCache(*cacheSize)
|
||||
server := http.New(r, cache, *profile)
|
||||
server.IPHeaders = headers
|
||||
if _, err := os.Stat(*template); err == nil {
|
||||
server.Template = *template
|
||||
} else {
|
||||
log.Printf("Not configuring default handler: Template not found: %s", *template)
|
||||
}
|
||||
if *reverseLookup {
|
||||
log.Println("Enabling reverse lookup")
|
||||
server.LookupAddr = iputil.LookupAddr
|
||||
}
|
||||
if *portLookup {
|
||||
log.Println("Enabling port lookup")
|
||||
server.LookupPort = iputil.LookupPort
|
||||
}
|
||||
if *sponsor {
|
||||
log.Println("Enabling sponsor logo")
|
||||
server.Sponsor = *sponsor
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
log.Printf("Trusting remote IP from header(s): %s", headers.String())
|
||||
}
|
||||
if *cacheSize > 0 {
|
||||
log.Printf("Cache capacity set to %d", *cacheSize)
|
||||
}
|
||||
if *profile {
|
||||
log.Printf("Enabling profiling handlers")
|
||||
}
|
||||
log.Printf("Listening on http://%s", *listen)
|
||||
if err := server.ListenAndServe(*listen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
8
go.mod
8
go.mod
@ -1,8 +0,0 @@
|
||||
module github.com/mpolden/echoip
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/oschwald/geoip2-golang v1.5.0
|
||||
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 // indirect
|
||||
)
|
20
go.sum
20
go.sum
@ -1,20 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw=
|
||||
github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
|
||||
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
|
||||
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 h1:fDE3p0qf2V1co1vfj3/o87Ps8Hq6QTGNxJ5Xe7xSp80=
|
||||
golang.org/x/sys v0.0.0-20210223212115-eede4237b368/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,99 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
capacity int
|
||||
mu sync.RWMutex
|
||||
entries map[uint64]*list.Element
|
||||
values *list.List
|
||||
evictions uint64
|
||||
}
|
||||
|
||||
type CacheStats struct {
|
||||
Capacity int
|
||||
Size int
|
||||
Evictions uint64
|
||||
}
|
||||
|
||||
func NewCache(capacity int) *Cache {
|
||||
if capacity < 0 {
|
||||
capacity = 0
|
||||
}
|
||||
return &Cache{
|
||||
capacity: capacity,
|
||||
entries: make(map[uint64]*list.Element),
|
||||
values: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func key(ip net.IP) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write(ip)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func (c *Cache) Set(ip net.IP, resp Response) {
|
||||
if c.capacity == 0 {
|
||||
return
|
||||
}
|
||||
k := key(ip)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
minEvictions := len(c.entries) - c.capacity + 1
|
||||
if minEvictions > 0 { // At or above capacity. Shrink the cache
|
||||
evicted := 0
|
||||
for el := c.values.Front(); el != nil && evicted < minEvictions; {
|
||||
value := el.Value.(Response)
|
||||
delete(c.entries, key(value.IP))
|
||||
next := el.Next()
|
||||
c.values.Remove(el)
|
||||
el = next
|
||||
evicted++
|
||||
}
|
||||
c.evictions += uint64(evicted)
|
||||
}
|
||||
current, ok := c.entries[k]
|
||||
if ok {
|
||||
c.values.Remove(current)
|
||||
}
|
||||
c.entries[k] = c.values.PushBack(resp)
|
||||
}
|
||||
|
||||
func (c *Cache) Get(ip net.IP) (Response, bool) {
|
||||
k := key(ip)
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
r, ok := c.entries[k]
|
||||
if !ok {
|
||||
return Response{}, false
|
||||
}
|
||||
return r.Value.(Response), true
|
||||
}
|
||||
|
||||
func (c *Cache) Resize(capacity int) error {
|
||||
if capacity < 0 {
|
||||
return fmt.Errorf("invalid capacity: %d\n", capacity)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.capacity = capacity
|
||||
c.evictions = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Stats() CacheStats {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return CacheStats{
|
||||
Size: len(c.entries),
|
||||
Capacity: c.capacity,
|
||||
Evictions: c.evictions,
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCacheCapacity(t *testing.T) {
|
||||
var tests = []struct {
|
||||
addCount, capacity, size int
|
||||
evictions uint64
|
||||
}{
|
||||
{1, 0, 0, 0},
|
||||
{1, 2, 1, 0},
|
||||
{2, 2, 2, 0},
|
||||
{3, 2, 2, 1},
|
||||
{10, 5, 5, 5},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := NewCache(tt.capacity)
|
||||
var responses []Response
|
||||
for i := 0; i < tt.addCount; i++ {
|
||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||
r := Response{IP: ip}
|
||||
responses = append(responses, r)
|
||||
c.Set(ip, r)
|
||||
}
|
||||
if got := len(c.entries); got != tt.size {
|
||||
t.Errorf("#%d: len(entries) = %d, want %d", i, got, tt.size)
|
||||
}
|
||||
if got := c.evictions; got != tt.evictions {
|
||||
t.Errorf("#%d: evictions = %d, want %d", i, got, tt.evictions)
|
||||
}
|
||||
if tt.capacity > 0 && tt.addCount > tt.capacity && tt.capacity == tt.size {
|
||||
lastAdded := responses[tt.addCount-1]
|
||||
if _, ok := c.Get(lastAdded.IP); !ok {
|
||||
t.Errorf("#%d: Get(%s) = (_, %t), want (_, %t)", i, lastAdded.IP.String(), ok, !ok)
|
||||
}
|
||||
firstAdded := responses[0]
|
||||
if _, ok := c.Get(firstAdded.IP); ok {
|
||||
t.Errorf("#%d: Get(%s) = (_, %t), want (_, %t)", i, firstAdded.IP.String(), ok, !ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheDuplicate(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
ip := net.ParseIP("192.0.2.1")
|
||||
response := Response{IP: ip}
|
||||
c.Set(ip, response)
|
||||
c.Set(ip, response)
|
||||
want := 1
|
||||
if got := len(c.entries); got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
if got := c.values.Len(); got != want {
|
||||
t.Errorf("want %d values, got %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheResize(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
for i := 1; i <= 20; i++ {
|
||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||
r := Response{IP: ip}
|
||||
c.Set(ip, r)
|
||||
}
|
||||
if got, want := len(c.entries), 10; got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
if got, want := c.evictions, uint64(10); got != want {
|
||||
t.Errorf("want %d evictions, got %d", want, got)
|
||||
}
|
||||
if err := c.Resize(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := c.evictions, uint64(0); got != want {
|
||||
t.Errorf("want %d evictions, got %d", want, got)
|
||||
}
|
||||
r := Response{IP: net.ParseIP("192.0.2.42")}
|
||||
c.Set(r.IP, r)
|
||||
if got, want := len(c.entries), 5; got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
type appError struct {
|
||||
Error error
|
||||
Message string
|
||||
Code int
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func internalServerError(err error) *appError {
|
||||
return &appError{
|
||||
Error: err,
|
||||
Message: "Internal server error",
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
func notFound(err error) *appError {
|
||||
return &appError{Error: err, Code: http.StatusNotFound}
|
||||
}
|
||||
|
||||
func badRequest(err error) *appError {
|
||||
return &appError{Error: err, Code: http.StatusBadRequest}
|
||||
}
|
||||
|
||||
func (e *appError) AsJSON() *appError {
|
||||
e.ContentType = jsonMediaType
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *appError) WithMessage(message string) *appError {
|
||||
e.Message = message
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *appError) IsJSON() bool {
|
||||
return e.ContentType == jsonMediaType
|
||||
}
|
466
http/http.go
466
http/http.go
@ -1,466 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/mpolden/echoip/iputil"
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
"github.com/mpolden/echoip/useragent"
|
||||
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonMediaType = "application/json"
|
||||
textMediaType = "text/plain"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Template string
|
||||
IPHeaders []string
|
||||
LookupAddr func(net.IP) (string, error)
|
||||
LookupPort func(net.IP, uint64) error
|
||||
cache *Cache
|
||||
gr geo.Reader
|
||||
profile bool
|
||||
Sponsor bool
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
IP net.IP `json:"ip"`
|
||||
IPDecimal *big.Int `json:"ip_decimal"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryISO string `json:"country_iso,omitempty"`
|
||||
CountryEU *bool `json:"country_eu,omitempty"`
|
||||
RegionName string `json:"region_name,omitempty"`
|
||||
RegionCode string `json:"region_code,omitempty"`
|
||||
MetroCode uint `json:"metro_code,omitempty"`
|
||||
PostalCode string `json:"zip_code,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Latitude float64 `json:"latitude,omitempty"`
|
||||
Longitude float64 `json:"longitude,omitempty"`
|
||||
Timezone string `json:"time_zone,omitempty"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
ASNOrg string `json:"asn_org,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
|
||||
}
|
||||
|
||||
type PortResponse struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint64 `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(db geo.Reader, cache *Cache, profile bool) *Server {
|
||||
return &Server{cache: cache, gr: db, profile: profile}
|
||||
}
|
||||
|
||||
func ipFromForwardedForHeader(v string) string {
|
||||
sep := strings.Index(v, ",")
|
||||
if sep == -1 {
|
||||
return v
|
||||
}
|
||||
return v[:sep]
|
||||
}
|
||||
|
||||
// ipFromRequest detects the IP address for this transaction.
|
||||
//
|
||||
// * `headers` - the specific HTTP headers to trust
|
||||
// * `r` - the incoming HTTP request
|
||||
// * `customIP` - whether to allow the IP to be pulled from query parameters
|
||||
func ipFromRequest(headers []string, r *http.Request, customIP bool) (net.IP, error) {
|
||||
remoteIP := ""
|
||||
if customIP && r.URL != nil {
|
||||
if v, ok := r.URL.Query()["ip"]; ok {
|
||||
remoteIP = v[0]
|
||||
}
|
||||
}
|
||||
if remoteIP == "" {
|
||||
for _, header := range headers {
|
||||
remoteIP = r.Header.Get(header)
|
||||
if http.CanonicalHeaderKey(header) == "X-Forwarded-For" {
|
||||
remoteIP = ipFromForwardedForHeader(remoteIP)
|
||||
}
|
||||
if remoteIP != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if remoteIP == "" {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteIP = host
|
||||
}
|
||||
ip := net.ParseIP(remoteIP)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("could not parse IP: %s", remoteIP)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func userAgentFromRequest(r *http.Request) *useragent.UserAgent {
|
||||
var userAgent *useragent.UserAgent
|
||||
userAgentRaw := r.UserAgent()
|
||||
if userAgentRaw != "" {
|
||||
parsed := useragent.Parse(userAgentRaw)
|
||||
userAgent = &parsed
|
||||
}
|
||||
return userAgent
|
||||
}
|
||||
|
||||
func (s *Server) newResponse(r *http.Request) (Response, error) {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
response, ok := s.cache.Get(ip)
|
||||
if ok {
|
||||
// Do not cache user agent
|
||||
response.UserAgent = userAgentFromRequest(r)
|
||||
return response, nil
|
||||
}
|
||||
ipDecimal := iputil.ToDecimal(ip)
|
||||
country, _ := s.gr.Country(ip)
|
||||
city, _ := s.gr.City(ip)
|
||||
asn, _ := s.gr.ASN(ip)
|
||||
var hostname string
|
||||
if s.LookupAddr != nil {
|
||||
hostname, _ = s.LookupAddr(ip)
|
||||
}
|
||||
var autonomousSystemNumber string
|
||||
if asn.AutonomousSystemNumber > 0 {
|
||||
autonomousSystemNumber = fmt.Sprintf("AS%d", asn.AutonomousSystemNumber)
|
||||
}
|
||||
response = Response{
|
||||
IP: ip,
|
||||
IPDecimal: ipDecimal,
|
||||
Country: country.Name,
|
||||
CountryISO: country.ISO,
|
||||
CountryEU: country.IsEU,
|
||||
RegionName: city.RegionName,
|
||||
RegionCode: city.RegionCode,
|
||||
MetroCode: city.MetroCode,
|
||||
PostalCode: city.PostalCode,
|
||||
City: city.Name,
|
||||
Latitude: city.Latitude,
|
||||
Longitude: city.Longitude,
|
||||
Timezone: city.Timezone,
|
||||
ASN: autonomousSystemNumber,
|
||||
ASNOrg: asn.AutonomousSystemOrganization,
|
||||
Hostname: hostname,
|
||||
}
|
||||
s.cache.Set(ip, response)
|
||||
response.UserAgent = userAgentFromRequest(r)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) {
|
||||
lastElement := filepath.Base(r.URL.Path)
|
||||
port, err := strconv.ParseUint(lastElement, 10, 16)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return PortResponse{Port: port}, fmt.Errorf("invalid port: %s", lastElement)
|
||||
}
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, false)
|
||||
if err != nil {
|
||||
return PortResponse{Port: port}, err
|
||||
}
|
||||
err = s.LookupPort(ip, port)
|
||||
return PortResponse{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Reachable: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.Country)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryISOHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.CountryISO)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICityHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.City)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICoordinatesHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintf(w, "%s,%s\n", formatCoordinate(response.Latitude), formatCoordinate(response.Longitude))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLIASNHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintf(w, "%s\n", response.ASN)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
b, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) HealthHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write([]byte(`{"status":"OK"}`))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) PortHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newPortResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
b, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) cacheResizeHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
capacity, err := strconv.Atoi(string(body))
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
if err := s.cache.Resize(capacity); err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
data := struct {
|
||||
Message string `json:"message"`
|
||||
}{fmt.Sprintf("Changed cache capacity to %d.", capacity)}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) cacheHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
cacheStats := s.cache.Stats()
|
||||
var data = struct {
|
||||
Size int `json:"size"`
|
||||
Capacity int `json:"capacity"`
|
||||
Evictions uint64 `json:"evictions"`
|
||||
}{
|
||||
cacheStats.Size,
|
||||
cacheStats.Capacity,
|
||||
cacheStats.Evictions,
|
||||
}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error())
|
||||
}
|
||||
t, err := template.ParseGlob(s.Template + "/*")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
json, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
|
||||
var data = struct {
|
||||
Response
|
||||
Host string
|
||||
BoxLatTop float64
|
||||
BoxLatBottom float64
|
||||
BoxLonLeft float64
|
||||
BoxLonRight float64
|
||||
JSON string
|
||||
Port bool
|
||||
Sponsor bool
|
||||
}{
|
||||
response,
|
||||
r.Host,
|
||||
response.Latitude + 0.05,
|
||||
response.Latitude - 0.05,
|
||||
response.Longitude - 0.05,
|
||||
response.Longitude + 0.05,
|
||||
string(json),
|
||||
s.LookupPort != nil,
|
||||
s.Sponsor,
|
||||
}
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotFoundHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
err := notFound(nil).WithMessage("404 page not found")
|
||||
if r.Header.Get("accept") == jsonMediaType {
|
||||
err = err.AsJSON()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cliMatcher(r *http.Request) bool {
|
||||
ua := useragent.Parse(r.UserAgent())
|
||||
switch ua.Product {
|
||||
case "curl", "HTTPie", "httpie-go", "Wget", "fetch libfetch", "Go", "Go-http-client", "ddclient", "Mikrotik", "xh":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) *appError
|
||||
|
||||
func wrapHandlerFunc(f http.HandlerFunc) appHandler {
|
||||
return func(w http.ResponseWriter, r *http.Request) *appError {
|
||||
f.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if e := fn(w, r); e != nil { // e is *appError
|
||||
if e.Code/100 == 5 {
|
||||
log.Println(e.Error)
|
||||
}
|
||||
// When Content-Type for error is JSON, we need to marshal the response into JSON
|
||||
if e.IsJSON() {
|
||||
var data = struct {
|
||||
Code int `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}{e.Code, e.Message}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e.Message = string(b)
|
||||
}
|
||||
// Set Content-Type of response if set in error
|
||||
if e.ContentType != "" {
|
||||
w.Header().Set("Content-Type", e.ContentType)
|
||||
}
|
||||
w.WriteHeader(e.Code)
|
||||
fmt.Fprint(w, e.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
r := NewRouter()
|
||||
|
||||
// Health
|
||||
r.Route("GET", "/health", s.HealthHandler)
|
||||
|
||||
// JSON
|
||||
r.Route("GET", "/", s.JSONHandler).Header("Accept", jsonMediaType)
|
||||
r.Route("GET", "/json", s.JSONHandler)
|
||||
|
||||
// CLI
|
||||
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
||||
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
||||
r.Route("GET", "/ip", s.CLIHandler)
|
||||
if !s.gr.IsEmpty() {
|
||||
r.Route("GET", "/country", s.CLICountryHandler)
|
||||
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
||||
r.Route("GET", "/city", s.CLICityHandler)
|
||||
r.Route("GET", "/coordinates", s.CLICoordinatesHandler)
|
||||
r.Route("GET", "/asn", s.CLIASNHandler)
|
||||
}
|
||||
|
||||
// Browser
|
||||
if s.Template != "" {
|
||||
r.Route("GET", "/", s.DefaultHandler)
|
||||
}
|
||||
|
||||
// Port testing
|
||||
if s.LookupPort != nil {
|
||||
r.RoutePrefix("GET", "/port/", s.PortHandler)
|
||||
}
|
||||
|
||||
// Profiling
|
||||
if s.profile {
|
||||
r.Route("POST", "/debug/cache/resize", s.cacheResizeHandler)
|
||||
r.Route("GET", "/debug/cache/", s.cacheHandler)
|
||||
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
|
||||
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
|
||||
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
|
||||
r.Route("GET", "/debug/pprof/trace", wrapHandlerFunc(pprof.Trace))
|
||||
r.RoutePrefix("GET", "/debug/pprof/", wrapHandlerFunc(pprof.Index))
|
||||
}
|
||||
|
||||
return r.Handler()
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
return http.ListenAndServe(addr, s.Handler())
|
||||
}
|
||||
|
||||
func formatCoordinate(c float64) string {
|
||||
return strconv.FormatFloat(c, 'f', 6, 64)
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
)
|
||||
|
||||
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
|
||||
func lookupPort(net.IP, uint64) error { return nil }
|
||||
|
||||
type testDb struct{}
|
||||
|
||||
func (t *testDb) Country(net.IP) (geo.Country, error) {
|
||||
return geo.Country{Name: "Elbonia", ISO: "EB", IsEU: new(bool)}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) City(net.IP) (geo.City, error) {
|
||||
return geo.City{Name: "Bornyasherk", RegionName: "North Elbonia", RegionCode: "1234", MetroCode: 1234, PostalCode: "1234", Latitude: 63.416667, Longitude: 10.416667, Timezone: "Europe/Bornyasherk"}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) ASN(net.IP) (geo.ASN, error) {
|
||||
return geo.ASN{AutonomousSystemNumber: 59795, AutonomousSystemOrganization: "Hosting4Real"}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) IsEmpty() bool { return false }
|
||||
|
||||
func testServer() *Server {
|
||||
return &Server{cache: NewCache(100), gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
||||
}
|
||||
|
||||
func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) {
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if acceptMediaType != "" {
|
||||
r.Header.Set("Accept", acceptMediaType)
|
||||
}
|
||||
r.Header.Set("User-Agent", userAgent)
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return string(data), res.StatusCode, nil
|
||||
}
|
||||
|
||||
func httpPost(url, body string) (*http.Response, string, error) {
|
||||
r, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return res, string(data), nil
|
||||
}
|
||||
|
||||
func TestCLIHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
userAgent string
|
||||
acceptMediaType string
|
||||
}{
|
||||
{s.URL, "127.0.0.1\n", 200, "curl/7.43.0", ""},
|
||||
{s.URL, "127.0.0.1\n", 200, "foo/bar", textMediaType},
|
||||
{s.URL + "/ip", "127.0.0.1\n", 200, "", ""},
|
||||
{s.URL + "/country", "Elbonia\n", 200, "", ""},
|
||||
{s.URL + "/country-iso", "EB\n", 200, "", ""},
|
||||
{s.URL + "/coordinates", "63.416667,10.416667\n", 200, "", ""},
|
||||
{s.URL + "/city", "Bornyasherk\n", 200, "", ""},
|
||||
{s.URL + "/foo", "404 page not found", 404, "", ""},
|
||||
{s.URL + "/asn", "AS59795\n", 200, "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, tt.acceptMediaType, tt.userAgent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d, got %d", tt.status, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q, got %q", tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
server := testServer()
|
||||
server.LookupPort = nil
|
||||
server.LookupAddr = nil
|
||||
server.gr, _ = geo.Open("", "", "")
|
||||
s := httptest.NewServer(server.Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{s.URL + "/port/1337", "404 page not found", 404},
|
||||
{s.URL + "/country", "404 page not found", 404},
|
||||
{s.URL + "/country-iso", "404 page not found", 404},
|
||||
{s.URL + "/city", "404 page not found", 404},
|
||||
{s.URL + "/json", "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d, got %d", tt.status, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q, got %q", tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{s.URL, "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"time_zone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
|
||||
{s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400},
|
||||
{s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400},
|
||||
{s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400},
|
||||
{s.URL + "/port/31337", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 31337,\n \"reachable\": true\n}", 200},
|
||||
{s.URL + "/port/80", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // checking that our test server is reachable on port 80
|
||||
{s.URL + "/port/80?ip=1.3.3.7", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // ensuring that the "ip" parameter is not usable to check remote host ports
|
||||
{s.URL + "/foo", "{\n \"status\": 404,\n \"error\": \"404 page not found\"\n}", 404},
|
||||
{s.URL + "/health", `{"status":"OK"}`, 200},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, jsonMediaType, "curl/7.2.6.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d for %s, got %d", tt.status, tt.url, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q for %s, got %q", tt.out, tt.url, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheHandler(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
got, _, err := httpGet(s.URL+"/debug/cache/", jsonMediaType, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "{\n \"size\": 0,\n \"capacity\": 100,\n \"evictions\": 0\n}"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheResizeHandler(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
_, got, err := httpPost(s.URL+"/debug/cache/resize", "10")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "{\n \"message\": \"Changed cache capacity to 10.\"\n}"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPFromRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
remoteAddr string
|
||||
headerKey string
|
||||
headerValue string
|
||||
trustedHeaders []string
|
||||
out string
|
||||
}{
|
||||
{"127.0.0.1:9999", "", "", nil, "127.0.0.1"}, // No header given
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", nil, "127.0.0.1"}, // Trusted header is empty
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Foo-Bar"}, "127.0.0.1"}, // Trusted header does not match
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Trusted header matches
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Second trusted header matches
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7,4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (commas separator)
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7, 4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (space+comma separator)
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "", []string{"X-Forwarded-For"}, "127.0.0.1"}, // Empty header
|
||||
{"127.0.0.1:9999?ip=1.2.3.4", "", "", nil, "1.2.3.4"}, // passed in "ip" parameter
|
||||
{"127.0.0.1:9999?ip=1.2.3.4", "X-Forwarded-For", "1.3.3.7,4.2.4.2", []string{"X-Forwarded-For"}, "1.2.3.4"}, // ip parameter wins over X-Forwarded-For with multiple entries
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := url.Parse("http://" + tt.remoteAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &http.Request{
|
||||
RemoteAddr: u.Host,
|
||||
Header: http.Header{},
|
||||
URL: u,
|
||||
}
|
||||
r.Header.Add(tt.headerKey, tt.headerValue)
|
||||
ip, err := ipFromRequest(tt.trustedHeaders, r, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := net.ParseIP(tt.out)
|
||||
if !ip.Equal(out) {
|
||||
t.Errorf("Expected %s, got %s", out, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIMatcher(t *testing.T) {
|
||||
browserUserAgent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.28 " +
|
||||
"Safari/537.36"
|
||||
var tests = []struct {
|
||||
in string
|
||||
out bool
|
||||
}{
|
||||
{"curl/7.26.0", true},
|
||||
{"Wget/1.13.4 (linux-gnu)", true},
|
||||
{"Wget", true},
|
||||
{"fetch libfetch/2.0", true},
|
||||
{"HTTPie/0.9.3", true},
|
||||
{"httpie-go/0.6.0", true},
|
||||
{"Go 1.1 package http", true},
|
||||
{"Go-http-client/1.1", true},
|
||||
{"Go-http-client/2.0", true},
|
||||
{"ddclient/3.8.3", true},
|
||||
{"Mikrotik/6.x Fetch", true},
|
||||
{browserUserAgent, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
r := &http.Request{Header: http.Header{"User-Agent": []string{tt.in}}}
|
||||
if got := cliMatcher(r); got != tt.out {
|
||||
t.Errorf("Expected %t, got %t for %q", tt.out, got, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
routes []*route
|
||||
}
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
path string
|
||||
prefix bool
|
||||
handler appHandler
|
||||
matcherFunc func(*http.Request) bool
|
||||
}
|
||||
|
||||
func NewRouter() *router {
|
||||
return &router{}
|
||||
}
|
||||
|
||||
func (r *router) Route(method, path string, handler appHandler) *route {
|
||||
route := route{
|
||||
method: method,
|
||||
path: path,
|
||||
handler: handler,
|
||||
}
|
||||
r.routes = append(r.routes, &route)
|
||||
return &route
|
||||
}
|
||||
|
||||
func (r *router) RoutePrefix(method, path string, handler appHandler) *route {
|
||||
route := r.Route(method, path, handler)
|
||||
route.prefix = true
|
||||
return route
|
||||
}
|
||||
|
||||
func (r *router) Handler() http.Handler {
|
||||
return appHandler(func(w http.ResponseWriter, req *http.Request) *appError {
|
||||
for _, route := range r.routes {
|
||||
if route.match(req) {
|
||||
return route.handler(w, req)
|
||||
}
|
||||
}
|
||||
return NotFoundHandler(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) Header(header, value string) {
|
||||
r.MatcherFunc(func(req *http.Request) bool {
|
||||
return req.Header.Get(header) == value
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) MatcherFunc(f func(*http.Request) bool) {
|
||||
r.matcherFunc = f
|
||||
}
|
||||
|
||||
func (r *route) match(req *http.Request) bool {
|
||||
if req.Method != r.method {
|
||||
return false
|
||||
}
|
||||
if r.prefix {
|
||||
if !strings.HasPrefix(req.URL.Path, r.path) {
|
||||
return false
|
||||
}
|
||||
} else if r.path != req.URL.Path {
|
||||
return false
|
||||
}
|
||||
return r.matcherFunc == nil || r.matcherFunc(req)
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net"
|
||||
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
Country(net.IP) (Country, error)
|
||||
City(net.IP) (City, error)
|
||||
ASN(net.IP) (ASN, error)
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
Name string
|
||||
ISO string
|
||||
IsEU *bool
|
||||
}
|
||||
|
||||
type City struct {
|
||||
Name string
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
PostalCode string
|
||||
Timezone string
|
||||
MetroCode uint
|
||||
RegionName string
|
||||
RegionCode string
|
||||
}
|
||||
|
||||
type ASN struct {
|
||||
AutonomousSystemNumber uint
|
||||
AutonomousSystemOrganization string
|
||||
}
|
||||
|
||||
type geoip struct {
|
||||
country *geoip2.Reader
|
||||
city *geoip2.Reader
|
||||
asn *geoip2.Reader
|
||||
}
|
||||
|
||||
func Open(countryDB, cityDB string, asnDB string) (Reader, error) {
|
||||
var country, city, asn *geoip2.Reader
|
||||
if countryDB != "" {
|
||||
r, err := geoip2.Open(countryDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
country = r
|
||||
}
|
||||
if cityDB != "" {
|
||||
r, err := geoip2.Open(cityDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
city = r
|
||||
}
|
||||
if asnDB != "" {
|
||||
r, err := geoip2.Open(asnDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
asn = r
|
||||
}
|
||||
return &geoip{country: country, city: city, asn: asn}, nil
|
||||
}
|
||||
|
||||
func (g *geoip) Country(ip net.IP) (Country, error) {
|
||||
country := Country{}
|
||||
if g.country == nil {
|
||||
return country, nil
|
||||
}
|
||||
record, err := g.country.Country(ip)
|
||||
if err != nil {
|
||||
return country, err
|
||||
}
|
||||
if c, exists := record.Country.Names["en"]; exists {
|
||||
country.Name = c
|
||||
}
|
||||
if c, exists := record.RegisteredCountry.Names["en"]; exists && country.Name == "" {
|
||||
country.Name = c
|
||||
}
|
||||
if record.Country.IsoCode != "" {
|
||||
country.ISO = record.Country.IsoCode
|
||||
}
|
||||
if record.RegisteredCountry.IsoCode != "" && country.ISO == "" {
|
||||
country.ISO = record.RegisteredCountry.IsoCode
|
||||
}
|
||||
isEU := record.Country.IsInEuropeanUnion || record.RegisteredCountry.IsInEuropeanUnion
|
||||
country.IsEU = &isEU
|
||||
return country, nil
|
||||
}
|
||||
|
||||
func (g *geoip) City(ip net.IP) (City, error) {
|
||||
city := City{}
|
||||
if g.city == nil {
|
||||
return city, nil
|
||||
}
|
||||
record, err := g.city.City(ip)
|
||||
if err != nil {
|
||||
return city, err
|
||||
}
|
||||
if c, exists := record.City.Names["en"]; exists {
|
||||
city.Name = c
|
||||
}
|
||||
if len(record.Subdivisions) > 0 {
|
||||
if c, exists := record.Subdivisions[0].Names["en"]; exists {
|
||||
city.RegionName = c
|
||||
}
|
||||
if record.Subdivisions[0].IsoCode != "" {
|
||||
city.RegionCode = record.Subdivisions[0].IsoCode
|
||||
}
|
||||
}
|
||||
if !math.IsNaN(record.Location.Latitude) {
|
||||
city.Latitude = record.Location.Latitude
|
||||
}
|
||||
if !math.IsNaN(record.Location.Longitude) {
|
||||
city.Longitude = record.Location.Longitude
|
||||
}
|
||||
// Metro code is US Only https://maxmind.github.io/GeoIP2-dotnet/doc/v2.7.1/html/P_MaxMind_GeoIP2_Model_Location_MetroCode.htm
|
||||
if record.Location.MetroCode > 0 && record.Country.IsoCode == "US" {
|
||||
city.MetroCode = record.Location.MetroCode
|
||||
}
|
||||
if record.Postal.Code != "" {
|
||||
city.PostalCode = record.Postal.Code
|
||||
}
|
||||
if record.Location.TimeZone != "" {
|
||||
city.Timezone = record.Location.TimeZone
|
||||
}
|
||||
|
||||
return city, nil
|
||||
}
|
||||
|
||||
func (g *geoip) ASN(ip net.IP) (ASN, error) {
|
||||
asn := ASN{}
|
||||
if g.asn == nil {
|
||||
return asn, nil
|
||||
}
|
||||
record, err := g.asn.ASN(ip)
|
||||
if err != nil {
|
||||
return asn, err
|
||||
}
|
||||
if record.AutonomousSystemNumber > 0 {
|
||||
asn.AutonomousSystemNumber = record.AutonomousSystemNumber
|
||||
}
|
||||
if record.AutonomousSystemOrganization != "" {
|
||||
asn.AutonomousSystemOrganization = record.AutonomousSystemOrganization
|
||||
}
|
||||
return asn, nil
|
||||
}
|
||||
|
||||
func (g *geoip) IsEmpty() bool {
|
||||
return g.country == nil && g.city == nil
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package iputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LookupAddr(ip net.IP) (string, error) {
|
||||
names, err := net.LookupAddr(ip.String())
|
||||
if err != nil || len(names) == 0 {
|
||||
return "", err
|
||||
}
|
||||
// Always return unrooted name
|
||||
return strings.TrimRight(names[0], "."), nil
|
||||
}
|
||||
|
||||
func LookupPort(ip net.IP, port uint64) error {
|
||||
address := fmt.Sprintf("[%s]:%d", ip, port)
|
||||
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToDecimal(ip net.IP) *big.Int {
|
||||
i := big.NewInt(0)
|
||||
if to4 := ip.To4(); to4 != nil {
|
||||
i.SetBytes(to4)
|
||||
} else {
|
||||
i.SetBytes(ip)
|
||||
}
|
||||
return i
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package iputil
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToDecimal(t *testing.T) {
|
||||
var msb = new(big.Int)
|
||||
msb, _ = msb.SetString("80000000000000000000000000000000", 16)
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
out *big.Int
|
||||
}{
|
||||
{"127.0.0.1", big.NewInt(2130706433)},
|
||||
{"::1", big.NewInt(1)},
|
||||
{"8000::", msb},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i := ToDecimal(net.ParseIP(tt.in))
|
||||
if tt.out.Cmp(i) != 0 {
|
||||
t.Errorf("Expected %d, got %d for IP %s", tt.out, i, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
0
rootfs/.gitkeep
Normal file
0
rootfs/.gitkeep
Normal file
BIN
rootfs/opt/echoip/geoip/GeoLite2-ASN.mmdb
Executable file
BIN
rootfs/opt/echoip/geoip/GeoLite2-ASN.mmdb
Executable file
Binary file not shown.
BIN
rootfs/opt/echoip/geoip/GeoLite2-City.mmdb
Executable file
BIN
rootfs/opt/echoip/geoip/GeoLite2-City.mmdb
Executable file
Binary file not shown.
After Width: | Height: | Size: 70 MiB |
BIN
rootfs/opt/echoip/geoip/GeoLite2-Country.mmdb
Executable file
BIN
rootfs/opt/echoip/geoip/GeoLite2-Country.mmdb
Executable file
Binary file not shown.
@ -49,7 +49,7 @@
|
||||
<div class="pure-g">
|
||||
<div class="pure-u pure-u-md-1">
|
||||
<div class="leafcloud-logo">
|
||||
<a href="https://jason.malaks.us" target="_blank">
|
||||
<a href="https://malaks-us.github.io/jason" target="_blank">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/126880?v=4"
|
||||
width="72"
|
308
rootfs/usr/local/bin/entrypoint.sh
Executable file
308
rootfs/usr/local/bin/entrypoint.sh
Executable file
@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
##@Version : 202211141226-git
|
||||
# @@Author : Jason Hempstead
|
||||
# @@Contact : jason@casjaysdev.com
|
||||
# @@License : WTFPL
|
||||
# @@ReadME : entrypoint.sh --help
|
||||
# @@Copyright : Copyright: (c) 2022 Jason Hempstead, Casjays Developments
|
||||
# @@Created : Monday, Nov 14, 2022 12:26 EST
|
||||
# @@File : entrypoint.sh
|
||||
# @@Description : entrypoint point for ifconfig
|
||||
# @@Changelog : New script
|
||||
# @@TODO : Better documentation
|
||||
# @@Other :
|
||||
# @@Resource :
|
||||
# @@Terminal App : no
|
||||
# @@sudo/root : no
|
||||
# @@Template : other/docker-entrypoint
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set bash options
|
||||
[ -n "$DEBUG" ] && set -x
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set functions
|
||||
__exec_command() {
|
||||
local exitCode=0
|
||||
local cmd="${*:-bash -l}"
|
||||
echo "${exec_message:-Executing command: $cmd}"
|
||||
$cmd || exitCode=1
|
||||
[ "$exitCode" = 0 ] || exitCode=10
|
||||
return ${exitCode:-$?}
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__curl() { curl -q -LSsf -o /dev/null "$@" &>/dev/null || return 10; }
|
||||
__find() { find "$1" -mindepth 1 -type ${2:-f,d} 2>/dev/null | grep '^' || return 10; }
|
||||
__pcheck() { [ -n "$(which pgrep 2>/dev/null)" ] && pgrep -x "$1" &>/dev/null || return 10; }
|
||||
__pgrep() { __pcheck "${1:-$SERVICE_NAME}" || ps aux 2>/dev/null | grep -Fw " ${1:-$SERVICE_NAME}" | grep -qv ' grep' || return 10; }
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__certbot() {
|
||||
[ -n "$DOMAINNAME" ] && [ -n "$CERT_BOT_MAIL" ] || { echo "The variables DOMAINNAME and CERT_BOT_MAIL are set" && exit 1; }
|
||||
[ "$SSL_CERT_BOT" = "true" ] && type -P certbot &>/dev/null || { export SSL_CERT_BOT="" && return 10; }
|
||||
certbot $1 --agree-tos -m $CERT_BOT_MAIL certonly --webroot -w "${WWW_ROOT_DIR:-/data/htdocs/www}" -d $DOMAINNAME -d $DOMAINNAME \
|
||||
--put-all-related-files-into "$SSL_DIR" -key-path "$SSL_KEY" -fullchain-path "$SSL_CERT"
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__heath_check() {
|
||||
status=0 health="Good"
|
||||
start-ifconfig.sh healthcheck || status=$((status + 1))
|
||||
[ "$status" -eq 0 ] || health="Errors reported see docker logs --follow $CONTAINER_NAME"
|
||||
echo "$(uname -s) $(uname -m) is running and the health is: $health"
|
||||
return ${status:-$?}
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__start_all_services() {
|
||||
echo "$service_message"
|
||||
start-ifconfig.sh
|
||||
return $?
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Additional functions
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# export functions
|
||||
export -f __exec_command __pcheck __pgrep __find __curl __heath_check __certbot __start_all_services
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Define default variables - do not change these - redefine with -e or set under Additional
|
||||
DISPLAY="${DISPLAY:-}"
|
||||
LANG="${LANG:-C.UTF-8}"
|
||||
DOMAINNAME="${DOMAINNAME:-}"
|
||||
TZ="${TZ:-America/New_York}"
|
||||
SERVICE_PORT="${SERVICE_PORT:-$PORT}"
|
||||
HOSTNAME="${HOSTNAME:-casjaysdev-ifconfig}"
|
||||
HOSTADMIN="${HOSTADMIN:-root@${DOMAINNAME:-$HOSTNAME}}"
|
||||
CERT_BOT_MAIL="${CERT_BOT_MAIL:-certbot-mail@casjay.net}"
|
||||
SSL_CERT_BOT="${SSL_CERT_BOT:-false}"
|
||||
SSL_ENABLED="${SSL_ENABLED:-false}"
|
||||
SSL_DIR="${SSL_DIR:-/config/ssl}"
|
||||
SSL_CA="${SSL_CA:-$SSL_DIR/ca.crt}"
|
||||
SSL_KEY="${SSL_KEY:-$SSL_DIR/server.key}"
|
||||
SSL_CERT="${SSL_CERT:-$SSL_DIR/server.crt}"
|
||||
SSL_CONTAINER_DIR="${SSL_CONTAINER_DIR:-/etc/ssl/CA}"
|
||||
WWW_ROOT_DIR="${WWW_ROOT_DIR:-/data/htdocs}"
|
||||
LOCAL_BIN_DIR="${LOCAL_BIN_DIR:-/usr/local/bin}"
|
||||
DEFAULT_DATA_DIR="${DEFAULT_DATA_DIR:-/usr/local/share/template-files/data}"
|
||||
DEFAULT_CONF_DIR="${DEFAULT_CONF_DIR:-/usr/local/share/template-files/config}"
|
||||
DEFAULT_TEMPLATE_DIR="${DEFAULT_TEMPLATE_DIR:-/usr/local/share/template-files/defaults}"
|
||||
CONTAINER_IP_ADDRESS="$(ip a 2>/dev/null | grep 'inet' | grep -v '127.0.0.1' | awk '{print $2}' | sed 's|/.*||g')"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Additional variables and variable overrides
|
||||
SERVICE_NAME="ifconfig"
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Show start message
|
||||
export service_message="Starting $CONTAINER_NAME"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
[ "$SERVICE_PORT" = "443" ] && SSL_ENABLED="true"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Check if this is a new container
|
||||
[ -f "/data/.docker_has_run" ] && DATA_DIR_INITIALIZED="true" || DATA_DIR_INITIALIZED="false"
|
||||
[ -f "/config/.docker_has_run" ] && CONFIG_DIR_INITIALIZED="true" || CONFIG_DIR_INITIALIZED="false"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# export variables
|
||||
export LANG TZ DOMAINNAME HOSTNAME HOSTADMIN SSL_ENABLED SSL_DIR SSL_CA SSL_KEY SERVICE_NAME
|
||||
export SSL_DIR LOCAL_BIN_DIR DEFAULT_CONF_DIR CONTAINER_IP_ADDRESS SSL_CONTAINER_DIR
|
||||
export SSL_CERT_BOT DISPLAY CONFIG_DIR_INITIALIZED DATA_DIR_INITIALIZED
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# import variables from file
|
||||
[ -f "/root/env.sh" ] && . "/root/env.sh"
|
||||
[ -f "/config/env.sh" ] && . "/config/env.sh"
|
||||
[ -f "/config/.env.sh" ] && . "/config/.env.sh"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set timezone
|
||||
[ -n "$TZ" ] && echo "$TZ" >"/etc/timezone"
|
||||
[ -f "/usr/share/zoneinfo/$TZ" ] && ln -sf "/usr/share/zoneinfo/$TZ" "/etc/localtime"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set hostname
|
||||
if [ -n "$HOSTNAME" ]; then
|
||||
echo "$HOSTNAME" >"/etc/hostname"
|
||||
echo "127.0.0.1 $HOSTNAME localhost $HOSTNAME.local" >"/etc/hosts"
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Add domain to hosts file
|
||||
if [ -n "$DOMAINNAME" ]; then
|
||||
echo "$HOSTNAME.${DOMAINNAME:-local}" >"/etc/hostname"
|
||||
echo "127.0.0.1 $HOSTNAME localhost $HOSTNAME.local" >"/etc/hosts"
|
||||
echo "${CONTAINER_IP_ADDRESS:-127.0.0.1} $HOSTNAME.$DOMAINNAME" >>"/etc/hosts"
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Delete any gitkeep files
|
||||
[ -d "/data" ] && rm -Rf "/data/.gitkeep" "/data"/*/*.gitkeep
|
||||
[ -d "/config" ] && rm -Rf "/config/.gitkeep" "/data"/*/*.gitkeep
|
||||
[ -f "/usr/local/bin/.gitkeep" ] && rm -Rf "/usr/local/bin/.gitkeep"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Create directories
|
||||
[ -d "/etc/ssl" ] || mkdir -p "$SSL_CONTAINER_DIR"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Create files
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Create symlinks
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
if [ "$SSL_ENABLED" = "true" ] || [ "$SSL_ENABLED" = "yes" ]; then
|
||||
if [ -f "/config/ssl/server.crt" ] && [ -f "/config/ssl/server.key" ]; then
|
||||
export SSL_ENABLED="true"
|
||||
if [ -n "$SSL_CA" ] && [ -f "$SSL_CA" ]; then
|
||||
mkdir -p "$SSL_CONTAINER_DIR/certs"
|
||||
cat "$SSL_CA" >>"/etc/ssl/certs/ca-certificates.crt"
|
||||
cp -Rf "/config/ssl/." "$SSL_CONTAINER_DIR/"
|
||||
fi
|
||||
else
|
||||
[ -d "$SSL_DIR" ] || mkdir -p "$SSL_DIR"
|
||||
create-ssl-cert
|
||||
fi
|
||||
type update-ca-certificates &>/dev/null && update-ca-certificates
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
[ -f "$SSL_CA" ] && cp -Rfv "$SSL_CA" "$SSL_CONTAINER_DIR/ca.crt"
|
||||
[ -f "$SSL_KEY" ] && cp -Rfv "$SSL_KEY" "$SSL_CONTAINER_DIR/server.key"
|
||||
[ -f "$SSL_CERT" ] && cp -Rfv "$SSL_CERT" "$SSL_CONTAINER_DIR/server.crt"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Setup bin directory
|
||||
SET_USR_BIN=""
|
||||
[ -d "/data/bin" ] && SET_USR_BIN+="$(__find /data/bin f) "
|
||||
[ -d "/config/bin" ] && SET_USR_BIN+="$(__find /config/bin f) "
|
||||
if [ -n "$SET_USR_BIN" ]; then
|
||||
echo "Setting up bin"
|
||||
for create_bin in $SET_USR_BIN; do
|
||||
if [ -n "$create_bin" ]; then
|
||||
create_bin_name="$(basename "$create_bin")"
|
||||
ln -sf "$create_bin" "$LOCAL_BIN_DIR/$create_bin_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Create default config
|
||||
if [ "$CONFIG_DIR_INITIALIZED" = "false" ] && [ -d "/config" ]; then
|
||||
echo "Copying default config files"
|
||||
if [ -n "$DEFAULT_TEMPLATE_DIR" ] && [ -d "$DEFAULT_TEMPLATE_DIR" ]; then
|
||||
for create_template in "$DEFAULT_TEMPLATE_DIR"/*; do
|
||||
create_template_name="$(basename "$create_template")"
|
||||
if [ -n "$create_template" ]; then
|
||||
if [ -d "$create_template" ]; then
|
||||
mkdir -p "/config/$create_template_name/"
|
||||
cp -Rf "$create_template/." "/config/$create_template_name/" 2>/dev/null
|
||||
else
|
||||
cp -Rf "$create_template" "/config/$create_template_name" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy custom config files
|
||||
if [ "$CONFIG_DIR_INITIALIZED" = "false" ] && [ -d "/config" ]; then
|
||||
echo "Copying custom config files"
|
||||
for create_config in "$DEFAULT_CONF_DIR"/*; do
|
||||
create_config_name="$(basename "$create_config")"
|
||||
if [ -n "$create_config" ]; then
|
||||
if [ -d "$create_config" ]; then
|
||||
mkdir -p "/config/$create_config_name"
|
||||
cp -Rf "$create_config/." "/config/$create_config_name/" 2>/dev/null
|
||||
else
|
||||
cp -Rf "$create_config" "/config/$create_config_name" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy custom data files
|
||||
if [ "$DATA_DIR_INITIALIZED" = "false" ] && [ -d "/data" ]; then
|
||||
echo "Copying data files"
|
||||
for create_data in "$DEFAULT_DATA_DIR"/*; do
|
||||
create_data_name="$(basename "$create_data")"
|
||||
if [ -n "$create_data" ]; then
|
||||
if [ -d "$create_data" ]; then
|
||||
mkdir -p "/data/$create_data_name"
|
||||
cp -Rf "$create_data/." "/data/$create_data_name/" 2>/dev/null
|
||||
else
|
||||
cp -Rf "$create_data" "/data/$create_data_name" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy /config to /etc
|
||||
if [ -d "/config" ]; then
|
||||
[ "$CONFIG_DIR_INITIALIZED" = "false" ] && echo "Copying /config to /etc"
|
||||
for create_conf in /config/*; do
|
||||
if [ -n "$create_conf" ]; then
|
||||
create_conf_name="$(basename "$create_conf")"
|
||||
if [ -e "/etc/$create_conf_name" ]; then
|
||||
if [ -d "/etc/$create_conf_name" ]; then
|
||||
mkdir -p "/etc/$create_conf_name/"
|
||||
cp -Rf "$create_conf/." "/etc/$create_conf_name/" 2>/dev/null
|
||||
else
|
||||
cp -Rf "$create_conf" "/etc/$create_conf_name" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Unset unneeded variables
|
||||
unset SET_USR_BIN create_bin create_bin_name create_template create_template_name
|
||||
unset create_data create_data_name create_config create_config_name create_conf create_conf_name
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
[ -f "/data/.docker_has_run" ] || { [ -d "/data" ] && echo "Initialized on: $(date)" >"/data/.docker_has_run"; }
|
||||
[ -f "/config/.docker_has_run" ] || { [ -d "/config" ] && echo "Initialized on: $(date)" >"/config/.docker_has_run"; }
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Additional commands
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Show message
|
||||
echo "Container ip address is: $CONTAINER_IP_ADDRESS"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
case "$1" in
|
||||
--help) # Help message
|
||||
echo 'Docker container for '$APPNAME''
|
||||
echo "Usage: $APPNAME [healthcheck, bash, command]"
|
||||
echo "Failed command will have exit code 10"
|
||||
echo ""
|
||||
exit ${exitCode:-$?}
|
||||
;;
|
||||
|
||||
healthcheck) # Docker healthcheck
|
||||
__heath_check "${1:-$SERVICE_NAME}" || exitCode=10
|
||||
exit ${exitCode:-$?}
|
||||
;;
|
||||
|
||||
*/bin/sh | */bin/bash | bash | shell | sh) # Launch shell
|
||||
shift 1
|
||||
__exec_command "${@:-/bin/bash}"
|
||||
exit ${exitCode:-$?}
|
||||
;;
|
||||
|
||||
certbot)
|
||||
shift 1
|
||||
SSL_CERT_BOT="true"
|
||||
if [ "$1" = "create" ]; then
|
||||
shift 1
|
||||
__certbot
|
||||
elif [ "$1" = "renew" ]; then
|
||||
shift 1
|
||||
__certbot "renew certonly --force-renew"
|
||||
else
|
||||
__exec_command "certbot" "$@"
|
||||
fi
|
||||
;;
|
||||
|
||||
*) # Execute primary command
|
||||
if [ $# -eq 0 ]; then
|
||||
__start_all_services
|
||||
exit ${exitCode:-$?}
|
||||
else
|
||||
__exec_command "$@"
|
||||
exitCode=$?
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# end of entrypoint
|
||||
exit ${exitCode:-$?}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# ex: ts=2 sw=2 et filetype=sh
|
174
rootfs/usr/local/bin/start-ifconfig.sh
Executable file
174
rootfs/usr/local/bin/start-ifconfig.sh
Executable file
@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
##@Version : 202211141226-git
|
||||
# @@Author : Jason Hempstead
|
||||
# @@Contact : jason@casjaysdev.com
|
||||
# @@License : WTFPL
|
||||
# @@ReadME : start-ifconfig.sh --help
|
||||
# @@Copyright : Copyright: (c) 2022 Jason Hempstead, Casjays Developments
|
||||
# @@Created : Monday, Nov 14, 2022 12:26 EST
|
||||
# @@File : start-ifconfig.sh
|
||||
# @@Description : script to start ifconfig
|
||||
# @@Changelog : New script
|
||||
# @@TODO : Better documentation
|
||||
# @@Other :
|
||||
# @@Resource :
|
||||
# @@Terminal App : no
|
||||
# @@sudo/root : no
|
||||
# @@Template : other/start-service
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set functions
|
||||
__curl() { curl -q -LSsf -o /dev/null "$@" &>/dev/null || return 10; }
|
||||
__find() { find "$1" -mindepth 1 -type ${2:-f,d} 2>/dev/null | grep '^' || return 10; }
|
||||
__pcheck() { [ -n "$(which pgrep 2>/dev/null)" ] && pgrep -x "$1" &>/dev/null || return 10; }
|
||||
__pgrep() { __pcheck "$1" || ps aux 2>/dev/null | grep -Fw " $1" | grep -qv ' grep' || return 10; }
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__certbot() {
|
||||
[ -n "$DOMAINNAME" ] && [ -n "$CERT_BOT_MAIL" ] || { echo "The variables DOMAINNAME and CERT_BOT_MAIL are set" && exit 1; }
|
||||
[ "$SSL_CERT_BOT" = "true" ] && type -P certbot &>/dev/null || { export SSL_CERT_BOT="" && return 10; }
|
||||
certbot $1 --agree-tos -m $CERT_BOT_MAIL certonly --webroot -w "${WWW_ROOT_DIR:-/data/htdocs/www}" -d $DOMAINNAME -d $DOMAINNAME \
|
||||
--put-all-related-files-into "$SSL_DIR" -key-path "$SSL_KEY" -fullchain-path "$SSL_CERT"
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__heath_check() {
|
||||
local healthStatus=0 health="Good"
|
||||
#__pgrep ${1:-} &>/dev/null || healthStatus=$((healthStatus + 1))
|
||||
#__curl "http://localhost:$SERVICE_PORT/server-health" || healthStatus=$((healthStatus + 1))
|
||||
[ "$healthStatus" -eq 0 ] || health="Errors reported see docker logs --follow $CONTAINER_NAME"
|
||||
return ${healthStatus:-$?}
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
__exec_command() {
|
||||
local exitCode=0
|
||||
local cmd="${*:-bash -l}"
|
||||
echo "Executing: $cmd"
|
||||
$cmd || exitCode=1
|
||||
[ "$exitCode" = 0 ] || exitCode=10
|
||||
return ${exitCode:-$?}
|
||||
}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set variables
|
||||
DISPLAY="${DISPLAY:-}"
|
||||
LANG="${LANG:-C.UTF-8}"
|
||||
DOMAINNAME="${DOMAINNAME:-}"
|
||||
TZ="${TZ:-America/New_York}"
|
||||
SERVICE_PORT="${SERVICE_PORT:-$PORT}"
|
||||
SERVICE_NAME="${CONTAINER_NAME:-}"
|
||||
HOSTNAME="${HOSTNAME:-casjaysdev-ifconfig}"
|
||||
HOSTADMIN="${HOSTADMIN:-root@${DOMAINNAME:-$HOSTNAME}}"
|
||||
SSL_CERT_BOT="${SSL_CERT_BOT:-false}"
|
||||
SSL_ENABLED="${SSL_ENABLED:-false}"
|
||||
SSL_DIR="${SSL_DIR:-/config/ssl}"
|
||||
SSL_CA="${SSL_CA:-$SSL_DIR/ca.crt}"
|
||||
SSL_KEY="${SSL_KEY:-$SSL_DIR/server.key}"
|
||||
SSL_CERT="${SSL_CERT:-$SSL_DIR/server.crt}"
|
||||
SSL_CONTAINER_DIR="${SSL_CONTAINER_DIR:-/etc/ssl/CA}"
|
||||
WWW_ROOT_DIR="${WWW_ROOT_DIR:-/data/htdocs}"
|
||||
LOCAL_BIN_DIR="${LOCAL_BIN_DIR:-/usr/local/bin}"
|
||||
DATA_DIR_INITIALIZED="${DATA_DIR_INITIALIZED:-}"
|
||||
CONFIG_DIR_INITIALIZED="${CONFIG_DIR_INITIALIZED:-}"
|
||||
DEFAULT_DATA_DIR="${DEFAULT_DATA_DIR:-/usr/local/share/template-files/data}"
|
||||
DEFAULT_CONF_DIR="${DEFAULT_CONF_DIR:-/usr/local/share/template-files/config}"
|
||||
DEFAULT_TEMPLATE_DIR="${DEFAULT_TEMPLATE_DIR:-/usr/local/share/template-files/defaults}"
|
||||
CONTAINER_IP_ADDRESS="$(ip a 2>/dev/null | grep 'inet' | grep -v '127.0.0.1' | awk '{print $2}' | sed 's|/.*||g')"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Overwrite variables
|
||||
#SERVICE_PORT=""
|
||||
SERVICE_NAME="ifconfig"
|
||||
SERVICE_COMMAND="$SERVICE_NAME"
|
||||
CONFIG="-t /opt/echoip/html"
|
||||
OPTS="-H x-forwarded-for -r -s -p"
|
||||
GEOIP="-a /opt/echoip/geoip/GeoLite2-ASN.mmdb -c /opt/echoip/geoip/GeoLite2-City.mmdb -f /opt/echoip/geoip/GeoLite2-Country.mmdb"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Show start message
|
||||
start_message="Starting $SERVICE_NAME on $CONTAINER_IP_ADDRESS:$SERVICE_PORT"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
[ "$SERVICE_PORT" = "443" ] && SSL_ENABLED="true"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Pre copy commands
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Check if this is a new container
|
||||
[ -z "$DATA_DIR_INITIALIZED" ] && [ -f "/data/.docker_has_run" ] && DATA_DIR_INITIALIZED="true"
|
||||
[ -z "$CONFIG_DIR_INITIALIZED" ] && [ -f "/config/.docker_has_run" ] && CONFIG_DIR_INITIALIZED="true"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Create default config
|
||||
if [ "$CONFIG_DIR_INITIALIZED" = "false" ] && [ -n "$DEFAULT_TEMPLATE_DIR" ]; then
|
||||
[ -d "/config" ] && cp -Rf "$DEFAULT_TEMPLATE_DIR/." "/config/" 2>/dev/null
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy custom config files
|
||||
if [ "$CONFIG_DIR_INITIALIZED" = "false" ] && [ -n "$DEFAULT_CONF_DIR" ]; then
|
||||
[ -d "/config" ] && cp -Rf "$DEFAULT_CONF_DIR/." "/config/" 2>/dev/null
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy custom data files
|
||||
if [ "$DATA_DIR_INITIALIZED" = "false" ] && [ -n "$DEFAULT_DATA_DIR" ]; then
|
||||
[ -d "/data" ] && cp -Rf "$DEFAULT_DATA_DIR/." "/data/" 2>/dev/null
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Copy html files
|
||||
if [ "$DATA_DIR_INITIALIZED" = "false" ] && [ -d "$DEFAULT_DATA_DIR/data/htdocs" ]; then
|
||||
[ -d "/data" ] && cp -Rf "$DEFAULT_DATA_DIR/data/htdocs/." "$WWW_ROOT_DIR/" 2>/dev/null
|
||||
fi
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Post copy commands
|
||||
[ -d "/data/geoip" ] && cp -Rf "/data/geoip/." "/opt/echoip/geoip/"
|
||||
[ -d "/data/htdocs" ] && cp -Rf "/data/htdocs/." "/opt/echoip/html/"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Initialized
|
||||
[ -d "/data" ] && touch "/data/.docker_has_run"
|
||||
[ -d "/config" ] && touch "/config/.docker_has_run"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# APP Variables overrides
|
||||
[ -f "/root/env.sh" ] && . "/root/env.sh"
|
||||
[ -f "/config/env.sh" ] && . "/config/env.sh"
|
||||
[ -f "/config/.env.sh" ] && . "/config/.env.sh"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Actions based on env
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# begin main app
|
||||
case "$1" in
|
||||
healthcheck)
|
||||
shift 1
|
||||
__heath_check "${SERVICE_NAME:-bash}"
|
||||
exit $?
|
||||
;;
|
||||
|
||||
certbot)
|
||||
shift 1
|
||||
SSL_CERT_BOT="true"
|
||||
if [ "$1" = "create" ]; then
|
||||
shift 1
|
||||
__certbot
|
||||
elif [ "$1" = "renew" ]; then
|
||||
shift 1
|
||||
__certbot "renew certonly --force-renew"
|
||||
else
|
||||
__exec_command "certbot" "$@"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
if __pgrep "$SERVICE_NAME" && [ ! -f "/tmp/$SERVICE_NAME.pid" ]; then
|
||||
echo "$SERVICE_NAME is running"
|
||||
else
|
||||
touch "/tmp/$SERVICE_NAME.pid"
|
||||
echo "$start_message"
|
||||
__exec_command "$SERVICE_COMMAND" $GEOIP $OPTS $CONFIG || rm -Rf "/tmp/$SERVICE_NAME.pid"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# Set exit code
|
||||
exitCode="${exitCode:-$?}"
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# End application
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# lets exit with code
|
||||
exit ${exitCode:-$?}
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# end
|
||||
# ex: ts=2 sw=2 et filetype=sh
|
@ -1,40 +0,0 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UserAgent struct {
|
||||
Product string `json:"product,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
RawValue string `json:"raw_value,omitempty"`
|
||||
}
|
||||
|
||||
func Parse(s string) UserAgent {
|
||||
parts := strings.SplitN(s, "/", 2)
|
||||
var version, comment string
|
||||
if len(parts) > 1 {
|
||||
// If first character is a number, treat it as version
|
||||
if len(parts[1]) > 0 && parts[1][0] >= 48 && parts[1][0] <= 57 {
|
||||
rest := strings.SplitN(parts[1], " ", 2)
|
||||
version = rest[0]
|
||||
if len(rest) > 1 {
|
||||
comment = rest[1]
|
||||
}
|
||||
} else {
|
||||
comment = parts[1]
|
||||
}
|
||||
} else {
|
||||
parts = strings.SplitN(s, " ", 2)
|
||||
if len(parts) > 1 {
|
||||
comment = parts[1]
|
||||
}
|
||||
}
|
||||
return UserAgent{
|
||||
Product: parts[0],
|
||||
Version: version,
|
||||
Comment: comment,
|
||||
RawValue: s,
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
var tests = []struct {
|
||||
in string
|
||||
out UserAgent
|
||||
}{
|
||||
{"", UserAgent{}},
|
||||
{"curl/", UserAgent{Product: "curl"}},
|
||||
{"curl/foo", UserAgent{Product: "curl", Comment: "foo"}},
|
||||
{"curl/7.26.0", UserAgent{Product: "curl", Version: "7.26.0"}},
|
||||
{"Wget/1.13.4 (linux-gnu)", UserAgent{Product: "Wget", Version: "1.13.4", Comment: "(linux-gnu)"}},
|
||||
{"Wget", UserAgent{Product: "Wget"}},
|
||||
{"fetch libfetch/2.0", UserAgent{Product: "fetch libfetch", Version: "2.0"}},
|
||||
{"Go 1.1 package http", UserAgent{Product: "Go", Comment: "1.1 package http"}},
|
||||
{"Mikrotik/6.x Fetch", UserAgent{Product: "Mikrotik", Version: "6.x", Comment: "Fetch"}},
|
||||
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.28 " +
|
||||
"Safari/537.36", UserAgent{Product: "Mozilla", Version: "5.0", Comment: "(Macintosh; Intel Mac OS X 10_8_4) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.28 " +
|
||||
"Safari/537.36"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ua := Parse(tt.in)
|
||||
if got := ua.Product; got != tt.out.Product {
|
||||
t.Errorf("got Product=%q for %q, want %q", got, tt.in, tt.out.Product)
|
||||
}
|
||||
if got := ua.Version; got != tt.out.Version {
|
||||
t.Errorf("got Version=%q for %q, want %q", got, tt.in, tt.out.Version)
|
||||
}
|
||||
if got := ua.Comment; got != tt.out.Comment {
|
||||
t.Errorf("got Comment=%q for %q, want %q", got, tt.in, tt.out.Comment)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user