Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2012, 2013 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package charmstore | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "gopkg.in/juju/charm.v2" | |
| ) | |
| const DefaultSeries = "precise" | |
| // Server is an http.Handler that serves the HTTP API of juju | |
| // so that juju clients can retrieve published charms. | |
| type Server struct { | |
| store *Store | |
| mux *http.ServeMux | |
| } | |
| // NewServer returns a new *Server using store. | |
| func NewServer(store *Store) (*Server, error) { | |
| s := &Server{ | |
| store: store, | |
| mux: http.NewServeMux(), | |
| } | |
| s.handle("/charm-info", s.serveInfo) | |
| s.handle("/charm-event", s.serveEvent) | |
| s.handle("/charm/", s.serveCharm) | |
| s.handle("/stats/counter/", s.serveStats) | |
| // This is just a validation key to allow blitz.io to run | |
| // performance tests against the site. | |
| s.handle("/mu-35700a31-6bf320ca-a800b670-05f845ee", s.serveBlitzKey) | |
| return s, nil | |
| } | |
| func (s *Server) handle(path string, handler http.HandlerFunc) { | |
| s.mux.Handle(path, http.StripPrefix(path, handler)) | |
| } | |
| // ServeHTTP serves an http request. | |
| // This method turns *Server into an http.Handler. | |
| func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/" { | |
| http.Redirect(w, r, "https://juju.ubuntu.com", http.StatusSeeOther) | |
| return | |
| } | |
| s.mux.ServeHTTP(w, r) | |
| } | |
| func statsEnabled(req *http.Request) bool { | |
| // It's fine to parse the form more than once, and it avoids | |
| // bugs from not parsing it. | |
| req.ParseForm() | |
| return req.Form.Get("stats") != "0" | |
| } | |
| func charmStatsKey(curl *charm.URL, kind string) []string { | |
| if curl.User == "" { | |
| return []string{kind, curl.Series, curl.Name} | |
| } | |
| return []string{kind, curl.Series, curl.Name, curl.User} | |
| } | |
| func (s *Server) resolveURL(url string) (*charm.URL, error) { | |
| ref, series, err := charm.ParseReference(url) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if series == "" { | |
| prefSeries, err := s.store.Series(ref) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(prefSeries) == 0 { | |
| return nil, ErrNotFound | |
| } | |
| return &charm.URL{Reference: ref, Series: prefSeries[0]}, nil | |
| } | |
| return &charm.URL{Reference: ref, Series: series}, nil | |
| } | |
| func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { | |
| r.ParseForm() | |
| response := map[string]*charm.InfoResponse{} | |
| for _, url := range r.Form["charms"] { | |
| c := &charm.InfoResponse{} | |
| response[url] = c | |
| curl, err := s.resolveURL(url) | |
| var info *CharmInfo | |
| if err == nil { | |
| info, err = s.store.CharmInfo(curl) | |
| } | |
| var skey []string | |
| if err == nil { | |
| skey = charmStatsKey(curl, "charm-info") | |
| c.CanonicalURL = curl.String() | |
| c.Sha256 = info.ArchiveSha256() | |
| c.Revision = info.Revision() | |
| c.Digest = info.Digest() | |
| } else { | |
| if err == ErrNotFound && curl != nil { | |
| skey = charmStatsKey(curl, "charm-missing") | |
| } | |
| c.Errors = append(c.Errors, err.Error()) | |
| } | |
| if skey != nil && statsEnabled(r) { | |
| go s.store.IncCounter(skey) | |
| } | |
| } | |
| data, err := json.Marshal(response) | |
| if err == nil { | |
| w.Header().Set("Content-Type", "application/json") | |
| _, err = w.Write(data) | |
| } | |
| if err != nil { | |
| logger.Errorf("cannot write content: %v", err) | |
| w.WriteHeader(http.StatusInternalServerError) | |
| return | |
| } | |
| } | |
| func (s *Server) serveEvent(w http.ResponseWriter, r *http.Request) { | |
| r.ParseForm() | |
| response := map[string]*charm.EventResponse{} | |
| for _, url := range r.Form["charms"] { | |
| shortURL := url | |
| digest := "" | |
| if i := strings.Index(url, "@"); i >= 0 && i+1 < len(url) { | |
| digest = url[i+1:] | |
| shortURL = url[:i] | |
| } | |
| c := &charm.EventResponse{} | |
| // By default, shortURL is used as the key in the response data. | |
| // This makes it impossible to return more than one event per charm. | |
| // If the query parameter "long_keys=1" is set, use the parameter | |
| // from the request (<cs-url>@<revision-id>) as the key. | |
| if r.Form.Get("long_keys") != "1" { | |
| response[shortURL] = c | |
| } else { | |
| response[url] = c | |
| } | |
| curl, err := s.resolveURL(shortURL) | |
| var event *CharmEvent | |
| if err == nil { | |
| event, err = s.store.CharmEvent(curl, digest) | |
| } | |
| var skey []string | |
| if err == nil { | |
| skey = charmStatsKey(curl, "charm-event") | |
| c.Kind = event.Kind.String() | |
| c.Revision = event.Revision | |
| c.Digest = event.Digest | |
| c.Errors = event.Errors | |
| c.Warnings = event.Warnings | |
| c.Time = event.Time.UTC().Format(time.RFC3339) | |
| } else { | |
| c.Errors = append(c.Errors, err.Error()) | |
| } | |
| if skey != nil && statsEnabled(r) { | |
| go s.store.IncCounter(skey) | |
| } | |
| } | |
| data, err := json.Marshal(response) | |
| if err == nil { | |
| w.Header().Set("Content-Type", "application/json") | |
| _, err = w.Write(data) | |
| } | |
| if err != nil { | |
| logger.Errorf("cannot write content: %v", err) | |
| w.WriteHeader(http.StatusInternalServerError) | |
| return | |
| } | |
| } | |
| func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) { | |
| curl, err := s.resolveURL("cs:" + r.URL.Path) | |
| if err != nil { | |
| w.WriteHeader(http.StatusNotFound) | |
| return | |
| } | |
| info, rc, err := s.store.OpenCharm(curl) | |
| if err == ErrNotFound { | |
| w.WriteHeader(http.StatusNotFound) | |
| return | |
| } | |
| if err != nil { | |
| w.WriteHeader(http.StatusInternalServerError) | |
| logger.Errorf("cannot open charm %q: %v", curl, err) | |
| return | |
| } | |
| if statsEnabled(r) { | |
| go s.store.IncCounter(charmStatsKey(curl, "charm-bundle")) | |
| } | |
| defer rc.Close() | |
| w.Header().Set("Connection", "close") // No keep-alive for now. | |
| w.Header().Set("Content-Type", "application/octet-stream") | |
| w.Header().Set("Content-Length", strconv.FormatInt(info.ArchiveSize(), 10)) | |
| _, err = io.Copy(w, rc) | |
| if err != nil { | |
| logger.Errorf("failed to stream charm %q: %v", curl, err) | |
| } | |
| } | |
| func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) { | |
| // TODO: Adopt a smarter mux that simplifies this logic. | |
| base := r.URL.Path | |
| if strings.Index(base, "/") > 0 { | |
| w.WriteHeader(http.StatusNotFound) | |
| return | |
| } | |
| if base == "" { | |
| w.WriteHeader(http.StatusForbidden) | |
| return | |
| } | |
| r.ParseForm() | |
| var by CounterRequestBy | |
| switch v := r.Form.Get("by"); v { | |
| case "": | |
| by = ByAll | |
| case "day": | |
| by = ByDay | |
| case "week": | |
| by = ByWeek | |
| default: | |
| w.WriteHeader(http.StatusBadRequest) | |
| w.Write([]byte(fmt.Sprintf("Invalid 'by' value: %q", v))) | |
| return | |
| } | |
| req := CounterRequest{ | |
| Key: strings.Split(base, ":"), | |
| List: r.Form.Get("list") == "1", | |
| By: by, | |
| } | |
| if v := r.Form.Get("start"); v != "" { | |
| var err error | |
| req.Start, err = time.Parse("2006-01-02", v) | |
| if err != nil { | |
| w.WriteHeader(http.StatusBadRequest) | |
| w.Write([]byte(fmt.Sprintf("Invalid 'start' value: %q", v))) | |
| return | |
| } | |
| } | |
| if v := r.Form.Get("stop"); v != "" { | |
| var err error | |
| req.Stop, err = time.Parse("2006-01-02", v) | |
| if err != nil { | |
| w.WriteHeader(http.StatusBadRequest) | |
| w.Write([]byte(fmt.Sprintf("Invalid 'stop' value: %q", v))) | |
| return | |
| } | |
| // Cover all timestamps within the stop day. | |
| req.Stop = req.Stop.Add(24*time.Hour - 1*time.Second) | |
| } | |
| if req.Key[len(req.Key)-1] == "*" { | |
| req.Prefix = true | |
| req.Key = req.Key[:len(req.Key)-1] | |
| if len(req.Key) == 0 { | |
| // No point in counting something unknown. | |
| w.WriteHeader(http.StatusForbidden) | |
| return | |
| } | |
| } | |
| var format func([]formatItem) []byte | |
| switch v := r.Form.Get("format"); v { | |
| case "": | |
| if !req.List && req.By == ByAll { | |
| format = formatCount | |
| } else { | |
| format = formatText | |
| } | |
| case "text": | |
| format = formatText | |
| case "csv": | |
| format = formatCSV | |
| case "json": | |
| format = formatJSON | |
| default: | |
| w.WriteHeader(http.StatusBadRequest) | |
| w.Write([]byte(fmt.Sprintf("Invalid 'format' value: %q", v))) | |
| return | |
| } | |
| entries, err := s.store.Counters(&req) | |
| if err != nil { | |
| logger.Errorf("cannot query counters: %v", err) | |
| w.WriteHeader(http.StatusInternalServerError) | |
| return | |
| } | |
| var buf []byte | |
| var items []formatItem | |
| for i := range entries { | |
| entry := &entries[i] | |
| if req.List { | |
| for j := range entry.Key { | |
| buf = append(buf, entry.Key[j]...) | |
| buf = append(buf, ':') | |
| } | |
| if entry.Prefix { | |
| buf = append(buf, '*') | |
| } else { | |
| buf = buf[:len(buf)-1] | |
| } | |
| } | |
| items = append(items, formatItem{string(buf), entry.Count, entry.Time}) | |
| buf = buf[:0] | |
| } | |
| buf = format(items) | |
| w.Header().Set("Content-Type", "text/plain") | |
| w.Header().Set("Content-Length", strconv.Itoa(len(buf))) | |
| _, err = w.Write(buf) | |
| if err != nil { | |
| logger.Errorf("cannot write content: %v", err) | |
| w.WriteHeader(http.StatusInternalServerError) | |
| } | |
| } | |
| func (s *Server) serveBlitzKey(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Connection", "close") | |
| w.Header().Set("Content-Type", "text/plain") | |
| w.Header().Set("Content-Length", "2") | |
| w.Write([]byte("42")) | |
| } | |
| type formatItem struct { | |
| key string | |
| count int64 | |
| time time.Time | |
| } | |
| func (fi *formatItem) hasKey() bool { | |
| return fi.key != "" | |
| } | |
| func (fi *formatItem) hasTime() bool { | |
| return !fi.time.IsZero() | |
| } | |
| func (fi *formatItem) formatTime() string { | |
| return fi.time.Format("2006-01-02") | |
| } | |
| func formatCount(items []formatItem) []byte { | |
| return strconv.AppendInt(nil, items[0].count, 10) | |
| } | |
| func formatText(items []formatItem) []byte { | |
| var maxKeyLength int | |
| for i := range items { | |
| if l := len(items[i].key); maxKeyLength < l { | |
| maxKeyLength = l | |
| } | |
| } | |
| spaces := make([]byte, maxKeyLength+2) | |
| for i := range spaces { | |
| spaces[i] = ' ' | |
| } | |
| var buf []byte | |
| for i := range items { | |
| item := &items[i] | |
| if item.hasKey() { | |
| buf = append(buf, item.key...) | |
| buf = append(buf, spaces[len(item.key):]...) | |
| } | |
| if item.hasTime() { | |
| buf = append(buf, item.formatTime()...) | |
| buf = append(buf, ' ', ' ') | |
| } | |
| buf = strconv.AppendInt(buf, item.count, 10) | |
| buf = append(buf, '\n') | |
| } | |
| return buf | |
| } | |
| func formatCSV(items []formatItem) []byte { | |
| var buf []byte | |
| for i := range items { | |
| item := &items[i] | |
| if item.hasKey() { | |
| buf = append(buf, item.key...) | |
| buf = append(buf, ',') | |
| } | |
| if item.hasTime() { | |
| buf = append(buf, item.formatTime()...) | |
| buf = append(buf, ',') | |
| } | |
| buf = strconv.AppendInt(buf, item.count, 10) | |
| buf = append(buf, '\n') | |
| } | |
| return buf | |
| } | |
| func formatJSON(items []formatItem) []byte { | |
| if len(items) == 0 { | |
| return []byte("[]") | |
| } | |
| var buf []byte | |
| buf = append(buf, '[') | |
| for i := range items { | |
| item := &items[i] | |
| if i == 0 { | |
| buf = append(buf, '[') | |
| } else { | |
| buf = append(buf, ',', '[') | |
| } | |
| if item.hasKey() { | |
| buf = append(buf, '"') | |
| buf = append(buf, item.key...) | |
| buf = append(buf, '"', ',') | |
| } | |
| if item.hasTime() { | |
| buf = append(buf, '"') | |
| buf = append(buf, item.formatTime()...) | |
| buf = append(buf, '"', ',') | |
| } | |
| buf = strconv.AppendInt(buf, item.count, 10) | |
| buf = append(buf, ']') | |
| } | |
| buf = append(buf, ']') | |
| return buf | |
| } |