html
This commit is contained in:
parent
b958ff7ca2
commit
6e98701299
14
.idea/webResources.xml
generated
Normal file
14
.idea/webResources.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="WebResourcesPaths">
|
||||||
|
<contentEntries>
|
||||||
|
<entry url="file://$PROJECT_DIR$">
|
||||||
|
<entryData>
|
||||||
|
<resourceRoots>
|
||||||
|
<path value="file://$PROJECT_DIR$/webassets" />
|
||||||
|
</resourceRoots>
|
||||||
|
</entryData>
|
||||||
|
</entry>
|
||||||
|
</contentEntries>
|
||||||
|
</component>
|
||||||
|
</project>
|
2
Makefile
2
Makefile
@ -22,7 +22,7 @@ ids:
|
|||||||
|
|
||||||
run: build
|
run: build
|
||||||
mkdir -p .run-data
|
mkdir -p .run-data
|
||||||
sudo _build/bunny_backend
|
sudo BUNNY_LIVERELOAD="$(shell pwd)/webassets" CONF_NS="local-host" _build/bunny_backend
|
||||||
|
|
||||||
gow:
|
gow:
|
||||||
# go install github.com/mitranim/gow@latest
|
# go install github.com/mitranim/gow@latest
|
||||||
|
47
api/handler/apiHandler.go
Normal file
47
api/handler/apiHandler.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
||||||
|
bunny "locbunny"
|
||||||
|
"locbunny/logic"
|
||||||
|
"locbunny/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
app *logic.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||||
|
return APIHandler{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServer swaggerdoc
|
||||||
|
//
|
||||||
|
// @Summary List running server
|
||||||
|
//
|
||||||
|
// @Success 200 {object} handler.ListServer.response
|
||||||
|
// @Failure 400 {object} models.APIError
|
||||||
|
// @Failure 500 {object} models.APIError
|
||||||
|
//
|
||||||
|
// @Router /server [GET]
|
||||||
|
func (h APIHandler) ListServer(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||||
|
type response struct {
|
||||||
|
Server []models.Server `json:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _, errResp := pctx.Start()
|
||||||
|
if errResp != nil {
|
||||||
|
return *errResp
|
||||||
|
}
|
||||||
|
defer ctx.Cancel()
|
||||||
|
|
||||||
|
srvs, err := h.app.ListServer(ctx, bunny.Conf.VerifyConnTimeoutAPI)
|
||||||
|
if err != nil {
|
||||||
|
return ginext.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ginext.JSON(http.StatusOK, response{Server: srvs})
|
||||||
|
}
|
@ -1,11 +1,20 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
templhtml "html/template"
|
||||||
|
bunny "locbunny"
|
||||||
"locbunny/logic"
|
"locbunny/logic"
|
||||||
"locbunny/models"
|
"locbunny/models"
|
||||||
|
"locbunny/webassets"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
templtext "text/template"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebHandler struct {
|
type WebHandler struct {
|
||||||
@ -18,32 +27,151 @@ func NewWebHandler(app *logic.Application) WebHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListServer swaggerdoc
|
// ServeIndexHTML swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary List running server
|
// @Summary (Website)
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.ListServer.response
|
// @Router / [GET]
|
||||||
// @Failure 400 {object} models.APIError
|
// @Router /index.html [GET]
|
||||||
// @Failure 500 {object} models.APIError
|
func (h WebHandler) ServeIndexHTML(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||||
//
|
|
||||||
// @Router /server [GET]
|
|
||||||
func (h WebHandler) ListServer(pctx ginext.PreContext) ginext.HTTPResponse {
|
|
||||||
type response struct {
|
|
||||||
Server []models.Server `json:"server"`
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, _, errResp := pctx.Start()
|
ctx, _, errResp := pctx.Start()
|
||||||
if errResp != nil {
|
if errResp != nil {
|
||||||
return *errResp
|
return *errResp
|
||||||
}
|
}
|
||||||
defer ctx.Cancel()
|
defer ctx.Cancel()
|
||||||
|
|
||||||
srvs, err := h.app.ListServer(ctx)
|
templ, err := h.app.Assets.Template("index.html", h.buildIndexHTMLTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginext.Error(err)
|
return ginext.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
langext.SortBy(srvs, func(v models.Server) int { return v.Port })
|
data := map[string]any{}
|
||||||
|
|
||||||
return ginext.JSON(http.StatusOK, response{Server: srvs})
|
bin := bytes.Buffer{}
|
||||||
|
err = templ.Execute(&bin, data)
|
||||||
|
if err != nil {
|
||||||
|
return ginext.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ginext.Data(http.StatusOK, "text/html", bin.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeScriptJS swaggerdoc
|
||||||
|
//
|
||||||
|
// @Summary (Website)
|
||||||
|
//
|
||||||
|
// @Router /scripts.script.js [GET]
|
||||||
|
func (h WebHandler) ServeScriptJS(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||||
|
ctx, _, errResp := pctx.Start()
|
||||||
|
if errResp != nil {
|
||||||
|
return *errResp
|
||||||
|
}
|
||||||
|
defer ctx.Cancel()
|
||||||
|
|
||||||
|
templ, err := h.app.Assets.Template("scripts/script.js", h.buildScriptJSTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return ginext.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]any{}
|
||||||
|
|
||||||
|
bin := bytes.Buffer{}
|
||||||
|
err = templ.Execute(&bin, data)
|
||||||
|
if err != nil {
|
||||||
|
return ginext.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ginext.Data(http.StatusOK, "text/javascript", bin.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebHandler) buildIndexHTMLTemplate(content []byte) (webassets.ITemplate, error) {
|
||||||
|
t := templhtml.New("index.html")
|
||||||
|
|
||||||
|
t.Funcs(h.templateFuncMap())
|
||||||
|
|
||||||
|
_, err := t.Parse(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebHandler) ServeAssets(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||||
|
type uri struct {
|
||||||
|
FP1 *string `uri:"fp1"`
|
||||||
|
FP2 *string `uri:"fp2"`
|
||||||
|
FP3 *string `uri:"fp3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var u uri
|
||||||
|
ctx, _, errResp := pctx.URI(&u).Start()
|
||||||
|
if errResp != nil {
|
||||||
|
return *errResp
|
||||||
|
}
|
||||||
|
defer ctx.Cancel()
|
||||||
|
|
||||||
|
assetpath := ""
|
||||||
|
if u.FP1 == nil && u.FP2 == nil && u.FP3 == nil {
|
||||||
|
assetpath = filepath.Join()
|
||||||
|
} else if u.FP2 == nil && u.FP3 == nil {
|
||||||
|
assetpath = filepath.Join(*u.FP1)
|
||||||
|
} else if u.FP3 == nil {
|
||||||
|
assetpath = filepath.Join(*u.FP1, *u.FP2)
|
||||||
|
} else {
|
||||||
|
assetpath = filepath.Join(*u.FP1, *u.FP2, *u.FP3)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.app.Assets.Read(assetpath)
|
||||||
|
if err != nil {
|
||||||
|
return ginext.JSON(http.StatusNotFound, gin.H{"error": "AssetNotFound", "assetpath": assetpath})
|
||||||
|
}
|
||||||
|
|
||||||
|
mime := bunny.FilenameToMime(assetpath, "text/plain")
|
||||||
|
|
||||||
|
return ginext.Data(http.StatusOK, mime, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebHandler) buildScriptJSTemplate(content []byte) (webassets.ITemplate, error) {
|
||||||
|
t := templtext.New("scripts/script.js")
|
||||||
|
|
||||||
|
t.Funcs(h.templateFuncMap())
|
||||||
|
|
||||||
|
_, err := t.Parse(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebHandler) templateFuncMap() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"listServers": func() []models.Server {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), bunny.Conf.VerifyConnTimeoutHTML+2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
v, err := h.app.ListServer(ctx, bunny.Conf.VerifyConnTimeoutHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
},
|
||||||
|
"safe_html": func(s string) templhtml.HTML { return templhtml.HTML(s) }, //nolint:gosec
|
||||||
|
"safe_js": func(s string) templhtml.JS { return templhtml.JS(s) }, //nolint:gosec
|
||||||
|
"json": func(obj any) string {
|
||||||
|
v, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(v)
|
||||||
|
},
|
||||||
|
"json_indent": func(obj any) string {
|
||||||
|
v, err := json.MarshalIndent(obj, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(v)
|
||||||
|
},
|
||||||
|
"mkarr": func(ln int) []int { return make([]int, ln) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ type Router struct {
|
|||||||
app *logic.Application
|
app *logic.Application
|
||||||
|
|
||||||
commonHandler handler.CommonHandler
|
commonHandler handler.CommonHandler
|
||||||
|
apiHandler handler.APIHandler
|
||||||
webHandler handler.WebHandler
|
webHandler handler.WebHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ func NewRouter(app *logic.Application) *Router {
|
|||||||
app: app,
|
app: app,
|
||||||
|
|
||||||
commonHandler: handler.NewCommonHandler(app),
|
commonHandler: handler.NewCommonHandler(app),
|
||||||
|
apiHandler: handler.NewAPIHandler(app),
|
||||||
webHandler: handler.NewWebHandler(app),
|
webHandler: handler.NewWebHandler(app),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,9 +52,18 @@ func (r *Router) Init(e *ginext.GinWrapper) {
|
|||||||
docs.GET("/swagger/*sub").Handle(swagger.Handle)
|
docs.GET("/swagger/*sub").Handle(swagger.Handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================ Website ================
|
||||||
|
|
||||||
|
e.Routes().GET("/").Handle(r.webHandler.ServeIndexHTML)
|
||||||
|
e.Routes().GET("/index.html").Handle(r.webHandler.ServeIndexHTML)
|
||||||
|
e.Routes().GET("/scripts/script.js").Handle(r.webHandler.ServeScriptJS)
|
||||||
|
e.Routes().GET("/:fp1").Handle(r.webHandler.ServeAssets)
|
||||||
|
e.Routes().GET("/:fp1/:fp2").Handle(r.webHandler.ServeAssets)
|
||||||
|
e.Routes().GET("/:fp1/:fp2/:fp3").Handle(r.webHandler.ServeAssets)
|
||||||
|
|
||||||
// ================ API ================
|
// ================ API ================
|
||||||
|
|
||||||
api.GET("/server").Handle(r.webHandler.ListServer)
|
api.GET("/server").Handle(r.apiHandler.ListServer)
|
||||||
|
|
||||||
// ================ ================
|
// ================ ================
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
bunny "locbunny"
|
bunny "locbunny"
|
||||||
"locbunny/api"
|
"locbunny/api"
|
||||||
"locbunny/logic"
|
"locbunny/logic"
|
||||||
|
"locbunny/webassets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -16,7 +17,9 @@ func main() {
|
|||||||
|
|
||||||
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace))
|
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace))
|
||||||
|
|
||||||
app := logic.NewApp()
|
assets := webassets.NewAssets()
|
||||||
|
|
||||||
|
app := logic.NewApp(assets)
|
||||||
|
|
||||||
ginengine := ginext.NewEngine(conf.Cors, conf.GinDebug, true, conf.RequestTimeout)
|
ginengine := ginext.NewEngine(conf.Cors, conf.GinDebug, true, conf.RequestTimeout)
|
||||||
|
|
||||||
|
140
config.go
140
config.go
@ -14,92 +14,110 @@ const APILevel = 1
|
|||||||
var SelfProcessID int
|
var SelfProcessID int
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
GinDebug bool `env:"GINDEBUG"`
|
GinDebug bool `env:"GINDEBUG"`
|
||||||
ReturnRawErrors bool `env:"RETURNERRORS"`
|
ReturnRawErrors bool `env:"RETURNERRORS"`
|
||||||
Custom404 bool `env:"CUSTOM404"`
|
Custom404 bool `env:"CUSTOM404"`
|
||||||
LogLevel zerolog.Level `env:"LOGLEVEL"`
|
LogLevel zerolog.Level `env:"LOGLEVEL"`
|
||||||
ServerIP string `env:"IP"`
|
ServerIP string `env:"IP"`
|
||||||
ServerPort int `env:"PORT"`
|
ServerPort int `env:"PORT"`
|
||||||
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
|
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
|
||||||
Cors bool `env:"CORS"`
|
Cors bool `env:"CORS"`
|
||||||
VerifyConnTimeout time.Duration `env:"VERIFY_CONN_TIMEOUT"`
|
VerifyConnTimeoutHTML time.Duration `env:"VERIFY_CONN_TIMEOUT_HTML"`
|
||||||
|
VerifyConnTimeoutAPI time.Duration `env:"VERIFY_CONN_TIMEOUT_API"`
|
||||||
|
LiveReload *string `env:"LIVERELOAD"`
|
||||||
|
CacheDuration time.Duration `env:"CACHE_DURATION"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Conf Config
|
var Conf Config
|
||||||
|
|
||||||
var configLocHost = func() Config {
|
var configLocHost = func() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Namespace: "local",
|
Namespace: "local",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: 80,
|
ServerPort: 80,
|
||||||
Custom404: true,
|
Custom404: true,
|
||||||
ReturnRawErrors: true,
|
ReturnRawErrors: true,
|
||||||
RequestTimeout: 16 * time.Second,
|
RequestTimeout: 16 * time.Second,
|
||||||
LogLevel: zerolog.DebugLevel,
|
LogLevel: zerolog.DebugLevel,
|
||||||
Cors: true,
|
Cors: true,
|
||||||
VerifyConnTimeout: time.Second,
|
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||||
|
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||||
|
LiveReload: nil,
|
||||||
|
CacheDuration: 8 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configLocDocker = func() Config {
|
var configLocDocker = func() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Namespace: "local-docker",
|
Namespace: "local-docker",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: 80,
|
ServerPort: 80,
|
||||||
Custom404: true,
|
Custom404: true,
|
||||||
ReturnRawErrors: true,
|
ReturnRawErrors: true,
|
||||||
RequestTimeout: 16 * time.Second,
|
RequestTimeout: 16 * time.Second,
|
||||||
LogLevel: zerolog.DebugLevel,
|
LogLevel: zerolog.DebugLevel,
|
||||||
Cors: true,
|
Cors: true,
|
||||||
VerifyConnTimeout: time.Second,
|
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||||
|
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||||
|
LiveReload: nil,
|
||||||
|
CacheDuration: 8 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configDev = func() Config {
|
var configDev = func() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Namespace: "develop",
|
Namespace: "develop",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: 80,
|
ServerPort: 80,
|
||||||
Custom404: false,
|
Custom404: false,
|
||||||
ReturnRawErrors: false,
|
ReturnRawErrors: false,
|
||||||
RequestTimeout: 16 * time.Second,
|
RequestTimeout: 16 * time.Second,
|
||||||
LogLevel: zerolog.DebugLevel,
|
LogLevel: zerolog.DebugLevel,
|
||||||
Cors: false,
|
Cors: false,
|
||||||
VerifyConnTimeout: time.Second,
|
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||||
|
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||||
|
LiveReload: nil,
|
||||||
|
CacheDuration: 8 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configStag = func() Config {
|
var configStag = func() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Namespace: "staging",
|
Namespace: "staging",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: 80,
|
ServerPort: 80,
|
||||||
Custom404: false,
|
Custom404: false,
|
||||||
ReturnRawErrors: false,
|
ReturnRawErrors: false,
|
||||||
RequestTimeout: 16 * time.Second,
|
RequestTimeout: 16 * time.Second,
|
||||||
LogLevel: zerolog.DebugLevel,
|
LogLevel: zerolog.DebugLevel,
|
||||||
Cors: false,
|
Cors: false,
|
||||||
VerifyConnTimeout: time.Second,
|
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||||
|
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||||
|
LiveReload: nil,
|
||||||
|
CacheDuration: 8 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configProd = func() Config {
|
var configProd = func() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Namespace: "production",
|
Namespace: "production",
|
||||||
GinDebug: false,
|
GinDebug: false,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: 80,
|
ServerPort: 80,
|
||||||
Custom404: false,
|
Custom404: false,
|
||||||
ReturnRawErrors: false,
|
ReturnRawErrors: false,
|
||||||
RequestTimeout: 16 * time.Second,
|
RequestTimeout: 16 * time.Second,
|
||||||
LogLevel: zerolog.InfoLevel,
|
LogLevel: zerolog.InfoLevel,
|
||||||
Cors: false,
|
Cors: false,
|
||||||
VerifyConnTimeout: time.Second,
|
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||||
|
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||||
|
LiveReload: nil,
|
||||||
|
CacheDuration: 8 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +137,7 @@ func InstanceID() string {
|
|||||||
|
|
||||||
func getConfig(ns string) (Config, bool) {
|
func getConfig(ns string) (Config, bool) {
|
||||||
if ns == "" {
|
if ns == "" {
|
||||||
ns = "local-host"
|
ns = "production"
|
||||||
}
|
}
|
||||||
if cfn, ok := allConfig[ns]; ok {
|
if cfn, ok := allConfig[ns]; ok {
|
||||||
c := cfn()
|
c := cfn()
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
bunny "locbunny"
|
bunny "locbunny"
|
||||||
"locbunny/models"
|
"locbunny/models"
|
||||||
|
"locbunny/webassets"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -30,12 +31,19 @@ type Application struct {
|
|||||||
Port string
|
Port string
|
||||||
IsRunning *syncext.AtomicBool
|
IsRunning *syncext.AtomicBool
|
||||||
|
|
||||||
Gin *ginext.GinWrapper
|
Gin *ginext.GinWrapper
|
||||||
Jobs []Job
|
Assets *webassets.Assets
|
||||||
|
Jobs []Job
|
||||||
|
|
||||||
|
cacheLock sync.Mutex
|
||||||
|
serverCacheValue []models.Server
|
||||||
|
serverCacheTime *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp() *Application {
|
func NewApp(ass *webassets.Assets) *Application {
|
||||||
|
//nolint:exhaustruct
|
||||||
return &Application{
|
return &Application{
|
||||||
|
Assets: ass,
|
||||||
stopChan: make(chan bool),
|
stopChan: make(chan bool),
|
||||||
IsRunning: syncext.NewAtomicBool(false),
|
IsRunning: syncext.NewAtomicBool(false),
|
||||||
}
|
}
|
||||||
@ -110,18 +118,35 @@ func (app *Application) Run() {
|
|||||||
app.IsRunning.Set(false)
|
app.IsRunning.Set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, error) {
|
func (app *Application) ListServer(ctx context.Context, timeout time.Duration) ([]models.Server, error) {
|
||||||
|
|
||||||
|
app.cacheLock.Lock()
|
||||||
|
if app.serverCacheTime != nil && app.serverCacheTime.After(time.Now().Add(-bunny.Conf.CacheDuration)) {
|
||||||
|
v := langext.ArrCopy(app.serverCacheValue)
|
||||||
|
log.Debug().Msg(fmt.Sprintf("Return cache values (from %s)", app.serverCacheTime.Format(time.RFC3339Nano)))
|
||||||
|
app.cacheLock.Unlock()
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
app.cacheLock.Unlock()
|
||||||
|
|
||||||
socks4, err := netstat.TCPSocks(netstat.NoopFilter)
|
socks4, err := netstat.TCPSocks(netstat.NoopFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
socks6, err := netstat.TCP6Socks(netstat.NoopFilter)
|
socks6, err := netstat.TCP6Socks(netstat.NoopFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
sockCount := len(socks4) + len(socks6)
|
sockCount := len(socks4) + len(socks6)
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
@ -135,7 +160,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
con1, err := app.verifyHTTPConn(socks4[i], "HTTP", "v4")
|
con1, err := app.verifyHTTPConn(socks4[i], "HTTP", "v4", timeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rchan <- con1
|
rchan <- con1
|
||||||
return
|
return
|
||||||
@ -148,7 +173,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
con2, err := app.verifyHTTPConn(socks4[i], "HTTPS", "v4")
|
con2, err := app.verifyHTTPConn(socks4[i], "HTTPS", "v4", timeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rchan <- con2
|
rchan <- con2
|
||||||
return
|
return
|
||||||
@ -164,7 +189,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
con1, err := app.verifyHTTPConn(socks6[i], "HTTP", "v6")
|
con1, err := app.verifyHTTPConn(socks6[i], "HTTP", "v6", timeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rchan <- con1
|
rchan <- con1
|
||||||
return
|
return
|
||||||
@ -177,7 +202,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
con2, err := app.verifyHTTPConn(socks6[i], "HTTPS", "v6")
|
con2, err := app.verifyHTTPConn(socks6[i], "HTTPS", "v6", timeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rchan <- con2
|
rchan <- con2
|
||||||
return
|
return
|
||||||
@ -191,6 +216,10 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
close(echan)
|
close(echan)
|
||||||
close(rchan)
|
close(rchan)
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
duplicates := make(map[int]bool, sockCount*3)
|
duplicates := make(map[int]bool, sockCount*3)
|
||||||
res := make([]models.Server, 0, sockCount*3)
|
res := make([]models.Server, 0, sockCount*3)
|
||||||
for v := range rchan {
|
for v := range rchan {
|
||||||
@ -201,35 +230,44 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
langext.SortBy(res, func(v models.Server) int { return v.Port })
|
||||||
|
|
||||||
|
app.cacheLock.Lock()
|
||||||
|
app.serverCacheValue = langext.ArrCopy(res)
|
||||||
|
app.serverCacheTime = langext.Ptr(time.Now())
|
||||||
|
app.cacheLock.Unlock()
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, ipversion string) (models.Server, error) {
|
func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, ipversion string, timeout time.Duration) (models.Server, error) {
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), bunny.Conf.VerifyConnTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if sock.State != netstat.Listen {
|
port := int(sock.LocalAddr.Port)
|
||||||
log.Debug().Msg(fmt.Sprintf("Failed to verify socket [%s|%s] invalid state: %s", ipversion, strings.ToUpper(proto), sock.State.String()))
|
|
||||||
|
if sock.State != netstat.Listen && sock.State != netstat.Established && sock.State != netstat.TimeWait {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("Failed to verify socket [%s|%s|%d] invalid state: %s", strings.ToUpper(proto), ipversion, port, sock.State.String()))
|
||||||
return models.Server{}, errors.New("invalid sock-state")
|
return models.Server{}, errors.New("invalid sock-state")
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(sock.LocalAddr.Port) == bunny.Conf.ServerPort && sock.Process != nil && sock.Process.Pid == bunny.SelfProcessID {
|
if port == bunny.Conf.ServerPort && sock.Process != nil && sock.Process.Pid == bunny.SelfProcessID {
|
||||||
log.Debug().Msg(fmt.Sprintf("Skip socket [%s|%s] (this is our own server)", ipversion, strings.ToUpper(proto)))
|
log.Debug().Msg(fmt.Sprintf("Skip socket [%s|%s|%d] (this is our own server)", strings.ToUpper(proto), ipversion, port))
|
||||||
return models.Server{}, errors.New("skip self")
|
return models.Server{}, errors.New("skip self")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := http.Client{}
|
c := http.Client{}
|
||||||
url := fmt.Sprintf("%s://localhost:%d", strings.ToLower(proto), sock.LocalAddr.Port)
|
url := fmt.Sprintf("%s://localhost:%d", strings.ToLower(proto), port)
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msg(fmt.Sprintf("Failed to create [%s|%s] request to %d", ipversion, strings.ToUpper(proto), sock.LocalAddr.Port))
|
log.Debug().Msg(fmt.Sprintf("Failed to create [%s|%s|%d] request to %d (-> %s)", strings.ToUpper(proto), ipversion, port, port, err.Error()))
|
||||||
return models.Server{}, err
|
return models.Server{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp1, err := c.Do(req)
|
resp1, err := c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msg(fmt.Sprintf("Failed to send [%s|%s] request to %s", ipversion, strings.ToUpper(proto), url))
|
log.Debug().Msg(fmt.Sprintf("Failed to send [%s|%s|%d] request to %s (-> %s)", strings.ToUpper(proto), ipversion, port, url, err.Error()))
|
||||||
return models.Server{}, err
|
return models.Server{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +275,7 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||||||
|
|
||||||
resbody, err := io.ReadAll(resp1.Body)
|
resbody, err := io.ReadAll(resp1.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msg(fmt.Sprintf("Failed to read [%s|%s] response from %s", ipversion, strings.ToUpper(proto), url))
|
log.Debug().Msg(fmt.Sprintf("Failed to read [%s|%s|%d] response from %s (-> %s)", strings.ToUpper(proto), ipversion, port, url, err.Error()))
|
||||||
return models.Server{}, err
|
return models.Server{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +290,7 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return models.Server{
|
return models.Server{
|
||||||
Port: int(sock.LocalAddr.Port),
|
Port: port,
|
||||||
IP: sock.LocalAddr.IP.String(),
|
IP: sock.LocalAddr.IP.String(),
|
||||||
Protocol: proto,
|
Protocol: proto,
|
||||||
StatusCode: resp1.StatusCode,
|
StatusCode: resp1.StatusCode,
|
||||||
@ -265,6 +303,6 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg(fmt.Sprintf("Failed to categorize [%s|%s] response from %s (Content-Type: '%s')", ipversion, strings.ToUpper(proto), url, ct))
|
log.Debug().Msg(fmt.Sprintf("Failed to categorize [%s|%s|%d] response from %s (Content-Type: '%s')", strings.ToUpper(proto), ipversion, port, url, ct))
|
||||||
return models.Server{}, errors.New("invalid response-type")
|
return models.Server{}, errors.New("invalid response-type")
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package swagger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
_ "embed"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -8,6 +8,12 @@
|
|||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"basePath": "/api/v1/",
|
"basePath": "/api/v1/",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"summary": "(Website)",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/health": {
|
"/api/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -169,6 +175,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/index.html": {
|
||||||
|
"get": {
|
||||||
|
"summary": "(Website)",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/scripts.script.js": {
|
||||||
|
"get": {
|
||||||
|
"summary": "(Website)",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/server": {
|
"/server": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "List running server",
|
"summary": "List running server",
|
||||||
|
@ -83,6 +83,10 @@ info:
|
|||||||
title: LocalHostBunny
|
title: LocalHostBunny
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
responses: {}
|
||||||
|
summary: (Website)
|
||||||
/api/health:
|
/api/health:
|
||||||
get:
|
get:
|
||||||
responses:
|
responses:
|
||||||
@ -187,6 +191,14 @@ paths:
|
|||||||
summary: Return 200 after x seconds
|
summary: Return 200 after x seconds
|
||||||
tags:
|
tags:
|
||||||
- Common
|
- Common
|
||||||
|
/index.html:
|
||||||
|
get:
|
||||||
|
responses: {}
|
||||||
|
summary: (Website)
|
||||||
|
/scripts.script.js:
|
||||||
|
get:
|
||||||
|
responses: {}
|
||||||
|
summary: (Website)
|
||||||
/server:
|
/server:
|
||||||
get:
|
get:
|
||||||
responses:
|
responses:
|
||||||
|
39
utils.go
Normal file
39
utils.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package bunny
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func FilenameToMime(fn string, fallback string) string {
|
||||||
|
lowerFN := strings.ToLower(fn)
|
||||||
|
if strings.HasSuffix(lowerFN, ".html") || strings.HasSuffix(lowerFN, ".htm") {
|
||||||
|
return "text/html"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".css") {
|
||||||
|
return "text/css"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".js") {
|
||||||
|
return "text/javascript"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".json") {
|
||||||
|
return "application/json"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".jpeg") || strings.HasSuffix(lowerFN, ".jpg") {
|
||||||
|
return "image/jpeg"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".png") {
|
||||||
|
return "image/png"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".svg") {
|
||||||
|
return "image/svg+xml"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".gif") {
|
||||||
|
return "image/gif"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".webp") {
|
||||||
|
return "audio/webm"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerFN, ".bmp") {
|
||||||
|
return "image/bmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback
|
||||||
|
}
|
20
webassets/css/fonts.css
Normal file
20
webassets/css/fonts.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceArgon'; font-weight: bold; font-style: normal; src: url('/fonts/MonaspaceArgon-Bold.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceArgon'; font-weight: bold; font-style: italic; src: url('/fonts/MonaspaceArgon-BoldItalic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceArgon'; font-weight: normal; font-style: italic; src: url('/fonts/MonaspaceArgon-Italic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceArgon'; font-weight: normal; font-style: normal; src: url('/fonts/MonaspaceArgon-Regular.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceKrypton'; font-weight: bold; font-style: normal; src: url('/fonts/MonaspaceKrypton-Bold.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceKrypton'; font-weight: bold; font-style: italic; src: url('/fonts/MonaspaceKrypton-BoldItalic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceKrypton'; font-weight: normal; font-style: italic; src: url('/fonts/MonaspaceKrypton-Italic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceKrypton'; font-weight: normal; font-style: normal; src: url('/fonts/MonaspaceKrypton-Regular.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceNeon'; font-weight: bold; font-style: normal; src: url('/fonts/MonaspaceNeon-Bold.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceNeon'; font-weight: bold; font-style: italic; src: url('/fonts/MonaspaceNeon-BoldItalic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceNeon'; font-weight: normal; font-style: italic; src: url('/fonts/MonaspaceNeon-Italic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceNeon'; font-weight: normal; font-style: normal; src: url('/fonts/MonaspaceNeon-Regular.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceRadon'; font-weight: bold; font-style: normal; src: url('/fonts/MonaspaceRadon-Bold.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceRadon'; font-weight: bold; font-style: italic; src: url('/fonts/MonaspaceRadon-BoldItalic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceRadon'; font-weight: normal; font-style: italic; src: url('/fonts/MonaspaceRadon-Italic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceRadon'; font-weight: normal; font-style: normal; src: url('/fonts/MonaspaceRadon-Regular.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceXenon'; font-weight: bold; font-style: normal; src: url('/fonts/MonaspaceXenon-Bold.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceXenon'; font-weight: bold; font-style: italic; src: url('/fonts/MonaspaceXenon-BoldItalic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceXenon'; font-weight: normal; font-style: italic; src: url('/fonts/MonaspaceXenon-Italic.woff') format('woff2') }
|
||||||
|
@font-face { font-display: swap; font-family: 'MonaspaceXenon'; font-weight: normal; font-style: normal; src: url('/fonts/MonaspaceXenon-Regular.woff') format('woff2') }
|
51
webassets/css/reset.css
Normal file
51
webassets/css/reset.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/* https://meyerweb.com/eric/tools/css/reset/ */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a more-intuitive box-sizing model.
|
||||||
|
*/
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
2. Remove default margin
|
||||||
|
*/
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Typographic tweaks!
|
||||||
|
3. Add accessible line-height
|
||||||
|
4. Improve text rendering
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
5. Improve media defaults
|
||||||
|
*/
|
||||||
|
img, picture, video, canvas, svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
6. Remove built-in form typography styles
|
||||||
|
*/
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
7. Avoid text overflows
|
||||||
|
*/
|
||||||
|
p, h1, h2, h3, h4, h5, h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
8. Create a root stacking context
|
||||||
|
*/
|
||||||
|
#root, #__next {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
55
webassets/css/styles.css
Normal file
55
webassets/css/styles.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'MonaspaceXenon';
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#maincontent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server {
|
||||||
|
background-color: #CCC;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 0.5rem;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
|
||||||
|
box-shadow: 0 0 4px #888;
|
||||||
|
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server:hover {
|
||||||
|
box-shadow: 0 0 4px #000;
|
||||||
|
background-color: #AAA;
|
||||||
|
color: #00F;
|
||||||
|
}
|
||||||
|
|
BIN
webassets/favicon.ico
Normal file
BIN
webassets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
webassets/fonts/MonaspaceArgon-Bold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-Bold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-BoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-BoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-ExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-ExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-ExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-ExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-ExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-ExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-ExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-ExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-Italic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-Italic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-Light.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-Light.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-LightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-LightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-Medium.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-Medium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-MediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-MediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-Regular.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-Regular.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideLight.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideMedium.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideMedium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideMediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideMediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideRegular.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideRegular.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideSemiBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideSemiBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-SemiWideSemiBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-SemiWideSemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideLight.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideMedium.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideMedium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideMediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideMediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideRegular.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideRegular.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideSemiBold.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideSemiBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgon-WideSemiBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceArgon-WideSemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgonVarVF[wght,wdth,slnt].woff
Normal file
BIN
webassets/fonts/MonaspaceArgonVarVF[wght,wdth,slnt].woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceArgonVarVF[wght,wdth,slnt].woff2
Normal file
BIN
webassets/fonts/MonaspaceArgonVarVF[wght,wdth,slnt].woff2
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-Bold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-Bold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-BoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-BoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-ExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-ExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-ExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-ExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-ExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-ExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-ExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-ExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-Italic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-Italic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-Light.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-Light.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-LightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-LightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-Medium.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-Medium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-MediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-MediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-Regular.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-Regular.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideLight.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideMedium.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideMedium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideMediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideMediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideRegular.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideRegular.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideSemiBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideSemiBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-SemiWideSemiBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-SemiWideSemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideExtraBold.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideExtraBold.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideExtraBoldItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideExtraLight.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideExtraLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideExtraLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideLight.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideLight.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideLightItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideLightItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideMedium.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideMedium.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideMediumItalic.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideMediumItalic.woff
Normal file
Binary file not shown.
BIN
webassets/fonts/MonaspaceKrypton-WideRegular.woff
Normal file
BIN
webassets/fonts/MonaspaceKrypton-WideRegular.woff
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user