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