From 627a217e7e74a6800475a9b3898855b26edd4a16 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 20 Sep 2012 02:50:07 +0200 Subject: pseudo persona stuff --- auth.go | 7 +- auth_persona.go | 69 +++ bommom.go | 1 + serve.go | 67 ++- templates/account.html | 29 + templates/base.html | 20 +- templates/bom_upload.html | 4 +- templates/bom_view.html | 4 +- templates/home.html | 4 +- templates/static/include.js | 1255 +++++++++++++++++++++++++++++++++++++++++++ templates/user.html | 4 +- 11 files changed, 1449 insertions(+), 15 deletions(-) create mode 100644 auth_persona.go create mode 100644 templates/account.html create mode 100644 templates/static/include.js diff --git a/auth.go b/auth.go index f0ef530..80b3088 100644 --- a/auth.go +++ b/auth.go @@ -12,6 +12,7 @@ type AuthService interface { NewAccount(name, pw, email string) error ChangePassword(name, oldPw, newPw string) error GetEmail(name string) (string, error) + GetUserName(email string) (string, error) } // DummyAuth is a "wide-open" implementation of AuthService for development and @@ -32,5 +33,9 @@ func (da DummyAuth) ChangePassword(name, oldPw, newPw string) error { } func (da DummyAuth) GetEmail(name string) (string, error) { - return "example@bommom.com", nil + return "example@localhost", nil +} + +func (da DummyAuth) GetUserName(name string) (string, error) { + return "common", nil } diff --git a/auth_persona.go b/auth_persona.go new file mode 100644 index 0000000..7895c2d --- /dev/null +++ b/auth_persona.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" +) + +type PersonaResponse struct { + Status, Email, Reason string +} + +func (b PersonaResponse) Okay() bool { + return b.Status == "okay" +} + +func VerifyPersonaAssertion(assertion, audience string) PersonaResponse { + resp, _ := http.PostForm( + "https://browserid.org/verify", + url.Values{ + "assertion": {assertion}, + "audience": {audience}, + }) + response := personaResponseFromJson(resp.Body) + resp.Body.Close() + + return response +} + +func personaResponseFromJson(r io.Reader) (resp PersonaResponse) { + body, err := ioutil.ReadAll(r) + + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(body, &resp) + + if err != nil { + log.Fatal(err) + } + + return resp +} + +type PersonaAuth bool + +func (pa PersonaAuth) CheckLogin(name, pw string) error { + return nil +} + +func (pa PersonaAuth) NewAccount(name, pw, email string) error { + return nil +} + +func (pa PersonaAuth) ChangePassword(name, oldPw, newPw string) error { + return nil +} + +func (pa PersonaAuth) GetEmail(name string) (string, error) { + return "example@localhost", nil +} + +func (pa PersonaAuth) GetUserName(name string) (string, error) { + return "common", nil +} diff --git a/bommom.go b/bommom.go index 2d14bf8..55071b8 100644 --- a/bommom.go +++ b/bommom.go @@ -27,6 +27,7 @@ var ( inFormat = flag.String("informat", "", "command output format (for 'load' etc)") listenPort = flag.Uint("port", 7070, "port to listen on (HTTP serve)") listenHost = flag.String("host", "", "hostname to listen on (HTTP serve)") + sessionSecret = flag.String("sessionSecret", "12345", "cookie session secret") ) func main() { diff --git a/serve.go b/serve.go index dedd0a1..d1d035d 100644 --- a/serve.go +++ b/serve.go @@ -8,12 +8,15 @@ import ( "regexp" "path/filepath" "time" + "code.google.com/p/gorilla/sessions" ) var ( - tmplHome, tmplView, tmplUser, tmplBomView, tmplBomUpload *template.Template + tmplHome, tmplView, tmplAccount, tmplUser, tmplBomView, tmplBomUpload *template.Template ) +var store = sessions.NewCookieStore([]byte(*sessionSecret)) + func baseHandler(w http.ResponseWriter, r *http.Request) { var err error log.Printf("serving %s\n", r.URL.Path) @@ -25,6 +28,12 @@ func baseHandler(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/": err = homeController(w, r) + case r.URL.Path == "/account/login/": + err = loginController(w, r) + case r.URL.Path == "/account/logout/": + err = logoutController(w, r) + //case r.URL.Path == "/account/newuser/": + // err = newUserController(w, r) case bomUploadUrlPattern.MatchString(r.URL.Path): match := bomUploadUrlPattern.FindStringSubmatch(r.URL.Path) err = bomUploadController(w, r, match[1], match[2]) @@ -48,7 +57,10 @@ func baseHandler(w http.ResponseWriter, r *http.Request) { } func homeController(w http.ResponseWriter, r *http.Request) (err error) { + session, _ := store.Get(r, "bommom") context := make(map[string]interface{}) + context["Session"] = session.Values + log.Printf("%s\n", session.Values["UserName"]) context["BomList"], err = bomstore.ListBoms("") if err != nil { return @@ -57,13 +69,54 @@ func homeController(w http.ResponseWriter, r *http.Request) (err error) { return } +func loginController(w http.ResponseWriter, r *http.Request) (err error) { + session, _ := store.Get(r, "bommom") + context := make(map[string]interface{}) + context["ActionLogin"] = true + context["Session"] = session.Values + if r.Method == "POST" { + if isShortName(r.FormValue("UserName")) != true { + context["Problem"] = "Ugh, need to use a SHORTNAME!" + err = tmplAccount.Execute(w, context) + return + } + audience := "http://localhost:7070" + vResponse := VerifyPersonaAssertion(r.FormValue("assertion"), audience) + if vResponse.Okay() { + session.Values["UserName"] = r.FormValue("UserName") + session.Values["Email"] = vResponse.Email + session.Save(r, w) + context["Session"] = session.Values + http.Redirect(w, r, "/", 302) + return + } else { + context["Problem"] = vResponse.Reason + err = tmplAccount.Execute(w, context) + return + } + } + err = tmplAccount.Execute(w, context) + return +} + +func logoutController(w http.ResponseWriter, r *http.Request) (err error) { + session, _ := store.Get(r, "bommom") + context := make(map[string]interface{}) + delete(session.Values, "UserName") + delete(session.Values, "Email") + session.Save(r, w) + context["Session"] = session.Values + context["ActionLogout"] = true + err = tmplAccount.Execute(w, context) + return +} + func userController(w http.ResponseWriter, r *http.Request, user, extra string) (err error) { + session, _ := store.Get(r, "bommom") if !isShortName(user) { http.Error(w, "invalid username: "+user, 400) return } - var email string - email, err = auth.GetEmail(user) if err != nil { // no such user http.NotFound(w, r) @@ -72,6 +125,7 @@ func userController(w http.ResponseWriter, r *http.Request, user, extra string) context := make(map[string]interface{}) context["BomList"], err = bomstore.ListBoms(ShortName(user)) context["UserName"] = user + context["Session"] = session.Values if err != nil { return } @@ -80,6 +134,7 @@ func userController(w http.ResponseWriter, r *http.Request, user, extra string) } func bomController(w http.ResponseWriter, r *http.Request, user, name string) (err error) { + session, _ := store.Get(r, "bommom") if !isShortName(user) { http.Error(w, "invalid username: "+user, 400) return @@ -90,6 +145,7 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e } context := make(map[string]interface{}) context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name)) + context["Session"] = session.Values if err != nil { http.Error(w, "404 couldn't open bom: " + user + "/" + name, 404) return nil @@ -99,6 +155,7 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name string) (e } func bomUploadController(w http.ResponseWriter, r *http.Request, user, name string) (err error) { + session, _ := store.Get(r, "bommom") if !isShortName(user) { http.Error(w, "invalid username: "+user, 400) @@ -109,6 +166,7 @@ func bomUploadController(w http.ResponseWriter, r *http.Request, user, name stri return } context := make(map[string]interface{}) + context["Session"] = session.Values context["user"] = ShortName(user) context["name"] = ShortName(name) context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name)) @@ -202,6 +260,9 @@ func serveCmd() { // load and parse templates baseTmplPath := *templatePath + "/base.html" tmplHome = template.Must(template.ParseFiles(*templatePath+"/home.html", baseTmplPath)) + tmplAccount = template.Must(template.ParseFiles(*templatePath+"/account.html", baseTmplPath)) + //tmplLogout = template.Must(template.ParseFiles(*templatePath+"/logout.html", baseTmplPath)) + //tmplNewUser = template.Must(template.ParseFiles(*templatePath+"/newuser.html", baseTmplPath)) tmplUser = template.Must(template.ParseFiles(*templatePath+"/user.html", baseTmplPath)) tmplBomView = template.Must(template.ParseFiles(*templatePath+"/bom_view.html", baseTmplPath)) tmplBomUpload = template.Must(template.ParseFiles(*templatePath+"/bom_upload.html", baseTmplPath)) diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..9b7e3b1 --- /dev/null +++ b/templates/account.html @@ -0,0 +1,29 @@ +{{ template "HEADER" . }} +{{ if .ActionLogin }} + +



