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
108 }
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))
113 }
115 type request struct {
116 r *http.Request
117 cmd string
118 args []string
119 }
121 type handler func(http.ResponseWriter, request)
123 type page struct {
124 Title string
125 Query string
126 Content template.HTML
127 }
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,
141 }
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 if idx := strings.Index(path, "?"); idx != -1 {
151 path = path[:idx]
152 }
153 var fields []string
154 for _, f := range strings.Split(path, "/") {
155 f, err := url.QueryUnescape(f)
156 if err != nil {
157 s.badrequest(w, 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 p, err := f(req, s)
189 if err != nil {
190 p = &page{
191 Content: "<section class=\"error\">" +
192 html(fmt.Sprintf("Error: %v", err)) +
193 "</section>",
194 }
195 }
197 p.Query = r.FormValue("query")
198 if p.Title == "" {
199 p.Title = "SysDB - The System Database"
200 }
202 var buf bytes.Buffer
203 err = s.main.Execute(&buf, p)
204 if err != nil {
205 s.internal(w, err)
206 return
207 }
209 w.WriteHeader(http.StatusOK)
210 io.Copy(w, &buf)
211 }
213 // static serves static content.
214 func (s *Server) static(w http.ResponseWriter, req request) {
215 http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
216 }
218 func index(_ request, s *Server) (*page, error) {
219 c := <-s.conns
220 defer func() { s.conns <- c }()
222 major, minor, patch, extra, err := c.ServerVersion()
223 if err != nil {
224 return nil, err
225 }
227 content := fmt.Sprintf("<section>"+
228 "<h1>Welcome to the System Database.</h1>"+
229 "<p>Connected to SysDB %d.%d.%d%s</p>"+
230 "</section>", major, minor, patch, html(extra))
231 return &page{Content: template.HTML(content)}, nil
232 }
234 func tmpl(t *template.Template, data interface{}) (*page, error) {
235 var buf bytes.Buffer
236 if err := t.Execute(&buf, data); err != nil {
237 return nil, fmt.Errorf("Template error: %v", err)
238 }
239 return &page{Content: template.HTML(buf.String())}, nil
240 }
242 func html(s string) template.HTML {
243 return template.HTML(template.HTMLEscapeString(s))
244 }
246 // vim: set tw=78 sw=4 sw=4 noexpandtab :