r19051: JSON-RPC server work-in-progress. It's almost working.
[jelmer/samba4-debian.git] / jsonrpc / request.esp
1 <%
2
3 /*
4  * Copyright:
5  *   (C) 2006 by Derrell Lipman
6  *       All rights reserved
7  *
8  * License:
9  *   LGPL 2.1: http://creativecommons.org/licenses/LGPL/2.1/
10  */
11
12 /*
13  * This is a simple JSON-RPC server.
14  */
15
16 /* Bring in the date class */
17 jsonrpc_include("json.esp");
18 jsonrpc_include("jsondate.esp");
19
20 /* bring the string functions into the global frame */
21 string_init(global);
22
23 /* Bring the system functions into the global frame */
24 sys_init(global);
25
26 function printf()
27 {
28         print(vsprintf(arguments));
29 }
30
31
32 /*
33  * All of our manipulation of JSON RPC methods will be through this object.
34  * Each class of methods will assign to here, and all of the constants will
35  * also be in this object.
36  */
37 jsonrpc = new Object();
38 jsonrpc.Constant = new Object();
39 jsonrpc.Constant.ErrorOrigin = new Object(); /* error origins */
40 jsonrpc.Constant.ErrorCode = new Object();   /* server-generated error codes */
41 jsonrpc.method = new Object();       /* methods available in requested class */
42
43
44 /*
45  * ScriptTransport constants
46  */
47 jsonrpc.Constant.ScriptTransport = new Object();
48 jsonrpc.Constant.ScriptTransport.NotInUse        = -1;
49
50
51 /*
52  * JSON-RPC error origin constants
53  */
54 jsonrpc.Constant.ErrorOrigin.Server              = 1;
55 jsonrpc.Constant.ErrorOrigin.Application         = 2;
56 jsonrpc.Constant.ErrorOrigin.Transport           = 3;
57 jsonrpc.Constant.ErrorOrigin.Client              = 4;
58
59
60
61 /*
62  * JSON-RPC server-generated error code constants
63  */
64
65 /**
66  * Error code, value 0: Unknown Error
67  *
68  * The default error code, used only when no specific error code is passed to
69  * the JsonRpcError constructor.  This code should generally not be used.
70  */
71 jsonrpc.Constant.ErrorCode.Unknown               = 0;
72
73 /**
74  * Error code, value 1: Illegal Service
75  *
76  * The service name contains illegal characters or is otherwise deemed
77  * unacceptable to the JSON-RPC server.
78  */
79 jsonrpc.Constant.ErrorCode.IllegalService        = 1;
80
81 /**
82  * Error code, value 2: Service Not Found
83  *
84  * The requested service does not exist at the JSON-RPC server.
85  */
86 jsonrpc.Constant.ErrorCode.ServiceNotFound       = 2;
87
88 /**
89  * Error code, value 3: Class Not Found
90  *
91  * If the JSON-RPC server divides service methods into subsets (classes), this
92  * indicates that the specified class was not found.  This is slightly more
93  * detailed than "Method Not Found", but that error would always also be legal
94  * (and true) whenever this one is returned. (Not used in this implementation)
95  */
96 jsonrpc.Constant.ErrorCode.ClassNotFound         = 3; // not used in this implementation
97
98 /**
99  * Error code, value 4: Method Not Found
100  *
101  * The method specified in the request is not found in the requested service.
102  */
103 jsonrpc.Constant.ErrorCode.MethodNotFound        = 4;
104
105 /**
106  * Error code, value 5: Parameter Mismatch
107  *
108  * If a method discovers that the parameters (arguments) provided to it do not
109  * match the requisite types for the method's parameters, it should return
110  * this error code to indicate so to the caller.
111  */
112 jsonrpc.Constant.ErrorCode.PaameterMismatch      = 5;
113
114 /**
115  * Error code, value 6: Permission Denied
116  *
117  * A JSON-RPC service provider can require authentication, and that
118  * authentication can be implemented such the method takes authentication
119  * parameters, or such that a method or class of methods requires prior
120  * authentication.  If the caller has not properly authenticated to use the
121  * requested method, this error code is returned.
122  */
123 jsonrpc.Constant.ErrorCode.PermissionDenied      = 6;
124
125 /*
126  * Error code, value 7: Unexpected Output
127  *
128  * The called method illegally generated output to the browser, which would
129  * have preceeded the JSON-RPC data.
130  */
131 jsonrpc.Constant.ErrorCode.UnexpectedOutput      = 7;
132
133
134
135
136
137
138
139 function sendReply(reply, scriptTransportId)
140 {
141     /* If not using ScriptTransport... */
142     if (scriptTransportId == jsonrpc.Constant.ScriptTransport.NotInUse)
143     {
144         /* ... then just output the reply. */
145         write(reply);
146     }
147     else
148     {
149         /* Otherwise, we need to add a call to a qooxdoo-specific function */
150         reply =
151             "qx.io.remote.ScriptTransport._requestFinished(" +
152             scriptTransportId + ", " + reply +
153             ");";
154         write(reply);
155     }
156 }
157
158
159 function _jsonValidRequest(req)
160 {
161     if (req == undefined)
162     {
163         return false;
164     }
165
166     if (req.id == undefined)
167     {
168         return false;
169     }
170
171     if (req.service == undefined)
172     {
173         return false;
174     }
175
176     if (req.method == undefined)
177     {
178         return false;
179     }
180
181     if (req.params == undefined)
182     {
183         return false;
184     }
185
186     return true;
187 }
188 jsonrpc.validRequest = _jsonValidRequest;
189 _jsonValidRequest = null;
190
191 /*
192  * class JsonRpcError
193  *
194  * This class allows service methods to easily provide error information for
195  * return via JSON-RPC.
196  */
197 function _JsonRpcError_create(origin, code, message)
198 {
199     var o = new Object();
200
201     o.data = new Object();
202     o.data.origin = origin;
203     o.data.code = code;
204     o.data.message = message;
205     o.scriptTransportId = jsonrpc.Constant.ScriptTransport.NotInUse;
206     o.__type = "_JsonRpcError";
207
208     function _origin(origin)
209     {
210         this.origin = origin;
211     }
212     o.setOrigin = _origin;
213
214     function _setError(code, message)
215     {
216         this.code = code;
217         this.message = message;
218     }
219     o.setError = _setError;
220
221     function _setId(id)
222     {
223         this.id = id;
224     }
225     o.setId = _setId;
226
227     function _setScriptTransportId(id)
228     {
229         this.scriptTransportId = id;
230     }
231     o.setScriptTransportId = _setScriptTransportId;
232
233     function _Send()
234     {
235         var error = this;
236         var id = this.id;
237         var ret = new Array(2);
238         ret.error = this.data;
239         ret.id = this.id;
240         sendReply(Json.encode(ret), this.scriptTransportId);
241     }
242     o.Send = _Send;
243
244     return o;
245 }
246
247 jsonrpc.createError = _JsonRpcError_create;
248 _JsonRpcError_create = null;
249
250 /*
251  * 'input' is the user-provided json-encoded request
252  * 'jsonInput' is that request, decoded into its object form
253  */
254 var input;
255 var jsonInput = null;
256
257 /* Allocate a generic error object */
258 error = jsonrpc.createError(jsonrpc.Constant.ErrorOrigin.Server,
259                             jsonrpc.Constant.ErrorCode.Unknown,
260                             "Unknown error");
261
262 /* Assume (default) we're not using ScriptTransport */
263 scriptTransportId = jsonrpc.Constant.ScriptTransport.NotInUse;
264
265 /* What type of request did we receive? */
266 if (request["REQUEST_METHOD"] == "POST" &&
267     request["CONTENT_TYPE"] == "text/json")
268 {
269     /* We found literal POSTed json-rpc data (we hope) */
270     input = request["POST_DATA"];
271     jsonInput = Json.decode(input);
272 }
273 else if (request["REQUEST_METHOD"] == "GET" &&
274          form["_ScriptTransport_id"] != undefined &&
275          form["_ScriptTransport_data"] != undefined)
276 {
277     /* We have what looks like a valid ScriptTransport request */
278     scriptTransportId = form["_ScriptTransport_id"];
279     error.setScriptTransportId(scriptTransportId);
280     input = form["_ScriptTransport_data"];
281     jsonInput = Json.decode(input);
282 }
283
284 /* Ensure that this was a JSON-RPC service request */
285 if (! jsonrpc.validRequest(jsonInput))
286 {
287     /*
288      * This request was not issued with JSON-RPC so echo the error rather than
289      * issuing a JsonRpcError response.
290      */
291     write("JSON-RPC request expected; service, method or params missing<br>");
292     return;
293 }
294
295 /*
296  * Ok, it looks like JSON-RPC, so we'll return an Error object if we encounter
297  * errors from here on out.
298  */
299 error.setId(jsonInput.id);
300
301 /* Service and method names may contain these characters */
302 var nameChars =
303     "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
304
305 /* The first letter of service and method names must be a letter */
306 var nameFirstLetter =
307     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
308
309 /*
310  * Ensure the method name is kosher.  A meethod name should be:
311  *
312  *   - first character is in [a-zA-Z] 
313  *   - other characters are in [_a-zA-Z0-9]
314  */
315
316 /* First check for legal characters */
317 if (strspn(jsonInput.method, nameChars) != strlen(jsonInput.method))
318 {
319     /* There's some illegal character in the service name */
320     error.setError(JsonRpcError.MethodNotFound,
321                    "Illegal character found in method name.");
322     error.Send();
323     return;
324 }
325
326 /* Now ensure that it begins with a letter */
327 if (strspn(substr(jsonInput.method, 0, 1), nameFirstLetter) != 1)
328 {
329     error.setError(jsonrpc.Constant.ErrorCode.MethodNotFound,
330                    "The method name does not begin with a letter");
331     error.Send();
332     return;
333 }
334
335 /*
336  * Ensure the requested service name is kosher.  A service name should be:
337  *
338  *   - a dot-separated sequences of strings; no adjacent dots
339  *   - first character of each string is in [a-zA-Z] 
340  *   - other characters are in [_a-zA-Z0-9]
341  */
342
343 /* First check for legal characters */
344 if (strspn(jsonInput.service, "." + nameChars) != strlen(jsonInput.service))
345 {
346     /* There's some illegal character in the service name */
347     error.setError(JsonRpcError.IllegalService,
348                    "Illegal character found in service name.");
349     error.Send();
350     return;
351 }
352
353 /*
354  * Now ensure there are no double dots.
355  *
356  * Frustration with ejs.  Result must be NULL, but we can't use the ===
357  * operator: strstr() === null so we have to use typeof.  If the result isn't
358  * null, then it'll be a number and therefore not type "pointer".
359  */
360 if (typeof(strstr(jsonInput.service, "..")) != "pointer")
361 {
362     error.setError(JsonRpcError.IllegalService,
363                    "Illegal use of two consecutive dots in service name");
364     error.Send();
365     return;
366 }
367
368 /* Explode the service name into its dot-separated parts */
369 var serviceComponents = split(".", jsonInput.service);
370
371 /* Ensure that each component begins with a letter */
372 for (var i = 0; i < serviceComponents.length; i++)
373 {
374     if (strspn(substr(serviceComponents[i], 0, 1), nameFirstLetter) != 1)
375     {
376         error.setError(jsonrpc.Constant.ErrorCode.IllegalService,
377                        "A service name component does not begin with a letter");
378         error.Send();
379         return;
380     }
381 }
382
383 /*
384  * Now replace all dots with slashes so we can locate the service script.  We
385  * also retain the split components of the path, as the class name of the
386  * service is the last component of the path.
387  */
388 var servicePath = join("/", serviceComponents) + ".esp";
389
390 /* Load the requested class */
391 if (jsonrpc_include(servicePath))
392 {
393     /* Couldn't find the requested service */
394     error.setError(jsonrpc.Constant.ErrorCode.ServiceNotFound,
395                    "Service class `" + servicePath + "` does not exist.");
396     error.Send();
397     return;
398 }
399
400 /*
401  * Find the requested method.
402  *
403  * What we really want to do here, and could do in any reasonable language,
404  * is:
405  *
406  *   method = jsonrpc.method[jsonInput.method];
407  *   if (method && typeof(method) == "function") ...
408  *
409  * The following completely unreasonable sequence of commands is because:
410  *
411  *  (a) ejs evaluates all OR'ed expressions even if an early one is false, and
412  *      bars on the typeof(method) call if method is undefined
413  *
414  *  (b) ejs does not allow comparing against the string "function"!!!  What
415  *      the hell is special about that particular string???
416  *
417  * E-gad.  What a mess.
418  */
419 var method = jsonrpc.method[jsonInput.method];
420 var valid = (method != undefined);
421 if (valid)
422 {
423     var type = typeof(method);
424     if (substr(type, 0, 1) != 'f' || substr(type, 1) != "unction")
425     {
426         valid = false;
427     }
428 }
429
430 if (! valid)
431 {
432     error.setError(jsonrpc.Constant.ErrorCode.MethodNotFound,
433                    "Method `" + method + "` not found.");
434     error.Send();
435     return;
436 }
437
438 /* Most errors from here on out will be Application-generated */
439 error.setOrigin(jsonrpc.Constant.ErrorOrigin.Application);
440
441 /* Call the requested method passing it the provided params */
442 var retval = method(jsonInput.params, error);
443
444 /* See if the result of the function was actually an error object */
445 var wasError = (retval["__type"] != undefined);
446 if (wasError)
447 {
448     wasError = retval.__type == "_JsonRpcError";
449 }
450 if (wasError)
451 {
452     /* Yup, it was.  Return the error */
453     retval.Send();
454     return;
455 }
456
457 /* Give 'em what they came for! */
458 var ret = new Object();
459 ret.result = retval;
460 ret.id = jsonInput.id;
461 sendReply(Json.encode(ret), scriptTransportId);
462 %>