Repository: Gallopsled/pwntools

Issue:
Update check prevents import if home dir not readable
This is the case on pwnable.kr right now

```bash
asm@ubuntu:~$ python
Python 2.7.12 (default, Jul  1 2016, 15:12:24)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/pwn/__init__.py", line 16, in <module>
    pwnlib.update.check_automatically()
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 185, in check_automatically
    if should_check():
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 101, in should_check
    if read(cache_file()).strip() == 'never':
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 88, in cache_file
    os.makedirs(cache_dir)
  File "/usr/lib/python2.7/os.py", line 157, in makedirs
    mkdir(name, mode)
OSError: [Errno 13] Permission denied: '/home/asm/.pwntools-cache'
```

So basically because the update decided to try today, and because pwnable has a weird setup, I can't 'conveniently' use pwntools.


Discussion:
Huh. I'll fix this sometime tomorrow, unless you want to contribute a pull
request first!
On Sun, Nov 27, 2016 at 1:31 PM Grazfather <notifications@github.com> wrote:

> This is the case on pwnable.kr right now
>
> asm@ubuntu:~$ python
> Python 2.7.12 (default, Jul  1 2016, 15:12:24)
> [GCC 5.4.0 20160609] on linux2
> Type "help", "copyright", "credits" or "license" for more information.>>> from pwn import *
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "/usr/local/lib/python2.7/dist-packages/pwn/__init__.py", line 16, in <module>
>     pwnlib.update.check_automatically()
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 185, in check_automatically
>     if should_check():
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 101, in should_check
>     if read(cache_file()).strip() == 'never':
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/update.py", line 88, in cache_file
>     os.makedirs(cache_dir)
>   File "/usr/lib/python2.7/os.py", line 157, in makedirs
>     mkdir(name, mode)
> OSError: [Errno 13] Permission denied: '/home/asm/.pwntools-cache'
>
> So basically because the update decided to try today, and because pwnable
> has a weird setup, I can't 'conveniently' use pwntools.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/Gallopsled/pwntools/issues/799>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAG0GFpDDlS_Rrq_grXtLygQ8UfFoM0rks5rCeibgaJpZM4K9P22>
> .
>


Resolving patch:
diff --git a/pwnlib/context/__init__.py b/pwnlib/context/__init__.py
--- a/pwnlib/context/__init__.py
+++ b/pwnlib/context/__init__.py
@@ -10,6 +10,7 @@
 import os
 import platform
 import socket
+import stat
 import string
 import subprocess
 import sys
@@ -1126,6 +1127,45 @@ def adb(self):
 
         return command
 
+    @property
+    def cache_dir(self):
+        """Directory used for caching data.
+
+        Note:
+            May be either a path string, or ``None``.
+
+        Example:
+
+            >>> cache_dir = context.cache_dir
+            >>> cache_dir is not None
+            True
+            >>> os.chmod(cache_dir, 0o000)
+            >>> context.cache_dir is None
+            True
+            >>> os.chmod(cache_dir, 0o755)
+            >>> cache_dir == context.cache_dir
+            True
+        """
+        home = os.path.expanduser('~')
+
+        if not os.access(home, os.W_OK):
+            return None
+
+        cache = os.path.join(home, '.pwntools-cache')
+
+        if not os.path.exists(cache):
+            try:
+                os.mkdir(cache)
+            except OSError:
+                return None
+
+        # Some wargames e.g. pwnable.kr have created dummy directories
+        # which cannot be modified by the user account (owned by root).
+        if not os.access(cache, os.W_OK):
+            return None
+
+        return cache
+
 
     #*************************************************************************
     #                               ALIASES
diff --git a/pwnlib/shellcraft/internal.py b/pwnlib/shellcraft/internal.py
--- a/pwnlib/shellcraft/internal.py
+++ b/pwnlib/shellcraft/internal.py
@@ -1,6 +1,8 @@
 import os
 from collections import defaultdict
 
+from ..context import context
+
 __all__ = ['make_function']
 
 loaded = {}
@@ -33,10 +35,14 @@ def go_inside(self):
 
     render_global = IsInside()
 
+    cache  = context.cache_dir
+    if cache:
+        cache = os.path.join(cache, 'mako')
+
     curdir = os.path.dirname(os.path.abspath(__file__))
     lookup = TemplateLookup(
         directories      = [os.path.join(curdir, 'templates')],
-        module_directory = os.path.expanduser('~/.pwntools-cache/mako')
+        module_directory = cache
     )
 
     # The purpose of this definition is to create a new Tag.
diff --git a/pwnlib/update.py b/pwnlib/update.py
--- a/pwnlib/update.py
+++ b/pwnlib/update.py
@@ -53,7 +53,11 @@ def available_on_pypi(prerelease=current_version.is_prerelease):
 
 def cache_file():
     """Returns the path of the file used to cache update data, and ensures that it exists."""
-    cache_dir  = os.path.expanduser('~/.pwntools-cache')
+    cache_dir = context.cache_dir
+
+    if not cache_dir:
+        return None
+
     cache_file = os.path.join(cache_dir, 'update')
 
     if not os.path.isdir(cache_dir):
@@ -66,12 +70,23 @@ def cache_file():
 
 def last_check():
     """Return the date of the last check"""
-    return os.path.getmtime(cache_file())
+    cache = cache_file()
+    if cache:
+        return os.path.getmtime(cache_file())
+
+    # Fallback
+    return time.time()
 
 def should_check():
     """Return True if we should check for an update"""
-    if read(cache_file()).strip() == 'never':
+    filename = cache_file()
+
+    if not filename:
         return False
+
+    if read(filename).strip() == 'never':
+        return False
+
     return time.time() > (last_check() + update_freq)
 
 def perform_check(prerelease=current_version.is_prerelease):
@@ -109,7 +124,10 @@ def perform_check(prerelease=current_version.is_prerelease):
     where = None
     command = None
 
-    os.utime(cache_file(), None)
+    cache = cache_file()
+
+    if cache:
+        os.utime(cache, None)
 
     if best == current_version:
         log.info("You have the latest version of Pwntools (%s)" % best)


Test patch: