From af947de6928daa70098edb8346effb9dc8146f47 Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Fri, 2 Aug 2024 19:24:07 -0400
Subject: Add 'LLPanelLogin' 'login', 'savedLogins' operations.

'login' accepts optional 'username', 'slurl', 'grid'.

'savedLogins' returns the list of saved usernames in both display form and
internal form.

Make LLPanelLogin::getUserName() accept (const LLPointer<LLCredential>&).
There's a whole separate discussion pending as to whether const LLPointer<T>
should provide access to non-const T methods.

Similarly, make LLCredential::getIdentifier() a const method. These two
changes enable read-only access to credentials.

Make LLPanelLogin methods capture and reuse LLGridManager::instance() as
appropriate.

Add require/login.lua and test_login.lua.
---
 indra/newview/llpanelloginlistener.cpp | 178 +++++++++++++++++++++++++++++++++
 1 file changed, 178 insertions(+)

(limited to 'indra/newview/llpanelloginlistener.cpp')

diff --git a/indra/newview/llpanelloginlistener.cpp b/indra/newview/llpanelloginlistener.cpp
index fb6f034b7f..a4418145d7 100644
--- a/indra/newview/llpanelloginlistener.cpp
+++ b/indra/newview/llpanelloginlistener.cpp
@@ -32,9 +32,19 @@
 #include "llpanelloginlistener.h"
 // STL headers
 // std headers
+#include <iomanip>
+#include <memory>
 // external library headers
 // other Linden headers
+#include "llcombobox.h"
 #include "llpanellogin.h"
+#include "llsdutil.h"
+#include "llsecapi.h"
+#include "llslurl.h"
+#include "llstartup.h"
+#include "lluictrl.h"
+#include "llviewernetwork.h"
+#include "stringize.h"
 
 LLPanelLoginListener::LLPanelLoginListener(LLPanelLogin* instance):
     LLEventAPI("LLPanelLogin", "Access to LLPanelLogin methods"),
@@ -43,9 +53,177 @@ LLPanelLoginListener::LLPanelLoginListener(LLPanelLogin* instance):
     add("onClickConnect",
         "Pretend user clicked the \"Log In\" button",
         &LLPanelLoginListener::onClickConnect);
+    add("login",
+        "Login to Second Life with saved credentials.\n"
+        "Pass [\"username\"] to select credentials previously saved with that username.\n"
+        "Pass [\"slurl\"] to specify target location.\n"
+        "Pass [\"grid\"] to select one of the valid grids in grids.xml.",
+        &LLPanelLoginListener::login,
+        llsd::map("reply", LLSD::String()));
+    add("savedLogins",
+        "Return \"logins\" as a list of {} saved login names.\n"
+        "Pass [\"grid\"] to select one of the valid grids in grids.xml.",
+        &LLPanelLoginListener::savedLogins,
+        llsd::map("reply", LLSD::String()));
 }
 
 void LLPanelLoginListener::onClickConnect(const LLSD&) const
 {
     mPanel->onClickConnect(false);
 }
