diff --git a/api/alldoormen.go b/api/alldoormen.go index eeb9438..ff06961 100644 --- a/api/alldoormen.go +++ b/api/alldoormen.go @@ -6,13 +6,13 @@ import ( ) func GetAllDoormen(w http.ResponseWriter, request *http.Request) { - fmt.Fprint(w, `{"doormen" :[`) doormenIds := conn.GetAllDoormen() buff := <-doormenIds if buff == nil { - fmt.Fprint(w, "]}") + fmt.Fprint(w, `{"doormen" :[]}`) return } + fmt.Fprint(w, `{"doormen" :[`) for doormanId := range doormenIds { fmt.Fprint(w, getDoormenIdsAsJson(buff)+`,`) buff = doormanId diff --git a/api/alldoormen_test.go b/api/alldoormen_test.go index aeae2b5..dcefc20 100644 --- a/api/alldoormen_test.go +++ b/api/alldoormen_test.go @@ -29,7 +29,7 @@ func TestGetAllDoormen(t *testing.T) { GetAllDoormen(w, req) d := json.NewDecoder(w.Body) - m := make(map[string][]*doormen.DoormanDefinition) + m := make(map[string][]*doormen.DoormanId) if err := d.Decode(&m); err != nil { t.Error(err) diff --git a/api/createdoorman.go b/api/createdoorman.go index bb1e882..dc0c0f3 100644 --- a/api/createdoorman.go +++ b/api/createdoorman.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/didiercrunch/doormanserver/doormen" + "github.com/didiercrunch/doormanserver/shared" "gopkg.in/mgo.v2/bson" "net/http" ) @@ -30,6 +31,6 @@ func CreateDoorman(w http.ResponseWriter, request *http.Request) { return } go publisher.Emit(wdef.Id, wdef) - w.Header().Set("location", "/api/doormen/"+wdef.Id.Hex()) + w.Header().Set("location", "/api/doormen/"+shared.ObjectIdToPublicId(wdef.Id)) w.WriteHeader(201) } diff --git a/api/createdoorman_test.go b/api/createdoorman_test.go index 00b29e4..4f16596 100644 --- a/api/createdoorman_test.go +++ b/api/createdoorman_test.go @@ -3,6 +3,7 @@ package api import ( "bytes" "github.com/didiercrunch/doormanserver/inmemoryconnector" + "github.com/didiercrunch/doormanserver/shared" "gopkg.in/mgo.v2/bson" "net/http" "net/http/httptest" @@ -11,7 +12,12 @@ import ( ) func GetIdFromLocation(p string) bson.ObjectId { - return bson.ObjectIdHex(path.Base(p)) + if id, err := shared.PublicIdToObjectId(path.Base(p)); err != nil { + panic(err) + } else { + return id + } + } func TestCreateDoorman(t *testing.T) { diff --git a/api/util.go b/api/util.go index 7295664..8a9af19 100644 --- a/api/util.go +++ b/api/util.go @@ -3,6 +3,7 @@ package api import ( "fmt" "github.com/didiercrunch/doormanserver/doormen" + "github.com/didiercrunch/doormanserver/shared" "github.com/gorilla/mux" "gopkg.in/mgo.v2/bson" "log" @@ -10,15 +11,16 @@ import ( ) func getDoormenIdsAsJson(id *doormen.DoormanId) string { - return fmt.Sprintf(`{"name": "%v", "id": "%v", "url": "/api/doormen/%v"}`, id.Name, id.Id.Hex(), id.Id.Hex()) + publicId := shared.ObjectIdToPublicId(id.Id) + return fmt.Sprintf(`{"name": "%v", "id": "%v", "url": "/api/doormen/%v"}`, id.Name, publicId, publicId) } func getDoormanIdFromRequest(w http.ResponseWriter, request *http.Request) bson.ObjectId { id := mux.Vars(request)["id"] - if !bson.IsObjectIdHex(id) { + if id, err := shared.PublicIdToObjectId(id); err != nil { return "" } else { - return bson.ObjectIdHex(id) + return id } } diff --git a/doormen/doormen.go b/doormen/doormen.go index e0c8ddc..e9a0eed 100644 --- a/doormen/doormen.go +++ b/doormen/doormen.go @@ -17,11 +17,6 @@ type DoormanValue struct { Probability *big.Rat `json:"probability"` } -type doormanValue struct { - Name string - Probability string -} - type DoormanId struct { Id bson.ObjectId `json:"id,omitempty" bson:"_id"` Name string `json:"name" bson:"name"` @@ -35,10 +30,10 @@ func (wId *DoormanId) AsJson() string { } type DoormanDefinition struct { - Id bson.ObjectId `json:"id,omitempty" bson:"_id"` - Name string `json:"name" bson:"name"` - Values []*DoormanValue `json:"values" bson:"values"` - OwnerEmails []string `json:"emails" bson:"emails"` + Id bson.ObjectId `bson:"_id"` + Name string `bson:"name"` + Values []*DoormanValue `bson:"values"` + OwnerEmails []string `bson:"emails"` } func NewDoormanDefinition(name string, ownerEmails ...string) *DoormanDefinition { diff --git a/doormen/encoding.go b/doormen/encoding.go index ede7879..dc877ae 100644 --- a/doormen/encoding.go +++ b/doormen/encoding.go @@ -1,10 +1,17 @@ package doormen import ( + "encoding/json" + "github.com/didiercrunch/doormanserver/shared" "gopkg.in/mgo.v2/bson" "math/big" ) +type doormanValue struct { + Name string + Probability string +} + func (val *DoormanValue) GetBSON() (interface{}, error) { ret := &doormanValue{val.Name, ""} txt, err := val.Probability.MarshalText() @@ -24,3 +31,60 @@ func (val *DoormanValue) SetBSON(raw bson.Raw) error { val.Probability = new(big.Rat) return val.Probability.UnmarshalText([]byte(v.Probability)) } + +type jsonDoormanDefinition struct { + Id string `json:"id,omitempty"` + Name string `json:"name"` + Values []*DoormanValue `json:"values"` + OwnerEmails []string `json:"emails"` +} + +func (dm *DoormanDefinition) MarshalJSON() ([]byte, error) { + jdm := new(jsonDoormanDefinition) + jdm.Name = dm.Name + jdm.OwnerEmails = dm.OwnerEmails + jdm.Values = dm.Values + jdm.Id = shared.ObjectIdToPublicId(dm.Id) + return json.Marshal(jdm) +} + +func (dm *DoormanDefinition) UnmarshalJSON(data []byte) error { + jsm := new(jsonDoormanDefinition) + var err error + if err = json.Unmarshal(data, jsm); err != nil { + return err + } + if dm.Id, err = shared.PublicIdToObjectId(jsm.Id); err != nil { + return err + } + dm.Name = jsm.Name + dm.Values = jsm.Values + dm.OwnerEmails = jsm.OwnerEmails + return nil +} + +type jsonDoormanId struct { + Name string `json:"name"` + Id string `json:"id"` +} + +func (did *DoormanId) MarshalJSON() ([]byte, error) { + jdid := new(jsonDoormanId) + jdid.Name = did.Name + jdid.Id = shared.ObjectIdToPublicId(did.Id) + return json.Marshal(jdid) + +} + +func (did *DoormanId) UnmarshalJSON(data []byte) error { + jdid := new(jsonDoormanId) + if err := json.Unmarshal(data, jdid); err != nil { + return err + } + var err error + if did.Id, err = shared.PublicIdToObjectId(jdid.Id); err != nil { + return err + } + did.Name = jdid.Name + return nil +} diff --git a/doormen/encoding_test.go b/doormen/encoding_test.go index 3f5b68b..e3d6a64 100644 --- a/doormen/encoding_test.go +++ b/doormen/encoding_test.go @@ -1,6 +1,7 @@ package doormen import ( + "encoding/base64" "encoding/json" "gopkg.in/mgo.v2/bson" "reflect" @@ -22,6 +23,42 @@ func TestJsonEncoding(t *testing.T) { } } +func TestJsonEncodingWithEmptyIds(t *testing.T) { + dmd := QuickNewDoormanDefinition("test json", "1/4", "3/4") + dmd.Id = "" + data, err := json.Marshal(dmd) + if err != nil { + t.Error(err) + } + dmd2 := new(DoormanDefinition) + if err = json.Unmarshal(data, dmd2); err != nil { + t.Error(err) + } + if !reflect.DeepEqual(dmd2, dmd) { + t.Error() + } +} + +func TestJsonIdFormatToBeCompatibleWithSiphashKey(t *testing.T) { + dmd := QuickNewDoormanDefinition("test json", "1/4", "3/4") + data, err := json.Marshal(dmd) + if err != nil { + t.Error(err) + return + } + m := make(map[string]interface{}) + if err = json.Unmarshal(data, &m); err != nil { + t.Error(err) + return + } + if b, err := base64.URLEncoding.DecodeString(m["id"].(string)); err != nil { + t.Error(err) + return + } else if len(b) != 16 { + t.Error("siphash requires a 16 bytes (128 bits) key not ", len(b)) + } +} + func TestBsonEncoding(t *testing.T) { dmd := QuickNewDoormanDefinition("test bson", "1/4", "3/4") data, err := bson.Marshal(dmd) @@ -36,3 +73,19 @@ func TestBsonEncoding(t *testing.T) { t.Error() } } + +func TestDoormanIdJsonEncoding(t *testing.T) { + did := &DoormanId{bson.NewObjectId(), "hahaha"} + data, err := json.Marshal(did) + if err != nil { + t.Error(err) + } + did2 := new(DoormanId) + if err = json.Unmarshal(data, did2); err != nil { + t.Error(err) + } + if !reflect.DeepEqual(did2, did) { + t.Error() + } + +} diff --git a/server.go b/server.go index f7d2862..684e9dc 100644 --- a/server.go +++ b/server.go @@ -17,7 +17,7 @@ func GetAddressToServe() string { return params.Host + ":" + strconv.Itoa(params.Port) } -func serveStatic2(w http.ResponseWriter, request *http.Request) { +func serveStatic(w http.ResponseWriter, request *http.Request) { filepath := request.URL.Path w.Header().Set("Cache-Control", "private, max-age=0, no-cache") http.ServeFile(w, request, path.Join(STATIC_DIR_PATH, filepath)) @@ -26,7 +26,7 @@ func serveStatic2(w http.ResponseWriter, request *http.Request) { func main() { mux := http.NewServeMux() mux.Handle("/api/", api.Create("/api")) - mux.HandleFunc("/", serveStatic2) + mux.HandleFunc("/", serveStatic) address := GetAddressToServe() fmt.Println("server running at", address) diff --git a/shared/params.go b/shared/params.go index 3bc99a6..36fc673 100644 --- a/shared/params.go +++ b/shared/params.go @@ -69,7 +69,7 @@ func (p *Params) InsertDefaultValues() *Params { } func LoadFromYamlFile(fileName string) (*Params, error) { - if !Exists(fileName) { + if !exists(fileName) { return new(Params).InsertDefaultValues(), nil } data, err := ioutil.ReadFile(fileName) diff --git a/shared/util.go b/shared/util.go index 1b83ab1..c6b6418 100644 --- a/shared/util.go +++ b/shared/util.go @@ -1,8 +1,17 @@ package shared -import "os" +import ( + "crypto/subtle" + "encoding/base64" + "errors" + "github.com/dchest/siphash" + "gopkg.in/mgo.v2/bson" + "os" +) -func Exists(name string) bool { +var idHashKey []byte = []byte("semi-private-key") + +func exists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false @@ -10,3 +19,38 @@ func Exists(name string) bool { } return true } + +func validateId(id []byte) error { + if len(id) != 16 { + return errors.New("valid ids are 16 bytes (126 bit) long.") + } + h := siphash.New(idHashKey) + h.Write(id[4:16]) + if subtle.ConstantTimeCompare(h.Sum(nil)[0:4], id[0:4]) != 1 { + return errors.New("invalid id checksum") + } + return nil +} + +func ObjectIdToPublicId(id bson.ObjectId) string { + if id == "" { + return "" + } + h := siphash.New(idHashKey) + h.Write([]byte(id)) + return base64.URLEncoding.EncodeToString(append(h.Sum(nil)[0:4], []byte(id)...)) +} + +func PublicIdToObjectId(id string) (bson.ObjectId, error) { + if id == "" { + return "", nil + } + cypher, err := base64.URLEncoding.DecodeString(id) + if err != nil { + return "", err + } + if err := validateId(cypher); err != nil { + return "", err + } + return bson.ObjectId(cypher[4:16]), nil +}