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

Implement initial login for ROS v6.43+ #37

Open
aTanCS opened this issue May 31, 2018 · 24 comments
Open

Implement initial login for ROS v6.43+ #37

aTanCS opened this issue May 31, 2018 · 24 comments
Assignees

Comments

@aTanCS
Copy link

aTanCS commented May 31, 2018

See https://wiki.mikrotik.com/wiki/Manual:API#Initial_login
But I'm not sure if it is possible to get ROS version before login. But without this change it won't be possible to use Net_RouterOS with v6.43+

@boenrobot
Copy link
Member

Thanks for the heads up. I hadn't seen the latest RC... I probably would've started implementing this shortly after the release of 6.43.

From a test with 6.42, it appears it's possible to make the login agnostic to RouterOS version - the new method (PAP) will be attempted first, and if there is no error and a challenge, then the client can fallback to the pre-6.43 login (CHAP), which is what I will do.

@boenrobot boenrobot self-assigned this Jun 3, 2018
@sieberlukas
Copy link

sieberlukas commented Jun 5, 2018

Hi, i already updated script to login with version 6.43.

private static function _login(
    Communicator $com,
    $username,
    $password = '',
    $timeout = null
) {
    $newVersion = false;

    $request = new Request('/login');
    $request->setArgument('name', $username);
    $request->setArgument('password', $password);
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
    if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
        // version >= 6.43
        $newVersion = true;
    } else {
        // version < 6.43
        $request->setArgument('password', '');
        $request->setArgument(
            'response',
            '00' . md5(
                chr(0) . $password
                . pack('H*', $response->getProperty('ret'))
            )
        );
        $request->verify($com)->send($com);
        $response = new Response($com, false, $timeout);
    }

    if ($response->getType() === Response::TYPE_FINAL) {
        if ($newVersion) {
            return null === $response->getProperty('message');
        } else {
            return null === $response->getProperty('ret');
        }
    } else {
        while ($response->getType() !== Response::TYPE_FINAL
            && $response->getType() !== Response::TYPE_FATAL
        ) {
            $response = new Response($com, false, $timeout);
        }
        return false;
    }
}

@RAD-X
Copy link

RAD-X commented Jul 13, 2018

If new version of ROS require unsecured password challenge, It look like that RouterOS in some newer versions deprecate or remove unsecured API.
If I remember that, secure connection a few years ago was not very stable in Net_RouterOS (some PHP limits...).
So I have small question about this. Is secured communication in Net_RouterOS stable now?

@boenrobot
Copy link
Member

boenrobot commented Jul 14, 2018

@RAD-X Newer PHP versions (everything 7.2 and above, plus some higher earlier ones) are better, in that if you keep the payload under around 512KBs per request/response cycle, it should work fine. It's still not exactly stable for all intents and purposes. Notable use cases that are likely to spontaneously cause errors are anything that involves long running continuous commands like "torch".

Both before and now, if you want an encrypted API connection with PHP, it's better to set up the web server and router so that they are both clients in an encrypted VPN, and connect with an unencrypted connection inside the encrypted VPN.

@mrambossek
Copy link

i am really curious why mikrotik opted for a pap-like login, kind of forcing us to go SSL soon.
as a colleague of mine pointed out to me, there are standardized methods like SCRAM ( https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism ) available that require neither storing the plaintext password on the "server", nor the client sending it over the wire.

i did actually mail mikrotik about this. while they are generally VERY nice and helpful, on this particular request i basically got a "dont worry, in the future, passwords will be encrypted!" (?) reply. sad :x

@sieberlukas
Copy link

Version 6.43 has been released.

@brsnik
Copy link

brsnik commented Sep 15, 2018

Hi, i already updated script to login with version 6.43.

