LocalhostBunny/logic/application.go

271 lines
6.0 KiB
Go

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"
)
type Application struct {
Config bunny.Config
stopChan chan bool
Port string
IsRunning *syncext.AtomicBool
Gin *ginext.GinWrapper
Jobs []Job
}
func NewApp() *Application {
return &Application{
stopChan: make(chan bool),
IsRunning: syncext.NewAtomicBool(false),
}
}
func (app *Application) Init(cfg bunny.Config, g *ginext.GinWrapper, jobs []Job) {
app.Config = cfg
app.Gin = g
app.Jobs = jobs
}
func (app *Application) Stop() {
syncext.WriteNonBlocking(app.stopChan, true)
}
func (app *Application) Run() {
addr := net.JoinHostPort(app.Config.ServerIP, strconv.Itoa(app.Config.ServerPort))
errChan, httpserver := app.Gin.ListenAndServeHTTP(addr, func(port string) {
app.Port = port
app.IsRunning.Set(true)
})
sigstop := make(chan os.Signal, 1)
signal.Notify(sigstop, os.Interrupt, syscall.SIGTERM)
for _, job := range app.Jobs {
err := job.Start()
if err != nil {
log.Fatal().Err(err).Msg("Failed to start job")
}
}
select {
case <-sigstop:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Info().Msg("Stopping HTTP-Server")
err := httpserver.Shutdown(ctx)
if err != nil {
log.Info().Err(err).Msg("Error while stopping the http-server")
} else {
log.Info().Msg("Stopped HTTP-Server")
}
case err := <-errChan:
log.Error().Err(err).Msg("HTTP-Server failed")
case _ = <-app.stopChan:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Info().Msg("Manually stopping HTTP-Server")
err := httpserver.Shutdown(ctx)
if err != nil {
log.Info().Err(err).Msg("Error while stopping the http-server")
} else {
log.Info().Msg("Manually stopped HTTP-Server")
}
}
for _, job := range app.Jobs {
job.Stop()
}
app.IsRunning.Set(false)
}
func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, error) {
socks4, err := netstat.TCPSocks(netstat.NoopFilter)
if err != nil {
return nil, err
}
socks6, err := netstat.TCP6Socks(netstat.NoopFilter)
if err != nil {
return nil, err
}
sockCount := len(socks4) + len(socks6)
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")
}