From 570903c9fcfbb9446715eb49eacb9d7ab91cccb8 Mon Sep 17 00:00:00 2001 From: "numkem@numkem.org" Date: Fri, 20 Feb 2026 17:08:44 -0500 Subject: [PATCH 1/2] Add new endpoints to see the content of the store - /_/ shows the README.md converted to HTML - /_/list shows a list of the current subjects - /_/subject/{subject} list the scripts associated to a subject by name - /_/info/{subject}/{name} shows information about a script including the source --- cmd/server/http.go | 46 ++++++---- cmd/server/http_endpoints.go | 32 +++++++ cmd/server/http_scripts.go | 161 +++++++++++++++++++++++++++++++++++ cmd/server/http_utils.go | 10 +++ cmd/server/main.go | 20 ++++- cmd/server/messages.go | 120 ++++++++++++++++++++++++++ embed.go | 9 ++ flake.lock | 6 +- flake.nix | 38 ++++++++- go.mod | 3 +- go.sum | 2 + 11 files changed, 422 insertions(+), 25 deletions(-) create mode 100644 cmd/server/http_endpoints.go create mode 100644 cmd/server/http_scripts.go create mode 100644 cmd/server/http_utils.go create mode 100644 cmd/server/messages.go create mode 100644 embed.go diff --git a/cmd/server/http.go b/cmd/server/http.go index bf58e35..c17640e 100644 --- a/cmd/server/http.go +++ b/cmd/server/http.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/gorilla/mux" "github.com/nats-io/nats.go" log "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -27,12 +28,12 @@ const DEFAULT_HTTP_TIMEOUT = 5 * time.Second var tracer = otel.Tracer("http-nats-proxy") -type httpNatsProxy struct { +type functionHandler struct { port string nc *nats.Conn } -func NewHttpNatsProxy(port int, natsURL string) (*httpNatsProxy, error) { +func newFunctionHandler(port int, natsURL string) (*functionHandler, error) { // Connect to NATS if natsURL == "" { natsURL = msgscript.NatsUrlByEnv() @@ -42,12 +43,12 @@ func NewHttpNatsProxy(port int, natsURL string) (*httpNatsProxy, error) { return nil, fmt.Errorf("Failed to connect to NATS: %w", err) } - return &httpNatsProxy{ + return &functionHandler{ nc: nc, }, nil } -func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (fh *functionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Extract context from incoming request headers ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) @@ -143,7 +144,7 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { otel.GetTextMapPropagator().Inject(ctx, natsHeaderCarrier(msg.Header)) // Send the message and wait for the response - response, err := p.nc.RequestMsgWithContext(ctx, msg) + response, err := fh.nc.RequestMsgWithContext(ctx, msg) if err != nil { natsSpan.RecordError(err) natsSpan.SetStatus(codes.Error, "NATS request failed") @@ -246,22 +247,33 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { span.SetStatus(codes.Ok, "") } -func hasHTMLResult(results map[string]*executor.ScriptResult) (bool, *executor.ScriptResult) { - for _, sr := range results { - if sr.IsHTML { - return true, sr - } - } - - return false, nil -} - func runHTTP(port int, natsURL string) { - proxy, err := NewHttpNatsProxy(port, natsURL) + r := mux.NewRouter() + + hfunc, err := newFunctionHandler(port, natsURL) if err != nil { log.Fatalf("failed to create HTTP proxy: %v", err) } + r.HandleFunc("/", index) + r.HandleFunc("/logo.webp", logo) + r.HandleFunc("/favicon.ico", ignore) // Keeps poluting logs + + r.HandleFunc("/_/list", hfunc.ListScripts) + r.HandleFunc("/_/subject/{subject}", hfunc.ListNamesForScript) + r.HandleFunc("/_/info/{subject}/{name}", hfunc.InfoForNamedScript) + + r.PathPrefix("/").Handler(hfunc) + + srv := &http.Server{ + Handler: r, + Addr: fmt.Sprintf("0.0.0.0:%d", port), + WriteTimeout: 1 * time.Minute, + ReadTimeout: 1 * time.Minute, + } + log.Infof("Starting HTTP server on port %d", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), proxy)) + log.Fatal(srv.ListenAndServe()) } + +func ignore(w http.ResponseWriter, r *http.Request) {} diff --git a/cmd/server/http_endpoints.go b/cmd/server/http_endpoints.go new file mode 100644 index 0000000..aa8954d --- /dev/null +++ b/cmd/server/http_endpoints.go @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + + "github.com/numkem/msgscript" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func logo(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "image/webp") + w.Write(msgscript.LOGO) +} + +func index(w http.ResponseWriter, r *http.Request) { + // create markdown parser with extensions + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + md := p.Parse(msgscript.README) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + w.WriteHeader(http.StatusOK) + w.Write(markdown.Render(md, renderer)) +} diff --git a/cmd/server/http_scripts.go b/cmd/server/http_scripts.go new file mode 100644 index 0000000..90544c7 --- /dev/null +++ b/cmd/server/http_scripts.go @@ -0,0 +1,161 @@ +package main + +import ( + "encoding/json" + "fmt" + "html" + "net/http" + "sort" + "strings" + + "github.com/gorilla/mux" + "github.com/nats-io/nats.go" +) + +func (fh *functionHandler) ListScripts(w http.ResponseWriter, r *http.Request) { + msg := nats.NewMsg(subjectListScripts) + msg.Data = []byte("") + + // Send the message and wait for the response + response, err := fh.nc.RequestMsgWithContext(r.Context(), msg) + if err != nil { + returnError(w, err) + return + } + + rep := &Reply{} + err = json.Unmarshal(response.Data, rep) + if err != nil { + returnError(w, err) + return + } + + var subjects []string + err = json.Unmarshal(rep.Results[0].Payload, &subjects) + if err != nil { + returnError(w, err) + return + } + + sort.Strings(subjects) + + w.WriteHeader(http.StatusOK) + resp := []byte(` + +
+ Msgscript :: List all subjects +
+ +

Subject List

`)) +} + +func (fh *functionHandler) ListNamesForScript(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + subject := vars["subject"] + + msg := nats.NewMsg(subjectListNamesForScript) + msg.Data = []byte(subject) + + // Send the message and wait for the response + response, err := fh.nc.RequestMsgWithContext(r.Context(), msg) + if err != nil { + returnError(w, err) + return + } + + rep := &Reply{} + err = json.Unmarshal(response.Data, rep) + if err != nil { + returnError(w, err) + return + } + + var names []string + err = json.Unmarshal(rep.Results[0].Payload, &names) + if err != nil { + returnError(w, err) + return + } + + sort.Strings(names) + + w.WriteHeader(http.StatusOK) + resp := fmt.Appendf(nil, ` +
+ Msgscript :: List all subjects +
+ +

Names for subject %s

`)) +} + +func (fh *functionHandler) InfoForNamedScript(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + subject := vars["subject"] + name := vars["name"] + + msg := nats.NewMsg(subjectInfoNamedSCript) + msg.Data = []byte(strings.Join([]string{subject, name}, ",")) + + response, err := fh.nc.RequestMsgWithContext(r.Context(), msg) + if err != nil { + returnError(w, err) + return + } + + rep := &Reply{} + err = json.Unmarshal(response.Data, rep) + if err != nil { + returnError(w, err) + return + } + + script := rep.Results[0] + + isHTMLValue := "No" + if script.IsHTML { + isHTMLValue = fmt.Sprintf(`Yes`, subject) + } + + w.WriteHeader(http.StatusOK) + w.Write(fmt.Appendf(nil, ` +
+ Msgscript :: Info for script %s/%s +
+ +

Script information

+

Properties

+ Subject: %s
+ Name: %s
+ Is HTML?: %s
+ Libraires used: %s
+ Executor: %s
+ +

Source

+
+%s
+    
+ + +`, subject, name, subject, name, isHTMLValue, script.Headers["libraries"], script.Headers["executor"], html.EscapeString(string(script.Payload)))) +} diff --git a/cmd/server/http_utils.go b/cmd/server/http_utils.go new file mode 100644 index 0000000..a596a3a --- /dev/null +++ b/cmd/server/http_utils.go @@ -0,0 +1,10 @@ +package main + +import ( + "net/http" +) + +func returnError(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 5412c42..39ef7c6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -156,6 +156,24 @@ func main() { return } + // Check for some special subjects we use internally + switch msg.Subject { + case subjectListScripts: + replyWithSubjectList(ctx, nc, scriptStore, msg.Reply) + return + case subjectListNamesForScript: + replyWithNamesForSubject(ctx, nc, scriptStore, string(msg.Data), msg.Reply) + return + case subjectInfoNamedSCript: + ss := strings.Split(string(msg.Data), ",") + if len(ss) != 2 { + replyWithError(nc, fmt.Errorf("invalid request"), msg.Reply) + } + replyWithNamedScriptInfo(ctx, nc, scriptStore, ss[0], ss[1], msg.Reply) + return + default: + } + m := new(executor.Message) err := json.Unmarshal(msg.Data, m) // if the payload isn't a JSON Message, take it as a whole @@ -212,7 +230,7 @@ func main() { span.RecordError(err) span.SetStatus(codes.Error, "Failed to get scripts") - replyWithError(nc, fmt.Errorf("failed to get scripts for subject"), msg.Reply) + replyWithError(nc, fmt.Errorf("failed to get scripts for subject: %v", err), msg.Reply) return } getScriptsSpan.SetStatus(codes.Ok, fmt.Sprintf("found %d scripts", len(scripts))) diff --git a/cmd/server/messages.go b/cmd/server/messages.go new file mode 100644 index 0000000..df4315e --- /dev/null +++ b/cmd/server/messages.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/nats-io/nats.go" + + "github.com/numkem/msgscript/executor" + "github.com/numkem/msgscript/store" +) + +const ( + subjectListScripts = "__listScrips" + subjectListNamesForScript = "__listNamesForScript" + subjectInfoNamedSCript = "__infoNamedScript" +) + +func replyWithSubjectList(ctx context.Context, nc *nats.Conn, scriptStore store.ScriptStore, replySubject string) { + subjects, err := scriptStore.ListSubjects(ctx) + if err != nil { + replyWithError(nc, err, replySubject) + return + } + + j, err := json.Marshal(subjects) + if err != nil { + replyWithError(nc, fmt.Errorf("failed to encode subject list: %v", err), replySubject) + return + } + + replyMessage(nc, &executor.Message{}, replySubject, &Reply{ + Results: []*executor.ScriptResult{ + { + Code: http.StatusOK, + Error: "", + Headers: map[string]string{}, + IsHTML: true, + Payload: j, + }, + }, + HTML: true, + Error: "", + }) +} + +func replyWithNamesForSubject(ctx context.Context, nc *nats.Conn, scriptStore store.ScriptStore, subject string, replySubject string) { + scripts, err := scriptStore.GetScripts(ctx, subject) + if err != nil { + replyWithError(nc, err, replySubject) + return + } + + var names []string + for name := range scripts { + names = append(names, name) + } + + j, err := json.Marshal(names) + if err != nil { + replyWithError(nc, fmt.Errorf("failed to encode name list: %v", err), replySubject) + return + } + + replyMessage(nc, &executor.Message{}, replySubject, &Reply{ + Results: []*executor.ScriptResult{ + { + Code: http.StatusOK, + Error: "", + Headers: map[string]string{}, + IsHTML: true, + Payload: j, + }, + }, + HTML: true, + Error: "", + }) +} + +func replyWithNamedScriptInfo(ctx context.Context, nc *nats.Conn, scriptStore store.ScriptStore, subject, name, replySubject string) { + allScripts, err := scriptStore.GetScripts(ctx, subject) + if err != nil { + replyWithError(nc, err, replySubject) + return + } + + script := allScripts[name] + if script == nil { + replyWithError(nc, fmt.Errorf("no script found for subject %s with name %s", subject, name), replySubject) + return + } + + if script.LibKeys == nil { + script.LibKeys = []string{} + } + if script.Executor == "" { + script.Executor = executor.EXECUTOR_LUA_NAME + } + + replyMessage(nc, &executor.Message{}, replySubject, &Reply{ + Results: []*executor.ScriptResult{ + { + Code: http.StatusOK, + Error: "", + Headers: map[string]string{ + "libraries": strings.Join(script.LibKeys, ", "), + "executor": script.Executor, + }, + IsHTML: script.HTML, + Payload: script.Content, + }, + }, + HTML: script.HTML, + Error: "", + }) + +} diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..fb8bfa0 --- /dev/null +++ b/embed.go @@ -0,0 +1,9 @@ +package msgscript + +import _ "embed" + +//go:embed logo.webp +var LOGO []byte + +//go:embed README.md +var README []byte diff --git a/flake.lock b/flake.lock index ba3132c..a608aa3 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1759036355, - "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "rev": "0182a361324364ae3f436a63005877674cf45efb", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b970ffa..818db91 100644 --- a/flake.nix +++ b/flake.nix @@ -8,8 +8,8 @@ outputs = { self, nixpkgs }: let - version = "0.8.2"; - vendorHash = "sha256-IsLAIKYuKhlD71fad8FuayTFbdQJla4ifjs8TexXDYQ="; + version = "0.9.0"; + vendorHash = "sha256-Jer/ADurA+BwvAuuTjEycJNExBnobEs72ccQMmOqV1k="; mkPlugin = pkgs: name: path: @@ -40,15 +40,28 @@ let pkgs = import nixpkgs { system = "x86_64-linux"; }; lib = pkgs.lib; + system = "x86_64-linux"; in rec { + default = server; + cli = pkgs.callPackage ./nix/pkgs/cli.nix { inherit version vendorHash; }; + server = pkgs.callPackage ./nix/pkgs/server.nix { inherit version vendorHash; }; - default = server; + + runServer = pkgs.writeScript "msgscript" '' + #!/usr/bin/env bash + ${self.packages.${system}.server}/bin/msgscript -plugin ${allPlugins}/ $@ + ''; + + runCli = pkgs.writeScript "msgscriptcli" '' + #!/usr/bin/env bash + ${self.packages.${system}.cli}/bin/msgscriptcli -plugin ${allPlugins}/ $@ + ''; allPlugins = pkgs.symlinkJoin { name = "msgscript-all-plugins"; @@ -95,6 +108,25 @@ lib.genAttrs pluginDirs (name: mkPlugin pkgs name "${self}/plugins/${name}"); }; + apps = + let + mkApps = system: { + server = { + type = "app"; + program = "${self.packages.${system}.runServer}"; + }; + + cli = { + type = "app"; + program = "${self.packages.${system}.runCli}"; + }; + }; + in + { + "x86_64-linux" = mkApps "x86_64-linux"; + "aarch64-linux" = mkApps "aarch64-linux"; + }; + devShells.x86_64-linux.default = let pkgs = import nixpkgs { system = "x86_64-linux"; }; diff --git a/go.mod b/go.mod index ae5c39a..de57ab0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,9 @@ require ( github.com/containers/podman/v5 v5.5.2 github.com/felipejfc/gluahttpscrape v0.0.0-20170525191632-10580c4a38f9 github.com/fsnotify/fsnotify v1.9.0 + github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/jedib0t/go-pretty/v6 v6.5.9 github.com/layeh/gopher-json v0.0.0-20201124131017-552bb3c4c3bf github.com/nats-io/nats-server/v2 v2.10.24 @@ -98,7 +100,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-containerregistry v0.20.3 // indirect github.com/google/go-intervals v0.0.2 // indirect - github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 3025aa4..c28a1b7 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= From d8f39c76393626eb802f3105f298cc550d6fc303 Mon Sep 17 00:00:00 2001 From: "numkem@numkem.org" Date: Fri, 20 Feb 2026 17:14:50 -0500 Subject: [PATCH 2/2] Update README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index de8931c..8f21717 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [In HTTP+HTML mode](#in-httphtml-mode) - [Return value](#return-value) - [HTTP handler](#http-handler) + - [Special HTTP Endpoints](#special-http-endpoints) - [Installation](#installation) - [Docker container](#docker-container) - [NixOS service](#nixos-service) @@ -145,6 +146,15 @@ Example using curl (if you are running locally and for the subject of the exampl curl -X POST -d 'John' http://127.0.0.1:7643/http.hello ``` +### Special HTTP Endpoints + +Some endpoints exists to see the content of the store. These endpoints are meant to be used with a browser. + +- `/_/` shows the README.md file converted to HTML +- `/_/list` shows a list of the current subjects +- `/_/subject/{subject}` list the scripts associated to a subject by name +- `/_/info/{subject}/{name}` shows information about a script including the source + ## Installation ### Docker container