diff --git a/assets/banner.txt b/assets/banner.txt new file mode 100644 index 0000000..688b889 --- /dev/null +++ b/assets/banner.txt @@ -0,0 +1,5 @@ + __ ______ + / /______ ____ / / / / + / //_/ __ \/ __ \/ / / / + / ,< / /_/ / / / /_/_/_/ +/_/|_|\____/_/ /_(_|_|_) \ No newline at end of file diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..07e0b16 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,67 @@ +package auth + +import ( + "kon/config" + "kon/utils" + "log" + "sync" + "time" + + "github.com/dchest/uniuri" + "github.com/golang-jwt/jwt/v5" +) + +var Hashes = map[string]time.Time{} +var HashesMutex = sync.RWMutex{} + +func CreateToken() string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "reqHash": uniuri.New(), + "exp": time.Now().Add(time.Hour * 24).Unix(), + }) + + tokenString, err := token.SignedString(config.AuthSecret) + utils.CheckError(err) + + return tokenString +} + +func VerifyToken(tokenString string) bool { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return config.AuthSecret, nil + }) + + if err != nil { + return false + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + _, does_claim_have_req_hash := claims["reqHash"] + if does_claim_have_req_hash { + HashesMutex.RLock() + val, exists := Hashes[claims["reqHash"].(string)] + HashesMutex.RUnlock() + if exists { + if time.Since(val) > 48*time.Hour { + HashesMutex.Lock() + Hashes[claims["reqHash"].(string)] = time.Now() + HashesMutex.Unlock() + return true + } else { + return false + } + } else { + HashesMutex.Lock() + Hashes[claims["reqHash"].(string)] = time.Now() + HashesMutex.Unlock() + return true + } + } else { + return false + } + } else { + log.Printf("Invalid JWT Token") + return false + } +} diff --git a/banner/banner.go b/banner/banner.go new file mode 100644 index 0000000..3414b24 --- /dev/null +++ b/banner/banner.go @@ -0,0 +1,14 @@ +package banner + +import ( + "kon/constants" + + "github.com/fatih/color" +) + +const Banner = " __ ______\n / /______ ____ / / / /\n / //_/ __ \\/ __ \\/ / / / \n / ,< / /_/ / / / /_/_/_/ \n /_/|_|\\____/_/ /_(_|_|_)" + +func FormatBanner() string { + d := color.New(color.FgHiWhite, color.Bold) + return d.Sprint(Banner) + " " + constants.VersionString +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..1fbf6e2 --- /dev/null +++ b/client/client.go @@ -0,0 +1,98 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "kon/auth" + "kon/banner" + "kon/client/state" + "kon/config" + "kon/constants" + "kon/pon/paths/status" + "kon/utils" + "net/http" + "net/url" + "sort" + "time" + + "github.com/fatih/color" +) + +func Status() { + for host := range config.Config.Hosts { + state.State.Mutex.Lock() + state.State.Machines[host] = state.Machine{ + State: state.Working, + JobStart: time.Now(), + } + state.State.Mutex.Unlock() + go StatusThread(host) + } + DisplayState() +} + +func StatusThread(host string) { + base, err := url.Parse("http://" + config.Config.Hosts[host] + constants.HostEndpoint) + utils.CheckError(err) + base.Path += "status" + params := url.Values{} + params.Add("token", auth.CreateToken()) + base.RawQuery = params.Encode() + resp, err := http.Get(base.String()) + if utils.JobFailed(err, host) { + return + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if utils.JobFailed(err, host) { + return + } + var status status.Status + err = json.Unmarshal(body, &status) + if utils.JobFailed(err, host) { + return + } + state.State.Mutex.Lock() + state.State.Machines[host] = state.Machine{ + State: state.Completed, + JobStart: state.State.Machines[host].JobStart, + JobEnd: time.Now(), + Message: fmt.Sprintf("[RAM: %0.2f%%] [Uptime: %s]", float64(status.MemUsed)/float64(status.MemTotal)*100.0, time.Duration(float64(status.Uptime)*float64(time.Second))), + } + state.State.Mutex.Unlock() +} + +func DisplayState() { + finished := false + for !finished { + finished = true + currentString := banner.FormatBanner() + "\n\n" + state.State.Mutex.RLock() + keys := make([]string, 0, len(state.State.Machines)) + for key := range state.State.Machines { + keys = append(keys, key) + } + sort.Strings(keys) + for _, host := range keys { + line := "" + switch state.State.Machines[host].State { + case state.Working: + finished = false + d := color.New(color.FgHiYellow, color.Bold) + line = d.Sprintf(" - [✸] %-15s %15s\n", host, time.Since(state.State.Machines[host].JobStart)) + case state.Error: + d := color.New(color.FgHiRed, color.Bold) + line = d.Sprintf(" - [✗] %-15s %15s %s\n", host, state.State.Machines[host].JobEnd.Sub(state.State.Machines[host].JobStart), state.State.Machines[host].Message) + case state.Completed: + d := color.New(color.FgHiGreen, color.Bold) + line = d.Sprintf(" - [✔] %-15s %15s %s\n", host, state.State.Machines[host].JobEnd.Sub(state.State.Machines[host].JobStart), state.State.Machines[host].Message) + } + currentString += line + } + state.State.Mutex.RUnlock() + print("\033[H\033[2J") + println(currentString) + } + println("") +} diff --git a/client/state/state.go b/client/state/state.go new file mode 100644 index 0000000..eeadd4c --- /dev/null +++ b/client/state/state.go @@ -0,0 +1,28 @@ +package state + +import ( + "sync" + "time" +) + +type StateStruct struct { + Machines map[string]Machine + Mutex sync.RWMutex +} + +type Machine struct { + State int + JobStart time.Time + JobEnd time.Time + Message string +} + +const ( + Working = iota + Completed + Error +) + +var State StateStruct = StateStruct{ + Machines: map[string]Machine{}, +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4be96d5 --- /dev/null +++ b/config/config.go @@ -0,0 +1,20 @@ +package config + +type ConfigStruct struct { + Hosts map[string]string +} + +var Config = ConfigStruct{ + Hosts: map[string]string{ + "alpha": "127.0.0.1", + "beta": "127.0.0.1", + "gamma": "127.0.0.1", + "delta": "127.0.0.1", + "epsilon": "127.0.0.1", + "zeta": "127.0.0.1", + "eta": "127.0.0.1", + "omega": "127.0.0.1", + }, +} + +var AuthSecret = []byte("ChangeMe!") diff --git a/constants/constants.go b/constants/constants.go new file mode 100644 index 0000000..44269c4 --- /dev/null +++ b/constants/constants.go @@ -0,0 +1,5 @@ +package constants + +const VersionString = "v0.0.1" + +const HostEndpoint = ":3000" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2cdc12d --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module kon + +go 1.22.1 + +require ( + github.com/dchest/uniuri v1.2.0 + github.com/fatih/color v1.16.0 + github.com/gofiber/fiber/v2 v2.52.4 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/mackerelio/go-osstat v0.2.4 +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..66ee99e --- /dev/null +++ b/go.sum @@ -0,0 +1,35 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= +github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs= +github.com/mackerelio/go-osstat v0.2.4/go.mod h1:Zy+qzGdZs3A9cuIqmgbJvwbmLQH9dJvtio5ZjJTbdlQ= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/help/help.go b/help/help.go new file mode 100644 index 0000000..fc77334 --- /dev/null +++ b/help/help.go @@ -0,0 +1,5 @@ +package help + +const Help = ` +Commands: + - pon: start server` diff --git a/kon b/kon new file mode 100755 index 0000000..eadd339 Binary files /dev/null and b/kon differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..d3f5a6e --- /dev/null +++ b/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "kon/banner" + "kon/client" + "kon/help" + "kon/pon" + "log" + "os" +) + +func main() { + args := os.Args + fmt.Println(banner.FormatBanner()) + if len(args) == 1 { + fmt.Println(help.Help) + } else if len(args) == 2 { + switch args[1] { + case "pon": + pon.StartServer() + case "status": + client.Status() + default: + log.Fatal("Cannot find command.") + } + + } else { + log.Fatal("Too many arguments.") + } +} diff --git a/pon/middlewares/middlewares.go b/pon/middlewares/middlewares.go new file mode 100644 index 0000000..d2cc2db --- /dev/null +++ b/pon/middlewares/middlewares.go @@ -0,0 +1,12 @@ +package middlewares + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func Init(app *fiber.App) { + app.Use(logger.New(logger.Config{ + TimeFormat: "2006-01-02 15:04:05", + })) +} diff --git a/pon/paths/paths.go b/pon/paths/paths.go new file mode 100644 index 0000000..b8a40a4 --- /dev/null +++ b/pon/paths/paths.go @@ -0,0 +1,11 @@ +package paths + +import ( + "kon/pon/paths/status" + + "github.com/gofiber/fiber/v2" +) + +func Init(app *fiber.App) { + app.Get("/status", status.HandleFunc) +} diff --git a/pon/paths/status/status.go b/pon/paths/status/status.go new file mode 100644 index 0000000..cd37090 --- /dev/null +++ b/pon/paths/status/status.go @@ -0,0 +1,47 @@ +package status + +import ( + "encoding/json" + "kon/auth" + "kon/utils" + "os" + + "github.com/mackerelio/go-osstat/memory" + "github.com/mackerelio/go-osstat/uptime" + + "github.com/gofiber/fiber/v2" +) + +type Status struct { + Hostname string + MemUsed uint64 + MemTotal uint64 + Uptime uint64 +} + +func GetStatus() Status { + hostname, err := os.Hostname() + utils.CheckError(err) + memory, err := memory.Get() + utils.CheckError(err) + uptime, err := uptime.Get() + utils.CheckError(err) + + return Status{ + Hostname: hostname, + MemUsed: memory.Used, + MemTotal: memory.Total, + Uptime: uint64(uptime) / 1000000000, + } +} + +func HandleFunc(c *fiber.Ctx) error { + queryValue := c.Query("token") + if auth.VerifyToken(queryValue) { + statusJSON, err := json.Marshal(GetStatus()) + utils.CheckError(err) + return c.SendString(string(statusJSON)) + } else { + return c.SendString("no") + } +} diff --git a/pon/pon.go b/pon/pon.go new file mode 100644 index 0000000..f8a06a4 --- /dev/null +++ b/pon/pon.go @@ -0,0 +1,16 @@ +package pon + +import ( + "kon/constants" + "kon/pon/middlewares" + "kon/pon/paths" + + "github.com/gofiber/fiber/v2" +) + +func StartServer() { + app := fiber.New() + middlewares.Init(app) + paths.Init(app) + app.Listen(constants.HostEndpoint) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..e130bc5 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,28 @@ +package utils + +import ( + "kon/client/state" + "log" + "time" +) + +func CheckError(err error) { + if err != nil { + log.Fatal(err) + } +} + +func JobFailed(err error, host string) bool { + if err != nil { + state.State.Mutex.Lock() + state.State.Machines[host] = state.Machine{ + State: state.Error, + Message: err.Error(), + JobStart: state.State.Machines[host].JobStart, + JobEnd: time.Now(), + } + state.State.Mutex.Unlock() + return true + } + return false +}