Skip to content

Commit

Permalink
Implement a captive portal for the access point
Browse files Browse the repository at this point in the history
  • Loading branch information
mairas committed Feb 8, 2024
1 parent 7644575 commit 6711dd6
Show file tree
Hide file tree
Showing 18 changed files with 2,282 additions and 2,335 deletions.
145 changes: 123 additions & 22 deletions src/sensesp/net/http_server.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#include "sensesp/net/http_server.h"

#include <lwip/sockets.h>

#include <functional>
#include <list>

#include "sensesp.h"

namespace sensesp {

std::list<HTTPRequestHandler *> HTTPRequestHandler::handlers_;

// from:
// https://stackoverflow.com/questions/2673207/c-c-url-decode-library/2766963
void urldecode2(char* dst, const char* src) {
Expand Down Expand Up @@ -40,39 +40,140 @@ void urldecode2(char* dst, const char* src) {

HTTPServer* HTTPServer::singleton_ = nullptr;

void HTTPServer::register_handler(const httpd_uri_t* uri_handler) {
esp_err_t error = httpd_register_uri_handler(server_, uri_handler);
if (error != ESP_OK) {
debugE("Error registering URI handler for %s: %s", uri_handler->uri,
esp_err_to_name(error));
esp_err_t HTTPServer::dispatch_request(httpd_req_t* req) {
debugI("Handling request: %s", req->uri);

if (captive_portal_) {
bool captured;
captured = handle_captive_portal(req);
if (captured) {
return ESP_OK;
}
};
}

if (auth_required_) {
bool success;
success =
authenticate_request(authenticator_, call_request_dispatcher, req);
if (!success) {
// Authentication failed; do not continue but return success.
// The client already received a 401 response.
return ESP_OK;
}
}

esp_err_t authenticate_request(HTTPAuthenticator* auth,
std::function<esp_err_t(httpd_req_t*)> handler,
httpd_req_t* req) {
bool continue_;
// dispatch request to the appropriate handler

String uri = req->uri;

// decode the uri
char decoded_uri[uri.length() + 1];
urldecode2(decoded_uri, uri.c_str());
String decoded_uri_str = String(decoded_uri);

// Drop the query part of the URI
int query_pos = decoded_uri_str.indexOf('?');
if (query_pos != -1) {
decoded_uri_str = decoded_uri_str.substring(0, query_pos);
}

for (auto handler : handlers_) {
String match_uri = handler->match_uri_;

// Check if the match uri ends with a wildcard
if (match_uri.endsWith("*")) {
// Remove the wildcard from the match uri
match_uri = match_uri.substring(0, match_uri.length() - 1);
// Check if the request uri starts with the match uri
if (decoded_uri_str.startsWith(match_uri)) {
if (handler->method_mask_ & (1 << req->method)) {
return handler->call(req);
}
}
} else if (handler->match_uri_ == req->uri) {
if (handler->method_mask_ & (1 << req->method)) {
return handler->call(req);
}
}
}

return httpd_resp_send_404(req);
}

bool HTTPServer::handle_captive_portal(httpd_req_t* req) {
// Get HTTP Host header
char host[100];
if (httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) != ESP_OK) {
return false;
}
String host_hdr = host;
// Drop port number
int pos = host_hdr.indexOf(':');
if (pos != -1) {
host_hdr = host_hdr.substring(0, pos);
}

// Get the HTTP request socket local IP address
int sockfd = httpd_req_to_sockfd(req);
char ipstr[INET6_ADDRSTRLEN];
struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
socklen_t addr_size = sizeof(addr);
if (getsockname(sockfd, (struct sockaddr*)&addr, &addr_size) < 0) {
debugE("Error getting client IP");
return false;
}
inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));
debugD("Local IP address: %s", ipstr);

String ap_ip = WiFi.softAPIP().toString();

// We should only apply the captive portal to the soft AP IP address
if (ap_ip != ipstr) {
debugD("Not a captive portal request");
return false;
}

// Check if the host matches our soft AP IP address
if (host_hdr != ap_ip) {
// Redirect the client to the captive portal url /wifi
httpd_resp_set_status(req, "302 Found");
String destination = String("http://") + ap_ip + "/wifi";
httpd_resp_set_hdr(req, "Location", destination.c_str());
httpd_resp_sendstr(req, "Redirecting to captive portal");
return true;
}
return false;
}

bool HTTPServer::authenticate_request(
HTTPAuthenticator* auth, std::function<esp_err_t(httpd_req_t*)> handler,
httpd_req_t* req) {
bool continue_ = false;
if (auth == nullptr) {
continue_ = true;
} else {
continue_ = auth->authenticate_request(req);
}
if (continue_) { // authentication successful
return handler(req);
} else {
return ESP_OK; // Even though the authentication failed, request handling
// was successful.
}

return continue_;
}

esp_err_t call_static_handler(httpd_req_t* req,
esp_err_t (*handler)(httpd_req_t*)) {
esp_err_t call_request_dispatcher(httpd_req_t* req) {
HTTPServer* server = HTTPServer::get_server();
bool continue_;

HTTPAuthenticator* auth = server->authenticator_;
return authenticate_request(auth, handler, req);
return server->dispatch_request(req);
}

String get_content_type(httpd_req_t* req) {
if (httpd_req_get_hdr_value_len(req, "Content-Type") != 16) {
debugE("Invalid content type");
return "";
} else {
char content_type[32];
httpd_req_get_hdr_value_str(req, "Content-Type", content_type, 32);
return String(content_type);
}
}

} // namespace sensesp
145 changes: 64 additions & 81 deletions src/sensesp/net/http_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,30 @@ namespace sensesp {
#include <stdlib.h>

void urldecode2(char* dst, const char* src);
String get_content_type(httpd_req_t* req);
esp_err_t call_request_dispatcher(httpd_req_t* req);

class HTTPServer;

/**
* @brief Base class for HTTP server handlers.
*
* A class that inherits from HTTPRequestHandler can be will have its
* `set_handler` method called when the HTTP server starts. This method
* should register the handler with the server.
* @brief HTTP request handler storage class.
*
*/
class HTTPRequestHandler {
public:
HTTPRequestHandler() {
// add this handler to the list of handlers
handlers_.push_back(this);
}
HTTPRequestHandler(uint32_t method_mask, String match_uri,
std::function<esp_err_t(httpd_req_t*)> handler_func)
: method_mask_(method_mask),
match_uri_(match_uri),
handler_func_(handler_func) {}

static void register_handlers(HTTPServer* server) {
for (auto& handler : handlers_) {
handler->register_handler(server);
}
}
const uint32_t method_mask_;
const String match_uri_;

protected:
virtual void set_handler(HTTPServer* server) = 0;
esp_err_t call(httpd_req_t* req) { return this->handler_func_(req); }

String get_content_type(httpd_req_t* req) {
if (httpd_req_get_hdr_value_len(req, "Content-Type") != 16) {
debugE("Invalid content type");
return "";
} else {
char content_type[32];
httpd_req_get_hdr_value_str(req, "Content-Type", content_type, 32);
return String(content_type);
}
}

void register_handler(HTTPServer* server) { this->set_handler(server); }

private:
static std::list<HTTPRequestHandler*> handlers_;

template <class T>
friend esp_err_t call_member_handler(httpd_req_t* req,
esp_err_t (T::*member)(httpd_req_t*));
protected:
const std::function<esp_err_t(httpd_req_t*)> handler_func_;
};

/**
Expand Down Expand Up @@ -105,7 +83,23 @@ class HTTPServer : public Configurable {
} else {
debugI("HTTP server started");
}
HTTPRequestHandler::register_handlers(this);

// register the request dispatcher for all methods and all URIs
httpd_uri_t uri = {
.uri = "/*",
.method = HTTP_GET,
.handler = call_request_dispatcher,
.user_ctx = this,
};
httpd_register_uri_handler(server_, &uri);
uri.method = HTTP_HEAD;
httpd_register_uri_handler(server_, &uri);
uri.method = HTTP_POST;
httpd_register_uri_handler(server_, &uri);
uri.method = HTTP_PUT;
httpd_register_uri_handler(server_, &uri);
uri.method = HTTP_DELETE;
httpd_register_uri_handler(server_, &uri);

// announce the server over mDNS
MDNS.addService("http", "tcp", 80);
Expand All @@ -115,15 +109,17 @@ class HTTPServer : public Configurable {

void stop() { httpd_stop(server_); }

void register_handler(const httpd_uri_t* uri_handler);

void set_auth_credentials(const String& username, const String& password,
bool auth_required = true) {
username_ = username;
password_ = password;
auth_required_ = auth_required;
}

void set_captive_portal(bool captive_portal) {
captive_portal_ = captive_portal;
}

virtual void get_configuration(JsonObject& config) override {
config["auth_required"] = auth_required_;
config["username"] = username_;
Expand All @@ -143,61 +139,48 @@ class HTTPServer : public Configurable {
return true;
}

void add_handler(HTTPRequestHandler* handler) {
handlers_.push_back(handler);
}

protected:
static HTTPServer* get_server() { return singleton_; }
static HTTPServer* singleton_;
bool captive_portal_ = false;
httpd_handle_t server_ = nullptr;
httpd_config_t config_;
String username_;
String password_;
bool auth_required_ = false;
HTTPAuthenticator* authenticator_ = nullptr;

template <class T>
friend esp_err_t call_member_handler(httpd_req_t* req,
esp_err_t (T::*member)(httpd_req_t*));
friend esp_err_t call_static_handler(httpd_req_t* req,
esp_err_t (*handler)(httpd_req_t*));
bool authenticate_request(HTTPAuthenticator* auth,
std::function<esp_err_t(httpd_req_t*)> handler,
httpd_req_t* req);

/**
* @brief Dispatcher method that captures all requests and forwards them to
* the appropriate handlers.
*
* @param req
* @return esp_err_t
*/
esp_err_t dispatch_request(httpd_req_t* req);

/**
* @brief Check if the request is for the captive portal and handle it if it
*
* @param req
* @return true Request was handled
* @return false Request was not handled
*/
bool handle_captive_portal(httpd_req_t* req);

std::list<HTTPRequestHandler*> handlers_;

friend esp_err_t call_request_dispatcher(httpd_req_t* req);
};

esp_err_t authenticate_request(HTTPAuthenticator* auth,
std::function<esp_err_t(httpd_req_t*)> handler,
httpd_req_t* req);

/**
* @brief Call a member function of class T as a request handler.
*
* The object pointer is cast from the user_ctx of the request.
*
* @tparam T
* @param req
* @param member
* @return esp_err_t
*/
template <typename T>
esp_err_t call_member_handler(httpd_req_t* req,
esp_err_t (T::*member)(httpd_req_t*)) {
HTTPServer* server = HTTPServer::get_server();
auto* handler = static_cast<T*>(req->user_ctx);
bool continue_;

std::function<esp_err_t(httpd_req_t*)> memberfunc =
[handler, member](httpd_req_t* req) { return (handler->*member)(req); };

HTTPAuthenticator* auth = server->authenticator_;
return authenticate_request(auth, memberfunc, req);
}

/**
* @brief Call a static function as a request handler.
*
* @param req
* @param handler
* @return esp_err_t
*/
esp_err_t call_static_handler(httpd_req_t* req,
esp_err_t (*handler)(httpd_req_t*));

} // namespace sensesp

#endif // SENSESP_NET_HTTP_SERVER_H_
Loading

0 comments on commit 6711dd6

Please sign in to comment.