diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh index e6ef03b..edf7bb3 100755 --- a/.docker/entrypoint.sh +++ b/.docker/entrypoint.sh @@ -75,6 +75,10 @@ if [ -z "$input" ] || [[ $input =~ ([^/]+)/([^:]+):([^/]+) ]]; then then args+=("--idle") fi + if [ ! -z "${DRUID_WATCH_PORTS}" ]; + then + args+=("--watch-ports") + fi echo "Running druid with args from env: ${args[@]}" exec druid "${args[@]}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33845cf..74141a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,8 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Install libpcap-dev + run: sudo apt update && sudo apt install -y libpcap-dev - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 054548e..7624523 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -6,6 +6,8 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Install libpcap-dev + run: sudo apt update && sudo apt install -y libpcap-dev - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -19,6 +21,8 @@ jobs: id: version with: version_format: "${major}.${minor}.${patch}-${{ env.BRANCH_NAME }}${increment}" + #- name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 - run: make test-integration-docker name: Run integration tests inside Docker - run: make test diff --git a/.vscode/launch.json b/.vscode/launch.json index 235026a..4007d00 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,6 +27,24 @@ "serve", "--cwd", "${workspaceFolder}/examples/minecraft", + "--watch-ports", + "-p", + "9190" + ], + }, + { + "name": "Debug Daemon serve --coldstarter (minecraft example)", + "type": "go", + "request": "launch", + "mode": "debug", + "console": "integratedTerminal", + "program": "${workspaceFolder}/main.go", + "args": [ + "serve", + "--cwd", + "${workspaceFolder}/examples/minecraft", + "--coldstarter", + "--watch-ports", "-p", "9190" ], @@ -121,7 +139,7 @@ "registry", "pull", "--cwd", - "${workspaceFolder}/examples/scroll-cwd-pull/", "registry-1.docker.io/highcard/scroll-minecraft-forge:1.20.1", + "${workspaceFolder}/examples/scroll-cwd-pull/", "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.20.1", ], }, { @@ -139,6 +157,17 @@ "${workspaceFolder}/examples/", ], }, + { + "name": "Debug Daemon port", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/main.go", + "args": [ + "port", + "3000", "9090" + ], + }, { "name": "Remote Debug Daemon serve", "type": "go", diff --git a/Dockerfile.testing b/Dockerfile.testing index ccd74ab..185f92a 100644 --- a/Dockerfile.testing +++ b/Dockerfile.testing @@ -2,18 +2,12 @@ FROM nginx WORKDIR /app -RUN apt update -RUN apt install -y ca-certificates wget jq moreutils htop procps nano net-tools gcc make +RUN apt update && apt install -y ca-certificates wget jq moreutils htop procps nano net-tools gcc make libpcap-dev openjdk-17-jdk ant netcat-traditional RUN wget https://go.dev/dl/go1.21.6.linux-$(dpkg --print-architecture).tar.gz -O go.tar.gz RUN tar -C /usr/local -xzf go.tar.gz && rm go.tar.gz -RUN apt-get update && \ - apt-get install -y openjdk-17-jdk && \ - apt-get install -y ant && \ - apt-get clean; - #/root/go/bin is not in the path ENV PATH=$PATH:/root/go/bin ENV PATH=$PATH:/usr/local/go/bin diff --git a/cmd/port_monitor.go b/cmd/port_monitor.go new file mode 100644 index 0000000..45782b1 --- /dev/null +++ b/cmd/port_monitor.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + "strconv" + "time" + + "github.com/highcard-dev/daemon/internal/core/services" + "github.com/spf13/cobra" +) + +var PortMonitorCmd = &cobra.Command{ + Use: "port", + Short: "Monitor ports", + Args: cobra.MinimumNArgs(1), + Long: "Utility to monitor ports and show their status and activity", + RunE: func(cmd *cobra.Command, args []string) error { + + ports := make([]int, len(args)) + + for idx, port := range args { + i, err := strconv.Atoi(port) + if err != nil { + return err + } + ports[idx] = i + } + + portMonitor := services.NewPortService(ports) + + go portMonitor.StartMonitoring(cmd.Context(), watchPortsInterfaces) + + for { + ps := portMonitor.GetPorts() + for _, p := range ps { + fmt.Printf("Port %s: %d, last activity %v, open: %t \n", p.Port.Name, p.Port.Port, p.InactiveSince, p.Open) + } + time.Sleep(5 * time.Second) + } + + }, +} + +func init() { + PortMonitorCmd.Flags().StringArrayVarP(&watchPortsInterfaces, "watch-ports-interfaces", "", []string{"lo0"}, "Interfaces to watch for port activity") +} diff --git a/cmd/root.go b/cmd/root.go index 2312b30..8097f42 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,6 +32,7 @@ func init() { RootCmd.AddCommand(SemverCmd) RootCmd.AddCommand(VersionCmd) RootCmd.AddCommand(ScrollCmd) + RootCmd.AddCommand(PortMonitorCmd) c, _ := os.Getwd() diff --git a/cmd/run.go b/cmd/run.go index ccd6cdd..052a853 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -39,7 +39,7 @@ var RunCmd = &cobra.Command{ logger.Log().Info("Lock file created") } - _, _, err = scrollService.Bootstrap(ignoreVersionCheck) + _, err = scrollService.Bootstrap(ignoreVersionCheck) if err != nil { return fmt.Errorf("error loading scroll: %w", err) diff --git a/cmd/serve.go b/cmd/serve.go index 6b87d7b..ea64273 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "fmt" + "runtime" "slices" "github.com/highcard-dev/daemon/cmd/server/web" @@ -23,6 +24,10 @@ var port int var shutdownWait int var additionalEndpoints []string var idleScroll bool +var watchPorts bool +var watchPortsInterfaces []string +var portInactivity uint +var useColdstarter bool var ServeCommand = &cobra.Command{ Use: "serve", @@ -92,15 +97,22 @@ to interact and monitor the Scroll Application`, } } + currentScroll := scrollService.GetCurrent() + processLauncher := services.NewProcedureLauncher(client, processManager, pluginManager, consoleService, logManager, scrollService) queueManager := services.NewQueueManager(scrollService, processLauncher) + portService := services.NewPortServiceWithScrollFile(scrollService.GetFile()) + + coldStarter := services.NewColdStarter(scrollService.GetDir(), currentScroll.GetColdStartPorts()) + scrollHandler := handler.NewScrollHandler(scrollService, pluginManager, processLauncher, queueManager) processHandler := handler.NewProcessHandler(processManager) scrollLogHandler := handler.NewScrollLogHandler(scrollService, logManager, processManager) scrollMetricHandler := handler.NewScrollMetricHandler(scrollService, processMonitor) queueHandler := handler.NewQueueHandler(queueManager) + portHandler := handler.NewPortHandler(portService) var annotationHandler *handler.AnnotationHandler = nil @@ -110,86 +122,141 @@ to interact and monitor the Scroll Application`, websocketHandler := handler.NewWebsocketHandler(authorizer, scrollService, consoleService) - s := web.NewServer(jwksUrl, scrollHandler, scrollLogHandler, scrollMetricHandler, annotationHandler, processHandler, queueHandler, websocketHandler, authorizer, cwd) + s := web.NewServer(jwksUrl, scrollHandler, scrollLogHandler, scrollMetricHandler, annotationHandler, processHandler, queueHandler, websocketHandler, portHandler, authorizer, cwd) a := s.Initialize() signals.SetupSignals(queueManager, processManager, a, shutdownWait) + if watchPorts { + logger.Log().Info("Starting port watcher") + go portService.StartMonitoring(ctx, watchPortsInterfaces) + } + if !idleScroll { - currentScroll, lock, err := scrollService.Bootstrap(ignoreVersionCheck) - if err != nil { - return err - } + doneChan := make(chan error, 1) + + go func() { + err := <-doneChan + if err != nil { + logger.Log().Error("Error in Daemon Startup", zap.Error(err)) + signals.Stop() + } + logger.Log().Info("Daemon Startup Complete") + }() + + go func() { + if useColdstarter { + if currentScroll.CanColdStart() { + err = coldStarter.StartOnce(ctx) + if err != nil { + logger.Log().Error("Error in coldstarter", zap.Error(err)) + doneChan <- err + return + } + logger.Log().Info("Coldstarter done, starting scroll") + } else { + logger.Log().Warn("No ports to start, skipping coldstarter") + } + } - newScroll := len(lock.Statuses) == 0 + lock, err := scrollService.Bootstrap(ignoreVersionCheck) + if err != nil { + doneChan <- err + return + } + + newScroll := len(lock.Statuses) == 0 - if newScroll { - logger.Log().Info("No lock file found, but init command available. Bootstrapping...") + if newScroll { + logger.Log().Info("No lock file found, but init command available. Bootstrapping...") + + logger.Log().Info("Creating lock and bootstrapping files") + //There is an error here. We need to bootstrap the files before we render out the templates in the bootstrap func above + err := scrollService.CreateLockAndBootstrapFiles() + if err != nil { + doneChan <- err + return + } + } else { + logger.Log().Info("Found lock file, bootstrapping done") + } - logger.Log().Info("Creating lock and bootstrapping files") - //There is an error here. We need to bootstrap the files before we render out the templates in the bootstrap func above - err := scrollService.CreateLockAndBootstrapFiles() + logger.Log().Info("Rendering cwd templates") + err = scrollService.RenderCwdTemplates() if err != nil { - return err + doneChan <- err + return } - } else { - logger.Log().Info("Found lock file, bootstrapping done") - } - logger.Log().Info("Rendering cwd templates") - err = scrollService.RenderCwdTemplates() - if err != nil { - return err - } + logger.Log().Info("Launching plugins") + //important to launch plugins, after the templates are rendered, sothat templates can provide for plugins + err = processLauncher.LaunchPlugins() - logger.Log().Info("Launching plugins") - //important to launch plugins, after the templates are rendered, sothat templates can provide for plugins - err = processLauncher.LaunchPlugins() + if err != nil { + doneChan <- err + return + } - if err != nil { - return err - } + logger.Log().Info("Starting queue manager") + go queueManager.Work() + + if newScroll { + logger.Log().Info("Starting scroll.init process") + //start scroll.init process + //initialize if nothing is there + err = queueManager.AddAndRememberItem(currentScroll.Init) + if err != nil { + doneChan <- err + return + } - logger.Log().Info("Starting queue manager") - go queueManager.Work() + logger.Log().Info("Writing new scroll lock") + scrollService.WriteNewScrollLock() + + logger.Log().Info("Bootstrapping done") + } - if newScroll { - logger.Log().Info("Starting scroll.init process") - //start scroll.init process - //initialize if nothing is there - err = queueManager.AddAndRememberItem(currentScroll.Init) + err = queueManager.QueueLockFile() if err != nil { - return err + doneChan <- err + return } - logger.Log().Info("Writing new scroll lock") - scrollService.WriteNewScrollLock() + //schedule crons + logger.Log().Info("Schedule crons") - logger.Log().Info("Bootstrapping done") - } + cronManager := services.NewCronManager(currentScroll.Cronjobs, queueManager) + err = cronManager.Init() - err = queueManager.QueueLockFile() - if err != nil { - return err - } + if err != nil { + doneChan <- err + return + } - //schedule crons - logger.Log().Info("Schedule crons") + var version string - cronManager := services.NewCronManager(currentScroll.Cronjobs, queueManager) - err = cronManager.Init() + if currentScroll.Version != nil { + version = currentScroll.Version.String() + } else { + version = "N/A" + } - if err != nil { - return err - } + logger.Log().Info("Active Scroll", + zap.String("Description", fmt.Sprintf("%s (%s)", currentScroll.Desc, currentScroll.Name)), + zap.String("Scroll Version", version), + zap.String("cwd", cwd)) + + doneChan <- nil + }() - logger.Log().Info("Active Scroll", - zap.String("Description", fmt.Sprintf("%s (%s)", currentScroll.Desc, currentScroll.Name)), - zap.String("Scroll Version", currentScroll.Version.String()), - zap.String("cwd", cwd)) + } else { + if useColdstarter { + go coldStarter.StartLoop(ctx) + } } + err = s.Serve(a, port) return err @@ -207,8 +274,21 @@ func init() { ServeCommand.Flags().BoolVarP(&idleScroll, "idle", "", false, "Don't start the queue manager") + ServeCommand.Flags().BoolVarP(&watchPorts, "watch-ports", "", false, "Track port activity") + + //macOS specific + + if runtime.GOOS == "darwin" { + ServeCommand.Flags().StringArrayVarP(&watchPortsInterfaces, "watch-ports-interfaces", "", []string{"lo0", "en0"}, "Interfaces to watch for port activity") + } else { + ServeCommand.Flags().StringArrayVarP(&watchPortsInterfaces, "watch-ports-interfaces", "", []string{"lo"}, "Interfaces to watch for port activity") + } + + ServeCommand.Flags().BoolVarP(&useColdstarter, "coldstarter", "", true, "Use coldstarter to not start immediately") + ServeCommand.Flags().BoolVarP(&ignoreVersionCheck, "ignore-version-check", "", false, "Ignore version check") ServeCommand.Flags().StringArrayVarP(&additionalEndpoints, "additional-endpoints", "", []string{}, "Additional endpoints to serve. Valid values: annotations") + ServeCommand.Flags().UintVarP(&portInactivity, "port-inactivity", "", 0, "Port inactivity timeout") } diff --git a/cmd/server/web/server.go b/cmd/server/web/server.go index b238c33..ec7c705 100644 --- a/cmd/server/web/server.go +++ b/cmd/server/web/server.go @@ -33,6 +33,7 @@ type Server struct { processHandler ports.ProcessHandlerInterface queueHandler ports.QueueHandlerInterface websocketHandler ports.WebsocketHandlerInterface + portHandler ports.PortHandlerInterface webdavPath string } @@ -45,6 +46,7 @@ func NewServer( processHandler ports.ProcessHandlerInterface, queueHandler ports.QueueHandlerInterface, websocketHandler ports.WebsocketHandlerInterface, + portHandler ports.PortHandlerInterface, authorizerService ports.AuthorizerServiceInterface, webdavPath string, ) *Server { @@ -62,6 +64,7 @@ func NewServer( processHandler: processHandler, queueHandler: queueHandler, websocketHandler: websocketHandler, + portHandler: portHandler, tokenAuthenticationMiddleware: middlewares.TokenAuthentication(authorizerService), webdavPath: webdavPath, } @@ -150,6 +153,8 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { wsRoutes.Get("/serve/:console", websocket.New(s.websocketHandler.HandleProcess)).Name("ws.serve") + apiRoutes.Get("/ports", s.portHandler.GetPorts).Name("ports.list") + if s.annotationHandler != nil { app.Get("/annotations", s.annotationHandler.Annotations).Name("annotations.list") } diff --git a/examples/.dockerignore b/examples/.dockerignore new file mode 100644 index 0000000..9aa334b --- /dev/null +++ b/examples/.dockerignore @@ -0,0 +1,6 @@ +*.jar +*.tgz + +world + +!**/.scroll \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore index 4c28e85..832837c 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1,2 +1,5 @@ -scroll-cwd -scroll-cwd-* \ No newline at end of file +*.* + +#except php files +!.scroll/** +!.dockerignore \ No newline at end of file diff --git a/examples/minecraft/.dockerignore b/examples/minecraft/.dockerignore deleted file mode 100644 index 564440f..0000000 --- a/examples/minecraft/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -scroll-lock.json \ No newline at end of file diff --git a/examples/minecraft/.scroll/json.lua b/examples/minecraft/.scroll/json.lua new file mode 100644 index 0000000..54d4448 --- /dev/null +++ b/examples/minecraft/.scroll/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/examples/minecraft/.scroll/packet_handler/json.lua b/examples/minecraft/.scroll/packet_handler/json.lua new file mode 100644 index 0000000..54d4448 --- /dev/null +++ b/examples/minecraft/.scroll/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/examples/minecraft/.scroll/packet_handler/minecraft.lua b/examples/minecraft/.scroll/packet_handler/minecraft.lua new file mode 100644 index 0000000..7d65f0f --- /dev/null +++ b/examples/minecraft/.scroll/packet_handler/minecraft.lua @@ -0,0 +1,189 @@ +json = require("json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function handle(data) + + hex = string.tohex(data) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + elseif hex:sub(1, 4) == "0100" then + debug_print("Received Status Packet") + sendData(pingResponse()) + return + -- check if second byte is 0x01 + elseif hex:sub(3, 4) == "01" then + debug_print("Received Ping Packet") + -- send same packet back + close(data) + return + -- login packet 0x20 0x00 + elseif hex:sub(1, 4) == "2000" then + debug_print("Received Login Packet") + + sendData(disconnectResponse()) + + -- sleep for a sec before closing + + finish() + return + else + debug_print("Received unknown packet") + debug_print("String: " .. data) + debug_print("Hex: " .. hex) + return + end + +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + -- https://minecraft.fandom.com/wiki/Formatting_codes + local messageList = {"🔴 Druid IDLE Minecraft", "To be or not to be 🤡", "Hi Marvin 💉", "Hi Adrian 💻", + "Hi Mark 📰", "§kKein Principle, weil es schon zu viele gibt"} + + realMessage = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + -- add realMessage to messageList + + table.insert(messageList, realMessage) + + local description = messageList[math.random(#messageList)] + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/examples/minecraft/.scroll/scroll-lock.json b/examples/minecraft/.scroll/scroll-lock.json index 05a36fd..169d6c9 100755 --- a/examples/minecraft/.scroll/scroll-lock.json +++ b/examples/minecraft/.scroll/scroll-lock.json @@ -1 +1 @@ -{"statuses":{"install":"done","start":"running"},"scroll_version":"0.0.1","scroll_name":"registry-1.docker.io/highcard/scroll-minecraft-spigot"} \ No newline at end of file +{"statuses":{"install":{"status":"done","exit_code":null,"last_status_change":1722713341},"start":{"status":"running","exit_code":null,"last_status_change":1722770093}},"scroll_version":"0.0.1","scroll_name":"registry-1.docker.io/highcard/scroll-minecraft-spigot"} \ No newline at end of file diff --git a/examples/minecraft/.scroll/scroll.yaml b/examples/minecraft/.scroll/scroll.yaml index 887315e..3cd1fc0 100644 --- a/examples/minecraft/.scroll/scroll.yaml +++ b/examples/minecraft/.scroll/scroll.yaml @@ -2,6 +2,12 @@ name: registry-1.docker.io/highcard/scroll-minecraft-spigot desc: Minecraft Spigot version: 0.0.1 app_version: 1.20.4 +ports: + - name: minecraft + protocol: tcp + port: 25565 + sleep_handler: packet_handler/minecraft.lua + init: "start" commands: start: diff --git a/examples/minecraft/eula.txt b/examples/minecraft/eula.txt deleted file mode 100644 index 02dccd9..0000000 --- a/examples/minecraft/eula.txt +++ /dev/null @@ -1 +0,0 @@ -eula=true diff --git a/examples/minecraft/spigot.jar b/examples/minecraft/spigot.jar deleted file mode 100644 index c87d08b..0000000 Binary files a/examples/minecraft/spigot.jar and /dev/null differ diff --git a/examples/minecraft/start.sh b/examples/minecraft/start.sh deleted file mode 100755 index b1695fc..0000000 --- a/examples/minecraft/start.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -MAX=${DRUID_MAX_MEMORY%?} -if [ -z "${MAX}" ]; -then - MAX=1024M -fi - -java -Xmx$MAX -Xms1024M -jar spigot.jar nogui \ No newline at end of file diff --git a/go.mod b/go.mod index 9aa8b19..78b77bf 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/contrib/websocket v1.3.2 github.com/gofiber/fiber/v2 v2.52.5 - github.com/gofiber/swagger v0.1.14 github.com/hashicorp/go-plugin v1.6.1 github.com/opencontainers/image-spec v1.1.0 github.com/prometheus/client_golang v1.19.1 @@ -95,8 +94,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/swaggo/files/v2 v2.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect ) @@ -107,9 +106,12 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/gofiber/jwt/v3 v3.3.10 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/gopacket/gopacket v1.2.0 github.com/gorilla/websocket v1.5.3 github.com/highcard-dev/gorcon v1.3.10 github.com/otiai10/copy v1.14.0 + github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 + github.com/yuin/gopher-lua v1.1.1 go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 4a1d81b..e92ee52 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -11,8 +10,6 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= @@ -22,7 +19,6 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -49,11 +45,9 @@ github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -66,13 +60,10 @@ github.com/gofiber/adaptor/v2 v2.2.1/go.mod h1:AhR16dEqs25W2FY/l8gSj1b51Azg5dtPD github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg= github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8= github.com/gofiber/fiber/v2 v2.45.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= -github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/jwt/v3 v3.3.10 h1:0bpWtFKaGepjwYTU4efHfy0o+matSqZwTxGMo5a+uuc= github.com/gofiber/jwt/v3 v3.3.10/go.mod h1:GJorFVaDyfMPSK9RB8RG4NQ3s1oXKTmYaoL/ny08O1A= -github.com/gofiber/swagger v0.1.14 h1:o524wh4QaS4eKhUCpj7M0Qhn8hvtzcyxDsfZLXuQcRI= -github.com/gofiber/swagger v0.1.14/go.mod h1:DCk1fUPsj+P07CKaZttBbV1WzTZSQcSxfub8y9/BFr8= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -81,10 +72,11 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= +github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -110,7 +102,6 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -137,7 +128,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -162,6 +152,8 @@ github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 h1:p4VuaitqUAqSZSomd7Wb4BPV/Jj7Hno2/iqtfX7DZJI= +github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5/go.mod h1:zIAoVKeWP0mz4zXY50UYQt6NLg2uwKRswMDcGEqOms4= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= @@ -187,7 +179,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -201,7 +192,8 @@ github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3 github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -231,9 +223,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= -github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= -github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= @@ -242,17 +231,17 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= -github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -278,14 +267,12 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -305,12 +292,12 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -318,7 +305,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -328,7 +314,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -341,7 +326,6 @@ golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -362,14 +346,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/core/domain/scroll.go b/internal/core/domain/scroll.go index feac148..d0b6ce4 100644 --- a/internal/core/domain/scroll.go +++ b/internal/core/domain/scroll.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "time" semver "github.com/Masterminds/semver/v3" "github.com/highcard-dev/daemon/internal/utils/logger" @@ -25,12 +26,27 @@ type Cronjob struct { Command string `yaml:"command"` } +type Port struct { + Port int `yaml:"port" json:"port"` + Protocol string `yaml:"protocol" json:"protocol"` + Name string `yaml:"name" json:"name"` + SleepHandler *string `yaml:"sleep_handler" json:"sleep_handler"` +} + +type AugmentedPort struct { + Port + InactiveSince time.Time `json:"inactive_since"` + InactiveSinceSec uint `json:"inactive_since_sec"` + Open bool `json:"open"` +} + type File struct { Name string `yaml:"name" json:"name"` Desc string `yaml:"desc" json:"desc"` Version *semver.Version `yaml:"version" json:"version"` AppVersion string `yaml:"app_version" json:"app_version"` //don't make this a semver, it's not allways Init string `yaml:"init" json:"init"` + Ports []Port `yaml:"ports" json:"ports"` Commands map[string]*CommandInstructionSet `yaml:"commands" json:"commands"` Plugins map[string]map[string]string `yaml:"plugins" json:"plugins"` Cronjobs []*Cronjob `yaml:"cronjobs" json:"cronjobs"` @@ -139,3 +155,22 @@ func (sc *Scroll) Validate() error { } return nil } + +func (sc *Scroll) CanColdStart() bool { + for _, port := range sc.Ports { + if port.SleepHandler != nil { + return true + } + } + return false +} + +func (sc *Scroll) GetColdStartPorts() []Port { + var ports []Port + for _, port := range sc.Ports { + if port.SleepHandler != nil { + ports = append(ports, port) + } + } + return ports +} diff --git a/internal/core/ports/handler_ports.go b/internal/core/ports/handler_ports.go index 1806620..9afa81a 100644 --- a/internal/core/ports/handler_ports.go +++ b/internal/core/ports/handler_ports.go @@ -38,3 +38,7 @@ type ProcessHandlerInterface interface { type QueueHandlerInterface interface { Queue(c *fiber.Ctx) error } + +type PortHandlerInterface interface { + GetPorts(c *fiber.Ctx) error +} diff --git a/internal/core/ports/services_ports.go b/internal/core/ports/services_ports.go index 097264f..92ec37c 100644 --- a/internal/core/ports/services_ports.go +++ b/internal/core/ports/services_ports.go @@ -1,6 +1,7 @@ package ports import ( + "context" "time" "github.com/gofiber/fiber/v2" @@ -97,3 +98,14 @@ type QueueManagerInterface interface { AddShutdownItem(cmd string) error GetQueue() map[string]domain.ScrollLockStatus } + +type PortServiceInterface interface { + StartMonitoring(context.Context, []string) + GetLastActivity(port int) uint + CheckOpen(prot int) bool + GetPorts() []*domain.AugmentedPort +} + +type ColdStarterHandlerInterface interface { + Handle(data []byte, funcs map[string]func(data ...string)) error +} diff --git a/internal/core/services/coldstarter.go b/internal/core/services/coldstarter.go new file mode 100644 index 0000000..36766aa --- /dev/null +++ b/internal/core/services/coldstarter.go @@ -0,0 +1,106 @@ +package services + +import ( + "context" + "fmt" + + "github.com/highcard-dev/daemon/internal/core/domain" + "github.com/highcard-dev/daemon/internal/core/ports" + lua "github.com/highcard-dev/daemon/internal/core/services/coldstarter/handler" + "github.com/highcard-dev/daemon/internal/core/services/coldstarter/servers" + "github.com/highcard-dev/daemon/internal/utils/logger" +) + +type ColdStarter struct { + handler map[string]uint + finishCount uint + dir string + ports []domain.Port +} + +func NewColdStarter( + dir string, + ports []domain.Port, +) *ColdStarter { + return &ColdStarter{ + make(map[string]uint), + 0, + dir, + ports, + } +} + +func (c ColdStarter) StartLoop(ctx context.Context) error { + return c.Start(ctx, false) +} +func (c ColdStarter) StartOnce(ctx context.Context) error { + return c.Start(ctx, true) +} + +func (c ColdStarter) Start(ctx context.Context, stopAfterFirst bool) error { + augmentedPorts := c.ports + + luactx, cancel := context.WithCancel(context.Background()) + + doneChan := make(chan error) + + finishFunc := func() { + c.finishCount++ + if stopAfterFirst { + doneChan <- nil + } + } + + for _, port := range augmentedPorts { + if port.SleepHandler == nil { + logger.Log().Warn(fmt.Sprintf("No sleep handler found for port %d", port.Port)) + continue + } + + path := fmt.Sprintf("%s/%s", c.dir, *port.SleepHandler) + + go func(port domain.Port) { + + var handler ports.ColdStarterHandlerInterface + + if *port.SleepHandler == "generic" { + handler = lua.NewGenericHandler() + } else { + handler = lua.NewLuaHandler(path, c.dir) + } + + if port.Protocol == "udp" { + logger.Log().Info(fmt.Sprintf("Starting UDP server on port %d", port.Port)) + udpServer := servers.NewUDP(handler) + err := udpServer.Start(luactx, port.Port, finishFunc) + if err != nil { + doneChan <- err + } + } else if port.Protocol == "tcp" { + logger.Log().Info(fmt.Sprintf("Starting TCP server on port %d", port.Port)) + tcpServer := servers.NewTCP(handler) + err := tcpServer.Start(luactx, port.Port, finishFunc) + if err != nil { + doneChan <- err + } + } else { + logger.Log().Warn(fmt.Sprintf("Unknown protocol %s for coldstarter", port.Protocol)) + return + } + logger.Log().Info(fmt.Sprintf("Server on port %d received finish signal", port.Port)) + }(port) + } + + select { + case err := <-doneChan: + cancel() + return err + case <-ctx.Done(): + cancel() + return nil + } +} + +func (c ColdStarter) FinishCount() uint { + return c.finishCount +} diff --git a/internal/core/services/coldstarter/handler/generic_handler.go b/internal/core/services/coldstarter/handler/generic_handler.go new file mode 100644 index 0000000..9f68c41 --- /dev/null +++ b/internal/core/services/coldstarter/handler/generic_handler.go @@ -0,0 +1,27 @@ +package lua + +import ( + "fmt" + + "github.com/highcard-dev/daemon/internal/utils/logger" + "go.uber.org/zap" +) + +type GenericHandler struct{} + +func NewGenericHandler() *GenericHandler { + return &GenericHandler{} +} + +func (handler *GenericHandler) Handle(data []byte, funcs map[string]func(data ...string)) error { + finishFunc, ok := funcs["finish"] + + if !ok { + return fmt.Errorf("finish function not found") + } + + logger.Log().Info("Executing finish func in generic handler", zap.String("data", string(data))) + + finishFunc() + return nil +} diff --git a/internal/core/services/coldstarter/handler/lua_handler.go b/internal/core/services/coldstarter/handler/lua_handler.go new file mode 100644 index 0000000..2b339db --- /dev/null +++ b/internal/core/services/coldstarter/handler/lua_handler.go @@ -0,0 +1,105 @@ +package lua + +import ( + "fmt" + + "github.com/highcard-dev/daemon/internal/utils/logger" + lua "github.com/yuin/gopher-lua" + "go.uber.org/zap" +) + +type LuaHandler struct { + file string + luaPath string +} + +func NewLuaHandler(file string, luaPath string) *LuaHandler { + handler := &LuaHandler{file: file, luaPath: luaPath} + return handler +} + +func (handler *LuaHandler) Handle(data []byte, funcs map[string]func(data ...string)) error { + l := lua.NewState(lua.Options{ + RegistrySize: 256 * 200, + }) + + for name, f := range funcs { + // Create a new variable to capture the current function + currentFunc := f + + var fn *lua.LFunction + + switch name { + case "sendData": + fn = l.NewFunction(func(l *lua.LState) int { + arg := l.CheckString(1) + logger.Log().Debug("Called lua fn sendData", zap.String("arg", arg), zap.String("file", handler.file)) + currentFunc(arg) + return 1 + }) + case "finish": + fn = l.NewFunction(func(l *lua.LState) int { + logger.Log().Debug("Called lua fn sendData", zap.String("file", handler.file)) + currentFunc() + return 0 + }) + case "close": + fn = l.NewFunction(func(l *lua.LState) int { + arg := l.CheckString(1) + logger.Log().Debug("Called lua fn sendData", zap.String("arg", arg), zap.String("file", handler.file)) + currentFunc(arg) + return 1 + }) + default: + return fmt.Errorf("unsupported function: %s", name) + } + l.SetGlobal(name, fn) + } + + l.SetGlobal("debug_print", l.NewFunction( + func(l *lua.LState) int { + arg := l.CheckString(1) + logger.Log().Debug(arg) + return 0 + }, + )) + + // set package.path to include the luaPath + l.DoString(fmt.Sprintf("package.path = package.path .. ';;%s/?.lua'", handler.luaPath)) + + if err := l.DoFile(handler.file); err != nil { + return err + } + + //call handler function + if err := callLuaFunction(l, "handle", data); err != nil { + return err + } + + return nil +} + +func callLuaFunction(l *lua.LState, functionName string, args ...interface{}) error { + var luaArgs []lua.LValue + for _, arg := range args { + switch arg.(type) { + case []byte: + luaArgs = append(luaArgs, lua.LString(string(arg.([]byte)))) + case string: + luaArgs = append(luaArgs, lua.LString(arg.(string))) + case int: + luaArgs = append(luaArgs, lua.LNumber(arg.(int))) + default: + return fmt.Errorf("unsupported argument type: %T", arg) + } + } + + if err := l.CallByParam(lua.P{ + Fn: l.GetGlobal(functionName), + NRet: len(luaArgs), + Protect: true, + }, luaArgs...); err != nil { + return err + } + return nil +} diff --git a/internal/core/services/coldstarter/servers/tcp.go b/internal/core/services/coldstarter/servers/tcp.go new file mode 100644 index 0000000..b81061a --- /dev/null +++ b/internal/core/services/coldstarter/servers/tcp.go @@ -0,0 +1,116 @@ +package servers + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "time" + + "github.com/highcard-dev/daemon/internal/core/ports" + "github.com/highcard-dev/daemon/internal/utils/logger" + "go.uber.org/zap" +) + +type TCPServer interface { + Start(port int, handlerFile string) +} + +type TCP struct { + handler ports.ColdStarterHandlerInterface + listener net.Listener + onFinish func() +} + +func NewTCP(handler ports.ColdStarterHandlerInterface) *TCP { + return &TCP{ + handler: handler, + } +} + +func (t *TCP) Start(ctx context.Context, port int, onFinish func()) error { + ser, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return fmt.Errorf("failed to resolve address [%v]", err) + } + tcp, err := net.ListenTCP("tcp", ser) + if err != nil { + return fmt.Errorf("failed to bind [%v]", err) + } + t.listener = tcp + t.onFinish = onFinish + + go func() { + for { + con, err := tcp.AcceptTCP() + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" { + logger.Log().Info("TCP Server stopped") + return + } + logger.Log().Warn("Error accepting TCP connection", zap.Error(err)) + continue + } + + _ = con.SetNoDelay(true) + _ = con.SetKeepAlive(true) + go t.handleConnection(con) + } + }() + + <-ctx.Done() + t.listener.Close() + + return nil +} +func (t *TCP) handleConnection(conn net.Conn) { + + sendFunc := func(data ...string) { + if len(data) == 0 { + return + } + _, err := conn.Write([]byte(data[0])) + if err != nil { + logger.Log().Error("Error sending data", zap.Error(err)) + } + } + + reader := bufio.NewReader(conn) + for { + // Adjust this buffer size based on your expected packet size + buffer := make([]byte, 1024) + n, err := reader.Read(buffer) + if err != nil { + if err != io.EOF { + logger.Log().Error("Error reading from connection", zap.Error(err)) + } + conn.Close() + break + } + + data := buffer[:n] + + err = t.handler.Handle(data, map[string]func(data ...string){ + "sendData": sendFunc, + "finish": func(data ...string) { + fmt.Println("Connection closed") + logger.Log().Info("Finish received", zap.Strings("data", data), zap.String("type", "tcp"), zap.String("address", conn.RemoteAddr().String())) + <-time.After(time.Second) + t.onFinish() + <-time.After(time.Second) + conn.Close() + }, + "close": func(data ...string) { + sendFunc(data...) + //wait for 1 second before closing the connection + <-time.After(time.Second) + conn.Close() + }, + }) + + if err != nil { + logger.Log().Error("Error handling packet", zap.Error(err)) + } + } +} diff --git a/internal/core/services/coldstarter/servers/udp.go b/internal/core/services/coldstarter/servers/udp.go new file mode 100644 index 0000000..275f34a --- /dev/null +++ b/internal/core/services/coldstarter/servers/udp.go @@ -0,0 +1,94 @@ +package servers + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/highcard-dev/daemon/internal/core/ports" + "github.com/highcard-dev/daemon/internal/utils/logger" + "go.uber.org/zap" +) + +type UDPServer interface { + Start(port int) +} + +type UDP struct { + handler ports.ColdStarterHandlerInterface + conn *net.UDPConn + onFinish func() +} + +func NewUDP(handler ports.ColdStarterHandlerInterface) *UDP { + return &UDP{ + handler: handler, + } +} + +func (u *UDP) Start(ctx context.Context, port int, onFinish func()) error { + addr := net.UDPAddr{ + Port: port, + IP: net.IPv4zero, + } + conn, err := net.ListenUDP("udp", &addr) + if err != nil { + return fmt.Errorf("failed to bind [%v]", err) + } + u.conn = conn + u.onFinish = onFinish + + go func() { + buf := make([]byte, 1024) + for { + n, remoteAddr, err := u.conn.ReadFromUDP(buf) + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" { + logger.Log().Info("UDP Server stopped") + return + } + logger.Log().Warn("Error reading from UDP connection", zap.Error(err)) + continue + } + + go u.handleConnection(buf[:n], remoteAddr) + } + }() + + <-ctx.Done() + u.conn.Close() + + return nil +} + +func (u *UDP) handleConnection(data []byte, remoteAddr *net.UDPAddr) { + sendFunc := func(data ...string) { + if len(data) == 0 { + return + } + u.conn.WriteToUDP([]byte(data[0]), remoteAddr) + } + + err := u.handler.Handle(data, map[string]func(data ...string){ + "sendData": sendFunc, + "finish": func(data ...string) { + fmt.Println("Connection closed") + logger.Log().Info("Finish received", zap.Strings("data", data), zap.String("type", "udp"), zap.String("address", remoteAddr.String())) + <-time.After(time.Second) + u.onFinish() + <-time.After(time.Second) + u.conn.Close() + }, + "close": func(data ...string) { + sendFunc(data...) + //wait for 1 second before closing the connection + <-time.After(time.Second) + u.conn.Close() + }, + }) + + if err != nil { + logger.Log().Error("Error handling packet", zap.Error(err)) + } +} diff --git a/internal/core/services/port_service.go b/internal/core/services/port_service.go new file mode 100644 index 0000000..253d896 --- /dev/null +++ b/internal/core/services/port_service.go @@ -0,0 +1,250 @@ +package services + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/highcard-dev/daemon/internal/core/domain" + "github.com/highcard-dev/daemon/internal/utils/logger" + pcap "github.com/packetcap/go-pcap" + "github.com/shirou/gopsutil/net" + "go.uber.org/zap" +) + +type PortMonitor struct { + ports []*domain.AugmentedPort + portPoolInterval time.Duration +} + +func NewPortServiceWithScrollFile( + file *domain.File, +) *PortMonitor { + p := &PortMonitor{ + portPoolInterval: 5 * time.Second, + } + p.SyncPortEnv(file) + return p +} + +func NewPortService(ports []int) *PortMonitor { + ap := make([]*domain.AugmentedPort, len(ports)) + + for idx, port := range ports { + ap[idx] = &domain.AugmentedPort{ + Port: domain.Port{ + Name: fmt.Sprintf("port%d", port), + Port: port, + }, + } + } + + p := &PortMonitor{ + ports: ap, + portPoolInterval: 5 * time.Second, + } + return p +} + +func (p *PortMonitor) SyncPortEnv(file *domain.File) []*domain.AugmentedPort { + ports := file.Ports + + augmentedPorts := make([]*domain.AugmentedPort, len(ports)) + + for idx, port := range ports { + portEnvName := fmt.Sprintf("DRUID_PORT_%s", strings.ToUpper(port.Name)) + envPort := os.Getenv(portEnvName) + + if envPort != "" { + portInt, err := strconv.Atoi(envPort) + if err != nil { + port.Port = portInt + } + } + + ap := &domain.AugmentedPort{ + Port: port, + } + + augmentedPorts[idx] = ap + os.Setenv(portEnvName, strconv.Itoa(port.Port)) + } + + p.ports = augmentedPorts + return p.ports +} + +func (p *PortMonitor) GetLastActivity(port int) uint { + for _, p := range p.ports { + if p.Port.Port == port { + return uint(time.Since(p.InactiveSince).Seconds()) + } + } + + return 0 +} + +func (po *PortMonitor) GetPorts() []*domain.AugmentedPort { + for _, p := range po.ports { + p.Open = po.CheckOpen(p.Port.Port) + + inactiveCorrected := time.Since(p.InactiveSince) - po.portPoolInterval + if inactiveCorrected < 0 { + p.InactiveSinceSec = 0 + } else { + p.InactiveSinceSec = uint(inactiveCorrected.Seconds()) + } + } + + return po.ports +} + +func (p *PortMonitor) CheckOpen(port int) bool { + //check if port is open + + connections, err := net.Connections("inet") + if err != nil { + return false + } + + for _, conn := range connections { + if conn.Laddr.Port == uint32(port) { + return true + } + } + return false +} + +func (p *PortMonitor) WaitForConnection(ifaces []string) { + + for { + ports := make([]int, len(p.ports)) + for idx, port := range p.ports { + ports[idx] = port.Port.Port + } + + firstOnlinePort, err := p.StartMonitorPorts(ports, ifaces, 5*time.Minute) + + if err != nil { + logger.Log().Error("Error on port monitoring", zap.Error(err)) + } else { + if firstOnlinePort == nil { + break + } + + for _, port := range p.ports { + //this is not right but sufficient for now, later we should only update one port + port.InactiveSince = time.Now() + } + } + + time.Sleep(p.portPoolInterval) + } +} + +func (p *PortMonitor) StartMonitoring(ctx context.Context, ifaces []string) { + //start monitoring the ports + for { + select { + case <-ctx.Done(): + return + default: + p.WaitForConnection(ifaces) + } + } +} + +func (p *PortMonitor) StartMonitorPorts(ports []int, ifaces []string, timeout time.Duration) (*int, error) { + + // Find all network interfaces + + logger.Log().Debug("Found interfaces", zap.Strings("ifaces", ifaces), zap.Strings("requestedInterfaces", ifaces)) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + + var doneIface string + var donePort int + + for _, iface := range ifaces { + go func(po []int, i string) { + port, err := p.waitForPortActiviy(ctx, ports, i) + if err != nil { + logger.Log().Error("Error on port monitoring", zap.String("iface", i), zap.Ints("ports", po), zap.Error(err)) + return + } + + if port == 0 { + return + } + donePort = port + doneIface = i + cancel() + }(ports, iface) + } + + <-ctx.Done() + + //this is not needed, but it's a good practice to call it + cancel() + + if doneIface != "" { + logger.Log().Debug("Port activity found", zap.String("iface", doneIface), zap.Int("port", donePort)) + return &donePort, nil + } else { + logger.Log().Debug("No port activity found on any interface\n") + return nil, nil + } + +} + +func (p *PortMonitor) waitForPortActiviy(ctx context.Context, ports []int, interfaceName string) (int, error) { + + handle, err := pcap.OpenLive(interfaceName, 1600, true, time.Hour, false) + if err != nil { + return 0, err + } + + go func() { + <-ctx.Done() + logger.Log().Debug("Closing handle ", zap.String("iface", interfaceName), zap.Ints("ports", ports)) + handle.Close() + }() + + portFilterParts := make([]string, len(ports)) + + for idx, port := range ports { + portFilterParts[idx] = fmt.Sprintf("port %d", port) + } + + filter := strings.Join(portFilterParts, " or ") + + err = handle.SetBPFFilter(filter) + if err != nil { + return 0, err + } + logger.Log().Debug("Listening on iface", zap.String("iface", interfaceName), zap.Ints("ports", ports)) + + lt1 := layers.LinkType(handle.LinkType()) + + packetSource := gopacket.NewPacketSource(handle, lt1) + for packet := range packetSource.Packets() { + // Process the packet here + if packet.ApplicationLayer() == nil { + continue + } else { + packetPort := packet.TransportLayer().TransportFlow().Dst().String() + packetPortInt, err := strconv.Atoi(packetPort) + if err != nil { + packetPortInt = 0 + } + logger.Log().Debug("Packet found on iface", zap.String("iface", interfaceName), zap.Int("port", packetPortInt)) + return packetPortInt, nil + } + } + return 0, nil +} diff --git a/internal/core/services/procedure_launcher.go b/internal/core/services/procedure_launcher.go index 66f2b48..eb7fe46 100644 --- a/internal/core/services/procedure_launcher.go +++ b/internal/core/services/procedure_launcher.go @@ -139,7 +139,7 @@ func (sc *ProcedureLauncher) Run(cmd string, runCommandCb func(cmd string) error func (sc *ProcedureLauncher) RunProcedure(proc *domain.Procedure, cmd string) (string, *int, error) { - logger.Log().Debug("Running procedure", + logger.Log().Info("Running procedure", zap.String("cmd", cmd), zap.String("mode", proc.Mode), zap.Any("data", proc.Data), diff --git a/internal/core/services/process_monitor.go b/internal/core/services/process_monitor.go index c06750d..2fd6bc7 100644 --- a/internal/core/services/process_monitor.go +++ b/internal/core/services/process_monitor.go @@ -86,13 +86,6 @@ func (po *ProcessMonitor) RefreshMetrics() { zap.String("processName", name), zap.Error(err), ) - if err == ErrorProcessNotActive { - po.RemoveProcess(name) - logger.Log().Info("Process not active, removing from monitoring", - zap.String(logger.LogKeyContext, logger.LogContextMonitor), - zap.String("processName", name), - ) - } } } } diff --git a/internal/core/services/queue_manager.go b/internal/core/services/queue_manager.go index e800c2f..4dc91ca 100644 --- a/internal/core/services/queue_manager.go +++ b/internal/core/services/queue_manager.go @@ -179,10 +179,14 @@ func (sc *QueueManager) QueueLockFile() error { if status.Status == domain.ScrollLockStatusDone { //not sure if this can even happen if command.Run != domain.RunModeRestart { + + //TODO: use addQueueItem here + sc.mu.Lock() sc.commandQueue[cmd] = &domain.QueueItem{ Status: domain.ScrollLockStatusDone, UpdateLockStatus: true, } + sc.mu.Unlock() continue } } @@ -282,8 +286,10 @@ func (sc *QueueManager) RunQueue() { //restart means we are never done! if command.Run == domain.RunModeRestart { + logger.Log().Info("Command done, restarting..", zap.String("command", c)) sc.setStatus(c, domain.ScrollLockStatusWaiting, i.UpdateLockStatus) } else { + logger.Log().Info("Command done", zap.String("command", c)) sc.setStatus(c, domain.ScrollLockStatusDone, i.UpdateLockStatus) } diff --git a/internal/core/services/scroll_service.go b/internal/core/services/scroll_service.go index 10106cf..75bf8bf 100644 --- a/internal/core/services/scroll_service.go +++ b/internal/core/services/scroll_service.go @@ -51,7 +51,7 @@ func (sc *ScrollService) LoadScroll() (*domain.Scroll, error) { } // Load Scroll and render templates in the cwd -func (sc *ScrollService) Bootstrap(ignoreVersionCheck bool) (*domain.Scroll, *domain.ScrollLock, error) { +func (sc *ScrollService) Bootstrap(ignoreVersionCheck bool) (*domain.ScrollLock, error) { var scroll = sc.scroll @@ -66,11 +66,11 @@ func (sc *ScrollService) Bootstrap(ignoreVersionCheck bool) (*domain.Scroll, *do lock.Write() } else { if !lock.ScrollVersion.Equal(sc.scroll.Version) && !ignoreVersionCheck { - return scroll, lock, errors.New("scroll version mismatch") + return lock, errors.New("scroll version mismatch") } } - return scroll, lock, nil + return lock, nil } func (sc *ScrollService) CreateLockAndBootstrapFiles() error { diff --git a/internal/handler/port_handler.go b/internal/handler/port_handler.go new file mode 100644 index 0000000..05f2afd --- /dev/null +++ b/internal/handler/port_handler.go @@ -0,0 +1,31 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/core/ports" +) + +type PortHandler struct { + portService ports.PortServiceInterface +} + +func NewPortHandler( + portService ports.PortServiceInterface, +) *PortHandler { + return &PortHandler{ + portService, + } +} + +// @Summary Get ports from scroll with additional information +// @ID getPorts +// @Tags port, druid, daemon +// @Accept */* +// @Produce json +// @Success 200 {object} domain.AugmentedPort +// @Router /api/v1/ports [get] +func (p PortHandler) GetPorts(c *fiber.Ctx) error { + augmentedPorts := p.portService.GetPorts() + + return c.JSON(augmentedPorts) +} diff --git a/test/integration/commands/serve_coldstarter_test.go b/test/integration/commands/serve_coldstarter_test.go new file mode 100644 index 0000000..bb9445a --- /dev/null +++ b/test/integration/commands/serve_coldstarter_test.go @@ -0,0 +1,336 @@ +package command_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "net" + "os" + "strconv" + "testing" + "time" + + "github.com/highcard-dev/daemon/cmd" + "github.com/highcard-dev/daemon/internal/core/domain" + "github.com/highcard-dev/daemon/internal/signals" + "github.com/highcard-dev/daemon/internal/utils/logger" + "gopkg.in/yaml.v2" +) + +func writeScroll(scroll domain.File, path string) error { + + b, err := yaml.Marshal(scroll) + if err != nil { + return err + } + + return os.WriteFile(path, b, 0644) +} + +func waitUntilFileExists(path string, duration time.Duration) error { + + timeout := time.After(duration) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout: + return errors.New("timeout waiting for file to exist") + case <-ticker.C: + if _, err := os.Stat(path); err == nil { + return nil + } + } + } +} + +var genericHandler = "generic" +var testHandler = "test.lua" + +var luaHandlerContent = ` +function handle(data) + if data == "test" then + sendData("testback") + finish() + end +end +` + +var testCommand = map[string]*domain.CommandInstructionSet{ + "start": { + Procedures: []*domain.Procedure{ + { + Mode: "exec", + Data: []string{"touch", "test.txt"}, + }, + }, + }, +} + +var tcpTester = func(answer string, port int) error { + println("dial tcpTester") + //tcp connect to 12349 and send test data + con, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return fmt.Errorf("failed to dial tcp: %v", err) + } + defer con.Close() + + println("write tcpTester") + _, err = con.Write([]byte("test")) + if err != nil { + return fmt.Errorf("failed to write test data: %v", err) + } + + if answer == "" { + return nil + } + + println("read tcpTester") + data := make([]byte, 1024) + n, err := con.Read(data) + if err != nil { + return fmt.Errorf("failed to read response: %v", err) + } + + dataStr := string(data[:n]) + if dataStr != answer { + return fmt.Errorf("unexpected response: %s != %s ", dataStr, answer) + } + return nil +} + +var udpTester = func(answer string, port int) error { + println("dial udpTester") + //udp connect to 12349 and send test data + con, err := net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return fmt.Errorf("failed to dial udp: %v", err) + } + defer con.Close() + + println("write udpTester") + _, err = con.Write([]byte("test")) + if err != nil { + return fmt.Errorf("failed to write test data: %v", err) + } + + if answer == "" { + return nil + } + + println("read udpTester") + data := make([]byte, 1024) + n, _, err := con.ReadFromUDP(data) + if err != nil { + return fmt.Errorf("failed to read response: %v", err) + } + + dataStr := string(data[:n]) + if dataStr != answer { + return fmt.Errorf("unexpected response: %s != %s ", dataStr, answer) + } + return nil +} + +var setupScroll = func(t *testing.T, scroll domain.File) (string, string) { + + logger.Log(logger.WithStructuredLogging()) + + //observer := logger.SetupLogsCapture() + unixTime := time.Now().Unix() + cwd := "./druid-cli-test/" + strconv.FormatInt(unixTime, 10) + "/" + scrollPath := cwd + ".scroll/" + + if err := os.MkdirAll(scrollPath, 0755); err != nil { + t.Fatalf("Failed to create test cwd: %v", err) + } + + err := writeScroll(scroll, scrollPath+"scroll.yaml") + if err != nil { + t.Fatalf("Failed to write test scroll file: %v", err) + } + return scrollPath, cwd +} + +var setupServeCmd = func(ctx context.Context, t *testing.T, cwd string, additionalArgs []string) { + + b := bytes.NewBufferString("") + + serveCmd := cmd.RootCmd + serveCmd.SetErr(b) + serveCmd.SetOut(b) + serveCmd.SetArgs(append([]string{"--cwd", cwd, "serve"}, additionalArgs...)) + + // Create a new context for each test case + + cmd.ServeCommand.SetContext(ctx) + + connected, err := startAndTestServeCommand(ctx, t, serveCmd) + if !connected { + t.Fatalf("Failed to connect to daemon web server: %v", err) + } +} + +func TestColdstarterServeCommand(t *testing.T) { + + type TestCase struct { + Name string + Scroll domain.File + ExecColdStarterFn func(string, int) error + LuaHandlerContent string + } + var testCases = []TestCase{ + { + Name: "TestServeColdstarterEmtpty", + Scroll: domain.File{ + Ports: []domain.Port{}, + Init: "start", + Commands: testCommand, + }, + }, + { + Name: "TestServeColdstarterWithoutHandler", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12350, + Name: "testport", + Protocol: "tcp", + }, + }, + Init: "start", + Commands: testCommand, + }, + }, + { + Name: "TestServeColdstarterWithoutHandler2", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12350, + Name: "testport", + Protocol: "tcp", + }, + { + Port: 12351, + Name: "testport2", + Protocol: "tcp", + }, + }, + Init: "start", + Commands: testCommand, + }, + }, { + Name: "TestServeColdstarterWithGenericTCPHandler", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12352, + Name: "testport", + Protocol: "tcp", + SleepHandler: &genericHandler, + }, + }, + Init: "start", + Commands: testCommand, + }, + ExecColdStarterFn: tcpTester, + }, { + Name: "TestServeColdstarterWithTestLuaTCPHandler", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12353, + Name: "testport", + Protocol: "tcp", + SleepHandler: &testHandler, + }, + }, + Init: "start", + Commands: testCommand, + }, + LuaHandlerContent: luaHandlerContent, + ExecColdStarterFn: tcpTester, + }, + { + Name: "TestServeColdstarterWithGenericUDPHandler", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12354, + Name: "testport", + Protocol: "udp", + SleepHandler: &genericHandler, + }, + }, + Init: "start", + Commands: testCommand, + }, + ExecColdStarterFn: udpTester, + }, + + { + Name: "TestServeColdstarterWithTestLuaUDPHandler", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12349, + Name: "testport", + Protocol: "udp", + SleepHandler: &testHandler, + }, + }, + Init: "start", + Commands: testCommand, + }, + LuaHandlerContent: luaHandlerContent, + ExecColdStarterFn: udpTester, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + println(tc.Name) + scrollPath, path := setupScroll(t, tc.Scroll) + defer os.RemoveAll(path) + + if tc.LuaHandlerContent != "" { + err := os.WriteFile(scrollPath+testHandler, []byte(tc.LuaHandlerContent), 0644) + if err != nil { + t.Fatalf("Failed to write test lua handler file: %v", err) + } + } + + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(errors.New("test ended")) + + setupServeCmd(ctx, t, path, []string{}) + + defer func() { + signals.Stop() + }() + + if tc.ExecColdStarterFn != nil { + //wait for server to start, maybe we can do this better, but we cannot do a tcp dial or somthing like that + time.Sleep(1 * time.Second) + var err error + if tc.LuaHandlerContent != "" { + err = tc.ExecColdStarterFn("testback", tc.Scroll.Ports[0].Port) + } else { + err = tc.ExecColdStarterFn("", tc.Scroll.Ports[0].Port) + } + if err != nil { + t.Fatalf("Failed to execute coldstarter function: %v", err) + } + } + + err := waitUntilFileExists(path+"test.txt", 15*time.Second) + if err != nil { + t.Fatalf("Failed to wait for test.txt to be created: %v", err) + } + }) + + } +} diff --git a/test/integration/commands/serve_idle_test.go b/test/integration/commands/serve_idle_test.go index 1ea9a0e..1315277 100644 --- a/test/integration/commands/serve_idle_test.go +++ b/test/integration/commands/serve_idle_test.go @@ -43,10 +43,11 @@ func startAndTestServeCommand(ctx context.Context, t *testing.T, serveCmd *cobra } }() - go func() { + go func(ctx context.Context) { + serveCmd.SetContext(ctx) err := serveCmd.ExecuteContext(ctx) executionDoneChan <- err - }() + }(ctx) select { case <-connectedChan: diff --git a/test/integration/commands/serve_test.go b/test/integration/commands/serve_test.go index 6cfea87..705e29b 100644 --- a/test/integration/commands/serve_test.go +++ b/test/integration/commands/serve_test.go @@ -144,7 +144,6 @@ func TestServeCommand(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - //observer := logger.SetupLogsCapture() unixTime := time.Now().Unix() path := "./druid-cli-test/" + strconv.FormatInt(unixTime, 10) + "/" diff --git a/test/integration/commands/serve_watch_ports_test.go b/test/integration/commands/serve_watch_ports_test.go new file mode 100644 index 0000000..62189c9 --- /dev/null +++ b/test/integration/commands/serve_watch_ports_test.go @@ -0,0 +1,117 @@ +package command_test + +import ( + "context" + "encoding/json" + "errors" + "os" + "runtime" + "testing" + "time" + + "github.com/highcard-dev/daemon/internal/core/domain" + "github.com/highcard-dev/daemon/internal/signals" +) + +func fetchPorts() ([]domain.AugmentedPort, error) { + body, err := fetch("http://localhost:8081/api/v1/ports") + if err != nil { + return nil, err + } + var ap []domain.AugmentedPort + json.Unmarshal([]byte(body), &ap) + return ap, nil +} + +var testCommandTCP = func() map[string]*domain.CommandInstructionSet { + var ncCommand = []string{"nc", "-l", "-p", "12349"} + if runtime.GOOS == "darwin" { + ncCommand = []string{"nc", "-l", "12349"} + } + return map[string]*domain.CommandInstructionSet{ + "start": { + Procedures: []*domain.Procedure{ + { + Mode: "exec", + Data: ncCommand, + }, + }, + }, + } +} + +func TestWatchPortsServeCommand(t *testing.T) { + + type TestCase struct { + Name string + Scroll domain.File + } + var testCases = []TestCase{ + { + Name: "TestServeWaitPortsCommandTCP", + Scroll: domain.File{ + Ports: []domain.Port{ + { + Port: 12349, + Name: "testport", + Protocol: "tcp", + }, + }, + Init: "start", + Commands: testCommandTCP(), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + println(tc.Name) + + _, path := setupScroll(t, tc.Scroll) + defer os.RemoveAll(path) + + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(errors.New("test ended")) + + setupServeCmd(ctx, t, path, []string{"--watch-ports"}) + + defer func() { + signals.Stop() + }() + + //give time to make sure everything is online + time.Sleep(1 * time.Second) + ap1, err := fetchPorts() + if err != nil { + t.Fatalf("Failed to fetch ports: %v", err) + } + + for _, p := range ap1 { + if !p.Open { + t.Fatalf("Port %d is not open", p.Port.Port) + } + } + //give time to to get picked up by the watcher + time.Sleep(1 * time.Second) + err = tcpTester("", 12349) + + //give time to to get picked up by the watcher + time.Sleep(1 * time.Second) + + if err != nil { + t.Fatalf("Failed to test tcp: %v", err) + } + + ap2, err := fetchPorts() + if err != nil { + t.Fatalf("Failed to fetch ports: %v", err) + } + + for idx, p := range ap2 { + if p.InactiveSince == ap1[idx].InactiveSince { + t.Fatalf("InactiveSince did not change for port %d (both: %s)", p.Port.Port, p.InactiveSince) + } + } + + }) + } +}