CYBR8470

This repo contains a digitized version of the course content for CYBR8470 Secure Web App Development at the University of Nebraska at Omaha.

View the Project on GitHub MLHale/CYBR8470

Building a Server

Introduction

In this module, you will learn how to build a server of your own and connect it up to IFTTT (a web automation toolkit mostly used for the Internet of Things).

Goals

By the end of this tutorial, you will be able to:

Materials Required

For this lesson, you will need:

Table of Contents

Step 1: Review - Where are we so far?

We saw how containers could be used to host isolated servers on another host machine.

Now, in this lesson, we will examine how to create our own server and deploy it in a container.

For reference, this is the overall design we are looking at. On the left side, you have some sensors - they could be anything, since this isn’t an IoT class, we can just assume we are getting data from them. In this lesson, we will begin building the item marked custom web API in the figure below. It will have features to support authentication, logging events, and we can even include a hook to trigger another service (like IFTTT). Web App Architecture

Step 2: No, you won’t be starting from scratch

The process of creating a new application server from the ground up takes some time and attention. Instead of having you start from the ground up, I’ll provide you with some starter skeleton code. This code does the basics - you will extend it to accepting requests, store the data that comes in, and then trigger another service.

Open a new terminal instance:

cd Desktop
git clone https://github.com/<your-github-id without the brackets>/cybr8470-web-service-lab
cd cybr8470-web-service-lab/

Now use docker to build the image that our container will use, from the cybr8470-web-service-lab directory:

docker compose build

With this, we should be able to type the following and see our new image.

docker images

It will be called something like cybr8470-web-service-lab-backend_django.

Step 3: Setup the server

This server is completely new, so we need to do some setup to get it initially configured. Execute the following to run the server and open up a bash terminal to interact with it.

docker compose up

This will initialize the server and tell our Django server to setup the database. It will create a database Schema that our SQL server can use to store data.

in a separate terminal:

docker compose exec django bash

In this terminal that opens in the container, we will use the manage utility to create new superuser account. Specify a password for admin. In development, you can use something simple (e.g. admin1234) for simplicity. In practice, you would want to use a much more secure password - since the server could be accessed from the wider internet.

python manage.py createsuperuser --username admin --email admin
exit

Step 4: Run the server

Although our server is already running, any time you want to stop it you can press Control + C. To bring it back up just type:

docker compose up

This server, diagrammatically looks like:

Django Architecture

The docker command executes the container using the docker-compose.yml file located in your /cybr8470-web-service-lab/ folder.

With the server running, you should be able to visit http://localhost to see your server.

This is a web client (also called a frontend) that I’ve built for demo purposes to work with our server. You will be making the server work with the client. I’ve included the client code in the /frontend folder, but you won’t need to modify it for this lab. Later labs will deal with client-side development.

Step 5: Building the server event API endpoint

Since our focus is the backend - lets take a look at our server environment. This is built with Python 2.7 and is intentionally using an old version of Django (the reasons will become clear in the next lab). First, Lets explore the file tree.

Models.py

class ApiKey(models.Model):
    owner = models.CharField(max_length=1000, blank=False)
    key = models.CharField(max_length=5000, blank=False)

    def __str__(self):
        return str(self.owner) + str(self.key)

class ApiKeyAdmin(admin.ModelAdmin):
    list_display = ('owner','key')
class Event(models.Model):
    eventtype = models.CharField(max_length=1000, blank=False)
    timestamp = models.DateTimeField()
    userid = models.CharField(max_length=1000, blank=True)
    requestor = models.GenericIPAddressField(blank=False)

    def __str__(self):
        return str(self.eventtype)

class EventAdmin(admin.ModelAdmin):
    list_display = ('eventtype', 'timestamp')

urls.py

Next lets look at urls.py. This file tells Django which URLs are accessible on the server. If a URL entry isn’t included in a urls.py file, then the method cannot be accessed.

api/urls.py

urlpatterns = [
    url(r'^session', csrf_exempt(controllers.Session.as_view())),
    url(r'^register', csrf_exempt(controllers.Register.as_view())),
    url(r'^', include(router.urls)),
]

django_backend/urls.py

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    url(r'^api/', include(api_urls)),
    url(r'^xss-example/', controllers.xss_example),
    url(r'^', controllers.home),
]

Controllers.py

Next, lets look at the controllers.py file to see what the server does when a URL is visited.

There is some code in this file that handles session-based authentication and registration, but we need to create more code to make this app work.

Specifically the client is trying to retrieve events, so it can display them accordingly in the app. We need an API endpoint that handles requests to the /api/events URL.

Inspect the network console to see the requests the client is making.

skeleton client app

Notice it is making a request to /api/events/ and getting a 405 Method not allow error - because our API does not support this endpoint.

These conventions are not specific to Django they are based on RESTful API design standards.

