launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/store/server.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package store 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "strconv" 12 "strings" 13 "time" 14 15 "launchpad.net/juju-core/charm" 16 "launchpad.net/juju-core/log" 17 ) 18 19 // Server is an http.Handler that serves the HTTP API of juju 20 // so that juju clients can retrieve published charms. 21 type Server struct { 22 store *Store 23 mux *http.ServeMux 24 } 25 26 // New returns a new *Server using store. 27 func NewServer(store *Store) (*Server, error) { 28 s := &Server{ 29 store: store, 30 mux: http.NewServeMux(), 31 } 32 s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) { 33 s.serveInfo(w, r) 34 }) 35 s.mux.HandleFunc("/charm-event", func(w http.ResponseWriter, r *http.Request) { 36 s.serveEvent(w, r) 37 }) 38 s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) { 39 s.serveCharm(w, r) 40 }) 41 s.mux.HandleFunc("/stats/counter/", func(w http.ResponseWriter, r *http.Request) { 42 s.serveStats(w, r) 43 }) 44 45 // This is just a validation key to allow blitz.io to run 46 // performance tests against the site. 47 s.mux.HandleFunc("/mu-35700a31-6bf320ca-a800b670-05f845ee", func(w http.ResponseWriter, r *http.Request) { 48 s.serveBlitzKey(w, r) 49 }) 50 return s, nil 51 } 52 53 // ServeHTTP serves an http request. 54 // This method turns *Server into an http.Handler. 55 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 56 if r.URL.Path == "/" { 57 http.Redirect(w, r, "https://juju.ubuntu.com", http.StatusSeeOther) 58 return 59 } 60 s.mux.ServeHTTP(w, r) 61 } 62 63 func statsEnabled(req *http.Request) bool { 64 // It's fine to parse the form more than once, and it avoids 65 // bugs from not parsing it. 66 req.ParseForm() 67 return req.Form.Get("stats") != "0" 68 } 69 70 func charmStatsKey(curl *charm.URL, kind string) []string { 71 if curl.User == "" { 72 return []string{kind, curl.Series, curl.Name} 73 } 74 return []string{kind, curl.Series, curl.Name, curl.User} 75 } 76 77 func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { 78 if r.URL.Path != "/charm-info" { 79 w.WriteHeader(http.StatusNotFound) 80 return 81 } 82 r.ParseForm() 83 response := map[string]*charm.InfoResponse{} 84 for _, url := range r.Form["charms"] { 85 c := &charm.InfoResponse{} 86 response[url] = c 87 curl, err := charm.ParseURL(url) 88 var info *CharmInfo 89 if err == nil { 90 info, err = s.store.CharmInfo(curl) 91 } 92 var skey []string 93 if err == nil { 94 skey = charmStatsKey(curl, "charm-info") 95 c.Sha256 = info.BundleSha256() 96 c.Revision = info.Revision() 97 c.Digest = info.Digest() 98 } else { 99 if err == ErrNotFound { 100 skey = charmStatsKey(curl, "charm-missing") 101 } 102 c.Errors = append(c.Errors, err.Error()) 103 } 104 if skey != nil && statsEnabled(r) { 105 go s.store.IncCounter(skey) 106 } 107 } 108 data, err := json.Marshal(response) 109 if err == nil { 110 w.Header().Set("Content-Type", "application/json") 111 _, err = w.Write(data) 112 } 113 if err != nil { 114 log.Errorf("store: cannot write content: %v", err) 115 w.WriteHeader(http.StatusInternalServerError) 116 return 117 } 118 } 119 120 func (s *Server) serveEvent(w http.ResponseWriter, r *http.Request) { 121 if r.URL.Path != "/charm-event" { 122 w.WriteHeader(http.StatusNotFound) 123 return 124 } 125 r.ParseForm() 126 response := map[string]*charm.EventResponse{} 127 for _, url := range r.Form["charms"] { 128 digest := "" 129 if i := strings.Index(url, "@"); i >= 0 && i+1 < len(url) { 130 digest = url[i+1:] 131 url = url[:i] 132 } 133 c := &charm.EventResponse{} 134 response[url] = c 135 curl, err := charm.ParseURL(url) 136 var event *CharmEvent 137 if err == nil { 138 event, err = s.store.CharmEvent(curl, digest) 139 } 140 var skey []string 141 if err == nil { 142 skey = charmStatsKey(curl, "charm-event") 143 c.Kind = event.Kind.String() 144 c.Revision = event.Revision 145 c.Digest = event.Digest 146 c.Errors = event.Errors 147 c.Warnings = event.Warnings 148 c.Time = event.Time.UTC().Format(time.RFC3339) 149 } else { 150 c.Errors = append(c.Errors, err.Error()) 151 } 152 if skey != nil && statsEnabled(r) { 153 go s.store.IncCounter(skey) 154 } 155 } 156 data, err := json.Marshal(response) 157 if err == nil { 158 w.Header().Set("Content-Type", "application/json") 159 _, err = w.Write(data) 160 } 161 if err != nil { 162 log.Errorf("store: cannot write content: %v", err) 163 w.WriteHeader(http.StatusInternalServerError) 164 return 165 } 166 } 167 168 func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) { 169 if !strings.HasPrefix(r.URL.Path, "/charm/") { 170 panic("serveCharm: bad url") 171 } 172 curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) 173 if err != nil { 174 w.WriteHeader(http.StatusNotFound) 175 return 176 } 177 info, rc, err := s.store.OpenCharm(curl) 178 if err == ErrNotFound { 179 w.WriteHeader(http.StatusNotFound) 180 return 181 } 182 if err != nil { 183 w.WriteHeader(http.StatusInternalServerError) 184 log.Errorf("store: cannot open charm %q: %v", curl, err) 185 return 186 } 187 if statsEnabled(r) { 188 go s.store.IncCounter(charmStatsKey(curl, "charm-bundle")) 189 } 190 defer rc.Close() 191 w.Header().Set("Connection", "close") // No keep-alive for now. 192 w.Header().Set("Content-Type", "application/octet-stream") 193 w.Header().Set("Content-Length", strconv.FormatInt(info.BundleSize(), 10)) 194 _, err = io.Copy(w, rc) 195 if err != nil { 196 log.Errorf("store: failed to stream charm %q: %v", curl, err) 197 } 198 } 199 200 func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) { 201 // TODO: Adopt a smarter mux that simplifies this logic. 202 const dir = "/stats/counter/" 203 if !strings.HasPrefix(r.URL.Path, dir) { 204 panic("bad url") 205 } 206 base := r.URL.Path[len(dir):] 207 if strings.Index(base, "/") > 0 { 208 w.WriteHeader(http.StatusNotFound) 209 return 210 } 211 if base == "" { 212 w.WriteHeader(http.StatusForbidden) 213 return 214 } 215 r.ParseForm() 216 var by CounterRequestBy 217 switch v := r.Form.Get("by"); v { 218 case "": 219 by = ByAll 220 case "day": 221 by = ByDay 222 case "week": 223 by = ByWeek 224 default: 225 w.WriteHeader(http.StatusBadRequest) 226 w.Write([]byte(fmt.Sprintf("Invalid 'by' value: %q", v))) 227 return 228 } 229 req := CounterRequest{ 230 Key: strings.Split(base, ":"), 231 List: r.Form.Get("list") == "1", 232 By: by, 233 } 234 if v := r.Form.Get("start"); v != "" { 235 var err error 236 req.Start, err = time.Parse("2006-01-02", v) 237 if err != nil { 238 w.WriteHeader(http.StatusBadRequest) 239 w.Write([]byte(fmt.Sprintf("Invalid 'start' value: %q", v))) 240 return 241 } 242 } 243 if v := r.Form.Get("stop"); v != "" { 244 var err error 245 req.Stop, err = time.Parse("2006-01-02", v) 246 if err != nil { 247 w.WriteHeader(http.StatusBadRequest) 248 w.Write([]byte(fmt.Sprintf("Invalid 'stop' value: %q", v))) 249 return 250 } 251 // Cover all timestamps within the stop day. 252 req.Stop = req.Stop.Add(24*time.Hour - 1*time.Second) 253 } 254 if req.Key[len(req.Key)-1] == "*" { 255 req.Prefix = true 256 req.Key = req.Key[:len(req.Key)-1] 257 if len(req.Key) == 0 { 258 // No point in counting something unknown. 259 w.WriteHeader(http.StatusForbidden) 260 return 261 } 262 } 263 var format func([]formatItem) []byte 264 switch v := r.Form.Get("format"); v { 265 case "": 266 if !req.List && req.By == ByAll { 267 format = formatCount 268 } else { 269 format = formatText 270 } 271 case "text": 272 format = formatText 273 case "csv": 274 format = formatCSV 275 case "json": 276 format = formatJSON 277 default: 278 w.WriteHeader(http.StatusBadRequest) 279 w.Write([]byte(fmt.Sprintf("Invalid 'format' value: %q", v))) 280 return 281 } 282 283 entries, err := s.store.Counters(&req) 284 if err != nil { 285 log.Errorf("store: cannot query counters: %v", err) 286 w.WriteHeader(http.StatusInternalServerError) 287 return 288 } 289 290 var buf []byte 291 var items []formatItem 292 for i := range entries { 293 entry := &entries[i] 294 if req.List { 295 for j := range entry.Key { 296 buf = append(buf, entry.Key[j]...) 297 buf = append(buf, ':') 298 } 299 if entry.Prefix { 300 buf = append(buf, '*') 301 } else { 302 buf = buf[:len(buf)-1] 303 } 304 } 305 items = append(items, formatItem{string(buf), entry.Count, entry.Time}) 306 buf = buf[:0] 307 } 308 309 buf = format(items) 310 w.Header().Set("Content-Type", "text/plain") 311 w.Header().Set("Content-Length", strconv.Itoa(len(buf))) 312 _, err = w.Write(buf) 313 if err != nil { 314 log.Errorf("store: cannot write content: %v", err) 315 w.WriteHeader(http.StatusInternalServerError) 316 } 317 } 318 319 func (s *Server) serveBlitzKey(w http.ResponseWriter, r *http.Request) { 320 w.Header().Set("Connection", "close") 321 w.Header().Set("Content-Type", "text/plain") 322 w.Header().Set("Content-Length", "2") 323 w.Write([]byte("42")) 324 } 325 326 type formatItem struct { 327 key string 328 count int64 329 time time.Time 330 } 331 332 func (fi *formatItem) hasKey() bool { 333 return fi.key != "" 334 } 335 336 func (fi *formatItem) hasTime() bool { 337 return !fi.time.IsZero() 338 } 339 340 func (fi *formatItem) formatTime() string { 341 return fi.time.Format("2006-01-02") 342 } 343 344 func formatCount(items []formatItem) []byte { 345 return strconv.AppendInt(nil, items[0].count, 10) 346 } 347 348 func formatText(items []formatItem) []byte { 349 var maxKeyLength int 350 for i := range items { 351 if l := len(items[i].key); maxKeyLength < l { 352 maxKeyLength = l 353 } 354 } 355 spaces := make([]byte, maxKeyLength+2) 356 for i := range spaces { 357 spaces[i] = ' ' 358 } 359 var buf []byte 360 for i := range items { 361 item := &items[i] 362 if item.hasKey() { 363 buf = append(buf, item.key...) 364 buf = append(buf, spaces[len(item.key):]...) 365 } 366 if item.hasTime() { 367 buf = append(buf, item.formatTime()...) 368 buf = append(buf, ' ', ' ') 369 } 370 buf = strconv.AppendInt(buf, item.count, 10) 371 buf = append(buf, '\n') 372 } 373 return buf 374 } 375 376 func formatCSV(items []formatItem) []byte { 377 var buf []byte 378 for i := range items { 379 item := &items[i] 380 if item.hasKey() { 381 buf = append(buf, item.key...) 382 buf = append(buf, ',') 383 } 384 if item.hasTime() { 385 buf = append(buf, item.formatTime()...) 386 buf = append(buf, ',') 387 } 388 buf = strconv.AppendInt(buf, item.count, 10) 389 buf = append(buf, '\n') 390 } 391 return buf 392 } 393 394 func formatJSON(items []formatItem) []byte { 395 if len(items) == 0 { 396 return []byte("[]") 397 } 398 var buf []byte 399 buf = append(buf, '[') 400 for i := range items { 401 item := &items[i] 402 if i == 0 { 403 buf = append(buf, '[') 404 } else { 405 buf = append(buf, ',', '[') 406 } 407 if item.hasKey() { 408 buf = append(buf, '"') 409 buf = append(buf, item.key...) 410 buf = append(buf, '"', ',') 411 } 412 if item.hasTime() { 413 buf = append(buf, '"') 414 buf = append(buf, item.formatTime()...) 415 buf = append(buf, '"', ',') 416 } 417 buf = strconv.AppendInt(buf, item.count, 10) 418 buf = append(buf, ']') 419 } 420 buf = append(buf, ']') 421 return buf 422 }