main
Brett 2023-08-18 02:21:39 -04:00
parent 1011152fbe
commit ef671700e1
13 changed files with 435 additions and 55 deletions

View File

@ -12,8 +12,13 @@ cmake_policy(SET CMP0057 NEW)
find_package(Crow)
find_package(CURL)
find_package(OpenSSL)
find_package(SQLite3)
message("SSL ${OPENSSL_INCLUDE_DIR}")
if (NOT SQLite3_FOUND)
message("Failed to find SQLite3")
endif ()
message("SQLite ${SQLite3_INCLUDE_DIRS} ${SQLite3_LIBRARIES}")
if (NOT CURL_FOUND)
message("libcurl is required!")
@ -24,6 +29,8 @@ endif ()
add_subdirectory(libs/BLT)
include_directories(include/)
include_directories(${CURL_INCLUDE_DIRS})
include_directories(${SQLite3_INCLUDE_DIRS})
include_directories(${OPENSSL_INCLUDE_DIR})
file(GLOB_RECURSE source_files src/*.cpp)
@ -33,6 +40,7 @@ target_link_libraries(crowsite BLT)
target_link_libraries(crowsite Crow::Crow)
target_link_libraries(crowsite ${CURL_LIBRARIES})
target_link_libraries(crowsite OpenSSL::SSL OpenSSL::Crypto)
target_link_libraries(crowsite SQLite::SQLite3)
target_compile_options(crowsite PRIVATE -Wall -Wextra -Wpedantic)
if (${ENABLE_ADDRSAN} MATCHES ON)

Binary file not shown.

View File

@ -1 +1,2 @@
jlbpNSZuchBeVInZ 1692336738
wDTvp2olKnTzXs0q 1692378069
vUJR5OaiqtXupR8v 1692378591

View File

@ -1 +0,0 @@
{"clientID":"2e68464b-a37c-58a2-a970-97fedf56e8f9","clientToken":"00000000-0000-0000-0000-000000000000"}

View File

@ -0,0 +1 @@
{"clientID":"50a21c33-66c4-5a0f-902f-9434632025e6","clientToken":"yfuMydsUxrYprB6ykuXBcJe3SDuu17W7OrZns1nweWBUnSUUdsHszJN/YAKTVYsPjsEVd8rGCpUly5VsYfx6FA=="}

View File

@ -0,0 +1 @@
{"clientID":"50a21c33-66c4-5a0f-902f-9434632025e6","clientToken":"6Ft+YVGtURGwMwi9yTemzakVoVpwkE3iRzshpUn/u58X6BWECdBZvE6nDCg4v628MLqHLwui59GIVyxc9HN0ww=="}

View File

@ -21,7 +21,6 @@ namespace cs::jellyfin
std::string name;
std::string serverId;
std::string Id;
std::string primaryImageTag;
bool hasPassword;
bool hasConfiguredPassword;
bool hasConfiguredEasyPassword;
@ -29,12 +28,13 @@ namespace cs::jellyfin
std::string lastLoginDate;
std::string lastActivityDate;
} user;
std::string accessToken;
bool isAdmin{};
};
void setToken(std::string_view token);
void processUserData();
const client_data& getUserData(const std::string& username);
std::string generateAuthHeader();
std::string getUserData();

View File

@ -10,12 +10,64 @@
namespace cs {
constexpr uint32_t PERM_ADMIN = 0x1;
constexpr uint32_t PERM_READ_FILES = 0x2;
constexpr uint32_t PERM_WRITE_FILES = 0x4;
constexpr uint32_t PERM_CREATE_POSTS = 0x8;
constexpr uint32_t PERM_CREATE_COMMENTS = 0x10;
constexpr uint32_t PERM_CREATE_SHARES = 0x20;
constexpr uint32_t PERM_EDIT_POSTS = 0x40;
constexpr uint32_t PERM_EDIT_COMMENTS = 0x80;
constexpr uint32_t PERM_DEFAULT = PERM_READ_FILES | PERM_CREATE_COMMENTS;
namespace auth {
void init();
void cleanup();
}
struct cookie_data {
std::string clientID;
std::string clientToken;
};
bool handleLoginPost(cs::parser::Post& postData, cookie_data& cookieOut);
/**
* An interface function which is used to validate login information provided as post data. Is is up to the caller
* to inform the user of the clientID and clientToken, along with the auth system of these values.
* Which is to say that this function is purely for auth.
*
* @param postData post data container class. This function checks for the existence of "username" and "password" in postData
* @related createUserAuthTokens(...)
* @related storeUserData(...)
* @return true if user is valid and authorized, false otherwise (including if "username" || "password" does not exist).
*/
bool checkUserAuthorization(cs::parser::Post& postData);
/**
* Generates a clientID (UUIDv5 based on user-agent and username) along with a unique high security (512 bit) base64 encoded token string
* @param postData post data including a "username"
* @param useragent user agent of the requesting client
* @return cookie_data structure containing clientId and clientToken
*/
cookie_data createUserAuthTokens(cs::parser::Post& postData, const std::string& useragent);
/**
* Informs the internal auth database of a successfully login attempt, updating the internal storage of the clientID -> Token.
* Username is passed directly as this function should only be called after checkUserAuthorization(...) returns true.
* @param username username of the user.
* @param useragent user-agent of the user
* @param tokens generated client tokens
* @return false if something failed (error will be logged!)
* @related createUserAuthTokens(...)
* @related checkUserAuthorization(...)
*/
bool storeUserData(const std::string& username, const std::string& useragent, const cookie_data& tokens);
bool isUserLoggedIn(const std::string& clientID, const std::string& token);
std::string getUserFromID(const std::string& clientID);
bool isUserAdmin(const std::string& username);
uint32_t getUserPermissions(const std::string& username);
}

