// tracker saves info customer short URL request. func tracker(ch <-chan *CuInfo) { var wg sync.WaitGroup for cui := range ch { c, err := conf.FromContext(cui.ctx) if err != nil { logger.Printf("tracker wasn't called, error: %v", err) continue } if !c.Settings.TrackOn { c.L.Debug.Println("request tracking is disabled") continue } wg.Add(2) // tracker handler go func() { defer wg.Done() if err := stats.Tracker(cui.ctx, cui.cu, cui.addr); err != nil { c.L.Error.Println(err) } }() // callback handler go func() { defer wg.Done() // anonymous callbacks will not be handled if cui.cu.User != auth.Anonymous { if err := stats.Callback(cui.ctx, cui.cu); err != nil { c.L.Error.Println(err) } } }() wg.Wait() } }
// HandlerIndex returns index web page. func HandlerIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler { data := map[string]string{} c, err := conf.FromContext(ctx) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } tpl, err := c.CacheTpl("base", "base.html", "index.html") if err != nil { return ErrHandler{err, http.StatusInternalServerError} } if r.Method == "POST" { p, err := validateParams(r) if err != nil { c.L.Error.Println(err) data["Error"] = "Invalid data." err = tpl.ExecuteTemplate(w, "base", data) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } return ErrHandler{nil, http.StatusOK} } params := []*trim.ReqParams{p} cus, err := trim.Shorten(ctx, params) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } data["Result"] = c.Address(cus[0].String()) } err = tpl.ExecuteTemplate(w, "base", data) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } return ErrHandler{nil, http.StatusOK} }
// CheckToken verifies the token, checks length and hash value. // If the token is valid, then its 2nd part (hash) will be added to the returned context. // It also marks empty token as ErrAnonymous error. func CheckToken(ctx context.Context, r *http.Request, api bool) (context.Context, error) { var token string if api { if bearer := r.Header.Get("Authorization"); bearer != "" && !strings.HasPrefix(bearer, "Bearer") { return ctx, errors.New("invalid authorization data format") } else if bearer != "" { // split "Bearer" bearer = strings.Trim(bearer, ";") token = bearer[6:] } } else { token = r.PostFormValue("token") } l := len(token) if l == 0 { return setTokenContext(ctx, ""), ErrAnonymous } hexToken, err := hex.DecodeString(token) if err != nil { return ctx, err } c, err := conf.FromContext(ctx) if err != nil { return ctx, err } n := len(hexToken) if !EqualBytes(tokenHash(hexToken[:n/2], c), hexToken[n/2:]) { return ctx, errors.New("invalid token") } // hex.EncodeToString(h) == token[l/2:] return setTokenContext(ctx, token[l/2:]), nil }
// HandlerNoWebIndex works like version but return only short link text. func HandlerNoWebIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler { c, err := conf.FromContext(ctx) if err != nil { fmt.Fprintf(w, "error: internal error\n") return ErrHandler{nil, http.StatusOK} } u := r.FormValue("u") if u == "" { fmt.Fprintf(w, "error: no data\n") return ErrHandler{nil, http.StatusOK} } param := &trim.ReqParams{ Original: u, Tag: "", NotDirect: false, TTL: nil, } err = param.Valid() if err != nil { fmt.Fprintf(w, "error: invalid url\n") return ErrHandler{nil, http.StatusOK} } params := []*trim.ReqParams{param} cus, err := trim.Shorten(ctx, params) if err != nil { fmt.Fprintf(w, "error: internal error\n") return ErrHandler{nil, http.StatusOK} } fmt.Fprintf(w, "%s\n", c.Address(cus[0].String())) return ErrHandler{nil, http.StatusOK} }
// HandlerTest handles test GET request. func HandlerTest(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler { c, err := conf.FromContext(ctx) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } coll, err := db.C(ctx, "tests") if err != nil { return ErrHandler{err, http.StatusInternalServerError} } command := r.FormValue("write") switch { case c.Debug && command == "add": err = coll.Insert(bson.M{"ts": time.Now()}) case c.Debug && command == "del": err = coll.Remove(nil) } if err != nil && err != mgo.ErrNotFound { return ErrHandler{err, http.StatusInternalServerError} } n, err := coll.Count() if err != nil { return ErrHandler{err, http.StatusInternalServerError} } u, err := auth.ExtractUser(ctx) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } c.L.Debug.Printf("user=%v", u) fmt.Fprintf(w, "found %v items", n) return ErrHandler{nil, http.StatusOK} }
// HandlerRedirect searches saved original URL by a short one. func HandlerRedirect(ctx context.Context, short string, r *http.Request) (string, error) { cu, err := trim.Lengthen(ctx, short) if err != nil { return "", err } c, err := conf.FromContext(ctx) if err != nil { return "", err } if c.Settings.TrackOn { ch, err := TrackerChan(ctx) if err != nil { // tracker's error is not critical // so only print it here c.L.Error.Println(err) } else { cui := &CuInfo{ctx, cu, r.RemoteAddr} if headProxy := c.Settings.TrackProxy; headProxy != "" { if proxyIP := r.Header.Get(headProxy); proxyIP != "" { _, port, _ := net.SplitHostPort(cui.addr) cui.addr = net.JoinHostPort(proxyIP, port) } } ch <- cui } } // TODO: check direct redirect return cu.Original, nil }
// HandlerInfo returns main API info. func HandlerInfo(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler { c, err := conf.FromContext(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } user, err := auth.ExtractUser(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } result := &infoResponse{ Err: 0, Msg: "ok", Result: []infoResponseItem{ infoResponseItem{ Version: Version, AuthOk: !user.IsAnonymous(), PackSize: c.Settings.MaxPack, }, }, } b, err := json.Marshal(result) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "%s", b) return core.ErrHandler{Err: nil, Status: http.StatusOK} }
// 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 }
// HandlerGet returns info about short URLs. func HandlerGet(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler { var grs []getRequest c, err := conf.FromContext(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } defer r.Body.Close() decoder := json.NewDecoder(r.Body) err = decoder.Decode(&grs) if (err != nil) && (err != io.EOF) { return core.ErrHandler{Err: err, Status: http.StatusBadRequest} } links := []string{} for i := range grs { link, err := core.TrimAddress(grs[i].Short) if err != nil { c.L.Debug.Printf("invalid short URL [%v] was skipped: %v", link, err) continue } if l, ok := trim.IsShort(link); ok { links = append(links, l) } } if len(links) == 0 { return core.ErrHandler{Err: ErrEmptyRequest, Status: http.StatusNoContent} } cus, err := trim.MultiLengthen(ctx, links) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } items := make([]addResponseItem, len(cus)) for i, cu := range cus { id := cu.Cu.String() items[i] = addResponseItem{ ID: id, Short: c.Address(id), Original: cu.Cu.Original, Err: cu.Err, } } result := &addResponse{ Err: 0, Msg: "ok", Result: items, } b, err := json.Marshal(result) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "%s", b) return core.ErrHandler{Err: nil, Status: http.StatusOK} }
// RunWorkers runs tracker workers. func RunWorkers(ctx context.Context) (context.Context, error) { c, err := conf.FromContext(ctx) if err != nil { return ctx, err } ch := make(chan *CuInfo, trackerBuffer) for i := 0; i < c.Settings.Trackers; i++ { go func() { tracker(ch) }() } c.L.Info.Printf("run %v trackers", c.Settings.Trackers) return context.WithValue(ctx, trackerKey, ch), nil }
// HandlerNotFound returns "not found" web page. func HandlerNotFound(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler { c, err := conf.FromContext(ctx) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } tpl, err := c.CacheTpl("error", "base.html", "error.html") if err != nil { return ErrHandler{err, http.StatusInternalServerError} } data := map[string]string{ "Message": "The page is not found.", "Error": "", } err = tpl.ExecuteTemplate(w, "base", data) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } return ErrHandler{nil, http.StatusOK} }
// 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 }
// HandlerError returns "error" web page. func HandlerError(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler { c, err := conf.FromContext(ctx) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } tpl, err := c.CacheTpl("error", "base.html", "error.html") if err != nil { return ErrHandler{err, http.StatusInternalServerError} } data := map[string]string{ "Message": "Error", "Error": "The error occurred, probably due to internal server problems.", } err = tpl.ExecuteTemplate(w, "base", data) if err != nil { return ErrHandler{err, http.StatusInternalServerError} } return ErrHandler{nil, http.StatusOK} }
// 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 }
// HandlerAdd creates new short URL. func HandlerAdd(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler { c, err := conf.FromContext(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } defer r.Body.Close() params, err := validateAddParams(r) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusBadRequest} } cus, err := trim.Shorten(ctx, params) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } items := make([]addResponseItem, len(cus)) for i, cu := range cus { id := cu.String() items[i] = addResponseItem{ ID: id, Short: c.Address(id), Original: cu.Original, } } result := &addResponse{ Err: 0, Msg: "ok", Result: items, } b, err := json.Marshal(result) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "%s", b) return core.ErrHandler{Err: nil, Status: http.StatusOK} }
// HandlerExport exports URLs data. func HandlerExport(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler { const ( layout = "2006-01-02" pageSize = 1000 ) c, err := conf.FromContext(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } user, err := auth.ExtractUser(ctx) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } if !user.HasRole("admin") { return core.ErrHandler{Err: errors.New("permissions error"), Status: http.StatusForbidden} } defer r.Body.Close() exp := &exportRequest{} decoder := json.NewDecoder(r.Body) err = decoder.Decode(exp) if (err != nil) && (err != io.EOF) { return core.ErrHandler{Err: err, Status: http.StatusBadRequest} } period, err := exp.parsePeriod() if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } filter := trim.Filter{ Group: exp.Group, Tag: exp.Tag, Period: period, Active: exp.Active, Page: exp.Page, PageSize: pageSize, } cus, pages, err := trim.Export(ctx, filter) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } items := make([]exportResponseItem, len(cus)) for i, cu := range cus { id := cu.String() items[i] = exportResponseItem{ ID: id, Short: c.Address(id), Original: cu.Original, Group: cu.Group, Tag: cu.Tag, Created: cu.Created.UTC().Format(layout), } } result := &exportResponse{ Err: 0, Msg: "ok", Pages: pages, Result: items, } b, err := json.Marshal(result) if err != nil { return core.ErrHandler{Err: err, Status: http.StatusInternalServerError} } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "%s", b) return core.ErrHandler{Err: nil, Status: http.StatusOK} }
// 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 }