Skip to content

Commit

Permalink
Merge pull request metajack#44 from jinroh/master
Browse files Browse the repository at this point in the history
Jabber-RPC module with parsing
  • Loading branch information
flosse committed Dec 30, 2011
2 parents 0a4be9f + 4474c58 commit ae7c1dc
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 18 deletions.
25 changes: 23 additions & 2 deletions rpc/README.md
Expand Up @@ -39,7 +39,7 @@ The plugin prototype is accessible from the `connection.rpc` variable

### Send an RPC

There are three functions to send RPC :
There are four functions to send RPC :

- `sendRequest(id, to, method, params)`
- `method` is the string name of the method to call
Expand All @@ -49,6 +49,8 @@ There are three functions to send RPC :
- `sendError(id, to, code, message)`
- `code` is the number of the error
- `message` is the message describing the error
- `sendXMLElement(id, to, xml)`
- `xml` is the XML Element that will be sent (whether it is properly formed or not)

The parameters `id` and `to` are respectively the id of the request and the the jid of the recipient.

Expand All @@ -58,8 +60,27 @@ It is possible to handle incoming RPCs using the functions :

- `addRequestHandler`
- `addResponseHandler`
- `addXMLHandler`

Both these functions take a handler function as parameter.
The handlers you pass to these functions take different parameters :

```javascript
var responseHandler = function(id, from, result, error) {
// error is a boolean
// it is true if the response was an error message
if (error === true) { ... }
else { ... }
}
connection.rpc.addResponseHandler(responseHandler);

var requestHandler = function(id, from, method, parameters) { ... }
connection.rpc.addRequestHandler(requestHandler);

var xmlHandler = function(xml) { ... }
connection.rpc.addXMLHandler(xmlHandler)
```

Note that the parser of this module won't throw any exception. However `result`, `method` and `parameters` will be set as `null` if the incoming message is not XML-RPC compliant.

### Whitelist

