Making the web work. Together.

Banned IP blocking in nopCommerce

Posted by Me on 4 December 2013 | 2 Comments

Tags: , , ,

Background

Nop Commerce used to let you do this [and maybe it will again in the future?] but as of the MVC version [2.*] you can't.

There are a few workarounds, such as adding IP addresses to the web.config but they aren't really manageable for the end user.

Solution

Errrmmm, put the banned ip stuff back, I guess?

Please note...

This is all a bit raw and rough and ready, but in principle, it's functional, manageable and although I haven't load testedi it, it doesn't seem to have any performance issues.

Use of this information entirely at your own risk. We don;t accept any responsibility for it breaking your application.

If you use this and do some stuff differently, please let us know. 

This has been written and tested on Nop Commerce 2.7 but it's pretty simple, so it should upscale and downscale without too many issues.

Let's get started

So, we need a list of IP addresses and a way of checking the visitor against them.

Adding the list is pretty easy. Just follow the Nop/MVC/Telerik/DI conventions and there won;t be a problem.

Checking the IP is done in the Global.asax file.

We can log any matches we find.

We can redirect to somewhere nice for the 'bot or spammer.

Make the database table

We just want a table with an id and an address field. Here's some SQL to do that for you.

------------------------------------------------------------------------------------------

CREATE TABLE [dbo].[BannedIP](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Address] [nvarchar](15) NULL,
 CONSTRAINT [PK_BannedIP] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

-------------------------------------------------------------------------------------------

Create the entities

I've added this all under 'common', (but you could easily create a domain namespace of your own for it) so in Nop.Core.Domain.Coomon, create BannedIP.cs as follows:

-------------------------------------------------------------------------------------------

namespace Nop.Core.Domain.Common
{
    public class BannedIP : BaseEntity
    {
        public string Address { get; set; }
    }
}

-------------------------------------------------------------------------------------------

Now under Nop.Data.Mapping.Common, create BannedIPMap.cs  as follows:

-------------------------------------------------------------------------------------------

using System.Data.Entity.ModelConfiguration;
using Nop.Core.Domain.Common;

namespace Nop.Data.Mapping.Common
{
    public partial class BannedIPMap : EntityTypeConfiguration<BannedIP>
    {
        public BannedIPMap()
        {
            this.ToTable("BannedIP");
            this.Property(a => a.Address);
        }
    }
}

-------------------------------------------------------------------------------------------

That's it for entities, now for services...

Create the services

In Nop.Services.Common, create BannedIPService.cs as follows:

-------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using Nop.Core.Data;
using Nop.Core.Domain.Common;
using Nop.Services.Events;

namespace Nop.Services.Common
{
    public partial class BannedIPService : IBannedIPService
    {
        #region Fields

        private readonly IRepository<BannedIP> _bannedIpRepository;
        private readonly IEventPublisher _eventPublisher;

        #endregion

        #region Ctor

        public BannedIPService(IRepository<BannedIP> bannedIpRepository
            , IEventPublisher eventPublisher)
        {
            _bannedIpRepository = bannedIpRepository;
            _eventPublisher = eventPublisher;
        }

        #endregion

        #region Methods

        public virtual IList<BannedIP> GetAllBannedIPs()
        {
            var query = from t in _bannedIpRepository.Table
                        orderby t.Id
                        select t;

            var documents = query.ToList();
            return documents;
        }

        public virtual BannedIP GetBannedIPById(int Id)
        {
            var query = from t in _bannedIpRepository.Table
                        where t.Id == Id
                        select t;

            var document = query.ToList().FirstOrDefault();
            return document;
        }

        public virtual BannedIP GetBannedIPByIP(string Ip)
        {
            var query = from t in _bannedIpRepository.Table
                        where t.Address == Ip
                        select t;

            var document = query.ToList().FirstOrDefault();
            return document;
        }

        public virtual void InsertBannedIP(BannedIP bannedip)
        {
            if (bannedip == null)
                throw new ArgumentNullException("document");

            _bannedIpRepository.Insert(bannedip);

            //event notification
            _eventPublisher.EntityInserted(bannedip);
        }

        public virtual void UpdateBannedIP(BannedIP bannedip)
        {
            if (bannedip == null)
                throw new ArgumentNullException("bannedip");

            _bannedIpRepository.Update(bannedip);

            //event notification
            _eventPublisher.EntityUpdated(bannedip);
        }

        public virtual void DeleteBannedIP(BannedIP bannedip)
        {
            if (bannedip == null)
                throw new ArgumentNullException("document");

            _bannedIpRepository.Delete(bannedip);

            //event notification
            _eventPublisher.EntityDeleted(bannedip);
        }

