mirror of
https://github.com/AuxXxilium/docker-ddns-server.git
synced 2024-11-23 23:00:59 +07:00
initial commit
This commit is contained in:
parent
4a3673e0d3
commit
c4809b85b4
9
.gitignore
vendored
9
.gitignore
vendored
@ -11,5 +11,14 @@
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
# dont want to share go.sum
|
||||||
|
go.sum
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# skip the database folder
|
||||||
|
database
|
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
dist: bionic
|
||||||
|
|
||||||
|
language: go
|
||||||
|
|
||||||
|
env: GO111MODULE=on
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.13.x
|
||||||
|
- 1.14.x
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: change
|
||||||
|
on_failure: always
|
3
LICENSE
3
LICENSE
@ -1,6 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 benjaminbear
|
Copyright (c) 2020 Benjamin Bärthlein
|
||||||
|
Copyright (c) 2016 David Prandzioch
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
26
deployment/Dockerfile
Normal file
26
deployment/Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM golang:latest as builder
|
||||||
|
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
ENV GOPATH=/root/go
|
||||||
|
RUN mkdir -p /root/go/src
|
||||||
|
COPY dyndns /root/go/src/dyndns
|
||||||
|
RUN cd /root/go/src/dyndns && go mod download && GOOS=linux GOARCH=amd64 go build -o /root/go/bin/dyndns && go test -v
|
||||||
|
|
||||||
|
FROM debian:buster-slim
|
||||||
|
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get install -q -y bind9 dnsutils curl && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
RUN chmod 770 /var/cache/bind
|
||||||
|
COPY deployment/setup.sh /root/setup.sh
|
||||||
|
RUN chmod +x /root/setup.sh
|
||||||
|
COPY deployment/named.conf.options /etc/bind/named.conf.options
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
COPY --from=builder /root/go/bin/dyndns /root/dyndns
|
||||||
|
COPY dyndns/views /root/views
|
||||||
|
COPY dyndns/static /root/static
|
||||||
|
|
||||||
|
EXPOSE 53 8080
|
||||||
|
CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/dyndns"]
|
17
deployment/docker-compose.yml
Normal file
17
deployment/docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
ddns:
|
||||||
|
image: bbaerthlein/docker-ddns-server:latest
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
DDNS_ADMIN_LOGIN: 'admin:$$3$$abcdefg'
|
||||||
|
DDNS_DOMAIN: 'dyndns.example.com'
|
||||||
|
DDNS_PARENT_NS: 'ns.example.com'
|
||||||
|
DDNS_DEFAULT_TTL: '3600'
|
||||||
|
ports:
|
||||||
|
- "53:53"
|
||||||
|
- "53:53/udp"
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./bind-data:/var/cache/bind
|
||||||
|
- ./database:/root/dyndns/database
|
4
deployment/envfile
Normal file
4
deployment/envfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
DDNS_ADMIN_LOGIN=admin:$$3$$abcdefg
|
||||||
|
DDNS_DOMAIN=dyndns.example.com
|
||||||
|
DDNS_PARENT_NS=ns.example.com
|
||||||
|
DDNS_DEFAULT_TTL=3600
|
8
deployment/named.conf.options
Normal file
8
deployment/named.conf.options
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
options {
|
||||||
|
directory "/var/cache/bind";
|
||||||
|
dnssec-validation auto;
|
||||||
|
recursion no;
|
||||||
|
allow-transfer { none; };
|
||||||
|
auth-nxdomain no;
|
||||||
|
listen-on-v6 { any; };
|
||||||
|
};
|
48
deployment/setup.sh
Normal file
48
deployment/setup.sh
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
[ -z "$DDNS_ADMIN_LOGIN" ] && echo "DDNS_ADMIN_LOGIN not set" && exit 1;
|
||||||
|
[ -z "$DDNS_DOMAIN" ] && echo "DDNS_DOMAIN not set" && exit 1;
|
||||||
|
[ -z "$DDNS_PARENT_NS" ] && echo "DDNS_PARENT_NS not set" && exit 1;
|
||||||
|
[ -z "$DDNS_DEFAULT_TTL" ] && echo "DDNS_DEFAULT_TTL not set" && exit 1;
|
||||||
|
|
||||||
|
DDNS_IP=$(curl icanhazip.com)
|
||||||
|
|
||||||
|
if ! grep 'zone "'$DDNS_DOMAIN'"' /etc/bind/named.conf > /dev/null
|
||||||
|
then
|
||||||
|
echo "creating zone...";
|
||||||
|
cat >> /etc/bind/named.conf <<EOF
|
||||||
|
zone "$DDNS_DOMAIN" {
|
||||||
|
type master;
|
||||||
|
file "$DDNS_DOMAIN.zone";
|
||||||
|
allow-query { any; };
|
||||||
|
allow-transfer { none; };
|
||||||
|
allow-update { localhost; };
|
||||||
|
};
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f /var/cache/bind/$DDNS_DOMAIN.zone ]
|
||||||
|
then
|
||||||
|
echo "creating zone file..."
|
||||||
|
cat > /var/cache/bind/$DDNS_DOMAIN.zone <<EOF
|
||||||
|
\$ORIGIN .
|
||||||
|
\$TTL 86400 ; 1 day
|
||||||
|
$DDNS_DOMAIN IN SOA ${DDNS_PARENT_NS}. root.${DDNS_DOMAIN}. (
|
||||||
|
74 ; serial
|
||||||
|
3600 ; refresh (1 hour)
|
||||||
|
900 ; retry (15 minutes)
|
||||||
|
604800 ; expire (1 week)
|
||||||
|
86400 ; minimum (1 day)
|
||||||
|
)
|
||||||
|
NS ${DDNS_PARENT_NS}.
|
||||||
|
A ${DDNS_IP}
|
||||||
|
\$ORIGIN ${DDNS_DOMAIN}.
|
||||||
|
\$TTL ${DDNS_DEFAULT_TTL}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If /var/cache/bind is a volume, permissions are probably not ok
|
||||||
|
chown root:bind /var/cache/bind
|
||||||
|
chown bind:bind /var/cache/bind/*
|
||||||
|
chmod 770 /var/cache/bind
|
||||||
|
chmod 644 /var/cache/bind/*
|
15
dyndns/go.mod
Normal file
15
dyndns/go.mod
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module github.com/benjaminbear/docker-ddns-server/dyndns
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/foolin/goview v0.3.0
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0
|
||||||
|
github.com/jinzhu/gorm v1.9.12
|
||||||
|
github.com/labstack/echo/v4 v4.1.15
|
||||||
|
github.com/labstack/gommon v0.3.0
|
||||||
|
github.com/tg123/go-htpasswd v1.0.0
|
||||||
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 // indirect
|
||||||
|
)
|
116
dyndns/handler/handler.go
Normal file
116
dyndns/handler/handler.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/tg123/go-htpasswd"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
AuthHost *model.Host
|
||||||
|
AuthAdmin bool
|
||||||
|
Config Envs
|
||||||
|
}
|
||||||
|
|
||||||
|
type Envs struct {
|
||||||
|
AdminLogin string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomValidator struct {
|
||||||
|
Validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *CustomValidator) Validate(i interface{}) error {
|
||||||
|
return cv.Validator.Struct(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Authenticate(username, password string, c echo.Context) (bool, error) {
|
||||||
|
h.AuthHost = nil
|
||||||
|
h.AuthAdmin = false
|
||||||
|
|
||||||
|
ok, err := h.authByEnv(username, password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
h.AuthAdmin = true
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err := h.DB.Where(&model.Host{UserName: username, Password: password}).First(host).Error; err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.AuthHost = host
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) authByEnv(username, password string) (bool, error) {
|
||||||
|
hashReader := strings.NewReader(h.Config.AdminLogin)
|
||||||
|
|
||||||
|
pw, err := htpasswd.NewFromReader(hashReader, htpasswd.DefaultSystems, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := pw.Match(username, password); ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ParseEnvs() error {
|
||||||
|
h.Config = Envs{}
|
||||||
|
h.Config.AdminLogin = os.Getenv("DDNS_ADMIN_LOGIN")
|
||||||
|
if h.Config.AdminLogin == "" {
|
||||||
|
return fmt.Errorf("environment variable DDNS_ADMIN_LOGIN has to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Config.Domain = os.Getenv("DDNS_DOMAIN")
|
||||||
|
if h.Config.Domain == "" {
|
||||||
|
return fmt.Errorf("environment variable DDNS_DOMAIN has to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) InitDB() (err error) {
|
||||||
|
if _, err := os.Stat("database"); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll("database", os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.DB, err = gorm.Open("sqlite3", "database/ddns.db")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.DB.HasTable(&model.Host{}) {
|
||||||
|
h.DB.CreateTable(&model.Host{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.DB.HasTable(&model.Log{}) {
|
||||||
|
h.DB.CreateTable(&model.Log{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
258
dyndns/handler/host.go
Normal file
258
dyndns/handler/host.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) GetHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err = h.DB.First(host, id).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display site
|
||||||
|
return c.JSON(http.StatusOK, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListHosts(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := new([]model.Host)
|
||||||
|
if err = h.DB.Find(hosts).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "listhosts", echo.Map{
|
||||||
|
"hosts": hosts,
|
||||||
|
"config": h.Config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AddHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "edithost", echo.Map{
|
||||||
|
"addEdit": "add",
|
||||||
|
"config": h.Config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) EditHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err = h.DB.First(host, id).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "edithost", echo.Map{
|
||||||
|
"host": host,
|
||||||
|
"addEdit": "edit",
|
||||||
|
"config": h.Config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err = c.Bind(host); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Validate(host); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.DB.Create(host).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a ip is set create dns entry
|
||||||
|
if host.Ip != "" {
|
||||||
|
ipType := getIPType(host.Ip)
|
||||||
|
if ipType == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.updateRecord(host.Hostname, host.Ip, ipType, host.Ttl); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
hostUpdate := &model.Host{}
|
||||||
|
if err = c.Bind(hostUpdate); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err = h.DB.First(host, id).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
forceRecordUpdate := host.UpdateHost(hostUpdate)
|
||||||
|
if err = c.Validate(host); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.DB.Save(host).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ip or ttl changed update dns entry
|
||||||
|
if forceRecordUpdate {
|
||||||
|
ipType := getIPType(host.Ip)
|
||||||
|
if ipType == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{fmt.Sprintf("ip %s is not a valid ip", host.Ip)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.updateRecord(host.Hostname, host.Ip, ipType, host.Ttl); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DeleteHost(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &model.Host{}
|
||||||
|
if err = h.DB.First(host, id).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err = tx.Unscoped().Delete(host).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Where(&model.Log{HostID: uint(id)}).Delete(&model.Log{}).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.deleteRecord(host.Hostname); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateIP(c echo.Context) (err error) {
|
||||||
|
if h.AuthHost == nil {
|
||||||
|
return c.String(http.StatusBadRequest, "badauth\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := &model.Log{Status: false, Host: *h.AuthHost, TimeStamp: time.Now(), UserAgent: shrinkUserAgent(c.Request().UserAgent())}
|
||||||
|
log.SentIP = c.QueryParam(("myip"))
|
||||||
|
|
||||||
|
// Get caller IP
|
||||||
|
log.CallerIP, err = getCallerIP(c.Request())
|
||||||
|
if log.CallerIP == "" {
|
||||||
|
log.CallerIP, _, err = net.SplitHostPort(c.Request().RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
if err = h.CreateLogEntry(log); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusBadRequest, "badrequest\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hostname
|
||||||
|
hostname := c.QueryParam("hostname")
|
||||||
|
if hostname == "" || hostname != h.AuthHost.Hostname+"."+h.Config.Domain {
|
||||||
|
if err = h.CreateLogEntry(log); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusBadRequest, "notfqdn\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IP type
|
||||||
|
ipType := getIPType(log.SentIP)
|
||||||
|
if ipType == "" {
|
||||||
|
log.SentIP = log.CallerIP
|
||||||
|
ipType = getIPType(log.SentIP)
|
||||||
|
if ipType == "" {
|
||||||
|
if err = h.CreateLogEntry(log); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusBadRequest, "badrequest\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add/update DNS record
|
||||||
|
if err = h.updateRecord(log.Host.Hostname, log.SentIP, ipType, log.Host.Ttl); err != nil {
|
||||||
|
if err = h.CreateLogEntry(log); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusBadRequest, "dnserr\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Host.Ip = log.SentIP
|
||||||
|
log.Host.LastUpdate = log.TimeStamp
|
||||||
|
log.Status = true
|
||||||
|
if err = h.CreateLogEntry(log); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "good\n")
|
||||||
|
}
|
53
dyndns/handler/log.go
Normal file
53
dyndns/handler/log.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) CreateLogEntry(log *model.Log) (err error) {
|
||||||
|
if err = h.DB.Create(log).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ShowLogs(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := new([]model.Log)
|
||||||
|
if err = h.DB.Preload("Host").Limit(30).Find(logs).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "listlogs", echo.Map{
|
||||||
|
"logs": logs,
|
||||||
|
"config": h.Config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ShowHostLogs(c echo.Context) (err error) {
|
||||||
|
if !h.AuthAdmin {
|
||||||
|
return c.JSON(http.StatusUnauthorized, &Error{"You are not allow to view that content"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := new([]model.Log)
|
||||||
|
if err = h.DB.Preload("Host").Where(&model.Log{HostID: uint(id)}).Limit(30).Find(logs).Error; err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "listlogs", echo.Map{
|
||||||
|
"logs": logs,
|
||||||
|
"config": h.Config,
|
||||||
|
})
|
||||||
|
}
|
181
dyndns/handler/update.go
Normal file
181
dyndns/handler/update.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/benjaminbear/docker-ddns-server/dyndns/ipparser"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) updateRecord(hostname string, ipAddr string, addrType string, ttl int) error {
|
||||||
|
fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, ipAddr)
|
||||||
|
|
||||||
|
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
w := bufio.NewWriter(f)
|
||||||
|
|
||||||
|
w.WriteString(fmt.Sprintf("server %s\n", "localhost"))
|
||||||
|
w.WriteString(fmt.Sprintf("zone %s\n", h.Config.Domain))
|
||||||
|
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", hostname, h.Config.Domain, addrType))
|
||||||
|
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, h.Config.Domain, ttl, addrType, ipAddr))
|
||||||
|
w.WriteString("send\n")
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("/usr/bin/nsupdate", f.Name())
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v: %v", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.String() != "" {
|
||||||
|
return fmt.Errorf(out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) deleteRecord(hostname string) error {
|
||||||
|
fmt.Printf("record delete request: %s\n", hostname)
|
||||||
|
|
||||||
|
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
w := bufio.NewWriter(f)
|
||||||
|
|
||||||
|
w.WriteString(fmt.Sprintf("server %s\n", "localhost"))
|
||||||
|
w.WriteString(fmt.Sprintf("zone %s\n", h.Config.Domain))
|
||||||
|
w.WriteString(fmt.Sprintf("update delete %s.%s\n", hostname, h.Config.Domain))
|
||||||
|
w.WriteString("send\n")
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("/usr/bin/nsupdate", f.Name())
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v: %v", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.String() != "" {
|
||||||
|
return fmt.Errorf(out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIPType(ipAddr string) string {
|
||||||
|
if ipparser.ValidIP4(ipAddr) {
|
||||||
|
return "A"
|
||||||
|
} else if ipparser.ValidIP6(ipAddr) {
|
||||||
|
return "AAAA"
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCallerIP(r *http.Request) (string, error) {
|
||||||
|
fmt.Println("request", r.Header)
|
||||||
|
for _, h := range []string{"X-Real-Ip", "X-Forwarded-For"} {
|
||||||
|
addresses := strings.Split(r.Header.Get(h), ",")
|
||||||
|
// march from right to left until we get a public address
|
||||||
|
// that will be the address right before our proxy.
|
||||||
|
for i := len(addresses) - 1; i >= 0; i-- {
|
||||||
|
ip := strings.TrimSpace(addresses[i])
|
||||||
|
// header can contain spaces too, strip those out.
|
||||||
|
realIP := net.ParseIP(ip)
|
||||||
|
if !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) {
|
||||||
|
// bad address, go to next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("no match")
|
||||||
|
}
|
||||||
|
|
||||||
|
//ipRange - a structure that holds the start and end of a range of ip addresses
|
||||||
|
type ipRange struct {
|
||||||
|
start net.IP
|
||||||
|
end net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// inRange - check to see if a given ip address is within a range given
|
||||||
|
func inRange(r ipRange, ipAddress net.IP) bool {
|
||||||
|
// strcmp type byte comparison
|
||||||
|
if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateRanges = []ipRange{
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("10.0.0.0"),
|
||||||
|
end: net.ParseIP("10.255.255.255"),
|
||||||
|
},
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("100.64.0.0"),
|
||||||
|
end: net.ParseIP("100.127.255.255"),
|
||||||
|
},
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("172.16.0.0"),
|
||||||
|
end: net.ParseIP("172.31.255.255"),
|
||||||
|
},
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("192.0.0.0"),
|
||||||
|
end: net.ParseIP("192.0.0.255"),
|
||||||
|
},
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("192.168.0.0"),
|
||||||
|
end: net.ParseIP("192.168.255.255"),
|
||||||
|
},
|
||||||
|
ipRange{
|
||||||
|
start: net.ParseIP("198.18.0.0"),
|
||||||
|
end: net.ParseIP("198.19.255.255"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPrivateSubnet - check to see if this ip is in a private subnet
|
||||||
|
func isPrivateSubnet(ipAddress net.IP) bool {
|
||||||
|
// my use case is only concerned with ipv4 atm
|
||||||
|
if ipCheck := ipAddress.To4(); ipCheck != nil {
|
||||||
|
// iterate over all our ranges
|
||||||
|
for _, r := range privateRanges {
|
||||||
|
// check if this ip is in a private range
|
||||||
|
if inRange(r, ipAddress) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func shrinkUserAgent(agent string) string {
|
||||||
|
agentParts := strings.Split(agent, " ")
|
||||||
|
|
||||||
|
return agentParts[0]
|
||||||
|
}
|
23
dyndns/ipparser/ipparser.go
Normal file
23
dyndns/ipparser/ipparser.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package ipparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidIP4(ipAddress string) bool {
|
||||||
|
testInput := net.ParseIP(ipAddress)
|
||||||
|
if testInput == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (testInput.To4() != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidIP6(ip6Address string) bool {
|
||||||
|
testInputIP6 := net.ParseIP(ip6Address)
|
||||||
|
if testInputIP6 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (testInputIP6.To16() != nil)
|
||||||
|
}
|
29
dyndns/ipparser/ipparser_test.go
Normal file
29
dyndns/ipparser/ipparser_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package ipparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidIP4ToReturnTrueOnValidAddress(t *testing.T) {
|
||||||
|
result := ValidIP4("1.2.3.4")
|
||||||
|
|
||||||
|
if result != true {
|
||||||
|
t.Fatalf("Expected ValidIP(1.2.3.4) to be true but got false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidIP4ToReturnFalseOnInvalidAddress(t *testing.T) {
|
||||||
|
result := ValidIP4("abcd")
|
||||||
|
|
||||||
|
if result == true {
|
||||||
|
t.Fatalf("Expected ValidIP(abcd) to be false but got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidIP4ToReturnFalseOnEmptyAddress(t *testing.T) {
|
||||||
|
result := ValidIP4("")
|
||||||
|
|
||||||
|
if result == true {
|
||||||
|
t.Fatalf("Expected ValidIP() to be false but got true")
|
||||||
|
}
|
||||||
|
}
|
67
dyndns/main.go
Normal file
67
dyndns/main.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/benjaminbear/docker-ddns-server/dyndns/handler"
|
||||||
|
"github.com/foolin/goview/supports/echoview-v4"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
|
||||||
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
"github.com/labstack/gommon/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
e.Logger.SetLevel(log.ERROR)
|
||||||
|
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
|
||||||
|
// Set Renderer
|
||||||
|
e.Renderer = echoview.Default()
|
||||||
|
|
||||||
|
// Set Validator
|
||||||
|
e.Validator = &handler.CustomValidator{Validator: validator.New()}
|
||||||
|
|
||||||
|
// Set Statics
|
||||||
|
e.Static("/static", "static")
|
||||||
|
|
||||||
|
// Initialize handler
|
||||||
|
h := &handler.Handler{}
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
if err := h.InitDB(); err != nil {
|
||||||
|
e.Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
defer h.DB.Close()
|
||||||
|
|
||||||
|
if err := h.ParseEnvs(); err != nil {
|
||||||
|
e.Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Use(middleware.BasicAuth(h.Authenticate))
|
||||||
|
|
||||||
|
// UI Routes
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
//render with master
|
||||||
|
return c.Render(http.StatusOK, "index", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/hosts/add", h.AddHost)
|
||||||
|
e.GET("/hosts/edit/:id", h.EditHost)
|
||||||
|
e.GET("/hosts", h.ListHosts)
|
||||||
|
e.GET("/logs", h.ShowLogs)
|
||||||
|
e.GET("/logs/host/:id", h.ShowHostLogs)
|
||||||
|
|
||||||
|
// Rest Routes
|
||||||
|
e.POST("/hosts/add", h.CreateHost)
|
||||||
|
e.POST("/hosts/edit/:id", h.UpdateHost)
|
||||||
|
e.GET("/hosts/delete/:id", h.DeleteHost)
|
||||||
|
e.GET("/update", h.UpdateIP)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
e.Logger.Fatal(e.Start(":8080"))
|
||||||
|
}
|
32
dyndns/model/host.go
Normal file
32
dyndns/model/host.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Host struct {
|
||||||
|
gorm.Model
|
||||||
|
Hostname string `gorm:"unique;not null" form:"hostname" validate:"required,hostname"`
|
||||||
|
Ip string `form:"ip" validate:"omitempty,ipv4"`
|
||||||
|
Ttl int `form:"ttl" validate:"required,min=20,max=86400"`
|
||||||
|
LastUpdate time.Time `form:"lastupdate"`
|
||||||
|
UserName string `gorm:"unique" form:"username" validate:"min=8"`
|
||||||
|
Password string `form:"password" validate:"min=8"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) UpdateHost(updateHost *Host) (updateRecord bool) {
|
||||||
|
updateRecord = false
|
||||||
|
if h.Ip != updateHost.Ip || h.Ttl != updateHost.Ttl {
|
||||||
|
updateRecord = true
|
||||||
|
h.LastUpdate = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Ip = updateHost.Ip
|
||||||
|
h.Ttl = updateHost.Ttl
|
||||||
|
h.UserName = updateHost.UserName
|
||||||
|
h.Password = updateHost.Password
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
18
dyndns/model/log.go
Normal file
18
dyndns/model/log.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
gorm.Model
|
||||||
|
Status bool
|
||||||
|
Host Host
|
||||||
|
HostID uint
|
||||||
|
SentIP string
|
||||||
|
CallerIP string
|
||||||
|
TimeStamp time.Time
|
||||||
|
UserAgent string
|
||||||
|
}
|
3719
dyndns/static/css/bootstrap-grid.css
vendored
Normal file
3719
dyndns/static/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dyndns/static/css/bootstrap-grid.css.map
Normal file
1
dyndns/static/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
dyndns/static/css/bootstrap-grid.min.css
vendored
Normal file
7
dyndns/static/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dyndns/static/css/bootstrap-grid.min.css.map
Normal file
1
dyndns/static/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
331
dyndns/static/css/bootstrap-reboot.css
vendored
Normal file
331
dyndns/static/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2019 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2019 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
line-height: 1.15;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #212529;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[tabindex="-1"]:focus {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-original-title] {
|
||||||
|
text-decoration: underline;
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 0;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]):focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: left;
|
||||||
|
caption-side: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: 1px dotted;
|
||||||
|
outline: 5px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled),
|
||||||
|
[type="button"]:not(:disabled),
|
||||||
|
[type="reset"]:not(:disabled),
|
||||||
|
[type="submit"]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="date"],
|
||||||
|
input[type="time"],
|
||||||
|
input[type="datetime-local"],
|
||||||
|
input[type="month"] {
|
||||||
|
-webkit-appearance: listbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
1
dyndns/static/css/bootstrap-reboot.css.map
Normal file
1
dyndns/static/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
dyndns/static/css/bootstrap-reboot.min.css
vendored
Normal file
8
dyndns/static/css/bootstrap-reboot.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2019 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2019 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
1
dyndns/static/css/bootstrap-reboot.min.css.map
Normal file
1
dyndns/static/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
10038
dyndns/static/css/bootstrap.css
vendored
Normal file
10038
dyndns/static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dyndns/static/css/bootstrap.css.map
Normal file
1
dyndns/static/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
dyndns/static/css/bootstrap.min.css
vendored
Normal file
7
dyndns/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dyndns/static/css/bootstrap.min.css.map
Normal file
1
dyndns/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
79
dyndns/static/css/narrow-jumbotron.css
Normal file
79
dyndns/static/css/narrow-jumbotron.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/* Space out content a bit */
|
||||||
|
body {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything but the jumbotron gets side spacing for mobile first views */
|
||||||
|
.header,
|
||||||
|
.marketing,
|
||||||
|
.footer {
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page header */
|
||||||
|
.header {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: .05rem solid #e5e5e5;
|
||||||
|
}
|
||||||
|
/* Make the masthead heading the same height as the navigation */
|
||||||
|
.header h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page footer */
|
||||||
|
.footer {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
color: #777;
|
||||||
|
border-top: .05rem solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customize container */
|
||||||
|
@media (min-width: 56em) {
|
||||||
|
.container {
|
||||||
|
max-width: 54rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container-narrow > hr {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main marketing message and sign up button */
|
||||||
|
.jumbotron {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: .05rem solid #e5e5e5;
|
||||||
|
}
|
||||||
|
.jumbotron .btn {
|
||||||
|
padding: .75rem 1.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Supporting marketing content */
|
||||||
|
.marketing {
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
.marketing p + h4 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: Portrait tablets and up */
|
||||||
|
@media screen and (min-width: 56em) {
|
||||||
|
/* Remove the padding we set earlier */
|
||||||
|
.header,
|
||||||
|
.marketing,
|
||||||
|
.footer {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
/* Space out the masthead */
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
/* Remove the bottom border on the jumbotron for visual effect */
|
||||||
|
.jumbotron {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
4
dyndns/static/icons/clipboard.svg
Normal file
4
dyndns/static/icons/clipboard.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg class="bi bi-clipboard" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4 1.5H3a2 2 0 00-2 2V14a2 2 0 002 2h10a2 2 0 002-2V3.5a2 2 0 00-2-2h-1v1h1a1 1 0 011 1V14a1 1 0 01-1 1H3a1 1 0 01-1-1V3.5a1 1 0 011-1h1v-1z" clip-rule="evenodd"/>
|
||||||
|
<path fill-rule="evenodd" d="M9.5 1h-3a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zm-3-1A1.5 1.5 0 005 1.5v1A1.5 1.5 0 006.5 4h3A1.5 1.5 0 0011 2.5v-1A1.5 1.5 0 009.5 0h-3z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 552 B |
4
dyndns/static/icons/pencil.svg
Normal file
4
dyndns/static/icons/pencil.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg class="bi bi-pencil" width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M13.293 3.293a1 1 0 011.414 0l2 2a1 1 0 010 1.414l-9 9a1 1 0 01-.39.242l-3 1a1 1 0 01-1.266-1.265l1-3a1 1 0 01.242-.391l9-9zM14 4l2 2-9 9-3 1 1-3 9-9z" clip-rule="evenodd"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.146 8.354l-2.5-2.5.708-.708 2.5 2.5-.708.708zM5 12v.5a.5.5 0 00.5.5H6v.5a.5.5 0 00.5.5H7v.5a.5.5 0 00.5.5H8v-1.5a.5.5 0 00-.5-.5H7v-.5a.5.5 0 00-.5-.5H5z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 550 B |
7
dyndns/static/icons/table.svg
Normal file
7
dyndns/static/icons/table.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg class="bi bi-table" width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M16 3H4a1 1 0 00-1 1v12a1 1 0 001 1h12a1 1 0 001-1V4a1 1 0 00-1-1zM4 2a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H4z" clip-rule="evenodd"/>
|
||||||
|
<path fill-rule="evenodd" d="M17 6H3V5h14v1z" clip-rule="evenodd"/>
|
||||||
|
<path fill-rule="evenodd" d="M7 17.5v-14h1v14H7zm5 0v-14h1v14h-1z" clip-rule="evenodd"/>
|
||||||
|
<path fill-rule="evenodd" d="M17 10H3V9h14v1zm0 4H3v-1h14v1z" clip-rule="evenodd"/>
|
||||||
|
<path d="M2 4a2 2 0 012-2h12a2 2 0 012 2v2H2V4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 618 B |
4
dyndns/static/icons/trash.svg
Normal file
4
dyndns/static/icons/trash.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg class="bi bi-trash" width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.5 7.5A.5.5 0 018 8v6a.5.5 0 01-1 0V8a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V8a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V8z"/>
|
||||||
|
<path fill-rule="evenodd" d="M16.5 5a1 1 0 01-1 1H15v9a2 2 0 01-2 2H7a2 2 0 01-2-2V6h-.5a1 1 0 01-1-1V4a1 1 0 011-1H8a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1v1zM6.118 6L6 6.059V15a1 1 0 001 1h6a1 1 0 001-1V6.059L13.882 6H6.118zM4.5 5V4h11v1h-11z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 566 B |
83
dyndns/static/js/additional.js
Normal file
83
dyndns/static/js/additional.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
function deleteHost(id) {
|
||||||
|
$.ajax({
|
||||||
|
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
type: 'GET',
|
||||||
|
url: "/hosts/delete/" + id
|
||||||
|
}).done(function(data, textStatus, jqXHR) {
|
||||||
|
location.href="/hosts";
|
||||||
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
|
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
|
||||||
|
location.reload()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEditHost(id, addedit) {
|
||||||
|
if (id == null) {
|
||||||
|
id = ""
|
||||||
|
} else {
|
||||||
|
id = "/"+id
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
data: $('#edithostform').serialize(),
|
||||||
|
type: 'POST',
|
||||||
|
url: '/hosts/'+addedit+id,
|
||||||
|
}).done(function(data, textStatus, jqXHR) {
|
||||||
|
location.href="/hosts";
|
||||||
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
|
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOut(){
|
||||||
|
try {
|
||||||
|
// This is for Firefox
|
||||||
|
$.ajax({
|
||||||
|
// This can be any path on your same domain which requires HTTPAuth
|
||||||
|
url: "",
|
||||||
|
username: 'reset',
|
||||||
|
password: 'reset',
|
||||||
|
// If the return is 401, refresh the page to request new details.
|
||||||
|
statusCode: { 401: function() {
|
||||||
|
document.location = document.location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (exception) {
|
||||||
|
// Firefox throws an exception since we didn't handle anything but a 401 above
|
||||||
|
// This line works only in IE
|
||||||
|
if (!document.execCommand("ClearAuthenticationCache")) {
|
||||||
|
// exeCommand returns false if it didn't work (which happens in Chrome) so as a last
|
||||||
|
// resort refresh the page providing new, invalid details.
|
||||||
|
document.location = "http://reset:reset@" + document.location.hostname + document.location.pathname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomHash() {
|
||||||
|
var chars = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+<>ABCDEFGHIJKLMNOP1234567890";
|
||||||
|
var pass = "";
|
||||||
|
for (var x = 0; x < 32; x++) {
|
||||||
|
var i = Math.floor(Math.random() * chars.length);
|
||||||
|
pass += chars.charAt(i);
|
||||||
|
}
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUsername() {
|
||||||
|
edithostform.username.value = randomHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePassword() {
|
||||||
|
edithostform.password.value = randomHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(inputId) {
|
||||||
|
var copyText = document.getElementById(inputId);
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999);
|
||||||
|
document.execCommand("copy");
|
||||||
|
}
|
7013
dyndns/static/js/bootstrap.bundle.js
vendored
Normal file
7013
dyndns/static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dyndns/static/js/bootstrap.bundle.js.map
Normal file
1
dyndns/static/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
dyndns/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
dyndns/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dyndns/static/js/bootstrap.bundle.min.js.map
Normal file
1
dyndns/static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4435
dyndns/static/js/bootstrap.js
vendored
Normal file
4435
dyndns/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dyndns/static/js/bootstrap.js.map
Normal file
1
dyndns/static/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
dyndns/static/js/bootstrap.min.js
vendored
Normal file
7
dyndns/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dyndns/static/js/bootstrap.min.js.map
Normal file
1
dyndns/static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
24
dyndns/static/js/ie10-viewport-bug-workaround.js
Normal file
24
dyndns/static/js/ie10-viewport-bug-workaround.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*!
|
||||||
|
* IE10 viewport hack for Surface/desktop Windows 8 bug
|
||||||
|
* Copyright 2014-2017 The Bootstrap Authors
|
||||||
|
* Copyright 2014-2017 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// See the Getting Started docs for more information:
|
||||||
|
// https://getbootstrap.com/getting-started/#support-ie10-width
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
|
||||||
|
var msViewportStyle = document.createElement('style')
|
||||||
|
msViewportStyle.appendChild(
|
||||||
|
document.createTextNode(
|
||||||
|
'@-ms-viewport{width:auto!important}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
document.head.appendChild(msViewportStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
}())
|
2
dyndns/static/js/jquery-3.4.1.min.js
vendored
Normal file
2
dyndns/static/js/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
69
dyndns/views/edithost.html
Normal file
69
dyndns/views/edithost.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{{define "content"}}
|
||||||
|
<div class="p-4" style="background-color: #e9ecef">
|
||||||
|
<h3 class="text-center mb-4">{{if eq .addEdit "edit" }}Edit{{else if eq .addEdit "add" }}Add{{end}} Host Entry</h3>
|
||||||
|
<form id="edithostform" action="javascript:void(0);">
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-2 text-right">Hostname:</div>
|
||||||
|
<div class="col-8 input-group">
|
||||||
|
<input type="text" class="form-control" placeholder="Enter hostname" name="hostname" value="{{.host.Hostname}}" {{if eq .addEdit "edit" }}readonly{{end}}>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text" id="basic-addon2">.{{.config.Domain}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-2 text-right">IP Address:</div>
|
||||||
|
<div class="col-8"><input type="text" class="form-control" placeholder="Enter IP Address" name="ip" value="{{.host.Ip}}"></div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-2 text-right">TTL:</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<select class="form-control" name="ttl">
|
||||||
|
<option value="20" {{if .host.Ttl }}{{if eq .host.Ttl 20 }}selected{{end}}{{end}}>20 s. Super dynamic DNS for frequent updates</option>
|
||||||
|
<option value="60" {{if .host.Ttl }}{{if eq .host.Ttl 60 }}selected{{end}}{{end}}>60 s. Default dynamic DNS value</option>
|
||||||
|
<option value="3600" {{if .host.Ttl }}{{if eq .host.Ttl 3600 }}selected{{end}}{{end}}>1 hr. Rarely updated IP address</option>
|
||||||
|
<option value="14400" {{if .host.Ttl }}{{if eq .host.Ttl 14400 }}selected{{end}}{{end}}>4 hrs. Static record with benefits of DNS caching</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-2 text-right">Username:</div>
|
||||||
|
<div class="col-8 input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('username')"><img src="/static/icons/clipboard.svg" style="vertical-align: baseline" alt="" width="16" height="16" title="Copy"></button>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" placeholder="Enter username" name="username" id="username" value="{{.host.UserName}}">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="generateUsername()">Generate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-2 text-right">Password:</div>
|
||||||
|
<div class="col-8 input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('password')"><img src="/static/icons/clipboard.svg" style="vertical-align: baseline" alt="" width="16" height="16" title="Copy"></button>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" placeholder="Enter password" name="password" id="password" value="{{.host.Password}}">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="generatePassword()">Generate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-11 d-flex justify-content-end"><button class="btn btn-primary" onclick="addEditHost({{.host.ID}}, {{.addEdit}})">{{if eq .addEdit "edit" }}Edit{{else if eq .addEdit "add" }}Add{{end}} Host Entry</button></div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
31
dyndns/views/index.html
Normal file
31
dyndns/views/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{define "content"}}
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1 class="display-3">Jumbotron heading</h1>
|
||||||
|
<p class="lead">Cras justo odio, dapibus ac facilisis in, egestas eget quam. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
|
||||||
|
<p><a class="btn btn-lg btn-success" href="#" role="button">Sign up today</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row marketing">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.</p>
|
||||||
|
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.</p>
|
||||||
|
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.</p>
|
||||||
|
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.</p>
|
||||||
|
|
||||||
|
<h4>Subheading</h4>
|
||||||
|
<p>Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
58
dyndns/views/layouts/master.html
Normal file
58
dyndns/views/layouts/master.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<link rel="icon" href="/static/icons/favicon.ico">
|
||||||
|
|
||||||
|
<title>Narrow Jumbotron Template for Bootstrap</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link href="/static/css/narrow-jumbotron.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="header clearfix">
|
||||||
|
<nav>
|
||||||
|
<ul class="nav nav-pills float-right">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/hosts">Hosts <span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/logs">Logs</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="" onclick="logOut()">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<h3 class="text-muted">TheBBCloud DynDNS</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
{{template "content" .}}
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© TheBBCloud 2020</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
<!-- Bootstrap core JavaScript
|
||||||
|
================================================== -->
|
||||||
|
<!-- Placed at the end of the document so the pages load faster -->
|
||||||
|
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
|
||||||
|
<script src="/static/js/ie10-viewport-bug-workaround.js"></script>
|
||||||
|
<script src="/static/js/jquery-3.4.1.min.js"></script>
|
||||||
|
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/static/js/additional.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
dyndns/views/listhosts.html
Normal file
27
dyndns/views/listhosts.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{{define "content"}}
|
||||||
|
<div class="container marketing">
|
||||||
|
<h3 class="text-center mb-4">DNS Host Entries</h3>
|
||||||
|
<table class="table table-striped text-center">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>TTL</th>
|
||||||
|
<th>LastUpdate</th>
|
||||||
|
<th><button class="btn btn-primary" onclick="location.href='/hosts/add'">Add Host Entry</button></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .hosts}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Hostname}}.{{$.config.Domain}}</td>
|
||||||
|
<td>{{.Ip}}</td>
|
||||||
|
<td>{{.Ttl}}</td>
|
||||||
|
<td>{{.LastUpdate.Format "01/02/2006 15:04 MEZ"}}</td>
|
||||||
|
<td><button onclick="location.href='/hosts/edit/{{.ID}}'" class="btn btn-outline-secondary btn-sm"><img src="/static/icons/pencil.svg" alt="" width="16" height="16" title="Edit"></button> <button class="btn btn-outline-secondary btn-sm" onclick="deleteHost('{{.ID}}')"><img src="/static/icons/trash.svg" alt="" width="16" height="16" title="Delete"></button> <button class="btn btn-outline-secondary btn-sm" onclick="location.href='/logs/host/{{.ID}}'"><img src="/static/icons/table.svg" alt="" width="16" height="16" title="Logs"></button></td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
29
dyndns/views/listlogs.html
Normal file
29
dyndns/views/listlogs.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{{define "content"}}
|
||||||
|
<div class="container marketing">
|
||||||
|
<h3 class="text-center mb-4">Log Entries</h3>
|
||||||
|
<table class="table table-striped text-center" style="font-size: 14px">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>IP sent</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>User Agent</th>
|
||||||
|
<th>Caller IP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .logs}}
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle mx-auto"><div class="{{if .Status}}bg-success{{else}}bg-danger{{end}}" style="width: 16px; height: 16px; margin: auto"></div></td>
|
||||||
|
<td>{{.Host.Hostname}}.{{$.config.Domain}}</td>
|
||||||
|
<td>{{.SentIP}}</td>
|
||||||
|
<td>{{.CreatedAt.Format "01/02/2006 15:04"}}</td>
|
||||||
|
<td>{{.UserAgent}}</td>
|
||||||
|
<td>{{.CallerIP}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue
Block a user