Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate CSMS-URL: opt. schema must fit with security-profile; chargepoint-ID deprecated in URL #201

Merged
merged 52 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
d590bae
new `helpers::URIAppendPath()`
Dominik-K Sep 28, 2023
28a3d52
append chargepoint ID to CSMS-URI
Dominik-K Sep 28, 2023
ad9f9dc
rename `cs_uri` to `csms_uri`
Dominik-K Sep 28, 2023
ed1495c
fix in `websocket_plain.cpp`; consolidate both to avoid copy and past…
Dominik-K Sep 28, 2023
ec0d20d
move `Uri`-struct to `websocket_common.cpp`
Dominik-K Sep 29, 2023
5a7103f
v201: use `SecurityCtrlrIdentity` instead `ChargePointId`
Dominik-K Oct 4, 2023
54e4f1c
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Oct 4, 2023
6062783
merge fix
Dominik-K Oct 4, 2023
f2d05dd
use `set_path()`, use `websocketpp` with workaround
Dominik-K Oct 13, 2023
fb54d8f
transfer `uri` around to avoid re-parsing
Dominik-K Oct 16, 2023
d4323ed
rename to `websocket_uri.*pp`
Dominik-K Oct 18, 2023
977c6d1
reworked with static `Uri::parse_from_string()`
Dominik-K Oct 18, 2023
88b8abd
use `set_connection_options()` generically
Dominik-K Oct 18, 2023
606a26b
`find . -iname '*.*pp' | xargs clang-format -i`
Dominik-K Oct 18, 2023
3143cba
fix linter issues
Dominik-K Oct 18, 2023
bbfafbf
Codacy & own PR-reviews
Dominik-K Oct 19, 2023
8b5b665
`clang-format` again :-(
Dominik-K Oct 19, 2023
79994bb
make Codacy happy again!?!
Dominik-K Oct 19, 2023
1651642
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Oct 23, 2023
f2f3f0a
remove `typedef Chargepoint`
Dominik-K Oct 23, 2023
534ae87
`WebsocketConnectionOptions.Uri`
Dominik-K Oct 23, 2023
7fb19ca
init `Uri` before adding to `WebsocketConnectionOptions`
Dominik-K Oct 23, 2023
1008d05
run `set_connection_options()` in `Websocket...` constructor
Dominik-K Oct 23, 2023
2ceec0d
single source for `chargepoint_id`
Dominik-K Oct 23, 2023
a4e1cca
check against security-profile at `set_connection_options()`
Dominik-K Oct 23, 2023
161ee4e
check conforming URI-scheme and security-profile
Dominik-K Oct 23, 2023
76aa1e7
make `clang-format` happy again
Dominik-K Oct 23, 2023
87ef3ef
fixes from review
Dominik-K Oct 27, 2023
afd502a
Merge branch 'fix/websockets/CP-ID' into websockets_options_URI
Dominik-K Oct 30, 2023
6f1e600
rename to singural form; TODO for using `SecurityProfile`-type
Dominik-K Oct 30, 2023
1250380
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Oct 31, 2023
acd942b
Merge branch 'fix/websockets/CP-ID' into websockets_options_URI
Dominik-K Oct 31, 2023
a919540
WIP: check CP-ID against path-base instead of full path
Dominik-K Oct 31, 2023
3bd6d62
remove CP-ID from path if last segment
Dominik-K Nov 2, 2023
8b8e269
WIP: check OCPP1.6 with security-profile=1
Dominik-K Nov 2, 2023
fbfcae7
remove optional last slash
Dominik-K Nov 3, 2023
39ddcc4
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Nov 6, 2023
bf9bdc6
fix correct path splitting
Dominik-K Nov 6, 2023
f5f0091
fix fallthrough in switch
Dominik-K Nov 6, 2023
75a1705
1.6: connection to SteVe working
Dominik-K Nov 6, 2023
fe08ff2
path normalization
Dominik-K Nov 7, 2023
30e81e2
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Nov 7, 2023
3af51e3
(merge) fixes
Dominik-K Nov 7, 2023
e9263b1
make clang-format happy again
Dominik-K Nov 8, 2023
11c0a93
re-format touched JSONs with new `.editorconfig`
Dominik-K Nov 8, 2023
2bfa19e
fix doubled `last_segment`
Dominik-K Nov 8, 2023
151dcd6
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Nov 9, 2023
2cb761d
use Doxygen comments
Dominik-K Nov 9, 2023
ae3922a
allow `SecurityProfile = 0` for OCPP 1.6
Dominik-K Nov 9, 2023
5375b55
require `SecurityCtrlr.Identity`
Dominik-K Nov 9, 2023
5295347
Merge branch 'main' into fix/websockets/CP-ID
Dominik-K Nov 9, 2023
bcc88df
make `clang-format` happy again
Dominik-K Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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