mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Refactor how clients access PCAP capture files. Fixes https://github.com/GNS3/gns3-gui/issues/2438.
* The PCAP file is directly accessed if controller and client are on the same host. * The PCAP file is streamed from the compute server to the client with the controller as a proxy when the controller is remote for the client.
This commit is contained in:
1 parent
bf1b801cc0
commit
2764828f38
47 files changed
+1071
-473
No files matched your search
@@ -330,31 +330,17 @@ class Compute:
|
||||
:returns: A file stream
|
||||
"""
|
||||
|
||||
# Due to Python 3.4 limitation we can't use with and asyncio
|
||||
# https://www.python.org/dev/peps/pep-0492/
|
||||
# that why we wrap the answer
|
||||
class StreamResponse:
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
def __enter__(self):
|
||||
return self._response.content
|
||||
|
||||
def __exit__(self):
|
||||
self._response.close()
|
||||
|
||||
url = self._getUrl("/projects/{}/stream/{}".format(project.id, path))
|
||||
response = await self._session().request("GET", url, auth=self._auth, timeout=timeout)
|
||||
if response.status == 404:
|
||||
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(path))
|
||||
raise aiohttp.web.HTTPNotFound(text="file '{}' not found on compute".format(path))
|
||||
elif response.status == 403:
|
||||
raise aiohttp.web.HTTPForbidden(text="forbidden to open {} on compute".format(path))
|
||||
raise aiohttp.web.HTTPForbidden(text="forbidden to open '{}' on compute".format(path))
|
||||
elif response.status != 200:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Unexpected error {}: {}: while opening {} on compute".format(response.status,
|
||||
response.reason,
|
||||
path))
|
||||
return StreamResponse(response)
|
||||
return response
|
||||
|
||||
async def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,6 @@ import os
|
||||
import re
|
||||
import uuid
|
||||
import html
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
import logging
|
||||
@@ -118,6 +117,7 @@ class Link:
|
||||
self._nodes = []
|
||||
self._project = project
|
||||
self._capturing = False
|
||||
self._capture_node = None
|
||||
self._capture_file_name = None
|
||||
self._streaming_pcap = None
|
||||
self._created = False
|
||||
@@ -139,6 +139,34 @@ class Link:
|
||||
"""
|
||||
return self._nodes
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
"""
|
||||
Get the project this link belongs to.
|
||||
|
||||
:return: Project instance.
|
||||
"""
|
||||
return self._project
|
||||
|
||||
@property
|
||||
def capture_node(self):
|
||||
"""
|
||||
Get the capturing node
|
||||
|
||||
:return: Node instance.
|
||||
"""
|
||||
return self._capture_node
|
||||
|
||||
@property
|
||||
def compute(self):
|
||||
"""
|
||||
Get the capturing node
|
||||
|
||||
:return: Node instance.
|
||||
"""
|
||||
assert self.capture_node
|
||||
return self.capture_node["node"].compute
|
||||
|
||||
def get_active_filters(self):
|
||||
"""
|
||||
Return the active filters.
|
||||
@@ -289,44 +317,8 @@ class Link:
|
||||
|
||||
self._capturing = True
|
||||
self._capture_file_name = capture_file_name
|
||||
self._streaming_pcap = asyncio.ensure_future(self._start_streaming_pcap())
|
||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
||||
|
||||
async def _start_streaming_pcap(self):
|
||||
"""
|
||||
Dump a pcap file on disk
|
||||
"""
|
||||
|
||||
if os.path.exists(self.capture_file_path):
|
||||
try:
|
||||
os.remove(self.capture_file_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not delete old capture file '{}': {}".format(self.capture_file_path, e))
|
||||
|
||||
try:
|
||||
stream_content = await self.read_pcap_from_source()
|
||||
except aiohttp.web.HTTPException as e:
|
||||
error_msg = "Could not stream PCAP file: error {}: {}".format(e.status, e.text)
|
||||
log.error(error_msg)
|
||||
self._capturing = False
|
||||
self._project.notification.project_emit("log.error", {"message": error_msg})
|
||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
||||
|
||||
with stream_content as stream:
|
||||
try:
|
||||
with open(self.capture_file_path, "wb") as f:
|
||||
while self._capturing:
|
||||
# We read 1 bytes by 1 otherwise the remaining data is not read if the traffic stops
|
||||
data = await stream.read(1)
|
||||
if data:
|
||||
f.write(data)
|
||||
# Flush to disk otherwise the live is not really live
|
||||
f.flush()
|
||||
else:
|
||||
break
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not write capture file '{}': {}".format(self.capture_file_path, e))
|
||||
|
||||
async def stop_capture(self):
|
||||
"""
|
||||
Stop capture on the link
|
||||
@@ -335,12 +327,26 @@ class Link:
|
||||
self._capturing = False
|
||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
||||
|
||||
async def _read_pcap_from_source(self):
|
||||
def pcap_streaming_url(self):
|
||||
"""
|
||||
Return a FileStream of the Pcap from the compute server
|
||||
Get the PCAP streaming URL on compute
|
||||
|
||||
:returns: URL
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
assert self.capture_node
|
||||
compute = self.capture_node["node"].compute
|
||||
node_type = self.capture_node["node"].node_type
|
||||
node_id = self.capture_node["node"].id
|
||||
adapter_number = self.capture_node["adapter_number"]
|
||||
port_number = self.capture_node["port_number"]
|
||||
url = "/projects/{project_id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap".format(project_id=self.project.id,
|
||||
node_type=node_type,
|
||||
node_id=node_id,
|
||||
adapter_number=adapter_number,
|
||||
port_number=port_number)
|
||||
|
||||
return compute._getUrl(url)
|
||||
|
||||
async def node_updated(self, node):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ class UDPLink(Link):
|
||||
|
||||
def __init__(self, project, link_id=None):
|
||||
super().__init__(project, link_id=link_id)
|
||||
self._capture_node = None
|
||||
self._created = False
|
||||
self._link_data = []
|
||||
|
||||
@@ -164,10 +163,8 @@ class UDPLink(Link):
|
||||
if not capture_file_name:
|
||||
capture_file_name = self.default_capture_file_name()
|
||||
self._capture_node = self._choose_capture_side()
|
||||
data = {
|
||||
"capture_file_name": capture_file_name,
|
||||
"data_link_type": data_link_type
|
||||
}
|
||||
data = {"capture_file_name": capture_file_name,
|
||||
"data_link_type": data_link_type}
|
||||
await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/start_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]), data=data)
|
||||
await super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name)
|
||||
|
||||
@@ -210,14 +207,6 @@ class UDPLink(Link):
|
||||
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot capture because there is no running device on this link")
|
||||
|
||||
async def read_pcap_from_source(self):
|
||||
"""
|
||||
Return a FileStream of the Pcap from the compute node
|
||||
"""
|
||||
if self._capture_node:
|
||||
compute = self._capture_node["node"].compute
|
||||
return compute.stream_file(self._project, "tmp/captures/" + self._capture_file_name)
|
||||
|
||||
async def node_updated(self, node):
|
||||
"""
|
||||
Called when a node member of the link is updated
|
||||
|
||||
Reference in new issue
Block a user