View File

@ -0,0 +1,126 @@
//
// Created by brett on 17/08/23.
//
#ifndef CROWSITE_SQL_HELPER_H
#define CROWSITE_SQL_HELPER_H
#include <sqlite3.h>
#include <type_traits>
namespace cs::sql
{
static int prepareStatement(sqlite3* db, const std::string& sqlStatement, sqlite3_stmt** ppStmt)
{
return sqlite3_prepare_v2(db, sqlStatement.c_str(), static_cast<int>(sqlStatement.size()) + 1, ppStmt, nullptr);
}
class statement
{
private:
sqlite3_stmt* stmt = nullptr;
sqlite3* db;
int err;
public:
statement(sqlite3* db, const std::string& statement): db(db)
{
err = prepareStatement(db, statement, &stmt);
if (err)
BLT_ERROR("Failed to execute statement, error code %d, error: %s.", err, sqlite3_errstr(err));
}
/**
* @return true if the last statement failed. Should be checked after construction!
*/
[[nodiscard]] bool fail() const
{
return !(err == SQLITE_OK || err == SQLITE_DONE || err == SQLITE_ROW);
}
[[nodiscard]] bool hasRow() const
{ return err == SQLITE_ROW; }
[[nodiscard]] bool complete() const
{ return err == SQLITE_DONE; }
[[nodiscard]] int error() const
{
return err;
}
bool execute()
{
if (err != SQLITE_ROW)
sqlite3_reset(stmt);
err = sqlite3_step(stmt);
return !fail();
}
template<typename T>
statement* set(const T& t, int column)
{
// make api consistent
column = column - 1;
if constexpr (std::is_floating_point_v<T>)
{
err = sqlite3_bind_double(stmt, column, t);
} else if constexpr (std::is_integral_v<T>)
{
if constexpr (std::is_same_v<T, int64_t> || std::is_same_v<T, uint64_t>)
err = sqlite3_bind_int64(stmt, column, t);
else
err = sqlite3_bind_int(stmt, column, t);
} else if constexpr (std::is_same_v<T, std::string>)
{
err = sqlite3_bind_text(stmt, column, t.c_str(), -1, nullptr);
}
return this;
}
template<typename T>
T get(int column)
{
if (err != SQLITE_ROW)
throw std::runtime_error("Unable to get data as statement didn't return a row!");
if constexpr (std::is_floating_point_v<T>)
{
return sqlite3_column_double(stmt, column);
} else if constexpr (std::is_integral_v<T>)
{
if constexpr (std::is_same_v<T, int64_t> || std::is_same_v<T, uint64_t>)
return sqlite3_column_int64(stmt, column);
else
return sqlite3_column_int(stmt, column);
} else if constexpr (std::is_same_v<T, std::string>)
{
return std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, column)));
} else
{
return sqlite3_column_blob(stmt, column);
}
}
/**
* do not run execute before this function. ever.
* This function will return a default initialized T if execute doesn't return a row.
*/
template<typename T>
T executeAndGet(int column)
{
execute();
if (err != SQLITE_ROW)
return T{};
return get<T>(column);
}
~statement()
{
sqlite3_finalize(stmt);
}
};
}
#endif //CROWSITE_SQL_HELPER_H