private static function _login(
    Communicator $com,
    $username,
    $password = '',
    $timeout = null
) {
    $newVersion = false;

    $request = new Request('/login');
    $request->setArgument('name', $username);
    $request->setArgument('password', $password);
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
    if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
        // version >= 6.43
        $newVersion = true;
    } else {
        // version < 6.43
        $request->setArgument('password', '');
        $request->setArgument(
            'response',
            '00' . md5(
                chr(0) . $password
                . pack('H*', $response->getProperty('ret'))
            )
        );
        $request->verify($com)->send($com);
        $response = new Response($com, false, $timeout);
    }

    if ($response->getType() === Response::TYPE_FINAL) {
        if ($newVersion) {
            return null === $response->getProperty('message');
        } else {
            return null === $response->getProperty('ret');
        }
    } else {
        while ($response->getType() !== Response::TYPE_FINAL
            && $response->getType() !== Response::TYPE_FATAL
        ) {
            $response = new Response($com, false, $timeout);
        }
        return false;
    }
}

Where would one implement this? Also will it slow down the request at all considering its checking the version in order to set $newVersion?

@boenrobot
Copy link
Member

boenrobot commented Sep 16, 2018

@sieberlukas
Could you make pull request to the develop branch for this please?

To be perfectly honest, I find your solution somewhat duct-tape-ish, but at the same time, I keep finding myself with not enough time to make a more robust and future proof implementation, so if you make a pull request, I'll merge it.

@borsn
If this lands in the develop branch, you'd be able to have it by getting the "develop" version with Composer. I would very much advise against manually patching the source, regardless of the installation method you used. If nevertheless, you insist on doing it, the login() method is in Client.php.

@sieberlukas
Copy link

@boenrobot
I try make pull request, but: Permission to pear2/Net_RouterOS.git denied to sieberlukas.

I edited the script so that it was not so duck-tape-ish?

private static function _login(
    Communicator $com,
    $username,
    $password = '',
    $timeout = null
) {
    $request = new Request('/login');
    $request->setArgument('name', $username);
    $request->setArgument('password', $password);
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
    if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
        // version >= 6.43
        return null === $response->getProperty('message');
    } elseif ($response->getType() === Response::TYPE_FINAL) {
        // version < 6.43
        $request->setArgument('password', '');
        $request->setArgument(
            'response',
            '00' . md5(
                chr(0) . $password
                . pack('H*', $response->getProperty('ret'))
            )
        );
        $request->verify($com)->send($com);
        $response = new Response($com, false, $timeout);
        if ($response->getType() === Response::TYPE_FINAL) {
            return null === $response->getProperty('ret');
        }
    }
    while ($response->getType() !== Response::TYPE_FINAL
        && $response->getType() !== Response::TYPE_FATAL
    ) {
        $response = new Response($com, false, $timeout);
    }
    return false;
}

@boenrobot
Copy link
Member

boenrobot commented Sep 16, 2018

@sieberlukas
To make a pull request in GitHub, you'd first make a fork, do the edits in your fork's develop branch (or a new branch based on the develop branch, if you're feeling proper; it's the same for me). GitHub should offer you to make a pull request then. Pick pear2/Net_RouterOS as the base fork, "develop" as the base branch, sieberlukas/Net_RouterOS as the compare fork, and the branch where you commit the change as the compare branch.

When I say duct-tape-ish, I'm not talking about the method itself, but the whole login process... The method itself is fine (which is why I'll accept a PR). But I'm thinking it will be best to somehow have a dedicated static method for each login method (i.e. allow users to call only a specified method), and have "login" be a version agnostic way that will try all methods before giving up. The hard part (due to which I haven't done this already) is to design the architecture in such a way that, if a 3rd login method appears, one can define it in their own class or callback, and register it with "login" to be performed before falling back to the previously registered methods... And all of that in a way that lets each method work regardless of whether it's in or not in a persistent connection.

Furthermore, this new method has error replies, while the old one didn't. It would be good if error replies (in full, not just their messages) are exposed to the caller in some fashion, but at the same time, there should be a way to indicate general failure for the old method, where you know there's an error, but no message to go along with it.

