Skip to content

Commit

Permalink
Merge pull request #771 from telefonicaid/task/add_body_parser_xml
Browse files Browse the repository at this point in the history
 read measures in application/soap+xml content-type
  • Loading branch information
mapedraza authored Nov 16, 2023
2 parents 3e752f4 + 72bfd1c commit 25e82b8
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGES_NEXT_RELEASE
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Add: allow read measures in application/soap+xml content-type (#759)
- Fix: binary data representation when sending data through HTTP & MQTT (#690)
- Fix: ensure service and subservice from device in logs about error proccesing message
- Remove: legacy code about unused parsedMessageError flag
127 changes: 100 additions & 27 deletions docs/usermanual.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ following query parameters:
It is possible to send a single measure to IoT Platform using an HTTP POST request to the
`/iot/json/attrs/<attributeName>` and the previously explained query parameters.

In this case, sending a single measure, there is possible to send other kinds of payloads like `text/plain` and
`application/octet-stream`, not just `application/json`. In case of using `application/octet-stream`, data will be
treated as binary data, saved in the attribute maped as hex string. I.E:
In this case, sending a single measure, there is possible to send other kinds of payloads like `text/plain`,
`application/octet-stream` and `application/soap+xml`, not just `application/json`. In case of using
`application/octet-stream`, data will be treated as binary data, saved in the attribute maped as hex string. I.E:

For a measure sent to `POST /iot/json/attrs/attrHex` with content-type: application/octet-stream and binary value

Expand All @@ -98,17 +98,95 @@ hello

then the resulting attribute sent to ContextBroker:

{
...
"attrHex": {
"value": "68656c6c6f"
"type": "<the one used at provisiong time for attrHex attribute>"
}
}
{ ... "attrHex": { "value": "68656c6c6f", "type": "<the one used at provisiong time for attrHex attribute>" } }

Note that every group of 2 character (I.E, the first group, `68`) corresponds to a single ASCII character or byte
received in the payload (in this case, the value `0x68` corresponds to `h` in ASCII). You can use one of the multiple
tools available online like [this one](https://string-functions.com/string-hex.aspx)

##### SOAP-XML Measure reporting

In case of `POST /iot/json/attrs/myAttr` with content-type `application/soap+xml` a measure like:

```
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soapenv:Header xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"/>
<soapenv:Body xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope">
<ns21:notificationEventRequest xmlns:ns21="http://myurl.com">
<ns21:Param1>ABC12345</ns21:Param1>
<ns21:Param2/>
<ns21:Date>28/09/2023 11:48:15 +0000</ns21:Date>
<ns21:NestedAttr>
<ns21:SubAttr>This is a description</ns21:SubAttr>
</ns21:NestedAttr>
<ns21:Status>Assigned</ns21:Status>
<ns21:OriginSystem/>
</ns21:notificationEventRequest>
</soapenv:Body>
</soap:Envelope>
```

then the resulting attribute `myAttr` sent to context borker:

```
"myAttr": {
"type": "None",
"value": {
"Envelope": {
"$": {
"xmlns:soap": "http://www.w3.org/2003/05/soap-envelope"
},
"Header": [
{
"$": {
"xmlns:soapenv": "http://www.w3.org/2003/05/soap-envelope"
}
}
],
"Body": [
{
"$": {
"xmlns:soapenv": "http://www.w3.org/2003/05/soap-envelope"
},
"notificationEventRequest": [
{
"$": {
"xmlns:ns21": "http://myurl.com"
},
"Param1": [
"ABC12345"
],
"Param2": [
""
],
"Date": [
"28/09/2023 11:48:15 +0000"
],
"NestedAttr": [
{
"SubAttr": [
"This is a description"
]
}
],
"Status": [
"Assigned"
],
"OriginSystem": [
""
]
}
]
}
]
}
}
}
```

Note that every group of 2 character (I.E, the first group, `68`) corresponds to a single ASCII character or byte received in
the payload (in this case, the value `0x68` corresponds to `h` in ASCII). You can use one of the multiple tools available
online like [this one](https://string-functions.com/string-hex.aspx)
Note that XML namespaces might change from one request to the next. It is useful to remove them from the document, to be
able to refer to tags later in JEXL transformations. See
[this issue](https://github.com/Leonidas-from-XIV/node-xml2js/issues/87)

#### Configuration retrieval

Expand Down Expand Up @@ -316,29 +394,24 @@ attribute IDs `h` and `t`, then humidity measures are reported this way:
$ mosquitto_pub -t /json/ABCDEF/id_sen1/attrs/h -m 70 -h <mosquitto_broker> -p <mosquitto_port> -u <user> -P <password>
```

In the single measure case, when the published data is not a valid JSON, it is interpreted as binary content. For instance,
if the following is published to `/json/ABCDEF/id_sen1/attrs/attrHex` topic:
In the single measure case, when the published data is not a valid JSON, it is interpreted as binary content. For
instance, if the following is published to `/json/ABCDEF/id_sen1/attrs/attrHex` topic:

```
hello
```

then the resulting attribute sent to ContextBroker:

{
...
"attrHex": {
"value": "68656c6c6f"
"type": "<the one used at provisiong time for attrHex attribute>"
}
}
{ ... "attrHex": { "value": "68656c6c6f", "type": "<the one used at provisiong time for attrHex attribute>" } }

Note that every group of 2 character (I.E, the first group, `68`) corresponds to a single ASCII character or byte received in
the payload (in this case, the value `0x68` corresponds to `h` in ASCII). You can use one of the multiple tools available
online like [this one](https://string-functions.com/string-hex.aspx).
Note that every group of 2 character (I.E, the first group, `68`) corresponds to a single ASCII character or byte
received in the payload (in this case, the value `0x68` corresponds to `h` in ASCII). You can use one of the multiple
tools available online like [this one](https://string-functions.com/string-hex.aspx).

Note this works differently that in HTTP transport. In HTTP the JSON vs. binary decission is based on `application/octed-stream` `content-type` header.
Given that in MQTT we don't have anything equivalent to HTTP headers, we apply the heuristics of checking for JSON format.
Note this works differently that in HTTP transport. In HTTP the JSON vs. binary decission is based on
`application/octet-stream` `content-type` header. Given that in MQTT we don't have anything equivalent to HTTP headers,
we apply the heuristics of checking for JSON format.

#### Configuration retrieval

Expand Down
28 changes: 26 additions & 2 deletions lib/bindings/HTTPBinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const http = require('http');
const https = require('https');
const commonBindings = require('../commonBindings');
const bodyParser = require('body-parser');
require('body-parser-xml')(bodyParser);
const constants = require('../constants');
let context = {
op: 'IOTAJSON.HTTP.Binding'
Expand All @@ -53,6 +54,19 @@ const { promisify } = require('util');
const json = promisify(bodyParser.json({ strict: false })); // accept anything JSON.parse accepts.
const text = promisify(bodyParser.text());
const raw = promisify(bodyParser.raw());
const xml2js = require('xml2js');
const xmlStripPrefix = xml2js.processors.stripPrefix;
const xml = promisify(bodyParser.xml({
xmlParseOptions: {
// XML namespaces might change from one request to the next.
// It is useful to remove them from the document,
// to be able to refer to tags later in JEXL transformations.
// See https://github.com/Leonidas-from-XIV/node-xml2js/issues/87
tagNameProcessors: [xmlStripPrefix],
attrNameProcessors: [xmlStripPrefix]
}
})
);

function parserBody() {
// generic bodyParser
Expand All @@ -61,6 +75,8 @@ function parserBody() {
text(req, res).then(() => next(), next);
} else if (req.is('application/octet-stream')) {
raw(req, res).then(() => next(), next);
} else if (req.is('application/soap+xml')) {
xml(req, res).then(() => next(), next);
} else {
// req.is('json')
json(req, res).then(() => next(), next);
Expand Down Expand Up @@ -89,8 +105,16 @@ function checkMandatoryParams(queryPayload) {
if (queryPayload && !req.query.d && req.query.getCmd !== '1') {
notFoundParams.push('Payload');
}
if (req.method === 'POST' && !req.is('json') && !req.is('text/plain') && !req.is('application/octet-stream')) {
error = new errors.UnsupportedType('application/json, text/plain, application/octet-stream');
if (
req.method === 'POST' &&
!req.is('json') &&
!req.is('text/plain') &&
!req.is('application/octet-stream') &&
!req.is('application/soap+xml')
) {
error = new errors.UnsupportedType(
'application/json, text/plain, application/octet-stream, application/soap+xml'
);
}

if (notFoundParams.length !== 0) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"amqplib": "~0.5.1",
"async": "2.6.4",
"body-parser": "1.20.0",
"body-parser-xml": "2.0.5",
"dateformat": "3.0.3",
"express": "4.18.1",
"iotagent-node-lib": "https://github.com/telefonicaid/iotagent-node-lib.git#master",
Expand Down
61 changes: 61 additions & 0 deletions test/unit/ngsiv2/HTTP_receive_measures-test3.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,4 +390,65 @@ describe('HTTP: Measure reception ', function () {
});
});
});

describe('When a POST single SOAP/XML measure arrives for the HTTP binding', function () {
var soapReq =
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"> ' +
'<soapenv:Header xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"/> ' +
'<soapenv:Body ' +
'xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"> ' +
'<ns21:notificationEventRequest ' +
'xmlns:ns21="http://myurl.com"> ' +
'<ns21:Param1>ABC12345</ns21:Param1> ' +
'<ns21:Param2/> ' +
'<ns21:Date>28/09/2023 11:48:15 +0000</ns21:Date> ' +
'<ns21:NestedAttr> ' +
'<ns21:SubAttr>This is a description</ns21:SubAttr> ' +
'</ns21:NestedAttr> ' +
'<ns21:Status>Assigned</ns21:Status> ' +
'<ns21:OriginSystem/> ' +
'</ns21:notificationEventRequest> ' +
'</soapenv:Body> ' +
'</soap:Envelope>';
const optionsMeasure = {
url: 'http://localhost:' + config.http.port + '/iot/json/attrs/data',
method: 'POST',
json: false,
body: soapReq,
headers: {
'fiware-service': 'smartgondor',
'fiware-servicepath': '/gardens',
'content-type': 'application/soap+xml'
},
qs: {
i: 'MQTT_2',
k: '1234'
}
};

beforeEach(function () {
contextBrokerMock
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', '/gardens')
.post(
'/v2/entities?options=upsert',
utils.readExampleFile('./test/unit/ngsiv2/contextRequests/singleMeasureSoapXml.json')
)
.reply(204);
});
it('should return a 200 OK with no error', function (done) {
request(optionsMeasure, function (error, result, body) {
should.not.exist(error);
result.statusCode.should.equal(200);
done();
});
});

it('should send its value to the Context Broker', function (done) {
request(optionsMeasure, function (error, result, body) {
contextBrokerMock.done();
done();
});
});
});
});
57 changes: 57 additions & 0 deletions test/unit/ngsiv2/contextRequests/singleMeasureSoapXml.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"id": "Second MQTT Device",
"type": "AnMQTTDevice",
"data": {
"type": "None",
"value": {
"Envelope": {
"$": {
"xmlns:soap": "http://www.w3.org/2003/05/soap-envelope"
},
"Header": [
{
"$": {
"xmlns:soapenv": "http://www.w3.org/2003/05/soap-envelope"
}
}
],
"Body": [
{
"$": {
"xmlns:soapenv": "http://www.w3.org/2003/05/soap-envelope"
},
"notificationEventRequest": [
{
"$": {
"xmlns:ns21": "http://myurl.com"
},
"Param1": [
"ABC12345"
],
"Param2": [
""
],
"Date": [
"28/09/2023 11:48:15 +0000"
],
"NestedAttr": [
{
"SubAttr": [
"This is a description"
]
}
],
"Status": [
"Assigned"
],
"OriginSystem": [
""
]
}
]
}
]
}
}
}
}

0 comments on commit 25e82b8

Please sign in to comment.