func main() { // setUp the logger toDefer := setUpLog() // the returned function is to defer, used to close log file on exit defer toDefer() log.Info("starting") // Load the configuration from the config.yaml file conf, err := confighelper.LoadConfigYAML("config") if err != nil { log.WithError(err).Fatal("incorrect config, exiting") return // do not exit since the log file still has to be closed by a defered function } log.Info("configuration loaded") // create the redis client redisClient := redis.NewClient(&redis.Options{ Addr: conf.RedisHost + ":" + strconv.Itoa(conf.RedisPort), Password: conf.RedisPassword, // no password set DB: int64(conf.RedisDB), // use default DB }) // create the router r := mux.NewRouter() // Routes var valueRegexp string = "[0-9a-zA-Z]{" + strconv.Itoa(conf.TokenLength) + "}" r.HandleFunc("/{token:"+valueRegexp+"}", handlers.RedirectHandler(redisClient, conf)). Methods("GET") r.HandleFunc("/shortlink", handlers.CreateHandler(redisClient, conf)). Methods("POST").Headers("Content-Type", "application/json") r.HandleFunc("/admin/{token:"+valueRegexp+"}", handlers.AdminHandler(redisClient, conf)). Methods("GET") // Bind to a port and pass our router in log.Info("starting the router...") err = http.ListenAndServe(":"+strconv.Itoa(conf.Port), r) if err != nil { log.WithError(err).Fatal("could not start the router, exiting") return } }
// factory to create the handler func RedirectHandler(redisClient *redis.Client, conf *confighelper.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // debug log log.WithField("request", r).Debug("redirect request received") // get the path variable to get the token vars := mux.Vars(r) token := vars["token"] // get the redirection url for this token url, err := redisClient.HGet(token, "url").Result() if err != nil && err.Error() != "redis: nil" { log.WithError(err).Error("error while retrieving the redirection url from redis") w.WriteHeader(500) // server error return } else if url == "" { // "redis: nil" is the error is the key is not found log.WithField("token", token).Info("token not found") w.WriteHeader(404) // not found return } // consider that url in Redis is correct from here // increment count count, err := redisClient.HIncrBy(token, "count", 1).Result() if err != nil { log.WithError(err).Error("error while incrementing count") // no server error, we can still redirect the user } // redirect w.Header().Set("Location", url) // avoid caching the page on the client side to not bias the counts w.Header().Set("cache-control", "private, max-age=0, no-cache") w.WriteHeader(301) // moved permanently log.WithFields(log.Fields{ "token": token, "count": count, "url": url}).Info("redirect request served") } }
// factory to create the handler func AdminHandler(redisClient *redis.Client, conf *confighelper.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // debug log log.WithField("request", r).Debug("admin request received") // get the path variable to get the token vars := mux.Vars(r) token := vars["token"] // get the redirection url for this token value, err := redisClient.HGetAllMap(token).Result() if err != nil && err.Error() != "redis: nil" { log.WithError(err).Error("error while retrieving the token infos from redis") w.WriteHeader(500) // server error return } else if value == nil { // "redis: nil" is the error is the key is not found log.WithField("token", token).Info("token not found") w.WriteHeader(404) // not found return } log.WithFields(log.Fields{ "token": token, "value": value}).Debug("mapped values retrieved") response := admin_response_body{ Url: value["url"], CreationTime: value["creationTime"], Count: value["count"], } encoder := json.NewEncoder(w) encoder.Encode(response) // avoid caching the page on the client side to not bias the counts w.Header().Set("cache-control", "private, max-age=0, no-cache") log.WithField("token", token).Info("admin request served") } }
// factory to create the handler func CreateHandler(redisClient *redis.Client, conf *confighelper.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // log request for debugging purposes (eg: crash, ...) log.WithField("request", r).Debug("create request received") // Unmarshall JSON to structure decoder := json.NewDecoder(r.Body) // unmarshall JSON var body create_request_body err := decoder.Decode(&body) if err != nil { log.WithError(err).Error("can not unmarshall JSON body of create request, returning 400: Bad Request") // return a 400: Bad Request response w.WriteHeader(400) return } log.WithField("request", r).Debug("create request received") // check that the url exists in the structure, and that it is correct if body.Url == "" || !urlhelper.IsValid(body.Url) { log.Error("incorrect url in body of create request, returning 400: Bad Request") w.WriteHeader(400) return } // check that URL is reachable (no intranet, no tor url, ...) if !urlhelper.IsReachable(body.Url, conf.ReachTimeoutMs) { log.WithField("url", body.Url).Error("unreachable URL submitted, returning 400 bad request") w.WriteHeader(400) return } // validate the suggestion (only letters and digits) if !validateToken(body.Token, conf.TokenLength) { log.WithField("token", body.Token).Error("invalid custom token, aborting") w.WriteHeader(400) return } // create a random token generator randomTokenGenerator := randomTokenGenerator(body.Token, conf.TokenLength) var token string // try to insert with a new random token as long as the lock on the token cannot be acquired for { token, err = randomTokenGenerator() if err != nil { // we tried to generate too many token, abort log.WithError(err).Error("too many collisions for token, aborting") w.WriteHeader(500) return } // use HSetNX to get lock on the Token lockAcquired, err := redisClient.HSetNX(token, "url", body.Url).Result() if lockAcquired { // debug log log.WithField("token", token).Debug("lock was acquired") // lock could be acquired: we reserved the token ! // proceed by setting other fields _, err = redisClient.HMSet(token, "creationTime", strconv.FormatInt(time.Now().Unix(), 10), "count", "0").Result() // set expiration time in 3 months redisClient.ExpireAt(token, time.Now().AddDate(0, conf.ExpirationTimeMonths, 0)) break // leave the loop: don't try to generate a new token } // if there was an error while setting in the map (more than just key already present) if err != nil { log.WithError(err).Error("can not set the entry in Redis, aborting") w.WriteHeader(500) return } // debug log log.WithField("token", token).Debug("could not acquire lock, retrying if allowed") } // log success log.WithFields(log.Fields{ "url": body.Url, "token": token}).Info("new short link created") // generate response response := create_response_body{ Url: urlhelper.Build(conf.Proto, conf.Host, conf.Port, token), } w.WriteHeader(201) // return 201: created encoder := json.NewEncoder(w) encoder.Encode(response) } }