+
+void LLPanelLoginListener::login(const LLSD& args) const
+{
+    // Although we require a "reply" key, don't instantiate a Response object:
+    // if things go well, response to our invoker will come later, once the
+    // viewer gets the response from the login server.
+    std::string username(args["username"]), slurlstr(args["slurl"]), grid(args["grid"]);
+    LL_DEBUGS("AppInit") << "Login with saved credentials";
+    if (! username.empty())
+    {
+        LL_CONT << " for user " << std::quoted(username);
+    }
+    if (! slurlstr.empty())
+    {
+        LL_CONT << " to location " << std::quoted(slurlstr);
+    }
+    if (! grid.empty())
+    {
+        LL_CONT << " on grid " << std::quoted(grid);
+    }
+    LL_CONT << LL_ENDL;
+
+    // change grid first, allowing slurl to override
+    auto& gridmgr{ LLGridManager::instance() };
+    if (! grid.empty())
+    {
+        // setGridChoice() can throw LLInvalidGridName -- but if so, let it
+        // propagate, trusting that LLEventAPI will catch it and send an
+        // appropriate reply.
+        auto server_combo = mPanel->getChild<LLComboBox>("server_combo");
+        server_combo->setSimple(gridmgr.getGridLabel(grid));
+        LLPanelLogin::updateServer();
+    }
+    if (! slurlstr.empty())
+    {
+        LLSLURL slurl(slurlstr);
+        if (! slurl.isSpatial())
+        {
+            // don't bother with LLTHROW() because we expect LLEventAPI to
+            // catch and report back to invoker
+            throw DispatchError(stringize("Invalid start SLURL ", std::quoted(slurlstr)));
+        }
+        // Bypass LLStartUp::setStartSLURL() because, after validating as
+        // above, it bounces right back to LLPanelLogin::onUpdateStartSLURL().
+        // It also sets the "NextLoginLocation" user setting, but as with grid,
+        // we don't yet know whether that's desirable for scripted login.
+        LLPanelLogin::onUpdateStartSLURL(slurl);
+    }
+    if (! username.empty())
+    {
+        // Transform (e.g.) "Nat Linden" to the internal form expected by
+        // loadFromCredentialMap(), e.g. "nat_linden"
+        LLStringUtil::toLower(username);
+        LLStringUtil::replaceChar(username, ' ', '_');
+        LLStringUtil::replaceChar(username, '.', '_');
+
+        // call gridmgr.getGrid() because our caller didn't necessarily pass
+        // ["grid"] -- or it might have been overridden by the SLURL
+        auto cred = gSecAPIHandler->loadFromCredentialMap("login_list",
+                                                          gridmgr.getGrid(),
+                                                          username);
+        LLPanelLogin::setFields(cred);
+    }
+
+    if (mPanel->getChild<LLUICtrl>("username_combo")->getValue().asString().empty() ||
+        mPanel->getChild<LLUICtrl>("password_edit") ->getValue().asString().empty())
+    {
+        // as above, let LLEventAPI report back to invoker
+        throw DispatchError(stringize("Unrecognized username ", std::quoted(username)));
+    }
+
+    // Here we're about to trigger actual login, which is all we can do in
+    // this method. All subsequent responses must come via the "login"
+    // LLEventPump.
+    LLEventPumps::instance().obtain("login").listen(
+        "Lua login",
+        [args]
+        (const LLBoundListener& conn, const LLSD& update)
+        {
+            LLSD response{ llsd::map("ok", false) };
+            if (update["change"] == "connect")
+            {
+                // success!
+                response["ok"] = true;
+            }
+            else if (update["change"] == "disconnect")
+            {
+                // Does this ever actually happen?
+                // LLLoginInstance::handleDisconnect() says it's a placeholder.
+                response["error"] = "login disconnect";
+            }
+            else if (update["change"] == "fail.login")
+            {
+                // why?
+                // LLLoginInstance::handleLoginFailure() has special code to
+                // handle "tos", "update" and "mfa_challenge". Do not respond
+                // automatically because these things are SUPPOSED to engage
+                // the user.
+                // Copy specific response fields because there's an enormous
+                // chunk of stuff that comes back on success.
+                LLSD data{ update["data"] };
+                const char* fields[] = {
+                    "reason",
+                    "error_code"
+                };
+                for (auto field : fields)
+                {
+                    response[field] = data[field];
+                }
+                response["error"] = update["message"];
+            }
+            else
+            {
+                // Ignore all other "change" values: LLLogin sends multiple update
+                // events. Only a few of them (above) indicate completion.
+                return;
+            }
+            // For all the completion cases, disconnect from the "login"
+            // LLEventPump.
+            conn.disconnect();
+            // and at last, send response to the original invoker
+            sendReply(response, args);
+        });
+
+    // Turn the Login crank.
+    mPanel->onClickConnect(false);
+}
+
+LLSD LLPanelLoginListener::savedLogins(const LLSD& args) const
+{
+    LL_INFOS() << "called with " << args << LL_ENDL;
+    std::string grid = (args.has("grid")? args["grid"].asString()
+                        : LLGridManager::instance().getGrid());
+    if (! gSecAPIHandler->hasCredentialMap("login_list", grid))
+    {
+        LL_INFOS() << "no creds for " << grid << LL_ENDL;
+        return llsd::map("error",
+                         stringize("No saved logins for grid ", std::quoted(grid)));
+    }
+    LLSecAPIHandler::credential_map_t creds;
+    gSecAPIHandler->loadCredentialMap("login.list", grid, creds);
+    auto result = llsd::map(
+        "logins",
+        llsd::toArray(
+            creds,
+            [](const auto& pair)
+            {
+                return llsd::map(
+                    "name",
+                    LLPanelLogin::getUserName(pair.second),
+                    "key",
+                    pair.first);
+            }));
+    LL_INFOS() << "returning creds " << result << LL_ENDL;
+    return result;
+}
-- 
cgit v1.2.3


