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
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(`
+
+
+%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=