func safeDecodeJSON(in interface{}, out interface{}) *he.Response { decoder, ok := in.(*json.Decoder) if !ok { ret := rfc7231.StatusUnsupportedMediaType(he.NetPrintf("PUT and POST requests must have a document media type")) return &ret } var tmp interface{} err := decoder.Decode(&tmp) if err != nil { ret := rfc7231.StatusUnsupportedMediaType(he.NetPrintf("Couldn't parse: %v", err)) return &ret } str, err := json.Marshal(tmp) if err != nil { panic(err) } err = json.Unmarshal(str, out) if err != nil { ret := rfc7231.StatusUnsupportedMediaType(he.NetPrintf("Request body didn't have expected structure (field had wrong data type): %v", err)) return &ret } if !jsondiff.Equal(tmp, out) { diff, err := jsondiff.NewJSONPatch(tmp, out) if err != nil { panic(err) } entity := decodeJSONError{ message: locale.Sprintf("Request body didn't have expected structure (extra or missing fields). The included diff would make the request acceptable."), diff: diff, } ret := rfc7231.StatusUnsupportedMediaType(entity) return &ret } return nil }
func (o *user) patchPassword(patch *jsonpatch.Patch) *he.Response { // this is in the running for the grossest code I've ever // written, but I think it's the best way to do it --lukeshu type patchop struct { Op string `json:"op"` Path string `json:"path"` Value string `json:"value"` } str, err := json.Marshal(patch) if err != nil { panic(err) } var ops []patchop err = json.Unmarshal(str, &ops) if err != nil { return nil } outOps := make([]patchop, 0, len(ops)) checkedpass := false for _, op := range ops { if op.Path == "/password" { switch op.Op { case "test": if !o.backend().CheckPassword(op.Value) { ret := rfc7231.StatusConflict(he.NetPrintf("old password didn't match")) return &ret } checkedpass = true case "replace": if !checkedpass { ret := rfc7231.StatusUnsupportedMediaType(he.NetPrintf("you must submit and old password (using 'test') before setting a new one")) return &ret } if o.backend().CheckPassword(op.Value) { ret := rfc7231.StatusConflict(he.NetPrintf("that new password is the same as the old one")) return &ret } o.backend().SetPassword(op.Value) default: ret := rfc7231.StatusUnsupportedMediaType(he.NetPrintf("you may only 'set' or 'replace' the password")) return &ret } } else { outOps = append(outOps, op) } } str, err = json.Marshal(outOps) if err != nil { panic(err) } var out jsonpatch.JSONPatch err = json.Unmarshal(str, &out) if err != nil { panic(out) } *patch = out return nil }
func StatusInternalServerError(err interface{}) he.Response { return he.Response{ Status: 500, Headers: http.Header{ "Content-Type": {"text/plain; charset=utf-8"}, }, Entity: he.NetPrintf("500 Internal Server Error: %v", err), } }
// For when rhe document the user requested has temporarily moved. // // The client must repeate the request exactly the same, except for // the URL. func StatusTemporaryRedirect(u *url.URL) he.Response { return he.Response{ Status: 307, Headers: http.Header{ "Location": {u.String()}, }, Entity: he.NetPrintf("307 Temporary Redirect: %v", u), } }
func StatusSeeOther(u *url.URL) he.Response { return he.Response{ Status: 303, Headers: http.Header{ "Location": {u.String()}, }, Entity: he.NetPrintf("303 See Other: %v", u), } }
// For when the document the user requested has permantly moved to a // new address. func StatusMovedPermanently(u *url.URL) he.Response { return he.Response{ Status: 301, Headers: http.Header{ "Location": {u.String()}, }, Entity: he.NetPrintf("301 Moved"), } }
// For when the document the user requested is currently found at // another address, but that may not be the case in the future. // // The client may change a POST to a GET request when trying the new // location. func StatusFound(u *url.URL) he.Response { return he.Response{ Status: 302, Headers: http.Header{ "Location": {u.String()}, }, Entity: he.NetPrintf("302 Found: %v", u), } }
func StatusForbidden(e he.NetEntity) he.Response { if e == nil { e = he.NetPrintf("403 Forbidden") } return he.Response{ Status: 403, Headers: http.Header{}, Entity: e, } }
// For when the *user* has screwed up a request. func StatusBadRequest(e he.NetEntity) he.Response { if e == nil { e = he.NetPrintf("400 Bad Request") } return he.Response{ Status: 400, Headers: http.Header{}, Entity: e, } }
func StatusUnsupportedMediaType(e he.NetEntity) he.Response { if e == nil { e = he.NetPrintf("415 Unsupported Media Type") } return he.Response{ Status: 415, Headers: http.Header{}, Entity: e, } }
func StatusMethodNotAllowed(entity he.Entity, request he.Request) he.Response { return he.Response{ Status: 405, Headers: http.Header{ "Allow": {methods2string(entity.Methods())}, }, Entity: he.NetPrintf("405 Method Not Allowed"), InhibitNotAcceptable: true, InhibitMultipleChoices: true, } }
func StatusNotFound(e he.NetEntity) he.Response { if e == nil { e = he.NetPrintf("404 Not Found") } return he.Response{ Status: 404, Headers: http.Header{}, Entity: e, InhibitNotAcceptable: true, InhibitMultipleChoices: true, } }
func newDirUsers() dirUsers { r := dirUsers{} r.methods = map[string]func(he.Request) he.Response{ "POST": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) type postfmt struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` PasswordVerification string `json:"password_verification,omitempty"` } var entity postfmt httperr := safeDecodeJSON(req.Entity, &entity) if httperr != nil { return *httperr } if entity.Username == "" || entity.Email == "" || entity.Password == "" { return rfc7231.StatusUnsupportedMediaType(he.NetPrintf("username, email, and password can't be emtpy")) } if entity.PasswordVerification != "" { if entity.Password != entity.PasswordVerification { // Passwords don't match return rfc7231.StatusConflict(he.NetPrintf("password and password_verification don't match")) } } usr := backend.NewUser(db, entity.Username, entity.Password, entity.Email) backend.NewUserAddress(db, usr.ID, "noop", backend.RandomString(20), true) backend.NewUserAddress(db, usr.ID, "admin", backend.RandomString(20), true) req.Things["user"] = usr return rfc7231.StatusCreated(r, usr.ID, req) }, } return r }
func (usr *userSubscriptions) Methods() map[string]func(he.Request) he.Response { return map[string]func(he.Request) he.Response{ "GET": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) usr.groupID = req.URL.Query().Get("group_id") usr.values = usr.backend().GetFrontEndSubscriptions(db) return rfc7231.StatusOK(usr) }, "POST": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) type postfmt struct { GroupID string `json:"group_id"` Medium string `json:"medium,omitempty"` Address string `json:"address,omitempty"` } var entity postfmt httperr := safeDecodeJSON(req.Entity, &entity) if httperr != nil { return *httperr } entity.GroupID = strings.ToLower(entity.GroupID) var address *backend.UserAddress if entity.Medium == "" && entity.Address == "" { address = &usr.Addresses[0] for _, addr := range usr.Addresses { if addr.SortOrder < address.SortOrder { address = &addr } } } else { for _, addr := range usr.Addresses { if addr.Medium == entity.Medium && addr.Address == entity.Address { address = &addr break } } } if address == nil { return rfc7231.StatusConflict(he.NetPrintf("You don't have that address")) } backend.NewSubscription(db, address.ID, entity.GroupID, sess != nil && sess.UserID == usr.ID) return rfc7231.StatusCreated(usr, entity.GroupID+":"+address.Medium+":"+address.Address, req) }, } }
func (o *captcha) Methods() map[string]func(he.Request) he.Response { return map[string]func(he.Request) he.Response{ "GET": func(req he.Request) he.Response { return rfc7231.StatusOK(o) }, "POST": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) type postfmt struct { Value string `json:"value"` Expiration time.Time `json:"password"` } var entity postfmt httperr := safeDecodeJSON(req.Entity, &entity) if httperr != nil { return *httperr } o := (*captcha)(backend.NewCaptcha(db)) if o == nil { return rfc7231.StatusForbidden(he.NetPrintf("Captcha generation failed")) } else { ret := rfc7231.StatusOK(o) return ret } }, "PUT": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) var newCaptcha captcha httperr := safeDecodeJSON(req.Entity, &newCaptcha) if httperr != nil { return *httperr } *o = newCaptcha o.backend().Save(db) return rfc7231.StatusOK(o) }, /* "PATCH": func(req he.Request) he.Response { panic("TODO: API: (*captcha).Methods()[\"PATCH\"]") }, */ } }
// For when you've created a document with a new URL. func StatusCreated(parent he.EntityGroup, childName string, req he.Request) he.Response { if childName == "" { panic("can't call StatusCreated with an empty child name") } // find the child child := parent.Subentity(childName, req) if child == nil { panic("called StatusCreated, but the subentity doesn't exist") } // prepare the response u, _ := req.URL.Parse(url.QueryEscape(childName)) return he.Response{ Status: 201, Headers: http.Header{ "Location": {u.String()}, }, Entity: he.NetPrintf("%s", u.String()), InhibitNotAcceptable: true, InhibitMultipleChoices: true, } }
func (o *group) Methods() map[string]func(he.Request) he.Response { return map[string]func(he.Request) he.Response{ "GET": func(req he.Request) he.Response { var enum Enumerategroup enum = EnumerateGroup(o) return rfc7231.StatusOK(he.NetJSON{Data: enum}) }, "PUT": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) var newGroup group httperr := safeDecodeJSON(req.Entity, &newGroup) if httperr != nil { return *httperr } if o.ID != newGroup.ID { return rfc7231.StatusConflict(he.NetPrintf("Cannot change group id")) } *o = newGroup o.backend().Save(db) return rfc7231.StatusOK(o) }, "PATCH": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) subscribed := backend.IsSubscribed(db, sess.UserID, *o.backend()) if !backend.IsAdmin(db, sess.UserID, *o.backend()) { if o.JoinPublic == 1 { if subscribed == 0 { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } if o.JoinConfirmed == 1 && subscribed == 1 { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } if o.JoinMember == 1 { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } } } enum := EnumerateGroup(o) var newGroup Enumerategroup patch, ok := req.Entity.(jsonpatch.Patch) if !ok { return rfc7231.StatusUnsupportedMediaType(he.NetPrintf("PATCH request must have a patch media type")) } err := patch.Apply(enum, &newGroup) if err != nil { return rfc7231.StatusConflict(he.NetPrintf("%v", err)) } if o.ID != newGroup.Groupname { return rfc7231.StatusConflict(he.NetPrintf("Cannot change group id")) } *o = RenumerateGroup(newGroup) o.backend().Save(db) return rfc7231.StatusOK(o) }, "DELETE": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) if !backend.IsAdmin(db, sess.UserID, *o.backend()) { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } o.backend().Delete(db) return rfc7231.StatusNoContent() }, } }
func newDirGroups() dirGroups { r := dirGroups{} r.methods = map[string]func(he.Request) he.Response{ "GET": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) var groups []backend.Group type getfmt struct { visibility string } var entity getfmt httperr := safeDecodeJSON(req.Entity, &entity) if httperr != nil { entity.visibility = "subscribed" } if sess == nil { groups = []backend.Group{} } else if entity.visibility == "subscribed" { groups = backend.GetGroupsByMember(db, *backend.GetUserByID(db, sess.UserID)) } else { //groups = GetAllGroups(db) groups = backend.GetPublicAndSubscribedGroups(db, *backend.GetUserByID(db, sess.UserID)) } type EnumerateGroup struct { Groupname string `json:"groupname"` Post map[string]string `json:"post"` Join map[string]string `json:"join"` Read map[string]string `json:"read"` Existence map[string]string `json:"existence"` Subscriptions []backend.Subscription `json:"subscriptions"` } data := make([]EnumerateGroup, len(groups)) for i, grp := range groups { var enum EnumerateGroup enum.Groupname = grp.ID exist := [...]int{grp.ExistencePublic, grp.ExistenceConfirmed} enum.Existence = backend.ReadExist(exist) read := [...]int{grp.ReadPublic, grp.ReadConfirmed} enum.Read = backend.ReadExist(read) post := [...]int{grp.PostPublic, grp.PostConfirmed, grp.PostMember} enum.Post = backend.PostJoin(post) join := [...]int{grp.JoinPublic, grp.JoinConfirmed, grp.JoinMember} enum.Join = backend.PostJoin(join) enum.Subscriptions = grp.Subscriptions data[i] = enum } return rfc7231.StatusOK(he.NetJSON{Data: data}) }, "POST": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) type Response1 struct { Groupname string `json:"groupname"` Post map[string]string `json:"post"` Join map[string]string `json:"join"` Read map[string]string `json:"read"` Existence map[string]string `json:"existence"` } var entity Response1 httperr := safeDecodeJSON(req.Entity, &entity) if httperr != nil { return *httperr } if entity.Groupname == "" { return rfc7231.StatusUnsupportedMediaType(he.NetPrintf("groupname can't be emtpy")) } grp := backend.NewGroup( db, entity.Groupname, backend.Reverse(entity.Existence), backend.Reverse(entity.Read), backend.Reverse(entity.Post), backend.Reverse(entity.Join), ) sess := req.Things["session"].(*backend.Session) address := backend.GetAddressesByUserAndMedium(db, sess.UserID, "noop")[0] backend.NewSubscription(db, address.ID, grp.ID, true) if grp == nil { return rfc7231.StatusConflict(he.NetPrintf("a group with that name already exists")) } else { return rfc7231.StatusCreated(r, grp.ID, req) } }, } return r }
func (usr *user) Methods() map[string]func(he.Request) he.Response { return map[string]func(he.Request) he.Response{ "GET": func(req he.Request) he.Response { var addresses []backend.UserAddress for _, addr := range usr.Addresses { if addr.Medium != "noop" && addr.Medium != "admin" { addresses = append(addresses, addr) } } usr.Addresses = addresses return rfc7231.StatusOK(usr) }, "PUT": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) if sess.UserID != usr.ID { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } var newUser user httperr := safeDecodeJSON(req.Entity, &newUser) if httperr != nil { return *httperr } if usr.ID != newUser.ID { return rfc7231.StatusConflict(he.NetPrintf("Cannot change user id")) } // TODO: this won't play nice with the // password hash (because it's private), or // with addresses (because the (private) IDs // need to be made to match up) *usr = newUser usr.backend().Save(db) return rfc7231.StatusOK(usr) }, "PATCH": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) sess := req.Things["session"].(*backend.Session) if sess.UserID != usr.ID { return rfc7231.StatusForbidden(he.NetPrintf("Unauthorized user")) } patch, ok := req.Entity.(jsonpatch.Patch) if !ok { return rfc7231.StatusUnsupportedMediaType(he.NetPrintf("PATCH request must have a patch media type")) } httperr := usr.patchPassword(&patch) if httperr != nil { return *httperr } var newUser user err := patch.Apply(usr, &newUser) if err != nil { return rfc7231.StatusConflict(he.ErrorToNetEntity(409, err)) } if usr.ID != newUser.ID { return rfc7231.StatusConflict(he.NetPrintf("Cannot change user id")) } if newUser.PwHash == nil || len(newUser.PwHash) == 0 { newUser.PwHash = usr.PwHash } *usr = newUser usr.backend().Save(db) return rfc7231.StatusOK(usr) }, "DELETE": func(req he.Request) he.Response { db := req.Things["db"].(*periwinkle.Tx) usr.backend().Delete(db) return rfc7231.StatusNoContent() }, } }