diff --git a/gns3server/handlers/api/controller/project_handler.py b/gns3server/handlers/api/controller/project_handler.py index 07ddad03..f956526f 100644 --- a/gns3server/handlers/api/controller/project_handler.py +++ b/gns3server/handlers/api/controller/project_handler.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import aiohttp import asyncio @@ -249,3 +250,85 @@ class ProjectHandler: yield from response.drain() yield from response.write_eof() + + @Route.get( + r"/projects/{project_id}/files/{path:.+}", + description="Get a file from a project. Beware you have warranty to be able to access only to file global to the project (for example README.txt)", + parameters={ + "project_id": "Project UUID", + }, + status_codes={ + 200: "File returned", + 403: "Permission denied", + 404: "The file doesn't exist" + }) + def get_file(request, response): + + controller = Controller.instance() + project = controller.get_project(request.match_info["project_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": + raise aiohttp.web.HTTPForbidden + path = os.path.join(project.path, path) + + response.content_type = "application/octet-stream" + response.set_status(200) + response.enable_chunked_encoding() + # Very important: do not send a content length otherwise QT closes the connection (curl can consume the feed) + response.content_length = None + + try: + with open(path, "rb") as f: + response.start(request) + while True: + data = f.read(4096) + if not data: + break + yield from response.write(data) + + except FileNotFoundError: + raise aiohttp.web.HTTPNotFound() + except PermissionError: + raise aiohttp.web.HTTPForbidden() + + @Route.post( + r"/projects/{project_id}/files/{path:.+}", + description="Write a file to a project", + parameters={ + "project_id": "Project UUID", + }, + raw=True, + status_codes={ + 200: "File returned", + 403: "Permission denied", + 404: "The path doesn't exist" + }) + def write_file(request, response): + + controller = Controller.instance() + project = controller.get_project(request.match_info["project_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": + raise aiohttp.web.HTTPForbidden + path = os.path.join(project.path, path) + + response.set_status(200) + + try: + with open(path, 'wb+') as f: + while True: + packet = yield from request.content.read(512) + if not packet: + break + f.write(packet) + + except FileNotFoundError: + raise aiohttp.web.HTTPNotFound() + except PermissionError: + raise aiohttp.web.HTTPForbidden() diff --git a/tests/handlers/api/compute/test_project.py b/tests/handlers/api/compute/test_project.py index 48deea76..86011dff 100644 --- a/tests/handlers/api/compute/test_project.py +++ b/tests/handlers/api/compute/test_project.py @@ -167,6 +167,21 @@ def test_get_file(http_compute, tmpdir): assert response.status == 403 +def test_write_file(http_compute, tmpdir): + + with patch("gns3server.config.Config.get_section_config", return_value={"projects_path": str(tmpdir)}): + project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b") + + response = http_compute.post("/projects/{project_id}/files/hello".format(project_id=project.id), body="world", raw=True) + assert response.status == 200 + + with open(os.path.join(project.path, "hello")) as f: + assert f.read() == "world" + + response = http_compute.post("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True) + assert response.status == 403 + + def test_stream_file(http_compute, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"projects_path": str(tmpdir)}): diff --git a/tests/handlers/api/controller/test_project.py b/tests/handlers/api/controller/test_project.py index 65f0333d..88ac41d1 100644 --- a/tests/handlers/api/controller/test_project.py +++ b/tests/handlers/api/controller/test_project.py @@ -175,3 +175,30 @@ def test_export(http_controller, tmpdir, loop, project): with myzip.open("a") as myfile: content = myfile.read() assert content == b"hello" + + +def test_get_file(http_controller, tmpdir, loop, project): + os.makedirs(project.path, exist_ok=True) + with open(os.path.join(project.path, 'hello'), 'w+') as f: + f.write('world') + + response = http_controller.get("/projects/{project_id}/files/hello".format(project_id=project.id), raw=True) + assert response.status == 200 + assert response.body == b"world" + + response = http_controller.get("/projects/{project_id}/files/false".format(project_id=project.id), raw=True) + assert response.status == 404 + + response = http_controller.get("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True) + assert response.status == 403 + + +def test_write_file(http_controller, tmpdir, project): + response = http_controller.post("/projects/{project_id}/files/hello".format(project_id=project.id), body="world", raw=True) + assert response.status == 200 + + with open(os.path.join(project.path, "hello")) as f: + assert f.read() == "world" + + response = http_controller.post("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True) + assert response.status == 403