Code

Simplified URL parsing and un-escape URIs.
[sysdb/webui.git] / server / server.go
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
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))
118 type request struct {
119         r    *http.Request
120         cmd  string
121         args []string
124 type handler func(http.ResponseWriter, request)
126 type page struct {
127         Title   string
128         Query   string
129         Content template.HTML
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,
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)
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)))
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
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)
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)
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)
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
288 func html(s string) template.HTML {
289         return template.HTML(template.HTMLEscapeString(s))
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         }
345 // vim: set tw=78 sw=4 sw=4 noexpandtab :