diff --git a/cmd/vetinari-server/main.go b/cmd/vetinari-server/main.go index f19e602a81..9800e8952c 100644 --- a/cmd/vetinari-server/main.go +++ b/cmd/vetinari-server/main.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" _ "expvar" "flag" "fmt" @@ -13,10 +14,12 @@ import ( "github.com/Sirupsen/logrus" _ "github.com/docker/distribution/registry/auth/token" "github.com/endophage/gotuf/signed" + _ "github.com/go-sql-driver/mysql" "golang.org/x/net/context" "github.com/docker/vetinari/config" "github.com/docker/vetinari/server" + "github.com/docker/vetinari/server/version" "github.com/docker/vetinari/signer" ) @@ -69,6 +72,12 @@ func main() { trust = signed.NewEd25519() } + db, err := sql.Open("mysql", "root:@/dockercondemo") + if err != nil { + logrus.Fatal("Error starting DB driver: ", err.Error()) + return // not strictly needed but let's be explicit + } + ctx = context.WithValue(ctx, "versionStore", version.NewVersionDB(db)) for { logrus.Info("[Vetinari] Starting Server") childCtx, cancel := context.WithCancel(ctx) diff --git a/server/handlers/default.go b/server/handlers/default.go index 8653df6c19..0fcea6f65c 100644 --- a/server/handlers/default.go +++ b/server/handlers/default.go @@ -2,11 +2,16 @@ package handlers import ( "encoding/json" + "fmt" + "io/ioutil" "net/http" + "github.com/endophage/gotuf/data" + "github.com/gorilla/mux" "golang.org/x/net/context" "github.com/docker/vetinari/errors" + "github.com/docker/vetinari/server/version" ) // MainHandler is the default handler for the server @@ -29,10 +34,70 @@ func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *e // AddHandler accepts urls in the form // func UpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { + defer r.Body.Close() + s := ctx.Value("versionStore") + store, ok := s.(*version.VersionDB) + if !ok { + return &errors.HTTPError{ + HTTPStatus: http.StatusInternalServerError, + Code: 9999, + Err: fmt.Errorf("Version store not configured"), + } + } + vars := mux.Vars(r) + qdn := vars["imageName"] + tufRole := vars["tufRole"] + input, err := ioutil.ReadAll(r.Body) + if err != nil { + return &errors.HTTPError{ + HTTPStatus: http.StatusBadRequest, + Code: 9999, + Err: err, + } + } + meta := &data.SignedTargets{} + err = json.Unmarshal(input, meta) + if err != nil { + return &errors.HTTPError{ + HTTPStatus: http.StatusBadRequest, + Code: 9999, + Err: err, + } + } + version := meta.Signed.Version + store.UpdateCurrent(qdn, tufRole, version, input) return nil } // GetHandler accepts urls in the form //.json func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { + s := ctx.Value("vesionStore") + store, ok := s.(*version.VersionDB) + if !ok { + return &errors.HTTPError{ + HTTPStatus: http.StatusInternalServerError, + Code: 9999, + Err: fmt.Errorf("Version store not configured"), + } + } + vars := mux.Vars(r) + qdn := vars["imageName"] + tufRole := vars["tufRole"] + data, err := store.GetCurrent(qdn, tufRole) + if err != nil { + return &errors.HTTPError{ + HTTPStatus: http.StatusInternalServerError, + Code: 9999, + Err: err, + } + } + if data == nil { + return &errors.HTTPError{ + HTTPStatus: http.StatusNotFound, + Code: 9999, + Err: err, + } + } + return nil } diff --git a/server/server.go b/server/server.go index 3ac7d8e68a..c0ca98a2df 100644 --- a/server/server.go +++ b/server/server.go @@ -97,16 +97,17 @@ func run(ctx context.Context, addr, tlsCertFile, tlsKeyFile string, trust signed } tlsLsnr := tls.NewListener(lsnr, tlsConfig) - ac, err := auth.GetAccessController("token", map[string]interface{}{}) - if err != nil { - return err - } + var ac auth.AccessController = nil + //ac, err := auth.GetAccessController("token", map[string]interface{}{}) + //if err != nil { + // return err + //} hand := utils.RootHandlerFactory(ac, context.Background(), trust) r := mux.NewRouter() // TODO (endophage): use correct regexes for image and tag names - r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufFile:(root.json|targets.json|timestamp.json|snapshot.json)}").Handler(hand(handlers.GetHandler, "pull")) - r.Methods("POST").Path("/v2/{imageName:.*}/_trust/tuf/{tufFile:(root.json|targets.json|timestamp.json|snapshot.json)}").Handler(hand(handlers.UpdateHandler, "push", "pull")) + r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:(root|targets|timestamp|snapshot)}.json").Handler(hand(handlers.GetHandler, "pull")) + r.Methods("POST").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:(root|targets|timestamp|snapshot)}.json").Handler(hand(handlers.UpdateHandler, "push", "pull")) svr := NewHTTPServer( http.Server{ diff --git a/server/version/database.go b/server/version/database.go new file mode 100644 index 0000000000..a888b190ae --- /dev/null +++ b/server/version/database.go @@ -0,0 +1,75 @@ +// version implementes a versioned store for TUF metadata +package version + +import ( + "database/sql" + "fmt" +) + +// VersionDB implements a versioned store using a relational database. +// The database table must look like: +// CREATE TABLE `tuf_files` ( +// `id` INT AUTO_INCREMENT, +// `qdn` VARCHAR(255) NOT NULL +// `role` VARCHAR(255) NOT NULL +// `version` INT +// `data` LONGBLOB +// PRIMARY KEY (`id`) +// UNIQUE INDEX (`qdn`, `role`, `version`) +// ) DEFAULT CHARSET=utf8; +type VersionDB struct { + sql.DB +} + +func NewVersionDB(db *sql.DB) *VersionDB { + return &VersionDB{ + DB: *db, + } +} + +// Update multiple TUF records in a single transaction. +// Always insert a new row. The unique constraint will ensure there is only ever +func (vdb VersionDB) UpdateCurrent(qdn, role string, version int, data []byte) error { + checkStmt := "SELECT 1 FROM `tuf_files` WHERE `qdn`=? AND `role`=? AND `version`=?;" + insertStmt := "INSERT INTO `tuf_files` (`qdn`, `role`, `version`, `data`) VALUES (?,?,?,?) ;" + + // ensure immediately previous version exists + row := vdb.QueryRow(checkStmt, qdn, role, version-1) + var exists bool + err := row.Scan(&exists) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("Attempting to increment version by more than 1 for QDN: %s, role: %s, version: %d", qdn, role, version) + } + + // attempt to insert. Due to race conditions with the check this could fail. + // That's OK, we're doing first write wins. The client will be messaged it + // needs to rebase. + _, err = vdb.Exec(insertStmt, qdn, role, version, data) + if err != nil { + return err + } + return nil +} + +// Get a specific TUF record +func (vdb VersionDB) GetCurrent(qdn, tufRole string) (data []byte, err error) { + stmt := "SELECT `data` FROM `tuf_files` WHERE `qdn`=? AND `role`=? ORDER BY `version` DESC LIMIT 1;" + rows, err := vdb.Query(stmt, qdn, tufRole) // this should be a QueryRow() + if err != nil { + return nil, err + } + defer rows.Close() + // unique constraint on (qdn, role) will ensure only one row is returned (or none if no match is found) + if !rows.Next() { + return nil, nil + } + + err = rows.Scan(&data) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/utils/http.go b/utils/http.go index 69e2f59ff9..046cfb745c 100644 --- a/utils/http.go +++ b/utils/http.go @@ -47,13 +47,14 @@ func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := context.WithValue(root.context, "repo", vars["imageName"]) ctx = context.WithValue(ctx, "trust", root.trust) + ctx = context.WithValue(ctx, "http.request", r) - access := buildAccessRecords(vars["imageName"], root.actions...) - var err error - if ctx, err = root.auth.Authorized(ctx, access...); err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } + // access := buildAccessRecords(vars["imageName"], root.actions...) + // var err error + // if ctx, err = root.auth.Authorized(ctx, access...); err != nil { + // http.Error(w, err.Error(), http.StatusUnauthorized) + // return + // } if err := root.handler(ctx, w, r); err != nil { logrus.Error("[Vetinari] ", err.Error()) http.Error(w, err.Error(), err.HTTPStatus)