Seth Hendrick's Website | Seth Hendrick2024-02-07T10:20:18https://shendrick.net/Seth Hendrickseth@shendrick.nethttps://shendrick.net/Coding Tips/2023/06/18/winformsasyncawait.htmlA Brief Overview on Backgrounding Tasks in a WinForms Application2023-06-18T00:00:00Seth Hendrickhttps://shendrick.net<p>At my job, recently, we ran into a problem I didn't know how to solve right away. We have a C# application written in <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Windows_Forms">WinForms</a> where we wanted to have it so when a user either clicked a button on the UI or hit a hot-key, the application would do something. This "something" would take time to perform. We didn't want the application's UI to lock-up when performing this long-running task, so we wanted to move this task to a background thread. We got the task running on a background thread with a button click easily enough, but we couldn't figure out how to background the task when the hot-key was pressed.</p>
<p>I finally found a potential solution, and wanted to share in case anyone else runs into a similar problem.</p>
<p>Some things to mention up-front:</p>
<ul>
<li>The examples below are purposefully simple. A real application would need a lot more logic and error handling.</li>
<li>While I do know enough about async/await and WinForms to write reliable applications using them, I would not call myself an expert with them. This is a very tip-of-the-iceberg overview of these concepts, and some descriptions may not be 100% correct.</li>
</ul>
<h2>The UI Thread</h2>
<p>The UI thread is the thread where all the UI updates happen. In fact, <em>all</em> UI changes must happen on the UI thread, or your application may crash. That is, you can't have a different thread other than the UI thread disable or enable buttons, change text, change colors, or do anything else to the UI. If you want to have another thread change an UI element, you need to call the <a target="_blank" rel="noopener noreferrer nofollow" href="https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.begininvoke?view=windowsdesktop-7.0">BeginInvoke</a> method on the UI object.</p>
<p>BeginInvoke is a method that takes in a delegate to run asynchronously on the thread that the control was created on. To put it another way, think of the UI thread as an event queue. Every single mouse click, mouse drag, key press, scroll wheel turn, etc. adds an event to this event queue that the UI thread then pulls from and runs the events on itself. The BeginInvoke method simply says "Hey, I have this task I want do. Please run it on the UI thread."</p>
<p>However, the fact that the UI thread behaves like an event queue opens the door to a problem that seems difficult to solve. What happens if your application needs to perform a task that will take a long time to execute when you click a button on the UI? Tasks such as writing to a file, doing a database query, or a web request can take some time. If this work is done on the UI thread, it means the UI thread stops processing other events queued up while its waiting for this long-running task to finish. This means that the UI can't be updated. If a user presses a cancel button, tries to drag the UI around, or anything else, it won't get processed until this task finishes running. This gives the appearance that your application locked up, which is not good for your application's reputation.</p>
<p>Ideally, you want to try to do as little work on the UI thread as possible to keep your UI as responsive as possible. This means that any long-running tasks should be run on a background thread and notify the UI when its done running. How does one go about doing this?</p>
<h2>Async/Await</h2>
<p>Async/Await can be a complex and confusing concept in C#. To be honest, I don't know all of the theory about how it <em>actually</em> works under the hood, and I'm not going to pretend that I do. But, I can at least give a high-level description about how it <em>appears</em> to work with WinForms.</p>
<p>First, let's start off with an example of a simple WinForm GUI. This GUI contains a single button that does some kind of task. When the application is performing the task, the button will be disabled and will display text that says it is "Doing Work".</p>
<pre><code class="language-C#">namespace WinFormsTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click( object sender, EventArgs e )
{
ChangeGuiAndDoWork();
}
private void ChangeGuiAndDoWork()
{
try
{
this.button1.Enabled = false;
this.button1.Text = "Doing Work...";
DoWork();
}
finally
{
this.button1.Text = "Do Work";
this.button1.Enabled = true;
}
}
private static void DoWork()
{
// Emulate work being done by sleeping.
Thread.Sleep( TimeSpan.FromSeconds( 5 ) );
}
}
}
</code></pre>
<p>If you ran the above code, what you would see is when you click the button, the UI will lock-up for 5 seconds. You won't be able to drag it around or anything else until DoWork() finishes running. The only way to prevent the UI from locking up is to have DoWork() run in a background thread. While one <em>could</em> have the button click event handler create a Thread and run DoWork inside of it, there's a much easier syntax that can be used instead with async/await.</p>
<pre><code class="language-C#"> private async void button1_Click( object sender, EventArgs e )
{
await ChangeGuiAndDoWork();
}
private async Task ChangeGuiAndDoWork()
{
try
{
this.button1.Enabled = false;
this.button1.Text = "Doing Work...";
await Task.Run( () => DoWork() );
}
finally
{
this.button1.Text = "Do Work";
this.button1.Enabled = true;
}
}
private static void DoWork()
{
// Emulate work being done by sleeping.
Thread.Sleep( TimeSpan.FromSeconds( 5 ) );
}
</code></pre>
<p>Now when one clicks the button on the UI, they'll see the UI's button become disabled and the text changed to "Doing Work...". The UI also no longer locks up, as the DoWork() method is being run on a thread that is not the UI thread. But, what does the syntax in ChangeGuiAndDoWork mean?</p>
<ul>
<li>The ChangeGuiAndDoWork method is called on the UI thread via the button click event, so we are able to modify button1 inside of this method with no problem.</li>
<li>The Task.Run more-or-less means "Please run the method passed inside of me on a different thread please."</li>
<li>When we hit the "await" keyword, ChangeGuiAndDoWork actually returns once the Task begins running in the background, thus unblocking the UI thread.</li>
<li>Though, before it returns, it does something to make it so when the method passed into Task.Run completes, any line in the method that comes <em>after</em> the "await" keyword gets magically (that is, I don't fully understand how) enqueued back to the UI's event queue.</li>
<li>Since we are back on the UI thread, we are able to enable the button and change the text inside of the finally block safely.</li>
</ul>
<p>You can think of the following code as <em>similar</em> behavior to the above. Its <em>not</em> identical behavior under-the-hood; there are things Task.Run and await do that I don't fully understand (for example, if an exception happens on the background thread, it is magically able to fall into the finally block on the UI thread). But, it is good enough to show how one could do this behavior without async/await using raw threads.</p>
<pre><code class="language-C#">namespace WinFormsTest
{
public partial class Form1 : Form
{
private Thread? workerThread;
public Form1()
{
InitializeComponent();
}
private void button1_Click( object sender, EventArgs e )
{
ChangeGuiAndDoWork();
}
private void ChangeGuiAndDoWork()
{
this.workerThread = new Thread(
() =>
{
// Performing the work in a background thread.
try
{
DoWork();
}
finally
{
// Work is completed, tell the UI thread
// to enable itself by adding an event to its event queue.
BeginInvoke(
() =>
{
// Can not change this in the background thread,
// must call BeginInvoke so it is done on the UI thread
// instead.
this.button1.Text = "Do Work";
this.button1.Enabled = true;
}
);
this.workerThread = null;
}
}
);
// Still on the UI thread, safe to modify here.
this.button1.Enabled = false;
this.button1.Text = "Doing Work...";
// Start the thread in the background, this method
// returns right away to keep the UI thread running and un-blocked.
this.workerThread.Start();
}
private static void DoWork()
{
// Emulate work being done by sleeping.
Thread.Sleep( TimeSpan.FromSeconds( 5 ) );
}
}
}
</code></pre>
<h2>Overriding Protected Methods</h2>
<p>One of the requirements of the application is if someone presses the F5 key, it needs to behave like someone pressed the button on the UI. To do this, one can override the ProcessCmdKey like so:</p>
<pre><code class="language-C#"> protected override bool ProcessCmdKey( ref Message msg, Keys keyData )
{
if( keyData == Keys.F5 )
{
ChangeGuiAndDoWork();
// Return true to signal that the button event
// was processed by this control, and to stop processing it.
return true;
}
// Return false to signal that we did not handle the button
// event.
return false;
}
</code></pre>
<p>Being able to background tasks with async/await for the button click event was as easy as marking the button click event handler as "async" and tossing in an "await" in the method body. But, if one tries to mark the overridden ProcessCmdKey as async, you'll get a compile time error. This is because there is no async version of this method to override. This was <em>almost</em> a road blocker for us at my job, as we didn't know how best to handle this situation. We needed to background DoWork, but we couldn't call await in ProcessCmdKey. We searched StackOverflow, and even asked ChatGPT out of desperation, but they all said the same thing of "sorry, can't make an overridden method async if the base method isn't as well". Which means we started to look towards using raw Threads.</p>
<p>The solution came to me when I was lying in bed trying to sleep. If we can't make ProcessCmdKey async, why don't we just have its job be as simple as adding an asynchronous action to the UI's event queue for us via BeginInvoke and then returning? Well, that's exactly what we tried, and it seemed to work!</p>
<pre><code class="language-C#"> protected override bool ProcessCmdKey( ref Message msg, Keys keyData )
{
if( keyData == Keys.F5 )
{
this.BeginInvoke( async () => await ChangeGuiAndDoWork() );
return true;
}
return false;
}
</code></pre>
<p>With the final code being this:</p>
<pre><code class="language-C#">namespace WinFormsTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override bool ProcessCmdKey( ref Message msg, Keys keyData )
{
if( keyData == Keys.F5 )
{
this.BeginInvoke( async () => await ChangeGuiAndDoWork() );
return true;
}
return false;
}
private async void button1_Click( object sender, EventArgs e )
{
await ChangeGuiAndDoWork();
}
private async Task ChangeGuiAndDoWork()
{
try
{
this.button1.Enabled = false;
this.button1.Text = "Doing Work...";
await Task.Run( () => DoWork() );
}
finally
{
this.button1.Text = "Do Work";
this.button1.Enabled = true;
}
}
private static void DoWork()
{
// Emulate work being done by sleeping.
Thread.Sleep( TimeSpan.FromSeconds( 5 ) );
}
}
}
</code></pre>
<p>Now, the UI no longer locks up when the button is clicked, or when the hot-key is pressed!</p>
<p>This example could use some more improvements. For example, one is able to hit the hot-key multiple times to spawn multiple calls to DoWork(). But, its good enough to get the idea across.</p>
<h2>Conclusion</h2>
<p>I'm sure some WinForms veterans may have already thought of this solution (or even have a better one). But, since I couldn't find one when searching the internet, I wanted to write this down somewhere in case anyone out there ever runs into the same problem we did.</p>
<p>Thanks for reading! If you have any ideas for improvements, please drop them in the comments below.</p>https://shendrick.net/Gaming/2022/05/30/sshonsteamdeck.htmlEnabling SSH Server on a Steam Deck2022-05-30T00:00:00Seth Hendrickhttps://shendrick.net<p>I recently got a <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.steamdeck.com/">Steam Deck</a>, the new handheld gaming system from Valve. So far, it has impressed me. However, I have a bunch of game installation DVDs, but the Steam Deck lacks a DVD drive. I needed to figure out a way to get the DVD contents onto the Steam Deck so I could install a game. One way to get the DVD files onto the Steam Deck is by copying the files from my computer with a DVD drive to the Steam Deck via SSH. However, I noticed that the Steam Deck does not turn on SSH Server (SSHD) by default, meaning I could not connect to it remotely.</p>
<p><a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Secure_Shell">SSH</a> stands for Secure Shell Protocol. It allows one to securely remote into their computers from a different computer. With SSH, one can send commands, or even copy files, between 2 computers. <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/OpenSSH">OpenSSH</a> is a common SSH implementation that is installed on the Steam Deck by default. However, while the Steam Deck is able to connect to other computers via SSH, it does not allow other computers to connect to it via SSH. This is because the SSH Daemon (SSHD), or SSH Server is disabled by default. From a security perspective, this makes sense. The average user of the Steam Deck is probably never going to remote into it via SSH, so Valve most likely decided to disable it by default so any unfriendly hackers don't try to brute force their way into your Steam Deck and cause problems.</p>
<p>This article will explain how to enable SSHD on your Steam Deck, while also doing it securely. There are multiple ways to go about doing this; this is simply the way I find easiest for me.</p>
<h2>Enabling SSHD on the Steam Deck</h2>
<h3>Enter the Desktop</h3>
<p>The first thing you need to do after booting up the Steam Deck is to enter the Desktop. To do this, hit the "Steam" button on the Steam Deck, and scroll down through the menu until you hit the "Power" setting. Then, select the "Switch to Desktop" option.</p>
<p>One the desktop is brought up, click on the Steam Deck logo in the lower-left corner of the desktop to bring up what is essentially the Steam Deck's equivalent of the Windows "Start" menu. On the left column, select the "System" option, and then select "Konsole" on the right column. You can also type in "Konsole" in the search bar.</p>
<p>A terminal will appear. I know, a terminal can be intimidating to anyone who has never used one before. But there's only a handful of commands you need to send to enable SSH. Once the terminal appears, either plug in a Keyboard or hit the "Steam" button + X to bring up the virtual keyboard, and we're ready to start.</p>
<h3>Set a Password</h3>
<p>The terminal should look something like this:</p>
<pre><code class="language-plaintext">(deck@steamdeck ~) $
</code></pre>
<p>"deck" is the username. The Steam Deck has a default user whose name is "deck". "steamdeck" is your hostname. The hostname you can change (more on that later). If you already changed your hostname, then this may be different.</p>
<p>The first thing you need to do is set a password for the "deck" user. By default, there is no password for the "deck" user to maximum security. With no password, it means a bad actor can't SSH into your Steam Deck, and the deck user can not run commands as root (or as admin, for those more familiar with Windows).</p>
<p>However, we need to run commands as root to enable SSH, so we need a password. To do this, type in the "passwd" command and press enter. It will then ask you for a new password. Then, retype in the same password to confirm. Note, while typing your password, you won't see the characters you are typing show up in the terminal.</p>
<pre><code class="language-plaintext">(deck@steamdeck ~) $ passwd
New Password:
Retype New Password:
</code></pre>
<h3>Enable SSHD</h3>
<p>Once you have your password set, you need to send one or two commands to enable SSHD, depending on what your goal is.</p>
<p>To enable SSHD, and allow incoming SSH connections, type the following command (you also may get a prompt asking for your password due to running the command with sudo):</p>
<pre><code class="language-plaintext">sudo systemctl start sshd
</code></pre>
<p>"<a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Sudo">sudo</a>" is a command in Linux that basically says "run the following command as a different user". By default, it will run the command as the "root" user. This is pretty much the equivalent of running a command as an administrator in Windows. "systemctl" talks to software known as <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Systemd">systemd</a>. Systemd does a lot of things that I won't go into detail here, but in this case, it is controlling a service or daemon. "start" starts the service, and "sshd" is the name of the service to start.</p>
<p>To stop the SSHD service, the command becomes:</p>
<pre><code class="language-plaintext">sudo systemctl stop sshd
</code></pre>
<p>However, starting the service will only have the service run until the system is rebooted. Now, maybe this is what you want, in which case, you're done. However, if you want SSHD to startup each time the Steam Deck boots, you need to tell systemd to enable the service, and that command is:</p>
<pre><code class="language-plaintext">sudo systemctl enable sshd
</code></pre>
<p>One quick thing to mention, "enable" does not start the service. If you want to both enable and start the service, you'll have to send both the start and enable. Meanwhile, if you no longer require SSHD to start on startup, simply disable it:</p>
<pre><code class="language-plaintext">sudo systemctl disable sshd
</code></pre>
<p>The choice on whether or not to have SSHD enabled on startup is a personal one. One who is security conscious may opt to only start SSHD, do what they need to with it, and then stop SSHD when they are done with it. After all, one can't break into a system via SSH if SSHD isn't even running! I personally do have it enabled on startup, but I have it configured in such a way that makes getting into it difficult (more on that later).</p>
<h3>Change the Hostname (optional)</h3>
<p>The <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Hostname">hostname</a> is a label assigned to a device on a network. When connecting to a device via SSH, usually the hostname is specified (more on this later). By default, the hostname for the Steam Deck is, well, "steamdeck". If you'll only ever have one Steam Deck on your network, you probably don't need to worry about changing the hostname. If you'll have more than one, it may not be a bad idea to consider changing the hostname so you'll always connect to the same device.</p>
<p>The easiest way to change the hostname is exit the desktop to go back to gaming mode, and click on the "Steam" button to bring up the menu. Select "Settings", and then select "System". Scroll down to the "About" section, and you'll see a "Hostname" option. Go ahead and click on the button, and set your Hostname to whatever you want. You'll have to reboot (not just put to sleep) the Steam Deck in order for the hostname to change.</p>
<h2>Installing an SSH Client</h2>
<p>Now that your Steam Deck is now running SSHD, it can now accept incoming SSH connections from other PCs. However, those other PCs need an SSH client installed in order to connect to the Steam Deck. There are several SSH clients out there.</p>
<h3>Windows</h3>
<p><a target="_blank" rel="noopener noreferrer nofollow" href="https://www.chiark.greenend.org.uk/%7Esgtatham/putty/">PuTTY</a> is a popular SSH Client that has been around for years, and works on many versions of Windows. You can install it via the following ways:</p>
<ul>
<li>Download it from PuTTY's website here: <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.chiark.greenend.org.uk/%7Esgtatham/putty/latest.html">https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html</a></li>
<li>Install it via the Microsoft Store here: <a target="_blank" rel="noopener noreferrer nofollow" href="https://apps.microsoft.com/store/detail/putty/XPFNZKSKLBP7RJ">https://apps.microsoft.com/store/detail/putty/XPFNZKSKLBP7RJ</a></li>
<li>Install it via <a target="_blank" rel="noopener noreferrer nofollow" href="https://chocolatey.org/">Chocolatey</a> via the <a target="_blank" rel="noopener noreferrer nofollow" href="https://community.chocolatey.org/packages/putty.install">putty.install</a> package.</li>
</ul>
<p>However, if you have a modern version of Windows 10, consider, instead, installing OpenSSH, as this is the same piece of software the Steam Deck uses, just built for Windows. Microsoft has instructions on how to do that <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse">here</a>, but here's a recap.</p>
<ul>
<li>Hit "Start" and type "Settings" and hit the Enter key.</li>
<li>Click on "Apps" and select "Apps and Features" on the left column.</li>
<li>Click on "Optional Features"</li>
<li>In the search bar, search for "OpenSSH" and select "OpenSSH Client" and hit "Install".</li>
</ul>
<p>To verify OpenSSH was installed successfully, hit Start and type "PowerShell" and hit enter. A terminal will show up. Type "ssh" and hit enter in the terminal, and you should see a usage print out that looks like this:</p>
<pre><code class="language-plaintext">usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [user@]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command]
</code></pre>
<h3>Linux</h3>
<p>Most Linux distributions come with an SSH client installed by default (usually OpenSSH). To see if its already installed, open a terminal and send "ssh", and you should see the usage output:</p>
<pre><code class="language-plaintext">usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [user@]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command]
</code></pre>
<p>If you do not have SSH installed, you can install it via your package manager:</p>
<pre><code class="language-bash"># Ubuntu / Debian
sudo apt install openssh-client
# Arch Linux
sudo pacman -S openssh
</code></pre>
<p>PuTTY is also available for Linux as well. You can install it from your package manager if you prefer PuTTY over OpenSSH:</p>
<pre><code class="language-bash"># Ubuntu / Debian
sudo apt install putty
# Arch Linux
sudo pacman -S putty
</code></pre>
<h2>Connecting to the Steam Deck</h2>
<p>Now that you have an SSH client installed, you can try connecting to the Steam Deck via SSH. One thing to double check is to make sure is both your Steam Deck and your computer you want to use to connect to it are on the same network. If they are not, you'll never be able to have them talk to each other (well, at least not easily).</p>
<h3>PuTTY</h3>
<p>To connect via PuTTY, go ahead and start PuTTY. A window will appear. You'll want to select the "SSH" radio button, if its not already selected. In the "Host Name (or IP address)" text box, you'll want to put in "deck@steamdeck". This tells PuTTY to login as the "deck" user to the device on the network labeled "steamdeck". If you changed hostnames, replace "steamdeck" with your hostname. If you changed your default SSH port (more on that later), you'll have to replace 22 in the port text box with that. Then click "Open".</p>
<p><a href="/static/img/steamdeck/steamdeckputty.png"><img src="/static/img/steamdeck/steamdeckputty.png" alt="Putty Setup" /></a></p>
<p>A black Window should appear with a terminal that looks like:</p>
<pre><code class="language-plaintext">(deck@steamdeck ~) $
</code></pre>
<p>If you see that, congratulations! You can now SSH into your Steam Deck!</p>
<h3>OpenSSH</h3>
<p>If connecting via OpenSSH, open PowerShell if on Windows, or your favorite terminal if on Linux. Then, simply send the command:</p>
<pre><code class="language-bash">ssh deck@steamdeck
</code></pre>
<p>If you changed hostnames, replace "steamdeck" with your hostname. If you changed your default SSH port (more on that later), you'll have to add an additional argument (the example below has the port changed to port 1000):</p>
<pre><code class="language-bash">ssh deck@steamdeck -p 1000
</code></pre>
<h2>Securing your Steam Deck</h2>
<p>Being able to SSH into your Steam Deck can be connivent for a variety of reasons, but it does come at a risk that someone could brute force their way into your Steam Deck and cause havoc. If you'll never connect to a public or untrusted network, you probably do not have to worry about any of this. However, if you do plan on doing so, you should consider securing your SSH server so bad actors can't get into your Steam Deck.</p>
<p>The most secure way to ensure someone can't SSH into your Steam Deck is to turn off SSHD when you're not using it. However, for those who want to keep SSHD running at all times, here are some tips you can use to prevent unauthorized access to your Steam Deck.</p>
<h3>Changing the Port</h3>
<p>Changing the default port SSHD listens for connections on (port 22) can help prevent novice hackers from brute forcing their way into an SSH connection since they may not be experienced enough to check for other ports. However, any experienced hacker is smart enough to run a port scan and figure out which ports are running SSHD. Changing the default SSHD port could be considered "<a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Security_through_obscurity">Security through Obscurity</a>", where keeping a secret (being the port number in this case) is the main method of providing security. I personally don't bother changing the default SSH port for this reason, but for those who are interested, here's how to do it.</p>
<p>You'll need to open a terminal on your Steam Deck, either by SSHing into it with another PC, or by doing it on the deck itself. If doing it on the deck itself, consider grabbing an external keyboard, since the built-in on-screen keyboard does not have a CTRL button. Then run the command:</p>
<pre><code class="language-bash">sudo nano /etc/ssh/sshd_config
</code></pre>
<p>"nano" is a text editor built into the terminal. Vim and Emacs are also popular choices, but nano is probably the easiest to use. /etc/ssh/sshd_config is a file that is the SSH Daemon configuration. Towards the top, you should see something like this:</p>
<pre><code class="language-bash">#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
</code></pre>
<p>The '#' characters represent comments, which means the lines are ignored. If you want to change your port to, say, port 9001, you'll have to edit the config file so it looks something like this:</p>
<pre><code class="language-bash">Port 9001
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
</code></pre>
<p>Deleting the '#' character tells SSHD to no longer ignore the line, and the next time it starts, listen for incoming SSH connections on port 9001. This means that when connecting to the Steam Deck from your other computer, you need to specify the port:</p>
<pre><code class="language-bash">ssh deck@steamdeck -p 9001
</code></pre>
<p>To save the configuration hit CTRL+O (Write Out), and then hit CTRL+X to exit.</p>
<p>Changes to SSHD are not applied until the process is restart. To do that, run the following command on the Steam Deck:</p>
<pre><code class="language-bash">sudo systemctl restart sshd
</code></pre>
<h3>Disable Root Login</h3>
<p>The "root" user on Linux is all-knowing. It has access to everything on the system. If a hacker gets access to the root login on a system, they can do whatever they want. Therefore, its important to make sure getting access to the root user is as difficult as possible.</p>
<p>By default, SSHD does not allow someone to login as the root user via SSH with a password. You can confirm this by checking the SSHD config by running the following command on the Steam Deck:</p>
<pre><code class="language-bash">sudo nano /etc/ssh/sshd_config
</code></pre>
<p>Arrow down, and you should see something like this:</p>
<pre><code class="language-bash"># Authentication:
#LoginGraceTime 2m
#PermitRootLogin prohibit-password
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10
</code></pre>
<p>If you see this, it means the only way someone to login as root is via an SSH key (more on that later). If you want to make it so no one can login as root <em>period</em>, change the setting to look like this:</p>
<pre><code class="language-bash"># Authentication:
#LoginGraceTime 2m
PermitRootLogin no
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10
</code></pre>
<p>Remove the '#' in front of PermitRootLogin tells SSHD to no longer ignore that setting, and the "no" says "never allow anyone to login directly as root via SSH". Hit CTRL+O to save the config, and CTRL+X to exit nano. Then, restart the SSHD service by sending the following command:</p>
<pre><code class="language-bash">sudo systemctl restart sshd
</code></pre>
<h3>Using Key-based Login</h3>
<p>The most effective way to ensure no one can get into your Steam Deck via SSH is to disable password-based login. This means that a hacker can't brute force their way into your system by guessing passwords. Rather, the way into the Steam Deck via SSH is with a key. While a strong password gives a lot of protection, brute forcing a key is pretty much impossible, and is connivent since you no longer need to specify a password.</p>
<p>An SSH Key is composed of two parts, a private key and a public key. The private key stays on your SSH client and should never, ever, be given out to anyone. A public key, meanwhile, gets added to SSH Servers saying "the user who has this key is allowed in". For more information about "Public-key Cryptography" see <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Public-key_cryptography">this Wikipedia article</a>.</p>
<h4>Generating an SSH Key</h4>
<p>The first step to using SSH keys is to generate one. Each client does their own thing, so here are instructions for both. This is NOT done on the Steam Deck, but rather the computer you'll be using to connect to it.</p>
<h5>PuTTY</h5>
<p>When PuTTY is installed, it installs a tool called PuTTYGen (PuTTY Key Generator). Launch this tool (if on Windows, hit Start and type "PuTTYGen"). On the Window that appears, ensure RSA is selected at the bottom (should be RSA by default), and then hit the "Generate" button. PuTTY Gen will ask you to move your mouse over the blank area to "generate randomness". Once that's done, you should see a window like below. For the "Key comment", I usually put my computer's login name @ the hostname. So, me@mycomputer.</p>
<p>If you trust that your device will only ever be used by you, you probably do not need to enter a passphrase here. The only thing a passphrase does is make it so if someone gains access to your PC and copies your private key, they won't be able to use it. If there is no passphrase, and someone steals your private key, then they'll be able to use it. If this is a concern, add a passphrase, otherwise leave it blank.</p>
<p><a href="/static/img/steamdeck/puttygen.png"><img src="/static/img/steamdeck/puttygen.png" alt="PuTTYGen" /></a></p>
<p>Go ahead and hit "Save private key" and save the .ppk file somewhere safe. I usually put it in C:\Users\Me\.ssh\key.ppk on Windows, but you can put it wherever you want, assuming its a spot only you have access to. You can also optionally hit "Save public key" and save that somewhere as well. I usually put it in C:\Users\Me\.ssh\key.pub on Windows.</p>
<p>Before you close out of PuTTYGen, copy the entire contents of the text box that says "Public key for pasting into OpenSSH authorized_keys file:". This is your public key in a format OpenSSH will use, and we'll need that later.</p>
<h5>OpenSSH</h5>
<p>If you're using OpenSSH, generating an SSH Key is a bit more straightforward. Open PowerShell on Windows or your favorite terminal on Linux, and run the following command:</p>
<pre><code class="language-bash">ssh-keygen
</code></pre>
<p>You'll then be prompted on where to save the public and private key. By default, the generated private keys will be saved to C:\Users\You\.ssh\id_rsa on Windows, or /home/you/.ssh/id_rsa on Linux. The public keys, meanwhile, will be saved to C:\Users\You\.ssh\id_rsa.pub on Windows, or /home/you/.ssh/id_rsa.pub on Linux by default. If these default values are fine with you, just hit enter at these prompts without entering anything. When it asks you for a passphrase for your key, you can also leave that blank if you want. If you trust that your device will only ever be used by you, you probably do not need to enter a passphrase here. The only thing a passphrase does is make it so if someone gains access to your PC and copies your private key, they won't be able to use it. If there is no passphrase, and someone steals your private key, then they'll be able to use it. If this is a concern, add a passphrase, otherwise leave it blank.</p>
<p>After creating your private/public key, send the following command to get the contents of your public key:</p>
<pre><code class="language-bash"># Windows Powershell
cat c:\\Users\\You\\.ssh\\id_rsa.pub
# Linux
cat /home/you/.ssh/id_rsa.pub
</code></pre>
<p>"<a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Cat_(Unix)">cat</a>" stands for "concatenate". While it can be used to stitch multiple files together, it can also be used to dump the contents of a file to the terminal.</p>
<p>Copy the output, we'll have to paste it in later.</p>
<h4>Adding the Public Key to SSHD</h4>
<p>Once you have your public key copied to your clipboard, SSH into your Steam Deck. Then, run the following commands in the SSH terminal:</p>
<pre><code class="language-bash">cd
mkdir -p .ssh
nano .ssh/authorized_keys
</code></pre>
<p>"cd" stands for "Change Directory". Sending "cd" by itself brings you to your home directory. "mkdir" stands for "Make Directory", and the "-p" argument stands for "don't error if the directory already exists". .ssh" is the directory to create. This command says "create a .ssh folder in my home directory if one doesn't already exist". nano is a text editor, and it opens an authorized_keys file.</p>
<p>The authorized_keys file contains one public key per line. The only thing you need to do is paste in your public key that you copied in the last step. If in PuTTY, you just need to right click to paste. If in OpenSSH, it depends on your terminal on how to paste. Once your key is pasted in, hit CTRL+O to write the file, and then CTRL+X to exit nano. If you want to add more public keys, simply paste it onto a new line. If you want to revoke a key later, delete the entire line.</p>
<h4>Connecting with an SSH Key</h4>
<p>Before disabling password-based login, we should make sure you are actually able to login via your SSH Key instead of a password.</p>
<h5>PuTTY</h5>
<p>With PuTTY, setup everything the way you normally would, but there's one additional step. On the left-most column, expand the tree, and click on Connection -> SSH -> Auth. There, you'll see a "Private key file used for authentication" text box. Hit "Browse" and select the private key (.ppk) file you generated earlier. You can optionally hit the "Session" at the top of the tree on the left and save your settings for next time.</p>
<p><a href="/static/img/steamdeck/puttykey.png"><img src="/static/img/steamdeck/puttykey.png" alt="Putty Private Key" /></a></p>
<p>Hit "Open", and it PuTTY does not ask you for a password, you successfully logged in via an SSH Key!</p>
<h5>OpenSSH</h5>
<p>For OpenSSH, its actually pretty easy to do. If you saved your private key to the default location, you just need to send the usual SSH command:</p>
<pre><code class="language-bash">ssh deck@steamdeck
</code></pre>
<p>If you saved the private key somewhere else, you'll have to specify that via the -i argument (i standing for "identity_file"):</p>
<pre><code class="language-bash">ssh -i /path/to/private/key deck@steamdeck
</code></pre>
<p>If SSH does not prompt you to enter a password, you logged in via a key! If not, double check your authorized_keys file on your deck, and make sure the key got pasted in correctly.</p>
<h4>Disabling Password Login</h4>
<p>The last thing to do is to actually disable password login if you want. It is important to make sure you were able to successfully login into your Steam Deck from another computer via an SSH key before proceeding, otherwise, without and SSH Key, you'll never be able to login without editing the SSHD config directly on the Steam Deck.</p>
<p>Open the SSHD config with the following command:</p>
<pre><code class="language-bash">sudo nano /etc/ssh/sshd_config
</code></pre>
<p>Arrow down and you should see something that looks like:</p>
<pre><code class="language-bash"># To disable tunneled clear text passwords, change to no here!
#PasswordAuthentication yes
#PermitEmptyPasswords no
</code></pre>
<p>By default, password authentication is enabled. To disable it, Remove the '#' in front of "PasswordAuthentication" to tell SSHD to no longer ignore this setting, and set "yes" to "no". It should look something like:</p>
<pre><code class="language-bash"># To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
#PermitEmptyPasswords no
</code></pre>
<p>Hit CTRL+O to save the settings, and then CTRL+X to exit. Then restart the SSH service by running:</p>
<pre><code class="language-bash">sudo systemctl restart sshd
</code></pre>
<p>One that's done, no one will be able to login to your Steam Deck via SSH without your private key!</p>
<h2>Conclusion</h2>
<p>SSH is a useful utility, and has many uses. Having it run on a system is extremely connivent. However, it should be done so securely. Hopefully these instructions allow you to make the most use of your Steam Deck!</p>https://shendrick.net/DevOps Rants/2020/12/31/jenkinsrunas.htmlMy Nightmare of Trying to Switch Users on a Windows Jenkins Agent2020-12-31T00:00:00Seth Hendrickhttps://shendrick.net<p>In 2019, the buzzword at my job was "DevOps". Every department was trying to introduce <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Continuous_integration">Continuous Integration</a> (CI) and <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Continuous_testing">Continuous Test</a> (CT) into their software processes. Since I work in automated test development, I helped in bringing our continuous testing infrastructure online. Honestly, although "DevOps" seemed like it was a shallow buzzword-of-the-year, I was excited! I've seen what CI and CT can do if done correctly at a previous job. At my first co-op, they were able to do a full regression test <em>nightly</em> while doing it manually would take weeks.</p>
<p>After probably a total of roughly 2 man-months of building up our DevOps infrastructure, we were able to bring online an automated smoke test. Since our smoke test was now automated, this saved about 8-10 man hours per week!</p>
<p>But this isn't a "DevOps success story" post. Oh no. When we first started bringing our DevOps infrastructure online, we used <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.atlassian.com/software/bamboo">Bamboo</a>. However, the higher-ups then demanded we switched to <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.jenkins.io/">Jenkins</a> instead. Fortunately, our testing infrastructure was built to be CI platform-agnostic, so the switch wasn't too painful...</p>
<p>...Except for one thing...</p>
<p>Switching users in the middle of a Jenkins job automatically is <strong><em>painful</em></strong> on a Windows Jenkins agent.</p>
<h2>Why switch users?</h2>
<p>The main reason is we need to run the automated tests as a specific shared account. There are a few reasons for this. First, it needs to be a shared account since no one wants to have Jenkins login as their own user accounts, for obvious reasons. Second, this shared account has access to the builds that need to be tested. And third, for better or for worse, our automated test software needs to have a one-time manual setup to work on an agent. This means that once and (hopefully) only once, someone needs to login as the shared account on the Windows PC that will run the automated tests and configure the automated test software. This is stuff like which serial ports to use, what files to test, etc. At some point, we would like to be able to <em>not</em> do this, and have our automated smoke test configure this stuff automatically on-the-fly, but we're not quite there yet. Maybe someday if my group gets more staff :P.</p>
<p>How this worked on Bamboo agents, is Bamboo installs a Windows service. We configured the Windows service to login as this shared account. This was a poor decision for a couple of reasons. For one, this meant that whenever the password changed, we would have to go in to each Bamboo agent, stop the service, update the password, and start the service. If it were just one agent, that's one thing. However, we have multiple, so it got really old really quickly. Oh, and if we forgot to update the password on one, the agent would keep trying to login with the wrong password, and the account would get locked out. Not fun. The other reason why this was a poor decision was because it violated the <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">Principle of Least Security</a>. Because the Bamboo Windows service was logged in as this shared account, <em>any</em> job that ran on this agent had access to anything the shared account had access to. So, someone could theoretically create or modify a Bamboo job, run on this agent, archive things they didn't have access to but the shared account did, download them, and delete or revert the Bamboo job. Unlikely to happen given other security measures in place, but it <em>could</em> have if someone knew what they were doing and had time to do it.</p>
<p>Jenkins takes a different approach when it comes to configuring agents. While Bamboo agents talk to the main node to form the connection, Jenkins does the opposite. The main Jenkins node connects to agents by <a target="_blank" rel="noopener noreferrer nofollow" href="https://en.wikipedia.org/wiki/SSH_(Secure_Shell)">SSH</a>'ing into them. This was great! There was no configuration needed on the agents other than installing SSH Server. This also meant we no longer have to login to the agents and start/stop Windows services when a password changed, as the password is stored on the main node. Thus, we only have to update passwords in one spot, through the Jenkins UI. One annoyance with Bamboo solved!</p>
<p>However, the user Jenkins logged in as via SSH was not our shared account, rather it was an account that was local to the PC, and had <em>no</em> permissions at all. Permissions had to be passed in from the Jenkins job with credentials configured on the main node. This also solved another problem with Bamboo, as now we follow the Principle of Least Security with the accounts Jenkins runs jobs under.</p>
<h2>The Problem</h2>
<p>However, this no-permission account opened up another whole can of worms. See, we still had to run automated tests using the shared account. The no-permission account that Jenkins ran under didn't have access to the builds we had to test. We also couldn't login to them to do the one-time manual setup of our automated test software, as only IT had the passwords to these no-permission accounts.</p>
<p>But, we figured this wouldn't be too big of a deal. We should just be able to switch users at the start of an automated smoke test. I mean, this problem has been solved on Linux for years with the <code>sudo -u -S</code> and <code>su -c</code> commands. The password could then be piped into STDIN.</p>
<p>Oh... how wrong I was!</p>
<h2>Failed Solutions</h2>
<p>Windows did not make this easy. I pulled my hair out for days trying to figure out how to switch users in a Jenkins environment. Here are all of the things I tried that did not work.</p>
<h3>Runas</h3>
<p>Windows does have something like Unix's <code>sudo -u</code> built in, and its called "runas". Runas allows one to run a process as a different user. One specifies the user they want to run the process as, followed by the process to run as the other user. Runas would then prompt for a password through STDIN. For example:</p>
<pre><code>PS C:> runas /profile /user:jenkinsuser cmd.exe
Enter the password for jenkinsuser:
</code></pre>
<p>When doing this manually, it worked great! We put in the path to our automated test software, and it loaded the shared user's profile. However, when we tried to input the password automatically via STDIN, runas failed. We double checked everything to make sure the password was correct, but, alas, it was still failing.</p>
<p>Turns out, this was a design decision by the authors of runas; the program demands you type the password manually. According to Raymond Chen in <a target="_blank" rel="noopener noreferrer nofollow" href="https://devblogs.microsoft.com/oldnewthing/20041129-00/?p=37183">this</a> blog post, there was a fear that people would start embedding passwords in batch files if runas allowed for this to happen, which is obviously insecure if one does it that way. I <em>think</em> I remember reading somewhere that if one pushes the enter button too quickly (such as a pipe via STDIN would), runas would fail, and if one were to add a couple second sleep before sending the password, it <em>could</em> work. However, I wasn't able to get this to work, so I'm not sure how true that is.</p>
<h3>Psexec</h3>
<p>Another option was to use a tool called <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/sysinternals/downloads/psexec">Psexec</a>. This allows one to specify the password on the command line. However, there is a security concern with using this tool. While Psexec allows one to specify a password with the <code>-p</code> argument, well, it is on the command line. This means any user can login to the agent and open <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer">Process Explorer</a> and see the command line arguments for any process on the machine. This means a leaked password, which is bad.</p>
<p>We also opted not to use this tool because it was yet another thing we had to install on our CT agents.</p>
<h3>C# Process Object</h3>
<p>In Raymond Chen's article, he suggested if one wanted to pass in a password via the command line, they would have to write their own tool, and use Window's <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw">CreateProcessWithLogonW</a> function. Turns out, C#'s <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=net-5.0">Process</a> object just does that (you can see the call in dotnet's source code <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/dotnet/runtime/blob/ef2a1878793e7e3fc3060396d3d2655ac53b1316/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs#L567">here</a>)!</p>
<p>So, we did just that, but tweaked it slightly. Instead of specifying the password directly through the command line, the command line would instead specify the name of the environment variable that would contain the password. The process would then get the password from the environment variable and pass it into the C# Process object. Since the raw password wasn't specified on the command line directly, if someone opened Process Explorer, they would only see the environment variable name, not the password itself unless they were an admin and looked at the process's environment variables.</p>
<p>The code looked somewhat like this, but more way more robust:</p>
<pre><code class="language-C#">using System;
using System.Diagnostics;
namespace RunAsTest
{
class Program
{
static int Main( string[] args )
{
ProcessStartInfo info = new ProcessStartInfo
{
CreateNoWindow = true,
FileName = args[2],
UserName = Environment.GetEnvironmentVariable( args[0] ),
PasswordInClearText = Environment.GetEnvironmentVariable( args[1] ),
LoadUserProfile = true,
Domain = "thedomain"
};
using( Process process = new Process() )
{
process.StartInfo = info;
process.Start();
process.WaitForExit();
return process.ExitCode;
}
}
}
}
</code></pre>
<p>It would be executed in Windows BATCH by:</p>
<pre><code class="language-batch">set USERNAME_ENV = "someuser";
set USERNAME_PASS = "somepass";
MyRunAs.exe USERNAME_ENV USERNAME_PASS c:\OurTestSoftware.exe
</code></pre>
<p>or in Jenkins:</p>
<pre><code class="language-java">withCredentials(
[usernamePassword(
credentialsId: "CREDS_ID",
passwordVariable: 'USERNAME_PASS',
usernameVariable: 'USERNAME_ENV'
)]
)
{
bat "MyRunAs.exe USERNAME_ENV USERNAME_PASS c:\\OurTestSoftware.exe"
}
</code></pre>
<p>On my standard work PC, writing our own runas worked great! We were able to launch our test software as our shared user account, and the password wasn't exposed via Process Explorer!</p>
<p>But, my soul was crushed just a few minutes later, when I tried it in a Jenkins Environment. It didn't work. I could not, for the life of me figure out why. I then remembered that Jenkins logs into agents via SSH, so I said to myself, "I wonder if SSH is the problem?" So, on my standard work PC, I SSH'ed into itself by connecting to "localhost". I then changed into the directory where I ran our version of runas, and ran the exact same command that worked outside of an SSH environment.</p>
<p>It failed for the same reason it was failing on Jenkins.</p>
<p>As mentioned earlier, the C# process object calls CreateProcessWithLogonW. Turns out, according to the <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw#remarks">documentation</a>, we can't call this function with the "SYSTEM" account. We need to, instead, use the <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessasusera">CreateProcessAsUser</a> and <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-logonusera">LogonUser</a> function, which do not exist in C#. So we would have to <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke">P/Invoke</a> the Windows API from C#.</p>
<p>The exact remarks from the documentation are:</p>
<blockquote>
<p>Windows XP with SP2,Windows Server 2003, or later: You cannot call CreateProcessWithLogonW from a process that is running under the "LocalSystem" account, because the function uses the logon SID in the caller token, and the token for the "LocalSystem" account does not contain this SID. As an alternative, use the CreateProcessAsUser and LogonUser functions.</p>
</blockquote>
<p>The SSH server service runs as the System account. So any sub-processes will not be able to login as a different user using CreateProcessWithLogonW.</p>
<p>Ugh. Back to the drawing board.</p>
<p>So before I went down the P/Invoke rabbit hole, I continued searching for something else that could work.</p>
<h3>Impersonation</h3>
<p>I then found Impersonation in Windows. According to Microsoft's <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/windows/win32/com/impersonation">documentation</a>:</p>
<blockquote>
<p>Impersonation is the ability of a thread to execute in a security context that is different from the context of the process that owns the thread. When running in the client's security context, the server "is" the client, to some degree. The server thread uses an access token representing the client's credentials to obtain access to the objects to which the client has access.</p>
</blockquote>
<p>Notice the phrase "a thread". That's important. But more on that later.</p>
<p>We actually had some success with impersonation. We were actually able to login as the shared account and access files it had permission to read. In fact, after we compile our builds, we impersonate to deploy them to file shares, where our testers can then download them. Compiling and deploying happen within the same thread, so this works. However, what happens if while we are impersonating a different user, we launched a sub-process? Could this be the solution I have searched for?</p>
<p>Turns out, no. A sub-process is an entirely different thread. So impersonating fails. Here is (a very hacky) sample program I wrote to demonstrate this.</p>
<pre><code class="language-C#">// This must be run in a directory the impersonated user also has access to,
// or the sub-process will not launch since it can't access the .exe.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
// Need the System.Security.Principal and System.Security.Principal.Windows
// NuGet packages installed for this to work, if dotnet core.
using System.Security.Principal;
using Microsoft.Win32.SafeHandles;
namespace RunAsTest
{
class Program
{
[DllImport( "advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode )]
public static extern bool LogonUser(
String lpszUsername,
String lpszDomain,
String lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeAccessTokenHandle phToken
);
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2; // This parameter causes LogonUser to create a primary token.
const string user = "jenkinsuser";
const string password = "thepassword"; // Obviously, not the real password.
const string domain = null;
static void Main( string[] args )
{
string fileLocation = Assembly.GetExecutingAssembly().Location; // Returns .dll, not .exe.
fileLocation = Path.GetFileNameWithoutExtension( fileLocation ) + ".exe";
if( args.Length == 0 )
{
Console.WriteLine( "Starting User Info:" );
}
else
{
Console.WriteLine( "Sub-process while impersonated Info:" );
}
PrintEnv();
// If this is the parent process, launch a sub-process.
if( args.Length == 0 )
{
Console.WriteLine();
SafeAccessTokenHandle handle;
bool loggedIn = LogonUser(
user,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out handle
);
if( loggedIn == false )
{
handle?.Dispose();
Console.Error.WriteLine( "Did not login" );
return;
}
WindowsIdentity.RunImpersonated(
handle,
() =>
{
Console.WriteLine( "Impersonation Info:" );
PrintEnv();
try
{
ProcessStartInfo info = new ProcessStartInfo
{
CreateNoWindow = true,
FileName = fileLocation,
LoadUserProfile = true,
// Specify an argument so we don't fork bomb.
Arguments = "stop",
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true
// Username / Password purposefully not specified, so we don't
// call CreateProcessWithLogonW that specifies a username/password,
// which we know doesn't work.
};
using( Process process = new Process() )
{
process.StartInfo = info;
process.Start();
string stdout = process.StandardOutput.ReadToEnd();
Console.WriteLine( stdout );
process.WaitForExit();
}
}
catch( Exception e )
{
Console.Error.WriteLine( "Error when starting process: " );
Console.Error.WriteLine( e.Message );
}
}
);
handle?.Dispose();
}
}
private static void PrintEnv()
{
Console.WriteLine( "\tUser Name: " + Environment.UserName );
Console.WriteLine( "\tMachine Name: " + Environment.MachineName );
Console.WriteLine( "\tCurrent Directory: " + Environment.CurrentDirectory );
Console.WriteLine( "\tMy Documents: " + Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments ) );
Console.WriteLine( "\tExe location: " + Assembly.GetExecutingAssembly().Location );
}
}
}
</code></pre>
<p>The resulting output is this:</p>
<pre><code>Starting User Info:
User Name: seth
Machine Name: hostname
Current Directory: C:\Jenkins\bin\Debug\netcoreapp3.1
My Documents: C:\Users\seth\Documents
Exe location: C:\Jenkins\bin\Debug\netcoreapp3.1\RunAsTest.dll
Impersonation Info:
User Name: jenkinsuser
Machine Name: hostname
Current Directory: C:\Jenkins\bin\Debug\netcoreapp3.1
My Documents: C:\Users\jenkinsuser\Documents
Exe location: C:\Jenkins\bin\Debug\netcoreapp3.1\RunAsTest.dll
Sub-process while impersonated Info:
User Name:
Machine Name: hostname
Current Directory: C:\Jenkins\bin\Debug\netcoreapp3.1
My Documents:
Exe location: C:\Jenkins\bin\Debug\netcoreapp3.1\RunAsTest.dll
</code></pre>
<p>So, we start the process as me. The process then impersonates jenkinsuser, and it prints the information one would expect while logged in as this user. However, the weirdness comes when when we try to launch a subprocess while impersonated. Notice, the user name and the documents folder are empty strings. I have no explanation as to why this is the case, but either way, it meant launching our test software while impersonated was NOT the right course of action. If we wanted to run a sub-process as a different user, we would always have to specify the username and password in the <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo?view=net-5.0">ProcessStartInfo</a>. However, it was a worthwhile experiment since we were still able to use impersonation elsewhere.</p>
<h2>The Actual Solution</h2>
<p>I don't remember how I came up with this idea. But it was so simple, I can't believe I didn't think of it sooner. There's an SSH server running on our CT agents. Why don't we just SSH from the no-permission account into the same PC via localhost and login to the shared account? We could then run the command to launch our test software?</p>
<p>This is the approach we use. It does work. However, we can not use the OpenSSH's "ssh" command itself, since there is no way to specify a password on the command line through that. Piping the password via STDIN to SSH does not work since it does some weird terminal thing that I don't fully understand. <a target="_blank" rel="noopener noreferrer nofollow" href="https://linux.die.net/man/1/sshpass">SshPass</a> is a tool that can be used on Linux to pass a password in, but it does not work on Windows.</p>
<p>So, I created a front-end to the C# library <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/sshnet/SSH.NET">SSH.NET</a> that allows one to pass in environment variable names that contain the username and password via the command line, the server to connect to, and the command to run. I called this tool SshRunAs, and it is on my GitHub <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/xforever1313/SshRunAs">here</a></p>
<p>To use in BATCH, this is what to do:</p>
<pre><code class="language-batch">set USER=me
set PASSWD=SuperSecretPassword
.\SshRunAs.exe -c "curl https://shendrick.net" -u USER -p PASSWD -s localhost
</code></pre>
<p>In Jenkins, meanwhile, withCredentials can be used:</p>
<pre><code class="language-java">withCredentials(
[usernamePassword(
credentialsId: "CREDS_ID",
passwordVariable: 'USER',
usernameVariable: 'PASSWD'
)]
)
{
bat "SshRunAs.exe -c \"curl https://shendrick.net\" -u USER -p PASSWD -s localhost"
}
</code></pre>
<p>And... that's how we change users in a Jenkins Environment running on a Windows machine. Its a bit hacky, but it works surprisingly well.</p>
<h2>Conclusion</h2>
<p>In an ideal scenario, we would not need to switch users to run our automated smoke tests. We should just be able to use the no-permission Jenkins account. Alas, we are not there quite yet, and may not be for quite some time. However, SshRunAs is working well for us for the time being, and, honestly, maybe its good enough. My group is still young in terms of experience in the realm of DevOps, and I'm sure those with more experience reading this are screaming "DO IT THIS WAY!". I will say, if anyone has any suggestions or comments, please leave one below!</p>
<p>In the end, I did learn more about how Windows and C# work under-the-hood. I also learned more about how Jenkins works as well. Although it was a frustrating few days at work, it was all worth it in the end.</p>https://shendrick.net/Coding Rants/2017/12/23/cppvscsharp1.htmlC# vs. C++ - Part 1: Introduction2017-12-23T00:00:00Seth Hendrickhttps://shendrick.net<p>I am still a novice in the world of Software Engineering. I am currently, as of writing, 26 years old. My first exposure to the world of software engineering was only 8 years ago, during my first year at Hudson Valley Community College. The class was ENGR-110: "Engineering Tools". In that class we learned about Excel, CAD, and an introduction to programming. Despite the fact that I was considered the "computer nerd" in high school, I didn't know any programming. Yeahhh, I knew HTML, but HTML is not a programming language. This was the first time I ever did anything with if statements, while loops, and arrays.</p>
<p>I'll never forget the first time I hit the "compile" button followed by the "run" button and saw "Hello, World!" appear just over 8 years ago. We were working in a little-known IDE we downloaded from <a target="_blank" rel="noopener noreferrer nofollow" href="http://bloodshed.net/">Bloodshet.net</a>. The language we were working in? C++.</p>
<p>That was my first exposure to C++. It wouldn't be until after I graduated HVCC and started my second quarter at RIT would I learn about it again. That class was Computer Science 4. A year later, I got my first co-op, which lasted for 9 months. The language the company used? C++. When I returned to RIT, I minored in Game Design and Development. Their introduction classes to OpenGL was all C++. I wrote my <a target="_blank" rel="noopener noreferrer nofollow" href="https://ctsn.shendrick.net">senior design</a> project in C++. The startup I was briefly involved in had all of our code written in C++.</p>
<p>All during this timed, I learned about the various compilers, which ones were great, which ones were... less-than-stellar. I learned which IDEs worked decently and which were just okay. I learned various build environments such as make, and SCons. I learned how to compile libraries myself on both Windows and Linux.</p>
<p>C++ was my favorite language! It had its problems, but it was powerful and the syntax made sense to me (more on that later). It got to the point where I would write everything in C++, because it was that flexible of a language!</p>
<p>Time passed, and I was ready for my last co-op while at RIT. I accepted an offer with Harris Corporation. I was going to be working on C/C++ code for some of the tactical radios there. My first day there, the HR people went around the room and told us who our managers would be. When they got to me, they revealed that my manager would not be who I interviewed with. My friend who co-oped there prior told me that the department did acceptance testing.</p>
<p>Uh-oh.</p>
<p>I was... upset... ACCEPTANCE TESTING? That's not what I signed up for! But maybe I would do something cool?</p>
<p>My supervisor came by to pick be up. I'll never forget the hallway chat we had.</p>
<ul>
<li>Him: "So what experience do you have?"</li>
<li>Me: "Well, I do a lot of C++ programming. I also have some embedded experience."</li>
<li>Him: "We don't do embedded development in our department. We mostly work in C#".</li>
</ul>
<p>I was disheartened. I not only was not going to work on an embedded system, but I was also going to work in what I thought was an inferior language at the time. "It is only 3 months," I thought to myself.</p>
<p>But, it being a co-op, I took the opportunity to try to learn something new. I programmed in a similar language (Java) before, and C# once. Maybe it would be cool?</p>
<p>My biased was showing early. My time there, there were multiple times where I had a thought such as: "C++ is clearly a better choice than C#. C# doesn't even have destructors (they're finalizers! those don't count!), or const references (we have to use interfaces), and templates (generics in C#) need to implement a common interface!? And what's the deal with .Equals and operator==?"</p>
<p>However, I did acknowledge the C# had some nice features about it. Properties are a godsend. Syntax is considerably nicer than C++. C# actually had filesystem, sockets, threading, and XML stuff build into its "standard library". C++11 was <em>just</em> going mainstream then, so having threading built into the language without using a third-party library (e.g. Boost) was wonderful.</p>
<p>Well, over 3.5 years after that dreaded walk down the hallway, my supervisor from my co-op is now my current, full-time, supervisor. I work full-time in the department I co-oped in, and I look forward to going into work everyday. I program in C# almost every single day, and I rarely touch C++ anymore.</p>
<p>Why? What caused me to change my favorite programming language?</p>
<p>In this series of posts, I'll be describing what I like about C++ and C#, what I dislike about both, and why I switched. I programmed in both of them of roughly the same amount of time (3 years each). I worked with both in a professional environment (though my time with C++ was a co-op, it was still an actual company with a complex codebase). And hopefully, we all learn something from these rants!</p>https://shendrick.net/Coding Tips/2015/03/15/cpparrayvsvector.htmlC++: C-Style arrays vs. std::array vs. std::vector2015-03-15T00:00:00Seth Hendrickhttps://shendrick.net<p>Arrays are a very basic data structure used in programming. Unlike linked lists, arrays are guaranteed have its elements contiguous in memory. That is, someArray[0] is directly next to someArray[1] in memory. However, in C++, there are three ways to use arrays: C-style arrays, std::array (as of C++11) and std::vector. What’s the difference between the three? Read on!</p>
<h2>C-Style Array:</h2>
<p>When many people think of C++ arrays, they think of something to the effect of:</p>
<pre><code class="language-c">int myArray[3] = {1, 2, 3};
</code></pre>
<p>This is a feature left over from C, so hence the name “C-Style array”. But, C-style arrays are very primitive. For example there is no easy way to determine the size of this array. Sure, you can do:</p>
<pre><code class="language-c">int myArray[5] = {1, 2, 3, 4, 5};
size_t arraySize = sizeof(myArray) / sizeof(int);
std::cout << "C-style array size: " << arraySize << std::endl;
// Outputs:
// C-style array size: 5
</code></pre>
<p>But this only works when the array is in the same scope as the size operation. An array is just a pointer to the first element in the array. That is dereferencing the array with the ‘*’ and doing myArray[0] return the same value (see example below). Consequently, doing myArray[1] and *(myArray + 1) will also return the same thing. That’s because under the hood, myArray[x] becomes *(myArray + x). We use operator [] since its cleaner.</p>
<pre><code class="language-c">std::cout << std::boolalpha << (myArray[0] == *myArray) << std::endl;
std::cout << std::boolalpha << (myArray[1] == *(myArray + 1) << std::endl;
// Outputs:
// true
// true
</code></pre>
<p>So when you pass an array into a function, the function just sees a pointer. If we try the same sizeof() thing we did above, we will get a very different outcome:</p>
<pre><code class="language-c">#include <iostream>
void printSize(int someArray[5]) {
std::cout << sizeof(someArray)/sizeof(int) << std::endl;
}
int main() {
int myArray[5] = {1, 2, 3, 4, 5};
printSize(myArray);
}
// Outputs:
// 2
</code></pre>
<p>Wait? WHAT! Where does 2 come from? Like I said, an array is just a pointer. To put it simply, when we pass an array into a function, it “loses” its array “properties” and becomes a pointer. int someArray[5] in the parameter list becomes int *someArray. A pointer is of size size_t, which on my computer is 8 bytes. An int, meanwhile, is 4 bytes. We are doing sizeof(size_t) / sizeof(int), which is 2.</p>
<p>So the only way to get the size of an array is to pass around a size_t with the array, which is annoying.</p>
<p>There is another limiting factor with C-style arrays. We can not use C++11’s fancy foreach loop with them outside the same scope. The below example works just fine:</p>
<pre><code class="language-c">int main() {
int myArray[5] = {1, 2, 3, 4, 5};
for (int &i : myArray) {
std::cout << i << ", " << std::endl;
}
}
</code></pre>
<p>However, once we pass it into a function, such as below:</p>
<pre><code class="language-c">#include <iostream>
void printElements(int someArray[5]) {
for (int &i : someArray) {
std::cout << i << ", " << std::endl;
}
}
int main() {
int myArray[5] = {1, 2, 3, 4, 5};
printElements(myArray);
}
</code></pre>
<p>We get a very nasty compile error.</p>
<p>The solution to both of these problems is C++11’s new std::array.</p>
<h2>std::array</h2>
<p>std::array is a very thin wrapper around C-style arrays that go on the stack (to put it simply, they do not use operator new. The examples above do this). Like arrays that go on the stack, its size must be known at compile time.</p>
<p>Constructing an std::array is easy. Instead of C’s int someArray[x], its std::array<int, x> someArray, where x is the size of the array. See the example below. Note that when we pass in std::array into the function, we can iterate over it, and it knows its size!</p>
<pre><code class="language-c">#include <array>
#include <iostream>
void printElements(const std::array<int, 5>; &someArray) {
for (const int &i : someArray) {
std::cout << i << ", " << std::endl;
}
std::cout << "Size: " << someArray.size() << std::endl;
}
int main() {
std::array<int, 5> myArray = {1, 2, 3, 4, 5};
printElements(myArray);
}
// Outputs:
// 1,
// 2,
// 3,
// 4,
// 5,
// Size: 5
</code></pre>
<p>Hurray! It worked!</p>
<h2>std::vector</h2>
<p>Now, I hear some of you saying “But wait, what about std::vector? Doesn’t this do the same thing?”. While it is true, you can swap out the above code with std::vector and get the same result, under the hood it does things differently.</p>
<p>std::array is a static array whose size is known at compile time. It is a thin wrapper of c-style arrays that go on the stack. std::vector is an entirely different beast. It is a dynamic array that goes on the heap. Its size does not need to be known at compile time. As you add or remove things from std::vector, the underlying array changes size. std::array can not change size once it is created, just like c-style arrays that go on the stack. std::vector can change its size once created, just like c-style arrays that go on the heap.</p>
<p>Think of std::array as a wrapper to:</p>
<pre><code class="language-c">int someArray[5];
</code></pre>
<p>while std::vector is a wrapper to:</p>
<pre><code class="language-c">int *someArray = new int[5];
</code></pre>
<p>You should use std::array when the array size is known at compile time. You should use std::vector when you do not, or the array can grow.</p>