Code

c0f449cd451dd8d735107c8d8ca877f6fa5aabbc
[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         "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         // Conns is a slice of connections to a SysDB server instance. The number of
46         // elements specifies the maximum number of parallel queries to the backend.
47         // Note that a client connection is not thread-safe but multiple idle
48         // connections don't impose any load on the server.
49         Conns []*client.Conn
51         // TemplatePath specifies the relative or absolute location of template files.
52         TemplatePath string
54         // StaticPath specifies the relative or absolute location of static files.
55         StaticPath string
56 }
58 // A Server implements an http.Handler that serves the SysDB user interface.
59 type Server struct {
60         conns chan *client.Conn
62         // Request multiplexer
63         mux map[string]handler
65         // Templates:
66         main    *template.Template
67         results map[string]*template.Template
69         // Base directory of static files.
70         basedir string
71 }
73 // New constructs a new SysDB web server using the specified configuration.
74 func New(cfg Config) (*Server, error) {
75         if len(cfg.Conns) == 0 {
76                 return nil, errors.New("need at least one client connection")
77         }
79         s := &Server{
80                 conns:   make(chan *client.Conn, len(cfg.Conns)),
81                 results: make(map[string]*template.Template),
82         }
83         for _, c := range cfg.Conns {
84                 s.conns <- c
85         }
87         var err error
88         s.main, err = cfg.parse("main.tmpl")
89         if err != nil {
90                 return nil, err
91         }
93         types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
94         for _, t := range types {
95                 s.results[t], err = cfg.parse(t + ".tmpl")
96                 if err != nil {
97                         return nil, err
98                 }
99         }
101         s.basedir = cfg.StaticPath
102         s.mux = map[string]handler{
103                 "images": s.static,
104                 "style":  s.static,
105                 "graph":  s.graph,
106         }
107         return s, nil
110 func (cfg Config) parse(name string) (*template.Template, error) {
111         t := template.New(filepath.Base(name))
112         return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
115 type request struct {
116         r    *http.Request
117         cmd  string
118         args []string
121 type handler func(http.ResponseWriter, request)
123 type page struct {
124         Title   string
125         Query   string
126         Content template.HTML
129 // Content generators for HTML pages.
130 var content = map[string]func(request, *Server) (*page, error){
131         "": index,
133         // Queries
134         "host":     fetch,
135         "service":  fetch,
136         "metric":   fetch,
137         "hosts":    listAll,
138         "services": listAll,
139         "metrics":  listAll,
140         "lookup":   lookup,
143 // ServeHTTP implements the http.Handler interface and serves
144 // the SysDB user interface.
145 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
146         path := r.RequestURI
147         if len(path) > 0 && path[0] == '/' {
148                 path = path[1:]
149         }
150         var fields []string
151         for _, f := range strings.Split(path, "/") {
152                 f, err := url.QueryUnescape(f)
153                 if err != nil {
154                         s.badrequest(w, fmt.Errorf("Error: %v", err))
155                         return
156                 }
157                 fields = append(fields, f)
158         }
160         req := request{
161                 r:   r,
162                 cmd: fields[0],
163         }
164         if len(fields) > 1 {
165                 if fields[len(fields)-1] == "" {
166                         // Slash at the end of the URL
167                         fields = fields[:len(fields)-1]
168                 }
169                 if len(fields) > 1 {
170                         req.args = fields[1:]
171                 }
172         }
174         if h := s.mux[fields[0]]; h != nil {
175                 h(w, req)
176                 return
177         }
179         f, ok := content[req.cmd]
180         if !ok {
181                 s.notfound(w, r)
182                 return
183         }
184         r.ParseForm()
185         p, err := f(req, s)
186         if err != nil {
187                 p = &page{
188                         Content: "<section class=\"error\">" +
189                                 html(fmt.Sprintf("Error: %v", err)) +
190                                 "</section>",
191                 }
192         }
194         p.Query = r.FormValue("query")
195         if p.Title == "" {
196                 p.Title = "SysDB - The System Database"
197         }
199         var buf bytes.Buffer
200         err = s.main.Execute(&buf, p)
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 func index(_ request, s *Server) (*page, error) {
216         return &page{Content: "<section><h1>Welcome to the System Database.</h1></section>"}, nil
219 func tmpl(t *template.Template, data interface{}) (*page, error) {
220         var buf bytes.Buffer
221         if err := t.Execute(&buf, data); err != nil {
222                 return nil, fmt.Errorf("Template error: %v", err)
223         }
224         return &page{Content: template.HTML(buf.String())}, nil
227 func html(s string) template.HTML {
228         return template.HTML(template.HTMLEscapeString(s))
231 // vim: set tw=78 sw=4 sw=4 noexpandtab :