Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication using dynamic token #164

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ find_package(Threads REQUIRED)
find_package(Angelscript)
find_package(jsoncpp REQUIRED)
find_package(SocketW REQUIRED)
find_package(fmt REQUIRED)
find_package(CURL)
cmake_dependent_option(RORSERVER_WITH_ANGELSCRIPT "Adds scripting support" ON "TARGET Angelscript::angelscript" OFF)
cmake_dependent_option(RORSERVER_WITH_CURL "Adds CURL request support (needs AngelScript)" ON "TARGET CURL::libcurl" OFF)
Expand Down
3 changes: 2 additions & 1 deletion conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ def requirements(self):
self.requires("jsoncpp/1.9.5")
self.requires("openssl/3.3.2", override=True)
self.requires("socketw/3.11.0@anotherfoxguy/stable")
self.requires("libcurl/8.10.1")
self.requires("libcurl/8.10.1")
self.requires("fmt/10.1.1")
4 changes: 2 additions & 2 deletions source/protocol/rornet.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace RoRnet {
#define RORNET_LAN_BROADCAST_PORT 13000 //!< port used to send the broadcast announcement in LAN mode
#define RORNET_MAX_USERNAME_LEN 40 //!< port used to send the broadcast announcement in LAN mode

#define RORNET_VERSION "RoRnet_2.45"
#define RORNET_VERSION "RoRnet_2.45AUTHPOC"

enum MessageType
{
Expand Down Expand Up @@ -172,7 +172,7 @@ struct UserInfo
char clientversion[25]; //!< a version number of the client. For example 1 for RoR 0.35
char clientGUID[40]; //!< the clients GUID
char sessiontype[10]; //!< the requested session type. For example "normal", "bot", "rcon"
char sessionoptions[128]; //!< reserved for future options
char authtoken[300]; //!< authorization token (the master server API-based method); PROOF OF CONCEPT: this is currently the temporary login key, see cvar 'remote_login_token'.
};

struct VehicleState //!< Formerly `oob_t`
Expand Down
2 changes: 1 addition & 1 deletion source/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if (RORSERVER_WITH_CURL)
target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl)
endif ()

target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads SocketW::SocketW jsoncpp_lib)
target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads SocketW::SocketW fmt::fmt jsoncpp_lib)

IF (WIN32)
target_compile_definitions(${PROJECT_NAME} PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
Expand Down
19 changes: 11 additions & 8 deletions source/server/CurlHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ static size_t CurlXferInfoFunc(void* ptr, curl_off_t filesize_B, curl_off_t down
return 0;
}

bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& response_code, std::string& response_payload)
bool GetUrlAsString(const std::string& url, const std::vector<std::string>& headers, CURLcode& curl_result, long& response_code, std::string& response_payload)
{
std::string response_header;
std::string user_agent = "Rigs of Rods Server";

struct curl_slist* slist;
slist = NULL;
for (const std::string& header : headers)
{
slist = curl_slist_append(slist, header.c_str());
}

CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
Expand All @@ -59,6 +66,7 @@ bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& respons
#endif // _WIN32
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlStringWriteFunc);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, CurlXferInfoFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
Expand All @@ -70,12 +78,7 @@ bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& respons
curl_easy_cleanup(curl);
curl = nullptr;

if (curl_result != CURLE_OK || response_code != 200)
{
return false;
}

return true;
return curl_result == CURLE_OK && response_code == 200;
}

bool CurlRequestThreadFunc(CurlTaskContext context)
Expand All @@ -84,7 +87,7 @@ bool CurlRequestThreadFunc(CurlTaskContext context)
std::string data;
CURLcode curl_result = CURLE_OK;
long http_response = 0;
if (GetUrlAsString(context.ctc_url, /*out:*/curl_result, /*out:*/http_response, /*out:*/data))
if (GetUrlAsString(context.ctc_url, context.ctc_headers, /*out:*/curl_result, /*out:*/http_response, /*out:*/data))
{
context.ctc_script_engine->curlStatus(CURL_STATUS_SUCCESS, (int)curl_result, (int)http_response, context.ctc_displayname, data);
return true;
Expand Down
5 changes: 3 additions & 2 deletions source/server/CurlHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ class ScriptEngine;
#include <curl/easy.h>

#include <string>

#include <vector>

struct CurlTaskContext
{
std::string ctc_displayname;
std::string ctc_url;
std::vector<std::string> ctc_headers;
ScriptEngine* ctc_script_engine;
// Status is reported via new server callback `curlStatus()`
};

bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& response_code, std::string& response_payload);
bool GetUrlAsString(const std::string& url, const std::vector<std::string>& headers, CURLcode& curl_result, long& response_code, std::string& response_payload);

bool CurlRequestThreadFunc(CurlTaskContext task);

Expand Down
4 changes: 4 additions & 0 deletions source/server/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ static std::string s_irc;
static std::string s_voip;
static std::string s_serverlist_host("api.rigsofrods.org");
static std::string s_serverlist_path("");
static std::string s_authapi_url("v2.api.rigsofrods.org");
static std::string s_resourcedir(RESOURCE_DIR);

