@0xengine/xmlrpc 0.0.1-security → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @0xengine/xmlrpc might be problematic. Click here for more details.

@@ -0,0 +1,188 @@
1
+ /**
2
+ * @class DateFormatter
3
+ * The DateFormatter supports decoding from and encoding to
4
+ * ISO8601 formatted strings. Accepts formats with and without
5
+ * hyphen/colon separators and correctly parses zoning info.
6
+ */
7
+ var DateFormatter = function (opts) {
8
+ this.opts = {}
9
+ this.setOpts(opts)
10
+ }
11
+
12
+ /**
13
+ * Default options for DateFormatter
14
+ * @static
15
+ * @see DateFormatter#setOpts
16
+ */
17
+ DateFormatter.DEFAULT_OPTIONS = {
18
+ colons: true
19
+ , hyphens: false
20
+ , local: true
21
+ , ms: false
22
+ , offset: false
23
+ }
24
+
25
+ /**
26
+ * Regular Expression that disects ISO 8601 formatted strings into
27
+ * an array of parts.
28
+ * @static
29
+ */
30
+ DateFormatter.ISO8601 = new RegExp(
31
+ '([0-9]{4})([-]?([0-9]{2}))([-]?([0-9]{2}))'
32
+ + '(T([0-9]{2})(((:?([0-9]{2}))?((:?([0-9]{2}))?(\.([0-9]+))?))?)'
33
+ + '(Z|([+-]([0-9]{2}(:?([0-9]{2}))?)))?)?'
34
+ )
35
+
36
+ /**
37
+ * Sets options for encoding Date objects to ISO8601 strings.
38
+ * Omitting the 'opts' argument will reset all options to the default.
39
+ *
40
+ * @param {Object} opts - Options (optional)
41
+ * @param {Boolean} opts.colons - Enable/disable formatting the time portion
42
+ * with a colon as separator (default: true)
43
+ * @param {Boolean} opts.hyphens - Enable/disable formatting the date portion
44
+ * with a hyphen as separator (default: false)
45
+ * @param {Boolean} opts.local - Encode as local time instead of UTC
46
+ * (default: true)
47
+ * @param {Boolean} opts.ms - Enable/Disable output of milliseconds
48
+ * (default: false)
49
+ * @param {Boolean} opts.offset - Enable/Disable output of UTC offset
50
+ * (default: false)
51
+ */
52
+ DateFormatter.prototype.setOpts = function (opts) {
53
+ if (!opts) opts = DateFormatter.DEFAULT_OPTIONS
54
+
55
+ var ctx = this
56
+ Object.keys(DateFormatter.DEFAULT_OPTIONS).forEach(function (k) {
57
+ ctx.opts[k] = opts.hasOwnProperty(k) ?
58
+ opts[k] : DateFormatter.DEFAULT_OPTIONS[k]
59
+ })
60
+ }
61
+
62
+ /**
63
+ * Converts a date time stamp following the ISO8601 format to a JavaScript Date
64
+ * object.
65
+ *
66
+ * @param {String} time - String representation of timestamp.
67
+ * @return {Date} - Date object from timestamp.
68
+ */
69
+ DateFormatter.prototype.decodeIso8601 = function(time) {
70
+ var dateParts = time.toString().match(DateFormatter.ISO8601)
71
+ if (!dateParts) {
72
+ throw new Error('Expected a ISO8601 datetime but got \'' + time + '\'')
73
+ }
74
+
75
+ var date = [
76
+ [dateParts[1], dateParts[3] || '01', dateParts[5] || '01'].join('-')
77
+ , 'T'
78
+ , [
79
+ dateParts[7] || '00'
80
+ , dateParts[11] || '00'
81
+ , dateParts[14] || '00'
82
+ ].join(':')
83
+ , '.'
84
+ , dateParts[16] || '000'
85
+ ].join('')
86
+
87
+ date += (dateParts[17] !== undefined) ?
88
+ dateParts[17] +
89
+ ((dateParts[19] && dateParts[20] === undefined) ? '00' : '') :
90
+ DateFormatter.formatCurrentOffset(new Date(date))
91
+
92
+ return new Date(date)
93
+ }
94
+
95
+ /**
96
+ * Converts a JavaScript Date object to an ISO8601 timestamp.
97
+ *
98
+ * @param {Date} date - Date object.
99
+ * @return {String} - String representation of timestamp.
100
+ */
101
+ DateFormatter.prototype.encodeIso8601 = function(date) {
102
+ var parts = this.opts.local ?
103
+ DateFormatter.getLocalDateParts(date) :
104
+ DateFormatter.getUTCDateParts(date)
105
+
106
+ return [
107
+ [parts[0],parts[1],parts[2]].join(this.opts.hyphens ? '-' : '')
108
+ , 'T'
109
+ , [parts[3],parts[4],parts[5]].join(this.opts.colons ? ':' : '')
110
+ , (this.opts.ms) ? '.' + parts[6] : ''
111
+ , (this.opts.local) ? ((this.opts.offset) ?
112
+ DateFormatter.formatCurrentOffset(date) : '') : 'Z'
113
+ ].join('')
114
+ }
115
+
116
+ /**
117
+ * Helper function to get an array of zero-padded date parts,
118
+ * in UTC
119
+ *
120
+ * @param {Date} date - Date Object
121
+ * @return {String[]}
122
+ */
123
+ DateFormatter.getUTCDateParts = function (date) {
124
+ return [
125
+ date.getUTCFullYear()
126
+ , DateFormatter.zeroPad(date.getUTCMonth()+1,2)
127
+ , DateFormatter.zeroPad(date.getUTCDate(),2)
128
+ , DateFormatter.zeroPad(date.getUTCHours(), 2)
129
+ , DateFormatter.zeroPad(date.getUTCMinutes(), 2)
130
+ , DateFormatter.zeroPad(date.getUTCSeconds(), 2)
131
+ , DateFormatter.zeroPad(date.getUTCMilliseconds(), 3)]
132
+ }
133
+
134
+
135
+ /**
136
+ * Helper function to get an array of zero-padded date parts,
137
+ * in the local time zone
138
+ *
139
+ * @param {Date} date - Date Object
140
+ * @return {String[]}
141
+ */
142
+ DateFormatter.getLocalDateParts = function (date) {
143
+ return [
144
+ date.getFullYear()
145
+ , DateFormatter.zeroPad(date.getMonth()+1,2)
146
+ , DateFormatter.zeroPad(date.getDate(),2)
147
+ , DateFormatter.zeroPad(date.getHours(), 2)
148
+ , DateFormatter.zeroPad(date.getMinutes(), 2)
149
+ , DateFormatter.zeroPad(date.getSeconds(), 2)
150
+ , DateFormatter.zeroPad(date.getMilliseconds(), 3)]
151
+ }
152
+
153
+ /**
154
+ * Helper function to pad the digits with 0s to meet date formatting
155
+ * requirements.
156
+ *
157
+ * @param {Number} digit - The number to pad.
158
+ * @param {Number} length - Length of digit string, prefix with 0s if not
159
+ * already length.
160
+ * @return {String} - String with the padded digit
161
+ */
162
+ DateFormatter.zeroPad = function (digit, length) {
163
+ var padded = '' + digit
164
+ while (padded.length < length) {
165
+ padded = '0' + padded
166
+ }
167
+
168
+ return padded
169
+ }
170
+
171
+ /**
172
+ * Helper function to get the current timezone to default decoding to
173
+ * rather than UTC. (for backward compatibility)
174
+ *
175
+ * @return {String} - in the format /Z|[+-]\d{2}:\d{2}/
176
+ */
177
+ DateFormatter.formatCurrentOffset = function (d) {
178
+ var offset = (d || new Date()).getTimezoneOffset()
179
+ return (offset === 0) ? 'Z' : [
180
+ (offset < 0) ? '+' : '-'
181
+ , DateFormatter.zeroPad(Math.abs(Math.floor(offset/60)),2)
182
+ , ':'
183
+ , DateFormatter.zeroPad(Math.abs(offset%60),2)
184
+ ].join('')
185
+ }
186
+
187
+ // export an instance of DateFormatter only.
188
+ module.exports = new DateFormatter()
@@ -0,0 +1,324 @@
1
+ var sax = require('sax')
2
+ , dateFormatter = require('./date_formatter')
3
+
4
+ var Deserializer = function(encoding) {
5
+ this.type = null
6
+ this.responseType = null
7
+ this.stack = []
8
+ this.marks = []
9
+ this.data = []
10
+ this.methodname = null
11
+ this.encoding = encoding || 'utf8'
12
+ this.value = false
13
+ this.callback = null
14
+ this.error = null
15
+
16
+ this.parser = sax.createStream()
17
+ this.parser.on('opentag', this.onOpentag.bind(this))
18
+ this.parser.on('closetag', this.onClosetag.bind(this))
19
+ this.parser.on('text', this.onText.bind(this))
20
+ this.parser.on('cdata', this.onCDATA.bind(this))
21
+ this.parser.on('end', this.onDone.bind(this))
22
+ this.parser.on('error', this.onError.bind(this))
23
+ }
24
+
25
+ Deserializer.prototype.deserializeMethodResponse = function(stream, callback) {
26
+ var that = this
27
+
28
+ this.callback = function(error, result) {
29
+ if (error) {
30
+ callback(error)
31
+ }
32
+ else if (result.length > 1) {
33
+ callback(new Error('Response has more than one param'))
34
+ }
35
+ else if (that.type !== 'methodresponse') {
36
+ callback(new Error('Not a method response'))
37
+ }
38
+ else if (!that.responseType) {
39
+ callback(new Error('Invalid method response'))
40
+ }
41
+ else {
42
+ callback(null, result[0])
43
+ }
44
+ }
45
+
46
+ stream.setEncoding(this.encoding)
47
+ stream.on('error', this.onError.bind(this))
48
+ stream.pipe(this.parser)
49
+ }
50
+
51
+ Deserializer.prototype.deserializeMethodCall = function(stream, callback) {
52
+ var that = this
53
+
54
+ this.callback = function(error, result) {
55
+ if (error) {
56
+ callback(error)
57
+ }
58
+ else if (that.type !== 'methodcall') {
59
+ callback(new Error('Not a method call'))
60
+ }
61
+ else if (!that.methodname) {
62
+ callback(new Error('Method call did not contain a method name'))
63
+ }
64
+ else {
65
+ callback(null, that.methodname, result)
66
+ }
67
+ }
68
+
69
+ stream.setEncoding(this.encoding)
70
+ stream.on('error', this.onError.bind(this))
71
+ stream.pipe(this.parser)
72
+ }
73
+
74
+ Deserializer.prototype.onDone = function() {
75
+ var that = this
76
+
77
+ if (!this.error) {
78
+ if (this.type === null || this.marks.length) {
79
+ this.callback(new Error('Invalid XML-RPC message'))
80
+ }
81
+ else if (this.responseType === 'fault') {
82
+ var createFault = function(fault) {
83
+ var error = new Error('XML-RPC fault' + (fault.faultString ? ': ' + fault.faultString : ''))
84
+ error.code = fault.faultCode
85
+ error.faultCode = fault.faultCode
86
+ error.faultString = fault.faultString
87
+ return error
88
+ }
89
+ this.callback(createFault(this.stack[0]))
90
+ }
91
+ else {
92
+ this.callback(undefined, this.stack)
93
+ }
94
+ }
95
+ }
96
+
97
+ // TODO:
98
+ // Error handling needs a little thinking. There are two different kinds of
99
+ // errors:
100
+ // 1. Low level errors like network, stream or xml errors. These don't
101
+ // require special treatment. They only need to be forwarded. The IO
102
+ // is already stopped in these cases.
103
+ // 2. Protocol errors: Invalid tags, invalid values &c. These happen in
104
+ // our code and we should tear down the IO and stop parsing.
105
+ // Currently all errors end here. Guess I'll split it up.
106
+ Deserializer.prototype.onError = function(msg) {
107
+ if (!this.error) {
108
+ if (typeof msg === 'string') {
109
+ this.error = new Error(msg)
110
+ }
111
+ else {
112
+ this.error = msg
113
+ }
114
+ this.callback(this.error)
115
+ }
116
+ }
117
+
118
+ Deserializer.prototype.push = function(value) {
119
+ this.stack.push(value)
120
+ }
121
+
122
+ //==============================================================================
123
+ // SAX Handlers
124
+ //==============================================================================
125
+
126
+ Deserializer.prototype.onOpentag = function(node) {
127
+ if (node.name === 'ARRAY' || node.name === 'STRUCT') {
128
+ this.marks.push(this.stack.length)
129
+ }
130
+ this.data = []
131
+ this.value = (node.name === 'VALUE')
132
+ }
133
+
134
+ Deserializer.prototype.onText = function(text) {
135
+ this.data.push(text)
136
+ }
137
+
138
+ Deserializer.prototype.onCDATA = function(cdata) {
139
+ this.data.push(cdata)
140
+ }
141
+
142
+ Deserializer.prototype.onClosetag = function(el) {
143
+ var data = this.data.join('')
144
+ try {
145
+ switch(el) {
146
+ case 'BOOLEAN':
147
+ this.endBoolean(data)
148
+ break
149
+ case 'INT':
150
+ case 'I4':
151
+ this.endInt(data)
152
+ break
153
+ case 'I8':
154
+ this.endI8(data)
155
+ break
156
+ case 'DOUBLE':
157
+ this.endDouble(data)
158
+ break
159
+ case 'STRING':
160
+ case 'NAME':
161
+ this.endString(data)
162
+ break
163
+ case 'ARRAY':
164
+ this.endArray(data)
165
+ break
166
+ case 'STRUCT':
167
+ this.endStruct(data)
168
+ break
169
+ case 'BASE64':
170
+ this.endBase64(data)
171
+ break
172
+ case 'DATETIME.ISO8601':
173
+ this.endDateTime(data)
174
+ break
175
+ case 'VALUE':
176
+ this.endValue(data)
177
+ break
178
+ case 'PARAMS':
179
+ this.endParams(data)
180
+ break
181
+ case 'FAULT':
182
+ this.endFault(data)
183
+ break
184
+ case 'METHODRESPONSE':
185
+ this.endMethodResponse(data)
186
+ break
187
+ case 'METHODNAME':
188
+ this.endMethodName(data)
189
+ break
190
+ case 'METHODCALL':
191
+ this.endMethodCall(data)
192
+ break
193
+ case 'NIL':
194
+ this.endNil(data)
195
+ break
196
+ case 'DATA':
197
+ case 'PARAM':
198
+ case 'MEMBER':
199
+ // Ignored by design
200
+ break
201
+ default:
202
+ this.onError('Unknown XML-RPC tag \'' + el + '\'')
203
+ break
204
+ }
205
+ }
206
+ catch (e) {
207
+ this.onError(e)
208
+ }
209
+ }
210
+
211
+ Deserializer.prototype.endNil = function(data) {
212
+ this.push(null)
213
+ this.value = false
214
+ }
215
+
216
+ Deserializer.prototype.endBoolean = function(data) {
217
+ if (data === '1') {
218
+ this.push(true)
219
+ }
220
+ else if (data === '0') {
221
+ this.push(false)
222
+ }
223
+ else {
224
+ throw new Error('Illegal boolean value \'' + data + '\'')
225
+ }
226
+ this.value = false
227
+ }
228
+
229
+ Deserializer.prototype.endInt = function(data) {
230
+ var value = parseInt(data, 10)
231
+ if (isNaN(value)) {
232
+ throw new Error('Expected an integer but got \'' + data + '\'')
233
+ }
234
+ else {
235
+ this.push(value)
236
+ this.value = false
237
+ }
238
+ }
239
+
240
+ Deserializer.prototype.endDouble = function(data) {
241
+ var value = parseFloat(data)
242
+ if (isNaN(value)) {
243
+ throw new Error('Expected a double but got \'' + data + '\'')
244
+ }
245
+ else {
246
+ this.push(value)
247
+ this.value = false
248
+ }
249
+ }
250
+
251
+ Deserializer.prototype.endString = function(data) {
252
+ this.push(data)
253
+ this.value = false
254
+ }
255
+
256
+ Deserializer.prototype.endArray = function(data) {
257
+ var mark = this.marks.pop()
258
+ this.stack.splice(mark, this.stack.length - mark, this.stack.slice(mark))
259
+ this.value = false
260
+ }
261
+
262
+ Deserializer.prototype.endStruct = function(data) {
263
+ var mark = this.marks.pop()
264
+ , struct = {}
265
+ , items = this.stack.slice(mark)
266
+ , i = 0
267
+
268
+ for (; i < items.length; i += 2) {
269
+ struct[items[i]] = items[i + 1]
270
+ }
271
+ this.stack.splice(mark, this.stack.length - mark, struct)
272
+ this.value = false
273
+ }
274
+
275
+ Deserializer.prototype.endBase64 = function(data) {
276
+ var buffer = new Buffer(data, 'base64')
277
+ this.push(buffer)
278
+ this.value = false
279
+ }
280
+
281
+ Deserializer.prototype.endDateTime = function(data) {
282
+ var date = dateFormatter.decodeIso8601(data)
283
+ this.push(date)
284
+ this.value = false
285
+ }
286
+
287
+ var isInteger = /^-?\d+$/
288
+ Deserializer.prototype.endI8 = function(data) {
289
+ if (!isInteger.test(data)) {
290
+ throw new Error('Expected integer (I8) value but got \'' + data + '\'')
291
+ }
292
+ else {
293
+ this.endString(data)
294
+ }
295
+ }
296
+
297
+ Deserializer.prototype.endValue = function(data) {
298
+ if (this.value) {
299
+ this.endString(data)
300
+ }
301
+ }
302
+
303
+ Deserializer.prototype.endParams = function(data) {
304
+ this.responseType = 'params'
305
+ }
306
+
307
+ Deserializer.prototype.endFault = function(data) {
308
+ this.responseType = 'fault'
309
+ }
310
+
311
+ Deserializer.prototype.endMethodResponse = function(data) {
312
+ this.type = 'methodresponse'
313
+ }
314
+
315
+ Deserializer.prototype.endMethodName = function(data) {
316
+ this.methodname = data
317
+ }
318
+
319
+ Deserializer.prototype.endMethodCall = function(data) {
320
+ this.type = 'methodcall'
321
+ }
322
+
323
+ module.exports = Deserializer
324
+
@@ -0,0 +1,202 @@
1
+ var xmlBuilder = require('xmlbuilder')
2
+ , dateFormatter = require('./date_formatter')
3
+ , CustomType = require('./customtype')
4
+
5
+ /**
6
+ * Creates the XML for an XML-RPC method call.
7
+ *
8
+ * @param {String} method - The method name.
9
+ * @param {Array} params - Params to pass in the call.
10
+ * @param {Function} callback - function (error, xml) { ... }
11
+ * - {Object|null} error - Any errors that occurred while building the XML,
12
+ * otherwise null.
13
+ * - {String} xml - The method call XML.
14
+ */
15
+ exports.serializeMethodCall = function(method, params, encoding) {
16
+ var params = params || []
17
+
18
+ var options = { version: '1.0', allowSurrogateChars: true }
19
+
20
+ if (encoding) {
21
+ options.encoding = encoding
22
+ }
23
+
24
+ var xml = xmlBuilder.create('methodCall', options)
25
+ .ele('methodName')
26
+ .txt(method)
27
+ .up()
28
+ .ele('params')
29
+
30
+ params.forEach(function(param) {
31
+ serializeValue(param, xml.ele('param'))
32
+ })
33
+
34
+ // Includes the <?xml ...> declaration
35
+ return xml.doc().toString()
36
+ }
37
+
38
+ /**
39
+ * Creates the XML for an XML-RPC method response.
40
+ *
41
+ * @param {mixed} value - The value to pass in the response.
42
+ * @param {Function} callback - function (error, xml) { ... }
43
+ * - {Object|null} error - Any errors that occurred while building the XML,
44
+ * otherwise null.
45
+ * - {String} xml - The method response XML.
46
+ */
47
+ exports.serializeMethodResponse = function(result) {
48
+ var xml = xmlBuilder.create('methodResponse', { version: '1.0', allowSurrogateChars: true })
49
+ .ele('params')
50
+ .ele('param')
51
+
52
+ serializeValue(result, xml)
53
+
54
+ // Includes the <?xml ...> declaration
55
+ return xml.doc().toString()
56
+ }
57
+
58
+ exports.serializeFault = function(fault) {
59
+ var xml = xmlBuilder.create('methodResponse', { version: '1.0', allowSurrogateChars: true })
60
+ .ele('fault')
61
+
62
+ serializeValue(fault, xml)
63
+
64
+ // Includes the <?xml ...> declaration
65
+ return xml.doc().toString()
66
+ }
67
+
68
+ function serializeValue(value, xml) {
69
+ var stack = [ { value: value, xml: xml } ]
70
+ , current = null
71
+ , valueNode = null
72
+ , next = null
73
+
74
+ while (stack.length > 0) {
75
+ current = stack[stack.length - 1]
76
+
77
+ if (current.index !== undefined) {
78
+ // Iterating a compound
79
+ next = getNextItemsFrame(current)
80
+ if (next) {
81
+ stack.push(next)
82
+ }
83
+ else {
84
+ stack.pop()
85
+ }
86
+ }
87
+ else {
88
+ // we're about to add a new value (compound or simple)
89
+ valueNode = current.xml.ele('value')
90
+ switch(typeof current.value) {
91
+ case 'boolean':
92
+ appendBoolean(current.value, valueNode)
93
+ stack.pop()
94
+ break
95
+ case 'string':
96
+ appendString(current.value, valueNode)
97
+ stack.pop()
98
+ break
99
+ case 'number':
100
+ appendNumber(current.value, valueNode)
101
+ stack.pop()
102
+ break
103
+ case 'object':
104
+ if (current.value === null) {
105
+ valueNode.ele('nil')
106
+ stack.pop()
107
+ }
108
+ else if (current.value instanceof Date) {
109
+ appendDatetime(current.value, valueNode)
110
+ stack.pop()
111
+ }
112
+ else if (Buffer.isBuffer(current.value)) {
113
+ appendBuffer(current.value, valueNode)
114
+ stack.pop()
115
+ }
116
+ else if (current.value instanceof CustomType) {
117
+ current.value.serialize(valueNode)
118
+ stack.pop()
119
+ }
120
+ else {
121
+ if (Array.isArray(current.value)) {
122
+ current.xml = valueNode.ele('array').ele('data')
123
+ }
124
+ else {
125
+ current.xml = valueNode.ele('struct')
126
+ current.keys = Object.keys(current.value)
127
+ }
128
+ current.index = 0
129
+ next = getNextItemsFrame(current)
130
+ if (next) {
131
+ stack.push(next)
132
+ }
133
+ else {
134
+ stack.pop()
135
+ }
136
+ }
137
+ break
138
+ default:
139
+ stack.pop()
140
+ break
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ function getNextItemsFrame(frame) {
147
+ var nextFrame = null
148
+
149
+ if (frame.keys) {
150
+ if (frame.index < frame.keys.length) {
151
+ var key = frame.keys[frame.index++]
152
+ , member = frame.xml.ele('member').ele('name').text(key).up()
153
+ nextFrame = {
154
+ value: frame.value[key]
155
+ , xml: member
156
+ }
157
+ }
158
+ }
159
+ else if (frame.index < frame.value.length) {
160
+ nextFrame = {
161
+ value: frame.value[frame.index]
162
+ , xml: frame.xml
163
+ }
164
+ frame.index++
165
+ }
166
+
167
+ return nextFrame
168
+ }
169
+
170
+ function appendBoolean(value, xml) {
171
+ xml.ele('boolean').txt(value ? 1 : 0)
172
+ }
173
+
174
+ var illegalChars = /^(?![^<&]*]]>[^<&]*)[^<&]*$/
175
+ function appendString(value, xml) {
176
+ if (value.length === 0) {
177
+ xml.ele('string')
178
+ }
179
+ else if (!illegalChars.test(value)) {
180
+ xml.ele('string').d(value)
181
+ }
182
+ else {
183
+ xml.ele('string').txt(value)
184
+ }
185
+ }
186
+
187
+ function appendNumber(value, xml) {
188
+ if (value % 1 == 0) {
189
+ xml.ele('int').txt(value)
190
+ }
191
+ else {
192
+ xml.ele('double').txt(value)
193
+ }
194
+ }
195
+
196
+ function appendDatetime(value, xml) {
197
+ xml.ele('dateTime.iso8601').txt(dateFormatter.encodeIso8601(value))
198
+ }
199
+
200
+ function appendBuffer(value, xml) {
201
+ xml.ele('base64').txt(value.toString('base64'))
202
+ }