Jump to content

SU Command Metin2 System


Alina

Recommended Posts

M2 Download Center

This is the hidden content, please
( Internal )

Hi everybody!

 

Tired of chaotic servers just because someone broke into a gm account?

With the following guide we'll stop that once and for all!

 

 

Quote

*** Disclaimer (kinda) ***

I hereby declare that the changes I made are all by myself. I did not steal from anyone. Therefore for this guide I am the author. If anyone wants to copy my guide and post it anywhere he's free to do as long as he mentions the original author. I do not provide or share the source code or anything else protected by copyright.

 

If something breaks I'm not the one to blame at. Always make sure to test changes. Never implement them on production releases, always use test distributions before! If you find any flaws, may it be regarding security or something else you're free to tell me so. I'm learning, as we all do, so I'm in no way too proud to admit I'm making mistakes. I'll correct them as soon as possible of course.

 

1.) What's the idea behind that?

Most systems (I can't imagine any system who doesn't do it at least a little bit like this) allow you to elevate processes or users and temporarily grant them higher permissions. Metin2 doesn't do something like that. If you get into a gm account (and you don't use ip binding which in most cases is useless) you'll most likely get full access to his permissions. Though many admins do limit the rights of their team members, some are left with higher permissions and you never know what happens.

So what to do? We implement such an elevation system into metin2.

It's nothing that big. We just simply add a new column to our gmlist. This will store a special password for every gamemaster. If there's no special password set, elevation won't have any effect and the user is left with special permissions permanently.

If you log in, you'll have to further use the new su-command to elevate yourself. This command takes the password as an argument. If it's the correct password, the player will be elevated for the current session. If he logs out, he'll have to run su again. Without su, he won't be able to use any of his privileges.

Most important are IMPLEMENTOR characters. LOW_WIZARD most likely won't need elevation so you can simply leave the password field blank for those guys.

 

Additionally on testing environments you can specify a special passphrase. People can't use gm commands unless they elevate with using su and providing it with your passphrase - but they will be able to have every other feature of your testing environment (e. g. debug logs, shorter waiting times etc..)

 

But enough of that! Let's get the party started! After each section you will be able to compile your game and test it.

 

2.) What do we need to edit?

- char.h

- char.cpp

- cmd_general.cpp

- config.cpp

- config.h

 

 

3.) Add elevation status

In this section we will add the elevation status as a bool variable.

Files to be edited:

- char.cpp

- char.h

 

Open char.h

Find

DWORD m_dwLastGoldDropTime;

and add:

bool bIsElevated;

 

Add the following functions in any public-section below:

 

void SetElevation(bool elevate) { bIsElevated = elevate; }
bool GetElevation() { return bIsElevated; }

 

Now close char.h and open char.cpp

Find

m_dwLastGoldDropTime = 0;

and add below:

bIsElevated = false;

 

 

That's all! Compile it and you'll notice.. Nothing. We just simply add a new variable. Well. At least it's a beginning! :)

 

4.) Manage elevation

In this section we will allow people to use the su command for elevation. It won't ask for a password yet, that'll be added in the next section.

Files to be edited:

- char.cpp

- char.h

- cmd.cpp

- cmd_general.cpp

 

Note: We edit cmd_general because users will get normal privileges when logging in. So restricting the command to gamemaster doesn't make any sense. Though the command will do nothing unless they really have privileges (that's why we need a new command to determine if the player using su actually can receive higher privileges.

 

First open char.cpp and find the following function:

BYTE CHARACTER::GetGMLevel() const

 

First remove the const at the end of the declaration (also make sure you remove it at char.h). Add at the beginning of the function the following statement:

 

if(GetElevation() == false)
	return GM_PLAYER;

 

By now people won't receive the status of a gm anymore. They first have to elevate. But how? Let's do this! First close char.cpp

 

Open cmd.cpp and add the following ACMD anywhere near the ACMD section:

ACMD (do_elevate);

 

Now add the following entry to the cmd list:

{ "su", do_elevate, 0, POS_DEAD, GM_PLAYER },

 

Close cmd.cpp and open cmd_general.cpp. There you can add the following function:

 

ACMD(do_elevate)
{
	if(ch->GetElevation()==true)
		return;

	ch->SetElevation(true);
	ch->ChatPacket(CHAT_TYPE_INFO, "You have been elevated!");
}

 

That'll allow our players to elevate. But! We don't want normal players to elevate. So we need something to determine if they are worthy at all. We need a true function that displays the gm level without having any impact on the source itself. We'll add that!

 

