18b004d8b8ad1542808ab06200aab1645f657a54
[project/luci.git] / libs / lucid-rpc / luasrc / lucid / rpc / server.lua
1 --[[
2 LuCI - Lua Development Framework
3
4 Copyright 2009 Steven Barth <steven@midlink.org>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 $Id$
13 ]]
14
15 local ipairs, pairs = ipairs, pairs
16 local tostring, tonumber = tostring, tonumber
17 local pcall, assert, type, unpack = pcall, assert, type, unpack
18
19 local nixio = require "nixio"
20 local json = require "luci.json"
21 local util = require "luci.util"
22 local table = require "table"
23 local ltn12 = require "luci.ltn12"
24
25 --- RPC daemom.
26 -- @cstyle instance
27 module "luci.lucid.rpc.server"
28
29 RQLIMIT = 32 * nixio.const.buffersize
30 VERSION = "1.0"
31
32 ERRNO_PARSE = -32700
33 ERRNO_INVALID = -32600
34 ERRNO_UNKNOWN = -32001
35 ERRNO_TIMEOUT = -32000
36 ERRNO_NOTFOUND = -32601
37 ERRNO_NOACCESS = -32002
38 ERRNO_INTERNAL = -32603
39 ERRNO_NOSUPPORT = -32003
40
41 ERRMSG = {
42         [ERRNO_PARSE] = "Parse error.",
43         [ERRNO_INVALID] = "Invalid request.",
44         [ERRNO_TIMEOUT] = "Connection timeout.",
45         [ERRNO_UNKNOWN] = "Unknown error.",
46         [ERRNO_NOTFOUND] = "Method not found.",
47         [ERRNO_NOACCESS] = "Access denied.",
48         [ERRNO_INTERNAL] = "Internal error.",
49         [ERRNO_NOSUPPORT] = "Operation not supported."
50 }
51
52
53 --- Create an RPC method wrapper.
54 -- @class function
55 -- @param method Lua function
56 -- @param description Method description
57 -- @return Wrapped RPC method 
58 Method = util.class()
59
60 --- Create an extended wrapped RPC method.
61 -- @class function
62 -- @param method Lua function
63 -- @param description Method description
64 -- @return Wrapped RPC method 
65 function Method.extended(...)
66         local m = Method(...)
67         m.call = m.xcall
68         return m
69 end
70
71 function Method.__init__(self, method, description)
72         self.description = description
73         self.method = method
74 end
75
76 --- Extended call the associated function.
77 -- @param session Session storage
78 -- @param argv Request parameters
79 -- @return function call response
80 function Method.xcall(self, session, argv)
81         return self.method(session, unpack(argv))
82 end
83
84 --- Standard call the associated function.
85 -- @param session Session storage
86 -- @param argv Request parameters
87 -- @return function call response
88 function Method.call(self, session, argv)
89         return self.method(unpack(argv))
90 end
91
92 --- Process a given request and create a JSON response.
93 -- @param session Session storage
94 -- @param request Requested method
95 -- @param argv Request parameters
96 function Method.process(self, session, request, argv)
97         local stat, result = pcall(self.call, self, session, argv)
98         
99         if stat then
100                 return { result=result }
101         else
102                 return { error={ 
103                         code=ERRNO_UNKNOWN, 
104                         message=ERRMSG[ERRNO_UNKNOWN],
105                         data=result
106                 } }
107         end
108 end
109
110 -- Remap IPv6-IPv4-compatibility addresses to IPv4 addresses
111 local function remapipv6(adr)
112         local map = "::ffff:"
113         if adr:sub(1, #map) == map then
114                 return adr:sub(#map+1)
115         else
116                 return adr
117         end 
118 end
119
120
121 --- Create an RPC module.
122 -- @class function
123 -- @param description Method description
124 -- @return RPC module 
125 Module = util.class()
126
127 function Module.__init__(self, description)
128         self.description = description
129         self.handler = {}
130 end
131
132 --- Add a handler.
133 -- @param k key
134 -- @param v handler
135 function Module.add(self, k, v)
136         self.handler[k] = v
137 end
138
139 --- Add an access restriction.
140 -- @param restriction Restriction specification
141 function Module.restrict(self, restriction)
142         if not self.restrictions then
143                 self.restrictions = {restriction}
144         else
145                 self.restrictions[#self.restrictions+1] = restriction
146         end
147 end
148
149 --- Enforce access restrictions.
150 -- @param request Request object
151 -- @return nil or HTTP statuscode, table of headers, response source
152 function Module.checkrestricted(self, session, request, argv)
153         if not self.restrictions then
154                 return
155         end
156         
157         for _, r in ipairs(self.restrictions) do
158                 local stat = true
159                 if stat and r.interface then    -- Interface restriction
160                         if not session.localif then
161                                 for _, v in ipairs(session.env.interfaces) do
162                                         if v.addr == session.localaddr then
163                                                 session.localif = v.name
164                                                 break
165                                         end
166                                 end
167                         end
168                         
169                         if r.interface ~= session.localif then
170                                 stat = false
171                         end
172                 end
173                 
174                 if stat and r.user and session.user ~= r.user then      -- User restriction
175                         stat = false
176                 end
177                 
178                 if stat then
179                         return
180                 end
181         end
182         
183         return {error={code=ERRNO_NOACCESS, message=ERRMSG[ERRNO_NOACCESS]}}
184 end
185
186 --- Register a handler, submodule or function.
187 -- @param m entity
188 -- @param descr description
189 -- @return Module (self)
190 function Module.register(self, m, descr)
191         descr = descr or {}
192         for k, v in pairs(m) do
193                 if util.instanceof(v, Method) then
194                         self.handler[k] = v
195                 elseif type(v) == "table" then
196                         self.handler[k] = Module()
197                         self.handler[k]:register(v, descr[k])
198                 elseif type(v) == "function" then
199                         self.handler[k] = Method(v, descr[k])
200                 end
201         end
202         return self
203 end
204
205 --- Process a request.
206 -- @param session Session storage
207 -- @param request Request object
208 -- @param argv Request parameters
209 -- @return JSON response object
210 function Module.process(self, session, request, argv)
211         local first, last = request:match("^([^.]+).?(.*)$")
212
213         local stat = self:checkrestricted(session, request, argv)
214         if stat then    -- Access Denied
215                 return stat
216         end
217         
218         local hndl = first and self.handler[first]
219         if not hndl then
220                 return {error={code=ERRNO_NOTFOUND, message=ERRMSG[ERRNO_NOTFOUND]}}
221         end
222         
223         session.chain[#session.chain+1] = self
224         return hndl:process(session, last, argv)
225 end
226
227
228 --- Create a server object.
229 -- @class function
230 -- @param root Root module
231 -- @return Server object
232 Server = util.class()
233
234 function Server.__init__(self, root)
235         self.root = root
236 end
237
238 --- Get the associated root module.
239 -- @return Root module
240 function Server.get_root(self)
241         return self.root
242 end
243
244 --- Set a new root module.
245 -- @param root Root module
246 function Server.set_root(self, root)
247         self.root = root
248 end
249
250 --- Create a JSON reply.
251 -- @param jsonrpc JSON-RPC version
252 -- @param id Message id
253 -- @param res Result
254 -- @param err Error
255 -- @reutrn JSON response source
256 function Server.reply(self, jsonrpc, id, res, err)
257         id = id or json.null
258         
259         -- 1.0 compatibility
260         if jsonrpc ~= "2.0" then
261                 jsonrpc = nil
262                 res = res or json.null
263                 err = err or json.null
264         end
265         
266         return json.Encoder(
267                         {id=id, result=res, error=err, jsonrpc=jsonrpc}, BUFSIZE
268                 ):source()
269 end
270
271 --- Handle a new client connection.
272 -- @param client client socket
273 -- @param env superserver environment
274 function Server.process(self, client, env)
275         local decoder
276         local sinkout = client:sink()
277         client:setopt("socket", "sndtimeo", 90)
278         client:setopt("socket", "rcvtimeo", 90)
279         
280         local close = false
281         local session = {server = self, chain = {}, client = client, env = env,
282                 localaddr = remapipv6(client:getsockname())}
283         local req, stat, response, result, cb
284         
285         repeat
286                 local oldchunk = decoder and decoder.chunk 
287                 decoder = json.ActiveDecoder(client:blocksource(nil, RQLIMIT))
288                 decoder.chunk = oldchunk
289                 
290                 result, response, cb = nil, nil, nil
291
292                 -- Read one request
293                 stat, req = pcall(decoder.get, decoder)
294                 
295                 if stat then
296                         if type(req) == "table" and type(req.method) == "string"
297                          and (not req.params or type(req.params) == "table") then
298                                 req.params = req.params or {}
299                                 result, cb = self.root:process(session, req.method, req.params)
300                                 if type(result) == "table" then
301                                         if req.id ~= nil then
302                                                 response = self:reply(req.jsonrpc, req.id,
303                                                         result.result, result.error)
304                                         end
305                                         close = result.close
306                                 else
307                                         if req.id ~= nil then
308                                                 response = self:reply(req.jsonrpc, req.id, nil,
309                                                   {code=ERRNO_INTERNAL, message=ERRMSG[ERRNO_INTERNAL]})
310                                         end
311                                 end
312                         else
313                                 response = self:reply(req.jsonrpc, req.id,
314                                  nil, {code=ERRNO_INVALID, message=ERRMSG[ERRNO_INVALID]})
315                         end
316                 else
317                         if nixio.errno() ~= nixio.const.EAGAIN then
318                                 response = self:reply("2.0", nil,
319                                         nil, {code=ERRNO_PARSE, message=ERRMSG[ERRNO_PARSE]})
320                         --[[else
321                                 response = self:reply("2.0", nil,
322                                         nil, {code=ERRNO_TIMEOUT, message=ERRMSG_TIMEOUT})]]
323                         end
324                         close = true
325                 end
326                 
327                 if response then
328                         ltn12.pump.all(response, sinkout)
329                 end
330                 
331                 if cb then
332                         close = cb(client, session, self) or close
333                 end
334         until close
335         
336         client:shutdown()
337         client:close()
338 end