Integration Testing a Web App with Forms Authentication

This turned out to be harder than I thought. I had to not only figure out how to post form fields using .Net code but also handle anti-forgery cookies and hidden fields.

When you request a protected URL, the browser is redirected to the login page. The header of this response contains a Set-Cookie header and the body a hidden input. When the login form is posted, these must be sent back and match.

Here is some example code that does all this:

        private string LogIn(string startUrl)
        {
            try
            {
                //var proxy = new WebProxy("127.0.0.1", 8888); // Fiddler
                IWebProxy proxy = null;

                // Make the request. This will result in the login page being returned.
                var request = WebRequest.CreateHttp(startUrl);
                request.Proxy = proxy;
                var response = (HttpWebResponse)request.GetResponse();
                Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
                
                // Get request verification token cookie:
                string setCookie = response.Headers["Set-Cookie"];
                Assert.That(setCookie, Is.Not.Null);
                var requestVerificationTokenCookie = setCookie.Split(';')[0];

                // Get form action to be posted to later:
                string responseBody = new StreamReader(response.GetResponseStream()).ReadToEnd();
                var regex = new Regex("form action=\"([^\"]*)\"");
                Assert.That(regex.IsMatch(responseBody));
                string formAction = regex.Match(responseBody).Groups[1].Value;
                Assert.That(formAction, Is.Not.Null);

                // Get request verification token form field:
                regex = new Regex("<input name=\"__RequestVerificationToken\" .* value=\"([^\"]*)\"");
                Assert.That(regex.IsMatch(responseBody));
                string requestVerificaitonToken = regex.Match(responseBody).Groups[1].Value;
                
                // Post the login form:
                var baseUri = new Uri(Properties.Settings.Default.AppUrl);
                var localUri = new Uri(formAction, UriKind.Relative);
                var loginUri = new Uri(baseUri, localUri);
                request = WebRequest.CreateHttp(loginUri);
                request.Proxy = proxy;
                request.Method = HttpMethod.Post.Method;
                request.Headers.Add(HttpRequestHeader.Cookie, requestVerificationTokenCookie);
                string postData = $"__RequestVerificationToken={requestVerificaitonToken}&UserId={Properties.Settings.Default.UserId}&Password={Properties.Settings.Default.Password}";
                byte[] byteArray = Encoding.UTF8.GetBytes(postData);
                request.ContentType = "application/x-www-form-urlencoded";
                request.ContentLength = byteArray.Length;
                Stream dataStream = request.GetRequestStream();
                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();
                request.AllowAutoRedirect = false; // Prevents redirecting back to returnUrl.
                response = (HttpWebResponse) request.GetResponse();
                Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Found)); // Redirect to original url
                setCookie = response.Headers["Set-Cookie"];
                Assert.That(setCookie, Is.Not.Null);
                var sessionCookie = setCookie.Split(';')[0];
                return sessionCookie;
            }
            catch (WebException ex)
            {
                Assert.Fail(ex.ToString());
                return null;
            }
        }

Now, this can be used in a test like this:

        [Test]
        public void Test1()
        {
            string url = "http://localhost/...";
            string sessionCookie = LogIn(url);
            var response = Request(sessionCookie, url);
            Assert.That(response, Contains.Substring("(Some string that is always in correct response)"));
        }

        private static string Request(string sessionCookie, string url)
        {
            var webClient = new WebClient();
            webClient.Headers.Add(HttpRequestHeader.Cookie, sessionCookie);
            string response = webClient.DownloadString(url);
            return response;
        }
Advertisements

Batch Resizing and Date Stamping Photos

I have a photo frame that displays pictures from a USB stick. It is fairly low resolution (1024*600), and in order to pack as many pictures as possible on a low-capacity USB stick, I resize them. I also like to have the photo date and time displayed in a corner. Since I have thousands of photos, I cannot do this manually on at a time.

I found that ImageMagick could be used to accomplish this. It works from the command line with a lot of parameters. I created a Windows batch file to iterate through some folders and in one step resize and annotate with date and time and write the result to the USB stick. Here it is. (It iterates through subfolders 2012, 2013, 2014, 2015.)