First open char.cpp and add our new function:

BYTE CHARACTER::GetRealGMLevel() const
{
	if (test_server)
		return GM_IMPLEMENTOR;

	return m_pointsInstant.gm_level;
}

 

Note that this function is const (unlike our new GetGMLevel() function)

Also add this function inside a public part of your char.h:

BYTE GetRealGMLevel() const;

 

Now we can close char.h and char.cpp. Open cmd_general.cpp and edit our function. Add to the beginning of it:

 

if(ch->GetRealGMLevel() == GM_PLAYER)
	return;

 

That's all! Compile and Test it. By now the following should occur:

At login you won't have gm permission.

You will be able to use the su command. By doing so, you'll receive a message that you have been elevated.

If you are a normal player, elevation won't work.

There's currently no special password or passphrase.

 

5.) Securing elevation

In this part we'll add a passphrase to our gmlist! What we need to edit:

- char.cpp

- char.h

- cmd_general.cpp

 

But first we need to run the following SQL statement:

 

ALTER TABLE common.gmlist
ADD passphrase varchar(45) DEFAULT "";

 

 

You can now give your gm characters a special passphrase. How you build up your passphrase is up to you (plain text is not recommended! Best solution is something like salt + mysql password function).

For this guide I'll just use the mysql password function but you'd adapt it to your needs ;)

Example query to set a passphrase:

 

UPDATE common.gmlist SET passphrase=PASSWORD('your_pass') WHERE mName='targetcharactername';

 

After doing so we'll now write the function to elevate!

Add

void DoElevate(char* pwd);

To your char.h inside a public section. You can close it and open char.cpp

 

There we add our new DoElevate()-function.

 

 

void CHARACTER::DoElevate(char* pwd)
{
	char szQuery[1024];
	snprintf(szQuery, sizeof(szQuery), "SELECT mID FROM common.gmlist WHERE mName='%s' AND (passphrase='' OR passphrase=PASSWORD('%s'))", GetName(), pwd);
	std::unique_ptr<SQLMsg> result( DBManager::instance().DirectQuery(szQuery) );
	if (result->Get()->uiNumRows == 1)
	{
		ChatPacket(CHAT_TYPE_INFO, "You have been elevated!");
		sys_log(0, "Player %s elevated successful!", GetName());
		SetElevation(true);
		return;
	}
	sys_err("Player failed to elevate! %s with %s result %d", GetName(), pwd, result->Get()->uiNumRows);
	ChatPacket(CHAT_TYPE_INFO, "Failed to elevate!");
}

 

Now we can close char.cpp and open cmd_general.cpp

There we navigate to our previously added ACMD sequence and replace it:

 

ACMD(do_elevate)
{
	if(ch->GetRealGMLevel() == GM_PLAYER)
		return;
	if(true == ch->GetElevation())
		return;
	char a1[256];
	one_argument(argument, a1, sizeof(a1));
	if(!*a1)
	{
		char safe[] = "nopass";
		ch->DoElevate(safe);
	} else {
		ch->DoElevate(a1);
	}
}

Now we can close everything and compile. Get ready to elevate! :)

 

6.) (optional) Elevating with testserver

 

In this section we'll make elevating compatible with testserver. You can simply provide a special key you can change everytime (we'll do that via config, that's the best way!) you want to. People need to elevate themselves by using this key. Note that you'll get everything in your syslog, so no one can do something bad ;)

 

What we need to edit:

- char.cpp

- config.cpp

- config.h

 

First open config.h. We'll declare our variable:

extern std::string ElevationKey;

 

Then we can close config.h

Let's open config.cpp and add our new variable:

string ElevationKey = "testkey";

 

Now we only need to be able to modify our variable. Let's do that too.

Find

TOKEN("pk_protect_level")

and add under the block our new token:

 

TOKEN("elevation_key")
{
	ElevationKey = value_string;
	fprintf(stderr, "ELEVATION_KEY: %s", ElevationKey.c_str());
}

 

Close config.cpp too. We can now open char.cpp and edit our DoElevate()-function. Add to the beginning of our function:

 

if(test_server)
{
	extern std::string ElevationKey;
	if(strcmp(pwd, ElevationKey.c_str()) == 0)
	{
		ChatPacket(CHAT_TYPE_INFO, "You have been elevated!");
		sys_log(0, "TESTSERVER! Player %s elevated successful!", GetName());
		SetElevation(true);
		return;
	}
}

 

