Programatically fill in GTK popup using Expect, xdotool, or similar

78 views Asked by At

Background: I use pass to manage my passwords, with the GPG key stored externally on a YubiKey smart card, and have a fair number of scripts which invoke pass to automate various tasks requiring authentication. This arrangement works great for most things, but there is one specific use-case for which it hasn't so far and which, unfortunately, comes up every time I login to a new session: automatically logging in and connecting to my ProtonVPN account, using Proton's CLI tool protonvpn-cli.

The problem: The following is a summary of the manual workflow I'm trying to automate. Note that I've already succeeded in automating almost all of these steps using expect; the only issue I have left is how to fill in the GTK dialog box spawned by gnome-keyring (in step 1).

  1. Login to ProtonVPN: protonvpn-cli login $username
    • This step triggers two authentication requests: one from gnome-keyring, which spawns a dialog box requesting my login password, and one from protonvpn-cli itself, requesting my Proton account credentials.
    • I can handle the second request by expecting the text of protonvpn-cli's password prompt, and then sending the output of the appropriate pass command (details in the script, included below).
    • The first request, however, is something I'm not sure how to deal with. Automatically grabbing the login password is easy (it's stored in my pass vault), and I imagine one can send that text to the stdin of a GTK process just like any other, but I don't know how to write an expect command that triggers if and only if a GTK password prompt appears at a specified point in the program. For instance, the naive approach of expecting the prompt text doesn't work (since that text isn't written to stdout, or at least not the stdout of any process expect is aware of).
  2. Connect to the VPN: protonvpn-cli connect -f
    • This sends a request to NetworkManager's secret agent for the appropriate network credentials.
    • By spawning the process nmcli agent all first, and then listening on its stdout for a password prompt, I've managed to fully automate this part of the process.

The attempt at a solution: Here's the full script I referenced above. I hacked it out pretty quickly, so it may not be pretty, but it works--except that I don't know how to expect something like a GTK popup, as explained above.

#!/usr/bin/expect -f
# Expect script to automatically read VPN credentials from `pass` and enter them when prompted by `protonvpn-cli` and `nmcli`.

proc connect {passwd} {
    # Connect to ProtonVPN.  Assumes user is already logged in.

    # setup nmcli secret agent to handle password request
    # job numbers and PIDs need to be saved separately
    set agent_pid [spawn nmcli agent all]
    set agent_id $spawn_id
    expect "nmcli successfully registered as a polkit agent."

    # start protonvpn-cli process, which will send a password request to nmcli
    set proton_pid [spawn protonvpn-cli connect -f]
    set proton_id $spawn_id

    # Switch back to polkit agent for password entry
    set spawn_id $agent_id
    expect {
        "*assword*:*" {
            send "$passwd\r"
        }
    }

    # Finally, switch to proton process to listen for success or failure
    set spawn_id $proton_id
    expect {
        "Successfully connected to Proton VPN." {
            exec kill $agent_pid $proton_pid
            return 0
        }
    }

    # Timeout
    exec kill $agent_pid $proton_pid
    return 2
}

proc login {username passwd} {
    # Login to ProtonVPN.  Note that this doesn't actually connect to the VPN itself; that requires a second authentication step.

    set pid [spawn protonvpn-cli login $username]

    expect {
        "*assword:*" {
            send "$passwd\r"
            expect {
                "Successful login." {
                    return 0
                }
            }
            # Login failed
            exec kill $pid
            return 1
        }
        "You are already logged in." {
            return 0
        }
    }

    # Timeout
    exec kill $pid
    return 2
}

proc print_errormsg {code} {
    switch $code {
        1 {
            puts "Issue: failed to authenticate"
        }
        2 {
            puts "Issue: timeout"
        }
    }
}

proc read_passfile {address dictname} {
    # Read output from a password file managed by `pass` and assign text to the keys of a dictionary based on my personal passfile format convention.
    # The convention is:
    #     * first line of passfile is the actual password
    #     * subsequent lines are key-value pairs in the form "key: value" (e.g., "username: user123")
    
    # Indirection (local_dict is a reference to dictname)
    upvar $dictname local_dict
    
    # This is pretty hacky, but it works
    set rawtext [exec pass show $address | sed "1s/^/password: /"]
    foreach line [split $rawtext "\n"] {
        set words [split [string map {":" {}} $line] " "]
        if { [llength $words] == 2 } {
            eval dict set local_dict $words
        }
    }
    return $local_dict
}

set timeout 30

# Get credentials from `pass`
set proton_info [dict create]
set network_info [dict create]
read_passfile personal/proton proton_info 
read_passfile wifi/vpn/proton network_info

# Login to ProtonVPN.  On the first run after logging into a new session, this spawns a GTK dialog box requesting my login password that I haven't figured out how to deal with in this script.
set login_result [login [dict get $proton_info username] [dict get $proton_info password]]
if { $login_result != 0 } {  
    puts "Login failed!"
    print_errormsg $login_result
    exit 1
}
puts "Login succeeded.  Connecting to VPN network..."

# Connect to VPN.  This part works fine.
set connect_result [connect [dict get $network_info password]]
if { $connect_result != 0 } {
    puts "Failed to connect to VPN network!"
    print_errormsg $connect_result
    exit 2
}
puts "VPN connection succeeded."
exit 0
0

There are 0 answers