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 // Conn specifies a connection to a SysDB server instance.
48 Conn *client.Conn
50 // TemplatePath specifies the relative or absolute location of template files.
51 TemplatePath string
53 // StaticPath specifies the relative or absolute location of static files.
54 StaticPath string
55 }
57 // A Server implements an http.Handler that serves the SysDB user interface.
58 type Server struct {
59 c *client.Conn
61 // Templates:
62 main *template.Template
63 results map[string]*template.Template
65 // Static content:
66 static http.Handler
67 }
69 // New constructs a new SysDB web server using the specified configuration.
70 func New(cfg Config) (*Server, error) {
71 s := &Server{c: cfg.Conn, results: make(map[string]*template.Template)}
73 var err error
74 s.main, err = cfg.parse("main.tmpl")
75 if err != nil {
76 return nil, err
77 }
79 types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
80 for _, t := range types {
81 s.results[t], err = cfg.parse(t + ".tmpl")
82 if err != nil {
83 return nil, err
84 }
85 }
87 s.static = http.FileServer(http.Dir(cfg.StaticPath))
88 return s, nil
89 }
91 func (cfg Config) parse(name string) (*template.Template, error) {
92 t := template.New(filepath.Base(name))
93 return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
94 }
96 type request struct {
97 r *http.Request
98 cmd string
99 args []string
100 }
102 var handlers = map[string]func(request, *Server) (template.HTML, error){
103 "": index,
105 // Queries
106 "host": fetch,
107 "service": fetch,
108 "metric": fetch,
109 "hosts": listAll,
110 "services": listAll,
111 "metrics": listAll,
112 "lookup": lookup,
113 }
115 // ServeHTTP implements the http.Handler interface and serves
116 // the SysDB user interface.
117 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118 path := r.URL.Path
119 if len(path) > 0 && path[0] == '/' {
120 path = path[1:]
121 }
122 fields := strings.Split(path, "/")
124 if fields[0] == "style" || fields[0] == "images" {
125 s.static.ServeHTTP(w, r)
126 return
127 }
129 req := request{
130 r: r,
131 cmd: fields[0],
132 }
133 if len(fields) > 1 {
134 if fields[len(fields)-1] == "" {
135 // Slash at the end of the URL
136 fields = fields[:len(fields)-1]
137 }
138 if len(fields) > 1 {
139 req.args = fields[1:]
140 }
141 }
143 f, ok := handlers[req.cmd]
144 if !ok {
145 s.notfound(w, r)
146 return
147 }
148 r.ParseForm()
149 content, err := f(req, s)
150 if err != nil {
151 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
152 return
153 }
155 page := struct {
156 Title string
157 Query string
158 Content template.HTML
159 }{
160 Title: "SysDB - The System Database",
161 Query: r.FormValue("query"),
162 Content: content,
163 }
165 var buf bytes.Buffer
166 err = s.main.Execute(&buf, &page)
167 if err != nil {
168 s.internal(w, err)
169 return
170 }
172 w.WriteHeader(http.StatusOK)
173 io.Copy(w, &buf)
174 }
176 // Content handlers.
178 func index(_ request, s *Server) (template.HTML, error) {
179 return "<section><h1>Welcome to the System Database.</h1></section>", nil
180 }
182 func listAll(req request, s *Server) (template.HTML, error) {
183 if len(req.args) != 0 {
184 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
185 }
187 res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
188 if err != nil {
189 return "", err
190 }
191 // the template *must* exist
192 return tmpl(s.results[req.cmd], res)
193 }
195 func lookup(req request, s *Server) (template.HTML, error) {
196 if req.r.Method != "POST" {
197 return "", errors.New("Method not allowed")
198 }
199 q := proto.EscapeString(req.r.FormValue("query"))
200 if q == "''" {
201 return "", errors.New("Empty query")
202 }
204 res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
205 if err != nil {
206 return "", err
207 }
208 return tmpl(s.results["hosts"], res)
209 }
211 func fetch(req request, s *Server) (template.HTML, error) {
212 if len(req.args) == 0 {
213 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
214 }
216 var q string
217 switch req.cmd {
218 case "host":
219 if len(req.args) != 1 {
220 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
221 }
222 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
223 case "service", "metric":
224 if len(req.args) < 2 {
225 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
226 }
227 host := proto.EscapeString(req.args[0])
228 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
229 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
230 default:
231 panic("Unknown request: fetch(" + req.cmd + ")")
232 }
234 res, err := s.query(q)
235 if err != nil {
236 return "", err
237 }
238 return tmpl(s.results[req.cmd], res)
239 }
241 func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
242 var buf bytes.Buffer
243 if err := t.Execute(&buf, data); err != nil {
244 return "", fmt.Errorf("Template error: %v", err)
245 }
246 return template.HTML(buf.String()), nil
247 }
249 func html(s string) template.HTML {
250 return template.HTML(template.HTMLEscapeString(s))
251 }
253 func (s *Server) query(cmd string) (interface{}, error) {
254 m := &proto.Message{
255 Type: proto.ConnectionQuery,
256 Raw: []byte(cmd),
257 }
258 if err := s.c.Send(m); err != nil {
259 return nil, fmt.Errorf("Query %q: %v", cmd, err)
260 }
262 for {
263 m, err := s.c.Receive()
264 if err != nil {
265 return nil, fmt.Errorf("Failed to receive server response: %v", err)
266 }
267 if m.Type == proto.ConnectionLog {
268 log.Println(string(m.Raw[4:]))
269 continue
270 } else if m.Type == proto.ConnectionError {
271 return nil, errors.New(string(m.Raw))
272 }
274 t, err := m.DataType()
275 if err != nil {
276 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
277 }
279 var res interface{}
280 switch t {
281 case proto.HostList:
282 var hosts []sysdb.Host
283 err = proto.Unmarshal(m, &hosts)
284 res = hosts
285 case proto.Host:
286 var host sysdb.Host
287 err = proto.Unmarshal(m, &host)
288 res = host
289 default:
290 return nil, fmt.Errorf("Unsupported data type %d", t)
291 }
292 if err != nil {
293 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
294 }
295 return res, nil
296 }
297 }
299 // vim: set tw=78 sw=4 sw=4 noexpandtab :