class Events(APIView):
    permission_classes = (AllowAny,)
    parser_classes = (parsers.JSONParser,parsers.FormParser)
    renderer_classes = (renderers.JSONRenderer, )

    def get(self, request, format=None):
        events = Event.objects.all()
        json_data = serializers.serialize('json', events)
        content = {'events': json_data}
        return HttpResponse(json_data, content_type='json')

    def post(self, request, *args, **kwargs):
        print 'REQUEST DATA'
        print str(request.data)

        eventtype = request.data.get('eventtype')
        timestamp = int(request.data.get('timestamp'))
        userid = request.data.get('userid')
        requestor = request.META['REMOTE_ADDR']

        newEvent = Event(
            eventtype=eventtype,
            timestamp=datetime.datetime.fromtimestamp(timestamp/1000, pytz.utc),
            userid=userid,
            requestor=requestor
        )

        try:
            newEvent.clean_fields()
        except ValidationError as e:
            print e
            return Response({'success':False, 'error':e}, status=status.HTTP_400_BAD_REQUEST)

        newEvent.save()
        print 'New Event Logged from: ' + requestor
        return Response({'success': True}, status=status.HTTP_200_OK)

new api/urls.py

urlpatterns = [
    ...
    url(r'^events', csrf_exempt(controllers.Events.as_view())),
    ...
]

request

Checkpoint

  1. Is the URL <myserver>/api/events a valid URL?
  2. Is the URL <myserver>/api/session a valid URL?
  3. What function gets called when the user visits <myserver>/api/register?
  4. What would be the result of making a DELETE request to <myserver>/api/events?
  5. What would be the result of making a POST request to <myserver>/api/events?

Step 6: Press the button

Ok, so you now have a loose familiarity with the skeleton backend code that was provided to you. Lets build some more upon it.

Go ahead and click login (on the left hand side menu). Authenticate with the username/password we made earlier (admin/admin1234). When you login, you should see a green button that says turn IFTTT on when visiting localhost. Time to push it!

Lets use the chrome development tools to take a closer look.

Step 7: Chrome Dev Tools - Your new best friend

Instead of me reinventing the wheel, head over to https://developers.google.com/web/tools/chrome-devtools/ to learn the basics of what Chrome Development tools can do.

When you’ve looked over the different features. Come back and click on the network tab to inspect what is happening with our button.

Step 8: Make a new REST endpoint to make the client button work with the backend

Currently, the server doesn’t know that it needs to do anything special with the URL /api/activateIFTTT so it is just rendering the home page (what we have been looking at this whole time) in response. What we need is for our server to recognize that a new event has occurred from the client and then do something to handle it, in this case, contact IFTTT.

For this to work, we need to create a new REST Endpoint controller to handle the request. Open up your controllers.py file and add a new entry called ActivateIFTTT. This entry will only expose a POST endpoint. The goal is to:

class ActivateIFTTT(APIView):
    permission_classes = (AllowAny,)
    parser_classes = (parsers.JSONParser,parsers.FormParser)
    renderer_classes = (renderers.JSONRenderer, )

    def post(self,request):
        print 'REQUEST DATA'
        print str(request.data)

        eventtype = request.data.get('eventtype')
        timestamp = int(request.data.get('timestamp'))
        requestor = request.META['REMOTE_ADDR']
        api_key = ApiKey.objects.all().first()
        event_hook = "test"

        print "Creating New event"

        newEvent = Event(
            eventtype=eventtype,
            timestamp=datetime.datetime.fromtimestamp(timestamp/1000, pytz.utc),
            userid=str(api_key.owner),
            requestor=requestor
        )

        print newEvent
        print "Sending Device Event to IFTTT hook: " + str(event_hook)

        #send the new event to IFTTT and print the result
        event_req = requests.post('https://maker.ifttt.com/trigger/'+str(event_hook)+'/with/key/'+api_key.key, data= {
            'value1' : timestamp,
            'value2':  "\""+str(eventtype)+"\"",
            'value3' : "\""+str(requestor)+"\""
        })
        print event_req.text

        #check that the event is safe to store in the databse
        try:
            newEvent.clean_fields()
        except ValidationError as e:
            print e
            return Response({'success':False, 'error':e}, status=status.HTTP_400_BAD_REQUEST)

        #log the event in the DB
        newEvent.save()
        print 'New Event Logged'
        return Response({'success': True}, status=status.HTTP_200_OK)

Now that we have the endpoint defined we need to make it available on the web server. Modify api/urls.py to include a new line in the urlpatterns, make it look like:

urlpatterns = [
    url(r'^session', csrf_exempt(controllers.Session.as_view())),
    url(r'^register', csrf_exempt(controllers.Register.as_view())),
    url(r'^events', csrf_exempt(controllers.Events.as_view())),
    url(r'^activateifttt', csrf_exempt(controllers.ActivateIFTTT.as_view())),
    url(r'^', include(router.urls)),
]

This will make the endpoint available on the webserver. Now go back to http://localhost and try to click the button. What happens?

Did you get an error? Did you restart your server? If not, press control+C on the docker terminal and then:

docker compose up

again.

Now try? Did you get a different error?

This is because we haven’t added our API Key to our server, so the field api_key = ApiKey.objects.all().first() returns null (or NoneType). To fix this, open your browser and go to http://localhost/admin/api/apikey/. Click ‘add api key’.

git status
git add -A
git status
git commit -m "added endpoints for events and IFTTT"
git push

note if you have a git authentication error - it is probably because you need to handle your credentials correctly - i recommend github desktop or the git credential manager (https://github.com/git-ecosystem/git-credential-manager)

You just pushed your local changes to remote on github!

Stray observations

Step 9: Profit!

Pretty neat. Observe your handy work.

License

Lesson content: Copyright (C) Dr. Matthew Hale 2017-2023.
Creative Commons License
This lesson is licensed by the author under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.