Skip to content

Commit

Permalink
scope endpoints to permissions, differentiate View and Control permis…
Browse files Browse the repository at this point in the history
…sions
  • Loading branch information
catink123 committed Jan 16, 2024
1 parent e7d9dc3 commit f361bef
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 38 deletions.
58 changes: 43 additions & 15 deletions src/auth.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,48 @@
#include <bcrypt.h>
#include <boost/beast/core/detail/base64.hpp>
#include <array>
#include <unordered_map>

namespace base64 = beast::detail::base64;

enum AuthorizationType {
Control,
View
View,
Blocked
};

const std::unordered_map<std::string, std::optional<AuthorizationType>> endpoint_map = {
{ "/", std::nullopt },
{ "/control", Control },
{ "/view", View }
};

template <class Body, class Allocator>
std::optional<AuthorizationType> get_endpoint_permissions(
const http::request<Body, http::basic_fields<Allocator>>& req
) {
auto& target = req.target();
std::size_t last_delimeter = target.rfind('/');
std::string endpoint;
if (last_delimeter == 0) {
if (target.size() > 1) {
endpoint = target;
}
else {
endpoint = "/";
}
}
else {
endpoint = target.substr(0, last_delimeter);
}

if (endpoint_map.find(endpoint) == endpoint_map.end()) {
return Blocked;
}

return endpoint_map.at(endpoint);
}

struct auth_data {
AuthorizationType permissions;
std::string password_hash;
Expand All @@ -25,8 +59,15 @@ struct auth_data {
) : permissions(permissions), password_hash(password_hash) {}
};

// catink123:testpassword123
// guest:guest
const std::unordered_map<std::string, auth_data> temp_auth_table = {
{ "catink123", auth_data(Control, "$2a$10$o12u27uUOjD6rJ0dlEE/EuL8EqGa7y8iwZqAp3wF0WBS4.Vu/9jhK") },
{ "guest", auth_data(View, "$2a$10$vYQHg8mBFTle1OzRp31MsOMvrmfQ52xfHUGFoi3aTe6Vp8GhDRzBy") }
};

