[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.