ASP.NET MVC 4 Custom Validation for CIDR Subnet

I recently worked on a rWhois project that required writing an ASP.NET MVC 4 based admin portal. One of the last things I tackled was adding validation to inputs. Adding validation to a MVC model is super-simple with Data Annotations. By adding these annotations to your model, both client and server-side validation comes to life. There’s a great tutorial describing how to accomplish this on the asp.net site:

http://www.asp.net/mvc/tutorials/mvc-4/getting-started-with-aspnet-mvc4/adding-validation-to-the-model

The in-box DataAnnotations class provides numerous built-in validation attributes that can be applied to any property. Things like strings of specific length, or integers within a range, or even regex matching is supported using the built-in validation attributes – and they are automatically wired up when you use Visual Studio to add views with scaffolding. But what about when the built-in attributes don’t quite do what you need?

I needed to create validation that confirms the input is both in proper CIDR format (ie. 10.0.0.0/8 and not “some text”) and that the supplied CIDR subnet is valid (ie. 192.168.100.0/24 is a proper network/bitmask, but 192.168.100.50/24 is not). Validating that the input is in the proper format can easily be done with regex. Just add the following to annotation to the appropriate property on your model:

[RegularExpression(@"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$", ErrorMessage="Not valid CIDR format")]

To check that the supplied input is a valid network/bitmask CIDR combination, I needed custom validation. You can easily extend the ValidationAttribute and implement IClientValidatable to add your custom validation. First, you’ll want to create a new class and reference the DataAnnotations and Mvc namespaces:

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace MvcApplication1.Validation
{
    public class CIDRSubnet : ValidationAttribute, IClientValidatable
    {
 
    }
}

Next, we’ll want to create the appropriate server-side validation by overriding the ValidationResult class and adding the CIDR subnet check:

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //validate that valus is valid cdir
        string[] cidr = ((string)value).Split(new char[] { '/' });
        string[] ip = cidr[0].Split(new char[] { '.' });
        int i = (Convert.ToInt32(ip[0]) << 24) | (Convert.ToInt32(ip[1]) << 16) | (Convert.ToInt32(ip[2]) << 8) | (Convert.ToInt32(ip[3]));
        int mask = Convert.ToInt32(cidr[1]) == 0 ? 0 : ~((1 << (32-29)) - 1);
        int network = i & mask;
 
        if (i != network) return new ValidationResult("Not a valid CIDR Subnet");
        return ValidationResult.Success;
    }

Now we want to create our client side validation function. Create a new javascript file and make sure it’s referenced in your View:

function ipv4checker(input) {
    //confirm cidr is valid network
    var cidr = input.split('/');
    var base = cidr[0];
    var bits = cidr[1];
    var ip = base.split('.');
    var i = (ip[0] << 24 | ip[1] << 16 | ip[2] << 8 | ip[3]);
    var mask = bits == 0 ? 0 : ((1 << (32 - bits)) - 1) ^ 0xFFFFFFFF;
    var network = i & mask;
 
    if (i == network) { return true; }
    else { return false; }
}

Assuming jquery and jquery unobtrusive validation libraries have already been added to your project, you can then add a method to the validator, and wire it up. I’m not passing any parameters here, but you could also include parameters:

 
//custom vlidation rule - check IPv4
$.validator.addMethod("checkcidr",
    function (value, element, param) {
    return ipv4checker(value);
    });
 
//wire up the unobtrusive validation
$.validator.unobtrusive.adapters.add
    ("checkcidr", function (options) {
        options.rules["checkcidr"] = true;
        options.messages["checkcidr"] = options.message;
    });

Then, you’ll need to implement the GetClientValidationRules method in your custom validation class:

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        ModelClientValidationRule rule = new ModelClientValidationRule();
        rule.ValidationType = "checkcidr";
        rule.ErrorMessage = "Not a valid CIDR Subnet";
        return new List<ModelClientValidationRule> { rule };
    }

Lastly, add a reference to your custom validation class in the model and add the custom validation attribute to your property:

using System.ComponentModel.DataAnnotations;
using MvcApplication1.Validation;
 
namespace MvcApplication1.Models
{
    public class Allocation
    {
        [Required]
        [RegularExpression(@"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$", ErrorMessage="Not valid CIDR format")]
        [CIDRSubnet(ErrorMessage="Not a valid CIDR Subnet")]
        public string IPNetwork { get; set; }
    }
}

Cannot find Newtonsoft.Json error when deploying

We recently revamped our source control, continuous integration, and bug tracking solution at OrcsWeb to take advantage of Git. Part of the solution includes a build server that automatically runs tests against projects and, assuming they pass, packages the and deploys the application using Web Deploy. Several of the projects typically include NuGet packages, and a new feature of NuGet (as of 1.6 & 2.0) allows you to exclude these NuGet packages when committing to source control. By enabling package restore, NuGet will automatically download any packages from packages.config on any system where they’re missing.