        #endregion
    }
}

-------------------------------------------------------------------------------------------

And next, create the interface for it to inherit from as follows:

-------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using Nop.Core.Data;
using Nop.Core.Domain.Common;

namespace Nop.Services.Common
{
    public partial interface IBannedIPService
    {
        IList<BannedIP> GetAllBannedIPs();

        BannedIP GetBannedIPById(int Id);

        BannedIP GetBannedIPByIP(string Ip);

        void InsertBannedIP(BannedIP bannedip);

        void UpdateBannedIP(BannedIP bannedip);

        void DeleteBannedIP(BannedIP bannedip);
    }
}

---------------------------------------------------------------------------------------------

Ok, that's it for the Logic/entities.data bit. No on to admin.

Creating the admin elements

Note: Because I've added all this to 'Common' and used the same namespace in the admin area, the naming is a bit messy, so by all means move it into its own namespace so you can follow add/edit/create/delete if it drives you insane.

First off, add a menu item to sitemap.config as follows:

<siteMapNode title="Banned IPs" nopResource="Admin.Configuration.BannedIPs" PermissionNames="ManageBannedIPs" controller="Common" action="ListBannedIPs"/>

I added this to the bottom of the configuration section, but anywhere is good.

So now we need a model for the banned IP object and a model for the list of them. Create the two models in Nop.Admin.Models.Common as follows:

-----------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using Nop.Web.Framework;
using Nop.Web.Framework.Mvc;
using Telerik.Web.Mvc.UI;

namespace Nop.Admin.Models.Common
{
    public partial class BannedIPModel : BaseNopEntityModel
    {
        public BannedIPModel() {}

        [NopResourceDisplayName("Admin.ContentManagement.BannedIP.Fields.Name")]
        [AllowHtml]
        public string Address { get; set; }
    }
}

using System.Collections.Generic;
using Nop.Web.Framework.Mvc;
using Telerik.Web.Mvc;

namespace Nop.Admin.Models.Common
{
    public class BannedIPListModel : BaseNopModel
    {
        public GridModel<BannedIPModel> BannedIPs { get; set; }
    }
}

-----------------------------------------------------------------------------------------------

Next, we need to create the views. There are 4 as follows:

#1. ListBannedIPs.cshtml

-----------------------------------------------------------------------------------------------

@model BannedIPListModel
@using Telerik.Web.Mvc.UI
@{    
    //page title
    ViewBag.Title = T("Admin.ContentManagement.BannedIPs").Text;
}
@using (Html.BeginForm())
{
    <div>
        <div>
            <img src="@Url.Content("~/Administration/Content/images/ico-content.png")" alt="" />
            @T("Admin.ContentManagement.BannedIPs")
        </div>
        <div>
            <a href="@Url.Action("CreateBannedIp")">@T("Admin.Common.AddNew")</a>
        </div>
    </div>
    
    <table>
        <tr>
            <td>
                @(Html.Telerik().Grid<BannedIPModel>(Model.BannedIPs.Data)
                        .Name("documents-grid")
                        .Columns(columns =>
                        {
                            columns.Bound(x => x.Address)
                                .Width(200);
                            columns.Bound(x => x.Id)
                                .Width(50)
                                .Centered()
                                .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "EditBannedIp", new { id = x.Id }))
                                .ClientTemplate("<a href=\"Edit/<#= Id #>\">" + T("Admin.Common.Edit").Text + "</a>")
                                .Title(T("Admin.Common.Edit").Text);
                        })
                        .DataBinding(dataBinding => dataBinding.Ajax().Select("ListBannedIPs", "Common"))
                        .EnableCustomBinding(true))
            </td>
        </tr>
    </table>
}

----------------------------------------------------------------------------------------------------

#2._CreateOrUpdateBannedIp.cshtml

-----------------------------------------------------------------------------------------------------

@model BannedIPModel

@using Telerik.Web.Mvc.UI;
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.Id)
<table>
    <tr>
        <td>
            @Html.NopLabelFor(model => model.Address):
        </td>
        <td>
            @Html.EditorFor(model => model.Address)
            @Html.ValidationMessageFor(model => model.Address)
        </td>
    </tr>
</table>

------------------------------------------------------------------------------------------------------

#3. CreateBannedIp.cshtml

------------------------------------------------------------------------------------------------------

