From ac7fe67b7e7f23d5ea5e245c12f50da3ef9b8a20 Mon Sep 17 00:00:00 2001 From: David Barth Date: Thu, 13 Oct 2016 23:11:21 +0200 Subject: [PATCH 01/22] add support for passing over macaroons into the Authorization header; also force application/json on all passthru responses --- cmd/snapweb/handlers.go | 31 +++++++++++++++++++++++++++++++ cmd/snapweb/handlers_test.go | 16 ++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/cmd/snapweb/handlers.go b/cmd/snapweb/handlers.go index cd286804..0958de39 100644 --- a/cmd/snapweb/handlers.go +++ b/cmd/snapweb/handlers.go @@ -18,6 +18,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" @@ -150,6 +151,7 @@ func initURLHandlers(log *log.Logger) { http.Handle("/api/v2/packages/", snappyHandler.MakeMuxer("/api/v2/packages")) http.HandleFunc("/api/v2/create-user", passThru) + http.HandleFunc("/api/v2/login", passThru) http.HandleFunc("/api/v2/time-info", handleTimeInfo) http.HandleFunc("/api/v2/device-info", handleDeviceInfo) @@ -165,12 +167,33 @@ func initURLHandlers(log *log.Logger) { http.HandleFunc("/", makeMainPageHandler()) } +// Name of cookies transporting the macaroon and discharge to authenticate snapd requests +const ( + SnapwebMacaroonCookieName = "SnapwebMacaroon" + SnapwebDischargeCookieName = "SnapwebDischarge" +) + +// Writes the 'Authorization' header +// with macaroon and discharges extracted from mere cookies +func setAuthorizationHeader(req *http.Request, outreq *http.Request) { + mc, _ := req.Cookie(SnapwebMacaroonCookieName) + dc, _ := req.Cookie(SnapwebDischargeCookieName) + if mc != nil && dc != nil { + var buf bytes.Buffer + fmt.Fprintf(&buf, `Macaroon root="%s"`, mc.Value) + fmt.Fprintf(&buf, `, discharge="%s"`, dc.Value) + outreq.Header.Set("Authorization", buf.String()) + } +} + func makePassthroughHandler(socketPath string, prefix string) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c := &http.Client{ Transport: &http.Transport{Dial: unixDialer(socketPath)}, } + log.Println(r.Method, r.URL.Path) + // need to remove the RequestURI field // and remove the /api prefix from snapweb URLs r.URL.Scheme = "http" @@ -183,14 +206,22 @@ func makePassthroughHandler(socketPath string, prefix string) http.HandlerFunc { return } + setAuthorizationHeader(r, outreq) + resp, err := c.Do(outreq) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Note: the client.Do method above only returns JSON responses + // even if it doesn't say so + hdr := w.Header() + hdr.Set("Content-Type", "application/json") w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + }) } diff --git a/cmd/snapweb/handlers_test.go b/cmd/snapweb/handlers_test.go index f5998c5d..649374e4 100644 --- a/cmd/snapweb/handlers_test.go +++ b/cmd/snapweb/handlers_test.go @@ -222,6 +222,7 @@ func (s *HandlersSuite) TestPassthroughHandler(c *C) { body := rec.Body.String() c.Assert(rec.Code, Equals, http.StatusOK) c.Check(strings.Contains(body, "42"), Equals, true) + // TODO: check that we receive Content-Type: json/application } func (s *HandlersSuite) TestModelInfoHandler(c *C) { @@ -250,3 +251,18 @@ func (s *HandlersSuite) TestModelInfoHandler(c *C) { c.Assert(deviceInfos["model"], Equals, "Model") c.Assert(deviceInfos["serial"], Equals, "Serial Number") } + +func (s *HandlersSuite) TestSetAuthorization(c *C) { + r, err := http.NewRequest("GET", "/api/dummy", nil) + c.Assert(err, IsNil) + + r.AddCookie(&http.Cookie{Name: SnapwebMacaroonCookieName, Value: "expected"}) + r.AddCookie(&http.Cookie{Name: SnapwebDischargeCookieName, Value: "expected"}) + + outreq, err := http.NewRequest(r.Method, r.URL.String(), r.Body) + c.Assert(err, IsNil) + + setAuthorizationHeader(r, outreq) + c.Check(outreq.Header["Authorization"][0], Equals, + "Macaroon root=\"expected\", discharge=\"expected\"") +} From b91ca31438dc5afedc35f632d764b9714562f39c Mon Sep 17 00:00:00 2001 From: David Barth Date: Thu, 13 Oct 2016 23:13:25 +0200 Subject: [PATCH 02/22] new simple login page using snapd's /v2/login end-point and returning a store macaroon/discharge --- www/src/js/controllers/simple-login.js | 18 +++++ www/src/js/models/simple-login.js | 32 +++++++++ www/src/js/routers/router.js | 8 +++ www/src/js/templates/simple-login.hbs | 60 ++++++++++++++++ www/src/js/views/simple-login.js | 95 ++++++++++++++++++++++++++ www/tests/simpleLoginSpec.js | 77 +++++++++++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 www/src/js/controllers/simple-login.js create mode 100644 www/src/js/models/simple-login.js create mode 100644 www/src/js/templates/simple-login.hbs create mode 100644 www/src/js/views/simple-login.js create mode 100644 www/tests/simpleLoginSpec.js diff --git a/www/src/js/controllers/simple-login.js b/www/src/js/controllers/simple-login.js new file mode 100644 index 00000000..c5baa68d --- /dev/null +++ b/www/src/js/controllers/simple-login.js @@ -0,0 +1,18 @@ +var $ = require('jquery'); +var Backbone = require('backbone'); +Backbone.$ = $; +var Marionette = require('backbone.marionette'); +var Radio = require('backbone.radio'); +var SimpleLoginView = require('../views/simple-login.js'); +var SimpleLoginModel = require('../models/simple-login.js'); + +module.exports = { + index: function() { + var chan = Radio.channel('root'); + var model = new SimpleLoginModel(); + var view = new SimpleLoginView({ + model: model, + }); + chan.command('set:content', view); + } +}; diff --git a/www/src/js/models/simple-login.js b/www/src/js/models/simple-login.js new file mode 100644 index 00000000..61e92e03 --- /dev/null +++ b/www/src/js/models/simple-login.js @@ -0,0 +1,32 @@ +// create-user API + +var Backbone = require('backbone'); +var Marionette = require('backbone.marionette'); + +module.exports = Backbone.Model.extend({ + url: '/api/v2/login', + + // forces POST requests on every model update + isNew: function() { + return true; + }, + + validate: function(attrs) { + var emailPattern = /^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/; + if (typeof attrs.email == 'undefined') { + return 'Empty email'; + } + if (!attrs.email.match(emailPattern)) { + return 'Invalid email'; + } + if (typeof attrs.password == 'undefined') { + return 'Empty password'; + } + }, + + setMacaroonCookiesFromResponse: function(result) { + document.cookie = "SnapwebMacaroon=" + result.macaroon + + "; path=/; SnapwebDischarge=" + result.discharges[0] + "; path=/"; + }, + +}); diff --git a/www/src/js/routers/router.js b/www/src/js/routers/router.js index 7afb0ddc..8adcc054 100644 --- a/www/src/js/routers/router.js +++ b/www/src/js/routers/router.js @@ -9,6 +9,7 @@ var searchController = require('../controllers/search.js'); var storeController = require('../controllers/store.js'); var settingsController = require('../controllers/settings.js'); var snapController = require('../controllers/snaps.js'); +var loginController = require('../controllers/simple-login.js'); module.exports = { @@ -26,6 +27,13 @@ module.exports = { } }), + login: new Marionette.AppRouter({ + controller: loginController, + appRoutes: { + 'login': 'index' + } + }), + store: new Marionette.AppRouter({ controller: storeController, appRoutes: { diff --git a/www/src/js/templates/simple-login.hbs b/www/src/js/templates/simple-login.hbs new file mode 100644 index 00000000..c5bafa39 --- /dev/null +++ b/www/src/js/templates/simple-login.hbs @@ -0,0 +1,60 @@ +