Note that while test server is active, both su-methods are allowed. You can either su with the passphrase or, if you do have a gm account, then you can use your passphrase.

 

7.) Fixing elevation in quests

Thanks to Think I was able to think (joking with words, huh?) about a problem I didn't notice earlier: When logging in, quests requiring gm permissions on login won't work anymore. We first have to elevate before we are registered as a GM.

 

How are we going to fix it?

Easy! We just have to edit questlua_pc.cpp

 

Find

pc_is_gm()

and

pc_get_gm_level()

 

Replace the function name GetGMLevel() with our real function: GetRealGMLevel()

That's all for fixing that! But wait! We want to be secure, right?

For this, we just add two new quest functions!

	int pc_is_gm_safe(lua_State* L)
	{
		LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr();
		lua_pushboolean(L, ch->GetGMLevel() >= GM_HIGH_WIZARD);
		return 1;
	}

	int pc_get_gm_level_safe(lua_State* L)
	{
		LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr();
		lua_pushnumber(L, ch->GetGMLevel());
		return 1;
	} 

They're... well.. they're the same than our old functions. We just changed the old functions to make sure quests will work as they did before. Now we can use the safe equivalent outside of login calls (e. g. when you're using it in a chat-trigger or button/info).

Just add them to our list and we're finished!

			{ "is_gm_safe",			pc_is_gm_safe		},
			{ "get_gm_level_safe",		pc_get_gm_level_safe		},

Compile and run the gamefile. Quests will work like they did before! ;)

 

Happy elevating! :)

  • Metin2 Dev 1
  • Good 1
  • Love 13
Link to comment
Share on other sites

Good concept, good implementation :)

 

To contirbute with my two cents, it'd be a bit better if you implemented auth for the whole session the GM is on and not just until teleport (i.e storing elevation outside of CHARACTER, and... for example making the auth expire after 10 minutes after logout or so, or if the login comes from a new IP), as it can get annoying to have to reenter the password over and over just to get gm powers. It also has the issue of not having gm auth when you login (quest-wise)

  • Love 1
Link to comment
Share on other sites

Oh, sharp thinking you've got there! Maybe I'd work it out like this:

- Adding new quest functions pc.is_gm_safe() and pc.get_gm_level_safe() for calling safety functions. They'd use the protected GetGMLevel() function

- Altering the old pc.is_gm() and pc.get_gm_level() functions so they use our new GetRealGMLevel() function. So every quest should work like before (I'm a mad genious :D)

 

That'd at least fix the problems with quests.

Your idea with storing it via auth is really good. I'd give it a shot and see if it works. I mean, the clientdesc wouldn't change at all. So it'd be stored there. Modifying would be easy too. What do you think?

 

Of course: Thanks for the good feedback! :)

Link to comment
Share on other sites

Your quest backwards compatibility idea sounds fine, yup! Nothing too critical is usually done on login either, so it wouldn't expose much to support.

 

Your idea with storing it via auth is really good. I'd give it a shot and see if it works. I mean, the clientdesc wouldn't change at all. So it'd be stored there. Modifying would be easy too. What do you think?

I didn't exactly suggest to store it via auth even though I was thinking of something like that (But not really indepth as now xD).

 

There's an inherent problem with Metin2's auth where it is impossible to track someone after they teleport. There's no linking of one desc to the other, and db also receives a logoff. For all intents and purposes, a rewarp is almost like the player closed the client and logged back in (directly into their char that is). The second problem lies with passing around things on login: login is this continuous bouncing of packets and checks. Not fun to work with in my experience.

 

Nevertheless, you made it sound easy. If you know some existing way to keep track of the user (which I could have missed somehow!), please do tell!

 

However, as far as I'm aware, there are not too many options.

Way 1

  • Elevate -> notify db -> which notifies auth -> which stores IP and current timestamp.
  • Login, which goes through auth, checks if the player was already elevated (recently), and if the elevation was on the same IP, marks the user as elevated in the login.

The last part, well, I implemented a few things in login already, and I really don't like working with it much. But this way is definitely better than...

 

Way 2 (less painful, less clean as well)

  • Elevate -> Notify all P2P peers -> store it in some map again with IP and timestamp (Even maybe start an event that clears the map entry).
  • Login -> On PlayerLoad, check the map. Profit!
Link to comment
Share on other sites

 

Your quest backwards compatibility idea sounds fine, yup! Nothing too critical is usually done on login either, so it wouldn't expose much to support.

 

