mirror of
https://github.com/casjaysdevdocker/ifconfig
synced 2025-05-09 14:11:25 -04:00
🗃️ Committing everything that changed 🗃️
This commit is contained in:
parent
40ba713617
commit
13c8d27e11
@ -1,5 +1,9 @@
|
|||||||
Dockerfile
|
# Files to ignore
|
||||||
Dockerfile.geoip.gitignore
|
|
||||||
.gitkeep
|
.gitkeep
|
||||||
.gitignore
|
.gitignore
|
||||||
|
node_modules/**
|
||||||
.node_modules/**
|
.node_modules/**
|
||||||
|
**/.gitkeep
|
||||||
|
**/.gitignore
|
||||||
|
**/node_modules/**
|
||||||
|
**/.node_modules/**
|
||||||
|
130
Dockerfile
130
Dockerfile
@ -1,38 +1,110 @@
|
|||||||
# Build
|
# Build
|
||||||
FROM golang:1.15-buster AS build
|
FROM golang:1.15-buster AS src
|
||||||
WORKDIR /go/src/github.com/mpolden/echoip
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 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
|
ENV GO111MODULE=on CGO_ENABLED=0
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
# Run
|
FROM casjaysdevdocker/alpine:latest AS build
|
||||||
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 run
|
ARG ALPINE_VERSION="v3.16"
|
||||||
ARG BUILD_DATE="$(date +'%Y-%m-%d %H:%M')"
|
|
||||||
|
|
||||||
LABEL \
|
ARG DEFAULT_DATA_DIR="/usr/local/share/template-files/data" \
|
||||||
org.label-schema.name="ifconfig" \
|
DEFAULT_CONF_DIR="/usr/local/share/template-files/config" \
|
||||||
org.label-schema.description="Sow ip information" \
|
DEFAULT_TEMPLATE_DIR="/usr/local/share/template-files/defaults"
|
||||||
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>"
|
|
||||||
|
|
||||||
EXPOSE 8080
|
ARG PACK_LIST="bash"
|
||||||
WORKDIR /opt/echoip
|
|
||||||
VOLUME /opt/echoip/html
|
|
||||||
|
|
||||||
HEALTHCHECK --start-period=1m --interval=10m --timeout=3s CMD ["/usr/local/bin/docker-entrypoint.sh", "healthcheck"]
|
ENV LANG=en_US.UTF-8 \
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
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
|
|
127
README.md
127
README.md
@ -1,131 +1,36 @@
|
|||||||
# echoip
|
## 👋 Welcome to ifconfig 🚀
|
||||||
|
|
||||||

|
Description
|
||||||
|
|
||||||
A simple service for looking up your IP address. This is the code that powers
|
|
||||||
<https://ifconfig.co>.
|
|
||||||
|
|
||||||
## Usage
|
## Install my system scripts
|
||||||
|
|
||||||
Just the business, please:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ curl ifconfig.co
|
sudo bash -c "$(curl -q -LSsf "https://github.com/systemmgr/installer/raw/main/install.sh")"
|
||||||
127.0.0.1
|
sudo systemmgr --config && sudo systemmgr install scripts
|
||||||
|
|
||||||
$ 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Country and city lookup:
|
## Get source files
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ curl ifconfig.co/country
|
dockermgr download src ifconfig
|
||||||
Elbonia
|
|
||||||
|
|
||||||
$ curl ifconfig.co/country-iso
|
|
||||||
EB
|
|
||||||
|
|
||||||
$ curl ifconfig.co/city
|
|
||||||
Bornyasherk
|
|
||||||
|
|
||||||
$ curl ifconfig.co/asn
|
|
||||||
AS59795
|
|
||||||
```
|
```
|
||||||
|
|
||||||
As JSON:
|
OR
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ curl -H 'Accept: application/json' ifconfig.co # or curl ifconfig.co/json
|
git clone "https://github.com/casjaysdevdocker/ifconfig" "$HOME/Projects/github/casjaysdevdocker/ifconfig"
|
||||||
{
|
|
||||||
"city": "Bornyasherk",
|
|
||||||
"country": "Elbonia",
|
|
||||||
"country_iso": "EB",
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"ip_decimal": 2130706433,
|
|
||||||
"asn": "AS59795",
|
|
||||||
"asn_org": "Hosting4Real"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Port testing:
|
## Build container
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ curl ifconfig.co/port/80
|
cd "$HOME/Projects/github/casjaysdevdocker/ifconfig"
|
||||||
{
|
buildx
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"port": 80,
|
|
||||||
"reachable": false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Pass the appropriate flag (usually `-4` and `-6`) to your client to switch
|
## Authors
|
||||||
between IPv4 and IPv6 lookup.
|
|
||||||
|
|
||||||
## Features
|
🤖 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) 📽
|
||||||
* Easy to remember domain name
|
⛵ CasjaysDev Docker: [Github](https://github.com/casjaysdevdocker) [Docker](https://hub.docker.com/r/casjaysdevdocker) ⛵
|
||||||
* 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")
|
|
||||||
```
|
|
||||||
|
@ -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-g">
|
||||||
<div class="pure-u pure-u-md-1">
|
<div class="pure-u pure-u-md-1">
|
||||||
<div class="leafcloud-logo">
|
<div class="leafcloud-logo">
|
||||||
<a href="https://jason.malaks.us" target="_blank">
|
<a href="https://malaks-us.github.io/jason" target="_blank">
|
||||||
<img
|
<img
|
||||||
src="https://avatars.githubusercontent.com/u/126880?v=4"
|
src="https://avatars.githubusercontent.com/u/126880?v=4"
|
||||||
width="72"
|
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