mirror of
https://github.com/Pacerino/CaddyProxyManager.git
synced 2026-04-05 00:54:10 -04:00
Initial Upload
This commit is contained in:
38
backend/cmd/main.go
Normal file
38
backend/cmd/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/api"
|
||||
"github.com/Pacerino/cpm/internal/config"
|
||||
"github.com/Pacerino/cpm/internal/database"
|
||||
"github.com/Pacerino/cpm/internal/jobqueue"
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "3.0.0"
|
||||
commit = "abcdefgh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Init(&version, &commit)
|
||||
database.NewDB()
|
||||
jobqueue.Start()
|
||||
|
||||
// HTTP Server
|
||||
api.StartServer()
|
||||
|
||||
// Clean Quit
|
||||
irqchan := make(chan os.Signal, 1)
|
||||
signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for irq := range irqchan {
|
||||
if irq == syscall.SIGINT || irq == syscall.SIGTERM {
|
||||
logger.Info("Got ", irq, " shutting server down ...")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
15
backend/embed/caddy/host.hbs
Normal file
15
backend/embed/caddy/host.hbs
Normal file
@@ -0,0 +1,15 @@
|
||||
{{host.domains}} {
|
||||
{{#if host.upstreams}}
|
||||
{{#if host.matcher}}
|
||||
reverse_proxy {{host.matcher}} {{#each host.upstreams}} {{backend}} {{/each}}
|
||||
{{else}}
|
||||
reverse_proxy {{#each host.upstreams}} {{backend}} {{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if LogPath}}
|
||||
log {
|
||||
output file {{LogPath}}
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
13
backend/embed/main.go
Normal file
13
backend/embed/main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package embed
|
||||
|
||||
import "embed"
|
||||
|
||||
// Frontend Files served by this app
|
||||
//
|
||||
//go:embed all:assets/**
|
||||
var Assets embed.FS
|
||||
|
||||
// CaddyFiles hold all template for caddy
|
||||
//
|
||||
//go:embed caddy
|
||||
var CaddyFiles embed.FS
|
||||
34
backend/go.mod
Normal file
34
backend/go.mod
Normal file
@@ -0,0 +1,34 @@
|
||||
module github.com/Pacerino/cpm
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/aymerick/raymond v2.0.2+incompatible
|
||||
github.com/go-playground/validator/v10 v10.11.1
|
||||
gorm.io/driver/sqlite v1.3.6
|
||||
gorm.io/gorm v1.23.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/qri-io/jsonschema v0.2.1
|
||||
github.com/vrischmann/envconfig v1.3.0
|
||||
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
|
||||
)
|
||||
93
backend/go.sum
Normal file
93
backend/go.sum
Normal file
@@ -0,0 +1,93 @@
|
||||
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
|
||||
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
|
||||
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
|
||||
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
|
||||
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
|
||||
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
|
||||
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
|
||||
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.9 h1:NSHG021i+MCznokeXR3udGaNyFyBQJW8MbjrJMVCfGw=
|
||||
gorm.io/gorm v1.23.9/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
19
backend/internal/api/context/context.go
Normal file
19
backend/internal/api/context/context.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package context
|
||||
|
||||
var (
|
||||
// BodyCtxKey is the name of the Body value on the context
|
||||
BodyCtxKey = &contextKey{"Body"}
|
||||
// PrettyPrintCtxKey is the name of the pretty print context
|
||||
PrettyPrintCtxKey = &contextKey{"Pretty"}
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "context value: " + k.name
|
||||
}
|
||||
19
backend/internal/api/handler/handler.go
Normal file
19
backend/internal/api/handler/handler.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/Pacerino/cpm/internal/database"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
db := database.GetInstance()
|
||||
handler := &Handler{
|
||||
DB: db,
|
||||
}
|
||||
return handler
|
||||
}
|
||||
29
backend/internal/api/handler/helpers.go
Normal file
29
backend/internal/api/handler/helpers.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func getURLParamInt(r *http.Request, varName string) (int, error) {
|
||||
required := true
|
||||
defaultValue := 0
|
||||
paramStr := chi.URLParam(r, varName)
|
||||
var err error
|
||||
var paramInt int
|
||||
|
||||
if paramStr == "" && required {
|
||||
return 0, fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if paramStr == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
if paramInt, err = strconv.Atoi(paramStr); err != nil {
|
||||
return 0, fmt.Errorf("%v is not a valid number", varName)
|
||||
}
|
||||
|
||||
return paramInt, nil
|
||||
}
|
||||
152
backend/internal/api/handler/hosts.go
Normal file
152
backend/internal/api/handler/hosts.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
h "github.com/Pacerino/cpm/internal/api/http"
|
||||
"github.com/Pacerino/cpm/internal/caddy"
|
||||
"github.com/Pacerino/cpm/internal/database"
|
||||
"github.com/Pacerino/cpm/internal/jobqueue"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetHosts will return a list of Hosts
|
||||
// Route: GET /hosts
|
||||
func (s Handler) GetHosts() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var hosts []database.Host
|
||||
if err := s.DB.Preload("Upstreams").Find(&hosts).Error; err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
}
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHost will return a single Host
|
||||
// Route: GET /hosts/{hostID}
|
||||
func (s Handler) GetHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
var host database.Host
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.DB.Where("id = ?", hostID).Preload("Upstreams").First(&host).Error; err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHost will create a Host
|
||||
// Route: POST /hosts
|
||||
func (s Handler) CreateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var newHost database.Host
|
||||
err := json.NewDecoder(r.Body).Decode(&newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
validate := validator.New()
|
||||
if err := validate.Struct(newHost); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := s.DB.Create(&newHost)
|
||||
if result.Error != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := jobqueue.AddJob(jobqueue.Job{
|
||||
Name: "CaddyConfigureHost",
|
||||
Action: func() error {
|
||||
return caddy.WriteHost(newHost)
|
||||
},
|
||||
}); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHost updates a host
|
||||
// Route: PUT /hosts
|
||||
func (s Handler) UpdateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var newHost database.Host
|
||||
err := json.NewDecoder(r.Body).Decode(&newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
validate := validator.New()
|
||||
if err := validate.Struct(newHost); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := jobqueue.AddJob(jobqueue.Job{
|
||||
Name: "CaddyConfigureHost",
|
||||
Action: func() error {
|
||||
return caddy.WriteHost(newHost)
|
||||
},
|
||||
}); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := s.DB.Session(&gorm.Session{FullSaveAssociations: true}).Save(&newHost)
|
||||
if result.Error != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteHost removes a host
|
||||
// Route: DELETE /hosts/{hostID}
|
||||
func (s Handler) DeleteHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := jobqueue.AddJob(jobqueue.Job{
|
||||
Name: "CaddyConfigureHost",
|
||||
Action: func() error {
|
||||
return caddy.RemoveHost(hostID)
|
||||
},
|
||||
}); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := s.DB.Delete(&database.Host{}, hostID)
|
||||
if result.Error != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil)
|
||||
return
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, true)
|
||||
} else {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrIDNotFound.Error(), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
backend/internal/api/http/requests.go
Normal file
48
backend/internal/api/http/requests.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidJSON is an error for invalid json
|
||||
ErrInvalidJSON = errors.New("json is invalid")
|
||||
// ErrInvalidPayload is an error for invalid incoming data
|
||||
ErrInvalidPayload = errors.New("payload is invalid")
|
||||
// ErrIDNotFound is an error for invalid or missing ID in database
|
||||
ErrIDNotFound = errors.New("id is missing in db")
|
||||
)
|
||||
|
||||
// ValidateRequestSchema takes a Schema and the Content to validate against it
|
||||
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
|
||||
var jsonErrors []jsonschema.KeyError
|
||||
var schemaBytes = []byte(schema)
|
||||
|
||||
// Make sure the body is valid JSON
|
||||
if !isJSON(requestBody) {
|
||||
return jsonErrors, ErrInvalidJSON
|
||||
}
|
||||
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal(schemaBytes, rs); err != nil {
|
||||
return jsonErrors, err
|
||||
}
|
||||
|
||||
var validationErr error
|
||||
ctx := context.TODO()
|
||||
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
|
||||
return jsonErrors, validationErr
|
||||
}
|
||||
|
||||
// Valid
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func isJSON(bytes []byte) bool {
|
||||
var js map[string]interface{}
|
||||
return json.Unmarshal(bytes, &js) == nil
|
||||
}
|
||||
91
backend/internal/api/http/responses.go
Normal file
91
backend/internal/api/http/responses.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
c "github.com/Pacerino/cpm/internal/api/context"
|
||||
"github.com/Pacerino/cpm/internal/errors"
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Response interface for standard API results
|
||||
type Response struct {
|
||||
Result interface{} `json:"result"`
|
||||
Error interface{} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse interface for errors returned via the API
|
||||
type ErrorResponse struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message interface{} `json:"message"`
|
||||
Invalid interface{} `json:"invalid,omitempty"`
|
||||
}
|
||||
|
||||
// ResultResponseJSON will write the result as json to the http output
|
||||
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
var response Response
|
||||
resultClass := fmt.Sprintf("%v", reflect.TypeOf(result))
|
||||
|
||||
if resultClass == "http.ErrorResponse" {
|
||||
response = Response{
|
||||
Error: result,
|
||||
}
|
||||
} else {
|
||||
response = Response{
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
var err error
|
||||
if getPrettyPrintFromContext(r) {
|
||||
payload, err = json.MarshalIndent(response, "", " ")
|
||||
} else {
|
||||
payload, err = json.Marshal(response)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("ResponseMarshalError", err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, string(payload))
|
||||
}
|
||||
|
||||
// ResultSchemaErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonschema.KeyError) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: errors.ErrValidationFailed,
|
||||
Invalid: errs,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse)
|
||||
}
|
||||
|
||||
// ResultErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: status,
|
||||
Message: message,
|
||||
Invalid: extended,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, status, errorResponse)
|
||||
}
|
||||
|
||||
// getPrettyPrintFromContext returns the PrettyPrint setting
|
||||
func getPrettyPrintFromContext(r *http.Request) bool {
|
||||
pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return pretty
|
||||
}
|
||||
61
backend/internal/api/router.go
Normal file
61
backend/internal/api/router.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Pacerino/cpm/embed"
|
||||
"github.com/Pacerino/cpm/internal/api/handler"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
func NewRouter() http.Handler {
|
||||
cors := cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
r := chi.NewRouter()
|
||||
h := handler.NewHandler()
|
||||
|
||||
r.Use(
|
||||
cors,
|
||||
)
|
||||
|
||||
return generateRoutes(r, h)
|
||||
}
|
||||
|
||||
func generateRoutes(r chi.Router, h *handler.Handler) chi.Router {
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
|
||||
//Hosts
|
||||
r.Route("/hosts", func(r chi.Router) {
|
||||
r.Get("/", h.GetHosts()) // Get List of Hosts
|
||||
r.Post("/", h.CreateHost()) // Create Host & save to the DB
|
||||
r.Get("/{hostID:[0-9]+}", h.GetHost()) // Get specific Host by ID
|
||||
r.Delete("/{hostID:[0-9]+}", h.DeleteHost()) // Delete Host by ID
|
||||
r.Put("/", h.UpdateHost()) // Update Host by ID
|
||||
})
|
||||
})
|
||||
fileServer(r)
|
||||
return r
|
||||
}
|
||||
|
||||
func fileServer(r chi.Router) {
|
||||
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||
fSys, err := fs.Sub(embed.Assets, "assets")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fs := http.StripPrefix(pathPrefix, http.FileServer(http.FS(fSys)))
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
19
backend/internal/api/server.go
Normal file
19
backend/internal/api/server.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
)
|
||||
|
||||
const httpPort = 3001
|
||||
|
||||
// StartServer creates a http server
|
||||
func StartServer() {
|
||||
logger.Info("Server starting on port %v", httpPort)
|
||||
err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%v", httpPort), NewRouter())
|
||||
if err != nil {
|
||||
logger.Error("HttpListenError", err)
|
||||
}
|
||||
}
|
||||
53
backend/internal/caddy/caddy.go
Normal file
53
backend/internal/caddy/caddy.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Pacerino/cpm/embed"
|
||||
"github.com/Pacerino/cpm/internal/config"
|
||||
"github.com/Pacerino/cpm/internal/database"
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
type hostEntity struct {
|
||||
Host database.Host
|
||||
LogPath string
|
||||
}
|
||||
|
||||
func WriteHost(h database.Host) error {
|
||||
data := &hostEntity{
|
||||
Host: h,
|
||||
LogPath: fmt.Sprintf("%s/host_%d.log", config.Configuration.LogFolder, h.ID),
|
||||
}
|
||||
filename := fmt.Sprintf("%s/host_%d.conf", config.Configuration.DataFolder, h.ID)
|
||||
// Read Template from Embed FS
|
||||
template, err := embed.CaddyFiles.ReadFile("caddy/host.hbs")
|
||||
if err != nil {
|
||||
logger.Error(err.Error(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse Data into Template
|
||||
tmplOutput, err := raymond.Render(string(template), data)
|
||||
if err != nil {
|
||||
logger.Error(err.Error(), err)
|
||||
return err
|
||||
}
|
||||
// Write filled out template to the config folder
|
||||
if err := os.WriteFile(filename, []byte(tmplOutput), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ReloadCaddy()
|
||||
}
|
||||
|
||||
func RemoveHost(hostID int) error {
|
||||
filename := fmt.Sprintf("%s/host_%d.conf", config.Configuration.DataFolder, hostID)
|
||||
if err := os.Remove(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return ReloadCaddy()
|
||||
}
|
||||
44
backend/internal/caddy/exec.go
Normal file
44
backend/internal/caddy/exec.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/config"
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
)
|
||||
|
||||
func ReloadCaddy() error {
|
||||
_, err := shExec([]string{"reload", "--config", config.Configuration.CaddyFile})
|
||||
return err
|
||||
}
|
||||
|
||||
func getCaddyFilePath() (string, error) {
|
||||
path, err := exec.LookPath("caddy")
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("cannot find caddy execuatable script in PATH")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// shExec executes caddy with arguments
|
||||
func shExec(args []string) (string, error) {
|
||||
ng, err := getCaddyFilePath()
|
||||
if err != nil {
|
||||
logger.Error("CaddyError", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
logger.Debug("CMD: %s %v", ng, args)
|
||||
// nolint: gosec
|
||||
c := exec.Command(ng, args...)
|
||||
|
||||
b, e := c.Output()
|
||||
|
||||
if e != nil {
|
||||
logger.Error("CaddyError", fmt.Errorf("command error: %s -- %v\n%+v", ng, args, e))
|
||||
logger.Warn(string(b))
|
||||
}
|
||||
|
||||
return string(b), e
|
||||
}
|
||||
70
backend/internal/config/config.go
Normal file
70
backend/internal/config/config.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
golog "log"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
|
||||
"github.com/vrischmann/envconfig"
|
||||
)
|
||||
|
||||
// Version is the version set by ldflags
|
||||
var Version string
|
||||
|
||||
// Commit is the git commit set by ldflags
|
||||
var Commit string
|
||||
|
||||
// Init will parse environment variables into the Env struct
|
||||
func Init(version, commit *string) {
|
||||
|
||||
Version = *version
|
||||
Commit = *commit
|
||||
|
||||
if err := envconfig.InitWithPrefix(&Configuration, "CPM"); err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
}
|
||||
initLogger()
|
||||
logger.Info("Build Version: %s (%s)", Version, Commit)
|
||||
createDataFolders()
|
||||
}
|
||||
|
||||
// Init initialises the Log object and return it
|
||||
func initLogger() {
|
||||
// this removes timestamp prefixes from logs
|
||||
golog.SetFlags(0)
|
||||
|
||||
switch Configuration.Log.Level {
|
||||
case "debug":
|
||||
logLevel = logger.DebugLevel
|
||||
case "warn":
|
||||
logLevel = logger.WarnLevel
|
||||
case "error":
|
||||
logLevel = logger.ErrorLevel
|
||||
default:
|
||||
logLevel = logger.InfoLevel
|
||||
}
|
||||
|
||||
err := logger.Configure(&logger.Config{
|
||||
LogThreshold: logLevel,
|
||||
Formatter: Configuration.Log.Format,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error("LoggerConfigurationError", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogLevel returns the logger const level
|
||||
func GetLogLevel() logger.Level {
|
||||
return logLevel
|
||||
}
|
||||
|
||||
/* func isError(errorClass string, err error) bool {
|
||||
if err != nil {
|
||||
logger.Error(errorClass, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
*/
|
||||
28
backend/internal/config/folders.go
Normal file
28
backend/internal/config/folders.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
)
|
||||
|
||||
func createDataFolders() {
|
||||
folders := []string{
|
||||
"hosts",
|
||||
}
|
||||
|
||||
for _, folder := range folders {
|
||||
path := folder
|
||||
if path[0:1] != "/" {
|
||||
path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder)
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
// folder does not exist
|
||||
logger.Debug("Creating folder: %s", path)
|
||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
logger.Error("CreateDataFolderError", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
backend/internal/config/vars.go
Normal file
21
backend/internal/config/vars.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import "github.com/Pacerino/cpm/internal/logger"
|
||||
|
||||
// IsSetup defines whether we have an admin user or not
|
||||
var IsSetup bool
|
||||
|
||||
var logLevel logger.Level
|
||||
|
||||
type log struct {
|
||||
Level string `json:"level" envconfig:"optional,default=debug"`
|
||||
Format string `json:"format" envconfig:"optional,default=nice"`
|
||||
}
|
||||
|
||||
// Configuration is the main configuration object
|
||||
var Configuration struct {
|
||||
DataFolder string `json:"data_folder" envconfig:"optional,default=/etc/caddy/"`
|
||||
LogFolder string `json:"log_folder" envconfig:"optional,default=/var/log/caddy"`
|
||||
CaddyFile string `json:"caddy_file" envconfig:"optional,default=/etc/caddy/Caddyfile"`
|
||||
Log log `json:"log"`
|
||||
}
|
||||
16
backend/internal/database/models.go
Normal file
16
backend/internal/database/models.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package database
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
Domains string `json:"domains" validate:"required,fqdn|hostname_port"`
|
||||
Matcher string `json:"matcher"`
|
||||
Upstreams []Upstream
|
||||
}
|
||||
|
||||
type Upstream struct {
|
||||
gorm.Model
|
||||
HostID uint `json:"hostId"`
|
||||
Backend string `json:"backend" validate:"required,hostname_port"`
|
||||
}
|
||||
40
backend/internal/database/sqlite.go
Normal file
40
backend/internal/database/sqlite.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/config"
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var dbInstance *gorm.DB
|
||||
|
||||
// NewDB creates a new connection
|
||||
func NewDB() {
|
||||
logger.Info("Creating new DB instance")
|
||||
dbInstance = SqliteDB()
|
||||
dbInstance.AutoMigrate(&Host{}, &Upstream{})
|
||||
}
|
||||
|
||||
// GetInstance returns an existing or new instance
|
||||
func GetInstance() *gorm.DB {
|
||||
if dbInstance == nil {
|
||||
NewDB()
|
||||
}
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
// SqliteDB Create sqlite client
|
||||
func SqliteDB() *gorm.DB {
|
||||
dbFile := fmt.Sprintf("%s/caddyproxymanager.db", config.Configuration.DataFolder)
|
||||
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{})
|
||||
if err != nil {
|
||||
logger.Error("SqliteError", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
16
backend/internal/errors/errors.go
Normal file
16
backend/internal/errors/errors.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
// All error messages used by the service package to report
|
||||
// problems back to calling clients
|
||||
var (
|
||||
ErrDatabaseUnavailable = errors.New("database-unavailable")
|
||||
ErrDuplicateEmailUser = errors.New("email-already-exists")
|
||||
ErrInvalidLogin = errors.New("invalid-login-credentials")
|
||||
ErrUserDisabled = errors.New("user-disabled")
|
||||
ErrSystemUserReadonly = errors.New("cannot-save-system-users")
|
||||
ErrValidationFailed = errors.New("request-failed-validation")
|
||||
ErrCurrentPasswordInvalid = errors.New("current-password-invalid")
|
||||
ErrCABundleDoesNotExist = errors.New("ca-bundle-does-not-exist")
|
||||
)
|
||||
46
backend/internal/jobqueue/main.go
Normal file
46
backend/internal/jobqueue/main.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package jobqueue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
worker *Worker
|
||||
)
|
||||
|
||||
// Start will intantiate the queue and start doing work
|
||||
func Start() {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
q := &Queue{
|
||||
jobs: make(chan Job),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
// Defines a queue worker, which will execute our queue.
|
||||
worker = newWorker(q)
|
||||
|
||||
// Execute jobs in queue.
|
||||
go worker.doWork()
|
||||
}
|
||||
|
||||
// Shutdown will gracefully stop the queue
|
||||
func Shutdown() error {
|
||||
if cancel == nil {
|
||||
return errors.New("unable to shutdown, jobqueue has not been started")
|
||||
}
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddJob adds a job to the queue for processing
|
||||
func AddJob(j Job) error {
|
||||
if worker == nil {
|
||||
return errors.New("unable to add job, jobqueue has not been started")
|
||||
}
|
||||
worker.Queue.AddJob(j)
|
||||
return nil
|
||||
}
|
||||
54
backend/internal/jobqueue/models.go
Normal file
54
backend/internal/jobqueue/models.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package jobqueue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Queue holds name, list of jobs and context with cancel.
|
||||
type Queue struct {
|
||||
jobs chan Job
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Job - holds logic to perform some operations during queue execution.
|
||||
type Job struct {
|
||||
Name string
|
||||
Action func() error // A function that should be executed when the job is running.
|
||||
}
|
||||
|
||||
// AddJobs adds jobs to the queue and cancels channel.
|
||||
func (q *Queue) AddJobs(jobs []Job) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(jobs))
|
||||
|
||||
for _, job := range jobs {
|
||||
// Goroutine which adds job to the queue.
|
||||
go func(job Job) {
|
||||
q.AddJob(job)
|
||||
wg.Done()
|
||||
}(job)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
// Cancel queue channel, when all goroutines were done.
|
||||
q.cancel()
|
||||
}()
|
||||
}
|
||||
|
||||
// AddJob sends job to the channel.
|
||||
func (q *Queue) AddJob(job Job) {
|
||||
q.jobs <- job
|
||||
}
|
||||
|
||||
// Run performs job execution.
|
||||
func (j Job) Run() error {
|
||||
err := j.Action()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
backend/internal/jobqueue/worker.go
Normal file
37
backend/internal/jobqueue/worker.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package jobqueue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Pacerino/cpm/internal/logger"
|
||||
)
|
||||
|
||||
// Worker responsible for queue serving.
|
||||
type Worker struct {
|
||||
Queue *Queue
|
||||
}
|
||||
|
||||
func newWorker(queue *Queue) *Worker {
|
||||
return &Worker{
|
||||
Queue: queue,
|
||||
}
|
||||
}
|
||||
|
||||
// doWork processes jobs from the queue (jobs channel).
|
||||
func (w *Worker) doWork() bool {
|
||||
for {
|
||||
select {
|
||||
// if context was canceled.
|
||||
case <-w.Queue.ctx.Done():
|
||||
logger.Info("JobQueue worker graceful shutdown")
|
||||
return true
|
||||
// if job received.
|
||||
case job := <-w.Queue.jobs:
|
||||
err := job.Run()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("%sError", job.Name), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
backend/internal/logger/config.go
Normal file
37
backend/internal/logger/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package logger
|
||||
|
||||
// Level type
|
||||
type Level int
|
||||
|
||||
// Log level definitions
|
||||
const (
|
||||
// DebugLevel usually only enabled when debugging. Very verbose logging.
|
||||
DebugLevel Level = 10
|
||||
// InfoLevel general operational entries about what's going on inside the application.
|
||||
InfoLevel Level = 20
|
||||
// WarnLevel non-critical entries that deserve eyes.
|
||||
WarnLevel Level = 30
|
||||
// ErrorLevel used for errors that should definitely be noted.
|
||||
ErrorLevel Level = 40
|
||||
)
|
||||
|
||||
// Config options for the logger.
|
||||
type Config struct {
|
||||
LogThreshold Level
|
||||
Formatter string
|
||||
}
|
||||
|
||||
// Interface for a logger
|
||||
type Interface interface {
|
||||
GetLogLevel() Level
|
||||
Debug(format string, args ...interface{})
|
||||
Info(format string, args ...interface{})
|
||||
Warn(format string, args ...interface{})
|
||||
Error(errorClass string, err error, args ...interface{})
|
||||
Errorf(errorClass, format string, err error, args ...interface{})
|
||||
}
|
||||
|
||||
// ConfigurableLogger is an interface for a logger that can be configured
|
||||
type ConfigurableLogger interface {
|
||||
Configure(c *Config) error
|
||||
}
|
||||
243
backend/internal/logger/logger.go
Normal file
243
backend/internal/logger/logger.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
/* "github.com/getsentry/sentry-go" */)
|
||||
|
||||
var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color
|
||||
|
||||
// Log message structure.
|
||||
type Log struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Pid int `json:"pid"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
StackTrace []string `json:"stack_trace,omitempty"`
|
||||
}
|
||||
|
||||
// Logger instance
|
||||
type Logger struct {
|
||||
Config
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
// global logging configuration.
|
||||
var logger = NewLogger()
|
||||
|
||||
// NewLogger creates a new logger instance
|
||||
func NewLogger() *Logger {
|
||||
color.NoColor = false
|
||||
colorReset = color.New(color.Reset)
|
||||
colorGray = color.New(color.FgWhite)
|
||||
colorYellow = color.New(color.Bold, color.FgYellow)
|
||||
colorBlue = color.New(color.Bold, color.FgBlue)
|
||||
colorRed = color.New(color.Bold, color.FgRed)
|
||||
colorMagenta = color.New(color.Bold, color.FgMagenta)
|
||||
colorBlack = color.New(color.Bold, color.FgBlack)
|
||||
colorWhite = color.New(color.Bold, color.FgWhite)
|
||||
|
||||
return &Logger{
|
||||
Config: NewConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfig returns the default config
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
LogThreshold: InfoLevel,
|
||||
Formatter: "json",
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func Configure(c *Config) error {
|
||||
return logger.Configure(c)
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func GetLogLevel() Level {
|
||||
return logger.GetLogLevel()
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Debug(format string, args ...interface{}) {
|
||||
logger.Debug(format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Info(format string, args ...interface{}) {
|
||||
logger.Info(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Warn(format string, args ...interface{}) {
|
||||
logger.Warn(format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func Error(errorClass string, err error) {
|
||||
logger.Error(errorClass, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func (l *Logger) Configure(c *Config) error {
|
||||
// ensure updates to the config are atomic
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
|
||||
if c == nil {
|
||||
return fmt.Errorf("a non nil Config is mandatory")
|
||||
}
|
||||
|
||||
if err := c.LogThreshold.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.LogThreshold = c.LogThreshold
|
||||
l.Formatter = c.Formatter
|
||||
/* l.SentryConfig = c.SentryConfig */
|
||||
|
||||
/* if c.SentryConfig.Dsn != "" {
|
||||
if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", sentryErr)
|
||||
}
|
||||
} */
|
||||
|
||||
stdlog.SetFlags(0) // this removes timestamp prefixes from logs
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate the log level is in the accepted list.
|
||||
func (l Level) validate() error {
|
||||
switch l {
|
||||
case DebugLevel, InfoLevel, WarnLevel, ErrorLevel:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid \"Level\" %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
var logLevels = map[Level]string{
|
||||
DebugLevel: "DEBUG",
|
||||
InfoLevel: "INFO",
|
||||
WarnLevel: "WARN",
|
||||
ErrorLevel: "ERROR",
|
||||
}
|
||||
|
||||
func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) {
|
||||
if logLevel < l.LogThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
errorClass := ""
|
||||
if logLevel == ErrorLevel {
|
||||
// First arg is the errorClass
|
||||
errorClass = args[0].(string)
|
||||
if len(args) > 1 {
|
||||
args = args[1:]
|
||||
} else {
|
||||
args = []interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
stringMessage := fmt.Sprintf(format, args...)
|
||||
|
||||
if l.Formatter == "json" {
|
||||
// JSON Log Format
|
||||
jsonLog, _ := json.Marshal(
|
||||
Log{
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano),
|
||||
Level: logLevels[logLevel],
|
||||
Message: stringMessage,
|
||||
Pid: os.Getpid(),
|
||||
},
|
||||
)
|
||||
|
||||
stdlog.Println(string(jsonLog))
|
||||
} else {
|
||||
// Nice Log Format
|
||||
var colorLevel *color.Color
|
||||
switch logLevel {
|
||||
case DebugLevel:
|
||||
colorLevel = colorMagenta
|
||||
case InfoLevel:
|
||||
colorLevel = colorBlue
|
||||
case WarnLevel:
|
||||
colorLevel = colorYellow
|
||||
case ErrorLevel:
|
||||
colorLevel = colorRed
|
||||
stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
stdlog.Println(
|
||||
colorBlack.Sprint("["),
|
||||
colorWhite.Sprint(t.Format("2006-01-02 15:04:05")),
|
||||
colorBlack.Sprint("] "),
|
||||
colorLevel.Sprintf("%-8v", logLevels[logLevel]),
|
||||
colorGray.Sprint(stringMessage),
|
||||
colorReset.Sprint(""),
|
||||
)
|
||||
|
||||
if logLevel == ErrorLevel && l.LogThreshold == DebugLevel {
|
||||
// Print a stack trace too
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func (l *Logger) GetLogLevel() Level {
|
||||
return l.LogThreshold
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.logLevel(DebugLevel, format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.logLevel(InfoLevel, format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||
l.logLevel(WarnLevel, format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func (l *Logger) Error(errorClass string, err error) {
|
||||
l.logLevel(ErrorLevel, err.Error(), errorClass)
|
||||
/* l.notifySentry(errorClass, err) */
|
||||
}
|
||||
|
||||
/* func (l *Logger) notifySentry(errorClass string, err error) {
|
||||
if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" {
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetTag("service", "backend")
|
||||
scope.SetTag("error_class", errorClass)
|
||||
})
|
||||
|
||||
sentry.CaptureException(err)
|
||||
// Since sentry emits events in the background we need to make sure
|
||||
// they are sent before we shut down
|
||||
sentry.Flush(time.Second * 5)
|
||||
}
|
||||
}
|
||||
*/
|
||||
36
backend/internal/util/interfaces.go
Normal file
36
backend/internal/util/interfaces.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package util
|
||||
|
||||
// FindItemInInterface Find key in interface (recursively) and return value as interface
|
||||
func FindItemInInterface(key string, obj interface{}) (interface{}, bool) {
|
||||
// if the argument is not a map, ignore it
|
||||
mobj, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for k, v := range mobj {
|
||||
// key match, return value
|
||||
if k == key {
|
||||
return v, true
|
||||
}
|
||||
|
||||
// if the value is a map, search recursively
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
if res, ok := FindItemInInterface(key, m); ok {
|
||||
return res, true
|
||||
}
|
||||
}
|
||||
// if the value is an array, search recursively
|
||||
// from each element
|
||||
if va, ok := v.([]interface{}); ok {
|
||||
for _, a := range va {
|
||||
if res, ok := FindItemInInterface(key, a); ok {
|
||||
return res, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// element not found
|
||||
return nil, false
|
||||
}
|
||||
44
backend/internal/util/slices.go
Normal file
44
backend/internal/util/slices.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SliceContainsItem returns whether the slice given contains the item given
|
||||
func SliceContainsItem(slice []string, item string) bool {
|
||||
for _, a := range slice {
|
||||
if a == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SliceContainsInt returns whether the slice given contains the item given
|
||||
func SliceContainsInt(slice []int, item int) bool {
|
||||
for _, a := range slice {
|
||||
if a == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ConvertIntSliceToString returns a comma separated string of all items in the slice
|
||||
func ConvertIntSliceToString(slice []int) string {
|
||||
strs := []string{}
|
||||
for _, item := range slice {
|
||||
strs = append(strs, strconv.Itoa(item))
|
||||
}
|
||||
return strings.Join(strs, ",")
|
||||
}
|
||||
|
||||
// ConvertStringSliceToInterface is required in some special cases
|
||||
func ConvertStringSliceToInterface(slice []string) []interface{} {
|
||||
res := make([]interface{}, len(slice))
|
||||
for i := range slice {
|
||||
res[i] = slice[i]
|
||||
}
|
||||
return res
|
||||
}
|
||||
Reference in New Issue
Block a user