template <class Body, class Allocator>
std::optional<AuthorizationType> check_auth(
std::optional<AuthorizationType> get_auth(
const http::request<Body, http::basic_fields<Allocator>>& req,
const std::unordered_map<std::string, auth_data>& auth_table
) {
Expand Down Expand Up @@ -69,17 +110,4 @@ std::optional<AuthorizationType> check_auth(
}
}

const std::array<std::string, 3> unauthable_resources = { "/", "/index.html", "/favicon.ico" };

template <class Body, class Allocator>
bool requires_auth(
const http::request<Body, http::basic_fields<Allocator>>& req
) {
return std::find(
unauthable_resources.begin(),
unauthable_resources.end(),
req.target()
) == unauthable_resources.end();
}

#endif
2 changes: 1 addition & 1 deletion src/client/control/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ <h1>Control Page</h1>
<p>Current state: <span class="state unknown">Unknown</span></p>

<script>
let ws = new WebSocket("ws://" + location.host);
let ws = new WebSocket("ws://" + location.host + location.pathname);
const stateSpan = document.querySelector("span.state");

ws.addEventListener('message', e => {
Expand Down
42 changes: 41 additions & 1 deletion src/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@
<title>Gate Control</title>
</head>
<body>
<h1>Login</h1>
<h1>Home Page</h1>
<a href="/view">View</a>
<a href="/control">Control</a>

<!--<h1>Login Test</h1>
<div>
<p>
<label for="login">Login </label>
<input type="text" id="login" />
</p>
<p>
<label for="password">Password </label>
<input type="password" id="password" />
</p>
<button id="login-view">View</button>
<button id="login-control">Control</button>
</div>
<script>
const loginInput = document.querySelector("input#login");
const passwordInput = document.querySelector("input#password");
function login(type) {
const auth = btoa(`${loginInput.value}:${passwordInput.value}`);
const url = `${location.protocol}//${location.host}/${type}`;
fetch(url, {
headers: {
'Authorization': `Basic ${auth}`
}
}).then(res => {
if (res.ok) {
location.href = url;
}
throw res;
}).catch(err => alert(err));
}
document.querySelector("#login-view").addEventListener("click", () => login("view"));
document.querySelector("#login-control").addEventListener("click", () => login("control"));
</script>-->
</body>
</html>
52 changes: 52 additions & 0 deletions src/client/view/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gate Control</title>
<style>
.state {
border-radius: 50px;
padding: 2px 5px;
}

.state.unknown {
background: yellow;
}

.state.raised {
background: lime;
}

.state.lowered {
background: red;
color: white;
}
</style>
</head>
<body>
<h1>View Page</h1>

<p>Current state: <span class="state unknown">Unknown</span></p>

<script>
let ws = new WebSocket("ws://" + location.host + location.pathname);
const stateSpan = document.querySelector("span.state");

ws.addEventListener('message', e => {
let msg = JSON.parse(e.data);
if (msg.type == "text") alert(`Text from Server: ${msg.payload}`);
if (msg.type == "query_state_result") {
if (msg.payload == true) {
stateSpan.className = "state raised";
stateSpan.innerText = "Raised";
} else if (msg.payload == false) {
stateSpan.className = "state lowered";
stateSpan.innerText = "Lowered";
}
}
if (msg.type == "availability") alert(`Availability: ${msg.payload}`);
});
</script>
</body>
</html>
32 changes: 24 additions & 8 deletions src/http_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,26 +287,42 @@ http::message_generator handle_request(
return bad_request("Illegal request target");
}

// build requested file path
std::string path = path_cat(doc_root, req.target());
if (req.target().back() == '/') {
path.append("index.html");
}

// if the request target is not the root page...
if (requires_auth(req)) {
if (const auto endpoint_perms = get_endpoint_permissions(req)) {
// make sure the client has sufficient permissions
if (
req.find(http::field::authorization) == req.end()
) {
return unauthorized(req.target());
}

const auto permissions = check_auth(req, temp_auth_table);
const auto permissions = get_auth(req, temp_auth_table);

if (!permissions) {
return unauthorized(req.target());
}

if (permissions != endpoint_perms) {
return forbidden(req.target());
}
}

// build requested file path
std::string path = path_cat(doc_root, req.target());

if (
req.target().back() == '/'
) {
path.append("index.html");
}
else if (
std::find(
indexable_endpoints.begin(),
indexable_endpoints.end(),
req.target()
) != indexable_endpoints.end()
) {
path.append("/index.html");
}

// open the file
Expand Down
8 changes: 2 additions & 6 deletions src/http_session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <memory>
#include <string>
#include <chrono>
#include <array>
#include "websocket_session.hpp"
#include "arduino_messenger.hpp"
#include "common_state.hpp"
Expand All @@ -22,12 +23,7 @@ std::string path_cat(
beast::string_view path
);

// catink123:testpassword123
// guest:guest
const std::unordered_map<std::string, auth_data> temp_auth_table = {
{ "catink123", auth_data(Control, "$2a$10$o12u27uUOjD6rJ0dlEE/EuL8EqGa7y8iwZqAp3wF0WBS4.Vu/9jhK") },
{ "guest", auth_data(View, "$2a$10$vYQHg8mBFTle1OzRp31MsOMvrmfQ52xfHUGFoi3aTe6Vp8GhDRzBy") }
};
const std::array<std::string, 3> indexable_endpoints = { "/", "/view", "/control" };

// handle given request by returning an appropriate response
template <class Body, class Allocator>
Expand Down
16 changes: 9 additions & 7 deletions src/websocket_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,17 @@ void websocket_session::on_write(
}

void websocket_session::queue_message(std::string_view message) {
write_queue.push(std::string(message));
write_queue.push(std::string(message));
}

void websocket_session::handle_message(std::string_view message) {
try {
auto parsed_msg = json_message::parse_message(message);

if (parsed_msg.type == json_message::QueryState || parsed_msg.type == json_message::ChangeState)
arduino_connection->send_message(parsed_msg);
if (permissions == Control) {
try {
auto parsed_msg = json_message::parse_message(message);

if (parsed_msg.type == json_message::QueryState || parsed_msg.type == json_message::ChangeState)
arduino_connection->send_message(parsed_msg);
}
catch (...) {}
}
catch (...) {}
}
9 changes: 9 additions & 0 deletions src/websocket_session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
#include <thread>
#include "json_message.hpp"
#include "arduino_messenger.hpp"
#include "auth.hpp"

class websocket_session : public std::enable_shared_from_this<websocket_session> {
websocket::stream<beast::tcp_stream> ws;
beast::flat_buffer buffer;
std::string write_buffer;
std::queue<std::string> write_queue;
std::shared_ptr<arduino_messenger> arduino_connection;
AuthorizationType permissions = Blocked;

public:
explicit websocket_session(
Expand All @@ -27,6 +29,13 @@ class websocket_session : public std::enable_shared_from_this<websocket_session>
void do_accept(
http::request<Body, http::basic_fields<Allocator>> req
) {
auto auth = get_auth(req, temp_auth_table);
if (auth == std::nullopt || auth == Blocked) {
return;
}

permissions = auth.value();

ws.set_option(
websocket::stream_base::timeout::suggested(
beast::role_type::server
Expand Down

0 comments on commit f361bef

Please sign in to comment.