Skip to content

Commit

Permalink
validate CSMS-URL: opt. schema must fit with security-profile; charge…
Browse files Browse the repository at this point in the history
…point-ID deprecated in URL (#201)

Signed-off-by: Dominik K <[email protected]>
  • Loading branch information
Dominik-K authored Nov 13, 2023
1 parent 6ef5252 commit 271e025
Show file tree
Hide file tree
Showing 19 changed files with 321 additions and 76 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[*.json]
indent_style = space
indent_size = 4
2 changes: 1 addition & 1 deletion config/v16/config-docker-tls.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"Internal": {
"ChargePointId": "cp001",
"CentralSystemURI": "127.0.0.1:8443/steve/websocket/CentralSystemService/cp001",
"CentralSystemURI": "127.0.0.1:8443/steve/websocket/CentralSystemService/",
"ChargeBoxSerialNumber": "cp001",
"ChargePointModel": "Yeti",
"ChargePointVendor": "Pionix",
Expand Down
7 changes: 4 additions & 3 deletions config/v16/config-docker.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"Internal": {
"ChargePointId": "cp001",
"CentralSystemURI": "127.0.0.1:8180/steve/websocket/CentralSystemService/cp001",
"CentralSystemURI": "127.0.0.1:8180/steve/websocket/CentralSystemService/",
"ChargeBoxSerialNumber": "cp001",
"ChargePointModel": "Yeti",
"ChargePointVendor": "Pionix",
Expand Down Expand Up @@ -36,8 +36,9 @@
"SupportedFileTransferProtocols": "FTP"
},
"Security": {
"SecurityProfile": 0,
"CpoName": "Pionix"
"CpoName": "Pionix",
"AuthorizationKey": "AABBCCDDEEFFGGHH",
"SecurityProfile": 1
},
"LocalAuthListManagement": {
"LocalAuthListEnabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"required": [
"CertificateEntries",
"OrganizationName",
"SecurityProfile"
"SecurityProfile",
"Identity"
]
}
6 changes: 6 additions & 0 deletions config/v201/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,12 @@
"Actual": "1"
}
},
"Identity": {
"variable_name": "Identity",
"attributes": {
"Actual": "cp001"
}
},
"BasicAuthPassword": {
"variable_name": "BasicAuthPassword",
"attributes": {
Expand Down
11 changes: 11 additions & 0 deletions include/ocpp/common/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,17 @@ firmware_status_notification_to_firmware_status_enum_type(const FirmwareStatusNo

} // namespace conversions

namespace security {
// The security profiles defined in OCPP 2.0.1 resp. in the OCPP 1.6 security-whitepaper.
enum SecurityProfile { // no "enum class" because values are used in implicit `switch`-comparisons to `int
// security_profile`
OCPP_1_6_ONLY_UNSECURED_TRANSPORT_WITHOUT_BASIC_AUTHENTICATION = 0,
UNSECURED_TRANSPORT_WITH_BASIC_AUTHENTICATION = 1,
TLS_WITH_BASIC_AUTHENTICATION = 2,
TLS_WITH_CLIENT_SIDE_CERTIFICATES = 3,
};
} // namespace security

namespace security_events {

// This is the list of security events defined in OCPP 2.0.1 (and the 1.6 security whitepper).
Expand Down
20 changes: 10 additions & 10 deletions include/ocpp/common/websocket/websocket_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@
#include <thread>

#include <everest/timer.hpp>

#include <ocpp/common/types.hpp>

#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>

#include <ocpp/common/types.hpp>
#include <ocpp/common/websocket/websocket_uri.hpp>

namespace ocpp {

struct WebsocketConnectionOptions {
OcppProtocolVersion ocpp_version;
std::string cs_uri;
int security_profile;
std::string chargepoint_id;
Uri csms_uri; // the URI of the CSMS
int security_profile; // FIXME: change type to `SecurityProfile`
std::optional<std::string> authorization_key;
int retry_backoff_random_range_s;
int retry_backoff_repeat_times;
Expand Down Expand Up @@ -50,7 +49,6 @@ class WebsocketBase {
std::function<void(const std::string& message)> message_callback;
websocketpp::lib::shared_ptr<boost::asio::steady_timer> reconnect_timer;
std::unique_ptr<Everest::SteadyTimer> ping_timer;
std::string uri;
websocketpp::connection_hdl handle;
std::mutex reconnect_mutex;
std::mutex connection_mutex;
Expand Down Expand Up @@ -84,16 +82,18 @@ class WebsocketBase {
void on_pong_timeout(websocketpp::connection_hdl hdl, std::string msg);

public:
/// \brief Creates a new WebsocketBase object with the providede \p connection_options
explicit WebsocketBase(const WebsocketConnectionOptions& connection_options);
/// \brief Creates a new WebsocketBase object. The `connection_options` must be initialised with
/// `set_connection_options()`
explicit WebsocketBase();
virtual ~WebsocketBase();

/// \brief connect to a websocket
/// \returns true if the websocket is initialized and a connection attempt is made
virtual bool connect() = 0;

/// \brief sets this connection_options to the given \p connection_options and resets the connection_attempts
void set_connection_options(const WebsocketConnectionOptions& connection_options);
virtual void set_connection_options(const WebsocketConnectionOptions& connection_options) = 0;
void set_connection_options_base(const WebsocketConnectionOptions& connection_options);

/// \brief reconnect the websocket after the delay
virtual void reconnect(std::error_code reason, long delay) = 0;
Expand Down
2 changes: 2 additions & 0 deletions include/ocpp/common/websocket/websocket_plain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class WebsocketPlain final : public WebsocketBase {
/// \brief Called when a plaintext websocket connection fails to be established
void on_fail_plain(client* c, websocketpp::connection_hdl hdl);

void set_connection_options(const WebsocketConnectionOptions& connection_options) override;

public:
/// \brief Creates a new WebsocketPlain object with the providede \p connection_options
explicit WebsocketPlain(const WebsocketConnectionOptions& connection_options);
Expand Down
9 changes: 3 additions & 6 deletions include/ocpp/common/websocket/websocket_tls.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>

#include <ocpp/common/evse_security.hpp>
#include <ocpp/common/websocket/websocket_base.hpp>

namespace ocpp {
Expand All @@ -25,12 +26,6 @@ class WebsocketTLS final : public WebsocketBase {
tls_client wss_client;
std::shared_ptr<EvseSecurity> evse_security;
websocketpp::lib::shared_ptr<websocketpp::lib::thread> websocket_thread;

/// \brief Extracts the hostname from the provided \p uri
/// FIXME(kai): this only works with a very limited subset of hostnames and should be extended to work spec conform
/// \returns the extracted hostname
std::string get_hostname(std::string uri);

/// \brief Called when a TLS websocket connection gets initialized, manages the supported TLS versions, cipher lists
/// and how verification of the server certificate is handled
tls_context on_tls_init(std::string hostname, websocketpp::connection_hdl hdl, int32_t security_profile);
Expand All @@ -50,6 +45,8 @@ class WebsocketTLS final : public WebsocketBase {
/// \brief Called when a TLS websocket connection fails to be established
void on_fail_tls(tls_client* c, websocketpp::connection_hdl hdl);

void set_connection_options(const WebsocketConnectionOptions& connection_options) override;

public:
/// \brief Creates a new Websocket object with the providede \p connection_options
explicit WebsocketTLS(const WebsocketConnectionOptions& connection_options,
Expand Down
73 changes: 73 additions & 0 deletions include/ocpp/common/websocket/websocket_uri.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef OCPP_WEBSOCKET_URI_HPP
#define OCPP_WEBSOCKET_URI_HPP

#include <string>
#include <string_view>
#include <websocketpp/uri.hpp>

namespace ocpp {

class Uri {
public:
Uri(){};

// clang-format off
/// \brief parse_and_validate parses the \p uri and checks
/// 1. the general validity of it and
/// 2. if optional scheme fits to given \p security_profile
///
/// \param uri The whole URI with optional scheme and \p chargepoint_id as last segment (as backward-compatibility).
/// \param chargepoint_id The identifier unique to the CSMS.
/// \param security_profile The security-profile.
/// \returns Uri
/// \throws std::invalid_argument for several checks
// clang-format on
static Uri parse_and_validate(std::string uri, std::string chargepoint_id, int security_profile);

/// \brief set_secure defines if the connection is done via TLS
///
/// \param secure true: connect via TLS; false: connect as plaintext
void set_secure(bool secure) {
this->secure = secure;
}

std::string get_hostname() {
return this->host;
}
std::string get_chargepoint_id() {
return this->chargepoint_id;
}

std::string string() {
auto uri = get_websocketpp_uri();
return uri.str();
}

websocketpp::uri get_websocketpp_uri() { // FIXME: wrap needed `websocketpp:uri` functionality inside `Uri`
return websocketpp::uri(this->secure, this->host, this->port,
this->path_without_chargepoint_id /* is normalized with ending slash */ +
this->chargepoint_id);
}

private:
Uri(bool secure, const std::string& host, uint16_t port, const std::string& path_without_chargepoint_id,
const std::string& chargepoint_id) :
secure(secure),
host(host),
port(port),
path_without_chargepoint_id(path_without_chargepoint_id),
chargepoint_id(chargepoint_id) {
}

bool secure;
std::string host;
uint16_t port;
std::string path_without_chargepoint_id;
std::string chargepoint_id;
};

} // namespace ocpp

#endif // OCPP_WEBSOCKET_URI_HPP */
7 changes: 7 additions & 0 deletions lib/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# OCPP library
add_library(ocpp)
add_library(everest::ocpp ALIAS ocpp)

target_compile_options(ocpp
PRIVATE
#-Werror # turn warnings into errors
-Wimplicit-fallthrough # avoid unintended fallthroughs
)

target_sources(ocpp
PRIVATE
ocpp/common/call_types.cpp
Expand Down
1 change: 1 addition & 0 deletions lib/ocpp/common/websocket/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
target_sources(ocpp
PRIVATE
websocket_base.cpp
websocket_uri.cpp
websocket_plain.cpp
websocket_tls.cpp
websocket.cpp
Expand Down
2 changes: 0 additions & 2 deletions lib/ocpp/common/websocket/websocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ Websocket::Websocket(const WebsocketConnectionOptions& connection_options, std::
std::shared_ptr<MessageLogging> logging) :
logging(logging) {
if (connection_options.security_profile <= 1) {
EVLOG_debug << "Creating plaintext websocket based on the provided URI: " << connection_options.cs_uri;
this->websocket = std::make_unique<WebsocketPlain>(connection_options);
} else if (connection_options.security_profile >= 2) {
EVLOG_debug << "Creating TLS websocket based on the provided URI: " << connection_options.cs_uri;
this->websocket = std::make_unique<WebsocketTLS>(connection_options, evse_security);
}
}
Expand Down
11 changes: 7 additions & 4 deletions lib/ocpp/common/websocket/websocket_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
#include <ocpp/common/websocket/websocket_base.hpp>
namespace ocpp {

WebsocketBase::WebsocketBase(const WebsocketConnectionOptions& connection_options) :
WebsocketBase::WebsocketBase() :
m_is_connected(false),
connection_options(connection_options),
connected_callback(nullptr),
closed_callback(nullptr),
message_callback(nullptr),
Expand All @@ -17,6 +16,9 @@ WebsocketBase::WebsocketBase(const WebsocketConnectionOptions& connection_option
reconnect_backoff_ms(0),
shutting_down(false),
reconnecting(false) {

set_connection_options_base(connection_options);

this->ping_timer = std::make_unique<Everest::SteadyTimer>();
const auto auth_key = connection_options.authorization_key;
if (auth_key.has_value() and auth_key.value().length() < 16) {
Expand All @@ -28,7 +30,7 @@ WebsocketBase::WebsocketBase(const WebsocketConnectionOptions& connection_option
WebsocketBase::~WebsocketBase() {
}

void WebsocketBase::set_connection_options(const WebsocketConnectionOptions& connection_options) {
void WebsocketBase::set_connection_options_base(const WebsocketConnectionOptions& connection_options) {
this->connection_attempts = 0;
this->connection_options = connection_options;
}
Expand Down Expand Up @@ -95,7 +97,8 @@ std::optional<std::string> WebsocketBase::getAuthorizationHeader() {
const auto authorization_key = this->connection_options.authorization_key;
if (authorization_key.has_value()) {
EVLOG_debug << "AuthorizationKey present, encoding authentication header";
std::string plain_auth_header = this->connection_options.chargepoint_id + ":" + authorization_key.value();
std::string plain_auth_header =
this->connection_options.csms_uri.get_chargepoint_id() + ":" + authorization_key.value();
auth_header.emplace(std::string("Basic ") + websocketpp::base64_encode(plain_auth_header));
EVLOG_debug << "Basic Auth header: " << auth_header.value();
}
Expand Down
44 changes: 33 additions & 11 deletions lib/ocpp/common/websocket/websocket_plain.cpp
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/logging.hpp>

#include <ocpp/common/types.hpp>
#include <ocpp/common/websocket/websocket_plain.hpp>

#include <everest/logging.hpp>

#include <boost/optional/optional.hpp>

#include <memory>
#include <stdexcept>

namespace ocpp {

WebsocketPlain::WebsocketPlain(const WebsocketConnectionOptions& connection_options) :
WebsocketBase(connection_options) {
WebsocketPlain::WebsocketPlain(const WebsocketConnectionOptions& connection_options) : WebsocketBase() {
set_connection_options(connection_options);

EVLOG_debug << "Initialised WebsocketPlain with URI: " << this->connection_options.csms_uri.string();
}

void WebsocketPlain::set_connection_options(const WebsocketConnectionOptions& connection_options) {
switch (connection_options.security_profile) { // `switch` used to lint on missing enum-values
case security::SecurityProfile::OCPP_1_6_ONLY_UNSECURED_TRANSPORT_WITHOUT_BASIC_AUTHENTICATION:
case security::SecurityProfile::UNSECURED_TRANSPORT_WITH_BASIC_AUTHENTICATION:
break;
case security::SecurityProfile::TLS_WITH_BASIC_AUTHENTICATION:
case security::SecurityProfile::TLS_WITH_CLIENT_SIDE_CERTIFICATES:
throw std::invalid_argument("`security_profile` is not a plain, unsecured one.");
default:
throw std::invalid_argument("unknown `security_profile`, value = " +
std::to_string(connection_options.security_profile));
}

set_connection_options_base(connection_options);
this->connection_options.csms_uri.set_secure(false);
}

bool WebsocketPlain::connect() {
if (!this->initialized()) {
return false;
}
const auto uri = this->connection_options.cs_uri.insert(0, "ws://");

EVLOG_info << "Connecting to plain websocket at uri: " << uri
<< " with profile: " << this->connection_options.security_profile;
EVLOG_info << "Connecting to plain websocket at uri: " << this->connection_options.csms_uri.string()
<< " with security profile: " << this->connection_options.security_profile;

this->ws_client.clear_access_channels(websocketpp::log::alevel::all);
this->ws_client.clear_error_channels(websocketpp::log::elevel::all);
this->ws_client.init_asio();
this->ws_client.start_perpetual();
this->uri = uri;

websocket_thread.reset(new websocketpp::lib::thread(&client::run, &this->ws_client));

this->reconnect_callback = [this](const websocketpp::lib::error_code& ec) {
EVLOG_info << "Reconnecting to plain websocket at uri: " << this->connection_options.cs_uri
<< " with profile: " << this->connection_options.security_profile;
EVLOG_info << "Reconnecting to plain websocket at uri: " << this->connection_options.csms_uri.string()
<< " with security profile: " << this->connection_options.security_profile;

// close connection before reconnecting
if (this->m_is_connected) {
Expand Down Expand Up @@ -120,7 +141,8 @@ void WebsocketPlain::connect_plain() {

websocketpp::lib::error_code ec;

client::connection_ptr con = this->ws_client.get_connection(this->uri, ec);
const client::connection_ptr con = this->ws_client.get_connection(
std::make_shared<websocketpp::uri>(this->connection_options.csms_uri.get_websocketpp_uri()), ec);

if (ec) {
EVLOG_error << "Connection initialization error for plain websocket: " << ec.message();
Expand Down
Loading

0 comments on commit 271e025

Please sign in to comment.