HI WELCOME TO SIRIS

Web.config redirects with rewrite rules - https, www, and more

Leave a Comment

 Rewrite rules are a powerful feature in IIS. Common tasks like redirecting www to non-www (or the other way around), implementing canonical URLs, redirecting to HTTPS, and similar tasks are documented right there in your Web.config file. In this post, you will learn about the syntax of rewrite rules and how to implement the mentioned common tasks, and much more.

Web.config redirects with rewrite rules - https, www, and more

When needing to redirect one URL to another, a lot of different options exist. Some tasks can be implemented in the DNS. Others are done through reverse proxies like Nginx and Squid. Sometimes you just want to redirect a single URL and you can choose to modify the HTTP response from within an MVC controller.

A much more generic solution is IIS rewrite rules. With rules, you can create a common set of rules and make IIS enforce these over multiple URLs and even across applications.

Rewrite rules can be either global (in the applicationHost.config file) or local (in the web.config file). There are several ways to add both global and local rules. IIS Manager has built-in support for creating rules, but it is my impression most people don't have IIS installed locally. Rules have been the source of lots of frustration on StackOverflow, which is why using IIS Manager for creating your first rule can be a good choice.

Creating a new rule

To create a new rule, launch IIS Manager, and click a website:

Website in IIS Manager

See the URL Rewrite icon below the IIS section? If not, make sure to download and install the URL Rewrite extension: https://www.iis.net/downloads/microsoft/url-rewrite.

Double-click the URL Rewrite icon and click the Add Rule(s)... link from the Actions menu:

Add rule

There are a wide range of different templates to choose from. As a first-time rule developer, getting a bit of assistance is a nice little feature of the IIS Manager. I'm not saying you shouldn't learn the underlying syntax (you need to), but looking at pre-generated examples is a good starting point.

Select the Append or remove the trailing slash symbol template and click the OK button. In the following step, keep the Appended if it does not exist selection and click OK. A new inbound rule is created:

URL Rewrite rule

Requesting the website without a trailing slash (/) will automatically append the slash using the newly created rule.

Since the rule was created directly on the website, the new rule is added to the web.config file of the website. You hopefully understand why this approach shouldn't be used in your production environment. Every piece of configuration should be in either source control or deployment software like Octopus Deploy or Azure DevOps. For playing around with rules, the UI approach is excellent, though.

Get the full overview of web.config errors

➡️ Full error logging support for web.config configuration problems with elmah.io centralized logging ⬅️

Let's take a look at the generated code inside the web.config file:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="AddTrailingSlashRule1" stopProcessing="true">
                    <match url="(.*[^/])$" />
                    <conditions>
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    </conditions>
                    <action type="Redirect" url="{R:1}/" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

At first glance, rewrite rules can be a challenge to read. If you feel this way, I understand you. Configuring software in deeply nested XML has never been my favorite either. Let's go through the markup one element at a time.

Inbound and outbound rules

All rules must be added to the <rewrite> element inside <system.webServer>. There are two types of rules available: inbound rules (defined in the <rules> element) and outbound rules (defined in the <outboundRules> element). The rewrite rule is added as an inbound rule in the example above. Inbound rules are executed against the incoming request. Examples of these rules are: adding a trailing slash (yes, the previous example), rewriting the request to a different URL, and blocking incoming requests based on headers. Outbound rules are executed against the response of an HTTP request. Examples of outbound rules are: adding a response header and modifying the response body.

Both inbound and outbound rules are defined in a <rule> element. In the example above, a new rule named AddTrailingSlashRule1 is added. The name of each rule is left for you to decide, as long as you give each rule a unique name. Also, take notice of the stopProcessing attribute specified on the rule. If set to true, IIS will stop executing additional rules if the condition specified inside the rule matches the current request. You mostly want to specify false in this attribute, unless you are redirecting the entire request to another URL.

Match, action, and conditions

All rules have a pattern, an action, and an optional set of conditions. The example above has all three.

The pattern is implemented in the <match> element and tells the rewrite extension which URLs to execute the rule for. Patterns are written using regular expressions in the url attribute. Regular expressions is an extremely complicated language, which deserves a very long blog post on its own. I write regular expressions by using Google to find examples of how people already wrote examples of regular expressions looking like what I would like to achieve in each case.

The <action> element tells IIS what to do about requests matching the pattern. All actions need a type attribute to tell IIS how to move forward. type can have one of the following values:

  • None: do nothing.
  • Rewrite: rewrite the request to another URL.
  • Redirect: redirect the request to another URL.
  • CustomResponse: return a custom response to the client.
  • AbortRequest: drop the HTTP connection for the request.