After uploading a new MVC 4 project that was using this NuGet Package Restore functionality, I was receiving the YSOD that asp.net “Could not load file or assembly ‘Newtonsoft.Jason, Version=4.5.0.0′”. I confirmed that NuGet was downloading the packages on the build server, but for some reason it wasn’t being included in the deployment. BlackSpy’s solution for the Newtonsoft.Json error on stackoverflow.com fixed the issue for me as well.

I removed the entry for Newtonsoft.Json version 4.5.6 from packages.config, saved and built the project, then re-added Newtonsoft.Json 4.5.11 from NuGet and committed the changes. The build server picked up the updates, downloaded the missing package, and deployed it to the server.

 

Running a MVC 4 application on IIS7

Recently tried to get a new MVC 4 application running on IIS7 and ran into an issue that required a hotfix. The IIS7 server was 2008 SP2 fully patched and had both .NET 2.0 and .NET 4.5 installed, however, whenever I attempted to load the application, IIS would return a 403.14 error message about a directory listing being denied.

Many solutions suggested that ASP.NET wasn’t registered properly, and I confirmed that my application pool settings were correct (need to run Integrated Mode or configure asp.net to handle all traffic so requests are routed to the controllers). Others recommended code solutions, but Sean Anderson’s solution on this stackoverflow.com post was the resolution for me.

There’s an extensionless URL hotfix (I blogged about this hotfix in relation to 404 errors) for IIS7 that is required: http://support.microsoft.com/kb/980368.

Read & Write .NET machine key with Powershell

We’re putting together webfarms all the time at OrcsWeb, and one of the cardinal rules with webfarms is that all systems need to have matching settings. To help automate this process, I put together a quick Powershell script that will read/write the machine key to the root machine.config files for all versions of the .NET framework.

machineKeys.ps1

# Machine Key Script
#
# Version 1.0
# 6/5/2012
#
# Jeff Graves
#
param ($readWrite = "read", $allkeys = $true, $version, $validationKey, $decryptionkey, $validation)
 
function GenKey ([int] $keylen) {
	$buff = new-object "System.Byte[]" $keylen
	$rnd = new-object System.Security.Cryptography.RNGCryptoServiceProvider
	$rnd.GetBytes($buff)
	$result =""
	for($i=0; $i -lt $keylen; $i++)	{
		$result += [System.String]::Format("{0:X2}",$buff[$i])
	}
	$result
}
 
function SetKey ([string] $version, [string] $validationKey, [string] $decryptionKey, [string] $validation) {
    write-host "Setting machineKey for $version"
    $currentDate = (get-date).tostring("mm_dd_yyyy-hh_mm_s") # month_day_year - hours_mins_seconds
 
    $machineConfig = $netfx[$version]
 
    if (Test-Path $machineConfig) {
        $xml = [xml](get-content $machineConfig)
        $xml.Save($machineConfig + "_$currentDate")
        $root = $xml.get_DocumentElement()
        $system_web = $root."system.web"
        if ($system_web.machineKey -eq $nul) { 
        	$machineKey = $xml.CreateElement("machineKey") 
        	$a = $system_web.AppendChild($machineKey)
        }
        $system_web.SelectSingleNode("machineKey").SetAttribute("validationKey","$validationKey")
        $system_web.SelectSingleNode("machineKey").SetAttribute("decryptionKey","$decryptionKey")
        $system_web.SelectSingleNode("machineKey").SetAttribute("validation","$validation")
        $a = $xml.Save($machineConfig)
    }
    else { write-host "$version is not installed on this machine" -fore yellow }
}
 
function GetKey ([string] $version) { 
    write-host "Getting machineKey for $version"
    $machineConfig = $netfx[$version]
 
    if (Test-Path $machineConfig) { 
        $machineConfig = $netfx.Get_Item($version)
        $xml = [xml](get-content $machineConfig)
        $root = $xml.get_DocumentElement()
        $system_web = $root."system.web"
        if ($system_web.machineKey -eq $nul) { 
        	write-host "machineKey is null for $version" -fore red
        }
        else {
            write-host "Validation Key: $($system_web.SelectSingleNode("machineKey").GetAttribute("validationKey"))" -fore green
    	    write-host "Decryption Key: $($system_web.SelectSingleNode("machineKey").GetAttribute("decryptionKey"))" -fore green
            write-host "Validation: $($system_web.SelectSingleNode("machineKey").GetAttribute("validation"))" -fore green
        }
    }
    else { write-host "$version is not installed on this machine" -fore yellow }
}
 
$global:netfx = @{"1.1x86" = "C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\CONFIG\machine.config"; `
           "2.0x86" = "C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config"; `
           "4.0x86" = "C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\CONFIG\machine.config"; `
           "2.0x64" = "C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\CONFIG\machine.config"; `
           "4.0x64" = "C:\WINDOWS\Microsoft.NET\Framework64\v4.0.30319\CONFIG\machine.config"}
