Code

Add support for variable root mount points.
[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         "fmt"
32         "html/template"
33         "io"
34         "log"
35         "net/http"
36         "net/url"
37         "path/filepath"
38         "strings"
40         "github.com/sysdb/go/client"
41 )
43 // A Config specifies configuration values for a SysDB web server.
44 type Config struct {
45         // TemplatePath specifies the relative or absolute location of template files.
46         TemplatePath string
48         // StaticPath specifies the relative or absolute location of static files.
49         StaticPath string
51         // Root mount point of the server.
52         Root string
53 }
55 // A Server implements an http.Handler that serves the SysDB user interface.
56 type Server struct {
57         c *client.Client
59         // Request multiplexer
60         mux map[string]handler
62         // Templates:
63         main    *template.Template
64         results map[string]*template.Template
66         // Base directory of static files.
67         basedir string
69         // Root mount point.
70         root string
71 }
73 // New constructs a new SysDB web server using the specified configuration.
74 func New(addr, user string, cfg Config) (*Server, error) {
75         s := &Server{
76                 results: make(map[string]*template.Template),
77                 basedir: cfg.StaticPath,
78                 root:    cfg.Root,
79         }
80         if s.root == "" {
81                 s.root = "/"
82         }
84         var err error
85         if s.c, err = client.Connect(addr, user); err != nil {
86                 return nil, err
87         }
88         if major, minor, patch, extra, err := s.c.ServerVersion(); err == nil {
89                 log.Printf("Connected to SysDB %d.%d.%d%s.", major, minor, patch, extra)
90         }
92         if s.main, err = cfg.parse(s, "main.tmpl"); err != nil {
93                 return nil, err
94         }
95         types := []string{"graphs", "host", "hosts", "service", "services", "metric", "metrics"}
96         for _, t := range types {
97                 s.results[t], err = cfg.parse(s, t+".tmpl")
98                 if err != nil {
99                         return nil, err
100                 }
101         }
103         s.mux = map[string]handler{
104                 "images": s.static,
105                 "style":  s.static,
106                 "graph":  s.graph,
107         }
108         return s, nil
111 func (cfg Config) parse(s *Server, name string) (*template.Template, error) {
112         t := template.New(filepath.Base(name)).Funcs(template.FuncMap{
113                 "root": s.Root,
114         })
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         "graphs":   graphs,
138         "host":     fetch,
139         "service":  fetch,
140         "metric":   fetch,
141         "hosts":    listAll,
142         "services": listAll,
143         "metrics":  listAll,
144         "lookup":   lookup,
147 // ServeHTTP implements the http.Handler interface and serves
148 // the SysDB user interface.
149 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
150         path := r.RequestURI
151         if !strings.HasPrefix(path, s.root) {
152                 s.notfound(w, r)
153                 return
154         }
155         path = strings.TrimPrefix(path, s.root)
156         r.URL.Path = strings.TrimPrefix(r.URL.Path, s.root)
158         if len(path) > 0 && path[0] == '/' {
159                 path = path[1:]
160         }
161         if idx := strings.Index(path, "?"); idx != -1 {
162                 path = path[:idx]
163         }
164         var fields []string
165         for _, f := range strings.Split(path, "/") {
166                 f, err := url.QueryUnescape(f)
167                 if err != nil {
168                         s.badrequest(w, fmt.Errorf("Error: %v", err))
169                         return
170                 }
171                 fields = append(fields, f)
172         }
174         req := request{
175                 r:   r,
176                 cmd: fields[0],
177         }
178         if len(fields) > 1 {
179                 if fields[len(fields)-1] == "" {
180                         // Slash at the end of the URL
181                         fields = fields[:len(fields)-1]
182                 }
183                 if len(fields) > 1 {
184                         req.args = fields[1:]
185                 }
186         }
188         if h := s.mux[fields[0]]; h != nil {
189                 h(w, req)
190                 return
191         }
193         f, ok := content[req.cmd]
194         if !ok {
195                 s.notfound(w, r)
196                 return
197         }
198         r.ParseForm()
199         p, err := f(req, s)
200         if err != nil {
201                 p = &page{
202                         Content: "<section class=\"error\">" +
203                                 html(fmt.Sprintf("Error: %v", err)) +
204                                 "</section>",
205                 }
206         }
208         p.Query = r.FormValue("query")
209         if p.Title == "" {
210                 p.Title = "SysDB - The System Database"
211         }
213         var buf bytes.Buffer
214         err = s.main.Execute(&buf, p)
215         if err != nil {
216                 s.internal(w, err)
217                 return
218         }
220         w.WriteHeader(http.StatusOK)
221         io.Copy(w, &buf)
224 // static serves static content.
225 func (s *Server) static(w http.ResponseWriter, req request) {
226         http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
229 // Root returns the root mount point of the server suitable for use as a path
230 // prefix.
231 func (s *Server) Root() string {
232         if s.root[len(s.root)-1] == '/' {
233                 return s.root
234         }
235         return s.root + "/"
238 func index(_ request, s *Server) (*page, error) {
239         major, minor, patch, extra, err := s.c.ServerVersion()
240         if err != nil {
241                 return nil, err
242         }
244         content := fmt.Sprintf("<section>"+
245                 "<h1>Welcome to the System Database.</h1>"+
246                 "<p>Connected to SysDB %d.%d.%d%s</p>"+
247                 "</section>", major, minor, patch, html(extra))
248         return &page{Content: template.HTML(content)}, nil
251 func tmpl(t *template.Template, data interface{}) (*page, error) {
252         var buf bytes.Buffer
253         if err := t.Execute(&buf, data); err != nil {
254                 return nil, fmt.Errorf("Template error: %v", err)
255         }
256         return &page{Content: template.HTML(buf.String())}, nil
259 func html(s string) template.HTML {
260         return template.HTML(template.HTMLEscapeString(s))
263 // vim: set tw=78 sw=4 sw=4 noexpandtab :