Repository: sirMackk/py3tftp

Issue:
local file not found error
I'm running `py3tftp` on Windows 7 64bit and Python 3.7.0

When I run `pytftp` in a folder containing the file `myip.py`, I expect it to be able to serve that file.
However, when I ran a tftp client (on the same machine) and requested this file, I got a `File Not Found` error. Am I doing something wrong? Also, how do I quit without having to kill the process in Task Manager? Ctrl+C etc doesn't work.

Server invocation:
```
 Directory of c:\dev

12/16/2018  10:59 AM    <DIR>          .
12/16/2018  10:59 AM    <DIR>          ..
12/16/2018  10:43 AM               246 myip.py
12/16/2018  11:01 AM               334 myports.py
12/16/2018  11:01 AM    <DIR>          nppBackup
07/13/2009  10:20 PM    <DIR>          PerfLogs
               2 File(s)            580 bytes
               4 Dir(s)  60,627,619,840 bytes free

c:\dev>py3tftp -p 69 -v
2018-12-16 11:11:21,443 [INFO] Starting TFTP server on 0.0.0.0:69
2018-12-16 11:11:21,443 [DEBUG] Using selector: SelectSelector
2018-12-16 11:11:21,443 [INFO] Listening...
2018-12-16 11:11:25,819 [DEBUG] received:  ☺myip.py octet tsize 0
2018-12-16 11:11:25,819 [DEBUG] Request: b'myip.py\x00octet\x00tsize\x000\x00'
2018-12-16 11:11:25,819 [DEBUG] b'tsize'
2018-12-16 11:11:25,819 [DEBUG] packet type: RRQ
2018-12-16 11:11:25,819 [DEBUG] Request: b'myip.py\x00octet\x00tsize\x000\x00'
2018-12-16 11:11:25,819 [DEBUG] b'tsize'
2018-12-16 11:11:25,819 [DEBUG] b'tsize'
2018-12-16 11:11:25,819 [INFO] Initiating RRQProtocol with ('127.0.0.1', 54144)
2018-12-16 11:11:25,819 [DEBUG] Initializing file transfer to ('127.0.0.1', 54144)
2018-12-16 11:11:25,819 [DEBUG] Set protocol attributes as {b'ack_timeout': 0.5, b'timeout': 3.0, b'blksize': 512, b'windowsize': 1, b'tsize': 0}
2018-12-16 11:11:25,819 [ERROR] File "b'myip.py'" does not exist!
2018-12-16 11:11:25,819 [DEBUG] opening pkt: <py3tftp.tftp_packet.TFTPErrPacket object at 0x00000000032E19B0>
2018-12-16 11:11:25,819 [INFO] Closing connection to ('127.0.0.1', 54144) due to error. "b'myip.py'" Not transmitted.
2018-12-16 11:11:25,819 [INFO] Connection to 127.0.0.1:54144 terminated
```

client invocation (using mobaxterm cygwin-like shell):
```
➤ tftp -g -r myip.py 0.0.0.0
tftp: using blksize 512
using server '0.0.0.0:69', remote_file 'myip.py', local_file 'myip.py'
sending 24 bytes
00 01 6d 79 69 70 2e 70 79 00 6f 63 74 65 74 00 74 73 69 7a 65 00 30 00
received 19 bytes: 0005 0001
tftp: server error: (1) File not found
                                         
```

Discussion:

Patch:
diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml
new file mode 100644
index 0000000..8386936
--- /dev/null
+++ b/.github/workflows/actions.yml
@@ -0,0 +1,21 @@
+name: Run Python tests
+on: [push]
+jobs:
+  verify:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest] # TODO: add 'windows-latest'
+        python-version: ['3.5'] # TODO: add '3.9'
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      # Install packages
+      - run: pip install coverage==4.1 flake8==3.5.0
+
+      # Run tests
+      - run: .github/workflows/test.sh
diff --git a/.travis/run.sh b/.github/workflows/test.sh
similarity index 95%
rename from .travis/run.sh
rename to .github/workflows/test.sh
index f549b1f..7f24792 100755
--- a/.travis/run.sh
+++ b/.github/workflows/test.sh
@@ -22,4 +22,4 @@ timeout=15
 
 timeout -s SIGINT "${timeout}" python -m py3tftp --host 127.0.0.1 &> /dev/null &
 sleep 1
