// Load a YAML config file and put values in a Config object func LoadConfigYAML(filename string) (*Config, error) { log.WithField("filename", filename).Debug("loading configuration from YAML") // load config from file from the working directory viper.SetConfigName(filename) viper.SetConfigType("yaml") viper.AddConfigPath(".") err := viper.ReadInConfig() if err != nil { // log the error log.WithFields(log.Fields{ "filename": filename, "error": err}).Error("config file not found") return nil, errors.New("config file not found") } // overrides with environment variables (for debug, or to use docker links) if os.Getenv("HOST") != "" { viper.Set("host", os.Getenv("HOST")) } if os.Getenv("PORT") != "" { viper.Set("port", os.Getenv("PORT")) } if os.Getenv("PROTO") != "" { viper.Set("proto", os.Getenv("PROTO")) } if os.Getenv("REDIS_PORT_6379_TCP_PORT") != "" { viper.Set("redisPort", os.Getenv("REDIS_PORT_6379_TCP_PORT")) } if os.Getenv("REDIS_PORT_6379_TCP_ADDR") != "" { viper.Set("redisHost", os.Getenv("REDIS_PORT_6379_TCP_ADDR")) } if os.Getenv("REDIS_DB") != "" { viper.Set("redisDB", os.Getenv("REDIS_DB")) } if os.Getenv("REDIS_PASSWORD") != "" { viper.Set("redisPassword", os.Getenv("REDIS_PASSWORD")) } config := Config{ TokenLength: viper.GetInt("tokenLength"), ReachTimeoutMs: viper.GetInt("reachTimeoutMs"), ExpirationTimeMonths: viper.GetInt("expirationTimeMonths"), Host: viper.GetString("host"), Port: viper.GetInt("port"), Proto: viper.GetString("proto"), RedisHost: viper.GetString("redisHost"), RedisPort: viper.GetInt("redisPort"), RedisDB: viper.GetInt("redisDB"), RedisPassword: viper.GetString("redisPassword")} return &config, nil }
// 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) } }