1 //
2 // Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 // Package server implements the core of the SysDB web server.
27 package server
29 import (
30 "bytes"
31 "errors"
32 "fmt"
33 "html/template"
34 "io"
35 "log"
36 "net/http"
37 "net/url"
38 "path/filepath"
39 "strings"
41 "github.com/sysdb/go/client"
42 "github.com/sysdb/go/proto"
43 "github.com/sysdb/go/sysdb"
44 )
46 // A Config specifies configuration values for a SysDB web server.
47 type Config struct {
48 // Conns is a slice of connections to a SysDB server instance. The number of
49 // elements specifies the maximum number of parallel queries to the backend.
50 // Note that a client connection is not thread-safe but multiple idle
51 // connections don't impose any load on the server.
52 Conns []*client.Conn
54 // TemplatePath specifies the relative or absolute location of template files.
55 TemplatePath string
57 // StaticPath specifies the relative or absolute location of static files.
58 StaticPath string
59 }
61 // A Server implements an http.Handler that serves the SysDB user interface.
62 type Server struct {
63 conns chan *client.Conn
65 // Request multiplexer
66 mux map[string]handler
68 // Templates:
69 main *template.Template
70 results map[string]*template.Template
72 // Base directory of static files.
73 basedir string
74 }
76 // New constructs a new SysDB web server using the specified configuration.
77 func New(cfg Config) (*Server, error) {
78 if len(cfg.Conns) == 0 {
79 return nil, errors.New("need at least one client connection")
80 }
82 s := &Server{
83 conns: make(chan *client.Conn, len(cfg.Conns)),
84 results: make(map[string]*template.Template),
85 }
86 for _, c := range cfg.Conns {
87 s.conns <- c
88 }
90 var err error
91 s.main, err = cfg.parse("main.tmpl")
92 if err != nil {
93 return nil, err
94 }
96 types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
97 for _, t := range types {
98 s.results[t], err = cfg.parse(t + ".tmpl")
99 if err != nil {
100 return nil, err
101 }
102 }
104 s.basedir = cfg.StaticPath
105 s.mux = map[string]handler{
106 "images": s.static,
107 "style": s.static,
108 "graph": s.graph,
109 }
110 return s, nil
111 }
113 func (cfg Config) parse(name string) (*template.Template, error) {
114 t := template.New(filepath.Base(name))
115 return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
116 }
118 type request struct {
119 r *http.Request
120 cmd string
121 args []string
122 }
124 type handler func(http.ResponseWriter, request)
126 type page struct {
127 Title string
128 Query string
129 Content template.HTML
130 }
132 // Content generators for HTML pages.
133 var content = map[string]func(request, *Server) (*page, error){
134 "": index,
136 // Queries
137 "host": fetch,
138 "service": fetch,
139 "metric": fetch,
140 "hosts": listAll,
141 "services": listAll,
142 "metrics": listAll,
143 "lookup": lookup,
144 }
146 // ServeHTTP implements the http.Handler interface and serves
147 // the SysDB user interface.
148 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
149 path := r.RequestURI
150 if len(path) > 0 && path[0] == '/' {
151 path = path[1:]
152 }
153 var fields []string
154 for _, f := range strings.Split(path, "/") {
155 f, err := url.QueryUnescape(f)
156 if err != nil {
157 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
158 return
159 }
160 fields = append(fields, f)
161 }
163 req := request{
164 r: r,
165 cmd: fields[0],
166 }
167 if len(fields) > 1 {
168 if fields[len(fields)-1] == "" {
169 // Slash at the end of the URL
170 fields = fields[:len(fields)-1]
171 }
172 if len(fields) > 1 {
173 req.args = fields[1:]
174 }
175 }
177 if h := s.mux[fields[0]]; h != nil {
178 h(w, req)
179 return
180 }
182 f, ok := content[req.cmd]
183 if !ok {
184 s.notfound(w, r)
185 return
186 }
187 r.ParseForm()
188 page, err := f(req, s)
189 if err != nil {
190 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
191 return
192 }
194 page.Query = r.FormValue("query")
195 if page.Title == "" {
196 page.Title = "SysDB - The System Database"
197 }
199 var buf bytes.Buffer
200 err = s.main.Execute(&buf, page)
201 if err != nil {
202 s.internal(w, err)
203 return
204 }
206 w.WriteHeader(http.StatusOK)
207 io.Copy(w, &buf)
208 }
210 // static serves static content.
211 func (s *Server) static(w http.ResponseWriter, req request) {
212 http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
213 }
215 // Content handlers.
217 func index(_ request, s *Server) (*page, error) {
218 return &page{Content: "<section><h1>Welcome to the System Database.</h1></section>"}, nil
219 }
221 func listAll(req request, s *Server) (*page, error) {
222 if len(req.args) != 0 {
223 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
224 }
226 res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
227 if err != nil {
228 return nil, err
229 }
230 // the template *must* exist
231 return tmpl(s.results[req.cmd], res)
232 }
234 func lookup(req request, s *Server) (*page, error) {
235 if req.r.Method != "POST" {
236 return nil, errors.New("Method not allowed")
237 }
238 q := proto.EscapeString(req.r.FormValue("query"))
239 if q == "''" {
240 return nil, errors.New("Empty query")
241 }
243 res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
244 if err != nil {
245 return nil, err
246 }
247 return tmpl(s.results["hosts"], res)
248 }
250 func fetch(req request, s *Server) (*page, error) {
251 if len(req.args) == 0 {
252 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
253 }
255 var q string
256 switch req.cmd {
257 case "host":
258 if len(req.args) != 1 {
259 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
260 }
261 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
262 case "service", "metric":
263 if len(req.args) != 2 {
264 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
265 }
266 host := proto.EscapeString(req.args[0])
267 name := proto.EscapeString(req.args[1])
268 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
269 default:
270 panic("Unknown request: fetch(" + req.cmd + ")")
271 }
273 res, err := s.query(q)
274 if err != nil {
275 return nil, err
276 }
277 return tmpl(s.results[req.cmd], res)
278 }
280 func tmpl(t *template.Template, data interface{}) (*page, error) {
281 var buf bytes.Buffer
282 if err := t.Execute(&buf, data); err != nil {
283 return nil, fmt.Errorf("Template error: %v", err)
284 }
285 return &page{Content: template.HTML(buf.String())}, nil
286 }
288 func html(s string) template.HTML {
289 return template.HTML(template.HTMLEscapeString(s))
290 }
292 func (s *Server) query(cmd string) (interface{}, error) {
293 c := <-s.conns
294 defer func() { s.conns <- c }()
296 m := &proto.Message{
297 Type: proto.ConnectionQuery,
298 Raw: []byte(cmd),
299 }
300 if err := c.Send(m); err != nil {
301 return nil, fmt.Errorf("Query %q: %v", cmd, err)
302 }
304 for {
305 m, err := c.Receive()
306 if err != nil {
307 return nil, fmt.Errorf("Failed to receive server response: %v", err)
308 }
309 if m.Type == proto.ConnectionLog {
310 log.Println(string(m.Raw[4:]))
311 continue
312 } else if m.Type == proto.ConnectionError {
313 return nil, errors.New(string(m.Raw))
314 }
316 t, err := m.DataType()
317 if err != nil {
318 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
319 }
321 var res interface{}
322 switch t {
323 case proto.HostList:
324 var hosts []sysdb.Host
325 err = proto.Unmarshal(m, &hosts)
326 res = hosts
327 case proto.Host:
328 var host sysdb.Host
329 err = proto.Unmarshal(m, &host)
330 res = host
331 case proto.Timeseries:
332 var ts sysdb.Timeseries
333 err = proto.Unmarshal(m, &ts)
334 res = ts
335 default:
336 return nil, fmt.Errorf("Unsupported data type %d", t)
337 }
338 if err != nil {
339 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
340 }
341 return res, nil
342 }
343 }
345 // vim: set tw=78 sw=4 sw=4 noexpandtab :