From b12d135c38e327774c01594acfa96e37a9e67a64 Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Mon, 5 Aug 2024 11:24:01 -0400
Subject: Fix a couple problems with "LLPanelLogin" listener (thanks Maxim!).

Convert plain grid (e.g. "agni") to domain form (e.g. "util.agni.lindenlab.com").

Fix a typo in `savedLogins()`: "login_list", not "login.list".

login.lua now returns a table with two functions: `login.login()` and
`login.savedLogins()`.

Defend Lua caller against trying to engage login features too late in startup
sequence: in addition to waiting for "STATE_LOGIN_WAIT", produce an error if
`startup.state()` is beyond that state. Since by then `LLPanelLogin` is
destroyed, `leap.request("LLPanelLogin", ...)` would get no response, causing
the calling Lua script to hang until viewer shutdown.
---
 indra/newview/llpanelloginlistener.cpp | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

(limited to 'indra/newview/llpanelloginlistener.cpp')

diff --git a/indra/newview/llpanelloginlistener.cpp b/indra/newview/llpanelloginlistener.cpp
index a4418145d7..aeaf2d1d00 100644
--- a/indra/newview/llpanelloginlistener.cpp
+++ b/indra/newview/llpanelloginlistener.cpp
@@ -72,6 +72,23 @@ void LLPanelLoginListener::onClickConnect(const LLSD&) const
     mPanel->onClickConnect(false);
 }
 
+namespace
+{
+
+    std::string gridkey(const std::string& grid)
+    {
+        if (grid.find('.') != std::string::npos)
+        {
+            return grid;
+        }
+        else
+        {
+            return stringize("util.", grid, ".lindenlab.com");
+        }
+    }
+
+} // anonymous namespace
+
 void LLPanelLoginListener::login(const LLSD& args) const
 {
     // Although we require a "reply" key, don't instantiate a Response object:
@@ -97,6 +114,7 @@ void LLPanelLoginListener::login(const LLSD& args) const
     auto& gridmgr{ LLGridManager::instance() };
     if (! grid.empty())
     {
+        grid = gridkey(grid);
         // setGridChoice() can throw LLInvalidGridName -- but if so, let it
         // propagate, trusting that LLEventAPI will catch it and send an
         // appropriate reply.
@@ -202,7 +220,7 @@ void LLPanelLoginListener::login(const LLSD& args) const
 LLSD LLPanelLoginListener::savedLogins(const LLSD& args) const
 {
     LL_INFOS() << "called with " << args << LL_ENDL;
-    std::string grid = (args.has("grid")? args["grid"].asString()
+    std::string grid = (args.has("grid")? gridkey(args["grid"].asString())
                         : LLGridManager::instance().getGrid());
     if (! gSecAPIHandler->hasCredentialMap("login_list", grid))
     {
@@ -211,7 +229,7 @@ LLSD LLPanelLoginListener::savedLogins(const LLSD& args) const
                          stringize("No saved logins for grid ", std::quoted(grid)));
     }
     LLSecAPIHandler::credential_map_t creds;
-    gSecAPIHandler->loadCredentialMap("login.list", grid, creds);
+    gSecAPIHandler->loadCredentialMap("login_list", grid, creds);
     auto result = llsd::map(
         "logins",
         llsd::toArray(
-- 
cgit v1.2.3