@ECHO OFF
SET convert=C:\Program Files\ImageMagick-6.9.3-Q16\convert.exe
SET destination=G:\Documents\Pictures
IF NOT EXIST %destination% MKDIR %destination%
FOR /D %%d IN (2012 2013 2014 2015) DO (
    PUSHD %%d
    ECHO *** Processing folder %%d ***
    FOR /R %%a in (*.jpg) DO (
		ECHO %%a | FINDSTR /I ".picasaoriginals" > NUL
		IF errorlevel 1 (
			ECHO Processing file: %%a to %destination%\%%~nxa
			"%convert%" "%%a" -adaptive-resize 1024x600 - | "%convert%" - -pointsize 16 -fill white -undercolor "#00000080" -gravity Southeast -annotate +0+0 " %%[exif:DateTimeOriginal] " "%destination%\%%~nxa"
		) ELSE (
			ECHO Skipped %%a
		)
    )
    POPD
)
PAUSE

Adding a Custom Http Header Using CasperJS

I was working with some web tests using CasperJS, and got the following error from the server (IIS):

HTTP Error 500.52 – URL Rewrite Module Error.
Outbound rewrite rules cannot be applied when the content of the HTTP response is encoded (“deflate”).

I couldn’t change the server configurations, so I had to force the server to return unencoded (uncompressed) content. It turned out to be quite hard to find a solution, but the solution was just three lines of code.

In theory, setting this in the options argument to Casper should work:

var casper = require(‘casper’).create(
{
    pageSettings: {
        customHeaders: {
            ‘Accept-Encoding’: ‘identity’
        }
    }
});

But it didn’t.