Expand Down
1 change: 0 additions & 1 deletion rpc/spec/strophe.rpc.spec.helper.js
@@ -1,7 +1,6 @@
var helper = (function() {
function receive(c,req) {
c._dataRecv(createRequest(req));
expect(c.send).toHaveBeenCalled();
}

function spyon (obj, method, cb) {
Expand Down
75 changes: 68 additions & 7 deletions rpc/spec/strophe.rpc.spec.js
Expand Up @@ -3,6 +3,8 @@ var mockConnection = helper.mockConnection,

describe("Strophe Jabber-RPC plugin", function() {

var connection, rpc;

beforeEach(function() {
connection = mockConnection();
rpc = connection.rpc;
Expand Down Expand Up @@ -154,15 +156,74 @@ describe("Strophe Jabber-RPC plugin", function() {
rpc.sendError("id231", "foo@bar/baz", -30021 ,"error message");
});

describe("Handlers", function() {

var handler;

beforeEach(function() {
handler = jasmine.createSpy();
});

it("should be possible to add a request handler", function() {
rpc.addRequestHandler(handler);
var iq = $iq({type: "set", id: "123", from: "foo@bar", to: connection.jid})
.c("query", {xmlns: Strophe.NS.RPC})
.c("methodCall")
.c("methodName").t("pong")
.up()
.c("params")
.c("param")
.c("value")
.c("string").t("foo")
.up().up().up()
.c("param")
.c("value")
.c("i4").t("-32");
receive(connection, iq);
expect(handler).toHaveBeenCalledWith("123", "foo@bar", "pong", ["foo", -32]);
});

it("should be possible to add a response handler", function() {
rpc.addResponseHandler(handler);
iq = $iq({ type: "result", id: "id123", from: "foo@bar/baz", to: connection.jid })
.c("query", {xmlns: Strophe.NS.RPC})
.c("methodResponse")
.c("params")
.c("param")
.c("value")
.c("string").t("foo");
receive(connection, iq);
expect(handler).toHaveBeenCalledWith("id123", "foo@bar/baz", "foo", false);
});

it("should be possible to parse a fault response", function() {
rpc.addResponseHandler(handler);
var iq =$iq({type: "result", id: "123", from: "foo@bar", to: connection.jid})
.c("query", {xmlns: Strophe.NS.RPC})
.c("methodResponse")
.c("fault")
.c("value")
.c("struct")
.c("member")
.c("name").t("faultString")
.up()
.c("value")
.c("string").t("parsererror");
receive(connection, iq);
expect(handler).toHaveBeenCalledWith("123", "foo@bar", {faultString:"parsererror"}, true)
});

});

describe("Forbidden access", function() {

beforeEach(function() {
rpc.addJidToWhiteList(["*@jabber.org", "*@localhost"]);
var handler = function() {};
rpc.addHandlers(handler, handler);
});

it("should send forbidden access to the wrong nodes", function() {
var handler = function() {};
rpc.addHandlers(handler, handler);
spyon(connection, "send", function(iq) {
expect(iq.getAttribute("type")).toEqual("error");
expect(iq.getAttribute("id")).toEqual("123");
Expand All @@ -179,21 +240,21 @@ describe("Strophe Jabber-RPC plugin", function() {
expect(forbidden.getAttribute("xmlns")).toEqual(Strophe.NS.STANZAS);
});

iq = $iq({type: "set", id: "123", from: "foo@bar", to: connection.jid})
var iq = $iq({type: "set", id: "123", from: "foo@bar", to: connection.jid})
.c("query", {xmlns: Strophe.NS.RPC});
receive(connection, iq);
});

it("should NOT send forbidden access to the right nodes", function() {
spyOn(connection, 'send');
expect(connection.send).not.toHaveBeenCalled();
iq = $iq({type: "set", id: "123", from: "foo@bar", to: connection.jid})
spyOn(connection, "send");
var iq = $iq({type: "set", id: "123", from: "foo@jabber.org", to: connection.jid})
.c("query", {xmlns: Strophe.NS.RPC});

receive(connection, iq);
expect(connection.send).not.toHaveBeenCalled();
});

});

});

});
164 changes: 156 additions & 8 deletions rpc/strophe.rpc.js
Expand Up @@ -218,7 +218,7 @@ Strophe.addConnectionPlugin("rpc", {
* @param {Object} context Context of the handler
*/
addRequestHandler: function(handler, context) {
this._connection.addHandler(this._filteredHandler(handler, context), Strophe.NS.RPC, "iq", "set");
this._connection.addHandler(this._filteredRequestHandler(handler, context), Strophe.NS.RPC, "iq", "set");
},

/**
Expand All @@ -229,19 +229,62 @@ Strophe.addConnectionPlugin("rpc", {
* @param {Object} context Context of the handler
*/
addResponseHandler: function(handler, context) {
this._connection.addHandler(this._filteredHandler(handler, context), Strophe.NS.RPC, "iq", "result");
this._connection.addHandler(this._filteredResponseHandler(handler, context), Strophe.NS.RPC, "iq", "result");
},

/**
* Add a raw XML handler for every RPC message incoming
* @param {Function} handler The handler function called every time a rpc is received
* @param {Object} context Context of the handler
*/
addXMLHandler: function(handler, context) {
this._connection.addHandler(this._filteredHandler(handler, context), Strophe.NS.RPC, "iq");
},

_filter: function(xml) {
var from = xml.getAttribute("from");
if (!this._jidInWhitelist(from)) {
this._sendForbiddenAccess(xml.getAttribute("id"), from);
return false;
}
return true;
},

_filteredHandler: function(handler, context) {
context = context || this;
var self = this;
return function(doc) {
var from = doc.getAttribute("from");
if (!self._jidInWhitelist(from)) {
self._sendForbiddenAccess(doc.getAttribute("id"), from);
return true;
return function(xml) {
if (self._filter(xml)) {
return handler.apply(context, arguments);
}
return handler.apply(context, arguments);
return true;
};
},

_filteredResponseHandler: function(handler, context) {
context = context || this;
var self = this;
return function(xml) {
if (self._filter(xml)) {
var rpc = self._parseResponseMessage(xml);
if (rpc.result || rpc.result === null)
return handler.call(context, rpc.id, rpc.from, rpc.result, false);
else if (rpc.fault || rpc.fault === null)
return handler.call(context, rpc.id, rpc.from, rpc.fault, true);
}
return true;
};
},

_filteredRequestHandler: function(handler, context) {
context = context || this;
var self = this;
return function(xml) {
if (self._filter(xml)) {
var rpc = self._parseRequestMessage(xml);
return handler.call(context, rpc.id, rpc.from, rpc.method, rpc.params);
}
return true;
};
},

Expand Down Expand Up @@ -311,6 +354,111 @@ Strophe.addConnectionPlugin("rpc", {
num = "0" + num;
}
return num;
},

_parseRequestMessage: function(iq) {
var rpc = {};
rpc.from = iq.getAttribute("from");
rpc.id = iq.getAttribute("id") || null;

// Method name
var method = iq.getElementsByTagName("methodName")[0];
rpc.method = method ? method.textContent : null;

// Parameters
rpc.params = null;
try {
var params = iq.getElementsByTagName("params")[0]
.childNodes;
if (params && params.length > 0) {
rpc.params = [];
for (var i = 0; i < params.length; i++) {
rpc.params.push(this._convertFromXML(params[i].firstChild));
}
}
} catch(e) {
rpc.params = null;
}
return rpc;
},

_parseResponseMessage: function(iq) {
var rpc = {};
rpc.from = iq.getAttribute("from");
rpc.id = iq.getAttribute("id") || null;

try {
var result = iq.getElementsByTagName("methodResponse")[0].firstChild;

// Response
var tag = result.tagName;
if (tag === "params") {
rpc.result = this._convertFromXML(result.firstChild.firstChild);
}
// Error
else if (tag === "fault") {
rpc.fault = this._convertFromXML(result.firstChild);
}
} catch(e) {
rpc.result = null;
}
return rpc;
},

_convertFromXML: function(obj) {
if (!obj)
return null;

var data;
var tag = obj.tagName.toLowerCase();

try {
switch (tag) {
case "value":
return this._convertFromXML(obj.firstChild);
case "double":
case "i4":
case "int":
var number = obj.textContent;
data = number * 1;
break;
case "boolean":
var bool = obj.textContent;
data = (bool === "1" || bool === "true") ? true : false;
break;
case "datetime.iso8601":
var date = obj.textContent;
data = new Date();
data.setFullYear(date.substring(0,4), date.substring(4,6) - 1, date.substring(6,8));
data.setHours(date.substring(9,11), date.substring(12,14), date.substring(15,17));
break;
case "array":
data = [];
var datatag = obj.firstChild;
for (var k = 0; k < datatag.childNodes.length; k++) {
var value = datatag.childNodes[k];
data.push(this._convertFromXML(value.firstChild));
}
break;
case "struct":
data = {};
for (var j = 0; j < obj.childNodes.length; j++) {
var membername = obj.childNodes[j].getElementsByTagName("name")[0].textContent;
var membervalue = obj.childNodes[j].getElementsByTagName("value")[0].firstChild;
data[membername] = membervalue ? this._convertFromXML(membervalue) : null;
}
break;
case "string":
data = obj.textContent;
break;
default:
data = null;
break;
}
} catch(e) {
data = null;
}
return data;
}

});

0 comments on commit ae7c1dc

Please sign in to comment.