コード例 #1
1
ファイル: util.go プロジェクト: LukeShu/periwinkle
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
}
コード例 #2
0
ファイル: path_v1_users.go プロジェクト: LukeShu/periwinkle
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
}
コード例 #3
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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),
	}
}
コード例 #4
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
// 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),
	}
}
コード例 #5
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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),
	}
}
コード例 #6
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
// 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"),
	}
}
コード例 #7
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
// 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),
	}
}
コード例 #8
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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,
	}
}
コード例 #9
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
// 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,
	}
}
コード例 #10
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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,
	}
}
コード例 #11
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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,
	}
}
コード例 #12
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
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,
	}
}
コード例 #13
0
ファイル: path_v1_users.go プロジェクト: LukeShu/periwinkle
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
}
コード例 #14
0
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)
		},
	}
}
コード例 #15
0
ファイル: path_v1_captcha.go プロジェクト: LukeShu/periwinkle
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\"]")
			},
		*/
	}
}
コード例 #16
0
ファイル: status.go プロジェクト: LukeShu/periwinkle
// 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,
	}
}
コード例 #17
0
ファイル: path_v1_groups.go プロジェクト: LukeShu/periwinkle
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()
		},
	}
}
コード例 #18
0
ファイル: path_v1_groups.go プロジェクト: LukeShu/periwinkle
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
}
コード例 #19
0
ファイル: path_v1_users.go プロジェクト: LukeShu/periwinkle
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()
		},
	}
}