Merge "Enable PATCH method in crow"
diff --git a/crow/include/crow/http_codes.h b/crow/include/crow/http_codes.h
new file mode 100644
index 0000000..9eff020
--- /dev/null
+++ b/crow/include/crow/http_codes.h
@@ -0,0 +1,14 @@
+#pragma once
+
+enum class HttpRespCode{
+  OK = 200,
+  CREATED = 201,
+  ACCEPTED = 202,
+  NO_CONTENT = 204,
+  BAD_REQUEST = 400,
+  UNAUTHORIZED = 401,
+  FORBIDDEN = 403,
+  NOT_FOUND = 404,
+  METHOD_NOT_ALLOWED = 405,
+  INTERNAL_ERROR = 500
+};
diff --git a/crow/include/crow/http_response.h b/crow/include/crow/http_response.h
index 83684cc..e90bfdc 100644
--- a/crow/include/crow/http_response.h
+++ b/crow/include/crow/http_response.h
@@ -9,6 +9,7 @@
 namespace crow {
 template <typename Adaptor, typename Handler, typename... Middlewares>
 class Connection;
+
 struct response {
   template <typename Adaptor, typename Handler, typename... Middlewares>
   friend class crow::Connection;
diff --git a/crow/include/crow/utility.h b/crow/include/crow/utility.h
index 6046909..e2e9c16 100644
--- a/crow/include/crow/utility.h
+++ b/crow/include/crow/utility.h
@@ -516,5 +516,90 @@
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
 }
 
+// TODO this is temporary and should be deleted once base64 is refactored out of
+// crow
+inline bool base64_decode(const std::string& input, std::string& output) {
+  static const char nop = -1;
+  // See note on encoding_data[] in above function
+  static const char decoding_data[] = {
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 62,  nop,
+      nop, nop, 63,  52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  nop, nop,
+      nop, nop, nop, nop, nop, 0,   1,   2,   3,   4,   5,   6,   7,   8,   9,
+      10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,
+      25,  nop, nop, nop, nop, nop, nop, 26,  27,  28,  29,  30,  31,  32,  33,
+      34,  35,  36,  37,  38,  39,  40,  41,  42,  43,  44,  45,  46,  47,  48,
+      49,  50,  51,  nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+      nop};
+
+  size_t input_length = input.size();
+
+  // allocate space for output string
+  output.clear();
+  output.reserve(((input_length + 2) / 3) * 4);
+
+  // for each 4-bytes sequence from the input, extract 4 6-bits sequences by
+  // droping first two bits
+  // and regenerate into 3 8-bits sequences
+
+  for (size_t i = 0; i < input_length; i++) {
+    char base64code0;
+    char base64code1;
+    char base64code2 = 0;  // initialized to 0 to suppress warnings
+    char base64code3;
+
+    base64code0 = decoding_data[static_cast<int>(input[i])];  // NOLINT
+    if (base64code0 == nop) {  // non base64 character
+      return false;
+    }
+    if (!(++i < input_length)) {  // we need at least two input bytes for first
+                                  // byte output
+      return false;
+    }
+    base64code1 = decoding_data[static_cast<int>(input[i])];  // NOLINT
+    if (base64code1 == nop) {  // non base64 character
+      return false;
+    }
+    output +=
+        static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3));
+
+    if (++i < input_length) {
+      char c = input[i];
+      if (c == '=') {  // padding , end of input
+        return (base64code1 & 0x0f) == 0;
+      }
+      base64code2 = decoding_data[static_cast<int>(input[i])];  // NOLINT
+      if (base64code2 == nop) {  // non base64 character
+        return false;
+      }
+      output += static_cast<char>(((base64code1 << 4) & 0xf0) |
+                                  ((base64code2 >> 2) & 0x0f));
+    }
+
+    if (++i < input_length) {
+      char c = input[i];
+      if (c == '=') {  // padding , end of input
+        return (base64code2 & 0x03) == 0;
+      }
+      base64code3 = decoding_data[static_cast<int>(input[i])];  // NOLINT
+      if (base64code3 == nop) {  // non base64 character
+        return false;
+      }
+      output += static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3));
+    }
+  }
+
+  return true;
+}
+
 }  // namespace utility
 }  // namespace crow