In the example above, the action used a Redirect type, meaning the web server will return an HTTP redirect to the client. You can specify if you want to use a permanent or temporary redirect using the redirectType attribute.

I don't want to list every possible element and value inside this post since the official MSDN documentation is quite good. Visual Studio also provides decent IntelliSense for rewrite rules. Browse through the examples last in this post to get an overview of what is possible with rewrite rules.

Common rules

This is a collection of common rules that we are either using on elmah.io or that I have used in the past.

Redirect to HTTPS

<rule name="RedirectToHTTPS" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{HTTPS}" pattern="off" ignoreCase="true" />
  </conditions>
  <action type="Redirect" url="https://{SERVER_NAME}/{R:1}" redirectType="Permanent" />
</rule>

Redirect www to non-www

<rule name="RedirectWwwToNonWww" stopProcessing="false">
  <match url="(.*)" />
  <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
    <add input="{HTTP_HOST}" pattern="^(www\.)(.*)$" />
  </conditions>
  <action type="Redirect" url="https://{C:2}{REQUEST_URI}" redirectType="Permanent" />
</rule>

Redirect non-www to www

<rule name="RedirectNonWwwToWww" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{HTTP_HOST}" pattern="^domain.com$" />
  </conditions>
  <action type="Redirect" url="http://www.domain.com/{R:0}" redirectType="Permanent" />
</rule>

Reverse proxy

In this example, I'm using rewrite rules to serve a blog hosted on GitHub but through a custom domain name. By using the Rewrite action type, the URL stays the same, even though the content is served from elmahio.github.io.

<rule name="ReverseProxy" stopProcessing="true">
  <match url="(.*)" />
  <conditions logicalGrouping="MatchAll">
    <add input="{REQUEST_URI}" negate="true" pattern="^(.*)/.well-known/(.*)$"/>
  </conditions>
  <action type="Rewrite" url="http://elmahio.github.io/blog/{R:1}" />
</rule>

Rewrite all but elmah.axd

<rule name="RewriteAllButElmahAxd" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
    <add input="{URL}" negate="true" pattern="\elmah.axd$" />
  </conditions>
  <action type="Rewrite" url="http://elmahio.github.io/blog/{R:1}" />
</rule> 

Add trailing slash

<rule name="AddTrailingSlash" stopProcessing="true">
  <match url="(.*[^/])$" />
  <conditions>
    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
  </conditions>
  <action type="Redirect" url="{R:1}/" redirectType="Permanent" />
</rule>

Remove trailing slash

<rule name="RemoveTrailingSlash" stopProcessing="true">
  <match url="(.*[^/])$" />
  <conditions>
    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
  </conditions>
  <action type="Redirect" url="{R:1}" redirectType="Permanent" />
</rule>

Lowercase all URLs

<rule name="LowercaseAllUrls" stopProcessing="true">
  <match url=".*[A-Z].*" ignoreCase="false" />
  <action type="Redirect" url="{ToLower:{R:0}}" />
</rule>

Custom maintenance response

If you are allowed downtime for maintenance on your site, using a rewrite rule to return a custom error response to the user might be an option.

<rule name="CustomErrorResponse" stopProcessing="true">
  <match url="(.*)" />
  <action type="CustomResponse" statusCode="503" statusReason="Down for maintenance" />
</rule>

Never rewrite elmah.axd

If you have ELMAH, the open-source library, installed on your site you have a local URL named /elmah.axd to help browse your error log. In some cases, you may have rewrite rules configured, the messes with ELMAH and therefore shouldn't be run on ELMAH. You can use the negate feature of rewrite rules to keep our specific URLs or patterns:

<rule name="CustomErrorResponse" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{URL}" negate="true" pattern="\elmah.axd$" />
  </conditions>
  <action type="CustomResponse" statusCode="503" statusReason="Down for maintenance" />
</rule>

Validating web.config and monitor errors

Rewrite rules can be hard to test. Being XML and in some cases modifications that you want running on production only, forces most of your test to happen manually in a browser. There are a couple of tools available to help you write and test rewrite rules. As already mentioned, Visual Studio has decent IntelliSense for rules. I always recommend people to validate the finished Web.config file using this Web.config Validator. If you are writing rewrite rules as part of a Web.config transformation file, also make sure to test the transformed file using the Web.config Transformation Tester. You can copy the transformed result and validating it with the already mentioned validator.