@sieberlukas
Copy link

#42

@boenrobot
Copy link
Member

Merged. Thank you.

@boenrobot boenrobot pinned this issue Apr 5, 2019
@Yentel
Copy link

Yentel commented Jun 6, 2019

If this lands in the develop branch, you'd be able to have it by getting the "develop" version with Composer.

I've switched to the develop branch but now, after crawling a list of mikrotik devices, I get this as reply:
This is not a compatible RouterOS service

Any way this is related to this change request and what might be causing it? Could it be version related?

@boenrobot
Copy link
Member

The most typical reason for the "This is not a compatible RouterOS service" error is that the API port isn't actually used by the API, but by something else, be it the router's HTTP server, SSH server... Also, if you're using the api-ssl port but haven't declared so in the constructor or vice-versa.

@Yentel
Copy link

Yentel commented Jun 8, 2019

The most typical reason for the "This is not a compatible RouterOS service" error is that the API port isn't actually used by the API, but by something else

The API port is not used for anything else, even devices with an out of the box default config do not work on this beta version. I've switched back to the master branch and all these devices work fine again. Only devices on 6.45beta54 no longer want to connect (invalid user/password).
My user can login through winbox/ssh but using these credentials for API does not work. User group is set to full.

Any idea what changed during this beta that impacts this?
(Sorry if this is the wrong place to discuss this)

@boenrobot
Copy link
Member

I haven't looked into it at all yet.

If you could run "roscon" over the router without login, and attempt to actually do the login via the new method, we might get some idea of what's going on, since roscon will display the raw data, including possibly the unrecognized control byte that made it trip up.

@boenrobot
Copy link
Member

boenrobot commented Jul 7, 2019

@Yentel I set up a new VM with 6.45.1, and ran my whole unit tests, and something interesting popped up with regards to the "incompatible service error"...

It seems MikroTik's new password method causes problems when the password contains non-ASCII characters and you're using charset conversion. PHP's iconv() fails to convert the password, causing an exception to be thrown, which is then caught, and because the error is during the login, and is not a recognized one (like "wrong password"), the conclusion is that this is not a compatible API service.

Since the new method is always tried first, before a fallback to the old one, this problem exists even if your router is older than 6.43. What matters only is that your password needs charset conversion.

If that's your issue, then the solution for now is to change the password to one limited to only ASCII characters, and remove calls to setDefaultCharset in favor of setCharset after the login.

@esmatullaharifi
Copy link

According to the new Router OS version, the login parameters changed. Now we need to send the password with the name of the password instead of response and the encryption is not needed.
$request = new Request('/login'); $request->send($com); $response = new Response($com, false, $timeout); $request->setArgument('name', $username); $request->setArgument( 'password', $password ); $request->verify($com)->send($com);
Link to source: https://github.com/pear2/Net_RouterOS/blob/8af33b009ec51c09d3bc09f3986034f19f7c439c/src/PEAR2/Net/RouterOS/Client.php#L293

@germon
Copy link

germon commented Jul 25, 2019

Is a phar file including the fix available for download ?

@ivosgem
Copy link

ivosgem commented Dec 11, 2019

Work for me. Thanks!

Hi, i already updated script to login with version 6.43.

private static function _login(
    Communicator $com,
    $username,
    $password = '',
    $timeout = null
) {
    $newVersion = false;

    $request = new Request('/login');
    $request->setArgument('name', $username);
    $request->setArgument('password', $password);
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
    if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
        // version >= 6.43
        $newVersion = true;
    } else {
        // version < 6.43
        $request->setArgument('password', '');
        $request->setArgument(
            'response',
            '00' . md5(
                chr(0) . $password
                . pack('H*', $response->getProperty('ret'))
            )
        );
        $request->verify($com)->send($com);
        $response = new Response($com, false, $timeout);
    }

    if ($response->getType() === Response::TYPE_FINAL) {
        if ($newVersion) {
            return null === $response->getProperty('message');
        } else {
            return null === $response->getProperty('ret');
        }
    } else {
        while ($response->getType() !== Response::TYPE_FINAL
            && $response->getType() !== Response::TYPE_FATAL
        ) {
            $response = new Response($com, false, $timeout);
        }
        return false;
    }
}

