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

Support mtasa:// protocol connect args #3211

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from

Conversation

Lpsd
Copy link
Member

@Lpsd Lpsd commented Oct 15, 2023

Changes

  • Allows arguments to be supplied to the mtasa:// protocol and received by the server in Lua via onPlayerConnect event parameters.
  • Adds an extra argument to redirectPlayer which allows you to pass a table to achieve the same result (all keys, values are automatically converted to strings, in order to form the 'argument path').
  • A fix for some of the SharedUtil tests (Use MTA temp path for client tests #3209) has been modified and merged into this PR.

Additional Info

Total length of arguments is limited to 1024 including / delimiter per key/value pair (arguments are treated as path string for 99% of lifetime)

Subject to change for typical url parameters e.g ?a=1&b=2

This feature is useful for the Discord RPC functionality, servers can create a party system using this or give rewards for joining via Discord (of course this can extend to other platforms).


Example

Have serverside script running:

SERVER_IP, SERVER_PORT, SERVER_PASS = "localhost", 22003, ""
DEFAULT_ARGS = {["foo"] = "abc", ["bar"] = "def"}

local storedArgs

function connect(nick, ip, username, serial, version, versionString, args)
    storedArgs = args
    iprint(args)
end
addEventHandler("onPlayerConnect", root, connect)

function redirect(player)
    redirectPlayer(player, SERVER_IP, SERVER_PORT, SERVER_PASS, (type(storedArgs) == "table") and storedArgs or DEFAULT_ARGS)
end
addCommandHandler("redirect", redirect)
Action Result
Player connects with mtasa://123.456.78.9:22003/foo/123/bar/456/baz/789 image
Player uses redirect command Same as above

Notes

  • To test mtasa:// protocol with debug build, edit registry Computer\HKEY_CLASSES_ROOT\mtasa\shell\open\command and set to your mtasa build path e.g "C:\mtasa-blue\Bin\Multi Theft Auto_d.exe"%1

To-do

  • Handle / in user provided keys/values, either remove character or URL encode? (/ is our delimiter so would affect converting to pairs)
    • / will be removed from user provided args
  • Fix argument parser issue when using std::unordered_map<type, type> for Lua args
  • Determine whether keys/values (protocol parameters) should be changed to ?a=1&b=2 format

The method used for URL parameters (key/value pairs delimited by slash) should be changed to use the standard URI parameter format ?foo=bar&baz=boz

This is going to require a lot of time to investigate how this is currently being used by various services, both public and private (internal). It will affect all areas, to do with downloads, updates, server connections, etc. This is the reason why I initially opted to use the key/value pairs delimited by slash, as it causes the least disruption to the client. This will be a major change, that fittingly should go into a major release.

@Inder00
Copy link
Contributor

Inder00 commented Oct 19, 2023

marge :shipit:

@fresholia
Copy link
Contributor

Also it should work with connect console command, as I see i can't connect the game with connect command.

@Fernando-A-Rocha
Copy link
Contributor

What about also changing redirectPlayer?

@Fernando-A-Rocha
Copy link
Contributor

well done, lgtm

@Lpsd
Copy link
Member Author

Lpsd commented Nov 16, 2023

the redirectPlayer functionality has uncovered an issue with the argument parser which needs resolving before this can be merged. See discussion on dev discord

@Lpsd
Copy link
Member Author

Lpsd commented Nov 16, 2023

Also it should work with connect console command, as I see i can't connect the game with connect command.

Works fine for me, can you confirm how you're using the command and what errors you're seeing? @fresholia

@Lpsd
Copy link
Member Author

Lpsd commented Nov 18, 2023

We can't use std::unordered_map<std::string, std::string> to retrieve table arguments in a Lua definition (because a script providing the wrong type ie {1, 2, 3} will cause a crash, and I couldn't find a solution for this).

For this reason I've changed to use CLuaArgument in the unordered_map, as it can handle any value from the Lua side, then convert this to a string using the already available CLuaArgument::GetAsString method

