Repository: web2py/web2py

Issue:
REST - IP address as parameter, web2py thinks the last octet is an extension
Hi, 

I am currently building a rather simple REST-interface based on Web2py (Version 2.14.6-stable+timestamp.2016.05.10.00.21.47) and found a bug. 

I want to provide an interface which queries our IPAM-solution for infos about a specified IP address/network, e.g.
1- https://web2pyhost/rest/net/192.168.1.1
2- https://web2pyhost/rest/net/192.168.1.0/24
3- https://web2pyhost/rest/net/192.168.1.1.

This is my controller:
```python
@request.restful()
def net():
    response.view = 'generic.json'

    def GET(ip, mask=0):
        if IS_IPV4()(ip)[1]:
            return dict(result=dict(error="Invalid ip address"))
        if IS_INT_IN_RANGE(0, 33)(mask)[1]:
            return dict(result=dict(error="Invalid mask"))
        if mask != 0:
            return dict(result=getRESTInfoForNetwork("%s/%s"%(ip,mask)))
        else:
            return dict(result=getRESTInfoForIP(ip))
    return locals()
```
The first URL does not work, but the second does. It seems like the parameter ip only contains the first three octets. A simple workaround is to append another "." and it works (URL 3). 

Everything after the last "." (if there is any) is considered an extension and therefore does not work for an ip address (line 371+372):

**web2py/gluon/globals.py:**
```python
 364     def restful(self):
 365         def wrapper(action, request=self):
 366             def f(_action=action, *a, **b):
 367                 request.is_restful = True
 368                 env = request.env
 369                 is_json = env.content_type=='application/json'
 370                 method = env.request_method
 371                 if len(request.args) and '.' in request.args[-1]:
 372                     request.args[-1], _, request.extension = request.args[-1].rpartition('.')
 373                     current.response.headers['Content-Type'] = \
 374                         contenttype('.' + request.extension.lower())
 375                 rest_action = _action().get(method, None)
 376                 if not (rest_action and method == method.upper()
 377                         and callable(rest_action)):
 378                     raise HTTP(405, "method not allowed")
 379                 try:
 380                     res = rest_action(*request.args, **request.vars)
 381                     if is_json and not isinstance(res, str):
 382                         res = json(res)
 383                     return res
 384                 except TypeError, e:
 385                     exc_type, exc_value, exc_traceback = sys.exc_info()
 386                     if len(traceback.extract_tb(exc_traceback)) == 1:
 387                         raise HTTP(400, "invalid arguments")
 388                     else:
 389                         raise
 390             f.__doc__ = action.__doc__
 391             f.__name__ = action.__name__
 392             return f
 393         return wrapper
```

This may be a very special and I can live with my workaround. It is up to you, if you want to handle arguments with an "." that's not meant as a separator between filename and extension. A simple solution would be to check if there is more than one ".", if it is just one, than it is probably an extension, if more than one, it might not be an extension.

best regards, 
Peter







Discussion:

Resolving patch:
diff --git a/gluon/globals.py b/gluon/globals.py
--- a/gluon/globals.py
+++ b/gluon/globals.py
@@ -349,14 +349,14 @@ def requires_https(self):
             current.session.forget()
             redirect(URL(scheme='https', args=self.args, vars=self.vars))
 
-    def restful(self):
+    def restful(self, ignore_extension=False):
         def wrapper(action, request=self):
             def f(_action=action, *a, **b):
                 request.is_restful = True
                 env = request.env
                 is_json = env.content_type=='application/json'
                 method = env.request_method
-                if len(request.args) and '.' in request.args[-1]:
+                if not ignore_extension and len(request.args) and '.' in request.args[-1]:
                     request.args[-1], _, request.extension = request.args[-1].rpartition('.')
                     current.response.headers['Content-Type'] = \
                         contenttype('.' + request.extension.lower())


Test patch:
diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py
--- a/gluon/tests/test_globals.py
+++ b/gluon/tests/test_globals.py
@@ -10,27 +10,76 @@
 import unittest
 
 from gluon.globals import Request, Response, Session
+from gluon.rewrite import regex_url_in
 from gluon import URL
 from gluon._compat import basestring
 