Your idea with storing it via auth is really good. I'd give it a shot and see if it works. I mean, the clientdesc wouldn't change at all. So it'd be stored there. Modifying would be easy too. What do you think?

I didn't exactly suggest to store it via auth even though I was thinking of something like that (But not really indepth as now xD).

 

There's an inherent problem with Metin2's auth where it is impossible to track someone after they teleport. There's no linking of one desc to the other, and db also receives a logoff. For all intents and purposes, a rewarp is almost like the player closed the client and logged back in (directly into their char that is). The second problem lies with passing around things on login: login is this continuous bouncing of packets and checks. Not fun to work with in my experience.

 

Nevertheless, you made it sound easy. If you know some existing way to keep track of the user (which I could have missed somehow!), please do tell!

 

However, as far as I'm aware, there are not too many options.

Way 1

  • Elevate -> notify db -> which notifies auth -> which stores IP and current timestamp.
  • Login, which goes through auth, checks if the player was already elevated (recently), and if the elevation was on the same IP, marks the user as elevated in the login.

The last part, well, I implemented a few things in login already, and I really don't like working with it much. But this way is definitely better than...

 

Way 2 (less painful, less clean as well)

  • Elevate -> Notify all P2P peers -> store it in some map again with IP and timestamp (Even maybe start an event that clears the map entry).
  • Login -> On PlayerLoad, check the map. Profit!

 

In your way 1 you're just taking a worthless step. Plus, auth shouldn't store nothing at all really, the auth core is only meant to Authenticate the user on login, and maybe send some of those hybrid keys, all the rest is done on db and game cores.

Your second option is the most logical one, and the best approach as well.

Link to comment
Share on other sites

I never worked with the login at all :D So I can't tell if it's possible or easy. I thought desc would be kept while switching maps but well... Turned out to be false :D

 

How about caching?

 

I'd define a tmp directory and write a cache-file with the name of our elevated user. It'd be easy to read the timestamp of it and compare it to the current time. After logging in, I'd automatically let the server look for the cache file and if there is one, it'd check it's creation date. If it's too old - delete and not elevate. If it's recent enough - just elevate. A second command could be introcuded, something like /return to revert the su status. It's meant for when logging off (though the timestamp does it's way). Executing commands could also keep the timestamp up to date so the user won't be kicked out of elevation status while working.

What do you guys think about that?

Link to comment
Share on other sites

If you don't want to keep it on auth, alright keep it on db, that's a matter of taste - it's just as painful to make it bounce around in all the packets :P

 

I disagree that the second option is the best approach. You are sending information over P2P, which is not as reliable as a db connection, to all cores, something they will probably not ever use. Just for that I prefer the first one.

 

How about caching?

 

I'd define a tmp directory and write a cache-file with the name of our elevated user. It'd be easy to read the timestamp of it and compare it to the current time. After logging in, I'd automatically let the server look for the cache file and if there is one, it'd check it's creation date. If it's too old - delete and not elevate. If it's recent enough - just elevate. A second command could be introcuded, something like /return to revert the su status. It's meant for when logging off (though the timestamp does it's way). Executing commands could also keep the timestamp up to date so the user won't be kicked out of elevation status while working.

What do you guys think about that?

It's a possibility, but I would prefer keeping all the cores up to date with the elevations (which is simple enough) rather than having to worry about creating and deleting files.

 

Edit: Well, upon reflection, miguelmig, I do agree that sending everything everywhere for the simplicity it carries with it might be worth a second thought.

Link to comment
Share on other sites

If you don't want to keep it on auth, alright keep it on db, that's a matter of taste - it's just as painful to make it bounce around in all the packets :P

 

I disagree that the second option is the best approach. You are sending information over P2P, which is not as reliable as a db connection, to all cores, something they will probably not ever use. Just for that I prefer the first one.

 

How about caching?

 

I'd define a tmp directory and write a cache-file with the name of our elevated user. It'd be easy to read the timestamp of it and compare it to the current time. After logging in, I'd automatically let the server look for the cache file and if there is one, it'd check it's creation date. If it's too old - delete and not elevate. If it's recent enough - just elevate. A second command could be introcuded, something like /return to revert the su status. It's meant for when logging off (though the timestamp does it's way). Executing commands could also keep the timestamp up to date so the user won't be kicked out of elevation status while working.

What do you guys think about that?

It's a possibility, but I would prefer keeping all the cores up to date with the elevations (which is simple enough) rather than having to worry about creating and deleting files.

 

Edit: Well, upon reflection, miguelmig, I do agree that sending everything everywhere for the simplicity it carries with it might be worth a second thought.

 

Don't wanna offend you or anything, but P2P packet is nothing more than Game -> DB -> All other Game Cores, plus, all the game cores should know if the player is indeed elevated, in case the player logs in to those cores, he should already be elevated.

 

If the elevation is a one-time thing ( the player would remain elevated after the server shutdowns ), the elevated accounts should be stored either:

1. in a cache file ( not a single file for each player as you said )

or 

2. in a mySQL table.

 

You should never keep any information in auth, auth is the busiest connection/core of them all, it's the most prone to overflowing and shouldn't be responsible for that data.

Link to comment
Share on other sites

I personally don't like using mysql for everything. Elevation already depends on a query to gmlist, I don't like the idea of stressing the dbcache unnecessarily.

Also the gamecores won't need to be informed when using cachefiles. They already do know because all they have to do is check if the file exists and if it's creation date is recent enough. That's all. If the requirements are fulfilled -> elevation. Otherwise the system can also clear the elevation cache automatically on shutdown.

It's no big deal of creating/deleting files as it's not that much to manage. Creation is only relevant when the player elevates, deletion doesn't matter since the file date is checked (but of course we want to keep it clean so yeah, that's why I said deleting it on shutdown could be the best way to just clean our mess up a bit ;))

Also it would be a way faster. The cores don't need to have a cache file for every instance, we just use something easy like /tmp/elevation and store the cachefiles there (just an example :D). Additionally, as long as we just check if the file exists and what creation date it has, we won't have to deal with access errors (e. g. when the file is already accessed by something else)

  • Love 1
Link to comment
Share on other sites

You should never keep any information in auth, auth is the busiest connection/core of them all, it's the most prone to overflowing and shouldn't be responsible for that data.

You can create slave auths which largely reduce their load. But fair point!

 

 

Don't wanna offend you or anything, but P2P packet is nothing more than Game -> DB -> All other Game Cores, plus, all the game cores should know if the player is indeed elevated, in case the player logs in to those cores, he should already be elevated.

A P2P packet to let's say all 13 cores (I'm thinking of the standard YMIR distribution with 4 channels, 3 cores each + a shared game99) are, well, 12 packets. And each core has to create their own map, and optionally start events to kill the auth off (but this is just optional).

