Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datasette on Amazon Linux on ARM returns 404 for static assets #1124

Closed
simonw opened this issue Dec 3, 2020 · 9 comments
Closed

Datasette on Amazon Linux on ARM returns 404 for static assets #1124

simonw opened this issue Dec 3, 2020 · 9 comments
Labels

Comments

@simonw
Copy link
Owner

simonw commented Dec 3, 2020

Very weird bug this one. Steps to reproduce:

# I started a amzn2-ami-hvm-2.0.20201126.0-arm64-gp2 t4g.micro instance
ec2 % ssh -i simonw-ec2.pem ec2-user@ec2-18-219-238-192.us-east-2.compute.amazonaws.com
[ec2-user@ip-172-31-30-7 ~]$ sudo yum install python3
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
...
Is this ok [y/d/N]: y
Downloading packages:
(1/4): python3-3.7.9-1.amzn2.0.1.aarch64.rpm                                                                                        |  72 kB  00:00:00     
(2/4): python3-pip-9.0.3-1.amzn2.0.2.noarch.rpm                                                                                     | 1.9 MB  00:00:00     
(3/4): python3-setuptools-38.4.0-3.amzn2.0.6.noarch.rpm                                                                             | 617 kB  00:00:00     
(4/4): python3-libs-3.7.9-1.amzn2.0.1.aarch64.rpm                                                                                   | 9.1 MB  00:00:00     
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Total                                                                                                                       68 MB/s |  12 MB  00:00:00     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : python3-pip-9.0.3-1.amzn2.0.2.noarch                                                                                                    1/4 
  Installing : python3-3.7.9-1.amzn2.0.1.aarch64                                                                                                       2/4 
  Installing : python3-setuptools-38.4.0-3.amzn2.0.6.noarch                                                                                            3/4 
  Installing : python3-libs-3.7.9-1.amzn2.0.1.aarch64                                                                                                  4/4 
  Verifying  : python3-setuptools-38.4.0-3.amzn2.0.6.noarch                                                                                            1/4 
  Verifying  : python3-pip-9.0.3-1.amzn2.0.2.noarch                                                                                                    2/4 
  Verifying  : python3-3.7.9-1.amzn2.0.1.aarch64                                                                                                       3/4 
  Verifying  : python3-libs-3.7.9-1.amzn2.0.1.aarch64                                                                                                  4/4 

Installed:
  python3.aarch64 0:3.7.9-1.amzn2.0.1                                                                                                                      

Dependency Installed:
  python3-libs.aarch64 0:3.7.9-1.amzn2.0.1          python3-pip.noarch 0:9.0.3-1.amzn2.0.2          python3-setuptools.noarch 0:38.4.0-3.amzn2.0.6         

Complete!
[ec2-user@ip-172-31-30-7 ~]$ python3 -m pip install --user pipx
Collecting pipx
  Downloading https://files.pythonhosted.org/packages/42/8a/8447ec14562c5d97afbee54ec390864718bccce9dfb0506c8c77852489d3/pipx-0.15.6.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 3.8MB/s 
Collecting packaging>=20.0 (from pipx)
  Downloading https://files.pythonhosted.org/packages/28/87/8edcf555adaf60d053ead881bc056079e29319b643ca710339ce84413136/packaging-20.7-py2.py3-none-any.whl
Collecting argcomplete<2.0,>=1.9.4 (from pipx)
  Downloading https://files.pythonhosted.org/packages/e3/d0/ee7fc6ceac8957196def9bfa3b955d02163058defd3edd51ef7b1ff2769e/argcomplete-1.12.2-py2.py3-none-any.whl
Collecting userpath>=1.4.1 (from pipx)
  Downloading https://files.pythonhosted.org/packages/fb/f1/faca8a3cc86bd2223aaf72edd222bc31d6ae2eea5feaf17a144634a92e6d/userpath-1.4.1-py2.py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging>=20.0->pipx)
  Downloading https://files.pythonhosted.org/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 8.8MB/s 
Collecting importlib-metadata<4,>=0.23; python_version == "3.7" (from argcomplete<2.0,>=1.9.4->pipx)
  Downloading https://files.pythonhosted.org/packages/99/c7/4ccf2baa455613aa9e61372365aba8594ff2806c82189c31a6c65e7c679e/importlib_metadata-3.1.1-py3-none-any.whl