+
 def setup_clean_session():
-        request = Request(env={})
-        request.application = 'a'
-        request.controller = 'c'
-        request.function = 'f'
-        request.folder = 'applications/admin'
-        response = Response()
-        session = Session()
-        session.connect(request, response)
+    request = Request(env={})
+    request.application = 'a'
+    request.controller = 'c'
+    request.function = 'f'
+    request.folder = 'applications/admin'
+    response = Response()
+    session = Session()
+    session.connect(request, response)
+    from gluon.globals import current
+    current.request = request
+    current.response = response
+    current.session = session
+    return current
+
+
+class testRequest(unittest.TestCase):
+
+    def setUp(self):
         from gluon.globals import current
-        current.request = request
-        current.response = response
-        current.session = session
-        return current
+        current.response = Response()
+
+    def test_restful_simple(self):
+        env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/1.pdf'}
+        r = Request(env)
+        regex_url_in(r, env)
+
+        @r.restful()
+        def simple_rest():
+            def GET(*args, **vars):
+                return args[0]
+            return locals()
+
+        self.assertEqual(simple_rest(), '1')
+
+    def test_restful_calls_post(self):
+        env = {'request_method': 'POST', 'PATH_INFO': '/welcome/default/index'}
+        r = Request(env)
+        regex_url_in(r, env)
+
+        @r.restful()
+        def post_rest():
+            def POST(*args, **vars):
+                return 'I posted'
+            return locals()
+
+        self.assertEqual(post_rest(), 'I posted')
+
+    def test_restful_ignore_extension(self):
+        env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/127.0.0.1'}
+        r = Request(env)
+        regex_url_in(r, env)
+
+        @r.restful(ignore_extension=True)
+        def ignore_rest():
+            def GET(*args, **vars):
+                return args[0]
+            return locals()
+
+        self.assertEqual(ignore_rest(), '127.0.0.1')
+
 
 class testResponse(unittest.TestCase):
 
-    #port from python 2.7, needed for 2.5 and 2.6 tests
+    # port from python 2.7, needed for 2.5 and 2.6 tests
     def assertRegexpMatches(self, text, expected_regexp, msg=None):
         """Fail the test unless the text matches the regular expression."""
         if isinstance(expected_regexp, basestring):
@@ -99,8 +148,8 @@ def return_includes(response, extensions=None):
         response.files.append(URL('a', 'static', 'css/file.css'))
         content = return_includes(response)
         self.assertEqual(content,
-            '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
-            '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
+                         '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
+                         '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
 
         response = Response()
         response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
@@ -109,12 +158,11 @@ def return_includes(response, extensions=None):
         response.files.append(URL('a', 'static', 'css/file.ts'))
         content = return_includes(response)
         self.assertEqual(content,
-            '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
-            '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
-            '<script src="/a/static/css/file.ts" type="text/typescript"></script>' +
-            '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
-        )
-
+                         '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
+                         '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
+                         '<script src="/a/static/css/file.ts" type="text/typescript"></script>' +
+                         '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
+                         )
 
         response = Response()
         response.files.append(URL('a', 'static', 'css/file.js'))
@@ -122,13 +170,13 @@ def return_includes(response, extensions=None):
         content = return_includes(response, extensions=['css'])
         self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
 
-        #regr test for #628
+        # regr test for #628
         response = Response()
         response.files.append('http://maps.google.com/maps/api/js?sensor=false')
         content = return_includes(response)
         self.assertEqual(content, '')
 
-        #regr test for #628
+        # regr test for #628
         response = Response()
         response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
         content = return_includes(response)
@@ -147,7 +195,7 @@ def return_includes(response, extensions=None):
     def test_cookies(self):
         current = setup_clean_session()
         cookie = str(current.response.cookies)
-        session_key='%s=%s'%(current.response.session_id_name,current.response.session_id)
+        session_key = '%s=%s' % (current.response.session_id_name, current.response.session_id)
         self.assertRegexpMatches(cookie, r'^Set-Cookie: ')
         self.assertTrue(session_key in cookie)
         self.assertTrue('Path=/' in cookie)