When it comes to monitoring errors, I will recommend you implement a good error monitoring process in your production environment. For obvious reasons, I want to highlight elmah.io as the best solution for monitoring problems with your Web.config file. But there are other options out there that will get you almost as far. At least pick a solution that will notify you through email, Slack, Teams, or similar, to avoid having to look through log files on your webserver.

Python Django + Microsoft SQL Server CRUD API tutorial

Leave a Comment

 In this post we will see how to create CRUD APIs using Python Django as backend and Microsoft SQL Server for database.

Lets install the necessary modules needed for our Django project.

First lets install the Django module.

>> pip install django

To create rest APIs we need to install Django rest framework.

>> pip install djangorestframework

By default, the Django project comes with a security that blocks requests coming from different domains. To disable this, lets install Django CORS headers module.

>> pip install django-cors-headers

Now lets create the Django project.

Open the command prompt in the desired folder and type the command. cmd in the filename

>> django-admin startproject <name of the project>

to open in visual studio got the the project folder type cmd in the filename and enter command 'code .'

Lets take a look at some of the important files in the project.

 


>> __init__.py file is just and empty file that indicates that the given folder is a python project or a python module.

>>  asgi.py is the entry point for the asgi compatible webservers.

>> wsgi.py is the entry point for the wsgi compatible web servers.

>> urls.py file contains all the url declarations needed for this project.

>> settings.py file contains all the settings or the configurations needed for the project.

>> manage.py is a command line utility that helps interact with the Django project.

 

Lets simply run the project and see how it looks in the browser using below command.

>> python manage.py runserver

The app is now running in the port 8000.


 Fn + B  to exit server compiler



Lets copy  the url and open in the browser.

What you see on the screen is the default template that comes with every Django project.

 


 

Now lets create an app in our Django project.

Quick difference between projects and apps.

The folder structure that you currently see on the screen is called the project.



A project may have multiple apps.

For example, you can have one app which acts like a blog, or may be another app which acts like a survey form.

Currently this project does not have any app.

Lets create one app to implement our api methods.

To create an app, we need to type this command.

>> python manage.py startapp <the name of the app>


Next let us register the app and the required modules in settings.py file.

In the installed apps section, lets add Rest framework, cors header, and the newly created app.

We need to add the cors headers in middle ware section as well.

We will also add instruction to enable all domains to access the APIs.

This is not recommended in production. Instead, just add only those domains that needs to be whitelisted.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'EmployeeApp.apps.EmployeeappConfig'
]

CORS_ORIGIN_ALLOW_ALL = True

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

 

Lets create the models needed for our app.

We need two models.

One to store department details and another one to store employee details.

The departments model will have two fields. One to store an autoincremented Department ID, and another one to store Department Name.

Employee model will have five fields.

Employee ID, Employee name, Department, Date of joining, and photo file name which stores the uploaded profile picture file name.

Models.py complete code:

from django.db import models

# Create your models here.

class Departments(models.Model):
    DepartmentId = models.AutoField(primary_key=True)
    DepartmentName = models.CharField(max_length=500)

class Employees(models.Model):
    EmployeeId = models.AutoField(primary_key=True)
    EmployeeName = models.CharField(max_length=500)
    Department = models.CharField(max_length=500)
    DateOfJoining = models.DateField()
    PhotoFileName = models.CharField(max_length=500)

We will be using the Microsoft SQL Server database.

To connect to SQL Server from our Python Django app, we need to install the database adapters.

>> pip install pyodbc

>> pip install django-pyodbc-azure

Add the database details in settings.py file.






Lets write the command to make migrations file for our models.

>> python manage.py makemigrations <app name>

After executing this, we can see a migration file which tells us what changes to the database will be done.



Once it looks fine, we can execute the command to push these changes to the database.

>> python manage.py migrate <app name>


Lets create serializers for our models.

Serializers basically help to convert the complex types or model instances into native python data types that can then be easily rendered into json or xml or other content types.

They also help in deserialization which is nothing but converting the passed data back to complex types.

 serializers.py complete code:

from rest_framework import serializers
from EmployeeApp.models import Departments,Employees

class DepartmentSerializer(serializers.ModelSerializer):
    class Meta:
        model=Departments 
        fields=('DepartmentId','DepartmentName')

class EmployeeSerializer(serializers.ModelSerializer):
    class Meta:
        model=Employees 
        fields=('EmployeeId','EmployeeName','Department','DateOfJoining','PhotoFileName')