Collecting click (from userpath>=1.4.1->pipx)
  Downloading https://files.pythonhosted.org/packages/d2/3d/fa76db83bf75c4f8d338c2fd15c8d33fdd7ad23a9b5e57eb6c5de26b430e/click-7.1.2-py2.py3-none-any.whl (82kB)
    100% |████████████████████████████████| 92kB 10.5MB/s 
Collecting distro; platform_system == "Linux" (from userpath>=1.4.1->pipx)
  Downloading https://files.pythonhosted.org/packages/25/b7/b3c4270a11414cb22c6352ebc7a83aaa3712043be29daa05018fd5a5c956/distro-1.5.0-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata<4,>=0.23; python_version == "3.7"->argcomplete<2.0,>=1.9.4->pipx)
  Downloading https://files.pythonhosted.org/packages/41/ad/6a4f1a124b325618a7fb758b885b68ff7b058eec47d9220a12ab38d90b1f/zipp-3.4.0-py3-none-any.whl
Installing collected packages: pyparsing, packaging, zipp, importlib-metadata, argcomplete, click, distro, userpath, pipx
Successfully installed argcomplete-1.12.2 click-7.1.2 distro-1.5.0 importlib-metadata-3.1.1 packaging-20.7 pipx-0.15.6.0 pyparsing-2.4.7 userpath-1.4.1 zipp-3.4.0
[ec2-user@ip-172-31-30-7 ~]$ python3 -m pipx ensurepath
/home/ec2-user/.local/bin is already in PATH.
/home/ec2-user/.local/bin is already in PATH.

All pipx binary directories have been added to PATH. If you are sure
you want to proceed, try again with the '--force' flag.

Otherwise pipx is ready to go! ✨ 🌟 ✨
[ec2-user@ip-172-31-30-7 ~]$ pipx install datasette
  installed package datasette 0.52.2, Python 3.7.9
  These apps are now globally available
    - datasette
done! ✨ 🌟 ✨
[ec2-user@ip-172-31-30-7 ~]$ datasette --version
datasette, version 0.52.2
[ec2-user@ip-172-31-30-7 ~]$ datasette --get /-/static/app.css
404
@simonw simonw added the bug label Dec 3, 2020
@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

The CSS files are in the expected location:

[ec2-user@ip-172-31-30-7 ~]$ find /home/ec2-user/.local/pipx/venvs/datasette | grep css
/home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/static/app.css
/home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/static/codemirror-5.57.0.min.css

Wow it's running an ANCIENT version of SQLite:

[ec2-user@ip-172-31-30-7 ~]$ datasette --get /-/versions.json
{"python": {"version": "3.7.9", "full": "3.7.9 (default, Aug 27 2020, 21:58:41) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]"}, "datasette": {"version": "0.52.2"}, "asgi": "3.0", "uvicorn": "0.12.3", "sqlite": {"version": "3.7.17", "fts_versions": ["FTS4", "FTS3"], "extensions": {}, "compile_options": ["DISABLE_DIRSYNC", "ENABLE_COLUMN_METADATA", "ENABLE_FTS3", "ENABLE_RTREE", "ENABLE_UNLOCK_NOTIFY", "SECURE_DELETE", "TEMP_STORE=1", "THREADSAFE=1"]}}

http://www.sqlite.org/releaselog/3_7_17.html - SQLite Release 3.7.17 On 2013-05-20

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

Added a line to print out app_root from

datasette/datasette/app.py

Lines 848 to 853 in e048791

add_route(favicon, "/favicon.ico")
add_route(
asgi_static(app_root / "datasette" / "static"), r"/-/static/(?P<path>.*)$"
)
for path, dirname in self.static_mounts:

app_root =  /home/ec2-user/.local/pipx/venvs/datasette/lib64/python3.7/site-packages

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

I'm suspicious of this code here:

