Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
46 changes: 29 additions & 17 deletions cmd/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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))

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {}
32 changes: 32 additions & 0 deletions cmd/server/http_endpoints.go
Original file line number Diff line number Diff line change
@@ -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))
}
161 changes: 161 additions & 0 deletions cmd/server/http_scripts.go
Original file line number Diff line number Diff line change
@@ -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(`
<html>
<header>
<title>Msgscript :: List all subjects</title>
</header>
<body>
<h1>Subject List</h1><ul>`)

for _, subject := range subjects {
if subject == "" {
continue
}

resp = fmt.Appendf(resp, `<li><a href="/_/subject/%s">%s</a></li>`, subject, subject)
}

w.Write(fmt.Append(resp, `</ul></body></html>`))
}

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, `<html>
<header>
<title>Msgscript :: List all subjects</title>
</header>
<body>
<h1>Names for subject %s</h1><ul>`, subject)

for _, name := range names {
if name == "" {
continue
}

resp = fmt.Appendf(resp, `<li><a href="/_/info/%s/%s">%s</a></li>`, subject, name, name)
}

w.Write(fmt.Append(resp, `</ul></body></html>`))
}

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(`<a href="/%s/">Yes</a>`, subject)
}

w.WriteHeader(http.StatusOK)
w.Write(fmt.Appendf(nil, `<html>
<header>
<title>Msgscript :: Info for script %s/%s</title>
</header>
<body>
<h1>Script information</h1>
<h2>Properties</h2>
Subject: %s<br />
Name: %s<br />
Is HTML?: %s<br />
Libraires used: %s<br />
Executor: %s<br />

<h2>Source</h2>
<pre>
%s
</pre>
</body>
</html>
`, subject, name, subject, name, isHTMLValue, script.Headers["libraries"], script.Headers["executor"], html.EscapeString(string(script.Payload))))
}
10 changes: 10 additions & 0 deletions cmd/server/http_utils.go
Original file line number Diff line number Diff line change
@@ -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()))
}
20 changes: 19 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
Expand Down
Loading