This commit is contained in:
Mike Schwörer 2023-12-01 13:44:58 +01:00
parent b958ff7ca2
commit 6e98701299
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
239 changed files with 827 additions and 102 deletions

14
.idea/webResources.xml generated Normal file
View 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>

View File

@ -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
View 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})
}

View File

@ -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) },
}
} }

View File

@ -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)
// ================ ================ // ================ ================

View File

@ -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)

View File

@ -23,7 +23,10 @@ type Config struct {
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
@ -39,7 +42,10 @@ var configLocHost = func() Config {
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,
} }
} }
@ -54,7 +60,10 @@ var configLocDocker = func() Config {
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,
} }
} }
@ -69,7 +78,10 @@ var configDev = func() Config {
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,
} }
} }
@ -84,7 +96,10 @@ var configStag = func() Config {
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,
} }
} }
@ -99,7 +114,10 @@ var configProd = func() Config {
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()

View File

@ -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"
@ -31,11 +32,18 @@ type Application struct {
IsRunning *syncext.AtomicBool IsRunning *syncext.AtomicBool
Gin *ginext.GinWrapper Gin *ginext.GinWrapper
Assets *webassets.Assets
Jobs []Job 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")
} }

View File

@ -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"

View File

@ -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",

View File

@ -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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More