SQLite
parent
1011152fbe
commit
ef671700e1
|
@ -12,8 +12,13 @@ cmake_policy(SET CMP0057 NEW)
|
||||||
find_package(Crow)
|
find_package(Crow)
|
||||||
find_package(CURL)
|
find_package(CURL)
|
||||||
find_package(OpenSSL)
|
find_package(OpenSSL)
|
||||||
|
find_package(SQLite3)
|
||||||
|
|
||||||
message("SSL ${OPENSSL_INCLUDE_DIR}")
|
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)
|
if (NOT CURL_FOUND)
|
||||||
message("libcurl is required!")
|
message("libcurl is required!")
|
||||||
|
@ -24,6 +29,8 @@ endif ()
|
||||||
add_subdirectory(libs/BLT)
|
add_subdirectory(libs/BLT)
|
||||||
include_directories(include/)
|
include_directories(include/)
|
||||||
include_directories(${CURL_INCLUDE_DIRS})
|
include_directories(${CURL_INCLUDE_DIRS})
|
||||||
|
include_directories(${SQLite3_INCLUDE_DIRS})
|
||||||
|
include_directories(${OPENSSL_INCLUDE_DIR})
|
||||||
|
|
||||||
file(GLOB_RECURSE source_files src/*.cpp)
|
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 Crow::Crow)
|
||||||
target_link_libraries(crowsite ${CURL_LIBRARIES})
|
target_link_libraries(crowsite ${CURL_LIBRARIES})
|
||||||
target_link_libraries(crowsite OpenSSL::SSL OpenSSL::Crypto)
|
target_link_libraries(crowsite OpenSSL::SSL OpenSSL::Crypto)
|
||||||
|
target_link_libraries(crowsite SQLite::SQLite3)
|
||||||
target_compile_options(crowsite PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(crowsite PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
if (${ENABLE_ADDRSAN} MATCHES ON)
|
if (${ENABLE_ADDRSAN} MATCHES ON)
|
||||||
|
|
Binary file not shown.
|
@ -1 +1,2 @@
|
||||||
jlbpNSZuchBeVInZ 1692336738
|
wDTvp2olKnTzXs0q 1692378069
|
||||||
|
vUJR5OaiqtXupR8v 1692378591
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{"clientID":"2e68464b-a37c-58a2-a970-97fedf56e8f9","clientToken":"00000000-0000-0000-0000-000000000000"}
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"clientID":"50a21c33-66c4-5a0f-902f-9434632025e6","clientToken":"yfuMydsUxrYprB6ykuXBcJe3SDuu17W7OrZns1nweWBUnSUUdsHszJN/YAKTVYsPjsEVd8rGCpUly5VsYfx6FA=="}
|
|
@ -0,0 +1 @@
|
||||||
|
{"clientID":"50a21c33-66c4-5a0f-902f-9434632025e6","clientToken":"6Ft+YVGtURGwMwi9yTemzakVoVpwkE3iRzshpUn/u58X6BWECdBZvE6nDCg4v628MLqHLwui59GIVyxc9HN0ww=="}
|
|
@ -21,7 +21,6 @@ namespace cs::jellyfin
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string serverId;
|
std::string serverId;
|
||||||
std::string Id;
|
std::string Id;
|
||||||
std::string primaryImageTag;
|
|
||||||
bool hasPassword;
|
bool hasPassword;
|
||||||
bool hasConfiguredPassword;
|
bool hasConfiguredPassword;
|
||||||
bool hasConfiguredEasyPassword;
|
bool hasConfiguredEasyPassword;
|
||||||
|
@ -29,12 +28,13 @@ namespace cs::jellyfin
|
||||||
std::string lastLoginDate;
|
std::string lastLoginDate;
|
||||||
std::string lastActivityDate;
|
std::string lastActivityDate;
|
||||||
} user;
|
} user;
|
||||||
std::string accessToken;
|
bool isAdmin{};
|
||||||
};
|
};
|
||||||
|
|
||||||
void setToken(std::string_view token);
|
void setToken(std::string_view token);
|
||||||
|
|
||||||
void processUserData();
|
void processUserData();
|
||||||
|
const client_data& getUserData(const std::string& username);
|
||||||
|
|
||||||
std::string generateAuthHeader();
|
std::string generateAuthHeader();
|
||||||
std::string getUserData();
|
std::string getUserData();
|
||||||
|
|
|
@ -10,12 +10,64 @@
|
||||||
|
|
||||||
namespace cs {
|
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 {
|
struct cookie_data {
|
||||||
std::string clientID;
|
std::string clientID;
|
||||||
std::string clientToken;
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
2
libs/BLT
2
libs/BLT
|
@ -1 +1 @@
|
||||||
Subproject commit 1e8f431f9eb06582083efde6489b59380f3e19ac
|
Subproject commit 1d03938f950568dd1082abfd55f664ede6023995
|
|
@ -13,8 +13,7 @@ namespace cs::jellyfin
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
std::string token;
|
std::string token;
|
||||||
HASHMAP<std::string, std::string> user_ids;
|
HASHMAP<std::string, client_data> user_ids;
|
||||||
HASHMAP<std::string, client_data> logged_in_users;
|
|
||||||
} GLOBALS;
|
} GLOBALS;
|
||||||
|
|
||||||
void setToken(std::string_view token)
|
void setToken(std::string_view token)
|
||||||
|
@ -24,16 +23,29 @@ namespace cs::jellyfin
|
||||||
|
|
||||||
void processUserData()
|
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());
|
const auto& policy = users["Policy"];
|
||||||
auto userid = std::string(user["Id"].s());
|
|
||||||
|
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());
|
//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);
|
auto response = cs::request::getResponseAndClear(l_url);
|
||||||
|
|
||||||
if (post.status() == 200)
|
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::AUTHORIZED;
|
||||||
}
|
|
||||||
|
|
||||||
return auth_response::ERROR;
|
return auth_response::ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client_data& jellyfin::getUserData(const std::string& username)
|
||||||
|
{
|
||||||
|
return GLOBALS.user_ids[username];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,15 +2,49 @@
|
||||||
// Created by brett on 16/08/23.
|
// Created by brett on 16/08/23.
|
||||||
//
|
//
|
||||||
#include <crowsite/site/auth.h>
|
#include <crowsite/site/auth.h>
|
||||||
|
#include <crowsite/config.h>
|
||||||
#include <crowsite/requests/jellyfin.h>
|
#include <crowsite/requests/jellyfin.h>
|
||||||
#include "blt/std/logging.h"
|
#include "blt/std/logging.h"
|
||||||
#include "blt/std/uuid.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;
|
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
|
// javascript should make sure we don't send post requests without information
|
||||||
// this way it can be interactive
|
// this way it can be interactive
|
||||||
|
@ -18,9 +52,163 @@ namespace cs {
|
||||||
return false;
|
return false;
|
||||||
auto auth = jellyfin::authenticateUser(postData["username"], postData["password"]);
|
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;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
31
src/main.cpp
31
src/main.cpp
|
@ -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;
|
crow::response res;
|
||||||
res.redirect(loc);
|
res.redirect(loc);
|
||||||
return res;
|
return res;
|
||||||
|
@ -63,6 +64,7 @@ int main(int argc, const char** argv)
|
||||||
auto args = parser.parse_args(argc, argv);
|
auto args = parser.parse_args(argc, argv);
|
||||||
cs::jellyfin::setToken(blt::arg_parse::get<std::string>(args["token"]));
|
cs::jellyfin::setToken(blt::arg_parse::get<std::string>(args["token"]));
|
||||||
cs::jellyfin::processUserData();
|
cs::jellyfin::processUserData();
|
||||||
|
cs::auth::init();
|
||||||
|
|
||||||
BLT_INFO("Starting site %s.", SITE_NAME);
|
BLT_INFO("Starting site %s.", SITE_NAME);
|
||||||
crow::mustache::set_global_base(SITE_FILES_PATH);
|
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;
|
const auto cookie_age = 180 * 24 * 60 * 60;
|
||||||
|
|
||||||
BLT_INFO("Init Crow with compression and logging enabled!");
|
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
|
// customize cookies
|
||||||
crow::CookieParser::Cookie("session").max_age(session_age).path("/"),
|
crow::CookieParser::Cookie("session").max_age(session_age).path("/"),
|
||||||
// set session id length (small value only for demonstration purposes)
|
// set session id length (small value only for demonstration purposes)
|
||||||
|
@ -147,21 +149,27 @@ int main(int argc, const char** argv)
|
||||||
|
|
||||||
crow::response res(303);
|
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
|
// 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("clientID", data.clientID);
|
||||||
session.set("clientToken", data.clientToken);
|
session.set("clientToken", data.clientToken);
|
||||||
if (pp.hasKey("remember_me")){
|
if (pp.hasKey("remember_me") && pp["remember_me"][0] == 'T')
|
||||||
auto value = pp["remember_me"];
|
{
|
||||||
auto& cookie_context = app.get_context<crow::CookieParser>(req);
|
auto& cookie_context = app.get_context<crow::CookieParser>(req);
|
||||||
if (value[0] == 'T')
|
cookie_context.set_cookie("clientID", data.clientID).path("/").max_age(cookie_age);
|
||||||
{
|
cookie_context.set_cookie("clientToken", data.clientToken).path("/").max_age(cookie_age);
|
||||||
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"] : "/");
|
res.set_header("Location", pp.hasKey("referer") ? pp["referer"] : "/");
|
||||||
} else
|
} else
|
||||||
|
@ -199,6 +207,7 @@ int main(int argc, const char** argv)
|
||||||
app.port(8080).multithreaded().run();
|
app.port(8080).multithreaded().run();
|
||||||
|
|
||||||
cs::requests::cleanup();
|
cs::requests::cleanup();
|
||||||
|
cs::auth::cleanup();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue