// DisableUsers deactivates users' accounts. // Administrator permissions should be checked before this call. func DisableUsers(ctx context.Context, names []string) ([]UserResult, error) { s, err := db.CtxSession(ctx) if err != nil { return nil, err } coll, err := db.Coll(s, "users") if err != nil { return nil, err } now := time.Now().UTC() result := make([]UserResult, len(names)) for i, name := range names { err = coll.Update(bson.M{"_id": name, "off": false}, bson.M{"$set": bson.M{"off": true, "mt": now}}) if err != nil { errMsg := "internal error" if err == mgo.ErrNotFound { errMsg = "not found" } result[i] = UserResult{Name: name, Err: errMsg} continue } result[i] = UserResult{Name: name, Err: ""} } return result, nil }
// Lengthen converts a short link to original one. // It uses own database session if it's needed // or it gets data from the cache. func Lengthen(ctx context.Context, short string) (*CustomURL, error) { c, err := conf.FromContext(ctx) if err != nil { return nil, err } cache, cacheOn := c.Cache.Strorage["URL"] if cacheOn { if cu, ok := cache.Get(short); ok { // c.L.Debug.Println("read from LRU cache", short) return cu.(*CustomURL), nil } } num, err := Decode(short) if err != nil { return nil, err } s, err := db.NewSession(c.Conn, false) if err != nil { return nil, err } defer s.Close() coll, err := db.Coll(s, "urls") if err != nil { return nil, err } cu := &CustomURL{} err = coll.Find(bson.D{{Name: "_id", Value: num}, {Name: "off", Value: false}}).One(cu) if err != nil { return nil, err } if cacheOn { cache.Add(short, cu) } return cu, nil }
// Shorten returns new short links. func Shorten(ctx context.Context, params []*ReqParams) ([]*CustomURL, error) { c, err := conf.FromContext(ctx) if err != nil { return nil, err } u, err := auth.ExtractUser(ctx) if err != nil { return nil, err } // check URLs pack size n := len(params) if n > c.Settings.MaxPack { return nil, fmt.Errorf("too big ReqParams pack size [%v]", n) } s, err := db.CtxSession(ctx) if err != nil { return nil, err } // prepare coll, err := db.Coll(s, "urls") if err != nil { return nil, err } now := time.Now().UTC() err = db.LockURL(s) if err != nil { return nil, err } defer db.UnlockURL(s) num, err := getMax(coll) if err != nil { return nil, err } documents := make([]interface{}, n) cus := make([]*CustomURL, n) for i, param := range params { num++ cus[i] = &CustomURL{ ID: num, Group: param.Group, Tag: param.Tag, Original: param.Original, User: u.Name, TTL: param.TTL, NotDirect: param.NotDirect, Created: now, Modified: now, Cb: param.Cb, API: param.IsAPI, } documents[i] = cus[i] } err = coll.Insert(documents...) if err != nil { return nil, err } return cus, nil }
// Export exports URLs data. func Export(ctx context.Context, filter Filter) ([]*CustomURL, [3]int, error) { var result []*CustomURL pages := [3]int{1, 1, filter.PageSize} s, err := db.CtxSession(ctx) if err != nil { return nil, pages, err } coll, err := db.Coll(s, "urls") if err != nil { return nil, pages, err } conditions := bson.M{"group": filter.Group, "tag": filter.Tag} if filter.Active { conditions["off"] = false } switch { case filter.Period[0] != nil && filter.Period[1] != nil: conditions["$and"] = []bson.M{ bson.M{"ts": bson.M{"$gte": *filter.Period[0]}}, bson.M{"ts": bson.M{"$lte": *filter.Period[1]}}, } case filter.Period[0] != nil: conditions["ts"] = bson.M{"$gte": *filter.Period[0]} case filter.Period[1] != nil: conditions["ts"] = bson.M{"$lte": *filter.Period[1]} } // TODO: add skip+limit n, err := coll.Find(conditions).Count() if err != nil { return nil, pages, err } if n == 0 { return result, pages, nil } pages[0] = filter.Page pages[1] = n / pages[2] if n%pages[2] != 0 { pages[1]++ } switch { case pages[0] < 1: pages[0] = 1 case pages[0] > pages[1]: pages[0] = pages[1] } err = coll.Find(conditions).Skip((pages[0] - 1) * pages[2]).Limit(pages[2]).Sort("-id").All(&result) if err != nil { return nil, pages, err } return result, pages, nil }
// ChangeUsers updates user's tokens. // Administrator permissions should be checked before this call. func ChangeUsers(ctx context.Context, names []string) ([]UserResult, error) { c, err := conf.FromContext(ctx) if err != nil { return nil, err } user, err := ExtractUser(ctx) if err != nil { return nil, err } s, err := db.CtxSession(ctx) if err != nil { return nil, err } coll, err := db.Coll(s, "users") if err != nil { return nil, err } now := time.Now().UTC() isAdmin := user.HasRole("admin") result := make([]UserResult, len(names)) for i, name := range names { if !isAdmin && user.Name != name { result[i] = UserResult{Name: name, U: nil, Err: "permissions error"} continue } token, hash, err := genToken(c) if err != nil { result[i] = UserResult{Name: name, U: nil, Err: "internal error"} continue } err = coll.UpdateId(name, bson.M{"$set": bson.M{"token": hash, "mt": now}}) if err != nil { errMsg := "internal error" if err == mgo.ErrNotFound { errMsg = "not found" } result[i] = UserResult{Name: name, U: nil, Err: errMsg} continue } result[i] = UserResult{Name: name, U: nil, T: token + hash, Err: ""} } return result, nil }
// InitUsers initializes admin and anonymous users. func InitUsers(c *conf.Config) error { s, err := db.NewSession(c.Conn, false) if err != nil { return err } defer s.Close() coll, err := db.Coll(s, "users") if err != nil { return err } b, err := hex.DecodeString(c.Listener.Security.Admin) if err != nil { return err } h := tokenHash(b, c) now := time.Now().UTC() users := []*User{ { Name: "admin", Disabled: false, Token: hex.EncodeToString(h), Roles: []string{"admin"}, Modified: now, Created: now, }, { Name: Anonymous, Disabled: false, Token: "", Roles: []string{}, Modified: now, Created: now, }, } for _, u := range users { err := coll.Insert(u) if err != nil && !mgo.IsDup(err) { return err } } return nil }
// clean disables expired short URLs. func clean(c *conf.Config) error { var change int s, err := db.NewSession(c.Conn, false) if err != nil { return err } defer s.Close() coll, err := db.Coll(s, "urls") if err != nil { return err } condition := bson.D{ {Name: "off", Value: false}, {Name: "ttl", Value: bson.M{"$lt": time.Now().UTC()}}, } update := bson.M{"$set": bson.M{"off": true}} cache, cacheOn := c.Cache.Strorage["URL"] if cacheOn { cu := &trim.CustomURL{} iter := coll.Find(condition).Iter() for iter.Next(cu) { if err := coll.UpdateId(cu.ID, update); err == nil { cache.Remove(cu.String()) change++ } } err = iter.Close() if err != nil { return err } } else { // cache is disable, update only URLs info, err := coll.UpdateAll(condition, update) if err != nil { return err } change = info.Updated } c.L.Debug.Printf("cleaned %v item(s)", change) return nil }
// CreateUsers creates new users. func CreateUsers(ctx context.Context, names []string) ([]UserResult, error) { c, err := conf.FromContext(ctx) if err != nil { return nil, err } s, err := db.CtxSession(ctx) if err != nil { return nil, err } coll, err := db.Coll(s, "users") if err != nil { return nil, err } now := time.Now().UTC() result := make([]UserResult, len(names)) for i, name := range names { token, hash, err := genToken(c) if err != nil { result[i] = UserResult{Name: name, U: nil, Err: "internal error"} continue } user := &User{ Name: name, Token: hash, Roles: []string{"user"}, Modified: now, Created: now, } if err := coll.Insert(user); err != nil { if mgo.IsDup(err) { result[i] = UserResult{Name: name, U: nil, Err: "duplicate item"} } else { result[i] = UserResult{Name: name, U: nil, Err: "internal error"} } continue } result[i] = UserResult{Name: name, U: user, T: token + hash, Err: ""} } return result, nil }
// Tracker saves info about short URL activities. // GeoIP database can be loaded from // http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz func Tracker(ctx context.Context, cu *trim.CustomURL, addr string) error { c, err := conf.FromContext(ctx) if err != nil { return err } host, _, err := net.SplitHostPort(addr) if err != nil { return err } geo := GeoData{IP: host} record, err := c.GeoDB.City(net.ParseIP(host)) if err != nil { // not critical: skip GeoIP data filling c.L.Error.Println(err) } else { geo.Country = record.Country.Names["en"] geo.City = record.City.Names["en"] geo.Latitude = record.Location.Latitude geo.Longitude = record.Location.Longitude geo.Tz = record.Location.TimeZone } s, err := db.NewSession(c.Conn, true) if err != nil { return err } defer s.Close() coll, err := db.Coll(s, "tracks") if err != nil { return err } err = coll.Insert(bson.M{ "short": cu.String(), "url": cu.Original, "group": cu.Group, "tag": cu.Tag, "geo": geo, "ts": time.Now().UTC(), }) return err }
// MultiLengthen returns short URLs info for slice of links. func MultiLengthen(ctx context.Context, links []string) ([]ChangeResult, error) { var result []ChangeResult c, err := conf.FromContext(ctx) if err != nil { return nil, err } n := len(links) if n > c.Settings.MaxPack { return nil, fmt.Errorf("too big pack size [%v]", n) } s, err := db.CtxSession(ctx) if err != nil { return nil, err } coll, err := db.Coll(s, "urls") if err != nil { return nil, err } for _, link := range links { id, err := Decode(link) if err != nil { c.L.Error.Printf("decode error [%v]: %v", link, err) result = append(result, ChangeResult{Cu: &CustomURL{ID: id}, Err: "invalid value"}) continue } cu := &CustomURL{} err = coll.FindId(id).One(cu) if err != nil { msg := "internal error" if err == mgo.ErrNotFound { msg = "not found" } result = append(result, ChangeResult{Cu: &CustomURL{ID: id}, Err: msg}) continue } result = append(result, ChangeResult{Cu: cu}) } return result, nil }
func TestCreateUser(t *testing.T) { const userName = "******" cfg, err := conf.Parse(test.TcConfigName()) if err != nil { t.Fatalf("invalid behavior") } err = cfg.Validate() if err != nil { t.Fatalf("invalid behavior") } ctx, cancel := context.WithCancel(conf.NewContext(cfg)) defer cancel() s, err := db.NewSession(cfg.Conn, true) if err != nil { t.Fatal(err) } defer s.Close() ctx = db.NewContext(ctx, s) coll, err := db.Coll(s, "users") if err != nil { t.Fatal(err) } _, err = coll.RemoveAll(nil) if err != nil { t.Fatal(err) } err = InitUsers(cfg) if err != nil { t.Fatal(err) } if n, err := coll.Count(); err != nil || n != 2 { t.Errorf("n=%v, err=%v", n, err) } users, err := CreateUsers(ctx, []string{userName}) if err != nil { t.Fatal(err) } if users, err := CreateUsers(ctx, []string{userName}); err == nil { if users[0].Err == "" { t.Error("invalid behavior") } } r := &http.Request{PostForm: url.Values{"token": {users[0].T}}} ctx, err = CheckToken(ctx, r, false) if err != nil { t.Errorf("invalid behavior: %v", err) } ctx, err = Authenticate(ctx) if err != nil { t.Fatal(err) } u, err := ExtractUser(ctx) if err != nil { t.Fatal(err) } if u.String() != userName { t.Error("invalid behavior") } if !u.HasRole("user") { t.Error("invalid behavior") } if u.IsAnonymous() { t.Error("invalid behavior") } _, err = ChangeUsers(ctx, []string{userName}) if err != nil { t.Fatal(err) } if result, err := ChangeUsers(ctx, []string{"bad"}); err != nil { if result[0].Err == "" { t.Error("invalid behavior") } } _, err = DisableUsers(ctx, []string{userName}) if err != nil { t.Fatal(err) } if result, err := DisableUsers(ctx, []string{"bad"}); err != nil { if result[0].Err == "" { t.Error("invalid behavior") } } }
// Import imports short URLs. func Import(ctx context.Context, links map[string]*ReqParams) ([]ChangeResult, error) { var result []ChangeResult c, err := conf.FromContext(ctx) if err != nil { return nil, err } u, err := auth.ExtractUser(ctx) if err != nil { return nil, err } n := len(links) if n > c.Settings.MaxPack { return nil, fmt.Errorf("too big pack size [%v]", n) } s, err := db.CtxSession(ctx) if err != nil { return nil, err } // prepare coll, err := db.Coll(s, "urls") if err != nil { return nil, err } now := time.Now().UTC() for short, param := range links { num, err := Decode(short) if err != nil { result = append(result, ChangeResult{Err: "invalid short URL value"}) continue } cu := &CustomURL{ ID: num, Group: param.Group, Tag: param.Tag, Original: param.Original, User: u.Name, TTL: param.TTL, NotDirect: param.NotDirect, Created: now, Modified: now, Cb: param.Cb, API: param.IsAPI, } // a locking of every insert is not fast // but the pack doesn't lock other operations. err = db.LockURL(s) if err != nil { return nil, err } errIns := coll.Insert(cu) err = db.UnlockURL(s) if err != nil { return nil, err } if errIns != nil { msg := "internal error" if mgo.IsDup(errIns) { msg = "duplicate item" } result = append(result, ChangeResult{Err: msg}) continue } result = append(result, ChangeResult{Cu: cu}) } return result, nil }