@ -1 +1 @@
Subproject commit 1e8f431f9eb06582083efde6489b59380f3e19ac
Subproject commit 1d03938f950568dd1082abfd55f664ede6023995

View File

@ -13,8 +13,7 @@ namespace cs::jellyfin
struct
{
std::string token;
HASHMAP<std::string, std::string> user_ids;
HASHMAP<std::string, client_data> logged_in_users;
HASHMAP<std::string, client_data> user_ids;
} GLOBALS;
void setToken(std::string_view token)
@ -24,16 +23,29 @@ namespace cs::jellyfin
void processUserData()
{
auto data = getUserData();
auto usr_data = getUserData();
auto json = crow::json::load(data);
auto json = crow::json::load(usr_data);
for (const auto& user : json)
for (const auto& users : json)
{
auto username = std::string(user["Name"].s());
auto userid = std::string(user["Id"].s());
const auto& policy = users["Policy"];
client_data data;
data.isAdmin = policy["IsAdministrator"].b();
auto& user = data.user;
user.name = users["Name"].s();
user.serverId = users["ServerId"].s();
user.Id = users["Id"].s();
user.hasPassword = users["HasPassword"].b();
user.hasConfiguredPassword = users["HasConfiguredPassword"].b();
user.hasConfiguredEasyPassword = users["HasConfiguredEasyPassword"].b();
user.enableAutoLogin = users["EnableAutoLogin"].b();
user.lastLoginDate = users["LastLoginDate"].s();
user.lastActivityDate = users["LastActivityDate"].s();
//BLT_TRACE("Processing %s = %s", username.operator std::string().c_str(), userid.operator std::string().c_str());
GLOBALS.user_ids[username] = userid;
GLOBALS.user_ids[user.name] = data;
}
}
@ -81,31 +93,14 @@ namespace cs::jellyfin
auto response = cs::request::getResponseAndClear(l_url);
if (post.status() == 200)
{
crow::json::rvalue read = crow::json::load(response);
const auto& users = read["User"];
client_data data;
data.accessToken = read["AccessToken"].s();
auto& user = data.user;
user.name = users["Name"].s();
user.serverId = users["ServerId"].s();
user.Id = users["Id"].s();
user.primaryImageTag = users["PrimaryImageTag"].s();
user.hasPassword = users["HasPassword"].b();
user.hasConfiguredPassword = users["HasConfiguredPassword"].b();
user.hasConfiguredEasyPassword = users["HasConfiguredEasyPassword"].b();
user.enableAutoLogin = users["EnableAutoLogin"].b();
user.lastLoginDate = users["LastLoginDate"].s();
user.lastActivityDate = users["LastActivityDate"].s();
GLOBALS.logged_in_users[std::string(username)] = data;
return auth_response::AUTHORIZED;
}
return auth_response::ERROR;
}
const client_data& jellyfin::getUserData(const std::string& username)
{
return GLOBALS.user_ids[username];
}
}

View File

