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  }