@freedarwuin
Copy link

Who helps me I am using PEAR2 which does not work for Mikrotik version 6.46.3 here the code that I must change

private static function _login(
Communicator $com,
$username,
$password = '',
$timeout = null
) {
$request = new Request('/login');
$request->send($com);
$response = new Response($com, false, $timeout);
$request->setArgument('name', $username);
$request->setArgument(
'response',
'00' . md5(
chr(0) . $password
. pack('H*', $response->getProperty('ret'))
)
);
$request->send($com);
$response = new Response($com, false, $timeout);
return $response->getType() === Response::TYPE_FINAL
&& null === $response->getProperty('ret');
}

@ubntomar
Copy link

ubntomar commented Feb 25, 2020

Thanks , i replaced all this code in PEAR2_Net_RouterOS-1.0.0b6/src/PEAR2/Net/RouterOSClient.php and now i can login to my rb:

private static function _login(
Communicator $com,
$username,
$password = '',
$timeout = null
) {
$newVersion = false;

$request = new Request('/login');
$request->setArgument('name', $username);
$request->setArgument('password', $password);
$request->verify($com)->send($com);
$response = new Response($com, false, $timeout);
if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
    // version >= 6.43
    $newVersion = true;
} else {
    // version < 6.43
    $request->setArgument('password', '');
    $request->setArgument(
        'response',
        '00' . md5(
            chr(0) . $password
            . pack('H*', $response->getProperty('ret'))
        )
    );
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
}

if ($response->getType() === Response::TYPE_FINAL) {
    if ($newVersion) {
        return null === $response->getProperty('message');
    } else {
        return null === $response->getProperty('ret');
    }
} else {
    while ($response->getType() !== Response::TYPE_FINAL
        && $response->getType() !== Response::TYPE_FATAL
    ) {
        $response = new Response($com, false, $timeout);
    }
    return false;
}

}

@mdrakibhasanbd
Copy link

tnx @ubntomar

@Knutowskie
Copy link

@boenrobot
I try make pull request, but: Permission to pear2/Net_RouterOS.git denied to sieberlukas.

I edited the script so that it was not so duck-tape-ish?

private static function _login(
    Communicator $com,
    $username,
    $password = '',
    $timeout = null
) {
    $request = new Request('/login');
    $request->setArgument('name', $username);
    $request->setArgument('password', $password);
    $request->verify($com)->send($com);
    $response = new Response($com, false, $timeout);
    if ($response->getType() === Response::TYPE_FINAL && null === $response->getProperty('ret')) {
        // version >= 6.43
        return null === $response->getProperty('message');
    } elseif ($response->getType() === Response::TYPE_FINAL) {
        // version < 6.43
        $request->setArgument('password', '');
        $request->setArgument(
            'response',
            '00' . md5(
                chr(0) . $password
                . pack('H*', $response->getProperty('ret'))
            )
        );
        $request->verify($com)->send($com);
        $response = new Response($com, false, $timeout);
        if ($response->getType() === Response::TYPE_FINAL) {
            return null === $response->getProperty('ret');
        }
    }
    while ($response->getType() !== Response::TYPE_FINAL
        && $response->getType() !== Response::TYPE_FATAL
    ) {
        $response = new Response($com, false, $timeout);
    }
    return false;
}

just logged in to say: you saved my ass. thank you. you know the story: some guy in my company implemented a raspi with this interface and then some other guy updated the mikrotiks and then... BOOM. No more wifi for guests. The first guy is not available some months and i am the one left to fix this with absolutely no glue. i copied this into my client.php and bazinga! it works! Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests