// 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")
	}
}
Esempio n. 2
0
// 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")
	}
}
Esempio n. 3
0
// 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
}
Esempio n. 4
0
// 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)
	}
}