static unsigned int s_listen_port(0);
Expand Down Expand Up @@ -338,6 +339,8 @@ namespace Config {

const std::string &GetServerlistPath() { return s_serverlist_path; }

const std::string& GetAuthApiUrl() { return s_authapi_url; }

bool GetShowVersion() { return s_show_version; }

bool GetShowHelp() { return s_show_help; }
Expand Down Expand Up @@ -507,6 +510,7 @@ namespace Config {
else if (strcmp(key, "voip") == 0) { setVoIP(VAL_STR (value)); }
else if (strcmp(key, "serverlist-host") == 0) { s_serverlist_host = VAL_STR (value); }
else if (strcmp(key, "serverlist-path") == 0) { s_serverlist_path = VAL_STR (value); }
else if (strcmp(key, "authapi-url") == 0) { s_authapi_url = VAL_STR(value); }
else if (strcmp(key, "verbosity") == 0) { Logger::SetLogLevel(LOGTYPE_DISPLAY, (LogLevel) VAL_INT(value)); }
else if (strcmp(key, "logverbosity") == 0) { Logger::SetLogLevel(LOGTYPE_FILE, (LogLevel) VAL_INT(value)); }
else if (strcmp(key, "heartbeat-interval") == 0) { setHeartbeatIntervalSec(VAL_INT(value)); }
Expand Down
2 changes: 2 additions & 0 deletions source/server/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ namespace Config {

const std::string &GetServerlistPath();

const std::string& GetAuthApiUrl();

unsigned int GetHeartbeatRetryCount();

unsigned int GetHeartbeatRetrySeconds();
Expand Down
2 changes: 1 addition & 1 deletion source/server/listener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ void Listener::ThreadMain() {
// authenticate
user->username[RORNET_MAX_USERNAME_LEN - 1] = 0;
std::string nickname = Str::SanitizeUtf8(user->username);
user->authstatus = m_sequencer->AuthorizeNick(std::string(user->usertoken, 40), nickname);
user->authstatus = m_sequencer->AuthorizeNick(std::string(user->usertoken, 40), std::string(user->authtoken, 300), nickname);
strncpy(user->username, nickname.c_str(), RORNET_MAX_USERNAME_LEN - 1);

if (Config::isPublic()) {
Expand Down
9 changes: 2 additions & 7 deletions source/server/sequencer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,12 @@ int Sequencer::getNumClients() {
return (int) m_clients.size();
}

int Sequencer::AuthorizeNick(std::string token, std::string &nickname) {
int Sequencer::AuthorizeNick(const std::string& user_token, const std::string& auth_token, std::string &nickname) {
std::lock_guard<std::mutex> scoped_lock(m_clients_mutex);
if (m_auth_resolver == nullptr) {
return RoRnet::AUTH_NONE;
}
return m_auth_resolver->resolve(token, nickname, m_free_user_id);
return m_auth_resolver->resolve(user_token, auth_token, nickname, m_free_user_id);
}

void Sequencer::KillerThreadMain()
Expand Down Expand Up @@ -965,11 +965,6 @@ void Sequencer::queueMessage(int uid, int type, unsigned int streamid, char *dat
client->user.uniqueid, streamid, reg->type, reg->name, reg->status);
client->streams[streamid] = *reg;

// send an event if user is rankend and if we are a official server
if (m_auth_resolver && (client->user.authstatus & RoRnet::AUTH_RANKED))
m_auth_resolver->sendUserEvent(std::string(client->user.usertoken, 40), std::string("newvehicle"),
std::string(reg->name), std::string());

// Notify the user about the vehicle limit
if ((client->streams.size() >= Config::getMaxVehicles() + NON_VEHICLE_STREAMS - 3) &&
(client->streams.size() > NON_VEHICLE_STREAMS)) {
Expand Down
2 changes: 1 addition & 1 deletion source/server/sequencer.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ class Sequencer {
void frameStepScripts(float dt);
void GetHeartbeatUserList(Json::Value &out_array);
void UpdateMinuteStats();
int AuthorizeNick(std::string token, std::string &nickname);
int AuthorizeNick(const std::string& user_token, const std::string& auth_token, std::string &nickname);
std::vector<WebserverClientInfo> GetClientListCopy();
int getStartTime();

Expand Down
119 changes: 56 additions & 63 deletions source/server/userauth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ along with Foobar. If not, see <http://www.gnu.org/licenses/>.
#include "rornet.h"
#include "logger.h"
#include "http.h"
#include "CurlHelpers.h"

#include "json/json.h"

#include <stdexcept>
#include <cstdio>
#include <fmt/format.h>
#include <curl/curl.h>

#ifdef __GNUC__

Expand Down Expand Up @@ -117,74 +121,63 @@ int UserAuth::readConfig(const char *authFile) {
return 0;
}

int UserAuth::setUserAuth(int flags, std::string user_nick, std::string token) {
user_auth_pair_t p;
p.first = flags;
p.second = user_nick;
local_auth[token] = p;
return 0;
}

int UserAuth::sendUserEvent(std::string user_token, std::string type, std::string arg1, std::string arg2) {
/* #### DISABLED UNTIL SUPPORTED BY NEW MULTIPLAYER PORTAL ####

// Only contact the master server if we are allowed to do so
if(trustlevel<=1) return 0;

char url[2048];
sprintf(url, "%s/userevent_utf8/?v=0&sh=%s&h=%s&t=%s&a1=%s&a2=%s", REPO_URLPREFIX, challenge.c_str(), user_token.c_str(), type.c_str(), arg1.c_str(), arg2.c_str());
Logger::Log(LOG_DEBUG, "UserAuth event to server: " + std::string(url));
Http::Response resp;
if (HTTPGET(url, resp) < 0)
{
Logger::Log(LOG_ERROR, "UserAuth event query result empty");
return -1;
}

std::string body = resp.GetBody();
Logger::Log(LOG_DEBUG,"UserEvent reply: " + body);

return (body!="ok");

*/

return -1;
}

int UserAuth::resolve(std::string user_token, std::string &user_nick, int clientid) {
int UserAuth::resolve(const std::string& user_token, const std::string& auth_token, std::string &user_nick, int clientid) {
// initialize the authlevel on none = normal user
int authlevel = RoRnet::AUTH_NONE;

// contact the master server
char url[512];
sprintf(url, "/%s/users", Config::GetServerlistPath().c_str());
Logger::Log(LOG_INFO, "Attempting user authentication (%s)", url);

Json::Value data(Json::objectValue);
data["username"] = user_nick;
data["user_token"] = user_token;
std::string json_str = data.toStyledString();

Http::Response resp;
int result_code = Http::Request(Http::METHOD_GET,
Config::GetServerlistHostC(), url, "application/json",
json_str.c_str(), &resp);

// 200 means success!
if (result_code == 200) {
Logger::Log(LOG_INFO, "User authentication success, result code: %d", result_code);
authlevel = RoRnet::AUTH_RANKED;
} else {
Logger::Log(LOG_INFO, "User authentication failed, result code: %d", result_code);
try
{
// contact the master server API (Carbon)
// Proof of concept - fetch the user profile using the temp-login token
const std::string url = fmt::format("{}/users/me", Config::GetAuthApiUrl());

std::vector<std::string> headers;
headers.push_back("Content-Type: application/json");
headers.push_back("Accept: application/json");
headers.push_back(fmt::format("Authorization: Bearer {}", auth_token));

CURLcode curl_result;
long response_code;
std::string response_payload;
const bool result = GetUrlAsString(url, headers, curl_result, response_code, response_payload);

if (result) {
Logger::Log(LOG_INFO, "User authentication success");
authlevel = RoRnet::AUTH_RANKED;
Json::Value root;
Json::Reader reader;
if (!reader.parse(response_payload, root)) {
Logger::Log(LOG_WARN, "Failed to parse JSON response from master server API");
}
else if (root.isMember("me") && root["me"].isMember("username")) {
user_nick = root["me"]["username"].asString();
}
else {
Logger::Log(LOG_WARN, "Unexpected contents of JSON response from master server API");
}
}
else {
Logger::Log(LOG_INFO, fmt::format("User authentication failed, HTTP response: {}, CURL result: {} ({})",
response_code, (int)curl_result, curl_easy_strerror(curl_result)));
}
}
catch (std::exception& e) {
Logger::Log(LOG_WARN, fmt::format("UserAuth: contacting auth API failed with exception: '{}'", e.what()));
}

//then check for overrides in the authorizations file (server admins, etc)
if (local_auth.find(user_token) != local_auth.end()) {
// local auth hit!
// the stored nickname can be empty if no nickname is specified.
if (!local_auth[user_token].second.empty())
user_nick = local_auth[user_token].second;
authlevel |= local_auth[user_token].first;
try
{
//then check for overrides in the authorizations file (server admins, etc)
if (local_auth.find(user_token) != local_auth.end()) {
// local auth hit!
// the stored nickname can be empty if no nickname is specified.
if (!local_auth[user_token].second.empty())
user_nick = local_auth[user_token].second;
authlevel |= local_auth[user_token].first;
}
}
catch (std::exception& e) {
Logger::Log(LOG_WARN, fmt::format("UserAuth: resolving local overrides failed with exception: '{}'", e.what()));
}

return authlevel;
Expand Down
12 changes: 6 additions & 6 deletions source/server/userauth.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Foobar. If not, see <http://www.gnu.org/licenses/>.
along with Rigs of Rods server.
If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once
Expand All @@ -27,16 +28,15 @@ along with Foobar. If not, see <http://www.gnu.org/licenses/>.

typedef std::pair<int, std::string> user_auth_pair_t;

// Uses 2 separate tokens for 2 separate purposes:
// * classic token (cvar 'mp_player_token') is since 2025 only used for admin/mod privs based on server's local auth file
// * auth token (cvar 'remote_login_token') is authorized by master server API and gives 'ranked' status.
class UserAuth {

public:
UserAuth(std::string authFile);

int resolve(std::string user_token, std::string &user_nick, int clientid);

int setUserAuth(int flags, std::string user_nick, std::string token);

int sendUserEvent(std::string user_token, std::string type, std::string arg1, std::string arg2);
int resolve(const std::string& user_token, const std::string& auth_token, std::string &user_nick, int clientid);

private:

Expand Down
Loading