[MUSIC PLAYING]
BRIAN YU: All right, welcome back, everyone,
to Web Programming with Python and JavaScript.
And for our final topic, we're going to explore scalability and security.
So far in the class, we've been building web applications.
And we've been building web applications that work on our own computer.
But if we want to take those web applications
and deploy them to the world so people all across the internet
can begin to use them, then we're going to need
to host our web application on some sort of web server--
some dedicated piece of hardware that is listening for web requests
and responding to them with the response that we would like
for our web application to deliver.
And when we do so, this introduces a whole bunch
of interesting issues surrounding scalability and security.
So we'll take a look at these issues today,
beginning with problems concerning scalability-- what those problems are
and how we might go about addressing them.
So when we deploy our web applications, we
deploy them by putting them onto a web server
that I'm, here, just representing with this rectangle.
But all the server is is some dedicated computer, some piece of hardware that
is listening for incoming requests.
So we'll draw this line to represent an incoming web request from a user.
The server takes that request and responds to it.
But ultimately, our web application isn't just
going to be servicing one user.
If it becomes popular, it might have many users
that are all trying to connect to that server at the same time.
And as multiple people start to connect to that server at the same time,
here is where we start to deal with issues of scalability.
A single computer or a single server can only service so many users
at any given time.
And so, therefore, we need to think in advance
about how we're going to deal with those issues of scale.
But the first question, before we even get there,
is where these servers actually exist.
And nowadays, there are two main options for where these servers can exist.
These servers can be on the cloud or they can be on premise.
And on-premise servers, you might imagine
is if a company is running their own web application.
On-premise servers are servers that are inside of the company's walls.
The company owns the physical servers, maybe
on some server racks inside of a room.
And therefore, they have very direct control
over all of the servers-- exactly what kind of servers they are,
exactly what software is running on them.
They can go and physically look at the servers and debug them, if need be,
in order to make sure that any issues are dealt with.
But increasingly, we're starting to move into a world
where cloud computing is becoming increasingly popular.
In cloud computing, rather than have dedicated servers that are on premise,
we have servers that are somewhere in the cloud
where cloud computing companies like Amazon, or Google, or Microsoft
are able to run their own servers.
And we simply use those servers that are provided by those third parties,
whether it's Amazon, or Google, or Microsoft, or someone else.
And there are trade offs.
With cloud computing, we no longer have as direct control over the machines
themselves because they're not on premise.
We can't physically manipulate those computers.
But we have the advantage of not having to worry
about dealing with physical objects that are inside
of the premise of the company whose servers we'd like to run code for.
When it's on the cloud, everything is managed externally
by some other company, and we can simply use the servers that we need to.
And we'll see that this lends itself to other benefits as well.
As we might need more servers, as we start to get more sophisticated web
applications that need more users, these cloud-computing companies
can allow us to create web applications that
are able to scale across multiple different servers
as we start to get more and more users.
But we'll discuss those issues of scale as we get to them.
The question we need to ask after we have these servers--
whether they're servers that are on premise or servers that
are operating somewhere in the cloud--
is, how many users can the server actually service at any given time?
And that's going to vary.
It's going to vary based on the size of the server, the computing
power of the server.
And it's going to be dependent upon how long it takes to process
any particular user's request.
If user requests are quite expensive, it might
mean that there are fewer users that can be serviced at any given time.
And it's for that reason that a helpful tool
is to do some kind of benchmarking, some process of trying
to do some analysis on how many users a server can actually
be handling at any particular time.
And there are numerous different tools that allow
us to do this kind of benchmarking.
Apache Bench, or otherwise known as AB, is a popular tool
for doing this kind of thing.
But benchmarking is going to be useful so that we know how many users one
particular server can handle.
Maybe it can handle 50 users.
Maybe it can handle 100 users.
Maybe it can handle more at any given time.
But ultimately, it's going to be some finite limit.
Every computer just has some finite amount of resources,
and servers are no exception.
There's going to be some number of users after which the server is not
going to be able to handle it.
So what do we do in that situation?
What do we do if our server can only handle 100 users at any given time,
but 101 users are trying to use our web application at the same time?
Something needs to change.
We need to deal with some sort of scaling
to make sure that our web application can scale.
And there are a couple of different types of scaling that we can try.
One approach is to do what's called vertical scaling, which
might be the simplest way you could imagine scaling.
If this server is not good enough for handling the number of users
that we need it to handle, well, just get a bigger serve.
In vertical scaling, we just take the server
and get a bigger server, a more powerful server,
a server that can handle more users at any given time.
It's going to cost more.
But if we need it to handle more users, we
can just get a bigger server to be able to deal with that problem.
This approach is fairly simple.
It just involves swapping out one server for another, one that
can handle more users concurrently.
But it also has drawbacks.
There is some limit to how big the server can be, to how many users
any physical one server is going to be able to handle because there's
a physical limitation on what is the biggest, fastest, most powerful
server we could possibly get.
So when vertical scaling ends up not being enough,
an alternative-- as you might imagine-- is what's known as horizontal scaling.
And the idea behind horizontal scaling is
that, when one server isn't enough to be able to service all of the users that
might be trying to use a web application at the same time, well,
then we can take the approach of saying, well, rather than just using
one server, let's go ahead and split it up into two different servers.
We now have two servers that are both running the web application.
And now, effectively, we've been able to double the number of users
that this web application can handle.
Rather than just a single server that can service 100 users,
if we have two of them, now we can service 200 users at any given time
if you imagine 100 of them using server A over here and 100 of them
using server B over there.
But this then lends itself to some other questions that we have to answer,
which is, how do these servers get their users in the first place?
When a user requests a web page, how does that user
get directed either to server A or to server B?
It seems that they need some way to make that decision in order to decide
whether to go one direction or another.
And it's for that reason that we might introduce another piece of hardware
into this picture.
And that additional piece of hardware is what we might call a load balancer.
And a load balancer is just another piece of hardware
that is going to sit in front of these servers, so to speak.
In other words, when a user makes a request to a web page,
rather than immediately getting that request to one of these web servers,
the request is first going to go through this load balancer
where the request first comes into the load balancer.
And the load balancer then decides whether to send that request to server
A or to send that request to server B. And this process
is likely less expensive than actually dealing with and processing
that request.
So the load balancer is effectively just acting as a dispatcher.
It waits for those requests to come in.
And when the requests do come in, the load balancer
directs those requests either to go to one server or to another.
And you might imagine the story where we have more than just two servers.
Maybe we have many servers.
And the load balancer is just going to balance
between all of those different servers.
And this process of deciding which server to send a request to
is known as load balancing, which is what the load balancer is ultimately
doing.
And there are various different methods that you might use in order
to perform this load balancing.
So you might imagine thinking about this intuitively.
How would the load balancer decide, given some request,
should we send the request to this router, to this server,
or should we send the request to some other server instead?
And there are many different approaches that our load balancer might take.
And here are just a couple.
Random choice might be the simplest of options.
Given a user that shows up and tries to make a request to our web server,
the load balancer first takes a look at the user
and just randomly assigns them to one of the various different servers
that might be processing that request.
If there are 10 different servers, it randomly chooses among those 10 servers
to decide which of them is going to be servicing that request.
This has the advantage of being very simple.
It's just a quick calculation.
The computers can pretty readily generate random numbers.
And based on that random number, the computer
can dispatch the user to one server or to another server.
But it might not be the best option because, if we happen to get unlucky,
we might end up with many more users on one server than another.
Or we might end up with servers that are entirely
unused if it just so happens that we don't end up
randomly selecting that server.
Now, in practice with many users that are all using this load balancer, all
being dispatched, odds are high that eventually all of them will be used.
But it might not be a totally even distribution.
And so for that reason, another approach you
might take is round-robin approach where the approach is, instead,
for the very first user, go ahead and assign that user to server number one.
For the next user, assign them to server number two.
And maybe, if there are five servers, you say,
the third user goes to server three, user four goes to server four,
user five goes to server five, and then user six
goes back to server number one.
You basically rotate going one through five.
And then, once you've assigned someone to each of the servers,
you go back to the beginning.
This is also a relatively easy thing to implement because you can simply just
keep count somewhere in the load balancer
saying, what was the most recent server that I assigned a user to?
And the next time a request comes in, go ahead and assign it
to the next server, and the next server after that,
effectively doing a round-robin style approach
where you go through all the servers once before going through the servers
again.
Now, this might seem better than random choice in the sense
that it's going to more equitably decide whether to assign
any particular request to any particular server.
But it also suffers from certain problems.
Round robin might be great, but if some requests
take longer than other requests, we might also get unlucky,
and the requests that are taking longer might
end up all going to one of the servers as opposed to another server.
So there are other approaches that we might want to go to as well--
for example, something like fewest connections
where the approach there is to say, go ahead, and when a user makes a request,
the load balancer should pick which of the servers
currently has the fewest active connections from other users
and other requests that are currently connected to those servers instead.
And by choosing the server that happens to have the fewest connections,
you're probably going to do a better job of trying to balance out
between all of the various different requests
that might be happening inside of your web application.
And while this might do a better job, there are trade offs here as well.
It might be more expensive, for example, to compute which of the servers
happens to have the fewest number of connections,
whereas it's much easier just to say, choose a server at random
or to do the round-robin style approach of just 1, 2, 3, 4, 5, 1, 2, 3, 4, 5,
again, and again, and again.
But all of these approaches naively have yet another problem,
which has to do with sessions.
And you'll recall that sessions we used whenever
we wanted to store information about the user's current interaction
with the web application.
When you log into a website--
you log into your email, or you log into Amazon, for example--
and then you come back to that website or visit another page on that website--
make another request, for example--
it's not the case that you have to sign in yet again, that the web browser has
totally forgotten who you are.
When I go back to my mail account, or when I go back to Amazon
for a second time, my mail account or Amazon remembers me from the last time
that I visited.
I have some sort of session where it's keeping track of who is logged in,
maybe information about what I've been doing on the page,
and allows me to continue interacting with the web application,
even if I'm making multiple requests.
And this, you might imagine, could be a problem
for this type of load balancing.
If I have multiple different servers, imagine if I try to log into a website.
And the first time I make a request, I'm directed to server number one.
And I'm now logged in on server number one.
But then I make another request.
I'm directed back to the load balancer.
And maybe the load balancer, this time, decides
to send me to server number two.
But if the session is stored in server number one somewhere--
server number one remembers who I am and what I'm doing--
then server number two is not going to know who I am.
And therefore, it's not going to remember that I've already
logged into this web application.
And as a result, I might be prompted to log in again.
And if I go make another request, and I end up on yet another server,
I might be logged out again and have to log in for a third time.
So the problem comes about when our load balancing happens,
but we're not doing so in a session-aware way--
that our load balancer isn't caring about when a user visits the page
and then visits another page on the same web application again--
because we want to remember information from the previous time
that the user was here.
So how can we solve this problem?
How can we make sure that, when we do this load
balancing across multiple different servers,
that we do so in a session-aware way?
Well, there are multiple different approaches
to session-aware load balancing.
One approach is this general idea known as sticky sessions
where the idea is that, when I come back to the load balancer,
the load balancer will remember what server I was sent to last time
and send me there yet again.
So for example, if I log into a website once,
and I'm directed to server number two, for example, then
the next time I visit this web application,
even if I should be directed to server three or four according
to random choice or according to fewest connections or any of these other load
balancing methods, the load balancer should
remember that, last time I came to this site,
I got directed to server number two.
And so this time, the load balancer is going
to direct me to server number two yet again.
That way, server number two, which contains information about my session,
is going to see me again and remember who it is that I am.
And it's not going to make me log in again into the exact same website
for a second time, for example.
And so sticky sessions are one way of dealing with this problem.
But again, with all of these approaches-- and this
will be a recurring theme as we talk about scalability and security--
there are trade offs here.
A trade to the sticky sessions is that it's possible that one of these servers
is going to end up getting far more load than another if one server happens
to have a lot of users that keep coming back to the website
and keep requesting additional pages.
But other pages, other servers might have
had users that decided not to come back, for example.
And so there's a difference in utilization where some of our servers
might be more heavily utilized than other servers,
and we're not doing a very good job of balancing between them.
And so one approach is to store sessions inside of the database
rather than store information about sessions inside of the server
themselves so that, if I get directed to another server,
that other server doesn't remember who I am,
doesn't remember information about my interaction with this website.
If we instead choose to store sessions inside of a database--
and, in particular, inside of a database that all of the servers
have the ability to access--
well, then it doesn't matter which of the servers I get directed to
and which server the load balancer decides to send me to
because, regardless of which server I end up getting sent to,
the session information is in the database.
And each of the servers can connect to the database
to find out who I am, to find out whether I've logged into the site
already, and therefore is able to recognize me.
And so that might be one approach as well.
Another approach is to store sessions on the client side.
We've talked a little bit about this idea
of cookies, which can be stored where the web browser can set a cookie so
that your web browser is able to present that cookie the next time
it makes a request to the same web application.
And inside this cookie, you can store a whole bunch of information, including
information about the session.
You might, inside of a cookie, store information
about what user is currently logged in, for example,
or other session-related information.
But here, too, there are drawbacks.
If you're not careful, someone could manipulate that cookie
and maybe pretend to be something else.
And so for that reason, you might want to do
some encryption or some kind of sign in to make sure
that you can't fake a cookie and pretend to be someone that you're not.
But another concern is that, as you start
to store more and more information inside of these cookies,
these cookies keep getting sent back and forth between the server and the client
every time a request is made.
That can start to get expensive, too-- more and more information
passing back and forth between the client and between the server.
So lots of possible approaches-- no one approach
that is necessarily the right approach or the best approach
to use in any cases.
But things to be aware of-- things to think about
as we begin to deal with these issues of scale, of making
sure we have multiple servers that are available for usage in case we do
need it.
But also making sure that, when we do so, we don't break the user
experience-- we don't result in a situation where a user is logged in
but then, suddenly, isn't logged in at all.
And so horizontal scaling gives us this kind of capacity--
the ability to have multiple different servers, all of which
can be dealing with user requests and responding to those user requests
as well.
But a reasonable question asked is, how many of those servers do we need?
Now, we can use benchmarking to try to estimate this.
If we have an estimate of how many users are going to be on our website
at any given time, we can benchmark and see
how many users can be handled by a single server and extrapolate,
based on that information, to infer how many servers we
might need in our web application to be able to service
all of these different users.
But it might be the case that our web application doesn't always
have the same number of users.
Maybe, sometimes, there are going to be far more users than another time.
You might imagine, for example, that in a news organization's website--
like the web application for a newspaper--
when there's breaking news, some big story,
there's going to be a lot more people that
are all trying to access the website at the same time than at other times.
So one approach might be, consider the maximum.
What is the most number of users that ever
might be trying to use our web application at any given time?
And choose a number of servers based on that maximum so that,
no matter how high the number of users get,
we will have enough servers to be able to service all of those users.
But that's probably not a great economical choice
if, in the vast majority of cases, there will be far fewer users.
In that case, you're going to have a lot of servers that are underutilized--
where you don't need that many servers, but you're still paying
for the electricity, for keeping all of them running--
which might not be an ideal choice either.
So one solution to this--
quite popular, especially in this world of cloud computing--
is the idea of autoscaling where you can have an autoscaler
to say that, you know what, let's start with, for example, two servers.
But if there's enough traffic to the website,
if enough people are making requests to the website--
maybe it's a peak time where people are using the website--
go ahead and scale up.
Go ahead and add a third server where now our load balancer can balance
between all three of those servers.
And if even more traffic ends up coming to the website--
more users are trying to use this application all at the same time--
well, then we can go ahead and add a fourth server as well.
And we can continue to do that.
Most autoscalers will let you configure, for example,
a minimum number of servers and a maximum number of servers.
And dependent on how many users happen to be using your web
application at any given time, the autoscaler
can scale up or scale down, adding new servers as more users come
to the website, removing servers as fewer users are
using the website as well.
And so this can be a nice solution to this problem of scale
where you don't have to worry about how many servers there are.
It just autoscales entirely on its own.
Now, there are trade offs here, too.
This auto scaling process might take time.
And if a lot of users all come into your website all at the exact same time,
well, it's going to take some time to be able to add
all of these additional servers to start them up.
And so there might be some trade offs there, too,
where you might not be able to service all of the users immediately.
And another problem worth thinking about is,
as you add more and more of these servers,
you introduce opportunities for failure.
Now, it's better than having a single server
where, if that single server fails, now suddenly the entire web application
doesn't work at all.
That's what we generally call a single point of failure--
a single place where, if it fails, the entire system is going to be broken.
One advantage of having multiple servers is
that we no longer have a single server that acts as a point of failure.
If one of the servers goes down then, ideally,
our load balancer should be able to know, based on that information,
to no longer send a request to that particular server-- to,
instead, balance the load across the remaining three servers instead.
Now, there's an interesting question there as well,
which is, how does the load balancer know that this server is
no longer responding?
For some reason, it has some sort of error
that it's not able to process requests appropriately.
Well, there are multiple ways you can do this.
But one of the most common is what's simply known as a heartbeat where,
effectively, every so often, every some number of seconds,
the load balancer pings all of the servers--
just sends a quick request to all the servers.
And all of the servers are supposed to respond back.
And using that information, the load balancer
knows a little bit about the latency of each of the servers--
how long it took for the server to respond to the request.
But also, it can get information about whether or not
the server is functioning properly.
If one of the servers doesn't respond to the ping,
well, then the load balancer knows that there's probably
something wrong with the server, that we probably shouldn't be directing
more users to that server at all.
And so this can solve for the problem of a single point of failure
by allowing ourselves multiple servers where, if any one of the servers fails,
the load balancer learns about that via heartbeat
and then, based on that information, can begin to redirect traffic
to the other servers instead.
Now, one thing you might notice is that, even in this picture,
now the load balancer appears to be like a single point of failure
where, if the low balance happens to fail, well, now
nothing is going to work because the load balancer is
the one responsible for directing traffic to all
of the various different servers.
And so even though there is no single server that is a point to failure,
this load balancer also appears to be a single point of failure.
And that's definitely true.
And you might imagine instead having multiple load balancers
where one load balancer goes down, another load balancer can swoop in,
acting as a hot spare where it picks up all of the traffic that was originally
going to the first load balancer.
And if it ever goes down, a second one is ready to take its place.
And it might also be doing this kind of heartbeat process-- checking up
on the first load balancer.
And if all goes well, the second load balancer doesn't have to do anything.
But if the first load balancer ever were to fail,
well, then the second load balancer can step in
and begin servicing those requests, directing them to all
of these individual servers as well.
And so there, too--
another opportunity to think about where the single points of failure
are and thinking about how we might address the single points of failure
in order to make sure that our web applications are scalable.
So that then deals with issues about how we might
go about scaling up these servers.
But ultimately, the servers are not the entirety of the story.
Inside of our applications, we mostly have
writing web applications that interact and deal with data in some way.
And there are multiple different databases that we've talked about.
SQLite Light has been the default one that Django
provides to us, which just stores data inside of a file.
But as we begin to grow our applications,
if we want to begin to scale them, it's quite popular and quite common
to put databases entirely somewhere separate--
to have a separate database server running somewhere else where
the servers are all communicating with that database,
whether it's we're running MySQL, or Postgres, or some other database system
instead.
And all of the servers then have access to that database.
And so there, too, are considerations that we
need to take into account-- issues of how it is that we
go about scaling up these databases.
In this picture, for example, you might imagine a load balancer
that is communicating with two servers.
But both of those servers, for example, need
to be communicating with this database.
And much like any server can only handle some number of requests,
some number of users at any given time, databases, too,
can only handle some number of requests, some concurrent number of connections
at any given time.
And so we need to begin to think about issues
of how it is that we scale these databases as well in order to be
able to handle more and more users.
Now, one approach, the first thing we might try to do,
is something called database partitioning-- effectively,
splitting up what is a big data set into multiple different parts
to that data set.
And we've already seen some examples of database partitioning.
We've seen one example where-- for example, when we talked about SQL,
we looked at a table of flights where each flight had an origin
city, the origin city's airport code, the destination city, the destination
city's airport code, and some number of minutes,
the duration for that particular flight.
And we decided that storing all of this data in a single table
probably wasn't the best idea.
And instead, we wanted to split that data up
in a type of partitioning where, instead, we said, all right, let's just
have one table that will have all of the airports.
And so each airport gets its own row inside of this airports table.
And we also had another table which was just
the flights table which, rather than storing all of those columns,
just mapped two airports to each other.
With any given flight, it has an origin idea, meaning which object,
which row in the origin airports table is represented by the flight,
and then which row in the airports table is
going to represent the destination for that flight.
So we took one table and effectively split it up
into multiple tables, each of which ultimately had fewer columns.
And this might be something we call the vertical partitioning
of a database where, instead of just having single big long tables,
we split them up into multiple tables, each
of which have fewer columns that are able to represent
data in a more relational way.
And that's something we've seen before, too.
But in addition to vertical partitioning,
we can also do horizontal partitioning where the idea there
is that we take a table and just split it up
into multiple tables that are all storing effectively the same data,
but split up into different data sets.
So the same type of data, but just in different tables--
where we might have originally had a flights table,
and instead we split it up into a domestic flights table
and an international flights table.
Each of these tables still has the exact same column.
They still have a destination column.
They still have an origin column.
They still have a duration column, for example.
But we've just now taken the data that used to be in one table
and split up that data into two or more multiple different tables instead--
one for all the domestic flights, one for all the international flights.
And the advantage there is that we no longer
need to search through the entirety of the data set if we're just looking
for one domestic flight, for example.
If you know the flight you're looking for is a domestic flight,
well, then it can be more efficient to just search the flight's domestic table
and not bother searching through the flight international table.
And so if we're intelligent about how we choose to take a table
and split it up into multiple different tables, the effect of that
is that we can often improve the efficiency
of our searches, the efficiency of our operations,
because we're dealing with multiple smaller tables
where these operations can come faster.
One drawback though is that, as we begin to split data
across multiple different tables, it becomes more expensive if ever we
need to join this data back together and connect
all the domestic and international flights running
separate queries on each.
And so in that case, we'll want to think about trying
to separate our data in such a way that, generally, we're
only going to need to deal with one table or the other at any given time.
And so domestic and international might be a reasonable way
to split up our flights table because maybe, most of the time, our airport
just cares about searching domestic flights
if we know we're looking for one kind of flight,
or just cares about searching for international flights
if there are different people or different computers that
are going to handle each of those different types of systems.
And so partitioning our database can sometimes help with issues of scale
by making it faster to search through large amounts of data
and being able to represent data a little bit more cleanly.
But it still seems to represent a single point of failure--
that we have multiple servers now that are all connected to the same database.
And there, again, is a single point of failure.
If the database fails for some reason, well now,
suddenly, none of our web application is going
to work because all of those servers are all
connected to that exact same database.
And so it's for that reason that we might--
just as we tried to add more servers in order
to solve the problem of a single point of failure with our servers,
we might also try database replication.
Rather than just have a single database in our web application,
in order to guard against potential failure,
we might replicate our database-- have multiple different databases
and, therefore, reduce the likelihood that our application entirely fails.
And there are a couple of approaches that we
can use for database replication.
Two of the most common are what are known as single-primary replication
and multi-primary replication.
And in single-primary database replication,
we have multiple different databases.
But one of those databases is considered to be the primary database.
And what we mean by a primary database is a database
to which we can both read data--
meaning select rows from the table--
but also write data, meaning insert rows,
or update rows, or delete rows to any of those tables.
So in single-primary replication, we have a single database
where we can both read and write.
And we have some number of other databases-- in this case,
two other databases--
from which we can only read data.
So we can get data from those databases.
But we can't update, or insert, or delete from those databases.
And now we need some mechanism to make sure that all of these databases
are kept in sync.
And ultimately, what that means is that, any time the database changes,
all of the databases are informed.
Now, the only database that can change is our primary one.
This is the only one that can be written to,
the only one that allows for the data to change.
The others are read only.
So anytime this primary database updates or changes in some way,
it needs to inform the other databases of that update.
And so it informs the other databases of that update.
And now all of the databases are kept in sync
where, if you try and run a query on any of these databases
to select and get some information, you'll
get the same results from all of these various different databases.
Now, the single-primary approach has some drawbacks.
It has the drawback of only one of these databases can be written to.
So if you have a lot of users that are all
trying to write data to the database at the exact same time,
well, there might be some issues here where
this one database is going to be carrying
all of that load for all of the people that might be trying
to update and change that database.
And it also has a slightly smaller version
of the same problem of a single point of failure.
There is no longer a single point of failure for reading from that data.
If you want to read from the data, and one of the databases goes out,
you can read data from any of the other databases, and they'll work just fine.
But it does have the drawback that, if this database fails,
if our primary database fails, well, then
we're no longer able to write data.
If we want to update data inside of our database,
this one database is no longer going to be operational.
And none of the other databases are going to allow us to write new changes.
So there are a couple of approaches we can use to try to solve this problem.
One approach though is, instead of having a single-primary database--
a single database to which we can read and write--
to use a multi-primary approach.
And in the multi-primary approach, we have multiple databases, all of which
we can read and write to.
We can select rows from all the databases.
And we can insert an update and delete rows to all of these databases as well.
But now the synchronization process becomes a little bit trickier.
And here, now, is the trade off-- that now we've
replicated the number of reads and writes
we can do by having many databases to which we can read data and write data.
But anytime any of these databases changes,
every database needs to inform all of the other databases of those updates.
And that's, certainly, going to take some amount of time.
It introduces some complexity into our system as well.
And it also introduces the possibility for conflicts.
You might imagine situations where, if two people are editing
similar data at the same time, you might run
into a number of different types of conflicts.
So one type of conflict, for example, would be an update conflict.
If I tried to edit one row in one database,
and someone else tries to edit the same row in another database, when they sync
up with each other via this update process,
our database system needs some way to decide
how it's going to resolve those various different updates.
Another conflict might be a uniqueness conflict.
We've seen, in the case of databases in SQL
that, when we're designing our tables, I can
specify that this particular field should be a unique field--
common one being the ID field, for example, where every single row is
going to have its own unique ideas.
Well, what happens if two people try to insert data at the same time
into two different databases?
They're each given a unique ID, but it's the same idea on both of the databases,
because neither database knows that the other database has added a new row yet.
So when they sync back up, we might run into a uniqueness conflict
where two different databases have assigned the same exact ID
to multiple different entries.
So we need some way to be able to resolve those conflicts as well.
And there are many other conflicts you might
imagine trying to deal with-- one example being, for instance, delete
conflicts, where one person tries to delete a row
and another person tries to update that row.
Well, which should take precedence?
Should we update the row?
Should we delete the row?
We need some way to be able to make those decisions because there
is some latency between when a change is made to a database
and when that database is able to communicate with another database.
So these issues of scale, these issues of synchronization
are always going to come up as we start to deal
with programs that are interacting with more and more of this kind of data.
And as a result, we need to design more and more sophisticated
systems that are able to deal with those issues of scale.
Now, ultimately, we'd ideally like to reduce the number of different database
servers that we have.
Every additional database server is going to cost time.
It's going to cost resources.
It costs money in terms of keeping all of these servers running.
And so, ideally, we'd like not to have to talk to this database
if we don't need to.
So you might imagine, for example, a news organization's website, something
like the front page of the New York Times.
If you go to the home page of the New York Times website,
it displays all of the day's headlines with images
and with information about what each of the stories are about, for example.
And you might imagine that the way they're doing something like this
is that they have some kind of database that's
storing all of these news articles.
And when you visit the front page of the New York Times,
it's going to do some kind of database query--
selecting all of the recent top headlines, for example--
and rendering all of that information in an HTML page that you can see.
And that would certainly work.
But if a lot of people are all requesting the front page
at the same time, well, it probably doesn't make all that much sense
if the web application, every time, is making a database query, getting
the latest articles, and then displaying that information to all of the users
because the articles might not be changing all that frequently.
If one person makes a request one second,
and another person makes the same request half a second later,
it probably is not going to be useful to re-request all of the information
from the database, regenerate that template yet again, because it's
an expensive process of requesting data from the database, of generating
that template.
We'd, ideally, like some way of dealing with that problem.
And the way we can deal with that problem is some form of caching.
And caching refers to a whole bunch of different types of ideas and tools
that we can use at various different places inside of our system.
But in general, when we're talking about caching,
we're talking about storing a saved version of some information in a way
that we can access it more quickly so that we don't need to continue making
requests to a database, for example.
And so there are a number of ways we can do caching.
One way we can do caching is on the client side via client-side caching
where the idea is that your browser--
whether it's Safari, or Chrome, or something else--
is able to cache data, store information,
so that the browser doesn't need to re-request the same information
the next time it visits the page.
For example, if you request a page and it loads an image--
on the page, for example--
and you reload the page, well, your web browser
might try and make a request again for the exact same image
and then display it to you.
But an alternative might be that your web browser could just
save a copy of the image inside of a cache
to locally store a version of the image so that, the next time
that the user makes a request to the website, the user
doesn't need to reload that entire image.
And that might be true of entire web pages and web resources--
that if there is some page that doesn't change very often then,
if the web browser just stores a cached, a saved version of that page,
then the next time the user goes to their web browser,
tries to access that page, rather than re-request to the server and make a new
request that the server needs to respond to, if the browser has that page
cached, the browser can just display the cached--
saved-- version of the page, saving the need to talk to the server at all.
So this can certainly help to reduce the load on any given server.
If users are caching information inside of the web browser,
it makes the experience faster for the user
because they can see the information immediately rather than
need to make a request and wait for a response to come back.
And it's good for the server because the server
doesn't need to be dealing with as many requests if some of those requests
are getting cached.
And so one approach to trying to do this is
by adding this inside of the headers of an HTTP response.
When your web server responds to some requests,
the web server can include a line like this inside of the response--
something like cache-control max-age-86400--
in effect, specifying the number of seconds
that you should cache this resource for.
But if I try to access this page 10 seconds later,
well, that's less than 86,400.
So rather than reload and re-request the entire page,
we're just going to use the version of the page that happens
to be cached inside of the web browser.
And so this has several advantages, that we've talked about,
in terms of reducing the amount of time it takes to see the content of the page
because it's already saved and reducing the load on any particular server.
But it also has drawbacks.
If, for example, the resource changes within this amount of time--
maybe in 60 seconds, the page has changed--
if I try and load the page again, well, then
if it's loading the cache version of the page,
I might be seeing an outdated version of a web page.
I'm seeing an older version of the web page
because my web browser just so happens to have
that particular resource cached.
And this might be true of a web page.
It's especially true of other static resources, things like CSS files
or JavaScript files.
The CSS of a web page probably doesn't change all that often.
And so, as a result, it's pretty natural that your web browser--
rather than request the exact same CSS files again, and again, and again--
might just save a copy of those CSS files,
cache them, such that it's able to just reuse the cached version.
But if the website were to update their CSS,
you might not see the latest changes.
And you might have experienced this yourself.
If you're working on your own web applications, when you change your CSS
and refresh the page, you might not always
see those changes reflected if your web browser is caching those results.
And so, in most web browsers, you can do a hard refresh
to say, ignore whatever is in the cache, and actually go out
and make a new request and get some new data.
But ultimately, if you don't do that, you're
subject to this cache control where the web browser is going to say,
unless this number of seconds has elapsed,
we're going to reuse the existing version of the page.
And so an alternative to this approach-- and this approach certainly works
and is quite popular--
we can add to this approach by adding what's known as ETag.
An ETag for a resource-- like a CSS file, or an image,
or a JavaScript file--
is just some unique sequence of characters
that identifies a particular version of a resource,
that identifies a particular version of a CSS file or a JavaScript file,
for example.
And what this allows a program to do--
like a web browser--
is that, when a web browser requests a resource--
makes a request for a CSS file or a JavaScript file--
they get it back.
And they get its associated ETag value, so I
know that this is the value that is associated
with this version of the CSS file.
And if the web server were ever to change that CSS file, replace it
with a new updated CSS file, the corresponding ETag will also change.
So why is this helpful?
Well, it means that if I am trying to decide, should I
load a new version of the resource or not,
should I try and make another request to get the latest version of the CSS,
what I can do first is just ask for, what
is the ETag value, the short sequence that can be answered very quickly?
Very quickly, we can just respond and say,
you know what, if the ETag value is the same as what I remembered
from last time, well, then I don't need to get
a whole new version of that resource.
And so this is quite common, too, that a web browser will say,
hey, let me request this resource.
But I already have a version of the resource with this particular ETag.
So if that ETag is still the ETag for the most recent version of a particular
resource-- like a CSS or JavaScript file--
then no need for the web server to send a new version of that file.
Just go ahead and respond and say, the version you have-- that one
works-- totally fine.
But if there is a new version, well, then the web server can respond with
the new asset-- the new CSS file, for example--
but also the new ETag value.
So these two approaches can work in concert with each other.
You can say, go ahead and cache this for some number of seconds
so that, for some number of seconds, you're
not going to ever request a new version of that resource.
But even if you do ask for a new version of the resource
after this number of seconds has elapsed,
if the ETag value hasn't updated, then no
need to redownload a whole new version of a particular file.
You can just reuse the version that happens
to be cached already in the browser.
So caching in the browser can be an incredibly powerful tool
for trying to speed up these requests, for trying to reduce
the load on any particular server.
But the client side is not the only place
where we can begin to do this kind of caching.
We also have the ability to do server-side caching.
And in server-side caching, we're going to introduce to our picture the notion
of a cache--
that we have these multiple servers that are all
communicating with the database.
But these servers can also communicate with a cache--
someplace where we've stored information that we
might want to reuse later rather than have to do all of that recalculation.
And Django, in turns out, has an entire cache framework,
a whole host of features that Django offers
that allow us to leverage this ability to use the cache
to be able to speed up requests.
So there are per-view caches where you can
specify a cache on a particular view to say that, rather than run
through all this Python code every time someone makes
a request to this particular view, instead,
just cache the view so that, for the next 30 seconds or 30 minutes,
the next time someone tries to visit the same view,
go ahead and just reuse the results of the last time
that that view was loaded.
And this can work not just for a single view.
It can work for fragments inside of a template.
Your template might have multiple different parts.
On your web page, you might render the navigation bar, and the sidebar,
and the footer, maybe based on information about today
that might change the next day.
But if you expect that the side bar of your page
is not going to change very often within the same minute
or within the same hour, well, then you might imagine
caching that part of the template so that, the next time
that Django tries to load that entire template,
it doesn't need to recalculate how to generate the sidebar for your website.
It just knows that we can use the same version of the sidebar
from the last time that we loaded this website instead.
And Django also gives you access to a lower level cache API
where, for any information that you might want to cache and store for use
later, you can save that information inside of the API.
You make an expensive database query that
takes a couple of milliseconds or a couple of seconds to process.
You can save those results inside of a cache
to make it easier to access that same data if ever you
try to get access to that again.
So caching allows us to be able to deal with these issues of scale
by reducing load on our servers, but also on our databases.
Rather than need to talk to the database every single time we
make a new request for a particular web application,
we can just reuse information that happens
to be in the cache to allow our web applications to become even more
scalable.
So that then was a look at some issues concerning scalability.
And we'll next turn our attention to security--
trying to make sure that, as we build our web applications, as we deploy
our web applications and more users start to use them,
we want to make sure that they're secure.
And there are a whole bunch of security considerations
to take into account across all of the topics
that we've looked at in the course.
We've looked at a number of different topics.
And with each of them, there are security vulnerabilities.
There are ideas to be mindful of when it comes towards making sure
that our applications are secure.
And we can begin our story, in fact, by talking about Git and version control.
Git is all about trying to make sure we're
able to keep track of different versions of our code.
And one thing that goes hand-in-hand with Git
is this idea of open-source software.
On websites like GitHub and other services that host Git repositories,
increasingly, a lot of software is becoming open source
where anyone can see and contribute to the source code of an application.
And this is great in the sense that it allows for many people
to be able to collaborate and work together
in order to try to find bugs that might exist inside of a web application.
But it also comes with drawbacks-- drawbacks
where, if there is a bug in the application,
now someone who's looking through the source code of our program
might be able to spot that bug.
Or you might imagine that, because Git keeps
track of different versions of our code every time
we make a commit to our repository, you have
to be very careful when it comes towards credentials or things that
might leak inside of the source code.
You generally never want to put passwords or any secure information
inside of the Git repository because the Git repository could
be shared with other people and might be open to anyone to look at.
And so those are security considerations to be mindful there as
well-- that if you make a commit, and accidentally make a commit to your code
where you expose those credentials, you might remove those credentials
and commit again so the latest version of your program
doesn't have those credentials in it.
But someone who has access to the Git repository
has access not just to the latest version of your code,
but to every version of your code.
And that person could, theoretically, go back
through the history of the repository and find the commit
where the credentials were exposed and see those credentials as well.
So while Git is a very powerful tool, it's also one to be mindful of.
Any change you make could potentially get saved inside of a commit--
could potentially, therefore, be accessed later on.
And so if ever credentials are exposed inside of the repository,
you want to make sure to wipe out all of those previous commits
and not just make some new commit in order
to try and hide the previous credentials that can be exposed because they can
still be retrieved if someone goes back through the history
of any particular repository.
And so that, then, was a look at some issues that might surround Git.
We also talked at the beginning of the course
about HTML, and about what it is that we can use with HTML,
and how we can use this language in order to design the structure of a web
page, in order to decide where all of the paragraphs are going to be,
what tables are going to be on the page.
We talked about links and how we can use anchor tags
to link one page to another page.
Now, one concern is this type of attack known as a phishing attack with HTML.
And a phishing attack really just comes down
to a little bit of HTML that looks like this-- very easy to write,
where I have an anchor tag that is going to direct the user to URL one.
But it looks like it directs the user to URL 2.
So what might an example of this be?
All right, so we'll take a look.
I'll go ahead and open up link.html.
And in link.html, I have a website that I've written
that appears to have a link to Google.
But if I click on that link, I'm suddenly
directed to this course's website, for example.
So how did that happen?
Why did that happen?
It seems like it's linking to Google.
Well, if you look at the code, if I go ahead and open up link.html,
we'll see that here I have an anchor tag that actually links to the course
website but appears to be linking-- the text
that the user sees appears that it is linking instead to Google.
And so this is a very common attack vector, especially in emails,
for example.
You might see an email that tells you to click on a particular link.
But that link takes you to somewhere else entirely instead.
And as a result, someone might inadvertently
share their bank account credentials or other sensitive information.
And so here, too, something be mindful of as you interact with the web,
maybe not necessarily on your own website, but in other websites
that you might interact with, just to be mindful about where links are actually
taking you.
And most web browsers, if you hover over a link,
will show you where that link might actually
be directing you to because it might be different than what
the text of that particular anchor tag might appear to link you to instead.
So HTML has all these various different vulnerabilities
where, because you can just decide what you want the structure of the page
to be, it leaves open the possibility that someone
might try to trick you into thinking that you were going to a page
that you're not actually on.
And this problem is more widespread because anyone
can look at the HTML for any page.
HTML comes back from the server.
And therefore, the web browser has access to all of that HTML
and can use that HTML in order to render a page, for example.
And this leaves open other vulnerabilities, too.
For example, let me go ahead and go to bankofamerica.com, just
Bank of America's website.
You can go to any other website instead.
If I wanted to create a fake version of Bank of America's website,
for example, to trick people into thinking
they're going to Bank of America's website
when really they're going to my website, well, then what I can do
is just go ahead and view the source of this page.
I go ahead and view page source.
And here is all of the HTML for Bank of America's website.
And nothing then stops me from copying all this content,
going into an HTML file, and creating a new file that I'll just call bank.html.
And I'll go ahead and paste in the contents of that HTML file,
secure then all of Bank of America's HTML.
And now, if I open up bank.html--
that HTML file that I have now written, but really
just copied from Bank of America--
I open it up.
And now here, on my page, is a web page that
appears to look like Bank of America.
It's using all of Bank of America's HTML.
But instead, it is my HTML page and not, actually, Bank of America.
And so you might imagine combining these to create an even more concerning
attack vector where, instead of linking to google.com,
let me try and link to bankofamerica.com.
But where I'm actually going to link to is bank.html, my version
of Bank of America's website.
Now, if I open up link.html, here appears
to be a link that links me to Bank of America.
If I click on that link, I get to a page that
looks like Bank of America's website.
But it's not Bank of America's website.
It's my bank.html file that I have written.
It just so happens to look like Bank of America's website
because I copied all of that underlying HTML.
So HTML has the ability to describe the structure of our web page.
But anytime you're writing this HTML, it's good to be mindful of the fact
that anyone can copy your HTML, could theoretically pretend to be you.
These are security vulnerabilities that are
worth bearing in mind as we start to develop web applications
and interacting with web applications as well.
So ultimately, we used HTML in the context of designing web applications
using Django, a framework.
And how exactly, then, did these web frameworks
work in terms of creating these web servers that are listening for requests
and that are responding to those requests?
Well, ultimately, much of the internet is
based around this idea of a client communicating with a server or, more
generally, any one computer communicating
with another computer using HTTP and, in particular,
HTTPS, a more secure version of the HTTP protocol.
And so you imagine that what these protocols are really about
is how information gets from one person to another
and what we're storing with that information.
We have one computer trying to communicate with some other computer.
And in order to do so, information is generally
going to flow through these routers.
You might imagine information going back and forth
between one computer and another computer,
going through these intermediate routers along the way.
And as a result, one thing to be cautious about
is, how do you know that this information that's
getting passed back and forth is getting passed back and forth securely?
Ideally, when I send a message to another computer--
I'm sending an email to someone else, I'm
sending a message, I'm making a request to a website that
might contain sensitive information, like my bank account, for example--
I don't want it so that any intercepting router that is taking my request
and passing it along--
I don't want those routers to be able to look at that request
and see the contents of my email or the contents of what password
I happen to be sending across the web or not.
Ideally, I'd like for this information to be encrypted.
And so here, we'll talk a little bit about cryptography--
this process of trying to make sure that I
am able to communicate with some other person
without some eavesdropper in the middle being able to intercept that message.
Obviously, if I just take a plain text version
of the message I'm trying to send and just literally take
the text of the message I'm trying to send and effectively pass it along
across the internet, well, then anyone who
is able to see that message is going to know what the text of that message is.
And so I want to do some kind of encryption,
some way of encrypting that message so that someone along the way
won't be able to do that decryption if a router in the middle
or someone in the middle is able to intercept that message.
And so the first approach we'll look at is what's
known as secret-key cryptography.
In secret-key cryptography, I have not just the plaintext, but some key,
some secret piece of information that can be used in order to encrypt
or decrypt information.
And so I'll use both the key and the plaintext
to generate what's known as the ciphertext, the encrypted version
of the message I'm trying to send.
And then, instead of sending the plaintext
across the internet to the other person, I
might instead want to just send the ciphertext across the internet
to the other person so that I'm not sending the plain version
of the message across the internet.
So the ciphertext goes across.
And the other person will also need the key.
Now, if the other person has both the ciphertext and the key,
well, then using that information, the other person
can use the key to decrypt the ciphertext
and obtain the original plaintext.
And this key is what we might call a symmetric key encryption and decryption
key.
You use the key in order to encrypt messages.
And you use the same key in order to do the decryption process.
And as long as both I and the person I'm communicating with both have access
to that key, well, then we'll be able to encrypt messages and decrypt messages.
And someone who just has the ciphertext but not the key
likely won't be able to figure out what that original message was.
But there's a problem here, especially in the context of the internet.
And that is that both I and the other person need to have access to this key.
The key is what I use to do the encryption and the decryption.
And I can't just send the key across the internet to the other person
because, if I do that, well, then someone in the middle who's
intercepting all of my requests could intercept
both the ciphertext and the key.
And therefore, they would be able to decrypt the message because they
have both the ciphertext and the key.
Now, if I were able to go to another person in person and exchange
this secret key in secret, well, then this scheme
might work, because we both have the key.
And I didn't share the key publicly with anyone who might intercept the message.
Only I and the other person had the key.
But in general, when communicating on the internet,
you're not communicating with servers you've
necessarily communicated with before.
I might be trying to make a request to a new website.
And we somehow still need to agree on a system where I can encrypt messages
but only the other person on the other side
is able to decrypt those messages instead.
So this kind of cryptography-- probably not great
for trying to initially try and create a secure connection on the internet.
And for that reason, a major advancement in cryptography
that allows for the internet to work is this notion of public-key cryptography.
In secret-key cryptography, it's important
that the key is secret because, if the key were known by everyone, well,
then anyone would be able to decrypt messages.
In public-key cryptography, we're able to create a secure encryption
system where the key is allowed to be public,
or one of the keys, as we'll soon see.
And the idea here is that we're using two keys instead of just one--
that we have both a public key and what's known as a private key.
The private key-- your private key is something
you should not share with other people to keep the encryption scheme secure.
But the public key is one that is OK to share with other people.
And the distinction between the two is that the public key will be
used in order to encrypt information.
And the private key will be used to decrypt information
that was encrypted by the public.
And the public key and the private key are mathematically related.
And there are a couple of ways that we might imagine doing that.
But the idea now is that, if I want to communicate with another person,
that person sends me their public key.
And it's OK for the public key to travel across the internet.
Anyone is allowed to see the public key because the public key is only
used for encrypting that data.
So I can then take the plaintext and the public key
and use that to generate the ciphertext, the encrypted version of the message
that I am trying to send across the internet.
And then I send the ciphertext to the other person
with whom I'm trying to communicate.
And the other person now, using the ciphertext, then uses the private key--
the private key that they did not share, and the private key
that has the ability to decrypt information that
was encrypted using the public key.
So using a combination of the ciphertext and the private key,
the person I'm communicating with can decrypt that information
and get back whatever the original plaintext of that information
happened to be.
And so this, then, is how we can do a lot
of this communication on the internet.
By using this public-private key pair, we
can say, use the public key to do the encrypting,
use the private key to do the decrypting.
And now two computers that have never interacted with each other
before, without having the opportunity to meet,
to exchange some secret information, can use a technique like this
in order to securely communicate with each other--
to send a message back and forth without anyone in the middle
being able to intercept the message and identify what the message is about.
And once you have this ability, the ability to communicate with another
secretly, well, then you can imagine agreeing on some secret key
and then using secret-key encryption to be able to encrypt and decrypt messages
as well.
And so that's an approach that you can also
take when trying to communicate with other people across the internet.
But this idea of encryption is what allows for HTTPS,
the secure version of the HTTP protocol, to actually work to make sure that--
when you are communicating with your bank's website, for example--
that someone along the way won't be able to intercept that information
and identify what it is that you're communicating about
and, instead, only has the encrypted version
of the information and a public key with which they can encrypt information,
but not a private key that can ultimately
be used in order to decrypt information as well.
And so that then is how we might allow for this kind of secure communication
on the internet and allow our web applications to be secure.
But in addition to our web applications just listening for requests
and then providing some sort of response,
our web applications were also dealing with data.
We introduced the idea of SQL data tables
where we had tables of data with rows and columns
that are representing information.
And we've also created web applications in this course where
we've had applications that have users.
Users sign in with a user name and a password, for example.
And so how might we represent that information
about users and their passwords?
Well, one way would be just stored inside of a table like this.
Here's a table of users.
Every user has an ID.
They have a user name, and they have a password.
But this turns out to be an incredibly insecure way
to store passwords-- to be storing passwords
in what might be called plaintext, just to literally store
the passwords inside of a database.
And we should never do this in practice because of the security vulnerabilities
associated with it.
If ever someone were to, unauthorized, get access to this database,
they would be able to see all of the passwords for all of the users.
So if this database ever leaked for whatever reason, suddenly
all of these passwords are now known.
And this kind of thing does happen.
If companies are not careful about how they represent user names
and passwords inside of their databases, and if ever there's
some sort of database leak, suddenly a whole bunch of passwords
could potentially be compromised.
And it's for that reason that the recommended approach,
rather than store an actual password, is to store
a hashed version of the same password using a hash function where
a hash function, in this context, is some function that
takes a password of input and outputs some hash--
some sequence of characters and numbers, in this case--
that represents that particular password,
a hashed version of the password.
But the important thing about this hash function
is that it's a one-way hash function.
From the password, you can get to the sequence of letters and numbers.
But it is very, very difficult to go the other way around
to use this information to figure out what the original password actually
was.
And so what this means is that the companies won't actually
know what any particular user's password is when a user tries to log in.
What we'll do is take their password that they're trying to log in with.
We'll hash it and compare that hash against the hash
that we've stored in the database.
If the hashes match up, that means the user probably typed in their password
correctly and, therefore, we can sign the user in.
And otherwise, that's a sign that the user did not
type their password in correctly.
So this, then, is the reason why companies--
if they're obeying these best practices-- usually
can't tell you what your password actually
is if you forget your password.
If you forget your password, the company will let you reset your password.
They can update the data inside of the table.
But the company won't be able to tell you
what your password actually is because the company doesn't know your password.
The company only knows some hashed version
of the password, some result of passing that password through a hash function.
And as a result, they're able to know whether you
logged in successfully or not with the correct credentials
without actually knowing what your password actually is.
And so this is another area where you might
imagine that, if you're not careful about how you're storing this data,
it could be a security vulnerability inside of your program
where, if ever that data is leaked, passwords suddenly become known.
And there are other more subtle ways that web applications could potentially
leak information that you, as the web developer,
need to decide if you're OK with or not.
Imagine a website, for example, where you do have a place where you can say,
if you forgot your password, you can be sent
to a place where you can reset your password, for example.
You might imagine that, if you type in your email address, click Reset
Password, you might get a message like, all right, password reset email
has been sent.
But you might imagine typing in an email address and getting something like,
error, there is no user with that email address.
And here, again, is a potential security vulnerability
in terms of leaked information.
This page that just seems to send you an email if you forgot your password is
now leaking information about which users happened to have accounts
on your website and which users do not because all someone needs to do
is type in an email address and find out whether it results in an error or not
in order to know whether a user happens to have an account on the website
or not.
And maybe that's not a big deal if that's not
something you care about securing.
But if it's a website where you do care about making sure
that, if someone has an account or doesn't have an account,
that information is kept private and secure only to the user,
unless they want to share it, well, then this type
of page, this type of interface with the database
could potentially be leaking that kind of information.
And information can be leaked in all sorts of different ways.
You can even leak information just based on the time
it takes for the database to be able to respond to a particular request.
You might imagine, if you make a request about a user,
and it takes longer to respond, that might tell you
something about the number of database queries it needs to run
or the amount of information that's stored about that user as opposed
to if a request takes less time.
So even something like how many milliseconds it takes for a web server
to respond to a request can reveal or leak information
about the data that is stored inside of the database.
And there have been examples of researchers who actually try and see
what information they can get just from looking at these kinds of information.
It doesn't seem like it would leak information,
but it might actually reveal information as well.
Now, another concern when dealing with SQL and databases we've talked about
is the context of SQL injection--
this threat where, if you're not careful about how
it is that you run your SQL code, you could inadvertently
end up executing code that you don't mean to be executing.
Situations like here-- we're in a username and password field.
We've seen this example before--
where, if a user tries to log in, you might imagine a query
like this is run selecting from the user's table
where user name equals whatever was typed in as the user name and password
equals whatever was typed in as the password.
And we saw how, for a normal user-- someone who types in, Harry and 1, 2,
3, 4, 5 as their username and password--
that this type of query works just fine.
But if a hacker tries to log into a website
and maybe includes a double quotation mark and two hyphens,
for example, where two hyphens mean a comment in SQL,
and we were to literally substitute these values into our SQL queries,
well, then you might end up substituting hacker hyphen hyphen hyphen
hyphen creating a comment that ignores the rest of this query,
effectively ignoring any kind of password checking that we might
want our web application to be doing.
So this, too-- another vulnerability that
comes about whenever we're dealing with executing
SQL code inside of a database.
And in order to deal with this, we want to make sure
that we're escaping any of these potentially dangerous characters that
might show up inside of our SQL queries.
And Django's models do this for us.
When we do these kinds of queries using Django saying, .objects, .filter,
to be able to filter out for only certain versions of a particular model,
it is going to take care of the process of making sure that it's not subject
to these kinds of SQL injection attacks.
But if ever you're writing a web application that is directly
executing secret code, which you might imagine doing,
you do want to be careful about making sure
that you're not exposing the application to be
vulnerable to these kinds of threats as well.
So that then are potential threats that come
about when we're just talking about what's happening on the server.
But we also can think about what might happen
when we're interacting with other servers--
when we're interacting with APIs, for example.
So we talked about JavaScript and using JavaScript
to be able to make additional requests to APIs or to other services that
are able to return back with certain types of information.
And with APIs, there are a number of techniques
that we can use in APIs to allow them to be more scalable, to allow
them to be more secure.
One is this notion of rate limiting where
we might want to make sure that no user is
able to make more than a certain number of requests to an API
in any particular amount of time.
This is in response to a security threat that
has to do with the scalability of a system, which
is known as a DOS or Denial of Service Attack where,
effectively, if you just make a whole bunch of requests to a single server
over, and over, and over again, you could potentially shut down that system
because you're making so many requests that it's not
able to handle that many requests all at the same time.
And for that reason, because it's so easy to make an API request--
you can do so using just a single line of Python or JavaScript, for example--
APIs will often institute some kind of rate
limiting to limit the number of requests you can make so that you're not
going to overwhelm the server or overwhelm the database that
needs to be queried in order to respond to those requests.
And so this kind of limiting might work as well.
APIs might also want to add some kind of route authentication.
You might not want everybody to access the same data via an API.
Maybe there's some sort of permission model
where only certain users are able to access certain pieces of data
from the API.
So you might imagine that a user needs to have an API key, for example--
effectively, a password that they need to pass around anytime
they're making an API request to your API
and that allows you to then be able to look at that key
and verify that they are who they say they are.
Now, with those API keys comes other potential security vulnerabilities
to be mindful of.
One is that, just as you should never be putting passwords inside of your source
code-- inside of your Git repository, for example--
you likewise generally shouldn't be putting your API keys
inside of your web applications as well, inside of the source code of those web
applications, because then anyone who has access
to the source code for the web application
can see what your API key is, could then use
the API key to pretend to be you and, therefore, get access
to potential API routes that they should not be able to access.
One common solution to this is to use what are known as environment variables
where, effectively, you in your program say that your API key is not
going to be some predetermined string that is in the text of your program
but instead is going to be drawn from the environment in which the program is
being run.
And then, on the server, when you're running the web application,
you'll first make sure the server has all of those environment variables set
correctly so that, rather than have the API key actually in the source
code of the program, the API key is simply in the environment on the server
where the web application is running.
And the server can just draw that information from the environment
so that it knows what the API key should be without the API key
actually having to be inside of the web application source code itself.
And so as we begin to deal with APIs, you
might notice that many APIs will require you to have an API key.
And often, it's for these sorts of reasons--
to make sure that we're able to authenticate users effectively
and also to make sure that we're able to limit users to make sure that they're
not making too many requests to the server
or to the database at any particular time.
But this, then, starts to get us into other potential vulnerabilities--
in particular, vulnerabilities concerning JavaScript.
JavaScript, again, is a programming language
that we use in order to write code that runs inside of our web browser--
a browser like Chrome, or Safari, or something like that.
And as a result, JavaScript has a lot of power to manipulate things on the page.
It can simulate the clicking of buttons.
It can change the content of what happens to be on any particular page.
And as a result, there are many, many vulnerabilities
that come about when it comes to thinking about JavaScript.
And one such vulnerability is this notion of cross-site scripting--
that, in general, when on your web application,
you only want JavaScript to run if you, yourself have written it.
Cross-site scripting is a potential threat
where someone else might be able to get JavaScript code to run on your website
when it's JavaScript code that someone else wrote instead of you, yourself.
And this is a potential vulnerability because, if someone else can
write the JavaScript code, they can manipulate the contents of what
happens to be on your website.
They can potentially manipulate the user experience
to get a result that is not, actually, desired.
So let's go ahead and take a look at one example of cross-site scripting.
All right, so I've prepared a web application in advance--
it's called security-- inside of which is a single Django app called XXS,
for Cross-Site Scripting.
And inside of here, we'll first take a look at the URLs.
So there's a single URL that just allows us to provide any path.
And then it's going to load the Index view.
And on the Index view, we're going to display in HTTP response.
It says, here was the path that just happened to be requested.
So you might imagine this is a simplified version of what
you might see on other websites, for example, where websites might show you
on any particular page what path you're on in order to get to that page,
some indication of where you are inside of this web application.
So I'd go ahead and see the security and run the server--
Python manage.py, run server.
So I am now running the server.
And now I'll go ahead and go into my web application, /hello, for example.
And so what I see here is the requested path hello,
which is what I would expect it to be.
I can change it to something else, like hi.
So here's requested path hi.
Here's hi/2, for example.
Whatever page I visit, it gives me a page
that says, requested path, and then whatever
path I happened to be visiting.
But watch what happens if I try and visit this URL instead.
I'm going to visit URL /script alert hi, and then end script.
So I run it.
And suddenly, an alert shows up on my page that says, hi.
And I press OK.
And it says, all right, requested path.
That alert was a JavaScript alert.
It was JavaScript code running on my web application.
But it was not code that was JavaScript code inside of my web application.
It was someone else who wrote based on the URL
to run particular JavaScript on my particular page.
And so someone linked to my web application
and passed in this script tag as part of the URL.
Someone who clicked on that link might have been taken to my web application
but ultimately had JavaScript run that was created by someone else.
And that, ultimately, is potentially dangerous.
It leaves open the possibility that someone else
could run JavaScript code on my page.
And it might not just be something like a script.
You might imagine someone not just displaying an alert,
but modifying something inside of the DOM-- changing the contents of the web
page, making API requests, doing other types of tasks
that you can do using JavaScript inside of a web browser
that, ultimately, leave my page open to potential security vulnerabilities.
And so these are cases where it's important to be mindful of when you're
designing these pages, if ever there is a possibility that someone could inject
their own JavaScript into your page somehow,
you'll want to either detect that or escape it in some way.
Or take other precautions to make sure that this kind of cross-site scripting
isn't going to be possible.
You might imagine that, in a messaging application-- for example,
if you're messaging back and forth--
you don't want it to be the case that, if you message someone
else some JavaScript code that, when they receive it,
that code actually ends up running as some JavaScript that
runs on that particular page.
You want to be sure to escape that information so
that they just see the text of the JavaScript code
but that the code isn't actually executed.
And this is a similar threat to that threat of SQL injection.
It all comes back to the idea of not wanting
to allow someone else to be able to inject
their own code into your program.
You don't want someone else to be able to inject SQL code into the queries you
run on your database.
And you don't want someone to be able to inject JavaScript code into your web
page because that leaves open potential security vulnerabilities as well.
One type of security vulnerability that Django
is quite good at defending against is one that we've seen before,
but we'll explore in more detail how it might work.
And it's this idea of cross-site request forgery where
you fake a request to a website when you didn't intend to actually make
a request to that website.
So you might imagine that, if your bank, for example,
had a URL that allowed you to transfer money
from one person to another person-- we've
talked about this idea a little bit.
But imagine now how you could implement this if it really was just a URL.
You could go to /transfer and say, as get parameters,
who am I transferring money to?
And what is the amount that I'm transferring?
Then someone else on some other website could, in the body of their page,
just have a link where that link says, click here.
And it links to your bank.com, or whatever
your bank is, transferring money to me in this amount.
And if some user unknowingly just clicked on that link not knowing
where it would take them, this website might
be able to forge a request to the bank-- make
it seem like the user had gone to the bank
and tried to initiate some kind of transfer
and, ultimately, tried to transfer money.
And it doesn't even necessarily need to be in a link.
How else might you get some new request to happen inside of the web browser?
You might imagine-- though it might seem a bit strange--
to put this inside of an image.
Image source, the source of the image, is this particular URL--
the bank's transfer page.
Now, that doesn't really make any sense.
The transfer page is not an image.
But it doesn't matter.
All an image tag is going to do is try to make a request to this source URL
to get that image and then try to display it in the user's web browser.
But the first part is what's important-- the fact that this source ends up
being requested by the web browser.
Without the user having to click on or do anything,
they might try and request from your bank.com/transfer this particular
request, which might initiate some sort of bank transfer without the user even
realizing it.
And it's for that reason that we generally suggest that,
anytime you're creating a website that is going to allow for the manipulation
of some kind of state-- that allows for some change to happen,
something like transferring money--
you don't want that to be a Git request, something that you could just
load in an image or load by clicking on a link that takes you to another page.
You don't want that to happen because then it
makes it very easy for someone else to fake a request to your page
by just creating an image or linking to, somehow, a website,
transferring funds from one user to another.
So a solution to this-- and we've talked about it--
is that, generally, we only want post requests
to be able to manipulate something inside of the database,
to be able to actually initiate a transfer from one user to another user.
But even then, this is not perfectly secure.
You could still be tricked into submitting a post request.
Imagine an adversarial website that had a form like this--
a form whose action was your bank.com/transfer and whose method was
post.
And now here-- two input fields whose type is hidden, meaning you
won't actually be able to see those input fields when
the user is looking at the page.
They'd only know about it if they inspected the source
code of this particular HTML page.
Here, there's a hidden input whose name is to,
meaning the person I'd like to transfer money to.
Here is the amount, the value that I would like to transfer.
And all the user is going to see is a button that says, click here.
They're not going to see either of the input fields, because they're hidden.
But if they do click the Click Here button, well, then
suddenly they're going to be submitting a post request to the bank
and initiating some transfer when they didn't intend to.
Now, maybe this seems like, oh, it's not a big deal, because the user still
needs to click a button.
And the user shouldn't be clicking on a button
if they don't know what the button is going to do.
Well, for one, it's probably reasonable to imagine that an adversary might
embed this button inside of a page where it looks totally
safe to be able to click on a button.
But moreover, the user doesn't even need to click on it in order
to submit the form.
We can just add a little bit of JavaScript.
You might imagine that an adversary could do something like this.
Add an unknown attribute to the body that says,
when the body of the page is done loading, go to document.form--
meaning all of the forms for this web page.
Get the first one, and submit it.
Submit the form.
And what that's going to do is, even without the user doing anything--
even without the user clicking on the Click Here button--
as soon as this page is loaded, this form is going to submit,
submitting a post request to the bank, and attempting to transfer funds
from one user to another user.
And so this is what we might call a cross-site request
forgery where some adversarial website has forged a request to our website.
And ideally, we wouldn't like for that to be able to happen.
So how do we guard against this?
Well, what Django allows us to do and a very common approach is to add a CSRF
token-- a Cross-Site Request Forgery token--
that is going to be regenerated for every session
such that, only if that token is present,
will the transfer be able to go through.
So on our website, we can include the CSRF token inside of this HTML form
and, as a result, make sure that we're able to transfer money only
when the CSRF token is present.
But if some other website tries to forge a request,
they won't know what the CSRF token should be
because it changes for every session.
And therefore, they won't be able to actually forge a request from one user
to another.
So all across the various different tools and technologies
we've been using--
Python, HTTP, Django, HTML in terms of creating these web
applications using JavaScript, and the APIs
that we might be interacting with--
there are security considerations all throughout.
We've only touched on a couple of them here.
But it just goes to show how it's important to be mindful as you think
about the practice of web programming, thinking about what you're going to add
to your web applications and what features your web application supports,
to think about what the potential vulnerabilities there are as well--
how someone might exploit your web application in order to do something
with it that they probably shouldn't.
And as you take your web applications from applications
that are just running on your own local computer
to applications that are running in some web server
that many people are starting to use, these
are the types of questions to start to be asking.
How can you make sure that your web application is scalable?
How can you make sure that your web application is secure?
So now that we've explored that-- a lot of web programming-- what comes next?
In this course, we've explored a number of different tools,
and technologies, and languages.
But there are many other web frameworks and ways
you can build web applications as well.
We spent most of our time looking at the Django web
framework, written in Python.
But you can use other programming languages
to build web applications as well.
Express.js, for example, is a very popular JavaScript framework
for building web applications.
Ruby on Rails is a popular server-side web framework built using Ruby.
And there are many others as well.
And there are also client-side frameworks
used primarily with JavaScript to be able to build user interfaces.
We've seen a little bit of React to both dynamic and interactive user
interfaces.
Other popular client-side frameworks include Angular JS, and Vue.js,
and a number of others as well.
And then, once you've built these web applications--
using any of these server-side frameworks and client-side frameworks--
then you might imagine wanting to take these applications
and deploy them to the web.
And to do that, there are a number of ways we can do this
as well-- a number of different services including Amazon Web Services, AWS,
Google Cloud, and Microsoft Azure that can be used in order to deploy
these web applications.
Roku is a service that uses AWS and tries
to simplify the process of making it easier to deploy your web applications.
And if you're web application is really just static-- it's just HTML,
and CSS, and JavaScript-- well, then you can use something like GitHub Pages
to be able to host a web application for free on GitHub's own servers instead.
And there are many other ways you can imagine deploying web applications as
well-- different services that you can use in order
to take the web applications that you have been building or web
applications you might build in the future
and make them available on the internet for others to be able to use as well.
So as we look back on the various topics within web programming we've explored,
we've seen a lot of tools and technologies
we can use that we can leverage in order to build interesting web applications.
We started by taking a closer look HTML and CSS,
diving into how we can use that to describe the structure of our page,
and then taking advantage of tools like SAS
that allow us to generate CSS that allows for much more
complex styling for our website that would have been much more
difficult to do with just CSS alone.
As we started to build larger web applications, we took a look at Git--
version control tools that we can use in order
to make sure that we keep track of versions and changes we
make to our code, allowing multiple people to collaborate on a project
simultaneously.
We then took a look at Python, looking at various different features
that the language offered-- functions, and conditions, and loops,
as we've seen in many other programming languages.
But also object-oriented programming-- the ability
to represent objects, and methods, and functions
that operate on those particular objects, which
prove especially powerful in the context of dealing with data inside of our web
applications.
Django was the example of a web framework written in Python
that we used to very quickly be able to start up
a web application, that's able to listen for requests, and make responses.
Django has a whole lot of features built in that
really make it easy to get started with building a web application.
And in particular, it makes it easy for writing
web applications that deal with data.
So Django allows us the ability to build models
that interact with SQL without us having to actually write any SQL code.
Django can generate the SQL for us just using these models and migrations that
allow us to continually apply changes that we make to our database.
As we add new tables, add and modify existing fields on those tables,
Django can take care of all of that.
After that, as you'll recall, we took our attention
towards the second of the main programming languages
in the course, JavaScript, which has a lot of uses and is very, very popular.
But we primarily use it on the client side
to be able to build interesting user interfaces-- using JavaScript
to manipulate the DOM, the structure of the page,
to change what it is the user sees.
And also to add event handling-- so that when
the user clicks on a button, when the user hovers over something, when
the user interacts with the page in some sort of way,
our code is able to respond to it.
And we saw React, a client-side framework that uses JavaScript in order
to allow us to create really interesting and interactive user interfaces
with not all that much code at all.
And then, finally, in these last couple of lectures,
we've been looking at some best practices-- how we can design tests,
tests the test the server, but also the client
to make sure that our code is working appropriately,
and also some industry practices like continuous integration
and continuous delivery that just help to make sure
that, as we make changes to our code, we're able to deploy and deliver them
rapidly and effectively and make sure that we're
able to make incremental changes to our code base
rather than need to wait on longer release cycles.
And then finally, today, we've been talking
about issues about scalability and security, especially important
as we begin to take our application and move them to the web.
We want to make sure that these applications are scalable,
that they're able to handle multiple different users,
and also to make sure that they're secure--
that we're not exposing ourselves to potential vulnerabilities like someone
who might inject SQL or inject JavaScript code into our pages
or who might try to access some data that they're not supposed to access.
We want to make sure that, when we go about designing these web applications,
we're able to do so in a scalable and, ultimately, in a secure way.
So hopefully, you enjoyed this exploration
into the world of web programming with Python and JavaScript.
Best of luck with the web programs that you,
yourself might build with the tools we've seen here today,
and also other tools that are inspired by our use similar tools
and techniques and ideas as the things that we've ultimately
talked about here.
A big thanks to the course's teaching staff and the production team
for making this entire class possible.
I look forward to seeing the web applications
that you might go on to create.
This was Web Programming with Python and JavaScript.