The site can be accessed at [url]:[portnum]/clicker_game/index.html
, which will retrieve the html necessary for loading the other files (CSS and JS). If you're running it locally in the CS130 development environment, you may need to download the following libraries:
sudo apt-get install sqlite3
sudo apt-get install libsqlite3-dev
sudo apt-get install libssl-dev
If you're playing for the first time, enter a new username and password to create an account. Enter a previous username and password to retrieve progress. Click the farm to earn radishes, and use it to buy upgrades to optimize your radish farming abilities.
This handler is primarily used to store and retrieve user data - this includes username, password, and their associated radishes (currency) and upgrades. In the back, we've used SQLite3 as the database as well as salting and hashing the passwords for extra security. All sets of SQL queries are done in exclusive transactions to prevent multiple users from trying to modify data at the same time, and values are bound to statements to avoid SQL injection. See the following for a quick description of the API:
This allows the user to create a new account with a specified username and password. The password is salted and hashed before storage. By default, the player has zero radishes. The request requires a JSON object in the body containing strings for username and password, as seen below, as well as "create" for the action.
curl -X POST -H "Content-Type: application/json" -d "{ \"action\": \"create\", \"username\": \"testuser1\", \"password\": \"testpass1\"}" localhost:8080/clicker_game/data
This allows the user to get information associated with a username, by providing a username and password. The password is salted and hashed before using it to match to the correct account. The response's body is JSON with the radish number, the upgrades (each type and its associated number), and a session ID. This session ID will be required to log out and store information later, preventing malicious actors from changing the user's data otherwise. The request requires a JSON object in the body containing strings for username and password, as seen below, as well as "login" for the action.
curl -X POST -H "Content-Type: application/json" -d "{ \"action\": \"login\", \"username\": \"testuser1\", \"password\": \"testpass1\" }" localhost:8080/clicker_game/data
This allows the user to update their information with new data. The request requires a JSON object in the body containing the username, a session ID, the radish number, and the upgrades. Note that session ID will be unique per login or session - any applications that utilize this API should store the session ID somewhere.
curl -X PUT -H "Content-Type: application/json" -d "{ \"username\": \"testuser1\", \"session_id\": \"[SESSION_ID]\", \"radish_num\": 7, \"upgrades\": {\"upgrade1\": 2, \"upgrade2\": 15} }" localhost:8080/clicker_game/data
- include - folder for header files
- most are self explanatory
- info.h to hold various structs used throughout
- src - folder for source files
- static_files - folder containing static files that can be requested from the webserver
- subfolders for applications, images, and text
- tests - folder for unit and integration tests
- requests - contains partial requests, URL is filled in by the integration script
- responses - contains corresponding responses in RegEx
- other subfolders for files used in tests
- gcloud_server_config vs server_config - former is used during deployment (Docker image), the other is used locally
- other files/folders - self explanatory
- main function for the config parser - used when calling it separately from server
- reads in a configuration file
- parses for port number (port) and paths mapped to different handlers (location blocks)
- returns struct containing parsed info
- port number
- mapping from location to handler (location_to_handler)
- mapping from location to another mapping of directive to argument (location_to_directives)
- this assumes that every directive only has one argument
- for logging purposes
- defines and creates a file sink (rotation, formatting, etc)
- creates a console sink (primarily formatting)
- main function for starting webserver
- runs parser on provided config, and passes parsed config to server
- starts a new session
- listens for requests
- upon receiving data, calls the request dispatcher for response
- sends response to client
- using given request's path, matches it to a handler using config info
- gets corresponding factory
- uses factory to create handler to handle request
- returns response
- contains mapping of handler name to handler factories
- mapping pairs added by handlers themselves
- returns factory for handler when provided handler name
- base class of request handlers
- method for initializating response
- logging method - logs both request and response
- sets request as body
- sets 200 OK status and content type to be text
- returns response
- checks if file path exists - if so, tries to read in file as binary and add it to body
- sets header with appropriate content type, depending on file extension
- returns response
- generally configured to "/" in the configuration - catches all the unconfigured paths
- sets 404 Not Found status
- returns response
- blocks for 1 second to test multithreading
- returns 200 OK response to confirm server is healthy
- configuration should match format as specified in Assignment 6
mkdir build
if no build directory existscd build
cmake ..
make
bin/webserver ../server_config
to start server locally
cmake .. -DCMAKE_BUILD_TYPE=Release
(cmake ..
OK if you didn't run coverage first)make
make test
to run tests
mkdir build_coverage
if no build coverage directory existscmake .. -DCMAKE_BUILD_TYPE=Coverage
make coverage
- To see line coverage, check the
build/reports
forindex.html
and Open with Live Server (may require downloading extension)
docker build -f docker/base.Dockerfile -t raddish:base .
docker build -f docker/Dockerfile -t my_image .
- If you want to clear the build cache,
docker build --no-cache -f docker/Dockerfile -t my_image
- If you want to clear the build cache,
docker run --rm -p 127.0.0.1:80:80 --name my_run my_image:latest
- Write your request handler as a child of the base class RequestHandler
- its constructor should call the parent constructor with the passed in request handler data
- it should implement the handle request method
- Write the init method for the factory - which just returns a new instance of itself
RequestHandler* [HandlerName]::init(const RequestHandlerData& request_handler_data) { return new [HandlerName](request_handler_data); }
- Register the handler with the factory map
bool [HandlerName]::registered_ = RequestHandlerFactory::register_handler("[HandlerName]", [HandlerName]::init);
- Link your handler in CMakeList.txt
- Note that ordering of the arguments matter! If A depends on B, A should be earlier than B in the list.
- Add library:
add_library([lib_name] OBJECT src/[handler_file_name])
- Add your library to the interface (used for linked multiple handlers at once):
target_link_libraries(all_handlers_lib INTERFACE $<TARGET_OBJECTS:[lib_name]> ...)
- Add and link test executable:
add_executable([test_name] tests/[test_file_name]) target_link_libraries([test_name] $<TARGET_OBJECTS:[lib_name]> handler_factory_lib handler_lib gtest_main Boost::log)
- Add test to gtest:
gtest_discover_tests([test_name] WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
- Add source and test libraries to coverage test:
generate_coverage_report(TARGETS ... [lib_name] ... TESTS ... [test_name] ...)
#include "not_found_request_handler.h"
#include "request_handler_factory.h"
namespace beast = boost::beast;
namespace http = beast::http;
// calls parent constructor
// can be used to set handler-specific variables - e.g. the root for file handler.
// all data needed should be added to the RequestHandlerData struct contained in info.h.
// also sets name to be used in logging
NotFoundHandler::NotFoundHandler(const RequestHandlerData& request_handler_data) : RequestHandler(request_handler_data) {
handler_name_ = "NotFoundHandler";
}
// factory function
// Defined as a static function in the header - added to registry map on startup.
RequestHandler* NotFoundHandler::init(const RequestHandlerData& request_handler_data) {
return new NotFoundHandler(request_handler_data);
}
// add self-init to registry on startup.
// defined as a static bool in the header file.
bool NotFoundHandler::registered_ = RequestHandlerFactory::register_handler("NotFoundHandler", NotFoundHandler::init);
// implementation of handle_request
http::response<http::string_body> NotFoundHandler::handle_request(const http::request<http::string_body>& request) {
//superclass function - creates a res object, and initializes it as a 404 not found.
init_response(request);
//set vars
res_.result(http::status::not_found); //technically already done in init_response, but done here for clarity.
//log
log_request(request, res_, "Path not configured");
return res_;
}
#ifndef NOT_FOUND_REQUEST_HANDLER
#define NOT_FOUND_REQUEST_HANDLER
#include "request_handler.h"
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
class NotFoundHandler : public RequestHandler {
public:
NotFoundHandler(const RequestHandlerData& request_handler_data); //constructor
http::response<http::string_body> handle_request(const http::request<http::string_body>& request);
static RequestHandler* init(const RequestHandlerData& request_handler_data); //factory
static bool registered_; //used to register
};
#endif
The CRUD handler provides capabilities as required by assignment 7, spec shown here.
One should also note before looking at the operations that follow, that we assume the CRUD handler to be setup and have a specified valid data_path
, or directory to be used internally by the persistent key-value store as implemented by the CRUD store
. This is enforced by the handler's construction and initialization.
Despite the name of CRUD where one would expect 4 operations, the handler supports 5 distinct operations. Additionally, we define an operation as those that follow as supported by the CRUD handler, and an op code, or ${OP}
, as one of the HTTP verbs such as GET or POST.
- Create:
POST /api/Shoes
-> which creates a new key-value store and writes the POST body todata_path/Shoes/${ID}
, and then returns the ID of a newly created shoe entity - Retrieve:
GET /api/Shoes/1
-> returns the data at the key of 1 in the entity Shoes by internally readingdata_path/Shoes/1
- Update:
PUT /api/Shoes/5
-> updates the data for shoe 5 with the PUT body, by writing todata_path/Shoes/5
. If shoe at ID 5 already existing, update the value. If shoe at 5 has not already been created, then we create a shoe at 5 and put the PUT body there, allowing for creation of entity values with a pre-determined ID. - Delete:
DELETE /api/Shoes/1
-> delete shoe 1 from the system by interally removing the filedata_path/Shoes/1
- List:
GET /api/Shoes
-> returns a JSON list of valid IDs for the given entity, in this case all IDs currently in use for Shoes, internally located atdata_path/Shoes/
GET
-> retrieval of a single item if ID specified OR a list of all valid IDs if no ID specifiedPOST
-> creation of a single item using the POST body as input to the valuePUT
-> update of data at existing ID, or creation with data at specific non-existing IDDELETE
-> deletion of item at specified ID
All requests delivered to the CRUD handler API are expected to be in the format ${OP} ${API}/${Entity}/${ID}?
, where each of the following are defined as follows:
${OP}
is one of the operations specified above${API}
is the location of the server's CRUD handler, such as/api/
or other defined by server config${Entity}
is any simply-defined string name of an entity type (with some limitations noted below)${ID}
is the ID if of the item in question if required by the operation
Edge cases will be specified per the 5 distinct operations and bad requests in general. Bad requests at the fault of the user will receive 400 Bad Request
. If the internals of the CRUD store fail for any reason not due to request input, then we will return 500 Internal Server Error
since this is not the fault of the user.
${Entity}
can be any word with upper or lowercase letters, but cannot contain any other characters. This is defined by the Regex[A-Za-z]+
and is our interpretation of a simply-defined string word.${ID}
can be any number of digits so long as there is one if required, but this is limited internally by its implementation and storage as anint
, so be wary of overflow which will lead to undefined behavior- If a request does not match our required format we return
400 Bad Request
with a body ofInvalid request format for CRUD API
. For example, if an operation requires an ID but one is not provided, then that is a400
. Vice-versa, if an ID should not be specified and one is specified, that is a400
.
- Uses an op code of
POST
- IDs start at 1 and count upwards.
- When creating a new item, the new ID will be
max value of the currently used IDs + 1
POST
with an ID specified is a bad requestPOST
with invalid JSON in the POST body is a bad request- If the
${Entity}
subdirectory does not already exist (perhaps because there were no entries previously to this entity type), then one will be created and operation continues as normal
- Uses an op code of
GET
- If no ID is specified, we assume List operation (discussed below)
- If an invalid ID is specified, we return
404 File Not Found
- Since IDs start at 1,
GET
with an ID of 0 or no ID will be interpreted as aList
operation, since internally an ID of 0 (or no ID given) is used to signal aList
operation
- Uses an op code of
PUT
- If the ID is 0, bad request
- If the JSON in the POST body is invalid, bad request
- If the ID is in use already, update the value at that ID
- If the ID is not in use already, create a new item with that ID and the POST body as the value
- If ID not in use already and
${Entity}
also does not exist, create the${Entity}
store and proceed as if everything is fine and ID not in use, so create a new item with that ID
- Uses an op code of
DELETE
- If no ID is specified or the ID specified is 0, bad request
- If the file exists at that ID, delete it
- If the file does not exist to delete, return
404 File Not Found
- Shares an op code of
GET
with retrieve - Unspecified or ID of 0 is routed to the list functionality
- The list of valid IDs returned will be sorted in ascending order
- If there are no valid IDs, then we give back an empty list of IDs with
200
- If the entity type does not exist, give back an empty list of IDs with
200
- The list implementation ignores non-integer filenames and only returns those with valid IDs (integers only). These files would only end up here if the server admin interfered and put them there.