A packet to DB is one packet. DB keeps the elevation associated with the player, and sends it on the login process if it is a valid elevation.

 

Edit: Ah and it's ok, I'm fine with arguing what would the best thing be :P!

Edit2: Aa I missed Alina's post.

 

Clearing the elevation files on shutdown sounds fine, but if you are going to check the changed date, then you need to keep refreshing it (just pointing this out because you mentioned "creation date" and that wouldn't cut it for the second, third, fourth elevations xD). It's not a bad solution and possibly more interesting than keeping a single cache file in terms of simplicity (no need to worry with reading the file or anything, well thought out!).

 

Overall I think that the three possibilities (DB, P2P, file) are somewhat equivalent to me and I really couldn't myself say anymore one is clearly better than the other. DB one is more centralized but its more complex, P2P informs everyone but its simple, and files are simple as well but I don't like creating files (for no particular reason, that's just me xD)

Edited by Think
Link to comment
Share on other sites

I was thinking a bit too high with refreshing them. The only need of refreshing is a clean new elevation. Everything else won't be needed. If you keep logged in, you won't lose elevation status unless you warp and for security reasons people should be careful with using their elevation status. So refreshing it with every command won't be needed at all I guess.

 

Yeah, it's just my "best way to approach" because I have no doubts in implementing it like this :D Maybe I'm just too lazy for something more difficult

Link to comment
Share on other sites

  • Premium

Hm... What about keeping data in a map in CHARACTER_MANAGER (player id and a timestamp used to check if elevation expired or something)? I don't think there's a problem if you keep those data in a single channel, even if you may change channel to 99 and back (like dungeons, OX map). Also, you can send packets to all channels to store those data. I am sorry if I missed something xD

Link to comment
Share on other sites

Finally! I changed the quote tag to code so there should be no problems anymore :D

 

I'm working on a cache-based solution. If someone's bored enough or wants to contribute I'd be glad to read their solutions as code. Of course, if you're okay with that, I'd add them to the topic too so people can decide what they like the most! :)

  • Love 1
Link to comment
Share on other sites

  • 1 year later...

Announcements



×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.