However, I did have to implement a hash function for CLuaArgument in order to use it with std::unordered_map - could do with some opinions on this; not sure if it's correct (approach and implementation-wise). This is an issue we inevitably need to tackle at some point anyway (accepting tables of any value using new arg parser)...

struct Hash
{
size_t operator()(const CLuaArgument& a) const
{
SString str;
if (!a.GetAsString(str)) // If we can't convert to a string, then use pointer address instead
str = std::to_string((unsigned long long)(void**)&a);
return std::hash<int>{}(a.GetType()) ^ (std::hash<std::string>{}(str) << 1);
}
};

@tederis
Copy link
Member

tederis commented Nov 19, 2023

We can't use std::unordered_map<std::string, std::string> to retrieve table arguments in a Lua definition (because a script providing the wrong type ie {1, 2, 3} will cause a crash, and I couldn't find a solution for this).

For this reason I've changed to use CLuaArgument in the unordered_map, as it can handle any value from the Lua side, then convert this to a string using the already available CLuaArgument::GetAsString method

However, I did have to implement a hash function for CLuaArgument in order to use it with std::unordered_map - could do with some opinions on this; not sure if it's correct (approach and implementation-wise). This is an issue we inevitably need to tackle at some point anyway (accepting tables of any value using new arg parser)...

struct Hash
{
size_t operator()(const CLuaArgument& a) const
{
SString str;
if (!a.GetAsString(str)) // If we can't convert to a string, then use pointer address instead
str = std::to_string((unsigned long long)(void**)&a);
return std::hash<int>{}(a.GetType()) ^ (std::hash<std::string>{}(str) << 1);
}
};

I think it would be better to fix a crash instead of constructing hash related complicated things. I hope my PR #3246 will help.

@@ -286,6 +288,9 @@ class CCore : public CCoreInterface, public CSingleton<CCore>
bool IsUsingCustomStreamingMemorySize();
size_t GetStreamingMemory();