@model BannedIPModel
@{    
    //page title
    ViewBag.Title = T("Admin.ContentManagement.BannedIp.AddNew").Text;
}
@using (Html.BeginForm("CreateBannedIp", "Common", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <div>
        <div>
            <img src="@Url.Content("~/Administration/Content/images/ico-content.png")" alt="" />
            @T("Admin.ContentManagement.BannedIp.AddNew") @Html.ActionLink("(" + T("Admin.ContentManagement.BannedIp.BackToList") + ")", "ListBannedIPs")
        </div>
        <div>
            <input type="submit" name="save" value="@T("Admin.Common.Save")" />
            <input type="submit" name="save-continue" value="@T("Admin.Common.SaveContinue")" />
        </div>
    </div>
    @Html.Partial("_CreateOrUpdateBannedIp", Model)
}

------------------------------------------------------------------------------------------------------

#4. EditBannedIp.cshtml

------------------------------------------------------------------------------------------------------

@model BannedIPModel
@{    
    //page title
    ViewBag.Title = T("Admin.ContentManagement.BannedIP.EditBannedIPDetails").Text;
}
@using (Html.BeginForm("EditBannedIP", "Common", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <div>
        <div>
            <img src="@Url.Content("~/Administration/Content/images/ico-content.png")" alt="" />
            @T("Admin.ContentManagement.Documents.EditBannedIPDetails") - @Model.Address @Html.ActionLink("(" + T("Admin.ContentManagement.BannedIP.BackToBannedIPList") + ")", "ListBannedIPs")
        </div>
        <div>
            <input type="submit" name="save" value="@T("Admin.Common.Save")" />
            <input type="submit" name="save-continue" value="@T("Admin.Common.SaveContinue")" />
            <span id="document-delete">@T("Admin.Common.Delete")</span>
        </div>
    </div>
    @Html.Partial("_CreateOrUpdateBannedIp", Model)
}
@Html.DeleteConfirmation("bannedip-delete")

----------------------------------------------------------------------------------------------------------

Ok, now we need the controller stuff. To CommonController.cs (in Admin.Controllers) add the following section/region of methods:

----------------------------------------------------------------------------------------------------------

        public ActionResult ListBannedIPs()
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            var documents = _bannedIpService.GetAllBannedIPs();

            var model = new BannedIPListModel();
            model.BannedIPs = new GridModel<BannedIPModel>
            {
                Data = documents.Select(x =>
                {
                    var documentsModel = x.ToModel();
                    return documentsModel;
                })
            };

            return View(model);
        }

        public ActionResult CreateBannedIp()
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            var model = new BannedIPModel();

            return View(model);
        }

        [HttpPost, ParameterBasedOnFormNameAttribute("save-continue", "continueEditing")]
        public ActionResult CreateBannedIp(BannedIPModel model, bool continueEditing)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            if (ModelState.IsValid)
            {
                var bannedip = model.ToEntity();

                _bannedIpService.InsertBannedIP(bannedip);

                //activity log
                _customerActivityService.InsertActivity("AddNewBannedIP", _localizationService.GetResource("ActivityLog.AddNewBannedIP"), bannedip.Address);

                SuccessNotification(_localizationService.GetResource("Admin.Catalog.Documents.Added"));
                return continueEditing ? RedirectToAction("EditBannedIp", new { id = bannedip.Id }) : RedirectToAction("ListBannedIPs");
            }

            //If we got this far, something failed, redisplay form
            return View(model);
        }

        public ActionResult EditBannedIp(int id)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            var document = _bannedIpService.GetBannedIPById(id);
            if (document == null)
                //No topic found with the specified id
                return RedirectToAction("ListBannedIPs");

            var model = document.ToModel();

            return View(model);
        }

        [HttpPost, ParameterBasedOnFormNameAttribute("save-continue", "continueEditing")]
        public ActionResult EditBannedIp(BannedIPModel model, bool continueEditing)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            var bannedip = _bannedIpService.GetBannedIPById(model.Id);
            if (bannedip == null)
                //No topic found with the specified id
                return RedirectToAction("List");

            if (ModelState.IsValid)
            {
                bannedip = model.ToEntity(bannedip);

                _bannedIpService.UpdateBannedIP(bannedip);

                SuccessNotification(_localizationService.GetResource("Admin.ContentManagement.BannedIP.Updated"));
                return continueEditing ? RedirectToAction("EditBannedIp", bannedip.Id) : RedirectToAction("ListBannedIPs");
            }

            //If we got this far, something failed, redisplay form
            return View(model);
        }

        [HttpPost]
        public ActionResult DeleteBannedIp(int id)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ManageDocuments))
                return AccessDeniedView();

            var bannedip = _bannedIpService.GetBannedIPById(id);
            if (bannedip == null)
                //No topic found with the specified id
                return RedirectToAction("ListBannedIPs");

            _bannedIpService.DeleteBannedIP(bannedip);

            SuccessNotification(_localizationService.GetResource("Admin.ContentManagement.BannedIP.Deleted"));
            return RedirectToAction("ListBannedIPs");
        }

