Exemple #1
0
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)
	}
}