summaryrefslogtreecommitdiffstats
path: root/src/lib/dhcp_ddns/ncr_msg.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/dhcp_ddns/ncr_msg.cc')
-rw-r--r--src/lib/dhcp_ddns/ncr_msg.cc696
1 files changed, 696 insertions, 0 deletions
diff --git a/src/lib/dhcp_ddns/ncr_msg.cc b/src/lib/dhcp_ddns/ncr_msg.cc
new file mode 100644
index 0000000..cc595b4
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_msg.cc
@@ -0,0 +1,696 @@
+// Copyright (C) 2013-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcp_ddns/ncr_msg.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_error.h>
+#include <cryptolink/cryptolink.h>
+#include <cryptolink/crypto_hash.h>
+#include <util/encode/hex.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <sstream>
+#include <limits>
+
+
+namespace isc {
+namespace dhcp_ddns {
+
+
+NameChangeFormat stringToNcrFormat(const std::string& fmt_str) {
+ if (boost::iequals(fmt_str, "JSON")) {
+ return FMT_JSON;
+ }
+
+ isc_throw(BadValue, "Invalid NameChangeRequest format: " << fmt_str);
+}
+
+
+std::string ncrFormatToString(NameChangeFormat format) {
+ if (format == FMT_JSON) {
+ return ("JSON");
+ }
+
+ std::ostringstream stream;
+ stream << "UNKNOWN(" << format << ")";
+ return (stream.str());
+}
+
+/********************************* D2Dhcid ************************************/
+
+namespace {
+
+///
+/// @name Constants which define DHCID identifier-type
+//@{
+/// DHCID created from client's HW address.
+const uint8_t DHCID_ID_HWADDR = 0x0;
+/// DHCID created from client identifier.
+const uint8_t DHCID_ID_CLIENTID = 0x1;
+/// DHCID created from DUID.
+const uint8_t DHCID_ID_DUID = 0x2;
+
+}
+
+D2Dhcid::D2Dhcid() {
+}
+
+D2Dhcid::D2Dhcid(const std::string& data) {
+ fromStr(data);
+}
+
+D2Dhcid::D2Dhcid(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromHWAddr(hwaddr, wire_fqdn);
+}
+
+D2Dhcid::D2Dhcid(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromClientId(clientid_data, wire_fqdn);
+}
+
+D2Dhcid::D2Dhcid(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromDUID(duid, wire_fqdn);
+}
+
+
+void
+D2Dhcid::fromStr(const std::string& data) {
+ bytes_.clear();
+ try {
+ isc::util::encode::decodeHex(data, bytes_);
+ } catch (const isc::Exception& ex) {
+ isc_throw(NcrMessageError, "Invalid data in Dhcid: " << ex.what());
+ }
+}
+
+std::string
+D2Dhcid::toStr() const {
+ return (isc::util::encode::encodeHex(bytes_));
+}
+
+void
+D2Dhcid::fromClientId(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ // IPv4 Client ID containing a DUID looks like this in RFC4361
+ // Type IAID DUID
+ // +-----+----+----+----+----+----+----+---
+ // | 255 | i1 | i2 | i3 | i4 | d1 | d2 |...
+ // +-----+----+----+----+----+----+----+---
+ if (!clientid_data.empty() && clientid_data[0] == 255) {
+ if (clientid_data.size() <= 5) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from client identifier, embedded DUID "
+ "length of: " << clientid_data.size() << ", is too short");
+ }
+ // RFC3315 states that the DUID is a type code of 2 octets followed
+ // by no more then 128 octets. So add the 5 from above and make sure
+ // the length is not too long.
+ if (clientid_data.size() > 135) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from client identifier, embedded DUID "
+ "length of: " << clientid_data.size() << ", is too long");
+ }
+ std::vector<uint8_t>::const_iterator start = clientid_data.begin() + 5;
+ std::vector<uint8_t>::const_iterator end = clientid_data.end();
+ std::vector<uint8_t> duid(start, end);
+ createDigest(DHCID_ID_DUID, duid, wire_fqdn);
+ } else {
+ createDigest(DHCID_ID_CLIENTID, clientid_data, wire_fqdn);
+ }
+}
+
+void
+D2Dhcid::fromHWAddr(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn) {
+ if (!hwaddr) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from the HW address, "
+ "NULL pointer has been specified");
+ } else if (hwaddr->hwaddr_.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from the HW address, "
+ "HW address is empty");
+ }
+ std::vector<uint8_t> hwaddr_data;
+ hwaddr_data.push_back(hwaddr->htype_);
+ hwaddr_data.insert(hwaddr_data.end(), hwaddr->hwaddr_.begin(),
+ hwaddr->hwaddr_.end());
+ createDigest(DHCID_ID_HWADDR, hwaddr_data, wire_fqdn);
+}
+
+
+void
+D2Dhcid::fromDUID(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn) {
+
+ createDigest(DHCID_ID_DUID, duid.getDuid(), wire_fqdn);
+}
+
+void
+D2Dhcid::createDigest(const uint8_t identifier_type,
+ const std::vector<uint8_t>& identifier_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ // We get FQDN in the wire format, so we don't know if it is
+ // valid. It is caller's responsibility to make sure it is in
+ // the valid format. Here we just make sure it is not empty.
+ if (wire_fqdn.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "empty FQDN used to create DHCID");
+ }
+
+ // It is a responsibility of the classes which encapsulate client
+ // identifiers, e.g. DUID, to validate the client identifier data.
+ // But let's be on the safe side and at least check that it is not
+ // empty.
+ if (identifier_data.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "empty DUID used to create DHCID");
+ }
+
+ // A data buffer will be used to compute the digest.
+ std::vector<uint8_t> data = identifier_data;
+
+ // Append FQDN in the wire format.
+ data.insert(data.end(), wire_fqdn.begin(), wire_fqdn.end());
+
+ // Use the DUID and FQDN to compute the digest (see RFC4701, section 3).
+
+ isc::util::OutputBuffer hash(0);
+ try {
+ // We have checked already that the DUID and FQDN aren't empty
+ // so it is safe to assume that the data buffer is not empty.
+ cryptolink::digest(&data[0], data.size(), cryptolink::SHA256, hash);
+ } catch (const std::exception& ex) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "error while generating DHCID from DUID: "
+ << ex.what());
+ }
+
+ // The DHCID RDATA has the following structure:
+ //
+ // < identifier-type > < digest-type > < digest >
+ //
+ // where identifier type
+
+ // Let's allocate the space for the identifier-type (2 bytes) and
+ // digest-type (1 byte). This is 3 bytes all together.
+ bytes_.resize(3 + hash.getLength());
+ // Leave first byte 0 and set the second byte. Those two bytes
+ // form the identifier-type.
+ bytes_[1] = identifier_type;
+ // Third byte is always equal to 1, which specifies SHA-256 digest type.
+ bytes_[2] = 1;
+ // Now let's append the digest.
+ std::memcpy(&bytes_[3], hash.getData(), hash.getLength());
+}
+
+std::ostream&
+operator<<(std::ostream& os, const D2Dhcid& dhcid) {
+ os << dhcid.toStr();
+ return (os);
+}
+
+
+
+/**************************** NameChangeRequest ******************************/
+
+NameChangeRequest::NameChangeRequest()
+ : change_type_(CHG_ADD), forward_change_(false),
+ reverse_change_(false), fqdn_(""), ip_io_address_("0.0.0.0"),
+ dhcid_(), lease_expires_on_(), lease_length_(0), conflict_resolution_(true),
+ status_(ST_NEW) {
+}
+
+NameChangeRequest::NameChangeRequest(const NameChangeType change_type,
+ const bool forward_change, const bool reverse_change,
+ const std::string& fqdn, const std::string& ip_address,
+ const D2Dhcid& dhcid,
+ const uint64_t lease_expires_on,
+ const uint32_t lease_length,
+ const bool conflict_resolution)
+ : change_type_(change_type), forward_change_(forward_change),
+ reverse_change_(reverse_change), fqdn_(fqdn), ip_io_address_("0.0.0.0"),
+ dhcid_(dhcid), lease_expires_on_(lease_expires_on),
+ lease_length_(lease_length), conflict_resolution_(conflict_resolution),
+ status_(ST_NEW) {
+
+ // User setter to validate fqdn.
+ setFqdn(fqdn);
+
+ // User setter to validate address.
+ setIpAddress(ip_address);
+
+ // Validate the contents. This will throw a NcrMessageError if anything
+ // is invalid.
+ validateContent();
+}
+
+NameChangeRequestPtr
+NameChangeRequest::fromFormat(const NameChangeFormat format,
+ isc::util::InputBuffer& buffer) {
+ // Based on the format requested, pull the marshalled request from
+ // InputBuffer and pass it into the appropriate format-specific factory.
+ NameChangeRequestPtr ncr;
+ switch (format) {
+ case FMT_JSON: {
+ try {
+ // Get the length of the JSON text.
+ size_t len = buffer.readUint16();
+
+ // Read the text from the buffer into a vector.
+ std::vector<uint8_t> vec;
+ buffer.readVector(vec, len);
+
+ // Turn the vector into a string.
+ std::string string_data(vec.begin(), vec.end());
+
+ // Pass the string of JSON text into JSON factory to create the
+ // NameChangeRequest instance. Note the factory may throw
+ // NcrMessageError.
+ ncr = NameChangeRequest::fromJSON(string_data);
+ } catch (const isc::util::InvalidBufferPosition& ex) {
+ // Read error accessing data in InputBuffer.
+ isc_throw(NcrMessageError, "fromFormat: buffer read error: "
+ << ex.what());
+ }
+
+ break;
+ }
+ default:
+ // Programmatic error, shouldn't happen.
+ isc_throw(NcrMessageError, "fromFormat - invalid format");
+ break;
+ }
+
+ return (ncr);
+}
+
+void
+NameChangeRequest::toFormat(const NameChangeFormat format,
+ isc::util::OutputBuffer& buffer) const {
+ // Based on the format requested, invoke the appropriate format handler
+ // which will marshal this request's contents into the OutputBuffer.
+ switch (format) {
+ case FMT_JSON: {
+ // Invoke toJSON to create a JSON text of this request's contents.
+ std::string json = toJSON();
+ uint16_t length = json.size();
+
+ // Write the length of the JSON text to the OutputBuffer first, then
+ // write the JSON text itself.
+ buffer.writeUint16(length);
+ buffer.writeData(json.c_str(), length);
+ break;
+ }
+ default:
+ // Programmatic error, shouldn't happen.
+ isc_throw(NcrMessageError, "toFormat - invalid format");
+ break;
+ }
+}
+
+NameChangeRequestPtr
+NameChangeRequest::fromJSON(const std::string& json) {
+ // This method leverages the existing JSON parsing provided by isc::data
+ // library. Should this prove to be a performance issue, it may be that
+ // lighter weight solution would be appropriate.
+
+ // Turn the string of JSON text into an Element set.
+ isc::data::ElementPtr elements;
+ try {
+ elements = isc::data::Element::fromJSON(json);
+ } catch (const isc::data::JSONError& ex) {
+ isc_throw(NcrMessageError,
+ "Malformed NameChangeRequest JSON: " << ex.what());
+ }
+
+ // Get a map of the Elements, keyed by element name.
+ ElementMap element_map = elements->mapValue();
+ isc::data::ConstElementPtr element;
+
+ // Use default constructor to create a "blank" NameChangeRequest.
+ NameChangeRequestPtr ncr(new NameChangeRequest());
+
+ // For each member of NameChangeRequest, find its element in the map and
+ // call the appropriate Element-based setter. These setters may throw
+ // NcrMessageError if the given Element is the wrong type or its data
+ // content is lexically invalid. If the element is NOT found in the
+ // map, getElement will throw NcrMessageError indicating the missing
+ // member.
+ element = ncr->getElement("change-type", element_map);
+ ncr->setChangeType(element);
+
+ element = ncr->getElement("forward-change", element_map);
+ ncr->setForwardChange(element);
+
+ element = ncr->getElement("reverse-change", element_map);
+ ncr->setReverseChange(element);
+
+ element = ncr->getElement("fqdn", element_map);
+ ncr->setFqdn(element);
+
+ element = ncr->getElement("ip-address", element_map);
+ ncr->setIpAddress(element);
+
+ element = ncr->getElement("dhcid", element_map);
+ ncr->setDhcid(element);
+
+ element = ncr->getElement("lease-expires-on", element_map);
+ ncr->setLeaseExpiresOn(element);
+
+ element = ncr->getElement("lease-length", element_map);
+ ncr->setLeaseLength(element);
+
+ // For backward compatibility use-conflict-resolution is optional
+ // and defaults to true.
+ auto found = element_map.find("use-conflict-resolution");
+ if (found != element_map.end()) {
+ ncr->setConflictResolution(found->second);
+ } else {
+ ncr->setConflictResolution(true);
+ }
+
+ // All members were in the Element set and were correct lexically. Now
+ // validate the overall content semantically. This will throw an
+ // NcrMessageError if anything is amiss.
+ ncr->validateContent();
+
+ // Everything is valid, return the new instance.
+ return (ncr);
+}
+
+std::string
+NameChangeRequest::toJSON() const {
+ // Create a JSON string of this request's contents. Note that this method
+ // does NOT use the isc::data library as generating the output is straight
+ // forward.
+ std::ostringstream stream;
+
+ stream << "{\"change-type\":" << getChangeType() << ","
+ << "\"forward-change\":"
+ << (isForwardChange() ? "true" : "false") << ","
+ << "\"reverse-change\":"
+ << (isReverseChange() ? "true" : "false") << ","
+ << "\"fqdn\":\"" << getFqdn() << "\","
+ << "\"ip-address\":\"" << getIpAddress() << "\","
+ << "\"dhcid\":\"" << getDhcid().toStr() << "\","
+ << "\"lease-expires-on\":\"" << getLeaseExpiresOnStr() << "\","
+ << "\"lease-length\":" << getLeaseLength() << ","
+ << "\"use-conflict-resolution\":"
+ << (useConflictResolution() ? "true" : "false") << "}";
+
+ return (stream.str());
+}
+
+
+void
+NameChangeRequest::validateContent() {
+ //@todo This is an initial implementation which provides a minimal amount
+ // of validation. FQDN and DHCID members are all currently
+ // strings, these may be replaced with richer classes.
+ if (fqdn_ == "") {
+ isc_throw(NcrMessageError, "FQDN cannot be blank");
+ }
+
+ // Validate the DHCID.
+ if (dhcid_.getBytes().size() == 0) {
+ isc_throw(NcrMessageError, "DHCID cannot be blank");
+ }
+
+ // Ensure the request specifies at least one direction to update.
+ if (!forward_change_ && !reverse_change_) {
+ isc_throw(NcrMessageError,
+ "Invalid Request, forward and reverse flags are both false");
+ }
+}
+
+isc::data::ConstElementPtr
+NameChangeRequest::getElement(const std::string& name,
+ const ElementMap& element_map) const {
+ // Look for "name" in the element map.
+ ElementMap::const_iterator it = element_map.find(name);
+ if (it == element_map.end()) {
+ // Didn't find the element, so throw.
+ isc_throw(NcrMessageError,
+ "NameChangeRequest value missing for: " << name );
+ }
+
+ // Found the element, return it.
+ return (it->second);
+}
+
+void
+NameChangeRequest::setChangeType(const NameChangeType value) {
+ change_type_ = value;
+}
+
+
+void
+NameChangeRequest::setChangeType(isc::data::ConstElementPtr element) {
+ long raw_value = -1;
+ try {
+ // Get the element's integer value.
+ raw_value = element->intValue();
+ } catch (const isc::data::TypeError& ex) {
+ // We expect a integer Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for change_type: " << ex.what());
+ }
+
+ if ((raw_value != CHG_ADD) && (raw_value != CHG_REMOVE)) {
+ // Value is not a valid change type.
+ isc_throw(NcrMessageError,
+ "Invalid data value for change_type: " << raw_value);
+ }
+
+ // Good to go, make the assignment.
+ setChangeType(static_cast<NameChangeType>(raw_value));
+}
+
+void
+NameChangeRequest::setForwardChange(const bool value) {
+ forward_change_ = value;
+}
+
+void
+NameChangeRequest::setForwardChange(isc::data::ConstElementPtr element) {
+ bool value;
+ try {
+ // Get the element's boolean value.
+ value = element->boolValue();
+ } catch (const isc::data::TypeError& ex) {
+ // We expect a boolean Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for forward-change: " << ex.what());
+ }
+
+ // Good to go, make the assignment.
+ setForwardChange(value);
+}
+
+void
+NameChangeRequest::setReverseChange(const bool value) {
+ reverse_change_ = value;
+}
+
+void
+NameChangeRequest::setReverseChange(isc::data::ConstElementPtr element) {
+ bool value;
+ try {
+ // Get the element's boolean value.
+ value = element->boolValue();
+ } catch (const isc::data::TypeError& ex) {
+ // We expect a boolean Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for reverse_change: " << ex.what());
+ }
+
+ // Good to go, make the assignment.
+ setReverseChange(value);
+}
+
+
+void
+NameChangeRequest::setFqdn(isc::data::ConstElementPtr element) {
+ setFqdn(element->stringValue());
+}
+
+void
+NameChangeRequest::setFqdn(const std::string& value) {
+ try {
+ dns::Name tmp(value);
+ fqdn_ = tmp.toText();
+ } catch (const std::exception& ex) {
+ isc_throw(NcrMessageError,
+ "Invalid FQDN value: " << value << ", reason: "
+ << ex.what());
+ }
+}
+
+void
+NameChangeRequest::setIpAddress(const std::string& value) {
+ // Validate IP Address.
+ try {
+ ip_io_address_ = isc::asiolink::IOAddress(value);
+ } catch (const isc::asiolink::IOError&) {
+ isc_throw(NcrMessageError,
+ "Invalid ip address string for ip_address: " << value);
+ }
+}
+
+void
+NameChangeRequest::setIpAddress(isc::data::ConstElementPtr element) {
+ setIpAddress(element->stringValue());
+}
+
+
+void
+NameChangeRequest::setDhcid(const std::string& value) {
+ dhcid_.fromStr(value);
+}
+
+void
+NameChangeRequest::setDhcid(isc::data::ConstElementPtr element) {
+ setDhcid(element->stringValue());
+}
+
+std::string
+NameChangeRequest::getLeaseExpiresOnStr() const {
+ return (isc::util::timeToText64(lease_expires_on_));
+}
+
+void
+NameChangeRequest::setLeaseExpiresOn(const std::string& value) {
+ try {
+ lease_expires_on_ = isc::util::timeFromText64(value);
+ } catch (...) {
+ // We were given an invalid string, so throw.
+ isc_throw(NcrMessageError,
+ "Invalid date-time string: [" << value << "]");
+ }
+
+}
+
+void NameChangeRequest::setLeaseExpiresOn(isc::data::ConstElementPtr element) {
+ // Pull out the string value and pass it into the string setter.
+ setLeaseExpiresOn(element->stringValue());
+}
+
+void
+NameChangeRequest::setLeaseLength(const uint32_t value) {
+ lease_length_ = value;
+}
+
+void
+NameChangeRequest::setLeaseLength(isc::data::ConstElementPtr element) {
+ long value = -1;
+ try {
+ // Get the element's integer value.
+ value = element->intValue();
+ } catch (const isc::data::TypeError& ex) {
+ // We expect a integer Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for lease_length: " << ex.what());
+ }
+
+ // Make sure we the range is correct and value is positive.
+ if (value > std::numeric_limits<uint32_t>::max()) {
+ isc_throw(NcrMessageError, "lease_length value " << value <<
+ "is too large for unsigned 32-bit integer.");
+ }
+ if (value < 0) {
+ isc_throw(NcrMessageError, "lease_length value " << value <<
+ "is negative. It must greater than or equal to zero ");
+ }
+
+ // Good to go, make the assignment.
+ setLeaseLength(static_cast<uint32_t>(value));
+}
+
+void
+NameChangeRequest::setConflictResolution(const bool value) {
+ conflict_resolution_ = value;
+}
+
+void
+NameChangeRequest::setConflictResolution(isc::data::ConstElementPtr element) {
+ bool value;
+ try {
+ // Get the element's boolean value.
+ value = element->boolValue();
+ } catch (const isc::data::TypeError& ex) {
+ // We expect a boolean Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for use-conflict-resolution: " << ex.what());
+ }
+
+ // Good to go, make the assignment.
+ setConflictResolution(value);
+}
+
+void
+NameChangeRequest::setStatus(const NameChangeStatus value) {
+ status_ = value;
+}
+
+std::string
+NameChangeRequest::toText() const {
+ std::ostringstream stream;
+
+ stream << "Type: " << static_cast<int>(change_type_) << " (";
+ switch (change_type_) {
+ case CHG_ADD:
+ stream << "CHG_ADD)\n";
+ break;
+ case CHG_REMOVE:
+ stream << "CHG_REMOVE)\n";
+ break;
+ default:
+ // Shouldn't be possible.
+ stream << "Invalid Value\n";
+ }
+
+ stream << "Forward Change: " << (forward_change_ ? "yes" : "no")
+ << std::endl
+ << "Reverse Change: " << (reverse_change_ ? "yes" : "no")
+ << std::endl
+ << "FQDN: [" << fqdn_ << "]" << std::endl
+ << "IP Address: [" << ip_io_address_ << "]" << std::endl
+ << "DHCID: [" << dhcid_.toStr() << "]" << std::endl
+ << "Lease Expires On: " << getLeaseExpiresOnStr() << std::endl
+ << "Lease Length: " << lease_length_ << std::endl
+ << "Conflict Resolution: " << (conflict_resolution_ ? "yes" : "no")
+ << std::endl;
+
+ return (stream.str());
+}
+
+bool
+NameChangeRequest::operator == (const NameChangeRequest& other) {
+ return ((change_type_ == other.change_type_) &&
+ (forward_change_ == other.forward_change_) &&
+ (reverse_change_ == other.reverse_change_) &&
+ (fqdn_ == other.fqdn_) &&
+ (ip_io_address_ == other.ip_io_address_) &&
+ (dhcid_ == other.dhcid_) &&
+ (lease_expires_on_ == other.lease_expires_on_) &&
+ (lease_length_ == other.lease_length_) &&
+ (conflict_resolution_ == other.conflict_resolution_));
+}
+
+bool
+NameChangeRequest::operator != (const NameChangeRequest& other) {
+ return (!(*this == other));
+}
+
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace