Skip to content

Commit

Permalink
basic HTTP auth, com-port as an argument, fix Actions upload step
Browse files Browse the repository at this point in the history
  • Loading branch information
catink123 committed Jan 15, 2024
1 parent d32e91b commit abe9b15
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 83 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: server-debug
path: out/windows-x64-debug/Debug
path: out/windows-x64-debug/src/Debug

- name: Upload Release artifacts
uses: actions/upload-artifact@v3
with:
name: server-release
path: out/windows-x64-release/Release
path: out/windows-x64-release/src/Release
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,11 @@ CPMAddPackage(
"BOOST_ENABLE_CMAKE ON"
)

CPMAddPackage(
NAME bcrypt
VERSION 2.0
GITHUB_REPOSITORY "hilch/Bcrypt.cpp"
GIT_TAG "V2.0_NODEBCRYPT"
)

add_subdirectory(${PROJECT_SOURCE_DIR}/src)
4 changes: 3 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ add_executable(
json_message.cpp
arduino_messenger.hpp
arduino_messenger.cpp
)
auth.hpp
)

set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)

Expand All @@ -28,6 +29,7 @@ target_link_libraries(
${PROJECT_NAME}
Boost::beast
nlohmann_json::nlohmann_json
bcrypt
)

# install server binary to bin
Expand Down
21 changes: 16 additions & 5 deletions src/arduino_messenger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ arduino_messenger::arduino_messenger(
unsigned int baud_rate
) : com(io)
{
com.open(std::string(device_name));
boost::system::error_code error;
com.open(std::string(device_name), error);

if (error) {
if (error == boost::system::errc::device_or_resource_busy)
throw open_error("serial port already in use");
if (error == boost::system::errc::no_such_file_or_directory)
throw open_error("serial port doesn't exist");
throw open_error(error.message().c_str());
}

com.set_option(net::serial_port_base::baud_rate(baud_rate));
}

Expand All @@ -20,11 +30,9 @@ void arduino_messenger::run() {

void arduino_messenger::do_read() {
net::async_read_until(com, buffer, '%',
std::bind(
beast::bind_front_handler(
&arduino_messenger::on_read,
shared_from_this(),
net::placeholders::error,
net::placeholders::bytes_transferred
shared_from_this()
)
);
}
Expand All @@ -41,7 +49,10 @@ void arduino_messenger::on_read(

std::string message(reinterpret_cast<const char*>(buffer_data.data()), buffer_data.size());

std::lock_guard lock(imq_mutex);
incoming_message_queue.push(json_message::parse_message(message));

do_read();
}

void arduino_messenger::do_write() {
Expand Down
5 changes: 5 additions & 0 deletions src/arduino_messenger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class arduino_messenger : public std::enable_shared_from_this<arduino_messenger>
std::queue<json_message> incoming_message_queue;
std::mutex imq_mutex;

class open_error : public std::runtime_error {
public:
open_error(const char* what) : std::runtime_error(what) {}
};

arduino_messenger(
net::io_context& io,
std::string_view device_name,
Expand Down
18 changes: 18 additions & 0 deletions src/arduino_program/gate_control/gate_control.ino
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ void setup() {
digitalWrite(13, LOW);
}

void flash() {
digitalWrite(13, HIGH);
delay(200);
digitalWrite(13, LOW);
delay(200);
digitalWrite(13, HIGH);
delay(200);
digitalWrite(13, LOW);
delay(200);
if (state) {
digitalWrite(13, HIGH);
} else {
digitalWrite(13, LOW);
}
}

void loop() {
if (Serial.available() > 0) {
String msg = Serial.readStringUntil('%');
Expand All @@ -30,6 +46,8 @@ void loop() {
serializeJson(dyn_doc, msg);
msg += '%';

flash();

Serial.write(msg.c_str());
} else {
Serial.write("{\"type\": \"error\", \"payload\": \"unknown_command\"}%");
Expand Down
85 changes: 85 additions & 0 deletions src/auth.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#ifndef AUTH_HPP
#define AUTH_HPP

#include "common.hpp"
#include <unordered_map>
#include <optional>
#include <bcrypt.h>
#include <boost/beast/core/detail/base64.hpp>
#include <array>

namespace base64 = beast::detail::base64;

enum AuthorizationType {
Control,
View
};

struct auth_data {
AuthorizationType permissions;
std::string password_hash;

auth_data(
const AuthorizationType permissions,
std::string_view password_hash
) : permissions(permissions), password_hash(password_hash) {}
};

template <class Body, class Allocator>
std::optional<AuthorizationType> check_auth(
const http::request<Body, http::basic_fields<Allocator>>& req,
const std::unordered_map<std::string, auth_data>& auth_table
) {
// get the authorization field and separate the base64 encoded user-pass combination
const std::string authorization = req.at(http::field::authorization);
const std::string user_pass = authorization.substr(authorization.find(' ') + 1);

// decode the base64 combination
const std::size_t decoded_size = base64::decoded_size(user_pass.size());
char* user_pass_decoded = new char[decoded_size];

auto decode_result =
beast::detail::base64::decode(
reinterpret_cast<void*>(user_pass_decoded),
user_pass.c_str(),
user_pass.size()
);

const std::string user_pass_str(user_pass_decoded, decode_result.first);

delete[] user_pass_decoded;

// separate the user-id and password (if the user exists in the auth_table)
const std::size_t user_pass_delimeter_loc = user_pass_str.find(':');
const std::string user_id = user_pass_str.substr(0, user_pass_delimeter_loc);

if (auth_table.find(user_id) == auth_table.end()) {
return std::nullopt;
}

const std::string password = user_pass_str.substr(user_pass_delimeter_loc + 1);

// compare the sent password with the stored hash to validate
const auth_data& data = auth_table.at(user_id);
if (bcrypt::validatePassword(password, data.password_hash)) {
return data.permissions;
}
else {
return std::nullopt;
}
}

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
41 changes: 41 additions & 0 deletions src/client/control/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!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>
</head>
<body>
<h1>Control Page</h1>

<button disabled id="send-query">Query State</button>
<button disabled id="send-up">Send Raise</button>
<button disabled id="send-down">Send Lower</button>

<script>
let ws = new WebSocket("ws://" + location.host);

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") alert(`Current Gate State: ${msg.payload}`);
if (msg.type == "availability") alert(`Availability: ${msg.payload}`);
});

function setListener(selector, type, listener) {
document.querySelector(selector).addEventListener(type, listener);
}

function bindMessage(selector, msgObj) {
setListener(selector, "click", () => ws.send(JSON.stringify(msgObj)));
}

ws.addEventListener('open', () => {
document.querySelectorAll('button').forEach(btn => btn.disabled = false);
bindMessage("#send-query", { type: "query_state", payload: null });
bindMessage("#send-up", { type: "change_state", payload: true });
bindMessage("#send-down", { type: "change_state", payload: false });
});
</script>
</body>
</html>
32 changes: 1 addition & 31 deletions src/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,6 @@
<title>Gate Control</title>
</head>
<body>
<h1>Test Client Page</h1>

<button disabled id="send-query">Query State</button>
<button disabled id="send-up">Send Raise</button>
<button disabled id="send-down">Send Lower</button>

<script>
let ws = new WebSocket("ws://" + location.host);

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") alert(`Current Gate State: ${msg.payload}`);
if (msg.type == "availability") alert(`Availability: ${msg.payload}`);
});

function setListener(selector, type, listener) {
document.querySelector(selector).addEventListener(type, listener);
}

function bindMessage(selector, msgObj) {
setListener(selector, "click", () => ws.send(JSON.stringify(msgObj)));
}

ws.addEventListener('open', () => {
document.querySelectorAll('button').forEach(btn => btn.disabled = false);
bindMessage("#send-query", { type: "query_state", payload: null });
bindMessage("#send-up", { type: "change_state", payload: true });
bindMessage("#send-down", { type: "change_state", payload: false });
});
</script>
<h1>Login</h1>
</body>
</html>
47 changes: 47 additions & 0 deletions src/http_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,37 @@ http::message_generator handle_request(
return res;
};

const auto unauthorized =
[&req] (beast::string_view target) {
http::response<http::string_body> res{
http::status::unauthorized,
req.version()
};

res.set(http::field::server, VERSION);
res.set(http::field::www_authenticate, "Basic realm=\"control\"");
res.keep_alive(req.keep_alive());
res.body() = "Unauthorized client on resource '" + std::string(target) + "'.";
res.prepare_payload();

return res;
};

const auto forbidden =
[&req](beast::string_view target) {
http::response<http::string_body> res{
http::status::forbidden,
req.version()
};

res.set(http::field::server, VERSION);
res.keep_alive(req.keep_alive());
res.body() = "Access to resource '" + std::string(target) + "' is forbidden";
res.prepare_payload();

return res;
};

// make sure we can handle the request
if (
req.method() != http::verb::get &&
Expand All @@ -255,6 +286,22 @@ http::message_generator handle_request(
path.append("index.html");
}

// if the request target is not the root page...
if (requires_auth(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);

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

// open the file
beast::error_code ec;
http::file_body::value_type body;
Expand Down
10 changes: 10 additions & 0 deletions src/http_session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include <chrono>
#include "websocket_session.hpp"
#include "arduino_messenger.hpp"
#include "auth.hpp"

namespace base64 = beast::detail::base64;

beast::string_view mime_type(
beast::string_view path
Expand All @@ -18,6 +21,13 @@ 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") }
};

// handle given request by returning an appropriate response
template <class Body, class Allocator>
http::message_generator handle_request(
Expand Down

0 comments on commit abe9b15

Please sign in to comment.