diff --git a/go.mod b/go.mod index 285ee79..b692ab4 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-swagger/go-swagger v0.23.0 github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 // indirect github.com/gorilla/mux v1.7.3 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/sour-is/go-assetfs v1.0.0 github.com/spf13/viper v1.6.2 github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e diff --git a/go.sum b/go.sum index 7dcf92f..1d42e17 100644 --- a/go.sum +++ b/go.sum @@ -251,6 +251,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/src/routes/shorturl.go b/src/routes/shorturl.go new file mode 100644 index 0000000..ff4c61b --- /dev/null +++ b/src/routes/shorturl.go @@ -0,0 +1,120 @@ +package routes + +import ( + "net/http" + "net/url" + "regexp" + "time" + + "github.com/gorilla/mux" + "github.com/patrickmn/go-cache" + "sour.is/x/toolbox/httpsrv" + "sour.is/x/toolbox/uuid" +) + +func init() { + s := NewShortManager(365 * 24 * time.Hour) + + httpsrv.HttpRegister("short", httpsrv.HttpRoutes{ + {Name: "getShort", Method: "GET", Pattern: "/s/{id}", HandlerFunc: s.getShort}, + {Name: "putShort", Method: "PUT", Pattern: "/s/{id}", HandlerFunc: s.putShort}, + }) +} + +type shortManager struct { + defaultExpire time.Duration + db *cache.Cache +} + +type shortURL struct { + ID string + URL string + Secret string +} + +func NewShortManager(defaultExpire time.Duration) *shortManager { + return &shortManager{ + defaultExpire: defaultExpire, + db: cache.New(defaultExpire, defaultExpire/10), + } +} + +func (s *shortManager) GetURL(id string) *shortURL { + if u, ok := s.db.Get(id); ok { + if url, ok := u.(*shortURL); ok { + return url + } + } + + return nil +} +func (s *shortManager) PutURL(id string, url *shortURL) { + s.db.SetDefault(id, url) +} + +func (s *shortManager) getShort(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + id := vars["id"] + url := s.GetURL(id) + + if url == nil { + httpsrv.WriteError(w, 404, "not found") + return + } + + w.Header().Set("Location", url.URL) + w.WriteHeader(http.StatusFound) +} + +func (s *shortManager) putShort(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + vars := mux.Vars(r) + + id := vars["id"] + secret := r.FormValue("secret") + u, err := url.Parse(r.FormValue("url")) + if err != nil { + httpsrv.WriteError(w, 400, "bad url") + return + } + + short := s.GetURL(id) + + if short == nil { + short = newshort(id, secret, u.String()) + + s.PutURL(short.ID, short) + httpsrv.WriteObject(w, 200, short) + + return + } + + if secret == "" { + httpsrv.WriteError(w, 401, "no auth") + return + } + + if secret != short.Secret { + httpsrv.WriteError(w, 403, "forbidden") + return + } + + short.URL = u.String() + + s.PutURL(short.ID, short) + httpsrv.WriteObject(w, 200, short) +} + +func newshort(id, secret, u string) *shortURL { + m, err := regexp.MatchString("[a-z-]{1,64}", id) + if id == "" || !m || err != nil { + id = uuid.V4() + } + m, err = regexp.MatchString("[a-z-]{1,64}", secret) + if secret == "" || !m || err != nil { + secret = uuid.V4() + } + return &shortURL{ID: id, Secret: secret, URL: u} +}