# Decide whether to allow the user into the waiting room or not.

if (
  fastly.ff.visits_this_service == 0 && # on edge nodes
  req.restarts == 0 # on first VCL pass
) {
  # Prevent the waiting room cookie from being sent by the client
  unset req.http.waitingroom_new_cookie;

  if (waitingroom_config_enabled()) {
    declare local var.logger_prefix STRING = "syslog " + req.service_id + " " + table.lookup(waitingroom_config, "logger_name") + " :: [WAITINGROOM] ";

    # If you want to allocate waiting room spots based on
    # user ID, set this to the user's verified ID
    declare local var.authed_user_id STRING = client.ip;

    # JUST FOR TESTING IN FIDDLE, DELETE OTHERWISE
    set var.authed_user_id = req.http.Fastly-Client-IP;

    # Determine the percentage of requests to allow though
    declare local var.percentage INTEGER = waitingroom_config_allow_percentage();

    # What decision should we make?
    declare local var.decision STRING;

    # Should we set a cookie?
    declare local var.set_cookie BOOL = true;

    if (var.percentage >= 100) {
      # Allow all through: allow
      set var.decision = "allow";
    } else if (!req.http.Cookie:waiting_room) {
      # No cookie: wait
      set var.decision = "wait";
    } else {
      # Validate the cookie
      declare local var.request_cookie_decoded STRING = digest.base64_decode(req.http.Cookie:waiting_room);
      declare local var.request_cookie_expires INTEGER = std.atoi(subfield(var.request_cookie_decoded, "exp", "&"));
      declare local var.request_cookie_signature STRING = subfield(var.request_cookie_decoded, "sig", "&");
      declare local var.request_cookie_key_id STRING = subfield(var.request_cookie_decoded, "kid", "&");
      declare local var.request_cookie_user_id STRING = subfield(var.request_cookie_decoded, "uid", "&");
      declare local var.request_cookie_decision STRING = subfield(var.request_cookie_decoded, "dec", "&");

      if (var.request_cookie_user_id != var.authed_user_id) {
        # Wrong user ID in cookie: wait
        set var.decision = "wait";
        log var.logger_prefix + "User " + var.authed_user_id + " denied while using a token generated for user " + var.request_cookie_user_id;
      } else if (!table.lookup(waitingroom_signingkeys, var.request_cookie_key_id)) {
        # No signing key for the key id in the cookie: wait
        set var.decision = "wait";
        log var.logger_prefix + "Unable to check signature due to missing key " + var.request_cookie_key_id;
      } else {
        # Check the signature
        declare local var.request_cookie_string_to_sign STRING = "dec=" + var.request_cookie_decision + "&exp=" + var.request_cookie_expires + "&uid=" + var.request_cookie_user_id + "&kid=" + var.request_cookie_key_id;
        declare local var.calculated_signature STRING = digest.hmac_sha256(table.lookup(waitingroom_signingkeys, var.request_cookie_key_id), var.request_cookie_string_to_sign);
        if (digest.secure_is_equal(var.request_cookie_signature, var.calculated_signature)) {
          # Valid cookie, use the previously-made decision from the cookie
          set var.decision = var.request_cookie_decision;
          set var.set_cookie = false;
        } else {
          # Invalid cookie: wait
          set var.decision = "wait";
        }
      }

      # Actions for cookies that have reached their expiry time
      if (time.is_after(now, std.integer2time(var.request_cookie_expires))) {
        if (var.decision == "allow") {
          # Previously allowed, but expired: wait
          log var.logger_prefix + "Expired allow token reverted to wait";
          set var.decision = "wait";
          set var.set_cookie = true;
        } else if (var.decision == "wait") {
          # Wait, but expired: allow or re-wait
          declare local var.seed INTEGER = std.strtol(substr(var.request_cookie_signature, 0, 8), 16);
          set var.decision = if (randombool_seeded(var.percentage, 100, var.seed), "allow", "re-wait");
          set var.set_cookie = true;
        }
      }
    }

    # We have now made a decision
    log var.logger_prefix + "Waiting room state: " + var.decision;

    if (var.set_cookie) {
      # When is the next entrance attempt?
      declare local var.expires INTEGER = now;
      if (var.decision == "allow") {
        # If allow, expire cookie after the configured allow period
        set var.expires += waitingroom_config_allow_period_timeout();
      } else {
        # Otherwise, expire cookie after the configured wait period
        declare local var.duration INTEGER = waitingroom_config_wait_period_duration();

        # For "wait" decisions, calculate expiration time by:
        # 1. Add one duration period
        # 2. Add a random portion of the duration
        # Adding random jitter prevents all users from a time window being
        # allowed in simultaneously, which could overwhelm the origin
        set var.expires += var.duration;
        set var.expires += randomint(0, var.duration);
      }

      # Always set a cookie to track the user's waiting room state
      declare local var.key_id STRING = table.lookup(waitingroom_config, "active_key", "key1");
      declare local var.string_to_sign STRING = "dec=" + if (var.decision == "allow", "allow", "wait") + "&exp=" + var.expires + "&uid=" + var.authed_user_id + "&kid=" + var.key_id;
      declare local var.signature STRING = digest.hmac_sha256(table.lookup(waitingroom_signingkeys, var.key_id), var.string_to_sign);
      set req.http.waitingroom_new_cookie = "waiting_room=" + digest.base64(var.string_to_sign + "&sig=" + var.signature) + "; path=/; max-age=" + table.lookup(waitingroom_config, "cookie_lifetime", "7200") + "; domain=" + table.lookup(waitingroom_config, "cookie_domain", req.http.Host) + "; secure; HttpOnly";
    }
    
    # Prevent normal request routing if decision is not 'allow'
    if (var.decision == "wait") {
      error 718 "waitingroom:startwaiting";
    } else if (var.decision == "re-wait") {
      error 718 "waitingroom:keepwaiting";
    }
  }
}