diff --git a/config.go b/config.go index d90292d..92eb59f 100644 --- a/config.go +++ b/config.go @@ -11,95 +11,95 @@ import ( const APILevel = 1 -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 string `env:"PORT"` - RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"` - Cors bool `env:"CORS"` -} +var SelfProcessID int -type MailConfig struct { - Host string `env:"HOST"` - Port int `env:"PORT"` - Username string `env:"USERNAME"` - Password string `env:"PASSWORD"` - Sender string `env:"SENDER"` +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"` } 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, + 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, } } 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, + 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, } } 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, + 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, } } 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, + 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, } } 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, + 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, } } @@ -143,4 +143,6 @@ func init() { } Conf = cfg + + SelfProcessID = os.Getpid() } diff --git a/jobs/runner.go b/jobs/runner.go index 761f10e..cf6484f 100644 --- a/jobs/runner.go +++ b/jobs/runner.go @@ -136,9 +136,6 @@ func (j *JobRunner[TData]) execute() (err error) { changes, err := langext.RunPanicSafeR2(func() (int, error) { return j.jobFunc(runCtx, j.app, lstr, j.data) }) - finCtx, cancelFinCtx := context.WithTimeout(context.Background(), time.Minute) - defer cancelFinCtx() - //goland:noinspection GoTypeAssertionOnErrors if panicerr, ok := err.(langext.PanicWrappedErr); ok { diff --git a/logic/application.go b/logic/application.go index 6d0e37c..c0ee0ee 100644 --- a/logic/application.go +++ b/logic/application.go @@ -2,15 +2,23 @@ package logic import ( "context" + "errors" + "fmt" "github.com/cakturk/go-netstat/netstat" "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/ginext" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/syncext" + "io" bunny "locbunny" "locbunny/models" "net" + "net/http" "os" "os/signal" + "strconv" + "strings" + "sync" "syscall" "time" ) @@ -45,7 +53,7 @@ func (app *Application) Stop() { func (app *Application) Run() { - addr := net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort) + addr := net.JoinHostPort(app.Config.ServerIP, strconv.Itoa(app.Config.ServerPort)) errChan, httpserver := app.Gin.ListenAndServeHTTP(addr, func(port string) { app.Port = port @@ -104,18 +112,159 @@ func (app *Application) Run() { func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, error) { - socks, err := netstat.TCPSocks(netstat.NoopFilter) + socks4, err := netstat.TCPSocks(netstat.NoopFilter) if err != nil { return nil, err } - res := make([]models.Server, 0) + socks6, err := netstat.TCP6Socks(netstat.NoopFilter) + if err != nil { + return nil, err + } - for _, sock := range socks { + sockCount := len(socks4) + len(socks6) - res = append(res, models.Server{Port: int(sock.LocalAddr.Port)}) + wg := sync.WaitGroup{} + echan := make(chan error, sockCount*3) + rchan := make(chan models.Server, sockCount*3) + + for _i := range socks4 { + i := _i + wg.Add(1) + go func() { + defer wg.Done() + + con1, err := app.verifyHTTPConn(socks4[i], "HTTP", "v4") + if err == nil { + rchan <- con1 + return + } else { + echan <- err + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + + con2, err := app.verifyHTTPConn(socks4[i], "HTTPS", "v4") + if err == nil { + rchan <- con2 + return + } else { + echan <- err + } + }() + } + + for _i := range socks6 { + i := _i + wg.Add(1) + go func() { + defer wg.Done() + + con1, err := app.verifyHTTPConn(socks6[i], "HTTP", "v6") + if err == nil { + rchan <- con1 + return + } else { + echan <- err + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + + con2, err := app.verifyHTTPConn(socks6[i], "HTTPS", "v6") + if err == nil { + rchan <- con2 + return + } else { + echan <- err + } + }() + } + + wg.Wait() + close(echan) + close(rchan) + + duplicates := make(map[int]bool, sockCount*3) + res := make([]models.Server, 0, sockCount*3) + for v := range rchan { + + if _, ok := duplicates[v.Port]; !ok { + res = append(res, v) + duplicates[v.Port] = true + } } return res, nil } + +func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, ipversion string) (models.Server, error) { + + ctx, cancel := context.WithTimeout(context.Background(), bunny.Conf.VerifyConnTimeout) + 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())) + 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))) + 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) + if err != nil { + log.Debug().Msg(fmt.Sprintf("Failed to create [%s|%s] request to %d", ipversion, strings.ToUpper(proto), sock.LocalAddr.Port)) + 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)) + return models.Server{}, err + } + + defer func() { _ = resp1.Body.Close() }() + + 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)) + return models.Server{}, err + } + + ct := resp1.Header.Get("Content-Type") + if ct != "" { + + var pnm *string = nil + var pid *int = nil + if sock.Process != nil { + pnm = langext.Ptr(sock.Process.Name) + pid = langext.Ptr(sock.Process.Pid) + } + + return models.Server{ + Port: int(sock.LocalAddr.Port), + IP: sock.LocalAddr.IP.String(), + Protocol: proto, + StatusCode: resp1.StatusCode, + Response: string(resbody), + ContentType: ct, + Process: pnm, + PID: pid, + UID: sock.UID, + SockState: sock.State.String(), + }, nil + } + + log.Debug().Msg(fmt.Sprintf("Failed to categorize [%s|%s] response from %s (Content-Type: '%s')", ipversion, strings.ToUpper(proto), url, ct)) + return models.Server{}, errors.New("invalid response-type") +} diff --git a/models/ids_gen.go b/models/ids_gen.go index 7ef7575..c0ae543 100644 --- a/models/ids_gen.go +++ b/models/ids_gen.go @@ -7,7 +7,7 @@ import "go.mongodb.org/mongo-driver/bson/bsontype" import "go.mongodb.org/mongo-driver/bson/primitive" import "gogs.mikescher.com/BlackForestBytes/goext/exerr" -const ChecksumIDGenerator = "8fa696914bf8d1c1c4b9f80be45d0a9dfbd0fed789856bf84ced2979c789b958" // GoExtVersion: 0.0.288 +const ChecksumIDGenerator = "cf5fe3d14932e535744418c0aa3751c08f392f9f117bfa5e8b04f70379879131" // GoExtVersion: 0.0.288 // ================================ AnyID (ids.go) ================================ diff --git a/models/server.go b/models/server.go index 21f40a3..fc7b565 100644 --- a/models/server.go +++ b/models/server.go @@ -1,5 +1,14 @@ package models type Server struct { - Port int + Port int `json:"port"` + IP string `json:"ip"` + Protocol string `json:"protocol"` + StatusCode int `json:"statusCode"` + Response string `json:"response"` + ContentType string `json:"contentType"` + Process *string `json:"process"` + PID *int `json:"pid"` + UID uint32 `json:"uid"` + SockState string `json:"sockState"` } diff --git a/swagger/swagger.json b/swagger/swagger.json index f0e0a30..6251f76 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -283,8 +283,35 @@ "models.Server": { "type": "object", "properties": { + "contentType": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "pid": { + "type": "integer" + }, "port": { "type": "integer" + }, + "process": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "response": { + "type": "string" + }, + "sockState": { + "type": "string" + }, + "statusCode": { + "type": "integer" + }, + "uid": { + "type": "integer" } } } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index d358ff7..f1b3c9f 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -56,8 +56,26 @@ definitions: type: object models.Server: properties: + contentType: + type: string + ip: + type: string + pid: + type: integer port: type: integer + process: + type: string + protocol: + type: string + response: + type: string + sockState: + type: string + statusCode: + type: integer + uid: + type: integer type: object host: localhost info: