vain/server.go
Stephen McQuay (smcquay) 295a7c3840
Only return the meta for requested package
the go tool currenty has a pathology where it mistakingly claims it
can't clone a repo because it checks prefix by bytes, not by splitting
the path on slash.

Once https://github.com/golang/go/issues/15947 comes out (go1.8) then go
tools will be able to handle being provided with valid but overlapping
packages.

For now though we'll just return meta for the requested package.

Change-Id: Ie5026e7d5c1377ff7d2c2140b21f9b745af69764
2016-06-05 22:41:56 -07:00

254 lines
6.4 KiB
Go

package vain
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/mail"
"strings"
"time"
"github.com/elazarl/go-bindata-assetfs"
verrors "mcquay.me/vain/errors"
"mcquay.me/vain/static"
)
const apiPrefix = "/api/v0/"
var prefix map[string]string
func init() {
prefix = map[string]string{
"pkgs": apiPrefix + "db/",
"register": apiPrefix + "register/",
"confirm": apiPrefix + "confirm/",
"forgot": apiPrefix + "forgot/",
"static": "/_static/",
}
}
// NewServer populates a server, adds the routes, and returns it for use.
func NewServer(sm *http.ServeMux, store *DB, static string, emailTimeout time.Duration) *Server {
s := &Server{
db: store,
static: static,
emailTimeout: emailTimeout,
}
addRoutes(sm, s)
return s
}
// Server serves up the http.
type Server struct {
db *DB
static string
emailTimeout time.Duration
}
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
req.ParseForm()
if _, ok := req.Form["go-get"]; !ok {
http.Redirect(w, req, prefix["static"], http.StatusTemporaryRedirect)
return
}
if req.URL.Path == "/" {
fmt.Fprintf(w, "<!DOCTYPE html>\n<html><head>\n")
for _, p := range s.db.Pkgs() {
fmt.Fprintf(w, "%s\n", p)
}
fmt.Fprintf(w, "</head>\n<body><p>go tool metadata in head</p></body>\n</html>\n")
} else {
p, err := s.db.Package(req.Host + req.URL.Path)
if err := verrors.ToHTTP(err); err != nil {
http.Error(w, err.Message, err.Code)
return
}
fmt.Fprintf(w, "<!DOCTYPE html>\n<html><head>\n%s\n</head>\n<body><p>go tool metadata in head</p></body>\n</html>\n", p)
}
return
}
const prefix = "Bearer "
var tok string
auth := req.Header.Get("Authorization")
if strings.HasPrefix(auth, prefix) {
tok = strings.TrimPrefix(auth, prefix)
}
if tok == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
ns, err := parseNamespace(req.URL.Path)
if err != nil {
http.Error(w, fmt.Sprintf("could not parse namespace:%v", err), http.StatusBadRequest)
return
}
if err := verrors.ToHTTP(s.db.NSForToken(ns, tok)); err != nil {
http.Error(w, err.Message, err.Code)
return
}
switch req.Method {
case "POST":
if req.URL.Path == "/" {
http.Error(w, fmt.Sprintf("invalid path %q", req.URL.Path), http.StatusBadRequest)
return
}
p := Package{}
if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
http.Error(w, fmt.Sprintf("unable to parse json from body: %v", err), http.StatusBadRequest)
return
}
if p.Repo == "" {
http.Error(w, fmt.Sprintf("invalid repository %q", p.Repo), http.StatusBadRequest)
return
}
if p.Vcs == "" {
p.Vcs = "git"
}
if !valid(p.Vcs) {
http.Error(w, fmt.Sprintf("invalid vcs %q", p.Vcs), http.StatusBadRequest)
return
}
p.Path = fmt.Sprintf("%s/%s", req.Host, strings.Trim(req.URL.Path, "/"))
p.Ns = ns
if !Valid(p.Path, s.db.Pkgs()) {
http.Error(w, fmt.Sprintf("invalid path; prefix already taken %q", req.URL.Path), http.StatusConflict)
return
}
if err := s.db.AddPackage(p); err != nil {
http.Error(w, fmt.Sprintf("unable to add package: %v", err), http.StatusInternalServerError)
return
}
case "DELETE":
p := fmt.Sprintf("%s/%s", req.Host, strings.Trim(req.URL.Path, "/"))
if !s.db.PackageExists(p) {
http.Error(w, fmt.Sprintf("package %q not found", p), http.StatusNotFound)
return
}
if err := s.db.RemovePackage(p); err != nil {
http.Error(w, fmt.Sprintf("unable to delete package: %v", err), http.StatusInternalServerError)
return
}
default:
http.Error(w, fmt.Sprintf("unsupported method %q; accepted: POST, GET, DELETE", req.Method), http.StatusMethodNotAllowed)
}
}
func (s *Server) register(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
email, ok := req.Form["email"]
if !ok || len(email) != 1 {
http.Error(w, "must provide one email parameter", http.StatusBadRequest)
return
}
addr := email[0]
if _, err := mail.ParseAddress(addr); err != nil {
http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest)
return
}
tok, err := s.db.Register(addr)
if err := verrors.ToHTTP(err); err != nil {
http.Error(w, err.Message, err.Code)
return
}
proto := "https"
if req.TLS == nil {
proto = "http"
}
log.Printf("%s://%s/api/v0/confirm/%+v", proto, req.Host, tok)
resp := struct {
Msg string `json:"msg"`
}{
Msg: "please check your email\n",
}
w.Header().Set("Content-type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *Server) confirm(w http.ResponseWriter, req *http.Request) {
tok := req.URL.Path[len(prefix["confirm"]):]
tok = strings.TrimRight(tok, "/")
if tok == "" {
http.Error(w, "must provide one email parameter", http.StatusBadRequest)
return
}
tok, err := s.db.Confirm(tok)
if err := verrors.ToHTTP(err); err != nil {
http.Error(w, err.Message, err.Code)
return
}
fmt.Fprintf(w, "new token: %s\n", tok)
}
func (s *Server) forgot(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
email, ok := req.Form["email"]
if !ok || len(email) != 1 {
http.Error(w, "must provide one email parameter", http.StatusBadRequest)
return
}
addr := email[0]
if _, err := mail.ParseAddress(addr); err != nil {
http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest)
return
}
tok, err := s.db.forgot(addr, s.emailTimeout)
if err := verrors.ToHTTP(err); err != nil {
http.Error(w, err.Message, err.Code)
return
}
log.Printf("http://%s/api/v0/confirm/%+v", req.Host, tok)
resp := struct {
Msg string `json:"msg"`
}{
Msg: "please check your email\n",
}
w.Header().Set("Content-type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *Server) pkgs(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-type", "application/json")
json.NewEncoder(w).Encode(s.db.Pkgs())
}
func addRoutes(sm *http.ServeMux, s *Server) {
sm.Handle("/", s)
if s.static == "" {
sm.Handle(
prefix["static"],
http.FileServer(
&assetfs.AssetFS{
Asset: static.Asset,
AssetDir: static.AssetDir,
AssetInfo: static.AssetInfo,
},
),
)
} else {
sm.Handle(
prefix["static"],
http.StripPrefix(
prefix["static"],
http.FileServer(http.Dir(s.static)),
),
)
}
sm.HandleFunc(prefix["pkgs"], s.pkgs)
sm.HandleFunc(prefix["register"], s.register)
sm.HandleFunc(prefix["confirm"], s.confirm)
sm.HandleFunc(prefix["forgot"], s.forgot)
}