Lets now start writing the API methods.

views.py code.


from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from django.http.response import JsonResponse

from EmployeeApp.models import Departments,Employees
from EmployeeApp.serializers import DepartmentSerializer,EmployeeSerializer

from django.core.files.storage import default_storage

# Create your views here.

@csrf_exempt
def departmentApi(request,id=0):
    if request.method=='GET':
        departments = Departments.objects.all()
        departments_serializer=DepartmentSerializer(departments,many=True)
        return JsonResponse(departments_serializer.data,safe=False)
    elif request.method=='POST':
        department_data=JSONParser().parse(request)
        departments_serializer=DepartmentSerializer(data=department_data)
        if departments_serializer.is_valid():
            departments_serializer.save()
            return JsonResponse("Added Successfully",safe=False)
        return JsonResponse("Failed to Add",safe=False)
    elif request.method=='PUT':
        department_data=JSONParser().parse(request)
        department=Departments.objects.get(DepartmentId=department_data['DepartmentId'])
        departments_serializer=DepartmentSerializer(department,data=department_data)
        if departments_serializer.is_valid():
            departments_serializer.save()
            return JsonResponse("Updated Successfully",safe=False)
        return JsonResponse("Failed to Update")
    elif request.method=='DELETE':
        department=Departments.objects.get(DepartmentId=id)
        department.delete()
        return JsonResponse("Deleted Successfully",safe=False)

@csrf_exempt
def employeeApi(request,id=0):
    if request.method=='GET':
        employees = Employees.objects.all()
        employees_serializer=EmployeeSerializer(employees,many=True)
        return JsonResponse(employees_serializer.data,safe=False)
    elif request.method=='POST':
        employee_data=JSONParser().parse(request)
        employees_serializer=EmployeeSerializer(data=employee_data)
        if employees_serializer.is_valid():
            employees_serializer.save()
            return JsonResponse("Added Successfully",safe=False)
        return JsonResponse("Failed to Add",safe=False)
    elif request.method=='PUT':
        employee_data=JSONParser().parse(request)
        employee=Employees.objects.get(EmployeeId=employee_data['EmployeeId'])
        employees_serializer=EmployeeSerializer(employee,data=employee_data)
        if employees_serializer.is_valid():
            employees_serializer.save()
            return JsonResponse("Updated Successfully",safe=False)
        return JsonResponse("Failed to Update")
    elif request.method=='DELETE':
        employee=Employees.objects.get(EmployeeId=id)
        employee.delete()
        return JsonResponse("Deleted Successfully",safe=False)

@csrf_exempt
def SaveFile(request):
    file=request.FILES['file']
    file_name=default_storage.save(file.name,file)
    return JsonResponse(file_name,safe=False)

For Save File API, create a folder with name 'Photos':

settings.py complete code:


"""
Django settings for DjangoAPI project.

Generated by 'django-admin startproject' using Django 3.2.4.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path
import os

BASE_DIR=Path(__file__).resolve(strict=True).parent.parent
MEDIA_URL='/Photos/'
MEDIA_ROOT=os.path.join(BASE_DIR,"Photos")


# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-@oxx-o(4f=mxha%-tlv97)x9m7x_fw=(@*k=*29q%r7c8*)%-&'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'EmployeeApp.apps.EmployeeappConfig'
]

CORS_ORIGIN_ALLOW_ALL = True

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'DjangoAPI.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'DjangoAPI.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'sql_server.pyodbc',
        'NAME': '<db name>',
        'USER':'<user>',
        'PASSWORD':'<password>',
        'HOST':'<server name>',
        'OPTIONS':{
            'driver':'ODBC Driver 17 for SQL Server',
            'isolation_level':'READ UNCOMMITTED' #to prevent deadlocks
        }
    }
}

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

EmployeeApp's urls.py:

from django.conf.urls import url
from EmployeeApp import views

from django.conf.urls.static import static
from django.conf import settings

urlpatterns=[
    url(r'^department$',views.departmentApi),
    url(r'^department/([0-9]+)$',views.departmentApi),

    url(r'^employee$',views.employeeApi),
    url(r'^employee/([0-9]+)$',views.employeeApi),

    url(r'^employee/savefile',views.SaveFile)
]+static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

Main urls.py:

"""DjangoAPI URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path

from django.conf.urls import url,include

urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^',include('EmployeeApp.urls'))
]