Repository: aio-libs/aiohttp

Issue:
request that StaticRoute has the option to follow symlinks
Currently `.resolve(), relative_to()` [here](https://github.com/KeepSafe/aiohttp/blob/master/aiohttp/web_urldispatcher.py#L399-L400) causes `ValueError` to be raised if the path is a symlink to a file outsite the directory and 404 is returned.

Would be good if there was a a way to get `StaticRoute` follow symlinks, obviously default to false.



Discussion:
You are asking for breaking security sandbox :)
But I'm ok with it unless the requested behavior is off by default.

Yes, I think it would be ok if it's off by default. Same as apache and nginx.

PR is welcome :)

will do.

A few of related extensions to `StaticRoute` which I would suggest:
- `/path/to/dir` serves `path/to/dir/index.html` if it exists
- `/path/to/file` serves `path/to/file.html` if it exists - this is slightly less standard.
- `/path/to/file` serves `/path/to/file` as `content-type: text/html` if `file` starts with `<!DOCTYPE` - not a convertion but would be useful where there's no easy way to set file metadata like content type. **Update:** This is perhaps a less good idea as it would mean opening every file without an extension twice: once to check first line, once for `sendfile`. Also "clean urls" can be accomplished with the above options.

Example [here](https://github.com/samuelcolvin/aiohttp-devtools/blob/1ea5b74aed8febad481a5ff53182bc9985758d31/aiohttp_devtools/runserver/serve.py#L251-L274) of the first two.

I can do on the same PR, or a new PR or create a new issue to discuss?

`index.html` should be optional -- now we display a directory index if `show_index=True`.
`file->file.html` convention looks harmless but should be optional too.
The third option is really strange.

Anyway, please add these extensions in separate PR.

Will do, third option is available with S3 if you set the content-type metadata on the file which is why I suggested it.

Will do PRs when I have a chance.

File system has no place for metadata.
Let's don't invite semi-working  strategy for this.
`<!DOCTYPE` is available for html pages only, it's not hard to name a file like `fname.html` and file type will be detected by file extension.
For other resources (images, js/css files etc) content type should be autocalculated by `mimetypes.guess_type` anyway and `FileSender` does this call.


Resolving patch:
diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py
--- a/aiohttp/web_urldispatcher.py
+++ b/aiohttp/web_urldispatcher.py
@@ -335,7 +335,7 @@ class StaticResource(PrefixResource):
     def __init__(self, prefix, directory, *, name=None,
                  expect_handler=None, chunk_size=256*1024,
                  response_factory=StreamResponse,
-                 show_index=False):
+                 show_index=False, follow_symlinks=False):
         super().__init__(prefix, name=name)
         try:
             directory = Path(directory)
@@ -351,6 +351,7 @@ def __init__(self, prefix, directory, *, name=None,
         self._file_sender = FileSender(resp_factory=response_factory,
                                        chunk_size=chunk_size)
         self._show_index = show_index
+        self._follow_symlinks = follow_symlinks
 
         self._routes = {'GET': ResourceRoute('GET', self._handle, self,
                                              expect_handler=expect_handler),
@@ -397,7 +398,8 @@ def _handle(self, request):
         filename = unquote(request.match_info['filename'])
         try:
             filepath = self._directory.joinpath(filename).resolve()
-            filepath.relative_to(self._directory)
+            if not self._follow_symlinks:
+                filepath.relative_to(self._directory)
         except (ValueError, FileNotFoundError) as error:
             # relatively safe
             raise HTTPNotFound() from error
@@ -709,7 +711,7 @@ def add_route(self, method, path, handler,
 
     def add_static(self, prefix, path, *, name=None, expect_handler=None,
                    chunk_size=256*1024, response_factory=StreamResponse,
-                   show_index=False):
+                   show_index=False, follow_symlinks=False):
         """Add static files view.
 
         prefix - url prefix
@@ -725,7 +727,8 @@ def add_static(self, prefix, path, *, name=None, expect_handler=None,
                                   expect_handler=expect_handler,
                                   chunk_size=chunk_size,
                                   response_factory=response_factory,
-                                  show_index=show_index)
+                                  show_index=show_index,
+                                  follow_symlinks=follow_symlinks)
         self._reg_resource(resource)
         return resource
 


Test patch:
diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py
--- a/tests/test_web_urldispatcher.py
+++ b/tests/test_web_urldispatcher.py
@@ -75,6 +75,36 @@ def test_access_root_of_static_handler(tmp_dir_path, loop, test_client,
     yield from r.release()
 
 
+@pytest.mark.parametrize('data', ['hello world'])
+@asyncio.coroutine
+def test_follow_symlink(tmp_dir_path, loop, test_client, data):
+    """
+    Tests the access to a symlink, in static folder
+    """
+    my_dir_path = os.path.join(tmp_dir_path, 'my_dir')
+    os.mkdir(my_dir_path)
+
+    my_file_path = os.path.join(my_dir_path, 'my_file_in_dir')
+    with open(my_file_path, 'w') as fw:
+        fw.write(data)
+
+    my_symlink_path = os.path.join(tmp_dir_path, 'my_symlink')
+    os.symlink(my_dir_path, my_symlink_path)
+
+    app = web.Application(loop=loop)
+
+    # Register global static route:
+    app.router.add_static('/', tmp_dir_path, follow_symlinks=True)
+    client = yield from test_client(app)
+
+    # Request the root of the static directory.
+    r = yield from client.get('/my_symlink/my_file_in_dir')
+    assert r.status == 200
+    assert (yield from r.text()) == data
+
+    yield from r.release()
+
+
 @pytest.mark.parametrize('dir_name,filename,data', [
     ('', 'test file.txt', 'test text'),
     ('test dir name', 'test dir file .txt', 'test text file folder')