+
+
+
+ Please Sign In + {{ if .Problem }} +
+ Problem! {{ .Problem }} +
+ {{ end }} +
+ + +
+ +
+
+
+ +{{ else }} +{{if .ActionLogout }} +

logged out, goodbye!

+{{ else }}{{if .ActionNewUser }} +

hello new human!

+{{end}}{{end}}{{end}} +{{ template "FOOTER" . }} diff --git a/templates/base.html b/templates/base.html index 8066063..4fe66ad 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,6 +2,19 @@ bommom + + @@ -12,10 +25,11 @@ {{ range .BreadCrumbs }} / {{.}} {{end}} diff --git a/templates/bom_upload.html b/templates/bom_upload.html index 30e2ff6..8cc15c8 100644 --- a/templates/bom_upload.html +++ b/templates/bom_upload.html @@ -1,4 +1,4 @@ -{{ template "HEADER" }} +{{ template "HEADER" . }} {{ if .BomMeta }}

{{ .BomMeta.Name }} is a bom.

@@ -21,4 +21,4 @@ -{{ template "FOOTER" }} +{{ template "FOOTER" . }} diff --git a/templates/bom_view.html b/templates/bom_view.html index 2e66bf1..7eca3e3 100644 --- a/templates/bom_view.html +++ b/templates/bom_view.html @@ -1,4 +1,4 @@ -{{ template "HEADER" }} +{{ template "HEADER" . }}

{{ .BomMeta.Name }} is a bom.


@@ -32,4 +32,4 @@ {{ end }} -{{ template "FOOTER" }} +{{ template "FOOTER" . }} diff --git a/templates/home.html b/templates/home.html index 8749c58..40e8f6c 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,4 +1,4 @@ -{{ template "HEADER" }} +{{ template "HEADER" . }}

this is bomom.

{{ range .BomList }} @@ -8,4 +8,4 @@ No Boms found! {{ end }}
-{{ template "FOOTER" }} +{{ template "FOOTER" . }} diff --git a/templates/static/include.js b/templates/static/include.js new file mode 100644 index 0000000..1df41c0 --- /dev/null +++ b/templates/static/include.js @@ -0,0 +1,1255 @@ +/** + * Uncompressed source can be found at https://login.persona.org/include.orig.js + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +(function() { + // this is the file that the RP includes to shim in the + // navigator.id.getVerifiedEmail() function + // "use strict"; + + // local embedded copy of jschannel: http://github.com/mozilla/jschannel + /** + * js_channel is a very lightweight abstraction on top of + * postMessage which defines message formats and semantics + * to support interactions more rich than just message passing + * js_channel supports: + * + query/response - traditional rpc + * + query/update/response - incremental async return of results + * to a query + * + notifications - fire and forget + * + error handling + * + * js_channel is based heavily on json-rpc, but is focused at the + * problem of inter-iframe RPC. + * + * Message types: + * There are 5 types of messages that can flow over this channel, + * and you may determine what type of message an object is by + * examining its parameters: + * 1. Requests + * + integer id + * + string method + * + (optional) any params + * 2. Callback Invocations (or just "Callbacks") + * + integer id + * + string callback + * + (optional) params + * 3. Error Responses (or just "Errors) + * + integer id + * + string error + * + (optional) string message + * 4. Responses + * + integer id + * + (optional) any result + * 5. Notifications + * + string method + * + (optional) any params + */ + var Channel = (function() { + "use strict"; + + // current transaction id, start out at a random *odd* number between 1 and a million + // There is one current transaction counter id per page, and it's shared between + // channel instances. That means of all messages posted from a single javascript + // evaluation context, we'll never have two with the same id. + var s_curTranId = Math.floor(Math.random()*1000001); + + // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window. + // futher if two bound channels have the same window and scope, they may not have *overlapping* origins + // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently + // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message + // handlers. Request and Notification messages are routed using this table. + // Finally, channels are inserted into this table when built, and removed when destroyed. + var s_boundChans = { }; + + // add a channel to s_boundChans, throwing if a dup exists + function s_addBoundChan(win, origin, scope, handler) { + function hasWin(arr) { + for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true; + return false; + } + + // does she exist? + var exists = false; + + + if (origin === '*') { + // we must check all other origins, sadly. + for (var k in s_boundChans) { + if (!s_boundChans.hasOwnProperty(k)) continue; + if (k === '*') continue; + if (typeof s_boundChans[k][scope] === 'object') { + exists = hasWin(s_boundChans[k][scope]); + if (exists) break; + } + } + } else { + // we must check only '*' + if ((s_boundChans['*'] && s_boundChans['*'][scope])) { + exists = hasWin(s_boundChans['*'][scope]); + } + if (!exists && s_boundChans[origin] && s_boundChans[origin][scope]) + { + exists = hasWin(s_boundChans[origin][scope]); + } + } + if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'"; + + if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { }; + if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ]; + s_boundChans[origin][scope].push({win: win, handler: handler}); + } + + function s_removeBoundChan(win, origin, scope) { + var arr = s_boundChans[origin][scope]; + for (var i = 0; i < arr.length; i++) { + if (arr[i].win === win) { + arr.splice(i,1); + } + } + if (s_boundChans[origin][scope].length === 0) { + delete s_boundChans[origin][scope]; + } + } + + function s_isArray(obj) { + if (Array.isArray) return Array.isArray(obj); + else { + return (obj.constructor.toString().indexOf("Array") != -1); + } + } + + // No two outstanding outbound messages may have the same id, period. Given that, a single table + // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and + // Response messages. Entries are added to this table when requests are sent, and removed when + // responses are received. + var s_transIds = { }; + + // class singleton onMessage handler + // this function is registered once and all incoming messages route through here. This + // arrangement allows certain efficiencies, message data is only parsed once and dispatch + // is more efficient, especially for large numbers of simultaneous channels. + var s_onMessage = function(e) { + try { + var m = JSON.parse(e.data); + if (typeof m !== 'object' || m === null) throw "malformed"; + } catch(e) { + // just ignore any posted messages that do not consist of valid JSON + return; + } + + var w = e.source; + var o = e.origin; + var s, i, meth; + + if (typeof m.method === 'string') { + var ar = m.method.split('::'); + if (ar.length == 2) { + s = ar[0]; + meth = ar[1]; + } else { + meth = m.method; + } + } + + if (typeof m.id !== 'undefined') i = m.id; + + // w is message source window + // o is message origin + // m is parsed message + // s is message scope + // i is message id (or undefined) + // meth is unscoped method name + // ^^ based on these factors we can route the message + + // if it has a method it's either a notification or a request, + // route using s_boundChans + if (typeof meth === 'string') { + var delivered = false; + if (s_boundChans[o] && s_boundChans[o][s]) { + for (var j = 0; j < s_boundChans[o][s].length; j++) { + if (s_boundChans[o][s][j].win === w) { + s_boundChans[o][s][j].handler(o, meth, m); + delivered = true; + break; + } + } + } + + if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) { + for (var j = 0; j < s_boundChans['*'][s].length; j++) { + if (s_boundChans['*'][s][j].win === w) { + s_boundChans['*'][s][j].handler(o, meth, m); + break; + } + } + } + } + // otherwise it must have an id (or be poorly formed + else if (typeof i != 'undefined') { + if (s_transIds[i]) s_transIds[i](o, meth, m); + } + }; + + // Setup postMessage event listeners + if (window.addEventListener) window.addEventListener('message', s_onMessage, false); + else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage); + + /* a messaging channel is constructed from a window and an origin. + * the channel will assert that all messages received over the + * channel match the origin + * + * Arguments to Channel.build(cfg): + * + * cfg.window - the remote window with which we'll communicate + * cfg.origin - the expected origin of the remote window, may be '*' + * which matches any origin + * cfg.scope - the 'scope' of messages. a scope string that is + * prepended to message names. local and remote endpoints + * of a single channel must agree upon scope. Scope may + * not contain double colons ('::'). + * cfg.debugOutput - A boolean value. If true and window.console.log is + * a function, then debug strings will be emitted to that + * function. + * cfg.debugOutput - A boolean value. If true and window.console.log is + * a function, then debug strings will be emitted to that + * function. + * cfg.postMessageObserver - A function that will be passed two arguments, + * an origin and a message. It will be passed these immediately + * before messages are posted. + * cfg.gotMessageObserver - A function that will be passed two arguments, + * an origin and a message. It will be passed these arguments + * immediately after they pass scope and origin checks, but before + * they are processed. + * cfg.onReady - A function that will be invoked when a channel becomes "ready", + * this occurs once both sides of the channel have been + * instantiated and an application level handshake is exchanged. + * the onReady function will be passed a single argument which is + * the channel object that was returned from build(). + */ + return { + build: function(cfg) { + var debug = function(m) { + if (cfg.debugOutput && window.console && window.console.log) { + // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic + try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { } + console.log("["+chanId+"] " + m); + } + }; + + /* browser capabilities check */ + if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage"); + if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) { + throw("jschannel cannot run this browser, no JSON parsing/serialization"); + } + + /* basic argument validation */ + if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument"); + + if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument"); + + /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same + * window... Not sure if we care to support that */ + if (window === cfg.window) throw("target window is same as present window -- not allowed"); + + // let's require that the client specify an origin. if we just assume '*' we'll be + // propagating unsafe practices. that would be lame. + var validOrigin = false; + if (typeof cfg.origin === 'string') { + var oMatch; + if (cfg.origin === "*") validOrigin = true; + // allow valid domains under http and https. Also, trim paths off otherwise valid origins. + else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) { + cfg.origin = oMatch[0].toLowerCase(); + validOrigin = true; + } + } + + if (!validOrigin) throw ("Channel.build() called with an invalid origin"); + + if (typeof cfg.scope !== 'undefined') { + if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string'; + if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"; + } + + /* private variables */ + // generate a random and psuedo unique id for this channel + var chanId = (function () { + var text = ""; + var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length)); + return text; + })(); + + // registrations: mapping method names to call objects + var regTbl = { }; + // current oustanding sent requests + var outTbl = { }; + // current oustanding received requests + var inTbl = { }; + // are we ready yet? when false we will block outbound messages. + var ready = false; + var pendingQueue = [ ]; + + var createTransaction = function(id,origin,callbacks) { + var shouldDelayReturn = false; + var completed = false; + + return { + origin: origin, + invoke: function(cbName, v) { + // verify in table + if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id; + // verify that the callback name is valid + var valid = false; + for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; } + if (!valid) throw "request supports no such callback '" + cbName + "'"; + + // send callback invocation + postMessage({ id: id, callback: cbName, params: v}); + }, + error: function(error, message) { + completed = true; + // verify in table + if (!inTbl[id]) throw "error called for nonexistent message: " + id; + + // remove transaction from table + delete inTbl[id]; + + // send error + postMessage({ id: id, error: error, message: message }); + }, + complete: function(v) { + completed = true; + // verify in table + if (!inTbl[id]) throw "complete called for nonexistent message: " + id; + // remove transaction from table + delete inTbl[id]; + // send complete + postMessage({ id: id, result: v }); + }, + delayReturn: function(delay) { + if (typeof delay === 'boolean') { + shouldDelayReturn = (delay === true); + } + return shouldDelayReturn; + }, + completed: function() { + return completed; + } + }; + }; + + var setTransactionTimeout = function(transId, timeout, method) { + return window.setTimeout(function() { + if (outTbl[transId]) { + // XXX: what if client code raises an exception here? + var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'"; + (1,outTbl[transId].error)("timeout_error", msg); + delete outTbl[transId]; + delete s_transIds[transId]; + } + }, timeout); + }; + + var onMessage = function(origin, method, m) { + // if an observer was specified at allocation time, invoke it + if (typeof cfg.gotMessageObserver === 'function') { + // pass observer a clone of the object so that our + // manipulations are not visible (i.e. method unscoping). + // This is not particularly efficient, but then we expect + // that message observers are primarily for debugging anyway. + try { + cfg.gotMessageObserver(origin, m); + } catch (e) { + debug("gotMessageObserver() raised an exception: " + e.toString()); + } + } + + // now, what type of message is this? + if (m.id && method) { + // a request! do we have a registered handler for this request? + if (regTbl[method]) { + var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]); + inTbl[m.id] = { }; + try { + // callback handling. we'll magically create functions inside the parameter list for each + // callback + if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) { + for (var i = 0; i < m.callbacks.length; i++) { + var path = m.callbacks[i]; + var obj = m.params; + var pathItems = path.split('/'); + for (var j = 0; j < pathItems.length - 1; j++) { + var cp = pathItems[j]; + if (typeof obj[cp] !== 'object') obj[cp] = { }; + obj = obj[cp]; + } + obj[pathItems[pathItems.length - 1]] = (function() { + var cbName = path; + return function(params) { + return trans.invoke(cbName, params); + }; + })(); + } + } + var resp = regTbl[method](trans, m.params); + if (!trans.delayReturn() && !trans.completed()) trans.complete(resp); + } catch(e) { + // automagic handling of exceptions: + var error = "runtime_error"; + var message = null; + // * if it's a string then it gets an error code of 'runtime_error' and string is the message + if (typeof e === 'string') { + message = e; + } else if (typeof e === 'object') { + // either an array or an object + // * if it's an array of length two, then array[0] is the code, array[1] is the error message + if (e && s_isArray(e) && e.length == 2) { + error = e[0]; + message = e[1]; + } + // * if it's an object then we'll look form error and message parameters + else if (typeof e.error === 'string') { + error = e.error; + if (!e.message) message = ""; + else if (typeof e.message === 'string') message = e.message; + else e = e.message; // let the stringify/toString message give us a reasonable verbose error string + } + } + + // message is *still* null, let's try harder + if (message === null) { + try { + message = JSON.stringify(e); + /* On MSIE8, this can result in 'out of memory', which + * leaves message undefined. */ + if (typeof(message) == 'undefined') + message = e.toString(); + } catch (e2) { + message = e.toString(); + } + } + + trans.error(error,message); + } + } + } else if (m.id && m.callback) { + if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback]) + { + debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")"); + } else { + // XXX: what if client code raises an exception here? + outTbl[m.id].callbacks[m.callback](m.params); + } + } else if (m.id) { + if (!outTbl[m.id]) { + debug("ignoring invalid response: " + m.id); + } else { + // XXX: what if client code raises an exception here? + if (m.error) { + (1,outTbl[m.id].error)(m.error, m.message); + } else { + if (m.result !== undefined) (1,outTbl[m.id].success)(m.result); + else (1,outTbl[m.id].success)(); + } + delete outTbl[m.id]; + delete s_transIds[m.id]; + } + } else if (method) { + // tis a notification. + if (regTbl[method]) { + // yep, there's a handler for that. + // transaction is null for notifications. + regTbl[method](null, m.params); + // if the client throws, we'll just let it bubble out + // what can we do? Also, here we'll ignore return values + } + } + }; + + // now register our bound channel for msg routing + s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage); + + // scope method names based on cfg.scope specified when the Channel was instantiated + var scopeMethod = function(m) { + if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::"); + return m; + }; + + // a small wrapper around postmessage whose primary function is to handle the + // case that clients start sending messages before the other end is "ready" + var postMessage = function(msg, force) { + if (!msg) throw "postMessage called with null message"; + + // delay posting if we're not ready yet. + var verb = (ready ? "post " : "queue "); + debug(verb + " message: " + JSON.stringify(msg)); + if (!force && !ready) { + pendingQueue.push(msg); + } else { + if (typeof cfg.postMessageObserver === 'function') { + try { + cfg.postMessageObserver(cfg.origin, msg); + } catch (e) { + debug("postMessageObserver() raised an exception: " + e.toString()); + } + } + + cfg.window.postMessage(JSON.stringify(msg), cfg.origin); + } + }; + + var onReady = function(trans, type) { + debug('ready msg received'); + if (ready) throw "received ready message while in ready state. help!"; + + if (type === 'ping') { + chanId += '-R'; + } else { + chanId += '-L'; + } + + obj.unbind('__ready'); // now this handler isn't needed any more. + ready = true; + debug('ready msg accepted.'); + + if (type === 'ping') { + obj.notify({ method: '__ready', params: 'pong' }); + } + + // flush queue + while (pendingQueue.length) { + postMessage(pendingQueue.pop()); + } + + // invoke onReady observer if provided + if (typeof cfg.onReady === 'function') cfg.onReady(obj); + }; + + var obj = { + // tries to unbind a bound message handler. returns false if not possible + unbind: function (method) { + if (regTbl[method]) { + if (!(delete regTbl[method])) throw ("can't delete method: " + method); + return true; + } + return false; + }, + bind: function (method, cb) { + if (!method || typeof method !== 'string') throw "'method' argument to bind must be string"; + if (!cb || typeof cb !== 'function') throw "callback missing from bind params"; + + if (regTbl[method]) throw "method '"+method+"' is already bound!"; + regTbl[method] = cb; + return this; + }, + call: function(m) { + if (!m) throw 'missing arguments to call function'; + if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string"; + if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call"; + + // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument + // object and pick out all of the functions that were passed as arguments. + var callbacks = { }; + var callbackNames = [ ]; + + var pruneFunctions = function (path, obj) { + if (typeof obj === 'object') { + for (var k in obj) { + if (!obj.hasOwnProperty(k)) continue; + var np = path + (path.length ? '/' : '') + k; + if (typeof obj[k] === 'function') { + callbacks[np] = obj[k]; + callbackNames.push(np); + delete obj[k]; + } else if (typeof obj[k] === 'object') { + pruneFunctions(np, obj[k]); + } + } + } + }; + pruneFunctions("", m.params); + + // build a 'request' message and send it + var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params }; + if (callbackNames.length) msg.callbacks = callbackNames; + + if (m.timeout) + // XXX: This function returns a timeout ID, but we don't do anything with it. + // We might want to keep track of it so we can cancel it using clearTimeout() + // when the transaction completes. + setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method)); + + // insert into the transaction table + outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success }; + s_transIds[s_curTranId] = onMessage; + + // increment current id + s_curTranId++; + + postMessage(msg); + }, + notify: function(m) { + if (!m) throw 'missing arguments to notify function'; + if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string"; + + // no need to go into any transaction table + postMessage({ method: scopeMethod(m.method), params: m.params }); + }, + destroy: function () { + s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : '')); + if (window.removeEventListener) window.removeEventListener('message', onMessage, false); + else if(window.detachEvent) window.detachEvent('onmessage', onMessage); + ready = false; + regTbl = { }; + inTbl = { }; + outTbl = { }; + cfg.origin = null; + pendingQueue = [ ]; + debug("channel destroyed"); + chanId = ""; + } + }; + + obj.bind('__ready', onReady); + setTimeout(function() { +// postMessage({ method: scopeMethod('__ready'), params: "ping" }, true); + }, 0); + + return obj; + } + }; + })(); + + // local embedded copy of winchan: http://github.com/lloyd/winchan + // BEGIN WINCHAN + + ;WinChan = (function() { + var RELAY_FRAME_NAME = "__winchan_relay_frame"; + var CLOSE_CMD = "die"; + + // a portable addListener implementation + function addListener(w, event, cb) { + if(w.attachEvent) w.attachEvent('on' + event, cb); + else if (w.addEventListener) w.addEventListener(event, cb, false); + } + + // a portable removeListener implementation + function removeListener(w, event, cb) { + if(w.detachEvent) w.detachEvent('on' + event, cb); + else if (w.removeEventListener) w.removeEventListener(event, cb, false); + } + + // checking for IE8 or above + function isInternetExplorer() { + var rv = -1; // Return value assumes failure. + if (navigator.appName === 'Microsoft Internet Explorer') { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + return rv >= 8; + } + + // checking Mobile Firefox (Fennec) + function isFennec() { + try { + // We must check for both XUL and Java versions of Fennec. Both have + // distinct UA strings. + var userAgent = navigator.userAgent; + return (userAgent.indexOf('Fennec/') != -1) || // XUL + (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java + } catch(e) {}; + return false; + } + + // feature checking to see if this platform is supported at all + function isSupported() { + return (window.JSON && window.JSON.stringify && + window.JSON.parse && window.postMessage); + } + + // given a URL, extract the origin + function extractOrigin(url) { + if (!/^https?:\/\//.test(url)) url = window.location.href; + var m = /^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(url); + if (m) return m[1]; + return url; + } + + // find the relay iframe in the opener + function findRelay() { + var loc = window.location; + var frames = window.opener.frames; + var origin = loc.protocol + '//' + loc.host; + for (var i = frames.length - 1; i >= 0; i--) { + try { + if (frames[i].location.href.indexOf(origin) === 0 && + frames[i].name === RELAY_FRAME_NAME) + { + return frames[i]; + } + } catch(e) { } + } + return; + } + + var isIE = isInternetExplorer(); + + if (isSupported()) { + /* General flow: + * 0. user clicks + * (IE SPECIFIC) 1. caller adds relay iframe (served from trusted domain) to DOM + * 2. caller opens window (with content from trusted domain) + * 3. window on opening adds a listener to 'message' + * (IE SPECIFIC) 4. window on opening finds iframe + * 5. window checks if iframe is "loaded" - has a 'doPost' function yet + * (IE SPECIFIC5) 5a. if iframe.doPost exists, window uses it to send ready event to caller + * (IE SPECIFIC5) 5b. if iframe.doPost doesn't exist, window waits for frame ready + * (IE SPECIFIC5) 5bi. once ready, window calls iframe.doPost to send ready event + * 6. caller upon reciept of 'ready', sends args + */ + return { + open: function(opts, cb) { + if (!cb) throw "missing required callback argument"; + + // test required options + var err; + if (!opts.url) err = "missing required 'url' parameter"; + if (!opts.relay_url) err = "missing required 'relay_url' parameter"; + if (err) setTimeout(function() { cb(err); }, 0); + + // supply default options + if (!opts.window_name) opts.window_name = null; + if (!opts.window_features || isFennec()) opts.window_features = undefined; + + // opts.params may be undefined + + var iframe; + + // sanity check, are url and relay_url the same origin? + var origin = extractOrigin(opts.url); + if (origin !== extractOrigin(opts.relay_url)) { + return setTimeout(function() { + cb('invalid arguments: origin of url and relay_url must match'); + }, 0); + } + + var messageTarget; + + if (isIE) { + // first we need to add a "relay" iframe to the document that's served + // from the target domain. We can postmessage into a iframe, but not a + // window + iframe = document.createElement("iframe"); + // iframe.setAttribute('name', framename); + iframe.setAttribute('src', opts.relay_url); + iframe.style.display = "none"; + iframe.setAttribute('name', RELAY_FRAME_NAME); + document.body.appendChild(iframe); + messageTarget = iframe.contentWindow; + } + + var w = window.open(opts.url, opts.window_name, opts.window_features); + + if (!messageTarget) messageTarget = w; + + var req = JSON.stringify({a: 'request', d: opts.params}); + + // cleanup on unload + function cleanup() { + if (iframe) document.body.removeChild(iframe); + iframe = undefined; + if (w) { + try { + w.close(); + } catch (securityViolation) { + // This happens in Opera 12 sometimes + // see https://github.com/mozilla/browserid/issues/1844 + messageTarget.postMessage(CLOSE_CMD, origin); + } + } + w = messageTarget = undefined; + } + + addListener(window, 'unload', cleanup); + + function onMessage(e) { + try { + var d = JSON.parse(e.data); + if (d.a === 'ready') messageTarget.postMessage(req, origin); + else if (d.a === 'error') { + if (cb) { + cb(d.d); + cb = null; + } + } else if (d.a === 'response') { + removeListener(window, 'message', onMessage); + removeListener(window, 'unload', cleanup); + cleanup(); + if (cb) { + cb(null, d.d); + cb = null; + } + } + } catch(err) { } + } + + addListener(window, 'message', onMessage); + + return { + close: cleanup, + focus: function() { + if (w) { + try { + w.focus(); + } catch (e) { + // IE7 blows up here, do nothing + } + } + } + }; + } + }; + } else { + return { + open: function(url, winopts, arg, cb) { + setTimeout(function() { cb("unsupported browser"); }, 0); + } + }; + } + })(); + + + + // END WINCHAN + + var BrowserSupport = (function() { + var win = window, + nav = navigator, + reason; + + // For unit testing + function setTestEnv(newNav, newWindow) { + nav = newNav; + win = newWindow; + } + + function getInternetExplorerVersion() { + var rv = -1; // Return value assumes failure. + if (nav.appName == 'Microsoft Internet Explorer') { + var ua = nav.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + + return rv; + } + + function checkIE() { + var ieVersion = getInternetExplorerVersion(), + ieNosupport = ieVersion > -1 && ieVersion < 8; + + if(ieNosupport) { + return "BAD_IE_VERSION"; + } + } + + function explicitNosupport() { + return checkIE(); + } + + function checkLocalStorage() { + // Firefox/Fennec/Chrome blow up when trying to access or + // write to localStorage. We must do two explicit checks, first + // whether the browser has localStorage. Second, we must check + // whether the localStorage can be written to. Firefox (at v11) + // throws an exception when querying win['localStorage'] + // when cookies are disabled. Chrome (v17) excepts when trying to + // write to localStorage when cookies are disabled. If an + // exception is thrown, then localStorage is disabled. If no + // exception is thrown, hasLocalStorage will be true if the + // browser supports localStorage and it can be written to. + try { + var hasLocalStorage = 'localStorage' in win + // Firefox will except here if cookies are disabled. + && win['localStorage'] !== null; + + if(hasLocalStorage) { + // browser has localStorage, check if it can be written to. If + // cookies are disabled, some browsers (Chrome) will except here. + win['localStorage'].setItem("test", "true"); + win['localStorage'].removeItem("test"); + } + else { + // Browser does not have local storage. + return "LOCALSTORAGE_NOT_SUPPORTED"; + } + } catch(e) { + return "LOCALSTORAGE_DISABLED"; + } + } + + function checkPostMessage() { + if(!win.postMessage) { + return "POSTMESSAGE_NOT_SUPPORTED"; + } + } + + function checkJSON() { + if(!(window.JSON && window.JSON.stringify && window.JSON.parse)) { + return "JSON_NOT_SUPPORTED"; + } + } + + function isSupported() { + reason = explicitNosupport() || checkLocalStorage() || checkPostMessage() || checkJSON(); + + return !reason; + } + + + function getNoSupportReason() { + return reason; + } + + return { + /** + * Set the test environment. + * @method setTestEnv + */ + setTestEnv: setTestEnv, + /** + * Check whether the current browser is supported + * @method isSupported + * @returns {boolean} + */ + isSupported: isSupported, + /** + * Called after isSupported, if isSupported returns false. Gets the reason + * why browser is not supported. + * @method getNoSupportReason + * @returns {string} + */ + getNoSupportReason: getNoSupportReason + }; + }()); + + if (!navigator.id) { + navigator.id = {}; + } + + if (!navigator.id.request || navigator.id._shimmed) { + var ipServer = "https://login.persona.org"; + var userAgent = navigator.userAgent; + // We must check for both XUL and Java versions of Fennec. Both have + // distinct UA strings. + var isFennec = (userAgent.indexOf('Fennec/') != -1) || // XUL + (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java + + var windowOpenOpts = + (isFennec ? undefined : + "menubar=0,location=1,resizable=1,scrollbars=1,status=0,dialog=1,minimizable=1,width=700,height=375"); + + var w; + + // table of registered observers + var observers = { + login: null, + logout: null, + ready: null + }; + + var compatMode = undefined; + function checkCompat(requiredMode) { + if (requiredMode === true) { + // this deprecation warning should be re-enabled when the .watch and .request APIs become final. + // try { console.log("this site uses deprecated APIs (see documentation for navigator.id.request())"); } catch(e) { } + } + + if (compatMode === undefined) compatMode = requiredMode; + else if (compatMode != requiredMode) { + throw "you cannot combine the navigator.id.watch() API with navigator.id.getVerifiedEmail() or navigator.id.get()" + + "this site should instead use navigator.id.request() and navigator.id.watch()"; + } + } + + var commChan, + browserSupported = BrowserSupport.isSupported(); + + // this is for calls that are non-interactive + function _open_hidden_iframe() { + // If this is an unsupported browser, do not even attempt to add the + // IFRAME as doing so will cause an exception to be thrown in IE6 and IE7 + // from within the communication_iframe. + if(!browserSupported) return; + + try { + if (!commChan) { + var doc = window.document; + var iframe = doc.createElement("iframe"); + iframe.style.display = "none"; + doc.body.appendChild(iframe); + iframe.src = ipServer + "/communication_iframe"; + commChan = Channel.build({ + window: iframe.contentWindow, + origin: ipServer, + scope: "mozid_ni", + onReady: function() { + // once the channel is set up, we'll fire a loaded message. this is the + // cutoff point where we'll say if 'setLoggedInUser' was not called before + // this point, then it wont be called (XXX: optimize and improve me) + commChan.call({ + method: 'loaded', + success: function(){ + if (observers.ready) observers.ready(); + }, error: function() { + } + }); + } + }); + + commChan.bind('logout', function(trans, params) { + if (observers.logout) observers.logout(); + }); + + commChan.bind('login', function(trans, params) { + if (observers.login) observers.login(params); + }); + } + } catch(e) { + // channel building failed! let's ignore the error and allow higher + // level code to handle user messaging. + commChan = undefined; + } + } + + function defined(item) { + return typeof item !== "undefined"; + } + + function warn(message) { + try { + console.warn(message); + } catch(e) { + /* ignore error */ + } + } + + function checkDeprecated(options, field) { + if(defined(options[field])) { + warn(field + " has been deprecated"); + return true; + } + } + + function checkRenamed(options, oldName, newName) { + if (defined(options[oldName]) && + defined(options[newName])) { + throw "you cannot supply *both* " + oldName + " and " + newName; + } + else if(checkDeprecated(options, oldName)) { + options[newName] = options[oldName]; + delete options[oldName]; + } + } + + function internalWatch(options) { + if (typeof options !== 'object') return; + + if (options.onlogin && typeof options.onlogin !== 'function' || + options.onlogout && typeof options.onlogout !== 'function' || + options.onready && typeof options.onready !== 'function') + { + throw "non-function where function expected in parameters to navigator.id.watch()"; + } + + if (!options.onlogin) throw "'onlogin' is a required argument to navigator.id.watch()"; + if (!options.onlogout) throw "'onlogout' is a required argument to navigator.id.watch()"; + + observers.login = options.onlogin || null; + observers.logout = options.onlogout || null; + observers.ready = options.onready || null; + + _open_hidden_iframe(); + + // back compat support for loggedInEmail + checkRenamed(options, "loggedInEmail", "loggedInUser"); + + // check that the commChan was properly initialized before interacting with it. + // on unsupported browsers commChan might still be undefined, in which case + // we let the dialog display the "unsupported browser" message upon spawning. + if (typeof options.loggedInUser !== 'undefined' && commChan) { + commChan.notify({ + method: 'loggedInUser', + params: options.loggedInUser + }); + } + } + + function internalRequest(options) { + checkDeprecated(options, "requiredEmail"); + checkRenamed(options, "tosURL", "termsOfService"); + checkRenamed(options, "privacyURL", "privacyPolicy"); + + if (options.termsOfService && !options.privacyPolicy) { + warn("termsOfService ignored unless privacyPolicy also defined"); + } + + if (options.privacyPolicy && !options.termsOfService) { + warn("privacyPolicy ignored unless termsOfService also defined"); + } + + // focus an existing window + if (w) { + try { + w.focus(); + } + catch(e) { + /* IE7 blows up here, do nothing */ + } + return; + } + + if (!BrowserSupport.isSupported()) { + var reason = BrowserSupport.getNoSupportReason(), + url = "unsupported_dialog"; + + if(reason === "LOCALSTORAGE_DISABLED") { + url = "cookies_disabled"; + } + + w = window.open( + ipServer + "/" + url, + null, + windowOpenOpts); + return; + } + + // notify the iframe that the dialog is running so we + // don't do duplicative work + if (commChan) commChan.notify({ method: 'dialog_running' }); + + w = WinChan.open({ + url: ipServer + '/sign_in', + relay_url: ipServer + '/relay', + window_features: windowOpenOpts, + window_name: '__persona_dialog', + params: { + method: "get", + params: options + } + }, function(err, r) { + // unpause the iframe to detect future changes in login state + if (commChan) { + // update the loggedInUser in the case that an assertion was generated, as + // this will prevent the comm iframe from thinking that state has changed + // and generating a new assertion. IF, however, this request is not a success, + // then we do not change the loggedInUser - and we will let the comm frame determine + // if generating a logout event is the right thing to do + if (!err && r && r.email) { + commChan.notify({ method: 'loggedInUser', params: r.email }); + } + commChan.notify({ method: 'dialog_complete' }); + } + + // clear the window handle + w = undefined; + if (!err && r && r.assertion) { + try { + if (observers.login) observers.login(r.assertion); + } catch(e) { + // client's observer threw an exception + } + } + + // if either err indicates the user canceled the signin (expected) or a + // null response was sent (unexpected), invoke the .oncancel() handler. + if (err === 'client closed window' || !r) { + if (options && options.oncancel) options.oncancel(); + delete options.oncancel; + } + }); + }; + + navigator.id = { + request: function(options) { + if (this != navigator.id) + throw new Error("all navigator.id calls must be made on the navigator.id object"); + options = options || {}; + checkCompat(false); + // returnTo is used for post-email-verification redirect + if (!options.returnTo) options.returnTo = document.location.pathname; + return internalRequest(options); + }, + watch: function(options) { + if (this != navigator.id) + throw new Error("all navigator.id calls must be made on the navigator.id object"); + checkCompat(false); + internalWatch(options); + }, + // logout from the current website + // The callback parameter is DEPRECATED, instead you should use the + // the .onlogout observer of the .watch() api. + logout: function(callback) { + if (this != navigator.id) + throw new Error("all navigator.id calls must be made on the navigator.id object"); + // allocate iframe if it is not allocated + _open_hidden_iframe(); + // send logout message if the commChan exists + if (commChan) commChan.notify({ method: 'logout' }); + if (typeof callback === 'function') setTimeout(callback, 0); + }, + // get an assertion + get: function(callback, passedOptions) { + var opts = {}; + passedOptions = passedOptions || {}; + opts.privacyPolicy = passedOptions.privacyPolicy || undefined; + opts.termsOfService = passedOptions.termsOfService || undefined; + opts.privacyURL = passedOptions.privacyURL || undefined; + opts.tosURL = passedOptions.tosURL || undefined; + + if (checkDeprecated(passedOptions, "silent")) { + // Silent has been deprecated, do nothing. Placing the check here + // prevents the callback from being called twice, once with null and + // once after internalWatch has been called. See issue #1532 + if (callback) setTimeout(function() { callback(null); }, 0); + return; + } + + checkCompat(true); + internalWatch({ + onlogin: function(assertion) { + if (callback) { + callback(assertion); + callback = null; + } + }, + onlogout: function() {} + }); + opts.oncancel = function() { + if (callback) { + callback(null); + callback = null; + } + observers.login = observers.logout = observers.ready = null; + }; + internalRequest(opts); + }, + // backwards compatibility with old API + getVerifiedEmail: function(callback) { + warn("navigator.id.getVerifiedEmail has been deprecated"); + checkCompat(true); + navigator.id.get(callback); + }, + // required for forwards compatibility with native implementations + _shimmed: true + }; + } +}()); diff --git a/templates/user.html b/templates/user.html index 296db8c..995819c 100644 --- a/templates/user.html +++ b/templates/user.html @@ -1,4 +1,4 @@ -{{ template "HEADER" }} +{{ template "HEADER" . }}

{{ .UserName }} is a bommom user.

{{ range .BomList }} @@ -8,4 +8,4 @@ No Boms found! {{ end }}
-{{ template "FOOTER" }} +{{ template "FOOTER" . }} -- cgit v1.2.3