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 "path/filepath"
38 "strings"
40 "github.com/sysdb/go/client"
41 "github.com/sysdb/go/proto"
42 "github.com/sysdb/go/sysdb"
43 )
45 // A Config specifies configuration values for a SysDB web server.
46 type Config struct {
47 // Conns is a slice of connections to a SysDB server instance. The number of
48 // elements specifies the maximum number of parallel queries to the backend.
49 // Note that a client connection is not thread-safe but multiple idle
50 // connections don't impose any load on the server.
51 Conns []*client.Conn
53 // TemplatePath specifies the relative or absolute location of template files.
54 TemplatePath string
56 // StaticPath specifies the relative or absolute location of static files.
57 StaticPath string
58 }
60 // A Server implements an http.Handler that serves the SysDB user interface.
61 type Server struct {
62 conns chan *client.Conn
64 // Request multiplexer
65 mux map[string]handler
67 // Templates:
68 main *template.Template
69 results map[string]*template.Template
71 // Base directory of static files.
72 basedir string
73 }
75 // New constructs a new SysDB web server using the specified configuration.
76 func New(cfg Config) (*Server, error) {
77 if len(cfg.Conns) == 0 {
78 return nil, errors.New("need at least one client connection")
79 }
81 s := &Server{
82 conns: make(chan *client.Conn, len(cfg.Conns)),
83 results: make(map[string]*template.Template),
84 }
85 for _, c := range cfg.Conns {
86 s.conns <- c
87 }
89 var err error
90 s.main, err = cfg.parse("main.tmpl")
91 if err != nil {
92 return nil, err
93 }
95 types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
96 for _, t := range types {
97 s.results[t], err = cfg.parse(t + ".tmpl")
98 if err != nil {
99 return nil, err
100 }
101 }
103 s.basedir = cfg.StaticPath
104 s.mux = map[string]handler{
105 "images": s.static,
106 "style": s.static,
107 }
108 return s, nil
109 }
111 func (cfg Config) parse(name string) (*template.Template, error) {
112 t := template.New(filepath.Base(name))
113 return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
114 }
116 type request struct {
117 r *http.Request
118 cmd string
119 args []string
120 }
122 type handler func(http.ResponseWriter, request)
124 // Content generators for HTML pages.
125 var content = map[string]func(request, *Server) (template.HTML, error){
126 "": index,
128 // Queries
129 "host": fetch,
130 "service": fetch,
131 "metric": fetch,
132 "hosts": listAll,
133 "services": listAll,
134 "metrics": listAll,
135 "lookup": lookup,
136 }
138 // ServeHTTP implements the http.Handler interface and serves
139 // the SysDB user interface.
140 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
141 path := r.URL.Path
142 if len(path) > 0 && path[0] == '/' {
143 path = path[1:]
144 }
145 fields := strings.Split(path, "/")
147 req := request{
148 r: r,
149 cmd: fields[0],
150 }
151 if len(fields) > 1 {
152 if fields[len(fields)-1] == "" {
153 // Slash at the end of the URL
154 fields = fields[:len(fields)-1]
155 }
156 if len(fields) > 1 {
157 req.args = fields[1:]
158 }
159 }
161 if h := s.mux[fields[0]]; h != nil {
162 h(w, req)
163 return
164 }
166 f, ok := content[req.cmd]
167 if !ok {
168 s.notfound(w, r)
169 return
170 }
171 r.ParseForm()
172 content, err := f(req, s)
173 if err != nil {
174 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
175 return
176 }
178 page := struct {
179 Title string
180 Query string
181 Content template.HTML
182 }{
183 Title: "SysDB - The System Database",
184 Query: r.FormValue("query"),
185 Content: content,
186 }
188 var buf bytes.Buffer
189 err = s.main.Execute(&buf, &page)
190 if err != nil {
191 s.internal(w, err)
192 return
193 }
195 w.WriteHeader(http.StatusOK)
196 io.Copy(w, &buf)
197 }
199 // static serves static content.
200 func (s *Server) static(w http.ResponseWriter, req request) {
201 http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
202 }
204 // Content handlers.
206 func index(_ request, s *Server) (template.HTML, error) {
207 return "<section><h1>Welcome to the System Database.</h1></section>", nil
208 }
210 func listAll(req request, s *Server) (template.HTML, error) {
211 if len(req.args) != 0 {
212 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
213 }
215 res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
216 if err != nil {
217 return "", err
218 }
219 // the template *must* exist
220 return tmpl(s.results[req.cmd], res)
221 }
223 func lookup(req request, s *Server) (template.HTML, error) {
224 if req.r.Method != "POST" {
225 return "", errors.New("Method not allowed")
226 }
227 q := proto.EscapeString(req.r.FormValue("query"))
228 if q == "''" {
229 return "", errors.New("Empty query")
230 }
232 res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
233 if err != nil {
234 return "", err
235 }
236 return tmpl(s.results["hosts"], res)
237 }
239 func fetch(req request, s *Server) (template.HTML, error) {
240 if len(req.args) == 0 {
241 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
242 }
244 var q string
245 switch req.cmd {
246 case "host":
247 if len(req.args) != 1 {
248 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
249 }
250 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
251 case "service", "metric":
252 if len(req.args) < 2 {
253 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
254 }
255 host := proto.EscapeString(req.args[0])
256 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
257 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
258 default:
259 panic("Unknown request: fetch(" + req.cmd + ")")
260 }
262 res, err := s.query(q)
263 if err != nil {
264 return "", err
265 }
266 return tmpl(s.results[req.cmd], res)
267 }
269 func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
270 var buf bytes.Buffer
271 if err := t.Execute(&buf, data); err != nil {
272 return "", fmt.Errorf("Template error: %v", err)
273 }
274 return template.HTML(buf.String()), nil
275 }
277 func html(s string) template.HTML {
278 return template.HTML(template.HTMLEscapeString(s))
279 }
281 func (s *Server) query(cmd string) (interface{}, error) {
282 c := <-s.conns
283 defer func() { s.conns <- c }()
285 m := &proto.Message{
286 Type: proto.ConnectionQuery,
287 Raw: []byte(cmd),
288 }
289 if err := c.Send(m); err != nil {
290 return nil, fmt.Errorf("Query %q: %v", cmd, err)
291 }
293 for {
294 m, err := c.Receive()
295 if err != nil {
296 return nil, fmt.Errorf("Failed to receive server response: %v", err)
297 }
298 if m.Type == proto.ConnectionLog {
299 log.Println(string(m.Raw[4:]))
300 continue
301 } else if m.Type == proto.ConnectionError {
302 return nil, errors.New(string(m.Raw))
303 }
305 t, err := m.DataType()
306 if err != nil {
307 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
308 }
310 var res interface{}
311 switch t {
312 case proto.HostList:
313 var hosts []sysdb.Host
314 err = proto.Unmarshal(m, &hosts)
315 res = hosts
316 case proto.Host:
317 var host sysdb.Host
318 err = proto.Unmarshal(m, &host)
319 res = host
320 default:
321 return nil, fmt.Errorf("Unsupported data type %d", t)
322 }
323 if err != nil {
324 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
325 }
326 return res, nil
327 }
328 }
330 // vim: set tw=78 sw=4 sw=4 noexpandtab :