@ -2,15 +2,49 @@
// Created by brett on 16/08/23.
//
#include <crowsite/site/auth.h>
#include <crowsite/config.h>
#include <crowsite/requests/jellyfin.h>
#include "blt/std/logging.h"
#include "blt/std/uuid.h"
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <crowsite/sql_helper.h>
#include <random>
#include <filesystem>
using namespace blt;
namespace cs {
namespace cs
{
sqlite3* user_database;
bool handleLoginPost(parser::Post& postData, cookie_data& cookieOut)
// https://stackoverflow.com/questions/5288076/base64-encoding-and-decoding-with-openssl
char* base64(const unsigned char* input, int length)
{
const auto pl = 4 * ((length + 2) / 3);
auto output = reinterpret_cast<char*>(calloc(pl + 1, 1)); //+1 for the terminating null that EVP_EncodeBlock adds on
const auto ol = EVP_EncodeBlock(reinterpret_cast<unsigned char*>(output), input, length);
if (pl != ol)
{
std::cerr << "Whoops, encode predicted " << pl << " but we got " << ol << "\n";
}
return output;
}
unsigned char* decode64(const char* input, int length)
{
const auto pl = 3 * length / 4;
auto output = reinterpret_cast<unsigned char*>(calloc(pl + 1, 1));
const auto ol = EVP_DecodeBlock(output, reinterpret_cast<const unsigned char*>(input), length);
if (pl != ol)
{
std::cerr << "Whoops, decode predicted " << pl << " but we got " << ol << "\n";
}
return output;
}
bool checkUserAuthorization(cs::parser::Post& postData)
{
// javascript should make sure we don't send post requests without information
// this way it can be interactive
@ -18,9 +52,163 @@ namespace cs {
return false;
auto auth = jellyfin::authenticateUser(postData["username"], postData["password"]);
cookieOut.clientID = uuid::toString(uuid::genV5("ClientID?"));
cookieOut.clientToken = uuid::toString(uuid::genV4());
return auth == jellyfin::auth_response::AUTHORIZED;
}
cookie_data createUserAuthTokens(parser::Post& postData, const std::string& useragent)
{
cookie_data cookieOut;
cookieOut.clientID = uuid::toString(uuid::genV5(postData["username"] + "::" + useragent));
std::string token;
std::random_device rd;
std::seed_seq seed{rd(), rd(), rd(), rd()};
std::mt19937_64 gen(seed);
std::uniform_int_distribution<int> charDist(0, 92);
for (int i = 0; i < SHA512_DIGEST_LENGTH * 8; i++)
token += char(33 + charDist(gen));
unsigned char hash[SHA512_DIGEST_LENGTH + 1];
hash[SHA512_DIGEST_LENGTH] = '\0';
SHA512(reinterpret_cast<const unsigned char*>(token.c_str()), token.size(), hash);
auto b64str = base64(hash, SHA512_DIGEST_LENGTH);
cookieOut.clientToken = std::string(reinterpret_cast<const char*>(b64str));
free(b64str);
return cookieOut;
}
bool storeUserData(const std::string& username, const std::string& useragent, const cookie_data& tokens)
{
sql::statement insertStmt{
user_database,
"INSERT OR REPLACE INTO user_sessions (clientID, username, useragent, token) VALUES (?, ?, ?, ?);"
};
if (insertStmt.fail())
return false;
insertStmt.set(tokens.clientID, 0);
insertStmt.set(username, 1);
insertStmt.set(useragent, 2);
insertStmt.set(tokens.clientToken, 3);
if (!insertStmt.execute())
return false;
sql::statement insertAuth {
user_database,
"INSERT OR REPLACE INTO user_permissions (username, permission) VALUES (?, ?);"
};
if (insertAuth.fail())
return false;
insertStmt.set(username, 0);
insertStmt.set(PERM_DEFAULT | (jellyfin::getUserData(username).isAdmin ? PERM_ADMIN : 0), 1);
if (!insertAuth.execute())
return false;
return true;
}
bool isUserLoggedIn(const std::string& clientID, const std::string& token)
{
sql::statement stmt {
user_database,
"SELECT username FROM user_sessions WHERE clientID='?' AND token='?';"
};
if (stmt.fail())
return false;
stmt.set(clientID, 0);
stmt.set(token, 1);
stmt.execute();
return stmt.hasRow();
}
bool isUserAdmin(const std::string& username)
{
return getUserPermissions(username) & PERM_ADMIN;
}
std::string getUserFromID(const std::string& clientID)
{
sql::statement stmt {
user_database,
"SELECT username FROM user_sessions WHERE clientID='?';"
};
if (stmt.fail())
return "";
stmt.set(clientID, 0);
return stmt.executeAndGet<std::string>(0);
}
uint32_t getUserPermissions(const std::string& username)
{
sql::statement stmt {
user_database,
"SELECT permission FROM user_permissions WHERE username='?';"
};
if (stmt.fail())
return 0;
stmt.set(username, 0);
return static_cast<uint32_t>(stmt.executeAndGet<int32_t>(0));
}
void auth::init()
{
// TODO: proper multithreading
auto path = (std::string(SITE_FILES_PATH) + "/data/db/");
auto dbname = "users.sqlite";
auto full_path = path + dbname;
BLT_TRACE("Using %s for users database", full_path.c_str());
std::filesystem::create_directories(path);
if (int err = sqlite3_open_v2(full_path.c_str(), &user_database,
SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX,
nullptr
) != SQLITE_OK)
{
BLT_FATAL("Unable to create database connection! err %d msg %s", err, sqlite3_errstr(err));
std::exit(1);
}
sql::statement v {
user_database,
"SELECT SQLITE_VERSION()"
};
if (v.fail())
BLT_WARN("Failed to create statement with error code: %d msg: %s", v.error(), sqlite3_errstr(v.error()));
if (!v.execute())
BLT_WARN("Failed to execute statement with error code: %d msg: %s", v.error(), sqlite3_errstr(v.error()));
BLT_INFO("SQLite Version: %s", v.get<std::string>(0).c_str());
sql::statement tableStmt{
user_database,
"CREATE TABLE IF NOT EXISTS user_sessions (clientID VARCHAR(36), username TEXT, useragent TEXT, token TEXT, PRIMARY KEY(clientID));"
};
if (tableStmt.fail() || !tableStmt.execute())
BLT_ERROR("Failed to execute user_sessions table creation! %d : %s", tableStmt.error(), sqlite3_errstr(tableStmt.error()));
sql::statement permsStmt{
user_database,
"CREATE TABLE IF NOT EXISTS user_permissions (username TEXT, permission INT, PRIMARY KEY(username));"
};
if (permsStmt.fail() || !permsStmt.execute())
BLT_ERROR("Failed to execute user_permissions table creation! %d : %s", permsStmt.error(), sqlite3_errstr(permsStmt.error()));
}
void auth::cleanup()
{
sqlite3_close_v2(user_database);
}
}

View File

@ -42,7 +42,8 @@ class BLT_CrowLogger : public crow::ILogHandler
}
};
inline crow::response redirect(const std::string& loc){
inline crow::response redirect(const std::string& loc)
{
crow::response res;
res.redirect(loc);
return res;
@ -63,6 +64,7 @@ int main(int argc, const char** argv)
auto args = parser.parse_args(argc, argv);
cs::jellyfin::setToken(blt::arg_parse::get<std::string>(args["token"]));
cs::jellyfin::processUserData();
cs::auth::init();
BLT_INFO("Starting site %s.", SITE_NAME);
crow::mustache::set_global_base(SITE_FILES_PATH);
@ -75,7 +77,7 @@ int main(int argc, const char** argv)
const auto cookie_age = 180 * 24 * 60 * 60;
BLT_INFO("Init Crow with compression and logging enabled!");
crow::App<crow::CookieParser, Session> app {Session{
crow::App<crow::CookieParser, Session> app{Session{
// customize cookies
crow::CookieParser::Cookie("session").max_age(session_age).path("/"),
// set session id length (small value only for demonstration purposes)
@ -147,22 +149,28 @@ int main(int argc, const char** argv)
crow::response res(303);
cs::cookie_data data;
std::string user_agent;
for (const auto& h : req.headers)
{
if (h.first == "User-Agent")
user_agent = h.second;
}
// either redirect to clear the form if failed or pass user to index
if (cs::handleLoginPost(pp, data))
if (cs::checkUserAuthorization(pp))
{
cs::cookie_data data = cs::createUserAuthTokens(pp, user_agent);
cs::storeUserData(pp["username"], user_agent, data);
session.set("clientID", data.clientID);
session.set("clientToken", data.clientToken);
if (pp.hasKey("remember_me")){
auto value = pp["remember_me"];
auto& cookie_context = app.get_context<crow::CookieParser>(req);
if (value[0] == 'T')
if (pp.hasKey("remember_me") && pp["remember_me"][0] == 'T')
{
auto& cookie_context = app.get_context<crow::CookieParser>(req);
cookie_context.set_cookie("clientID", data.clientID).path("/").max_age(cookie_age);
cookie_context.set_cookie("clientToken", data.clientToken).path("/").max_age(cookie_age);
}
}
res.set_header("Location", pp.hasKey("referer") ? pp["referer"] : "/");
} else
res.set_header("Location", "/login.html");
@ -199,6 +207,7 @@ int main(int argc, const char** argv)
app.port(8080).multithreaded().run();
cs::requests::cleanup();
cs::auth::cleanup();
return 0;
}