1 // Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions
6 // are met:
7 // 1. Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 // 2. Redistributions in binary form must reproduce the above copyright
10 // notice, this list of conditions and the following disclaimer in the
11 // documentation and/or other materials provided with the distribution.
12 //
13 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
15 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
17 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
20 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
22 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
23 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 // ui is a web-based UI interacting with the backend server.
26 package main
28 import (
29 "bytes"
30 "errors"
31 "flag"
32 "fmt"
33 "html/template"
34 "io"
35 "log"
36 "net/http"
37 "strings"
38 "sync"
39 "time"
41 "golang.org/x/net/context"
42 "google.golang.org/grpc"
44 pb "tokkee.org/go-talk/grpc/proto/backend"
45 )
47 var (
48 listen = flag.String("listen", ":8080", "listen address")
49 backend = flag.String("backend", "localhost:50051", "backend server address")
51 queryTmpl = template.Must(template.New("query").Parse(`<html>
52 <head><title>Backend Query</title></head>
53 <body>
54 <form action="/query" method="POST">
55 Query: <input type="text" name="q" placeholder="<cmd> <arg>..." value="{{.Query}}" />
56 <input type="submit" value="Submit">
57 </form>
58 {{range .Responses}}
59 <p><b>{{.Request}} =></b> {{.Type}}: {{.N}}</p>
60 {{end}}
61 </body>
62 </html>
63 `))
64 )
66 type server struct {
67 c pb.BackendClient
68 }
70 type queryResponse struct {
71 Request string
72 Type string
73 N int64
74 }
76 // runQueries executes multiple queries, separated by semicolon, in parallel.
77 func (s *server) runQueries(ctx context.Context, query string) ([]queryResponse, error) {
78 requests := strings.Split(query, ";")
79 for i, r := range requests {
80 requests[i] = strings.TrimSpace(r)
81 }
83 responses := make([]queryResponse, len(requests))
84 errCh := make(chan error, len(requests))
86 for i, req := range requests {
87 go func(i int, req string) {
88 res, err := s.c.Query(ctx, &pb.QueryRequest{Query: req})
89 defer func() { errCh <- err }()
90 if err != nil {
91 return
92 }
94 responses[i] = queryResponse{
95 Request: req,
96 Type: res.Type,
97 N: res.N,
98 }
99 }(i, req)
100 }
102 timeout := time.After(50 * time.Millisecond)
104 for _ = range requests {
105 select {
106 case err := <-errCh:
107 if err != nil {
108 return nil, err
109 }
111 case <-timeout:
112 return nil, errors.New("request timed out")
113 }
114 }
116 return responses, nil
117 }
119 func (s *server) query(ctx context.Context, w http.ResponseWriter, r *http.Request) {
120 data := &struct {
121 Query string
122 Responses []queryResponse
123 }{}
125 data.Query = r.Form.Get("q")
126 if data.Query != "" {
127 if r.Method != "POST" {
128 // RFC 2616 requires us to set the "Allow" header.
129 w.Header().Add("Allow", "POST")
130 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
131 return
132 }
133 // TODO: use and validate XSRF tokens
135 res, err := s.runQueries(ctx, data.Query)
136 if err != nil {
137 http.Error(w, fmt.Sprintf("query failed: %v", err), http.StatusBadRequest)
138 return
139 }
140 data.Responses = res
141 }
143 var buf bytes.Buffer
144 if err := queryTmpl.Execute(&buf, data); err != nil {
145 http.Error(w, fmt.Sprintf("template error: %v", err), http.StatusInternalServerError)
146 return
147 }
148 io.Copy(w, &buf)
149 }
151 type handler func(context.Context, http.ResponseWriter, *http.Request)
153 // A mux multiplexes incoming requests based on the first part of the incoming
154 // request URI. It differs from http.ServerMux in that it only support routing
155 // based on the first part of the request path and it adds context handling
156 // and form parsing. mux implements http.Handler.
157 type mux struct {
158 mu sync.RWMutex
159 handlers map[string]handler
160 }
162 // Handle registers a handler for the specified path. It panics if a handler
163 // has already been registered for the same path or if the path includes a
164 // slash.
165 func (m *mux) Handle(path string, h handler) {
166 if strings.Index(path, "/") != -1 {
167 panic("invalid path: " + path)
168 }
169 if h == nil {
170 panic("invalid nil handler")
171 }
173 m.mu.Lock()
174 defer m.mu.Unlock()
176 if m.handlers == nil {
177 m.handlers = make(map[string]handler)
178 }
179 if m.handlers[path] != nil {
180 panic(fmt.Sprintf("duplicate handlers registered for %q", path))
181 }
182 m.handlers[path] = h
183 }
185 // ServeHTTP handles incoming requests using the registered handlers and takes
186 // care of request-specific setup (context management and form parsing).
187 func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
188 path := r.URL.Path
189 if len(path) > 0 && path[0] == '/' {
190 path = path[1:]
191 }
192 if f := strings.Split(path, "/"); len(f) > 1 {
193 path = f[0]
194 }
196 if err := r.ParseForm(); err != nil {
197 log.Printf("Invalid URL query: %v", err)
198 http.Error(w, fmt.Sprintf("invalid URL query: %v", err), http.StatusBadRequest)
199 return
200 }
202 m.mu.RLock()
203 h := m.handlers[path]
204 m.mu.RUnlock()
206 if h == nil {
207 log.Printf("Not found: %q (%v)", path, r.URL)
208 http.NotFound(w, r)
209 return
210 }
211 h(context.Background(), w, r)
212 }
214 func main() {
215 flag.Parse()
217 conn, err := grpc.Dial(*backend, grpc.WithInsecure())
218 if err != nil {
219 log.Fatalf("Failed to connect to backend server at %s: %v", *backend, err)
220 }
221 defer conn.Close()
223 s := &server{c: pb.NewBackendClient(conn)}
224 m := &mux{}
225 m.Handle("query", s.query)
226 log.Printf("Listening on %s ...", *listen)
227 log.Fatal(http.ListenAndServe(*listen, m))
228 }