void SetProtocolConnectArgs(const std::string&& args) { m_strProtocolConnectArgs = args; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, const rvalue reference doesn't make sense. Even without a const qualifier it wont work as expected because in the function's body args is actually lvalue reference. So you should remove const and cast args to the rvalue explicitly: SetProtocolConnectArgs(std::string&& args) { m_strProtocolConnectArgs = std::move(args); }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this.
Also, that custom hash function for CLuaArgument isn't good at all, and should be avoided at all costs.
Actually, and kind of comparasion/hashing of it should be avoided.

@@ -190,6 +190,9 @@ class CCoreInterface
virtual bool IsUsingCustomStreamingMemorySize() = 0;
virtual size_t GetStreamingMemory() = 0;

virtual void SetProtocolConnectArgs(const std::string&& args) = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same: const rvalue has no sense

@@ -44,6 +44,9 @@ class CPlayerJoinDataPacket final : public CPacket

bool IsOptionalUpdateInfoRequired() { return m_bOptionalUpdateInfoRequired; }

const char* GetConnectArgs() { return m_strConnectArgs; };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you return a string object reference?

@@ -1735,6 +1735,11 @@ void CGame::Packet_PlayerJoinData(CPlayerJoinDataPacket& Packet)
pPlayer->SetBitStreamVersion(Packet.GetBitStreamVersion());
g_pNetServer->SetClientBitStreamVersion(Packet.GetSourceSocket(), Packet.GetBitStreamVersion());

const char* connectArgs = Packet.GetConnectArgs();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This related to a CPlayerJoinDataPacket class problem: it's probably better to use a string.

@@ -335,6 +335,9 @@ class CPlayer final : public CPed, public CClient
const SString& GetQuitReasonForLog() { return m_strQuitReasonForLog; }
void SetQuitReasonForLog(const SString& strReason) { m_strQuitReasonForLog = strReason; }

void SetConnectArgs(const SString&& args) { m_strConnectArgs = args; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same problem with const rvalue ref

const char* connectArgs = Packet.GetConnectArgs();

if (std::strlen(connectArgs) > 0)
pPlayer->SetConnectArgs(std::move(Packet.GetConnectArgs()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to remove std::move here because Packet_PlayerJoinData(and even CGame) method doesn't own this string.

@@ -240,6 +240,20 @@ void CInputRPCs::ForceReconnect(NetBitStreamInterface& bitStream)

if (bitStream.Read(usPort))
{
char* szArgs = new char[MAX_PROTOCOL_CONNECT_ARGS_LENGTH + 1];
Copy link
Member

@tederis tederis Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we have a memory leak here. Because szArgs isn't deleted below.
Maybe a preallocated std::string is the solution? It's null terminated and guaranteed to contiguous since C++11 and we have non-const ::data member since C++17(please, use it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's even better to use bitStream.ReadString() (with the modification of a packet itself).

@StrixG StrixG added the enhancement New feature or request label Dec 21, 2023
@Dutchman101
Copy link
Member

Here's the last status update on this PR:

Dutchman101 — 01/07/2024 1:42 AM
If you address the final Dec 1 code reviews on #3211, it can be merged

lopsi — 01/07/2024 3:04 PM
there is more to do than that
I would like to use other arg format, ?foo=bar&baz=boz
I know it's possible, I already started working on it, but ran into few issues
MTA updater also uses this format in some weird way, I would like to change it too

@Nico8340
Copy link
Contributor

Nico8340 commented Apr 7, 2024

Any updates on this? Would be a really nice feature.

@Inder00
Copy link
Contributor

Inder00 commented Jan 10, 2025

Bump

@lynconsix
Copy link

Bump?

@arisudesu
Copy link

Why would you use this weird syntax with slashes? Aren't usual ampersands enough, as seen in URLs?

What if an argument is specified twice, what value will be in the table - first or last? What if it is specified thrice? E.g. a /arg/1/arg/2/arg/3 or a /arg[]/1/arg[]/2/arg[]/3?

Are all the values of arg[] possible to be extracted? Real-world examples which I know are:

  • PHP where repeated keys suffixed with [] in the query string form an array;
  • Golang, we have net/url.Values which is a map of keys to subarrays, that is similar to PHP but doesn't require [];
  • JavaScript we have URLSearchParams.getAll

and so on.

What if an argument does not have value, then how will it be combined with the arguments following it - like this: /novaluearg//valuearg/1?

Why not pass your "query" string as is and let the server/client script decide how to parse it? And to implement general querystring-like parsing, just add a new fromURLQuery utility function.

Don't reinvent the wheel please, we all (mostly) are used to web URLs and they do their job well, use them.

@arisudesu
Copy link

/ will be removed from user provided args

What is syntax if one wants to encode slash to an argument key or a value? Is it possible at all, and if it is, how? A usual percent-encoding, as in web URLs?

@Lpsd
Copy link
Member Author

Lpsd commented Feb 11, 2025

Why would you use this weird syntax with slashes? Aren't usual ampersands enough, as seen in URLs?

Requires much more thought and effort to ensure compatibility with existing services which utilize the standard protocol/parameters, such as updates. We'll have to change code surrounding that which hasn't changed in a while.

Also please refer to existing comments to save time #3211 (comment)

I haven't had much interest in this lately so, if you feel motivated, feel free to pick this up!

@Lpsd
Copy link
Member Author

Lpsd commented Feb 11, 2025

All of your other questions can be answered by taking a glance at CLuaPlayerDefs::ArgsToString and testing using what's provided. Not that it is relevant, as the intention was already to use standard URI parameters instead.

@Lpsd Lpsd marked this pull request as draft February 11, 2025 20:41
@Lpsd Lpsd added the help wanted Extra attention is needed label Feb 11, 2025
@Lpsd
Copy link
Member Author

Lpsd commented Feb 11, 2025

I've converted to draft and updated the description so there's no confusion on the state of this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.