if(!$allkeys)
{
    while(!$version) {
        $input = read-host "Version (1.1x86, 2.0x86, 4.0x86, 2.0x64, 4.0x64)"
        if ($netfx.ContainsKey($input)) { $version = $input }
    }
}
 
if ($readWrite -eq "read")
{
    if($allkeys) {
        foreach ($key in $netfx.Keys) { GetKey -version $key }
    }
    else {
        GetKey -version $version
    }
}
elseif ($readWrite -eq "write")
{   
    if (!$validationkey) {
    	$validationkey = GenKey -keylen 64
    	write-host "Validation Key: $validationKey" -fore green
    }
 
    if (!$decryptionkey) {
    	$decryptionKey = GenKey -keylen 24
    	write-host "Decryption Key: $decryptionKey" -fore green
    }
 
    if (!$validation) {
    	$validation = "SHA1"
    	write-host "Validation: $validation" -fore green
    }
 
    if($allkeys) {
        foreach ($key in $netfx.Keys) { SetKey -version $key -validationkey $validationkey -decryptionKey $decryptionKey -validation $validation}
    }
    else {
        SetKey -version $version -validationkey $validationkey -decryptionKey $decryptionKey -validation $validation
    }
}

 

Razor ASP.NET web pages and CSHTML Forbidden errors

Recently, we had a support request come through for Cytanium’s ASP.NET 4.5 beta from a user trying to access an app written for ASP.NET web pages with Razor syntax. After publishing the files, the user was receiving the following YSOD:

Server Error in ‘/’ Application.


This type of page is not served.

Description: The type of page you have requested is not served because it has been explicitly forbidden.  The extension ‘.cshtml’ may be incorrect.   Please review the URL below and make sure that it is spelled correctly.
Requested URL: /testpage.cshtml

Normally, this is indicative of incorrect Application Pool settings. Razor syntax only works with ASP.NET 4.0 and requires the Integrated Pipeline to function properly. However, you also need to appropriate ASP.NET MVC files on the server – either in the GAC or deployed to your local /bin folder. Most people have ASP.NET MVC GAC’d on their development systems, so the application will work locally without having the appropriate DLL’s in the /bin folder of the web application. But that’s not necessarily the case on the server side. Per Microsoft’s recommendation, ASP.NET MVC is not GAC’d on the servers as there could be version issues that have a wide impact on all sites running on a shared host. Rather, it is recommended to bin deploy ASP.NET MVC DLL’s to each site. Once the appropriate DLL’s are in the /bin folder, and the app is running under ASP.NET 4.0 Integrate Pipeline, IIS will serve files written with Razor syntax.

ASP.NET Routing and IIS 404 errors

ASP.NET Routing is a powerful feature introduced in .NET 3.5 SP1 and included with .NET 4.0 that allows a developer to route URL’s that are not real files. There are several ways you can accomplish this same task, but having it in the ASP.NET pipeline allows the developer great flexibility in how URL’s are routed. However, it does not always work “out-of-the-box” for devs. The most common error I’ve seen is a 404 error – meaning the page cannot be found.

There can be several contributing factors. If you are attempting to utilize extenionless URL’s, ensure you have the appropriate IIS hotfix installed: http://support.microsoft.com/kb/980368. More commonly however, the issue is a result of the module not firing for your request type. The easy fix is to add runAllManagedModulesForAllRequests=”true” to the modules tag in your web.config. However, that could have performance implications as you are now telling IIS to ignore the preCondition setting for all modules. The alternative solution is to remove the managedHandler preCondition for the URLRouting module:

    <modules>
      <removename=UrlRoutingModule />
      <addname=UrlRoutingModuletype=System.Web.Routing.UrlRoutingModule preCondition=“”/>
    </modules>

IIS6: 404 Error serving content with .com in URL

We ran into an issue today where a customer was having problems serving content from a folder named “example.com”. IIS6 was simply returning a 404 error. I immediately suspected something like URLScan but I eventually found it was due to the execute permissions configured on the parent virtual directory. When the customer configured the virtual directory, they set the execute permissions to “Scripts and executables”. This means that IIS will try to run any cgi compliant executables (.com and .exe files by default) in the virtual directory. In order to run the application, the executable also needs to be authorized in Web Service Extensions.

However, in this case, the URL simply contained “example.com” in the URL: http://server/example.com/images/image1.jpg and we were not trying to run an application. IIS was seeing the “example.com” in the URL and assuming it was a cgi executable and attempting to run the application. However, the file “example.com” did not exist and was therefore returning a 404 error. To correct the issue, we simply set the execute permissions to “None” since the customer was attempting to serve static content, though you can also use “Scripts only”.

The key to this is that there does not need to be a specifc mapping for executables. IIS 6 will attempt to run any executable if the vdir is configured with “Scripts and executables” permissions.