Basic gin site

This commit is contained in:
aceberg 2024-06-30 01:17:35 +07:00
parent 8826bd8cb6
commit 0c410d9458
59 changed files with 640 additions and 2062 deletions

View file

View file

@ -3,19 +3,18 @@ package main
import (
"flag"
"github.com/aceberg/WatchYourLAN/internal/check"
_ "time/tzdata"
"github.com/aceberg/WatchYourLAN/internal/web"
)
const configPath = "/data/config.yaml"
const dirPath = "/data/WatchYourLAN"
const nodePath = ""
func main() {
confPtr := flag.String("c", configPath, "Path to config file")
dirPtr := flag.String("d", dirPath, "Path to config dir")
nodePtr := flag.String("n", nodePath, "Path to node modules")
flag.Parse()
check.Path(*confPtr)
web.Gui(*confPtr, *nodePtr) // webgui.go
web.Gui(*dirPtr, *nodePtr) // webgui.go
}

55
go.mod
View file

@ -1,28 +1,34 @@
module github.com/aceberg/WatchYourLAN
go 1.21.4
go 1.22.4
require (
github.com/containrrr/shoutrrr v0.8.0
github.com/google/uuid v1.5.0
github.com/jmoiron/sqlx v1.3.5
github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.17.0
modernc.org/sqlite v1.28.0
github.com/gin-gonic/gin v1.10.0
github.com/spf13/viper v1.19.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@ -30,22 +36,17 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.29.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)

160
go.sum
View file

@ -1,68 +1,68 @@
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
@ -77,41 +77,47 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -120,31 +126,5 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View file

@ -1,35 +0,0 @@
package auth
import (
"net/http"
"time"
)
// Auth - main auth func
func Auth(next http.HandlerFunc, conf *Conf) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !conf.Auth || conf.User == "" || conf.Password == "" {
next.ServeHTTP(w, r)
return
}
sessionToken := getTokenFromCookie(r)
userSession, exists := allSessions[sessionToken]
if !exists {
http.Redirect(w, r, "/login/", 302)
return
}
if userSession.Before(time.Now()) {
delete(allSessions, sessionToken)
http.Redirect(w, r, "/login/", 302)
return
}
userSession = time.Now().Add(conf.Expire)
allSessions[sessionToken] = userSession
next.ServeHTTP(w, r)
}
}

View file

@ -1,22 +0,0 @@
package auth
import (
"net/http"
)
func setTokenCookie(w http.ResponseWriter, token string) {
cookie := http.Cookie{Name: cookieName, Value: token, Path: "/"}
http.SetCookie(w, &cookie)
}
func getTokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie(cookieName)
if err != nil {
return ""
}
return cookie.Value
}

View file

@ -1,27 +0,0 @@
package auth
import (
"log"
"golang.org/x/crypto/bcrypt"
)
// HashPassword - generate hash from password
func HashPassword(pw string) string {
hashed, err := bcrypt.GenerateFromPassword([]byte(pw), 10)
if err != nil {
log.Println("PASSWORD ERROR:", err)
return ""
}
return string(hashed)
}
// MatchPasswords - check if password and hash matches
func MatchPasswords(hash, pw string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
return err == nil
}

View file

@ -1,18 +0,0 @@
package auth
import (
"time"
)
// Conf - auth config
type Conf struct {
Auth bool
User string
Password string
ExpStr string
Expire time.Duration
}
var allSessions = map[string]time.Time{}
var cookieName = "wyl_session_token"

View file

@ -1,33 +0,0 @@
package auth
import (
"net/http"
"time"
"github.com/google/uuid"
)
// StartSession for new login
func StartSession(w http.ResponseWriter, r *http.Request) {
sessionToken := uuid.NewString()
allSessions[sessionToken] = time.Now().Add(60 * time.Second)
setTokenCookie(w, sessionToken)
http.Redirect(w, r, "/", 302)
}
// LogOut - log out
func LogOut(w http.ResponseWriter, r *http.Request) {
sessionToken := getTokenFromCookie(r)
delete(allSessions, sessionToken)
setTokenCookie(w, "")
http.Redirect(w, r, "/", 302)
}

View file

@ -1,46 +0,0 @@
package auth
import (
"errors"
"time"
)
func timeParse(timeout string) (time.Duration, error) {
var err error
var t time.Duration
length := len(timeout)
if length > 1 {
suffix := timeout[length-1]
switch string(suffix) {
case "h":
t, err = time.ParseDuration(timeout)
case "m":
t, err = time.ParseDuration(timeout)
case "d": //day
t, err = time.ParseDuration(timeout[:length-1] + "h")
t = 24 * t
case "M": // month
t, err = time.ParseDuration(timeout[:length-1] + "h")
t = 730 * t
default:
err = errors.New("ERROR: TimeParse: wrong time format")
}
} else {
err = errors.New("ERROR: TimeParse: wrong time format")
}
return t, err
}
// ToTime - converts string (example: 3d) to time.Duration
func ToTime(s string) time.Duration {
t, err := timeParse(s)
if err != nil {
t, _ = timeParse("7d")
}
return t
}

View file

@ -1,7 +1,8 @@
package check
import (
"log"
"fmt"
"log/slog"
)
// IfError prints error, if it is not nil
@ -10,6 +11,6 @@ func IfError(err error) bool {
return false
}
log.Println("ERROR:", err)
slog.Error(fmt.Sprintf("%v", err))
return true
}

View file

