html
This commit is contained in:
parent
b958ff7ca2
commit
6e98701299
|
@ -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
|
||||
mkdir -p .run-data
|
||||
sudo _build/bunny_backend
|
||||
sudo BUNNY_LIVERELOAD="$(shell pwd)/webassets" CONF_NS="local-host" _build/bunny_backend
|
||||
|
||||
gow:
|
||||
# go install github.com/mitranim/gow@latest
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
templhtml "html/template"
|
||||
bunny "locbunny"
|
||||
"locbunny/logic"
|
||||
"locbunny/models"
|
||||
"locbunny/webassets"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
templtext "text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
// @Failure 400 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
//
|
||||
// @Router /server [GET]
|
||||
func (h WebHandler) ListServer(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
type response struct {
|
||||
Server []models.Server `json:"server"`
|
||||
}
|
||||
|
||||
// @Router / [GET]
|
||||
// @Router /index.html [GET]
|
||||
func (h WebHandler) ServeIndexHTML(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
ctx, _, errResp := pctx.Start()
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
srvs, err := h.app.ListServer(ctx)
|
||||
templ, err := h.app.Assets.Template("index.html", h.buildIndexHTMLTemplate)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
commonHandler handler.CommonHandler
|
||||
apiHandler handler.APIHandler
|
||||
webHandler handler.WebHandler
|
||||
}
|
||||
|
||||
|
@ -21,6 +22,7 @@ func NewRouter(app *logic.Application) *Router {
|
|||
app: app,
|
||||
|
||||
commonHandler: handler.NewCommonHandler(app),
|
||||
apiHandler: handler.NewAPIHandler(app),
|
||||
webHandler: handler.NewWebHandler(app),
|
||||
}
|
||||
}
|
||||
|
@ -50,9 +52,18 @@ func (r *Router) Init(e *ginext.GinWrapper) {
|
|||
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.GET("/server").Handle(r.webHandler.ListServer)
|
||||
api.GET("/server").Handle(r.apiHandler.ListServer)
|
||||
|
||||
// ================ ================
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
bunny "locbunny"
|
||||
"locbunny/api"
|
||||
"locbunny/logic"
|
||||
"locbunny/webassets"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -16,7 +17,9 @@ func main() {
|
|||
|
||||
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)
|
||||
|
||||
|
|
140
config.go
140
config.go
|
@ -14,92 +14,110 @@ const APILevel = 1
|
|||
var SelfProcessID int
|
||||
|
||||
type Config struct {
|
||||
Namespace string
|
||||
GinDebug bool `env:"GINDEBUG"`
|
||||
ReturnRawErrors bool `env:"RETURNERRORS"`
|
||||
Custom404 bool `env:"CUSTOM404"`
|
||||
LogLevel zerolog.Level `env:"LOGLEVEL"`
|
||||
ServerIP string `env:"IP"`
|
||||
ServerPort int `env:"PORT"`
|
||||
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
|
||||
Cors bool `env:"CORS"`
|
||||
VerifyConnTimeout time.Duration `env:"VERIFY_CONN_TIMEOUT"`
|
||||
Namespace string
|
||||
GinDebug bool `env:"GINDEBUG"`
|
||||
ReturnRawErrors bool `env:"RETURNERRORS"`
|
||||
Custom404 bool `env:"CUSTOM404"`
|
||||
LogLevel zerolog.Level `env:"LOGLEVEL"`
|
||||
ServerIP string `env:"IP"`
|
||||
ServerPort int `env:"PORT"`
|
||||
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
|
||||
Cors bool `env:"CORS"`
|
||||
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 configLocHost = func() Config {
|
||||
return Config{
|
||||
Namespace: "local",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: true,
|
||||
ReturnRawErrors: true,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: true,
|
||||
VerifyConnTimeout: time.Second,
|
||||
Namespace: "local",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: true,
|
||||
ReturnRawErrors: true,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: true,
|
||||
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||
LiveReload: nil,
|
||||
CacheDuration: 8 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
var configLocDocker = func() Config {
|
||||
return Config{
|
||||
Namespace: "local-docker",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: true,
|
||||
ReturnRawErrors: true,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: true,
|
||||
VerifyConnTimeout: time.Second,
|
||||
Namespace: "local-docker",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: true,
|
||||
ReturnRawErrors: true,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: true,
|
||||
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||
LiveReload: nil,
|
||||
CacheDuration: 8 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
var configDev = func() Config {
|
||||
return Config{
|
||||
Namespace: "develop",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: false,
|
||||
VerifyConnTimeout: time.Second,
|
||||
Namespace: "develop",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: false,
|
||||
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||
LiveReload: nil,
|
||||
CacheDuration: 8 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
var configStag = func() Config {
|
||||
return Config{
|
||||
Namespace: "staging",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: false,
|
||||
VerifyConnTimeout: time.Second,
|
||||
Namespace: "staging",
|
||||
GinDebug: true,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.DebugLevel,
|
||||
Cors: false,
|
||||
VerifyConnTimeoutAPI: 2 * time.Second,
|
||||
VerifyConnTimeoutHTML: 500 * time.Millisecond,
|
||||
LiveReload: nil,
|
||||
CacheDuration: 8 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
var configProd = func() Config {
|
||||
return Config{
|
||||
Namespace: "production",
|
||||
GinDebug: false,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.InfoLevel,
|
||||
Cors: false,
|
||||
VerifyConnTimeout: time.Second,
|
||||
Namespace: "production",
|
||||
GinDebug: false,
|
||||
ServerIP: "0.0.0.0",
|
||||
ServerPort: 80,
|
||||
Custom404: false,
|
||||
ReturnRawErrors: false,
|
||||
RequestTimeout: 16 * time.Second,
|
||||
LogLevel: zerolog.InfoLevel,
|
||||
Cors: false,
|
||||
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) {
|
||||
if ns == "" {
|
||||
ns = "local-host"
|
||||
ns = "production"
|
||||
}
|
||||
if cfn, ok := allConfig[ns]; ok {
|
||||
c := cfn()
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"io"
|
||||
bunny "locbunny"
|
||||
"locbunny/models"
|
||||
"locbunny/webassets"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -30,12 +31,19 @@ type Application struct {
|
|||
Port string
|
||||
IsRunning *syncext.AtomicBool
|
||||
|
||||
Gin *ginext.GinWrapper
|
||||
Jobs []Job
|
||||
Gin *ginext.GinWrapper
|
||||
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{
|
||||
Assets: ass,
|
||||
stopChan: make(chan bool),
|
||||
IsRunning: syncext.NewAtomicBool(false),
|
||||
}
|
||||
|
@ -110,18 +118,35 @@ func (app *Application) Run() {
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
socks6, err := netstat.TCP6Socks(netstat.NoopFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sockCount := len(socks4) + len(socks6)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -135,7 +160,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
con1, err := app.verifyHTTPConn(socks4[i], "HTTP", "v4")
|
||||
con1, err := app.verifyHTTPConn(socks4[i], "HTTP", "v4", timeout)
|
||||
if err == nil {
|
||||
rchan <- con1
|
||||
return
|
||||
|
@ -148,7 +173,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
con2, err := app.verifyHTTPConn(socks4[i], "HTTPS", "v4")
|
||||
con2, err := app.verifyHTTPConn(socks4[i], "HTTPS", "v4", timeout)
|
||||
if err == nil {
|
||||
rchan <- con2
|
||||
return
|
||||
|
@ -164,7 +189,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
con1, err := app.verifyHTTPConn(socks6[i], "HTTP", "v6")
|
||||
con1, err := app.verifyHTTPConn(socks6[i], "HTTP", "v6", timeout)
|
||||
if err == nil {
|
||||
rchan <- con1
|
||||
return
|
||||
|
@ -177,7 +202,7 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
con2, err := app.verifyHTTPConn(socks6[i], "HTTPS", "v6")
|
||||
con2, err := app.verifyHTTPConn(socks6[i], "HTTPS", "v6", timeout)
|
||||
if err == nil {
|
||||
rchan <- con2
|
||||
return
|
||||
|
@ -191,6 +216,10 @@ func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, err
|
|||
close(echan)
|
||||
close(rchan)
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duplicates := make(map[int]bool, sockCount*3)
|
||||
res := make([]models.Server, 0, sockCount*3)
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if sock.State != netstat.Listen {
|
||||
log.Debug().Msg(fmt.Sprintf("Failed to verify socket [%s|%s] invalid state: %s", ipversion, strings.ToUpper(proto), sock.State.String()))
|
||||
port := int(sock.LocalAddr.Port)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if int(sock.LocalAddr.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)))
|
||||
if port == bunny.Conf.ServerPort && sock.Process != nil && sock.Process.Pid == bunny.SelfProcessID {
|
||||
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")
|
||||
}
|
||||
|
||||
c := http.Client{}
|
||||
url := fmt.Sprintf("%s://localhost:%d", strings.ToLower(proto), sock.LocalAddr.Port)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
url := fmt.Sprintf("%s://localhost:%d", strings.ToLower(proto), port)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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
|
||||
}
|
||||
|
||||
resp1, err := c.Do(req)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -237,7 +275,7 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||
|
||||
resbody, err := io.ReadAll(resp1.Body)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -252,7 +290,7 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||
}
|
||||
|
||||
return models.Server{
|
||||
Port: int(sock.LocalAddr.Port),
|
||||
Port: port,
|
||||
IP: sock.LocalAddr.IP.String(),
|
||||
Protocol: proto,
|
||||
StatusCode: resp1.StatusCode,
|
||||
|
@ -265,6 +303,6 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string,
|
|||
}, 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")
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package swagger
|
|||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
|
||||
"net/http"
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
"host": "localhost",
|
||||
"basePath": "/api/v1/",
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"summary": "(Website)",
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/api/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
@ -169,6 +175,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/index.html": {
|
||||
"get": {
|
||||
"summary": "(Website)",
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/scripts.script.js": {
|
||||
"get": {
|
||||
"summary": "(Website)",
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/server": {
|
||||
"get": {
|
||||
"summary": "List running server",
|
||||
|
|
|
@ -83,6 +83,10 @@ info:
|
|||
title: LocalHostBunny
|
||||
version: "1.0"
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
responses: {}
|
||||
summary: (Website)
|
||||
/api/health:
|
||||
get:
|
||||
responses:
|
||||
|
@ -187,6 +191,14 @@ paths:
|
|||
summary: Return 200 after x seconds
|
||||
tags:
|
||||
- Common
|
||||
/index.html:
|
||||
get:
|
||||
responses: {}
|
||||
summary: (Website)
|
||||
/scripts.script.js:
|
||||
get:
|
||||
responses: {}
|
||||
summary: (Website)
|
||||
/server:
|
||||
get:
|
||||
responses:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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') }
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
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.
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
Loading…
Reference in New Issue