This repo contains a digitized version of the course content for CYBR8470 Secure Web App Development at the University of Nebraska at Omaha.
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).
By the end of this tutorial, you will be able to:
Django
server into a container
REST endpoint
on the application server
For this lesson, you will need:
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
).
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.
fork
the repo for this lab by visiting the https://github.com/MLHale/cybr8470-web-service-lab and clicking ‘fork’. This will copy the code from our repository into your GitHub account - so you can track your changes as you go.git
to clone the skeleton code repository and get it in onto our local machine.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
.
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
cybr8470-web-service-lab
folderAlthough 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:
The docker command executes the container using the docker-compose.yml
file located in your /cybr8470-web-service-lab/
folder.
port 80
on the host
to port 8000
in the container.postgres
database server.Dockerfile
in your /cybr8470-web-service-lab/
folder to learn more about what happens behind the scenes.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.
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.
backend
in the file tree to explore the actual files our server is usingapi
and django_backend
to expand out the folders and see what we have.Model View Controller
framework called Django
.
Models
(in models.py
) are abstraction
mechanisms that help you represent your data without worrying about the nitty gritty of database technologies.Controllers
(in controllers.py
) and Views
are modularization mechanisms that separate user-facing code (like user interfaces) from backend code that handles number crunching and getting data to the views.models.py
, then urls.py
, then controllers.py
models.py
you will see that we have defined two models
: Event
and ApiKey
. Both of these are schema
that have fields in them for each of the types of data they hold.ApiKey
model we have fields for the owner
and the actual key
. These will hold our IFTTT key that we will use later to communicate with IFTTT via webhooks
.Admin
class that lists out the fields. This is used by Django’s admin interface to display the data for management.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')
Event
model we have fields for:
eventtype
which describes what occurred,timestamp
(when it happened)userid
which is the id of the userrequestor
which logs the IP of the client that sent the messageclass 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')
__str__
function which outputs a string if the model is converted to a string.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.
url patterns
that are acceptable for Django
to server up to any would-be requestorscontrollers.py
file. Basically, when someone attempts to visit a URL, Django goes through its list of acceptable patterns. If it matches a pattern it executes the corresponding code in that method. If it doesn’t match any acceptable pattern, it gives the user an HTTP 404
error (not found).api/urls.py
is a sub set of patterns that are mapped behind /api/
as given in the file django_backend/urls.py
.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),
]
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.
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.
class
that extends the Django REST class APIView
to implement our endpoint.APIView
allows you to define functions that handle GET
(single), GET
(list), POST
, PUT
, and DELETE
requests that might arrive at /api/events
GET
(single) request is used whenever a user wants to get a single item (typically by id), something like /api/events/4
would return the event with id 4.GET
(list) request is used whenever a user wants to get all of the events.POST
request is used whenever a user wants to make a new event.PUT
request is used whenever a user wants to modify an existing event.DELETE
request is used whenever a user wants to delete an existing event.These conventions are not specific to
Django
they are based onRESTful API
design standards.
In our APIView
we need to create two REST endpoints
for handling POST
requests and GET
(list) requests.
The post
function looks at the incoming request, extracts the data fields from it, and then creates and stores a new Event
record based on the incoming request data. IF we were working with a real app, another sensor or service could call this endpoint and store data about an event that occured.
The get
function simply queries the database for all Event
objects and returns them to the requestor in JSON
format
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)
Django REST Framework
each of these items is defined. They are unimportant, at the moment.GET
method that we need to support our client.
Query
using Django’s Database management system (DBMS) to get all of the events..e.g. Event.objects.all()
events
(which is what our client was expecting).GET
request handler, we also have a POST
handler.
BAD_REQUEST
error response.api/urls.py
and we can see that we already have a url for /events:new api/urls.py
urlpatterns = [
...
url(r'^events', csrf_exempt(controllers.Events.as_view())),
...
]
api/events
POSTMAN
docker compose up
<myserver>/api/events
a valid URL?<myserver>/api/session
a valid URL?<myserver>/api/register
?DELETE
request to <myserver>/api/events
?POST
request to <myserver>/api/events
?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.
Inspect
chrome development tools
which have a number of very helpful capabilities for debugging websites and inspecting the behind-the-scenes action that happens when you interact with web contentInstead 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.
activateIFTTT
you will see the exact request that is getting sent.method not allowed
like we saw before. This is because we haven’t actually defined or enabled the activateIFTTT endpoint on the server-side yet. We will do that next.response
tab you will see the raw response that the server is returning when this button is clicked.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:
test
and log the resulting event locallyclass 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’.
admin
) in the owner
field.key
field add in your IFTTT
API key.
https://maker.ifttt.com/use/
test
to receive our events.
IF
condition select webhook
, then select receive a web requestthen
condition you can have IFTTT send you an email or any number of other events. Pick one. When done, save the applet.Now go back to your app at localhost and click the green button. What happened?
Since you made some changes to your code repository, lets track the changes with git
:
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
!
modules
that exemplifies the modularization
Cybersecurity First Principle. They don’t rely on the other modules (endpoints).API key
in the code to protect it from static lookup - this is an example of the information hiding
Cybersecurity First Principle.abstraction
and resource encapsulation
.web service
!Pretty neat. Observe your handy work.
Lesson content: Copyright (C) Dr. Matthew Hale 2017-2023.
This lesson is licensed by the author under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.