@ -1,88 +0,0 @@
package conf
import (
"github.com/spf13/viper"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// Get - get app config
func Get(path string) (config models.Conf, authConf auth.Conf) {
viper.SetDefault("IFACE", "enp1s0")
viper.SetDefault("DBPATH", "/data/db.sqlite")
viper.SetDefault("GUIIP", "0.0.0.0")
viper.SetDefault("GUIPORT", "8840")
viper.SetDefault("TIMEOUT", "60")
viper.SetDefault("SHOUTRRR_URL", "")
viper.SetDefault("THEME", "solar")
viper.SetDefault("COLOR", "dark")
viper.SetDefault("IGNOREIP", "no")
viper.SetDefault("LOGLEVEL", "verbose")
viper.SetDefault("HISTORY_DAYS", "30")
viper.SetDefault("ARP_TIMEOUT", "500")
viper.SetDefault("AUTH_USER", "")
viper.SetDefault("AUTH_PASSWORD", "")
viper.SetDefault("AUTH_EXPIRE", "7d")
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
check.IfError(err)
viper.AutomaticEnv() // Get ENVIRONMENT variables
config.Iface = viper.Get("IFACE").(string)
config.DbPath = viper.Get("DBPATH").(string)
config.GuiIP = viper.Get("GUIIP").(string)
config.GuiPort = viper.Get("GUIPORT").(string)
config.Timeout = viper.GetInt("TIMEOUT")
config.ShoutURL = viper.Get("SHOUTRRR_URL").(string)
config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(string)
config.IgnoreIP = viper.Get("IGNOREIP").(string)
config.LogLevel = viper.Get("LOGLEVEL").(string)
config.HistDays = viper.Get("HISTORY_DAYS").(string)
config.ArpTimeout = viper.Get("ARP_TIMEOUT").(string)
authConf.Auth = viper.GetBool("AUTH")
authConf.User, _ = viper.Get("AUTH_USER").(string)
authConf.Password, _ = viper.Get("AUTH_PASSWORD").(string)
authConf.ExpStr, _ = viper.Get("AUTH_EXPIRE").(string)
authConf.Expire = auth.ToTime(authConf.ExpStr)
config.Auth = authConf.Auth
return config, authConf
}
// Write - write config to file
func Write(path string, config models.Conf, authConf auth.Conf) {
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
viper.Set("IFACE", config.Iface)
viper.Set("DBPATH", config.DbPath)
viper.Set("GUIIP", config.GuiIP)
viper.Set("GUIPORT", config.GuiPort)
viper.Set("TIMEOUT", config.Timeout)
viper.Set("SHOUTRRR_URL", config.ShoutURL)
viper.Set("THEME", config.Theme)
viper.Set("COLOR", config.Color)
viper.Set("IGNOREIP", config.IgnoreIP)
viper.Set("LOGLEVEL", config.LogLevel)
viper.Set("HISTORY_DAYS", config.HistDays)
viper.Set("ARP_TIMEOUT", config.ArpTimeout)
viper.Set("auth", authConf.Auth)
viper.Set("auth_user", authConf.User)
viper.Set("auth_password", authConf.Password)
viper.Set("auth_expire", authConf.ExpStr)
err := viper.WriteConfig()
check.IfError(err)
}

View file

@ -0,0 +1,46 @@
package conf
import (
"github.com/spf13/viper"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// Get - get app config
func Get(path string) (config models.Conf) {
viper.SetDefault("HOST", "0.0.0.0")
viper.SetDefault("PORT", "8840")
viper.SetDefault("THEME", "solar")
viper.SetDefault("COLOR", "dark")
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
check.IfError(err)
viper.AutomaticEnv() // Get ENVIRONMENT variables
config.Host = viper.Get("HOST").(string)
config.Port = viper.Get("PORT").(string)
config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(string)
return config
}
// Write - write config to file
func Write(config models.Conf) {
viper.SetConfigFile(config.ConfPath)
viper.SetConfigType("yaml")
viper.Set("HOST", config.Host)
viper.Set("PORT", config.Port)
viper.Set("THEME", config.Theme)
viper.Set("COLOR", config.Color)
err := viper.WriteConfig()
check.IfError(err)
}

View file

@ -1,73 +0,0 @@
package db
import (
"fmt"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// Create - create DB if not exists
func Create(path string) {
sqlStatement := `CREATE TABLE IF NOT EXISTS "now" (
"ID" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
"NAME" TEXT NOT NULL,
"IP" TEXT,
"MAC" TEXT,
"HW" TEXT,
"DATE" TEXT,
"KNOWN" INTEGER DEFAULT 0,
"NOW" INTEGER DEFAULT 0
);`
dbExec(path, sqlStatement)
sqlStatement = `CREATE TABLE IF NOT EXISTS "history" (
"ID" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
"HOST" INTEGER DEFAULT 0,
"NAME" TEXT NOT NULL,
"IP" TEXT,
"MAC" TEXT,
"HW" TEXT,
"DATE" TEXT,
"KNOWN" INTEGER DEFAULT 0,
"STATE" INTEGER DEFAULT 0
);`
dbExec(path, sqlStatement)
}
// Insert - insert host into table
func Insert(path string, oneHost models.Host) {
oneHost.Name = quoteStr(oneHost.Name)
oneHost.Hw = quoteStr(oneHost.Hw)
sqlStatement := `INSERT INTO "now" (NAME, IP, MAC, HW, DATE, KNOWN, NOW)
VALUES ('%s','%s','%s','%s','%s','%d','%d');`
sqlStatement = fmt.Sprintf(sqlStatement, oneHost.Name, oneHost.IP, oneHost.Mac, oneHost.Hw, oneHost.Date, oneHost.Known, oneHost.Now)
dbExec(path, sqlStatement)
}
// Update - update host
func Update(path string, oneHost models.Host) {
oneHost.Name = quoteStr(oneHost.Name)
oneHost.Hw = quoteStr(oneHost.Hw)
sqlStatement := `UPDATE "now" set
NAME = '%s', IP = '%s', MAC = '%s', HW = '%s', DATE = '%s',
KNOWN = '%d', NOW = '%d'
WHERE ID = '%d';`
sqlStatement = fmt.Sprintf(sqlStatement, oneHost.Name, oneHost.IP, oneHost.Mac, oneHost.Hw, oneHost.Date, oneHost.Known, oneHost.Now, oneHost.ID)
dbExec(path, sqlStatement)
}
// Delete - delete host from DB
func Delete(path string, id int) {
sqlStatement := `DELETE FROM "now" WHERE ID='%d';`
sqlStatement = fmt.Sprintf(sqlStatement, id)
dbExec(path, sqlStatement)
}
// Clear - delete all hosts from table
func Clear(path string) {
sqlStatement := `DELETE FROM "now";`
dbExec(path, sqlStatement)
}

View file

@ -1,31 +0,0 @@
package db
import (
"fmt"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// InsertHist - insert history into table
func InsertHist(path string, hist models.History) {
hist.Name = quoteStr(hist.Name)
hist.Hw = quoteStr(hist.Hw)
sqlStatement := `INSERT INTO "history" (HOST, NAME, IP, MAC, HW, DATE, KNOWN, STATE)
VALUES ('%d','%s','%s','%s','%s','%s','%d','%d');`
sqlStatement = fmt.Sprintf(sqlStatement, hist.Host, hist.Name, hist.IP, hist.Mac, hist.Hw, hist.Date, hist.Known, hist.State)
//fmt.Println("Insert statement:", sqlStatement)
dbExec(path, sqlStatement)
}
// DeleteHist - delete history from DB
func DeleteHist(path string, id int) {
sqlStatement := `DELETE FROM "history" WHERE ID='%d';`
sqlStatement = fmt.Sprintf(sqlStatement, id)
dbExec(path, sqlStatement)
}
// ClearHist - delete all history from table
func ClearHist(path string) {
sqlStatement := `DELETE FROM "history";`
dbExec(path, sqlStatement)
}

View file

@ -1,7 +0,0 @@
package db
import "strings"
func quoteStr(str string) string {
return strings.ReplaceAll(str, "'", "''")
}

View file

@ -1,62 +0,0 @@
package db
import (
"sync"
"github.com/jmoiron/sqlx"
// Import sqlite module
_ "modernc.org/sqlite"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var mu sync.Mutex
func dbExec(path, sqlStatement string) {
mu.Lock()
db, err := sqlx.Connect("sqlite", path)
check.IfError(err)
defer db.Close()
_, err = db.Exec(sqlStatement)
mu.Unlock()
check.IfError(err)
}
// Select - select all hosts
func Select(path string) (dbHosts []models.Host) {
sqlStatement := `SELECT * FROM "now" ORDER BY DATE DESC`
mu.Lock()
db, _ := sqlx.Connect("sqlite", path)
defer db.Close()
err := db.Select(&dbHosts, sqlStatement)
mu.Unlock()
check.IfError(err)
return dbHosts
}
// SelectHist - select all history
func SelectHist(path string) (hist []models.History) {
sqlStatement := `SELECT * FROM "history" ORDER BY ID DESC`
mu.Lock()
db, _ := sqlx.Connect("sqlite", path)
defer db.Close()
err := db.Select(&hist, sqlStatement)
mu.Unlock()
check.IfError(err)
return hist
}

View file

@ -1,44 +0,0 @@
package migrate
import (
"github.com/spf13/viper"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func getOldConfig(path string) (config models.Conf) {
viper.SetDefault("IFACE", "enp1s0")
viper.SetDefault("DBPATH", "/data/db.sqlite")
viper.SetDefault("GUIIP", "localhost")
viper.SetDefault("GUIPORT", "8840")
viper.SetDefault("TIMEOUT", "60")
viper.SetDefault("SHOUTRRR_URL", "")
viper.SetDefault("THEME", "solar")
viper.SetDefault("COLOR", "light")
viper.SetDefault("IGNOREIP", "no")
viper.SetDefault("LOGLEVEL", "verbose")
viper.SetConfigFile(path)
viper.SetConfigType("env")
viper.AutomaticEnv() // Get ENVIRONMENT variables
err := viper.ReadInConfig()
if err == nil {
config.Iface = viper.Get("IFACE").(string)
config.DbPath = viper.Get("DBPATH").(string)
config.GuiIP = viper.Get("GUIIP").(string)
config.GuiPort = viper.Get("GUIPORT").(string)
config.Timeout = viper.GetInt("TIMEOUT")
config.ShoutURL = viper.Get("SHOUTRRR_URL").(string)
config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(string)
config.IgnoreIP = viper.Get("IGNOREIP").(string)
config.LogLevel = viper.Get("LOGLEVEL").(string)
}
return config
}

View file

@ -1,52 +0,0 @@
package migrate
import (
"fmt"
"log"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/notify"
)
// ToYAML - check if config file is YAML and migrate from ENV-file if not
func ToYAML(path string) string {
if check.IsYaml(path) {
if check.IsEmpty(path) {
oldPath := path[:len(path)-5] // path w/o .yaml
if !check.IsEmpty(oldPath) {
migrateConfig(oldPath, path)
}
}
} else {
newPath := path + ".yaml"
check.Path(newPath)
if check.IsEmpty(newPath) && !check.IsEmpty(path) {
migrateConfig(path, newPath)
}
path = newPath
}
return path
}
func migrateConfig(oldPath, newPath string) {
var authConf auth.Conf
config := getOldConfig(oldPath)
conf.Write(newPath, config, authConf)
msg := fmt.Sprintf("Config file migrated from ENV to YAML format. \nOld config file %s is no longer used and can be deleted. \nNew config file is %s, please update your settings.", oldPath, newPath)
log.Println("=================================== ")
log.Println("WARNING:", msg)
log.Println("=================================== ")
notify.Shoutrrr("WatchYourLAN: "+msg, config.ShoutURL)
}

View file

@ -1,26 +1,14 @@
package models
import (
"github.com/aceberg/WatchYourLAN/internal/auth"
)
// Conf - app config
type Conf struct {
Iface string
DbPath string
GuiIP string
GuiPort string
Timeout int
ShoutURL string
Host string
Port string
Theme string
Color string
IgnoreIP string
LogLevel string
DirPath string
ConfPath string
NodePath string
Icon string
Auth bool
HistDays string
ArpTimeout string
}
// Host - one host
@ -54,6 +42,5 @@ type GuiData struct {
Hosts []Host
Themes []string
Version string
Auth auth.Conf
Hist []History
}

View file

@ -1,16 +0,0 @@
package notify
import (
"github.com/containrrr/shoutrrr"
"log"
)
// Shoutrrr - send message with shoutrrr
func Shoutrrr(message string, url string) {
if url != "" {
err := shoutrrr.Send(url, message)
if err != nil {
log.Println("ERROR: Notification failed (shoutrrr):", err)
}
}
}

View file

@ -1,42 +0,0 @@
package port
import (
"fmt"
"net"
"time"
)
func isOpen(host, proto string, port int) bool {
timeout := 3 * time.Second
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout(proto, target, timeout)
if err != nil {
return false
}
if conn != nil {
conn.Close()
return true
}
return false
}
// Scan - scan all TCP ports of a host
func Scan(host, proto string, begin, end int) []string {
var onePort string
var ports []string
ports = []string{}
for i := begin; i < end+1; i++ {
if isOpen(host, proto, i) {
onePort = fmt.Sprintf("%s:%d", host, i)
ports = append(ports, onePort)
}
}
return ports
}

View file

@ -1,59 +0,0 @@
package scan
import (
"log"
"os/exec"
"strings"
"time"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func scanIface(iface string) string {
cmd, err := exec.Command("arp-scan", "-glNx", "-t", appConfig.ArpTimeout, "-I", iface).Output()
if err != nil {
return string("")
}
return string(cmd)
}
func parseOutput(text string) []models.Host {
var foundHosts = []models.Host{}
perString := strings.Split(text, "\n")
currentTime := time.Now()
for _, host := range perString {
if host != "" {
var oneHost models.Host
p := strings.Split(host, " ")
oneHost.IP = p[0]
oneHost.Mac = p[1]
oneHost.Hw = p[2]
oneHost.Date = currentTime.Format("2006-01-02 15:04:05")
oneHost.Now = 1
foundHosts = append(foundHosts, oneHost)
}
}
return foundHosts
}
// Scan all interfaces
func arpScan() []models.Host {
var text string
var foundHosts = []models.Host{}
perString := strings.Split(appConfig.Iface, " ")
for _, iface := range perString {
text = scanIface(iface)
if appConfig.LogLevel != "short" {
log.Println("INFO: scanning interface", iface)
log.Println("INFO: found IPs:", text)
}
foundHosts = append(foundHosts, parseOutput(text)...)
}
return foundHosts
}

View file

@ -1,79 +0,0 @@
package scan
import (
"fmt"
"log"
"time"
"github.com/aceberg/WatchYourLAN/internal/db"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/notify"
)
func hostsCompare() {
for _, oneHost := range dbHosts {
host, exists := foundHostsMap[oneHost.Mac]
if exists && (appConfig.IgnoreIP == "yes" || host.IP == oneHost.IP) {
oneHost.Date = host.Date
if oneHost.Now == 0 {
histAdd(oneHost, 1)
}
oneHost.Now = 1
delete(foundHostsMap, oneHost.Mac)
db.Update(appConfig.DbPath, oneHost)
} else if oneHost.Now == 1 {
oneHost.Now = 0
oneHost.Date = time.Now().Format("2006-01-02 15:04:05")
db.Update(appConfig.DbPath, oneHost)
histAdd(oneHost, 0)
}
}
for _, oneHost := range foundHostsMap {
msg := fmt.Sprintf("UNKNOWN HOST IP: '%s', MAC: '%s', Hw: '%s'", oneHost.IP, oneHost.Mac, oneHost.Hw)
log.Println("WARN:", msg)
notify.Shoutrrr(msg, appConfig.ShoutURL) // Notify through Shoutrrr
db.Insert(appConfig.DbPath, oneHost)
histAdd(oneHost, 1)
}
}
func histAdd(oneHost models.Host, state int) {
var history models.History
if oneHost.ID == 0 {
dbHosts = db.Select(appConfig.DbPath)
for _, host := range dbHosts {
if host.IP == oneHost.IP && host.Mac == oneHost.Mac {
oneHost.ID = host.ID
break
}
}
}
history.Host = oneHost.ID
history.Name = oneHost.Name
history.IP = oneHost.IP
history.Mac = oneHost.Mac
history.Hw = oneHost.Hw
history.Date = oneHost.Date
history.Known = oneHost.Known
history.State = state
db.InsertHist(appConfig.DbPath, history)
}

View file

@ -1,51 +0,0 @@
package scan
import (
"time"
"github.com/aceberg/WatchYourLAN/internal/db"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var appConfig models.Conf
var dbHosts, structHosts []models.Host
var foundHostsMap map[string]models.Host
// Start - start arp-scan goroutine
func Start(config models.Conf, quit chan bool) {
var lastDate time.Time
appConfig = config
db.Create(appConfig.DbPath)
dbHosts = db.Select(appConfig.DbPath)
for {
select {
case <-quit:
return
default:
nowDate := time.Now()
plusDate := lastDate.Add(time.Duration(appConfig.Timeout) * time.Second)
if nowDate.After(plusDate) {
structHosts = arpScan() // arpscan.go
dbHosts = db.Select(appConfig.DbPath)
toMap()
hostsCompare() // compare.go
lastDate = time.Now()
}
time.Sleep(time.Duration(1) * time.Second)
}
}
}
func toMap() {
foundHostsMap = make(map[string]models.Host)
for _, host := range structHosts {
foundHostsMap[host.Mac] = host
}
}

View file

@ -1,52 +0,0 @@
package web
import (
"log"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func authConfHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
guiData.Auth.Auth = authConf.Auth
guiData.Auth.User = authConf.User
guiData.Auth.ExpStr = authConf.ExpStr
execTemplate(w, "auth", guiData)
}
func saveAuthHandler(w http.ResponseWriter, r *http.Request) {
authConf.User = r.FormValue("user")
authConf.ExpStr = r.FormValue("expire")
authStr := r.FormValue("auth")
pw := r.FormValue("password")
if authStr == "on" {
authConf.Auth = true
} else {
authConf.Auth = false
}
AppConfig.Auth = authConf.Auth
if pw != "" {
authConf.Password = auth.HashPassword(pw)
}
authConf.Expire = auth.ToTime(authConf.ExpStr)
if authConf.Auth && (authConf.User == "" || authConf.Password == "") {
log.Println("WARNING: Auth won't work with empty login or password.")
authConf.Auth = false
}
conf.Write(ConfigPath, AppConfig, authConf)
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}

View file

@ -1,80 +1,52 @@
package web
import (
"log"
"log/slog"
"net/http"
"strconv"
// "strconv"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/db"
// "github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/notify"
"github.com/aceberg/WatchYourLAN/internal/scan"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
func configHandler(c *gin.Context) {
var guiData models.GuiData
guiData.Config = AppConfig
guiData.Config = appConfig
file, err := TemplHTML.ReadFile(TemplPath + "version")
guiData.Themes = []string{"cerulean", "cosmo", "cyborg", "darkly", "emerald", "flatly", "grass", "grayscale", "journal", "litera", "lumen", "lux", "materia", "minty", "morph", "ocean", "pulse", "quartz", "sand", "sandstone", "simplex", "sketchy", "slate", "solar", "spacelab", "superhero", "united", "vapor", "wood", "yeti", "zephyr"}
file, err := pubFS.ReadFile("public/version")
check.IfError(err)
version := string(file)
guiData.Version = version[8:]
guiData.Themes = []string{"cerulean", "cosmo", "cyborg", "darkly", "emerald", "flatly", "grass", "journal", "litera", "lumen", "lux", "materia", "minty", "morph", "pulse", "quartz", "sand", "sandstone", "simplex", "sketchy", "slate", "solar", "spacelab", "superhero", "united", "vapor", "yeti", "zephyr"}
execTemplate(w, "config", guiData)
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "config.html", guiData)
}
func saveConfigHandler(w http.ResponseWriter, r *http.Request) {
var err error
func saveConfigHandler(c *gin.Context) {
AppConfig.Iface = r.FormValue("iface")
AppConfig.DbPath = r.FormValue("dbpath")
AppConfig.GuiIP = r.FormValue("host")
AppConfig.GuiPort = r.FormValue("port")
AppConfig.ShoutURL = r.FormValue("shout")
AppConfig.Theme = r.FormValue("theme")
AppConfig.Color = r.FormValue("color")
AppConfig.IgnoreIP = r.FormValue("ignoreip")
AppConfig.LogLevel = r.FormValue("loglevel")
AppConfig.HistDays = r.FormValue("history")
AppConfig.ArpTimeout = r.FormValue("arp_timeout")
// appConfig.Host = c.PostForm("host")
// appConfig.Port = c.PostForm("port")
// appConfig.Theme = c.PostForm("theme")
// appConfig.Color = c.PostForm("color")
// tStr := c.PostForm("timeout")
// hStr := c.PostForm("trim")
timeout := r.FormValue("timeout")
AppConfig.Timeout, err = strconv.Atoi(timeout)
check.IfError(err)
// if tStr != "" {
// appConfig.Timeout, _ = strconv.Atoi(tStr)
// }
// if hStr != "" {
// appConfig.HistTrim, _ = strconv.Atoi(hStr)
// }
close(QuitScan)
// conf.Write(appConfig)
conf.Write(ConfigPath, AppConfig, authConf)
slog.Info("writing new config to " + appConfig.ConfPath)
log.Println("INFO: writing new config")
QuitScan = make(chan bool)
go scan.Start(AppConfig, QuitScan)
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
func clearHandler(w http.ResponseWriter, r *http.Request) {
log.Println("INFO: delting all hosts from DB")
db.Clear(AppConfig.DbPath)
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
func testNotifyHandler(w http.ResponseWriter, r *http.Request) {
log.Println("INFO: Test notification send")
msg := "Test notification"
notify.Shoutrrr(msg, AppConfig.ShoutURL)
http.Redirect(w, r, r.Header.Get("Referer"), 302)
c.Redirect(http.StatusFound, "/config")
}

File diff suppressed because one or more lines are too long

View file

@ -3,30 +3,20 @@ package web
import (
"embed"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var (
authConf auth.Conf
// AppConfig - app config
AppConfig models.Conf
// AllHosts - all hosts from DB
AllHosts []models.Host
// ConfigPath - path to config file
ConfigPath string
// QuitScan - send stop signal to scan
QuitScan chan bool
// TemplHTML - embed templates
//
//go:embed templates/*
TemplHTML embed.FS
// appConfig - config for Web Gui
appConfig models.Conf
)
// TemplPath - path to html templates
const TemplPath = "templates/"
// templFS - html templates
//
//go:embed templates/*
var templFS embed.FS
// pubFS - public folder
//
//go:embed public/*
var pubFS embed.FS

View file

@ -1,19 +0,0 @@
package web
import (
"html/template"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func execTemplate(w http.ResponseWriter, tpl string, guiData models.GuiData) {
// tmpl, err := template.ParseFiles(TemplPath+tpl+".html", TemplPath+"header.html", TemplPath+"footer.html")
tmpl, err := template.ParseFS(TemplHTML, TemplPath+tpl+".html", TemplPath+"header.html", TemplPath+"footer.html")
check.IfError(err)
err = tmpl.ExecuteTemplate(w, "header", guiData)
check.IfError(err)
err = tmpl.ExecuteTemplate(w, tpl, guiData)
check.IfError(err)
}

View file

@ -1,17 +0,0 @@
package web
import (
"net/http"
"github.com/aceberg/WatchYourLAN/internal/db"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func historyHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
guiData.Hist = db.SelectHist(AppConfig.DbPath)
execTemplate(w, "history", guiData)
}

View file

@ -1,60 +0,0 @@
package web
import (
"log"
"net"
"net/http"
"strconv"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/db"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func delHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.FormValue("id")
id, err := strconv.Atoi(idStr)
check.IfError(err)
log.Println("INFO: delete host ID =", id)
db.Delete(AppConfig.DbPath, id)
http.Redirect(w, r, "/", 302)
}
func hostHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
var host models.Host
guiData.Config = AppConfig
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
check.IfError(err)
for _, oneHost := range AllHosts {
if id == oneHost.ID {
host = oneHost
break
}
}
guiData.Hosts = append(guiData.Hosts, host)
addr, err := net.LookupAddr(host.IP)
check.IfError(err)
guiData.Themes = addr
history := db.SelectHist(AppConfig.DbPath)
for _, hist := range history {
if hist.Host == id {
guiData.Hist = append(guiData.Hist, hist)
}
}
execTemplate(w, "host", guiData)
}

View file

@ -3,16 +3,15 @@ package web
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
func indexHandler(c *gin.Context) {
var guiData models.GuiData
guiData.Config = appConfig
updateAllHosts()
guiData.Config = AppConfig
guiData.Hosts = AllHosts
execTemplate(w, "index", guiData)
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "index.html", guiData)
}

View file

@ -1,27 +0,0 @@
package web
import (
"net/http"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func lineHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
guiData.Hosts = []models.Host{}
state := r.URL.Query().Get("state")
for _, oneHost := range AllHosts {
if state == "off" && oneHost.Now == 0 {
guiData.Hosts = append(guiData.Hosts, oneHost)
}
if state == "on" && oneHost.Now == 1 {
guiData.Hosts = append(guiData.Hosts, oneHost)
}
}
execTemplate(w, "line", guiData)
}

View file

@ -1,31 +0,0 @@
package web
import (
"log"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
username := r.FormValue("username")
password := r.FormValue("password")
logout := r.FormValue("logout")
if username == authConf.User && auth.MatchPasswords(authConf.Password, password) {
log.Println("INFO: user '"+username+"' logged in. Session expire time", authConf.Expire)
auth.StartSession(w, r)
}
if logout == "yes" {
auth.LogOut(w, r)
}
execTemplate(w, "login", guiData)
}

View file

@ -1,30 +0,0 @@
package web
import (
"net/http"
"strconv"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/port"
)
func portHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
ip := r.FormValue("ip")
beginStr := r.FormValue("begin")
endStr := r.FormValue("end")
begin, err := strconv.Atoi(beginStr)
check.IfError(err)
end, err := strconv.Atoi(endStr)
check.IfError(err)
guiData.Themes = port.Scan(ip, "tcp", begin, end)
execTemplate(w, "port", guiData)
}

View file

@ -0,0 +1,9 @@
.my-button {
height: 100%;
background-color: #00000000;
color: var(--bs-primary);
text-align: center;
}
.my-button:hover {
background-color: #0000001a;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,45 @@
var histArray = {};
loadHistory();
function createHTML(hist, i) {
let allState = "";
let color = "";
for (let state of hist.State){
if (state.State) {
color = `bi-check-circle-fill" style="color:var(--bs-success);"`;
} else {
color = `bi-dash-circle-fill" style="color:var(--bs-danger);"`;
}
allState = allState + `<i class="bi ${color} title="${state.Date}"></i>`;
}
let html = `
<tr>
<td style="opacity: 45%;">${i}.</td>
<td><a href="/scan/?addr=${hist.Addr}">${hist.Name}</a></td>
<td><a href="/scan/?addr=${hist.Addr}">${hist.Addr}</a></td>
<td><a href="http://${hist.Addr}:${hist.Port}">${hist.Port}</a></td>
<td><a href="http://${hist.Addr}:${hist.Port}">${hist.PortName}</a></td>
<td>${allState}</td>
</tr>`;
return html;
}
async function loadHistory() {
let url = '/api/history';
let histMap = await (await fetch(url)).json();
if (histMap != null) {
histArray = Object.values(histMap);
}
displayArrayData(histArray);
}
function sortBy(field) {
sortByAny(histArray, field);
}

View file

@ -0,0 +1,46 @@
var addrsArray = {};
loadAddrs();
function createHTML(addr, i) {
let off = '';
if (addr.Offline == 0) {
off = `<span style="color:var(--bs-success);">0</span>`;
} else {
off = `<span style="color:var(--bs-danger);">${addr.Offline}</span>`;
}
let html = `
<tr>
<td style="opacity: 45%;">${i}.</td>
<td>
<a href="/scan/?addr=${addr.Addr}">${addr.Name}</a>
</td>
<td>
<a href="/scan/?addr=${addr.Addr}">${addr.Addr}</a>
</td>
<td>${addr.Total}</td>
<td>${addr.Watching}</td>
<td>${addr.Online}</td>
<td>${off}</td>
</tr>
`;
return html;
}
async function loadAddrs() {
let url = '/api/all';
let addrsMap = await (await fetch(url)).json();
if (addrsMap != null) {
addrsArray = Object.values(addrsMap);
}
displayArrayData(addrsArray);
}
function sortBy(field) {
sortByAny(addrsArray, field);
}

View file

@ -0,0 +1,125 @@
var addr = '';
var stop = false;
var portMap = {};
var portArray = [];
// Run, when page loadad
window.addEventListener('load', function() {
loadSavedPorts();
});
function delRow(id) {
document.getElementById(id).innerHTML = "";
}
function stopScan() {
stop = true;
}
async function scanAddr() {
let begin = document.getElementById("begin").value;
let end = document.getElementById("end").value;
let savedPorts = [];
if (begin == "") {
begin = 1
}
if (end == "") {
end = 65535
}
let port = {};
stop = false;
if (portMap != null) {
savedPorts = Object.keys(portMap);
}
// console.log("Saved ports:", savedPorts);
document.getElementById('stopBtn').style.visibility = "visible";
for (let i = begin ; i <= end; i++) {
if (stop) {
break;
}
let url = '/api/port/'+addr+'/'+i;
port = await (await fetch(url)).json();
document.getElementById("curPort").innerHTML = "Scanning port "+i;
if ((port.State) && (!savedPorts.includes(port.Port.toString()))) {
html = createHTML(port, '', true);
document.getElementById('tBody').insertAdjacentHTML('afterbegin', html);
}
}
document.getElementById('stopBtn').style.visibility = "hidden";
}
function createHTML(port, i, found) {
let state = ``;
let checked = ``;
let sup = ``;
if (found) {
sup = `&nbsp;<sup style="background-color:var(--bs-danger);">new</sup>`;
}
if (port.Watch) {
checked = `checked`;
if (port.State) {
state = `<i class="bi bi-check-circle-fill" style="color:var(--bs-success);"></i>`;
} else {
state = `<i class="bi bi-dash-circle-fill" style="color:var(--bs-danger);"></i>`;
}
} else {
state = `<i class="bi bi-circle-fill" style="color:var(--bs-gray-500);"></i>`;
}
let html = `
<tr id="row${port.Port}">
<td style="opacity: 45%;">${i}.</td>
<td>
<input name="name" type="text" class="form-control" value="${port.Name}">
</td>
<td>
<a href="http://${addr}:${port.Port}">${port.Port}</a>${sup}
<input name="port" type="hidden" value="${port.Port}">
<input name="state" type="hidden" value="${port.State}">
</td>
<td>
${state}
</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" value="${port.Port}" name="watch" ${checked}>
</div>
</td>
<td>
<a href="#" onclick="delRow('row${port.Port}')">
<i class="bi bi-x-circle"></i>
</a>
</td>
</tr>`;
return html;
}
async function loadSavedPorts() {
addr = document.getElementById("pageAddr").value;
let url = '/api/port/'+addr;
portMap = await (await fetch(url)).json();
if (portMap != null) {
portArray = Object.values(portMap);
}
displayArrayData(portArray); // sort.js
}
function sortBy(field) {
sortByAny(portArray, field); // sort.js
}

View file

@ -0,0 +1,38 @@
var oldField = '';
function displayArrayData(someArray) {
document.getElementById('tBody').innerHTML = "";
let i = 0;
for (let item of someArray){
i = i + 1;
html = createHTML(item, i);
document.getElementById('tBody').insertAdjacentHTML('beforeend', html);
}
}
function sortByAny(someArray, field) {
// console.log("Field =", field);
if (field != oldField) {
someArray.sort(byFieldDown(field));
oldField = field;
} else {
someArray.sort(byFieldUp(field));
oldField = '';
}
if (field == 'State') {
someArray.sort(byFieldUp('Watch'));
}
displayArrayData(someArray);
}
function byFieldUp(fieldName){
return (a, b) => a[fieldName] < b[fieldName] ? 1 : -1;
}
function byFieldDown(fieldName){
return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}

View file

@ -0,0 +1 @@
VERSION=0.1.2

View file

@ -1,35 +0,0 @@
package web
import (
"net/http"
"strings"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func searchHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
search := r.FormValue("search")
updateAllHosts()
foundHosts := []models.Host{}
for _, oneHost := range AllHosts {
if inString(oneHost.Name, search) || inString(oneHost.IP, search) || inString(oneHost.Mac, search) || inString(oneHost.Hw, search) || inString(oneHost.Date, search) {
foundHosts = append(foundHosts, oneHost)
}
}
guiData.Config = AppConfig
guiData.Hosts = foundHosts
execTemplate(w, "index", guiData)
}
func inString(str1 string, str2 string) bool {
return strings.Contains(
strings.ToLower(str1),
strings.ToLower(str2),
)
}

View file

@ -1,116 +0,0 @@
package web
import (
"bytes"
"net"
"net/http"
"reflect"
"sort"
"strconv"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func sortByIPs(method string) {
type ipHost struct {
Host models.Host
IP net.IP
}
toSort := []ipHost{}
var oneSort ipHost
for _, oneHost := range AllHosts {
oneSort.Host = oneHost
oneSort.IP = net.ParseIP(oneHost.IP)
toSort = append(toSort, oneSort)
}
switch method {
case "asc":
sort.Slice(toSort, func(i, j int) bool {
return bytes.Compare(toSort[i].IP, toSort[j].IP) < 0
})
case "desc":
sort.Slice(toSort, func(i, j int) bool {
return bytes.Compare(toSort[i].IP, toSort[j].IP) > 0
})
}
AllHosts = []models.Host{}
for _, oneSort := range toSort {
AllHosts = append(AllHosts, oneSort.Host)
}
}
func sortHandler(w http.ResponseWriter, r *http.Request) {
sortMethod := r.FormValue("sort_method")
switch sortMethod {
case "name-up":
sortByField("asc", "Name")
case "name-down":
sortByField("desc", "Name")
case "ip-up":
sortByIPs("asc")
case "ip-down":
sortByIPs("desc")
case "mac-up":
sortByField("asc", "Mac")
case "mac-down":
sortByField("desc", "Mac")
case "hw-up":
sortByField("asc", "Hw")
case "hw-down":
sortByField("desc", "Hw")
case "date-up":
sortByField("asc", "Date")
case "date-down":
sortByField("desc", "Date")
case "known-up":
sortByField("asc", "Known")
case "known-down":
sortByField("desc", "Known")
default:
updateAllHosts()
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}
func sortByField(method string, field string) {
type fieldHost struct {
Host models.Host
F string
}
toSort := []fieldHost{}
var oneSort fieldHost
for _, host := range AllHosts {
oneSort.Host = host
r := reflect.ValueOf(&host)
f := reflect.Indirect(r).FieldByName(field)
if field == "Known" {
oneSort.F = strconv.FormatInt(f.Int(), 10)
} else {
oneSort.F = f.String()
}
toSort = append(toSort, oneSort)
}
switch method {
case "asc":
sort.Slice(toSort, func(i, j int) bool {
return toSort[i].F < toSort[j].F
})
case "desc":
sort.Slice(toSort, func(i, j int) bool {
return toSort[i].F > toSort[j].F
})
}
AllHosts = []models.Host{}
for _, oneSort := range toSort {
AllHosts = append(AllHosts, oneSort.Host)
}
}

View file

@ -1,53 +0,0 @@
{{ define "auth" }}
<body>
<div class="container-lg mt-5">
<div class="row">
<div class="col-md-6">
<table class="table">
<form action="/auth_save/" method="post">
<tr>
<td>Enable</td>
<td>
<div class="form-check form-switch">
{{ if .Auth.Auth }}
<input class="form-check-input" type="checkbox" name="auth" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="auth">
{{ end }}
</div>
</td>
</tr>
<tr>
<td>Expire after</td>
<td><input name="expire" type="text" class="form-control" value="{{ .Auth.ExpStr }}"></td>
</tr>
<tr>
<td>Login</td>
<td><input name="user" type="text" class="form-control" value="{{ .Auth.User }}"></td>
</tr>
<tr>
<td>New password</td>
<td><input name="password" type="password" class="form-control"></td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
</tr>
</form>
</table>
</div>
<div class="col-md-6">
<div class="alert alert-info" role="alert">
<p><b>WARNING!</b> Authentication is only safe when used over <b>https</b> and with a strong password.</p>
<p><b>Expire after</b> - when user session expires. Can be set with suffixes <b>m</b>(minute), <b>h</b>(hour), <b>d</b>(day) or <b>M</b>(month). Default: <b>7d</b>.</p>
<p><b>Login</b>. For Auth to work, login and password must not be empty.</p>
<p><b>Password</b> is stored encrypted in config file. If you forgot your password, just set <b>auth_password:</b> field to empty there.</p>
</div>
</div>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1,36 +1,21 @@
{{ define "config" }}
{{ define "config.html" }}
<body>
<div class="container-lg mt-5">
<div class="container-lg">
<div class="row">
<div class="col-md-6 table-responsive">
<table class="table">
<form action="/save_config/" method="post">
<tr>
<td>Interfaces</td>
<td><input name="iface" type="text" class="form-control" value="{{ .Config.Iface }}"></td>
</tr>
<tr>
<td>Path to DB</td>
<td><input name="dbpath" type="text" class="form-control" value="{{ .Config.DbPath }}"></td>
</tr>
<div class="col-md mt-4">
<div class="card border-primary">
<div class="card-header">Config</div>
<div class="card-body table-responsive">
<table class="table table-borderless">
<form action="/config/" method="post">
<tr>
<td>Host</td>
<td><input name="host" type="text" class="form-control" value="{{ .Config.GuiIP }}"></td>
<td><input name="host" type="text" class="form-control" value="{{ .Config.Host }}"></td>
</tr>
<tr>
<td>Port</td>
<td><input name="port" type="text" class="form-control" value="{{ .Config.GuiPort }}"></td>
</tr>
<tr>
<td>Timeout (seconds)</td>
<td><input name="timeout" type="number" class="form-control" value="{{ .Config.Timeout }}"></td>
</tr>
<tr>
<td>Shoutrrr URL</td>
<td>
<textarea name="shout" class="form-control" style="width: 100%;" rows="3" wrap="soft">{{ .Config.ShoutURL }}</textarea>
</td>
<td><input name="port" type="text" class="form-control" value="{{ .Config.Port }}"></td>
</tr>
<tr>
<td>Theme</td>
@ -50,74 +35,95 @@
</select></td>
</tr>
<tr>
<td>Ignore IP</td>
<td>Timeout (minutes)</td>
<td><input name="timeout" type="number" class="form-control" value="{{ .Config.Timeout }}"></td>
</tr>
<tr>
<td>Trim History</td>
<td><input name="trim" type="number" class="form-control" value="{{ .Config.HistTrim }}"></td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
</tr>
</form>
</table>
</div>
</div>
<div class="card border-primary mt-4">
<div class="card-header">Version</div>
<div class="card-body">
<a href="https://github.com/aceberg/WatchYourLAN/releases/tag/{{ .Version }}" target="_blank">{{ .Version }}</a>
</div>
</div>
</div>
<div class="col-md mt-4">
<div class="card border-primary">
<div class="card-header">InfluxDB2 config</div>
<div class="card-body table-responsive">
<table class="table table-borderless">
<form action="/config_influx/" method="post">
<tr>
<td>Enable</td>
<td>
<select name="ignoreip" class="form-select">
<option selected value="{{ .Config.IgnoreIP }}">{{ .Config.IgnoreIP }}</option>
<option value="yes">yes</option>
<option value="no">no</option>
</select>
<div class="form-check form-switch">
{{ if .Config.InfluxEnable }}
<input class="form-check-input" type="checkbox" name="enable" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="enable">
{{ end }}
</div>
</td>
</tr>
<tr>
<td>Log Level</td>
<td>
<select name="loglevel" class="form-select">
<option selected value="{{ .Config.LogLevel }}">{{ .Config.LogLevel }}</option>
<option value="short">short</option>
<option value="verbose">verbose</option>
</select>
</td>
<td>Address</td>
<td><input name="addr" type="text" class="form-control" value="{{ .Config.InfluxAddr }}"></td>
</tr>
<tr>
<td>Keep history (days)</td>
<td><input name="history" type="number" class="form-control" value="{{ .Config.HistDays }}"></td>
<td>Token</td>
<td><input name="token" type="text" class="form-control" value="{{ .Config.InfluxToken }}"></td>
</tr>
<tr>
<td>Arp Timeout</td>
<td>Org</td>
<td><input name="org" type="text" class="form-control" value="{{ .Config.InfluxOrg }}"></td>
</tr>
<tr>
<td>Bucket</td>
<td><input name="bucket" type="text" class="form-control" value="{{ .Config.InfluxBucket }}"></td>
</tr>
<tr>
<td>Skip TLS verify</td>
<td>
<select name="arp_timeout" class="form-select">
<option selected value="{{ .Config.ArpTimeout }}">{{ .Config.ArpTimeout }}</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option value="1500">1500</option>
<option value="2000">2000</option>
<option value="3000">3000</option>
<option value="5000">5000</option>
</select>
<div class="form-check form-switch">
{{ if .Config.InfluxSkipTLS }}
<input class="form-check-input" type="checkbox" name="skip" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="skip">
{{ end }}
</div>
</td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
</form>
<td>
<a href="/test_notify/"><button style="float: right;" class="btn btn-success">Test notification</button></a>
</td>
<td></td>
</tr>
</form>
</table>
</div>
<div class="col-md-6">
<p>Current version: {{ .Version }}</p>
<div class="alert alert-info" role="alert">
<p>● There can be one or more <b>Interfaces</b>, space separated</p>
<p>● After changing <b>Host</b>, <b>Port</b> or <b>Path to DB</b> you need to restart the app</p>
<p><b>Timeout</b> means time between scans (in seconds)</p>
<p><b>Shoutrrr URL</b> provides notifications to Discord, Email, Gotify, Telegram and other services. <a href="https://containrrr.dev/shoutrrr/v0.5/services/overview/" target="_blank">Link to documentation</a></p>
<p>● If you want to detect unknown hosts by MAC only, set <b>Ignore IP</b> to "yes"</p>
<p><b>Log Level</b> defines how much log output you want to see</p>
<p>● The <b>Clear table</b> button below will delete all records from table. If you want to delete a single host, click on its MAC and press <b>Delete host</b> button</p>
<p><b>Arp Timeout</b> per host timeout for <b>arp-scan</b> (in milliseconds). Default: 500</p>
</div>
<br>
<form action="/clear/" method="post">
<button type="submit" class="btn btn-danger">Clear table</button>
</form>
<div class="card border-primary mt-4 mb-4">
<div class="card-header">About</div>
<div class="card-body">
<p>● After changing <b>Host</b> or <b>Port</b> the app must be restarted</p>
<p><b>Timeout (minutes)</b> - how often watched ports are scanned</p>
<p><b>Trim History</b> - how many port states are saved in memory and displayed on the <a href="/history/">History</a> page</p>
<p>● If you find this app useful, please, <a href="https://github.com/aceberg#donate" target="_blank">donate</a></p>
<p>● Commission you own app (Golang, HTML/JS). Contact and prices <a href="https://github.com/aceberg" target="_blank">here</a></p>
</div>
</div>
</div>
</div>
{{ template "footer" }}
{{ template "footer.html" }}
{{ end }}

View file

@ -1,4 +1,4 @@
{{ define "footer"}}
{{ define "footer.html"}}
</body>
</html>
{{ end }}

View file

@ -1,18 +1,21 @@
{{ define "header"}}
{{ define "header.html"}}
<!DOCTYPE html>
<html lang="en" data-bs-theme="{{ .Config.Color }}">
<head>
<title>WatchYourLAN</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--Favicon-->
<link href="data:image/x-icon;base64,{{ .Config.Icon }}" rel="icon" type="image/x-icon" />
<link rel="icon" type="image/x-icon" href="/fs/public/favicon.png">
{{ if eq .Config.NodePath "" }}
<!-- Bootstrap and theme -->
<link href="https://cdn.jsdelivr.net/gh/aceberg/aceberg-bootswatch-fork@v5/dist/{{ .Config.Theme }}/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<!-- Font for icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
{{ else }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<!-- Bootstrap and theme -->
<link href="https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/{{ .Config.Theme }}/bootstrap.min.css" rel="stylesheet">
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{{ else }} <!-- Local node_modules -->
<link rel="stylesheet" href="{{ .Config.NodePath }}/node_modules/bootstrap-icons/font/bootstrap-icons.css">
<link href="{{ .Config.NodePath }}/node_modules/bootswatch/dist/{{ .Config.Theme }}/bootstrap.min.css" rel="stylesheet">
<script src="{{ .Config.NodePath }}/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
@ -21,6 +24,9 @@
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
<div class="container-lg">
<a class="navbar-brand" href="/">
<img src="/fs/public/favicon.png" style="width: 2em"/>
</a>
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn navbar-toggler nav-link active fs-2" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
@ -34,30 +40,12 @@
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/line?state=on">Online</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/line?state=off">Offline</a>
<a class="nav-link active" href="/config/">Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/history/">History</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config/">Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/auth_conf/">Auth</a>
</li>
{{ if .Config.Auth }}
<li class="nav-item">
<a class="nav-link active" href="/login/?logout=yes">Log Out</a>
</li>
{{ end }}
</ul>
<form action="/search_hosts/" method="post" class="d-flex mb-2 mb-md-0">
<input name="search" type="text" class="form-control text-dark me-2" placeholder="Search">
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<ul class="navbar-nav">
<li class="nav-item">

View file

@ -1,43 +0,0 @@
{{ define "history"}}
<!--Auto refresh (seconds)-->
<meta http-equiv="refresh" content="{{ .Config.Timeout }}">
<body>
<div class="container-lg mt-3 table-responsive">
<table class="table table-striped">
<tr>
<th>Name</th>
<th>IP</th>
<th>Mac</th>
<th>Hardware</th>
<th>Time</th>
<th>Known</th>
<th>State</th>
</tr>
{{ range .Hist }}
<tr>
<td class="align-middle">{{ .Name }}</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
<td class="align-middle"><a href="/host?id={{ .Host }}">{{ .Mac }}</a></td>
<td class="align-middle">{{ .Hw }}</td>
<td class="align-middle">{{ .Date }}</td>
<td class="align-middle">
{{ if eq .Known 1 }}
Yes
{{ else }}
No
{{ end }}
</td>
<td class="align-middle">
{{ if eq .State 1 }}
<i class="bi bi-circle-fill text-success"></i>
{{ else }}
<i class="bi bi-circle-fill text-danger"></i>
{{ end }}
</td>
</tr>
{{ end }}
</table>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1,137 +0,0 @@
{{ define "host" }}
<body>
<div class="container-lg mt-5">
<div class="row">
<div class="col-md table-responsive">
<table class="table">
{{ range .Hosts }}
<tr>
<td class="align-middle">ID</td>
<td class="align-middle">{{ .ID }}</td>
</tr>
<tr>
<td class="align-middle">Name</td>
<td class="align-middle">{{ .Name }}</td>
</tr>
<tr>
<td class="align-middle">IP</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
</tr>
<tr>
<td class="align-middle">Mac</td>
<td class="align-middle">{{ .Mac }}</td>
</tr>
<tr>
<td class="align-middle">Hw</td>
<td class="align-middle">{{ .Hw }}</td>
</tr>
<tr>
<td class="align-middle">Date</td>
<td class="align-middle">{{ .Date }}</td>
</tr>
<tr>
<td class="align-middle">Known</td>
<td class="align-middle">
{{ if eq .Known 1 }}
Yes
{{ else }}
No
{{ end }}
</td>
</tr>
<tr>
<td class="align-middle">Now</td>
<td class="align-middle">
{{ if eq .Now 1 }}
Online
{{ else }}
Offline
{{ end }}
</td>
</tr>
<tr>
<td class="align-middle">
<form action="/del_host/" method="post">
<input name="id" type="hidden" class="form-control" value="{{ .ID }}"><button type="submit" class="btn btn-danger">Delete host</button>
</form>
</td>
<td class="align-middle"></td>
</tr>
{{ end }}
</table>
</div>
<div class="col-md table-responsive">
<table class="table">
<tr>
<td class="align-middle">Domain name</td>
<td class="align-middle">
{{ range .Themes }}
<a href="http://{{ . }}" target="_blank">{{ . }}</a>
{{ end }}
</td>
</tr>
</table>
<br>
<br>
{{ range .Hosts }}
{{ if eq .Now 1 }}
<p>Scan for open TCP ports (may take some time)</p>
<table class="table">
<form action="/port_scan/" method="post">
<input name="ip" type="hidden" class="form-control" value="{{ .IP }}">
<tr>
<td class="align-middle">Start port</td>
<td class="align-middle"><input name="begin" type="text" class="form-control" placeholder="1"></td>
</tr>
<tr>
<td class="align-middle">End port</td>
<td class="align-middle"><input name="end" type="text" class="form-control" placeholder="65535"></td>
</tr>
<tr>
<td class="align-middle"><button type="submit" class="btn btn-success">Scan host</button></td>
<td class="align-middle"></td>
</tr>
</form>
</table>
{{ end }}
{{ end }}
</div>
</div>
<div class="row table-responsive">
<table class="table table-striped">
<tr>
<th>Name</th>
<th>IP</th>
<th>Time</th>
<th>Known</th>
<th>State</th>
</tr>
{{ range .Hist }}
<tr>
<td class="align-middle">{{ .Name }}</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
<td class="align-middle">{{ .Date }}</td>
<td class="align-middle">
{{ if eq .Known 1 }}
Yes
{{ else }}
No
{{ end }}
</td>
<td class="align-middle">
{{ if eq .State 1 }}
<i class="bi bi-circle-fill text-success"></i>
{{ else }}
<i class="bi bi-circle-fill text-danger"></i>
{{ end }}
</td>
</tr>
{{ end }}
</table>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1,78 +1,40 @@
{{ define "index"}}
<!--Auto refresh (seconds)-->
<meta http-equiv="refresh" content="{{ .Config.Timeout }}">
{{ define "index.html" }}
</head>
<script src="/fs/public/js/index.js"></script>
<script src="/fs/public/js/sort.js"></script>
<body>
<div class="container-lg mt-3">
<div class="container-lg">
<div class="row">
<div class="col">
<h2>Online</h2>
<p>Scanning interfaces: {{ .Config.Iface }}</p>
<div class="table-responsive">
<div class="col-md mt-4">
<form action="/addr_add/" method="post" class="input-group">
<input name="name" type="text" class="form-control" placeholder="Name">
<input name="addr" type="text" class="form-control" placeholder="Address">
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md mt-4 mb-4">
<div class="card border-primary">
<div class="card-body table-responsive">
<table class="table table-striped">
<tr>
<th>Name</th>
<th>IP</th>
<th>Mac</th>
<th>Hardware</th>
<th>Known</th>
</tr>
{{ range .Hosts }}
{{ if eq .Now 1 }}
<tr>
<td class="align-middle">{{ .Name }}</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
<td class="align-middle"><a href="/host?id={{ .ID }}">{{ .Mac }}</a></td>
<td class="align-middle">{{ .Hw }}</td>
<td class="align-middle">
{{ if eq .Known 1 }}
<button type="button" class="btn btn-success" disabled>Yes</button>
{{ else }}
<button type="button" class="btn btn-warning" disabled>No</button>
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
<thead>
<th style="width: 3em;"></th>
<th>Name <i class="bi bi-sort-down-alt" onclick="sortBy('Name')"></i></th>
<th>Addr <i class="bi bi-sort-down-alt" onclick="sortBy('Addr')"></i></th>
<th>Total <i class="bi bi-sort-down-alt" onclick="sortBy('Total')"></i></th>
<th>Watching <i class="bi bi-sort-down-alt" onclick="sortBy('Watching')"></i></th>
<th>Online <i class="bi bi-sort-down-alt" onclick="sortBy('Online')"></i></th>
<th>Offline <i class="bi bi-sort-down-alt" onclick="sortBy('Offline')"></i></th>
</thead>
<tbody id="tBody"></tbody>
<!-- index.js -->
</table>
</div>
<h2>Log</h2>
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th>Name</th>
<th>IP</th>
<th>Mac</th>
<th>Hardware</th>
<th>Last seen</th>
<th>Known</th>
</tr>
{{ range .Hosts }}
<form action="/update_host/" method="post">
<input name="id" type="hidden" class="form-control" value="{{ .ID }}">
<tr>
<td class="align-middle">
<input name="name" type="text" class="form-control text-dark" value="{{ .Name }}">
</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
<td class="align-middle"><a href="/host?id={{ .ID }}">{{ .Mac }}</a></td>
<td class="align-middle">{{ .Hw }}</td>
<td class="align-middle">{{ .Date }}</td>
<td class="align-middle">
{{ if eq .Known 1 }}
<button value="off" name="known" type="submit" class="btn btn-success">Yes</button>
{{ else }}
<button value="on" name="known" type="submit" class="btn btn-warning">No</button>
{{ end }}
</td>
</tr>
</form>
{{ end }}
</table>
</div>
</div>
</div>
</div>
{{ template "footer" }}
{{ template "footer.html" }}
{{ end }}

View file

@ -1,83 +0,0 @@
{{ define "line"}}
<body>
<div class="container-lg mt-3 table-responsive">
<table class="table table-striped">
<tr>
<form action="/sort_hosts/" method="post">
<th>Name<br>
<button type="submit" name="sort_method" value="name-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="name-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
<th>IP<br>
<button type="submit" name="sort_method" value="ip-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="ip-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
<th>Mac<br>
<button type="submit" name="sort_method" value="mac-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="mac-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
<th>Hardware<br>
<button type="submit" name="sort_method" value="hw-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="hw-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
<th>Last seen<br>
<button type="submit" name="sort_method" value="date-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="date-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
<th>Known<br>
<button type="submit" name="sort_method" value="known-up" class="btn btn-primary btn-sm">
<i class="bi bi-caret-up"></i>
</button>
<button type="submit" name="sort_method" value="known-down" class="btn btn-primary btn-sm">
<i class="bi bi-caret-down"></i>
</button>
</th>
</form>
</tr>
{{ range .Hosts }}
<form action="/update_host/" method="post">
<input name="id" type="hidden" class="form-control" value="{{ .ID }}">
<tr>
<td class="align-middle">
<input name="name" type="text" class="form-control text-dark" value="{{ .Name }}">
</td>
<td class="align-middle"><a href="http://{{ .IP }}" target="_blank">{{ .IP }}</a></td>
<td class="align-middle"><a href="/host?id={{ .ID }}">{{ .Mac }}</a></td>
<td class="align-middle">{{ .Hw }}</td>
<td class="align-middle">{{ .Date }}</td>
<td class="align-middle">
{{ if eq .Known 1 }}
<button value="off" name="known" type="submit" class="btn btn-success">Yes</button>
{{ else }}
<button value="on" name="known" type="submit" class="btn btn-warning">No</button>
{{ end }}
</td>
</tr>
</form>
{{ end }}
</table>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1,19 +0,0 @@
{{ define "login" }}
</head>
<body>
<div class="container-lg">
<br>
<br>
<div class="row text-center">
<div class="d-flex align-items-center justify-content-center">
<form action="/login/" method="post">
<input name="username" type="text" placeholder="Login" class="form-control p-2 m-2">
<input name="password" type="password" placeholder="Password" class="form-control p-2 m-2">
<button type="submit" class="btn btn-primary p-2 m-2">Login</button>
</form>
</div>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1,18 +0,0 @@
{{ define "port" }}
<body>
<div class="container-lg mt-5">
<div class="row">
<div class="col">
<p>Open TCP ports:</p>
{{ range .Themes }}
<a href="http://{{ . }}" target="_blank">{{ . }}</a>
<br>
{{ end }}
</div>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -1 +0,0 @@
VERSION=1.0.6

View file

@ -1,42 +0,0 @@
package web
import (
"log"
"strconv"
"time"
"github.com/aceberg/WatchYourLAN/internal/db"
)
func trimHistoryRoutine() {
for {
trimHistory()
time.Sleep(time.Duration(1) * time.Hour) // Every hour
}
}
func trimHistory() {
days, _ := strconv.Atoi(AppConfig.HistDays)
nowStr := time.Now().Format("2006-01-02 15:04:05") // This needed so all time is
now, _ := time.Parse("2006-01-02 15:04:05", nowStr) // in one format
nowMinus := now.Add(-time.Duration(days) * 24 * time.Hour)
history := db.SelectHist(AppConfig.DbPath)
if AppConfig.LogLevel != "short" {
log.Println("INFO: removing all history before", nowMinus.Format("2006-01-02 15:04:05"))
}
for _, hist := range history {
date, _ := time.Parse("2006-01-02 15:04:05", hist.Date)
if date.Before(nowMinus) {
db.DeleteHist(AppConfig.DbPath, hist.ID)
}
}
}

View file

@ -1,36 +0,0 @@
package web
import (
"fmt"
"net/http"
"strconv"
"github.com/aceberg/WatchYourLAN/internal/db"
)
func updateHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.FormValue("id")
name := r.FormValue("name")
knownStr := r.FormValue("known")
if idStr == "" {
fmt.Fprintf(w, "No data!")
} else {
var known int
id, _ := strconv.Atoi(idStr)
known = 0
if knownStr == "on" {
known = 1
}
for i, oneHost := range AllHosts {
if oneHost.ID == id {
AllHosts[i].Name = name
AllHosts[i].Known = known
db.Update(AppConfig.DbPath, AllHosts[i])
}
}
}
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}

View file

@ -1,58 +1,49 @@
package web
import (
"log"
"html/template"
"log/slog"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/db"
"github.com/aceberg/WatchYourLAN/internal/migrate"
"github.com/aceberg/WatchYourLAN/internal/scan"
)
// Gui - start web GUI
func Gui(configPath, nodePath string) {
// Gui - start web server
func Gui(dirPath, nodePath string) {
ConfigPath = migrate.ToYAML(configPath)
AppConfig, authConf = conf.Get(ConfigPath)
AppConfig.NodePath = nodePath
AppConfig.Icon = Icon
confPath := dirPath + "/config.yaml"
check.Path(confPath)
address := AppConfig.GuiIP + ":" + AppConfig.GuiPort
appConfig = conf.Get(confPath)
QuitScan = make(chan bool)
go scan.Start(AppConfig, QuitScan)
go trimHistoryRoutine() // trim-history.go
appConfig.DirPath = dirPath
appConfig.ConfPath = confPath
appConfig.NodePath = nodePath
updateAllHosts() // webgui.go
slog.Info("config", "path", appConfig.DirPath)
log.Println("=================================== ")
log.Printf("Web GUI at http://%s", address)
log.Println("=================================== ")
address := appConfig.Host + ":" + appConfig.Port
http.HandleFunc("/login/", loginHandler) // login.go
slog.Info("=================================== ")
slog.Info("Web GUI at http://" + address)
slog.Info("=================================== ")
http.HandleFunc("/", auth.Auth(indexHandler, &authConf))
http.HandleFunc("/auth_conf/", auth.Auth(authConfHandler, &authConf)) // auth.go
http.HandleFunc("/auth_save/", auth.Auth(saveAuthHandler, &authConf)) // auth.go
http.HandleFunc("/clear/", auth.Auth(clearHandler, &authConf)) // config.go
http.HandleFunc("/config/", auth.Auth(configHandler, &authConf)) // config.go
http.HandleFunc("/del_host/", auth.Auth(delHandler, &authConf)) // host.go
http.HandleFunc("/history/", auth.Auth(historyHandler, &authConf)) // history.go
http.HandleFunc("/host/", auth.Auth(hostHandler, &authConf)) // host.go
http.HandleFunc("/line/", auth.Auth(lineHandler, &authConf)) // line.go
http.HandleFunc("/port_scan/", auth.Auth(portHandler, &authConf)) // port.go
http.HandleFunc("/save_config/", auth.Auth(saveConfigHandler, &authConf)) // config.go
http.HandleFunc("/search_hosts/", auth.Auth(searchHandler, &authConf)) // search.go
http.HandleFunc("/sort_hosts/", auth.Auth(sortHandler, &authConf)) // sort.go
http.HandleFunc("/test_notify/", auth.Auth(testNotifyHandler, &authConf)) // config.go
http.HandleFunc("/update_host/", auth.Auth(updateHandler, &authConf)) // update.go
err := http.ListenAndServe(address, nil)
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
templ := template.Must(template.New("").ParseFS(templFS, "templates/*"))
router.SetHTMLTemplate(templ) // templates
router.StaticFS("/fs/", http.FS(pubFS)) // public
router.GET("/", indexHandler) // index.go
router.GET("/config/", configHandler) // config.go
router.POST("/config/", saveConfigHandler) // config.go
err := router.Run(address)
check.IfError(err)
}
func updateAllHosts() {
AllHosts = db.Select(AppConfig.DbPath)
}