Nor did passing the headers option to open(http://docs.casperjs.org/en/latest/modules/casper.html?highlight=header#open). The problem with that approach was that the header was only set to the first request, not for subsequent requests as a result of redirects.

This conversation inspired me to come up with my solution: https://github.com/n1k0/casperjs/issues/667. I simply hooked up the start event and set the header:

casper.on(‘started’, function () {
    this.page.customHeaders = { ‘Accept-Encoding’: ‘identity’ }
});

Monitoring My Home IP Phone Connection

A couple of years ago, I got fibre to my house and switched to IP telephony for my home number. The primary reason is cost – it is must cheaper that traditional copper. But unfortunately, it also less reliable. With irregular intervals, it simply stops working and we cannot call out and, which is more problematic, others cannot call us. So I thought about how to set up some kind of automatic monitoring, and finally found a solution.

I have a computer at home which is always on, and in my technology archive, I found a classic analogue modem. Problem one was how to connect them, since the modem of course has a serial RS232 port but the computer only has USB ports. That part I solved by buying an adapter/converter (I found this one (EAN 4040849954351) at my local dealer (kjell.com).) It had a 9-pin connector and my modem a 25 pin, but luckily my archive also contained an adapter for that.

When connected to the computer, it showed up as COM3. The next part of the solution was to write a script to send a command to the modem to dial my mobile phone and collect the result. If the IP telephony is down, the modem will not get a dial-tone and answer “NO DIALTONE”. The script, which I wrote in PowerShell, detects this and sends an e-mail in this case. Here it is:

param([string] $logfile)

$phonenumber = "a telephone number"
$comport = "COM3"
$smtphost = "smtp.live.com"
$smtpport = 587
$smtpuser = "user@domain"
$smtppassword = "password"
$emailsubject = "Telefonövervakaren"

function InitLog($logfile)
{
    if ($logfile -eq $null -or $logfile -eq "") 
    {
        $logfile = ($MyInvocation.ScriptName) + ".log"
    }
    Write-Host "Logfile: $logfile"
    if ([System.IO.File]::Exists($logfile)) { Remove-Item $logfile }
    return $logfile
}

function SendCommand ($port, $cmd, $logfile)
{
    $cmd = "AT" + $cmd
    Write-Host "> $cmd"
    "> $cmd" >> $logfile
    $port.WriteLine($cmd)
    $response = ""
    for ($i = 0; ($response -eq $null -or $response -eq "" -or $response -eq $cmd) -and ($i -lt 100); $i++)
    {
        Write-Host "." -NoNewline
        Start-Sleep -Seconds 1
        $response = $port.ReadExisting().Trim("`r", "`n");
    }
    Write-Host
    Write-Host $response
    $response >> $logfile
    return $response
}

try
{
    $logfile = (InitLog $logfile)
    #[System.IO.Ports.SerialPort]::GetPortNames()
    $port = new-Object System.IO.Ports.SerialPort $comport,9600,None,8,one
    $port.NewLine = "`r"
    $port.open()
    $response = (SendCommand $port "DT$phonenumber" $logfile)
    $port.Close()
}
catch
{
    Write-Host $_ -ForegroundColor Red
    $_ >> $logfile
}
$success = $response -eq "BUSY" -or $response -eq "NO ANSWER" -or $response -eq "NO CARRIER" -or $response -eq "VOICE"
Write-Host "Success: $success"
if (-not $success)
{
    try {
        Write-Host "Sending e-mail"
        $body = (Get-Content $logfile | Out-String)
        $smtpclient = New-Object Net.Mail.SmtpClient($smtphost, $smtpport) 
        $smtpclient.EnableSsl = $true 
        $smtpclient.Credentials = New-Object System.Net.NetworkCredential($smtpuser, $smtppassword); 
        $smtpclient.Send($smtpuser, $smtpuser, $emailsubject, $body)
    }
    catch
    {
        Write-Host $_ -ForegroundColor Red
        Write-Host $_.Exception -ForegroundColor Red
        $_ >> $logfile
        $_.Exception >> $logfile
    }
}

I saved this script as PhoneMonitor.ps1 and scheduled it using Windows task scheduler. On the action tab, I entered:

  • Program/script: PowerShell.exe
  • Arguments: C:\Users\HTPC\Documents\PhoneMonitor.ps1

Migrating from ASP.NET Identity to SQL Membership

If you crate a new ASP.NET MVC 5 project you have four choices for authentication and one of them is “individual user accounts”. This will create a lot of template code using ASP.NET Identity. This is not the same as classic SQL membership system. New tables will be created in your database.

I was in the situation that I had to work with the classic membership system for compatibility reasons. So what changes must be made to the application?

1. Remove the following from web.config:

<remove name="FormsAuthentication" />

2. Still in web.config, add a connection string and membership and rolemanager sections and set authentication mode to forms:

<connectionStrings>
  <add name="AspNetConnection" connectionString="Data Source=MyServer;Initial Catalog=aspnetdb;…" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
  <authentication mode="Forms">
    <forms loginUrl="~/Account/Login" name="MyApp" />
  </authentication>
  <membership defaultProvider="SqlMembershipProvider">
    <providers>
      <clear />
      <add 
        name="SqlMembershipProvider" 
        type="System.Web.Security.SqlMembershipProvider" 
        connectionStringName="AspNetConnection" 
        applicationName="MyApp" />
    </providers>
  </membership>
  <roleManager defaultProvider="SqlRoleProvider"
      enabled="true"
      cacheRolesInCookie="true"
      cookieName=".ASPROLES"
      cookieTimeout="30"
      cookiePath="/"
      cookieRequireSSL="true"
      cookieSlidingExpiration="true"
      cookieProtection="All" >
    <providers>
      <clear />
      <add
        name="SqlRoleProvider"
        type="System.Web.Security.SqlRoleProvider"
        connectionStringName="AspNetConnection"
        applicationName="Cassius" />
    </providers>
  </roleManager>
</system.web>

3. In AccountViewModel, you can delete everything but LoginViewModel. LoginViewModel can contain only the following:

public class LoginViewModel
{
    [Required]
    [Display(Name = "User id")]
    public string UserId { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }
}

4. In AccountController, you can delete everything except Login and LogOff. They should look something like this:

//
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
    ViewBag.ReturnUrl = returnUrl;
    return View();
}

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
     if (!ModelState.IsValid)
     {
          return View(model);
     }

     if (System.Web.Security.Membership.ValidateUser(model.UserId, model.Password))
     {
         System.Web.Security.FormsAuthentication.SetAuthCookie(model.UserId, false);
         return RedirectToLocal(returnUrl);
     }
     else
     {
         ModelState.AddModelError("", "Invalid user name or password.");
         return View(model);
     }
 }

//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
     System.Web.Security.FormsAuthentication.SignOut();
     return RedirectToAction("Index", "Home");
}

5. Login.cshtml can be simplified as well:

@using ResellerData.WebAdmin.Models
@model LoginViewModel
@{
    ViewBag.Title = "Log in";
}
<h2>@ViewBag.Title.</h2>
<div class="row">
<div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
<h4>Use a local account to log in.</h4>

<hr />

@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
                    @Html.LabelFor(m => m.UserId, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserId, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserId, "", new { @class = "text-danger" })</div>
</div>
<div class="form-group">
                    @Html.LabelFor(m => m.Password, new {@class = "col-md-2 control-label"})
<div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new {@class = "form-control"})
                        @Html.ValidationMessageFor(m => m.Password, "", new {@class = "text-danger"})</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" /></div>
</div>
}
        </section></div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

6. Then, delete a lot of unecessary files:

  • IdentityConfig.cs
  • Startup.Auth.cs
  • ManageController.cs
  • IdentityModels.cs
  • ManageViewModel.cs
  • Everything under Views\Account except Login.cshtml
  • Everything under Views\Manage