initial commit
This commit is contained in:
parent
4a3673e0d3
commit
c4809b85b4
|
@ -11,5 +11,14 @@
|
|||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# dont want to share go.sum
|
||||
go.sum
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
|
||||
# skip the database folder
|
||||
database
|
|
@ -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
|
||||
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
options {
|
||||
directory "/var/cache/bind";
|
||||
dnssec-validation auto;
|
||||
recursion no;
|
||||
allow-transfer { none; };
|
||||
auth-nxdomain no;
|
||||
listen-on-v6 { any; };
|
||||
};
|
|
@ -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/*
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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 */
|
File diff suppressed because one or more lines are too long
|
@ -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 */
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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)
|
||||
}
|
||||
|
||||
}())
|
File diff suppressed because one or more lines are too long
|
@ -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}}
|
|
@ -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}}
|
|
@ -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>
|
|
@ -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}}
|
|
@ -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