def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
async def inner_static(request, send):
path = request.scope["url_route"]["kwargs"]["path"]
try:
full_path = (Path(root_path) / path).resolve().absolute()
except FileNotFoundError:
await asgi_send_html(send, "404", 404)
return
if full_path.is_dir():
await asgi_send_html(send, "403: Directory listing is not allowed", 403)
return
# Ensure full_path is within root_path to avoid weird "../" tricks
try:
full_path.relative_to(root_path)
except ValueError:
await asgi_send_html(send, "404", 404)
return
try:
await asgi_send_file(send, full_path, chunk_size=chunk_size)
except FileNotFoundError:
await asgi_send_html(send, "404", 404)
return
return inner_static

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

I replaced that function with this code:

def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None):
    async def inner_static(request, send):
        path = request.scope["url_route"]["kwargs"]["path"]
        print("path =", path)
        try:
            full_path = (Path(root_path) / path).resolve().absolute()
        except FileNotFoundError as e:
            print("FileNotFoundError:", e)
            await asgi_send_html(send, "404", 404)
            return
        if full_path.is_dir():
            await asgi_send_html(send, "403: Directory listing is not allowed", 403)
            return
        # Ensure full_path is within root_path to avoid weird "../" tricks
        try:
            print("full_path={}, root_path={}".format(full_path, root_path))
            full_path.relative_to(root_path)
        except ValueError as e:
            print("  ValueError:", e)
            await asgi_send_html(send, "404", 404)
            return
        try:
            await asgi_send_file(send, full_path, chunk_size=chunk_size)
        except FileNotFoundError:
            await asgi_send_html(send, "404", 404)
            return

    return inner_static

Edited using vi /home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/utils/asgi.py

The output shows me what the bug is:

$ datasette --get /-/static/app.css --pdb
app_root =  /home/ec2-user/.local/pipx/venvs/datasette/lib64/python3.7/site-packages
path = app.css
full_path=/home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/static/app.css, root_path=/home/ec2-user/.local/pipx/venvs/datasette/lib64/python3.7/site-packages/datasette/static
  ValueError: '/home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/static/app.css' does not start with '/home/ec2-user/.local/pipx/venvs/datasette/lib64/python3.7/site-packages/datasette/static'
404

ValueError: '/home/ec2-user/.local/pipx/venvs/datasette/lib/python3.7/site-packages/datasette/static/app.css' does not start with '/home/ec2-user/.local/pipx/venvs/datasette/lib64/python3.7/site-packages/datasette/static'

One is ../lib/python3.7/.. and the other is ../lib64/python3.7/.. - there's clearly some kind of symlink in play here which I'm not taking into account.

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

This fix works - calling .resolve() on the root_path before the comparison to ensure symlinks are resolved:

        # Ensure full_path is within root_path to avoid weird "../" tricks
        try:
            print("full_path={}, root_path={}".format(full_path, root_path))
            full_path.relative_to(root_path.resolve())
        except ValueError as e:
            print("  ValueError:", e)
            await asgi_send_html(send, "404", 404)
            return

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

I'm going to punt on writing a unit test for this (not sure how I'd simulate those symlinks) - I'll manually test it and push out a dot release instead.

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

I tested this by running:

pipx uninstall datasette
pipx install 'https://github.com/simonw/datasette/archive/6b4c55efea3e9d34d92cbe5f0066553ad9b14071.zip'

To replace that version of Datasette (in the correct virtual environment) with this patch. It worked!

[ec2-user@ip-172-31-30-7 ~]$ datasette --get /-/static/app.css
/* Reset and Page Setup ==================================================== */
...

@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

https://github.com/simonw/datasette/runs/1494631261

/home/runner/work/datasette/datasette/tests/test_html.py:81: AssertionError
----------------------------- Captured stderr call -----------------------------
Traceback (most recent call last):
  File "/home/runner/work/datasette/datasette/datasette/app.py", line 1039, in route_path
    response = await view(request, send)
  File "/home/runner/work/datasette/datasette/datasette/utils/asgi.py", line 297, in inner_static
    full_path.relative_to(root_path.resolve())
AttributeError: 'str' object has no attribute 'resolve'

simonw added a commit that referenced this issue Dec 3, 2020
simonw added a commit that referenced this issue Dec 3, 2020
@simonw
Copy link
Owner Author

simonw commented Dec 3, 2020

Confirmed that installing a fresh copy of Datasette 0.52.3 on that server works correctly as expected.

@simonw simonw closed this as completed Dec 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant