blob: aee240766bc186d672534d8f997aefbd3f529e88 [file] [log] [blame]
Ed Tanousba9f9a62017-10-11 16:40:35 -07001#pragma once
2
Ed Tanousba9f9a62017-10-11 16:40:35 -07003#include <nlohmann/json.hpp>
4#include <pam_authenticate.hpp>
5#include <webassets.hpp>
Borawski.Lukasz16238972018-01-17 15:36:53 +01006#include <random>
Ed Tanousba9f9a62017-10-11 16:40:35 -07007#include <crow/app.h>
8#include <crow/http_request.h>
9#include <crow/http_response.h>
10#include <boost/container/flat_map.hpp>
11#include <boost/uuid/uuid.hpp>
12#include <boost/uuid/uuid_generators.hpp>
13#include <boost/uuid/uuid_io.hpp>
14
15namespace crow {
16
17namespace PersistentData {
18struct UserSession {
19 std::string unique_id;
20 std::string session_token;
21 std::string username;
22 std::string csrf_token;
Ed Tanousc963aa42017-10-27 16:00:19 -070023 std::chrono::time_point<std::chrono::steady_clock> last_updated;
Ed Tanousba9f9a62017-10-11 16:40:35 -070024};
25
26void to_json(nlohmann::json& j, const UserSession& p) {
27 j = nlohmann::json{{"unique_id", p.unique_id},
28 {"session_token", p.session_token},
29 {"username", p.username},
30 {"csrf_token", p.csrf_token}};
31}
32
33void from_json(const nlohmann::json& j, UserSession& p) {
34 try {
35 p.unique_id = j.at("unique_id").get<std::string>();
36 p.session_token = j.at("session_token").get<std::string>();
37 p.username = j.at("username").get<std::string>();
38 p.csrf_token = j.at("csrf_token").get<std::string>();
Ed Tanousc963aa42017-10-27 16:00:19 -070039 // For now, sessions that were persisted through a reboot get their timer
40 // reset. This could probably be overcome with a better understanding of
41 // wall clock time and steady timer time, possibly persisting values with
42 // wall clock time instead of steady timer, but the tradeoffs of all the
43 // corner cases involved are non-trivial, so this is done temporarily
44 p.last_updated = std::chrono::steady_clock::now();
Ed Tanousba9f9a62017-10-11 16:40:35 -070045 } catch (std::out_of_range) {
46 // do nothing. Session API incompatibility, leave sessions empty
47 }
48}
49
Ed Tanousc963aa42017-10-27 16:00:19 -070050class Middleware;
Ed Tanousba9f9a62017-10-11 16:40:35 -070051
Ed Tanousc963aa42017-10-27 16:00:19 -070052class SessionStore {
Ed Tanousba9f9a62017-10-11 16:40:35 -070053 public:
Ed Tanousc963aa42017-10-27 16:00:19 -070054 const UserSession& generate_user_session(const std::string& username) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070055 static constexpr std::array<char, 62> alphanum = {
56 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
57 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
58 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
59 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
60 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
61
62 // entropy: 30 characters, 62 possibilies. log2(62^30) = 178 bits of
63 // entropy. OWASP recommends at least 60
64 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
65 std::string session_token;
66 session_token.resize(20, '0');
67 std::uniform_int_distribution<int> dist(0, alphanum.size() - 1);
68 for (int i = 0; i < session_token.size(); ++i) {
69 session_token[i] = alphanum[dist(rd)];
70 }
71 // Only need csrf tokens for cookie based auth, token doesn't matter
72 std::string csrf_token;
73 csrf_token.resize(20, '0');
74 for (int i = 0; i < csrf_token.size(); ++i) {
75 csrf_token[i] = alphanum[dist(rd)];
76 }
77
78 std::string unique_id;
79 unique_id.resize(10, '0');
80 for (int i = 0; i < unique_id.size(); ++i) {
81 unique_id[i] = alphanum[dist(rd)];
82 }
Ed Tanousc963aa42017-10-27 16:00:19 -070083 const auto session_it = auth_tokens.emplace(
84 session_token,
85 std::move(UserSession{unique_id, session_token, username, csrf_token,
86 std::chrono::steady_clock::now()}));
87 const UserSession& user = (session_it).first->second;
88 need_write_ = true;
89 return user;
Ed Tanousba9f9a62017-10-11 16:40:35 -070090 }
91
Ed Tanousc963aa42017-10-27 16:00:19 -070092 const UserSession* login_session_by_token(const std::string& token) {
93 apply_session_timeouts();
94 auto session_it = auth_tokens.find(token);
95 if (session_it == auth_tokens.end()) {
96 return nullptr;
97 }
98 UserSession& foo = session_it->second;
99 foo.last_updated = std::chrono::steady_clock::now();
100 return &foo;
101 }
102
103 const UserSession* get_session_by_uid(const std::string& uid) {
104 apply_session_timeouts();
105 // TODO(Ed) this is inefficient
106 auto session_it = auth_tokens.begin();
107 while (session_it != auth_tokens.end()) {
108 if (session_it->second.unique_id == uid) {
109 return &session_it->second;
110 }
111 session_it++;
112 }
113 return nullptr;
114 }
115
116 void remove_session(const UserSession* session) {
117 auth_tokens.erase(session->session_token);
118 need_write_ = true;
119 }
120
121 std::vector<const std::string*> get_unique_ids() {
122 std::vector<const std::string*> ret;
123 ret.reserve(auth_tokens.size());
124 for (auto& session : auth_tokens) {
125 ret.push_back(&session.second.unique_id);
126 }
127 return ret;
128 }
129
130 bool needs_write() { return need_write_; }
131
132 // Persistent data middleware needs to be able to serialize our auth_tokens
133 // structure, which is private
134 friend Middleware;
135
136 private:
137 void apply_session_timeouts() {
138 std::chrono::minutes timeout(60);
139 auto time_now = std::chrono::steady_clock::now();
140 if (time_now - last_timeout_update > std::chrono::minutes(1)) {
141 last_timeout_update = time_now;
142 auto auth_tokens_it = auth_tokens.begin();
143 while (auth_tokens_it != auth_tokens.end()) {
144 if (time_now - auth_tokens_it->second.last_updated >= timeout) {
145 auth_tokens_it = auth_tokens.erase(auth_tokens_it);
146 need_write_ = true;
147 } else {
148 auth_tokens_it++;
149 }
150 }
151 }
152 }
153 std::chrono::time_point<std::chrono::steady_clock> last_timeout_update;
154 boost::container::flat_map<std::string, UserSession> auth_tokens;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700155 std::random_device rd;
Ed Tanousc963aa42017-10-27 16:00:19 -0700156 bool need_write_{false};
157};
158
159class Middleware {
160 // todo(ed) should read this from a fixed location somewhere, not CWD
161 static constexpr const char* filename = "bmcweb_persistent_data.json";
162 int json_revision = 1;
163
164 public:
165 struct context {
166 SessionStore* sessions;
167 };
168
169 Middleware() { read_data(); }
170
171 ~Middleware() {
172 if (sessions.needs_write()) {
173 write_data();
174 }
175 }
176
177 void before_handle(crow::request& req, response& res, context& ctx) {
178 ctx.sessions = &sessions;
179 }
180
181 void after_handle(request& req, response& res, context& ctx) {}
182
183 // TODO(ed) this should really use protobuf, or some other serialization
184 // library, but adding another dependency is somewhat outside the scope of
185 // this application for the moment
186 void read_data() {
187 std::ifstream persistent_file(filename);
188 int file_revision = 0;
189 if (persistent_file.is_open()) {
190 // call with exceptions disabled
191 auto data = nlohmann::json::parse(persistent_file, nullptr, false);
192 if (!data.is_discarded()) {
193 file_revision = data.value("revision", 0);
194 sessions.auth_tokens =
195 data.value("sessions", decltype(sessions.auth_tokens)());
196 system_uuid = data.value("system_uuid", "");
197 }
198 }
199 bool need_write = false;
200
201 if (system_uuid.empty()) {
202 system_uuid = boost::uuids::to_string(boost::uuids::random_generator()());
203 need_write = true;
204 }
205 if (file_revision < json_revision) {
206 need_write = true;
207 }
208 // write revision changes or system uuid changes immediately
209 if (need_write) {
210 write_data();
211 }
212 }
213
214 void write_data() {
215 std::ofstream persistent_file(filename);
216 nlohmann::json data;
217 data["sessions"] = sessions.auth_tokens;
218 data["system_uuid"] = system_uuid;
219 data["revision"] = json_revision;
220 persistent_file << data;
221 }
222
223 SessionStore sessions;
224 std::string system_uuid;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700225};
226
227} // namespaec PersistentData
228} // namespace crow