-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathweb.go
155 lines (135 loc) · 4.37 KB
/
web.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package graphblast
import (
"bytes"
"errors"
"fmt"
"github.com/hut8labs/graphblast/bind"
"github.com/hut8labs/graphblast/bundle"
"html/template"
"io"
"net/http"
"net/url"
"regexp"
"time"
)
const DEFAULT_GRAPH_NAME = "_"
func Index() http.HandlerFunc {
indexfile := bundle.ReadFile("assets/index.html")
indexpage := template.Must(template.New("index").Parse(string(indexfile)))
namePattern := regexp.MustCompile("^/(?P<name>\\w+)")
return LogRequest(func(w http.ResponseWriter, r *http.Request) {
params := ExtractNamed(r.URL.Path, namePattern)
if len(params["name"]) == 0 {
params["name"] = DEFAULT_GRAPH_NAME
}
indexpage.Execute(w, params["name"])
})
// TODO Consider building the JS into the HTML, and removing Script()
}
func Script() http.HandlerFunc {
scriptfile := bytes.NewReader(bundle.ReadFile("assets/script.js"))
return LogRequest(func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "script.js", time.Now(), scriptfile)
})
}
// ExtractNamed returns a map from capture group names to their matched values
// when the regexp pattern is matched against text.
func ExtractNamed(text string, pattern *regexp.Regexp) map[string]string {
names := pattern.SubexpNames()
matches := pattern.FindStringSubmatch(text)
result := make(map[string]string, len(names)-1)
for index, name := range names {
if index == 0 {
continue
}
if index >= len(matches) {
break
}
result[name] = matches[index]
}
return result
}
// ParseGraphURL returns a graph name and a graph object built from the
// path and query parameters of the URL. A regular expression is used to
// extract the graph name and type, and is expected to have two named capture
// groups: "name" for the graph name and "type" for the graph type.
func ParseGraphURL(url *url.URL, pattern *regexp.Regexp) (string, Graph) {
parts := ExtractNamed(url.Path, pattern)
graphType, ok := parts["type"]
if !ok {
return "", nil
}
graph := NewGraphFromType(graphType)
if graph == nil {
return "", nil
}
boundOk := bind.Bind(graph, bind.Parameters(url.Query()))
if !boundOk {
return "", nil
}
graphName, ok := parts["name"]
if !ok {
return "", nil
}
return graphName, graph
}
// Inputs returns a HandlerFunc for responding to requests for streaming
// uploads of graph data. When called, the handler creates a new graph based on
// the URL arguments and reads from the request body until the response is
// finished.
func Inputs(requests chan<- GraphRequest) http.HandlerFunc {
graphPattern := regexp.MustCompile("^/graph/(?P<type>\\w+)/(?P<name>\\w+)")
return LogRequest(func(w http.ResponseWriter, r *http.Request) {
name, graph := ParseGraphURL(r.URL, graphPattern)
if graph == nil {
http.Error(w, "Invalid graph type or parameters", 400)
} else {
PopulateGraph(name, graph, r.Body, requests)
}
})
}
// Events returns a HandlerFunc for responding to requests for updates via an
// HTML EventSource (a.k.a. SSE, server-sent events). When called, the handler
// listens for graph data and pushes it to the client as JSON.
func Events(requests chan<- GraphRequest, publisher Publisher) http.HandlerFunc {
return LogRequest(func(w http.ResponseWriter, r *http.Request) {
// Get the necessary parts for being an EventSource, or fail.
flusher, cn, err := toEventSource(w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
messages := publisher.Subscribe(r.RemoteAddr)
defer publisher.Unsubscribe(r.RemoteAddr)
requests <- DumpGraphs(r.RemoteAddr)
for {
select {
case _ = <-cn.CloseNotify():
return
case msg := <-messages:
envelope := msg.Envelope()
contents, msgErr := msg.Contents()
Log("Sending: %s %s", envelope, string(contents))
if msgErr != nil {
// TODO Log it
continue
}
io.WriteString(w, fmt.Sprintf("event: %s\n", envelope))
io.WriteString(w, fmt.Sprintf("data: %s\n\n", string(contents)))
flusher.Flush()
}
}
})
}
// Sets up a ResponseWriter for use as an EventSource.
func toEventSource(w http.ResponseWriter) (http.Flusher, http.CloseNotifier, error) {
f, canf := w.(http.Flusher)
cn, cancn := w.(http.CloseNotifier)
if !canf || !cancn {
return f, cn, errors.New("connection not suitable for EventSource")
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
return f, cn, nil
}