-timeout -s SIGINT "${timeout}" python -m unittest discover -s tests/acceptance -p *_test.py
+timeout -s SIGINT "${timeout}" python -m unittest discover -s tests/acceptance -p *_test.py
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5be3b48..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: python
-python:
-  - "3.5"
-
-install: pip install coverage==4.1 flake8==3.5.0
-
-script: ./.travis/run.sh
-
-notifications:
-  email: false
diff --git a/py3tftp/file_io.py b/py3tftp/file_io.py
index ff17b31..edaf72a 100644
--- a/py3tftp/file_io.py
+++ b/py3tftp/file_io.py
@@ -1,5 +1,5 @@
 import os
-import os.path as opath
+from pathlib import Path
 from .netascii import Netascii
 
 
@@ -7,11 +7,21 @@ def sanitize_fname(fname):
     """
     Ensures that fname is a path under the current working directory.
     """
-    root_dir = os.getcwd()
-    return opath.join(
-        bytes(root_dir, encoding='ascii'),
-        opath.normpath(
-            b'/' + fname).lstrip(b'/'))
+    # Remove root (/) and parent (..) directory references.
+    path = os.fsdecode(fname).lstrip('./')
+    abs_path = Path.cwd() / path
+
+    # Verify that the formed path is under the current working directory.
+    try:
+        abs_path.relative_to(Path.cwd())
+    except ValueError:
+        raise FileNotFoundError
+
+    # Verify that we are not accesing a reserved file.
+    if abs_path.is_reserved():
+        raise FileNotFoundError
+
+    return abs_path
 
 
 class FileReader(object):
@@ -25,9 +35,9 @@ class FileReader(object):
     """
 
     def __init__(self, fname, chunk_size=0, mode=None):
+        self._f = None
         self.fname = sanitize_fname(fname)
         self.chunk_size = chunk_size
-        self._f = None
         self._f = self._open_file()
         self.finished = False
 
@@ -35,10 +45,10 @@ class FileReader(object):
             self._f = Netascii(self._f)
 
     def _open_file(self):
-        return open(self.fname, 'rb')
+        return self.fname.open('rb')
 
     def file_size(self):
-        return os.stat(self.fname).st_size
+        return self.fname.stat().st_size
 
     def read_chunk(self, size=None):
         size = size or self.chunk_size
@@ -66,16 +76,16 @@ class FileWriter(object):
     When it goes out of scope, it ensures the file is closed.
     """
     def __init__(self, fname, chunk_size, mode=None):
+        self._f = None
         self.fname = sanitize_fname(fname)
         self.chunk_size = chunk_size
-        self._f = None
         self._f = self._open_file()
 
         if mode == b'netascii':
             self._f = Netascii(self._f)
 
     def _open_file(self):
-        return open(self.fname, 'xb')
+        return self.fname.open('xb')
 
     def _flush(self):
         if self._f:


Test patch:
diff --git a/tests/unit/file_util_test.py b/tests/unit/file_util_test.py
index 171e79e..7be4bf1 100644
--- a/tests/unit/file_util_test.py
+++ b/tests/unit/file_util_test.py
@@ -93,11 +93,8 @@ class FileWriterTest(t.TestCase):
 class TestSanitizeFname(t.TestCase):
     @classmethod
     def setUpClass(cls):
-        from os import getcwd
-        from os.path import join as path_join
-        cls.target_dir = bytes(
-            path_join(getcwd(), 'tmp/testfile'),
-            encoding='ascii')
+        from pathlib import Path
+        cls.target_dir = Path.cwd() / 'tmp/testfile'
 
     def test_under_root_dir(self):
         fname = b'/tmp/testfile'