diff --git a/include/persistent_data_middleware.hpp b/include/persistent_data_middleware.hpp
index aee2407..b52e225 100644
--- a/include/persistent_data_middleware.hpp
+++ b/include/persistent_data_middleware.hpp
@@ -15,19 +15,28 @@
 namespace crow {
 
 namespace PersistentData {
+
+enum class PersistenceType {
+  TIMEOUT,        // User session times out after a predetermined amount of time
+  SINGLE_REQUEST  // User times out once this request is completed.
+};
+
 struct UserSession {
   std::string unique_id;
   std::string session_token;
   std::string username;
   std::string csrf_token;
   std::chrono::time_point<std::chrono::steady_clock> last_updated;
+  PersistenceType persistence;
 };
 
 void to_json(nlohmann::json& j, const UserSession& p) {
-  j = nlohmann::json{{"unique_id", p.unique_id},
-                     {"session_token", p.session_token},
-                     {"username", p.username},
-                     {"csrf_token", p.csrf_token}};
+  if (p.persistence != PersistenceType::SINGLE_REQUEST) {
+    j = nlohmann::json{{"unique_id", p.unique_id},
+                       {"session_token", p.session_token},
+                       {"username", p.username},
+                       {"csrf_token", p.csrf_token}};
+  }
 }
 
 void from_json(const nlohmann::json& j, UserSession& p) {
@@ -51,7 +60,11 @@
 
 class SessionStore {
  public:
-  const UserSession& generate_user_session(const std::string& username) {
+  const UserSession& generate_user_session(
+      const std::string& username,
+      PersistenceType persistence = PersistenceType::TIMEOUT) {
+    // TODO(ed) find a secure way to not generate session identifiers if
+    // persistence is set to SINGLE_REQUEST
     static constexpr std::array<char, 62> alphanum = {
         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
         'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
@@ -59,7 +72,7 @@
         'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
         'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
 
-    // entropy: 30 characters, 62 possibilies.  log2(62^30) = 178 bits of
+    // entropy: 30 characters, 62 possibilities.  log2(62^30) = 178 bits of
     // entropy.  OWASP recommends at least 60
     // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
     std::string session_token;
@@ -80,12 +93,14 @@
     for (int i = 0; i < unique_id.size(); ++i) {
       unique_id[i] = alphanum[dist(rd)];
     }
+
     const auto session_it = auth_tokens.emplace(
         session_token,
         std::move(UserSession{unique_id, session_token, username, csrf_token,
-                              std::chrono::steady_clock::now()}));
+                              std::chrono::steady_clock::now(), persistence}));
     const UserSession& user = (session_it).first->second;
-    need_write_ = true;
+    // Only need to write to disk if session isn't about to be destroyed.
+    need_write_ = persistence == PersistenceType::TIMEOUT;
     return user;
   }
 
diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp
index 81d9e58..bbbaa15 100644
--- a/include/token_authorization_middleware.hpp
+++ b/include/token_authorization_middleware.hpp
@@ -5,119 +5,190 @@
 #include <webassets.hpp>
 #include <random>
 #include <crow/app.h>
+#include <crow/http_codes.h>
 #include <crow/http_request.h>
 #include <crow/http_response.h>
 #include <boost/bimap.hpp>
 #include <boost/container/flat_set.hpp>
+
 namespace crow {
 
 namespace TokenAuthorization {
-struct User {};
 
 class Middleware {
  public:
-  struct context {};
+  struct context {
+    const crow::PersistentData::UserSession* session;
+  };
   template <typename AllContext>
   void before_handle(crow::request& req, response& res, context& ctx,
                      AllContext& allctx) {
-    auto return_unauthorized = [&req, &res]() {
-      res.code = 401;
+    auto& sessions =
+        allctx.template get<crow::PersistentData::Middleware>().sessions;
+    std::string auth_header = req.get_header_value("Authorization");
+    if (auth_header != "") {
+      // Reject any kind of auth other than basic or token
+      if (boost::starts_with(auth_header, "Basic ")) {
+        ctx.session = perform_basic_auth(auth_header, sessions);
+      } else if (boost::starts_with(auth_header, "Token ")) {
+        ctx.session = perform_token_auth(auth_header, sessions);
+      }
+    } else if (req.headers.count("X-Auth-Token") == 1) {
+      ctx.session = perform_xtoken_auth(req, sessions);
+    } else if (req.headers.count("Cookie") == 1) {
+      ctx.session = perform_cookie_auth(req, sessions);
+    }
+
+    if (ctx.session == nullptr && !is_on_whitelist(req)) {
+      CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
+      res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
+      res.add_header("WWW-Authenticate", "Basic");
       res.end();
-    };
+      return;
+    }
 
-    if (crow::webassets::routes.find(req.url) !=
-        crow::webassets::routes.end()) {
-      // TODO this is total hackery to allow the login page to work before the
-      // user is authenticated.  Also, it will be quite slow for all pages
-      // instead of a one time hit for the whitelist entries.  Ideally, this
-      // should be done in the url router handler, with tagged routes for the
-      // whitelist entries. Another option would be to whitelist a minimal form
-      // based page that didn't load the full angular UI until after login
-    } else if (req.url == "/login" || req.url == "/redfish/v1" ||
-               req.url == "/redfish/v1/" ||
-               req.url == "/redfish/v1/$metadata" ||
-               (req.url == "/redfish/v1/SessionService/Sessions/" &&
-                req.method == "POST"_method)) {
-    } else {
-      // Normal, non login, non static file request
-      // Check for an authorization header, reject if not present
-      std::string auth_key;
-      bool require_csrf = true;
-      if (req.headers.count("Authorization") == 1) {
-        std::string auth_header = req.get_header_value("Authorization");
-        // If the user is attempting any kind of auth other than token, reject
-        if (!boost::starts_with(auth_header, "Token ")) {
-          return_unauthorized();
-          return;
-        }
-        auth_key = auth_header.substr(6);
-        require_csrf = false;
-      } else if (req.headers.count("X-Auth-Token") == 1) {
-        auth_key = req.get_header_value("X-Auth-Token");
-        require_csrf = false;
-      } else {
-        int count = req.headers.count("Cookie");
-        if (count == 1) {
-          auto& cookie_value = req.get_header_value("Cookie");
-          auto start_index = cookie_value.find("SESSION=");
-          if (start_index != std::string::npos) {
-            start_index += 8;
-            auto end_index = cookie_value.find(";", start_index);
-            if (end_index == std::string::npos) {
-              end_index = cookie_value.size();
-            }
-            auth_key =
-                cookie_value.substr(start_index, end_index - start_index);
-          }
-        }
-        require_csrf = true;  // Cookies require CSRF
-      }
-      if (auth_key.empty()) {
-        res.code = 400;
-        res.end();
-        return;
-      }
-      auto& data_mw = allctx.template get<PersistentData::Middleware>();
-      const PersistentData::UserSession* session =
-          data_mw.sessions->login_session_by_token(auth_key);
-      if (session == nullptr) {
-        return_unauthorized();
-        return;
-      }
+    // TODO get user privileges here and propagate it via MW context
+    // else let the request continue unharmed
+  }
 
-      if (require_csrf) {
-        // RFC7231 defines methods that need csrf protection
-        if (req.method != "GET"_method) {
-          const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
-          // Make sure both tokens are filled
-          if (csrf.empty() || session->csrf_token.empty()) {
-            return_unauthorized();
-            return;
-          }
-          // Reject if csrf token not available
-          if (csrf != session->csrf_token) {
-            return_unauthorized();
-            return;
-          }
-        }
-      }
+  template <typename AllContext>
+  void after_handle(request& req, response& res, context& ctx,
+                    AllContext& allctx) {
+    // TODO(ed) THis should really be handled by the persistent data middleware,
+    // but because it is upstream, it doesn't have access to the session
+    // information.  Should the data middleware persist the current user
+    // session?
+    if (ctx.session != nullptr &&
+        ctx.session->persistence ==
+            crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
+      auto& session_store =
+          allctx.template get<crow::PersistentData::Middleware>().sessions;
 
-      if (req.url == "/logout" && req.method == "POST"_method) {
-        data_mw.sessions->remove_session(session);
-        res.code = 200;
-        res.end();
-        return;
-      }
-
-      // else let the request continue unharmed
+      session_store->remove_session(ctx.session);
     }
   }
 
-  void after_handle(request& req, response& res, context& ctx) {
-    // Do nothing
+ private:
+  const crow::PersistentData::UserSession* perform_basic_auth(
+      const std::string& auth_header,
+      crow::PersistentData::SessionStore* sessions) const {
+    CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
+
+    std::string auth_data;
+    std::string param = auth_header.substr(strlen("Basic "));
+    if (!crow::utility::base64_decode(param, auth_data)) {
+      return nullptr;
+    }
+    std::size_t separator = auth_data.find(':');
+    if (separator == std::string::npos) {
+      return nullptr;
+    }
+
+    std::string user = auth_data.substr(0, separator);
+    separator += 1;
+    if (separator > auth_data.size()) {
+      return nullptr;
+    }
+    std::string pass = auth_data.substr(separator);
+
+    CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
+
+    if (!pam_authenticate_user(user, pass)) {
+      return nullptr;
+    }
+
+    // TODO(ed) generate_user_session is a little expensive for basic
+    // auth, as it generates some random identifiers that will never be
+    // used.  This should have a "fast" path for when user tokens aren't
+    // needed.
+    // This whole flow needs to be revisited anyway, as we can't be
+    // calling directly into pam for every request
+    return &(sessions->generate_user_session(
+        user, crow::PersistentData::PersistenceType::SINGLE_REQUEST));
   }
 
-  boost::container::flat_set<std::string> allowed_routes;
+  const crow::PersistentData::UserSession* perform_token_auth(
+      const std::string& auth_header,
+      crow::PersistentData::SessionStore* sessions) const {
+    CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
+
+    std::string token = auth_header.substr(strlen("Token "));
+    auto session = sessions->login_session_by_token(token);
+    return session;
+  }
+
+  const crow::PersistentData::UserSession* perform_xtoken_auth(
+      const crow::request& req,
+      crow::PersistentData::SessionStore* sessions) const {
+    CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
+
+    auto& token = req.get_header_value("X-Auth-Token");
+    auto session = sessions->login_session_by_token(token);
+    return session;
+  }
+
+  const crow::PersistentData::UserSession* perform_cookie_auth(
+      const crow::request& req,
+      crow::PersistentData::SessionStore* sessions) const {
+    CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
+
+    auto& cookie_value = req.get_header_value("Cookie");
+
+    auto start_index = cookie_value.find("SESSION=");
+    if (start_index == std::string::npos) {
+      return nullptr;
+    }
+    start_index += sizeof("SESSION=");
+    auto end_index = cookie_value.find(";", start_index);
+    if (end_index == std::string::npos) {
+      end_index = cookie_value.size();
+    }
+    std::string auth_key =
+        cookie_value.substr(start_index, end_index - start_index);
+
+    const crow::PersistentData::UserSession* session =
+        sessions->login_session_by_token(auth_key);
+    if (session == nullptr) {
+      return nullptr;
+    }
+
+    // RFC7231 defines methods that need csrf protection
+    if (req.method != "GET"_method) {
+      const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
+      // Make sure both tokens are filled
+      if (csrf.empty() || session->csrf_token.empty()) {
+        return nullptr;
+      }
+      // Reject if csrf token not available
+      if (csrf != session->csrf_token) {
+        return nullptr;
+      }
+    }
+    return session;
+  }
+
+  // checks if request can be forwarded without authentication
+  bool is_on_whitelist(const crow::request& req) const {
+    // it's allowed to GET root node without authentication
+    if ("GET"_method == req.method) {
+      if (req.url == "/redfish/v1") {
+        return true;
+      } else if (crow::webassets::routes.find(req.url) !=
+                 crow::webassets::routes.end()) {
+        return true;
+      }
+    }
+
+    // it's allowed to POST on session collection & login without authentication
+    if ("POST"_method == req.method) {
+      if ((req.url == "/redfish/v1/SessionService/Sessions") ||
+          (req.url == "/login") || (req.url == "/logout")) {
+        return true;
+      }
+    }
+
+    return false;
+  }
 };
 
 // TODO(ed) see if there is a better way to allow middlewares to request
@@ -193,7 +264,7 @@
 
         if (!username.empty() && !password.empty()) {
           if (!pam_authenticate_user(username, password)) {
-            res.code = 401;
+            res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
           } else {
             auto& context =
                 app.template get_context<PersistentData::Middleware>(req);
@@ -207,10 +278,11 @@
               nlohmann::json ret{{"data", "User '" + username + "' logged in"},
                                  {"message", "200 OK"},
                                  {"status", "ok"}};
+              res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
               res.add_header(
                   "Set-Cookie",
                   "SESSION=" + session.session_token + "; Secure; HttpOnly");
-              res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
+
               res.write(ret.dump());
             } else {
               // if content type is json, assume json token
@@ -222,10 +294,27 @@
           }
 
         } else {
-          res.code = 400;
+          res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
         }
         res.end();
       });
+
+  CROW_ROUTE(app, "/logout")
+      .methods(
+          "POST"_method)([&](const crow::request& req, crow::response& res) {
+        auto& session_store =
+            app.template get_context<PersistentData::Middleware>(req).sessions;
+        auto& session =
+            app.template get_context<TokenAuthorization::Middleware>(req)
+                .session;
+        if (session != nullptr) {
+          session_store->remove_session(session);
+        }
+        res.code = static_cast<int>(HttpRespCode::OK);
+        res.end();
+        return;
+
+      });
 }
 }  // namespaec TokenAuthorization
 }  // namespace crow