-----------------------------------------------------------------------------------------------------------

Ok, now we have the basic outline of storing, adding and, well, not deleting because I have to fix that bit yet, but the rest works ok so far.

Now we need to do all the DI and Autofac stuff, so we can take advantage of out new functionality.

First, add service references at the top of the CommonController.cs as follows:

        private readonly IBannedIPService _bannedIpService;
        private readonly ICustomerActivityService _customerActivityService;

Then reference them in the constructor:

            , IBannedIPService bannedIpService
            , ICustomerActivityService customerActivityService

Then bind them:

            this._bannedIpService = bannedIpService;
            this._customerActivityService = customerActivityService;

(You should see the pattern for all this at the top of the Controller. It's common in nop).

Next, we add the mappings to Nop.Admin.Infrastructure AutoMapperStartupTask.cs as follows:

            // BannedIP mapping
            Mapper.CreateMap<BannedIP, BannedIPModel>();
            Mapper.CreateMap<BannedIPModel, BannedIP>();

Just add them at the bottom.

Ok, next we need to add the stuff to convert entities to model. So, open MappingExtensions.cs from  Nop.Admin, then  add the following at the bottom:

-------------------------------------------------------------------------------------------------------------------------

      #region BannedIps

        public static BannedIPModel ToModel(this BannedIP entity)
        {
            return Mapper.Map<BannedIP, BannedIPModel>(entity);
        }

        public static BannedIP ToEntity(this BannedIPModel model)
        {
            return Mapper.Map<BannedIPModel, BannedIP>(model);
        }

        public static BannedIP ToEntity(this BannedIPModel model, BannedIP destination)
        {
            return Mapper.Map(model, destination);
        }

        #endregion

-------------------------------------------------------------------------------------------------------------------------

I put it in a region, so it's all neat and titdy.

Next, we need to add the Service to the DependencyRegistrar.cs in Nop.Web.Framework as follows:

            // Banned IP service
            builder.RegisterType<BannedIPService>().As<IBannedIPService>().InstancePerHttpRequest();

Just add it at the bottom of the Register() method.

Now last but not least, we need to add the check in the actual web site  for aninvalid/banned IP. We add this in the Global.asax file in the Application_BeginRequest() method, but first we need to add a few things to expose the services.

Add a using to the Global.asax as follows:

using Nop.Services.Common;

Then inside the Application_BeginRequest() method, add the following:

            // Check IP for an entry in the banned list.
            var bannedipservice = EngineContext.Current.Resolve<IBannedIPService>();
            var ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"].ToString();

            var bannedip = bannedipservice.GetBannedIPByIP(ip);

            if (bannedip != null)
            {
                // Log something here?
                var logger = EngineContext.Current.Resolve<ILogger>();
                logger.Error("Banned IP detected: " + bannedip.Address, null, null);

                // Redirect to somewhere here?
                HttpContext.Current.Response.Redirect("http://www.spamhaus.org");
            }

So this does the work. It creates the service, then finds the remote IP. gets a banned ip object, compares and if it matches, it logs it and redirects to a different place, in this case Spamhaus.

So now you can build everything and it should work. It's entirely possible I've missed something, so if you have any errors or there is an omission, please let me know. I have a 3.00 and a 3.1 site to put this on shortly, so I'll update with any differences. I do think Dependency Injection is slighly different on 3?

ToDo...

I really should give it all its own namespaces.

I should make it into a plugin?

I need to add all the proper localisation strings.

It needs a link from the customer IP to add data to the list , but this breaks the upgrade pattern so something in the plugin needs to be looking at users.

It could do with a few settings for stuff like the redirect URL.

I need to fix the delete... :)

################

Thanks for reading. Good luck.

 


Post your comment

Comments

  • Hi Steven,

    I'm glad it's of some interest. You are right about there being no obvious hook into Start_Request so it's either change the Global, create a http module or try something with Action Filters?

    As an aside to this, I've been looking at the Honey Pot option and I'm thinking of adding some 'smart' checking of form data and also some means by which common spam insertions ('bots, payed human zombies) can be checked for and added to the banned IP list immediately. I'm still thinking about the best approach, but it's tricky without mangling base nop code.

    Posted by Ed, 11/12/2013 3:47pm (8 years ago)

  • Great post! Thanks - I'm interested in this as a plugin but am unsure how possible it is from a plugin, not sure what nop offers in terms of allowing a plugin to bind to the beginning of incoming requests.

    Posted by Steven Sproat, 05/12/2013 11:41pm (8 years ago)

RSS feed for comments on this page | RSS feed for all comments