diff --git a/dev-requirements.txt b/dev-requirements.txt
index 5e29ebc7..ac6470e5 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -4,3 +4,4 @@ pytest-timeout==2.1.0
pytest-asyncio==0.21.1
requests==2.31.0
httpx==0.24.1
+httpx_ws==0.4.2
diff --git a/gns3server/api/routes/compute/__init__.py b/gns3server/api/routes/compute/__init__.py
index 4628dfd8..1922e31a 100644
--- a/gns3server/api/routes/compute/__init__.py
+++ b/gns3server/api/routes/compute/__init__.py
@@ -199,14 +199,12 @@ compute_api.include_router(
compute_api.include_router(
docker_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/docker/nodes",
tags=["Docker nodes"]
)
compute_api.include_router(
dynamips_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/dynamips/nodes",
tags=["Dynamips nodes"]
)
@@ -234,7 +232,6 @@ compute_api.include_router(
compute_api.include_router(
iou_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/iou/nodes",
tags=["IOU nodes"])
@@ -247,28 +244,24 @@ compute_api.include_router(
compute_api.include_router(
qemu_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/qemu/nodes",
tags=["Qemu nodes"]
)
compute_api.include_router(
virtualbox_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/virtualbox/nodes",
tags=["VirtualBox nodes"]
)
compute_api.include_router(
vmware_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/vmware/nodes",
tags=["VMware nodes"]
)
compute_api.include_router(
vpcs_nodes.router,
- dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/vpcs/nodes",
tags=["VPCS nodes"]
)
diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py
index 5efb9927..377a89dc 100644
--- a/gns3server/api/routes/compute/dependencies/authentication.py
+++ b/gns3server/api/routes/compute/dependencies/authentication.py
@@ -15,12 +15,17 @@
# along with this program. If not, see .
import secrets
+import base64
+import binascii
+import logging
-from fastapi import Depends, HTTPException, status
+from fastapi import Depends, HTTPException, WebSocket, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
+from fastapi.security.utils import get_authorization_scheme_param
from gns3server.config import Config
-from typing import Optional
+from typing import Optional, Union
+log = logging.getLogger(__name__)
security = HTTPBasic()
@@ -35,3 +40,44 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends
detail="Invalid compute username or password",
headers={"WWW-Authenticate": "Basic"},
)
+
+async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]:
+ """
+ """
+
+ await websocket.accept()
+
+ # handle basic HTTP authentication
+ invalid_user_credentials_exc = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Basic"},
+ )
+
+ try:
+ authorization = websocket.headers.get("Authorization")
+ scheme, param = get_authorization_scheme_param(authorization)
+ if not authorization or scheme.lower() != "basic":
+ raise invalid_user_credentials_exc
+ try:
+ data = base64.b64decode(param).decode("ascii")
+ except (ValueError, UnicodeDecodeError, binascii.Error):
+ raise invalid_user_credentials_exc
+
+ username, separator, password = data.partition(":")
+ if not separator:
+ raise invalid_user_credentials_exc
+
+ server_settings = Config.instance().settings.Server
+ username = secrets.compare_digest(username, server_settings.compute_username)
+ password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
+ if not (username and password):
+ raise invalid_user_credentials_exc
+
+ except HTTPException as e:
+ err_msg = f"Could not authenticate while connecting to compute WebSocket: {e.detail}"
+ websocket_error = {"action": "log.error", "event": {"message": err_msg}}
+ await websocket.send_json(websocket_error)
+ log.error(err_msg)
+ return await websocket.close(code=1008)
+ return websocket
diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py
index e9a1e29f..11a2cf65 100644
--- a/gns3server/api/routes/compute/docker_nodes.py
+++ b/gns3server/api/routes/compute/docker_nodes.py
@@ -20,15 +20,18 @@ API routes for Docker nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Body, Response, status
+from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
+from typing import Union
from gns3server import schemas
from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
router = APIRouter(responses=responses)
@@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
"""
@@ -85,7 +89,11 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate)
return container.asdict()
-@router.get("/{node_id}", response_model=schemas.Docker)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.Docker,
+ dependencies=[Depends(compute_authentication)]
+)
def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Return a Docker node.
@@ -94,7 +102,11 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.Docker)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.Docker,
+ dependencies=[Depends(compute_authentication)]
+)
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Update a Docker node.
@@ -131,7 +143,11 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
return node.asdict()
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Start a Docker node.
@@ -140,7 +156,11 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Stop a Docker node.
@@ -149,7 +169,11 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Suspend a Docker node.
@@ -158,7 +182,11 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Reload a Docker node.
@@ -167,7 +195,11 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.restart()
-@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/pause",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Pause a Docker node.
@@ -176,7 +208,11 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
-@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/unpause",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Unpause a Docker node.
@@ -185,7 +221,11 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.unpause()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Delete a Docker node.
@@ -194,7 +234,12 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.delete()
-@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/duplicate",
+ response_model=schemas.Docker,
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
async def duplicate_docker_node(
destination_node_id: UUID = Body(..., embed=True),
node: DockerVM = Depends(dep_node)
@@ -211,6 +256,7 @@ async def duplicate_docker_node(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@@ -229,6 +275,7 @@ async def create_docker_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@@ -245,7 +292,11 @@ async def update_docker_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_docker_node_nio(
adapter_number: int,
port_number: int,
@@ -259,7 +310,10 @@ async def delete_docker_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_docker_node_capture(
adapter_number: int,
port_number: int,
@@ -278,7 +332,8 @@ async def start_docker_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
- status_code=status.HTTP_204_NO_CONTENT
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_docker_node_capture(
adapter_number: int,
@@ -293,7 +348,10 @@ async def stop_docker_node_capture(
await node.stop_capture(adapter_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@@ -310,15 +368,23 @@ async def stream_pcap_file(
@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)) -> None:
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: DockerVM = Depends(dep_node)
+) -> None:
"""
Console WebSocket.
"""
- await node.start_websocket_console(websocket)
+ if websocket:
+ await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: DockerVM = Depends(dep_node)) -> None:
await node.reset_console()
diff --git a/gns3server/api/routes/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py
index 5f34f066..89be7f69 100644
--- a/gns3server/api/routes/compute/dynamips_nodes.py
+++ b/gns3server/api/routes/compute/dynamips_nodes.py
@@ -20,16 +20,18 @@ API routes for Dynamips nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Response, status
+from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
-from typing import List
+from typing import List, Union
from uuid import UUID
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.router import Router
from gns3server import schemas
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
router = APIRouter(responses=responses)
@@ -53,6 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router:
response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips:
"""
@@ -84,7 +87,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.Dynamips)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.Dynamips,
+ dependencies=[Depends(compute_authentication)]
+)
def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Return Dynamips router.
@@ -93,7 +100,11 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.Dynamips)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.Dynamips,
+ dependencies=[Depends(compute_authentication)]
+)
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Update a Dynamips router.
@@ -104,7 +115,11 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_router(node: Router = Depends(dep_node)) -> None:
"""
Delete a Dynamips router.
@@ -113,7 +128,11 @@ async def delete_router(node: Router = Depends(dep_node)) -> None:
await Dynamips.instance().delete_node(node.id)
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_router(node: Router = Depends(dep_node)) -> None:
"""
Start a Dynamips router.
@@ -126,7 +145,11 @@ async def start_router(node: Router = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_router(node: Router = Depends(dep_node)) -> None:
"""
Stop a Dynamips router.
@@ -135,13 +158,21 @@ async def stop_router(node: Router = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_router(node: Router = Depends(dep_node)) -> None:
await node.suspend()
-@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/resume",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def resume_router(node: Router = Depends(dep_node)) -> None:
"""
Resume a suspended Dynamips router.
@@ -150,7 +181,11 @@ async def resume_router(node: Router = Depends(dep_node)) -> None:
await node.resume()
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_router(node: Router = Depends(dep_node)) -> None:
"""
Reload a suspended Dynamips router.
@@ -163,6 +198,7 @@ async def reload_router(node: Router = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_nio(
adapter_number: int,
@@ -183,6 +219,7 @@ async def create_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_nio(
adapter_number: int,
@@ -201,7 +238,11 @@ async def update_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@@ -211,7 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen
await nio.delete()
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_capture(
adapter_number: int,
port_number: int,
@@ -228,7 +272,9 @@ async def start_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
@@ -238,7 +284,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep
await node.stop_capture(adapter_number, port_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@@ -253,7 +302,10 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.get("/{node_id}/idlepc_proposals")
+@router.get(
+ "/{node_id}/idlepc_proposals",
+ dependencies=[Depends(compute_authentication)]
+)
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
"""
Retrieve Dynamips idle-pc proposals
@@ -263,7 +315,10 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
return await node.get_idle_pc_prop()
-@router.get("/{node_id}/auto_idlepc")
+@router.get(
+ "/{node_id}/auto_idlepc",
+ dependencies=[Depends(compute_authentication)]
+)
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
"""
Get an automatically guessed best idle-pc value.
@@ -273,7 +328,12 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
return {"idlepc": idlepc}
-@router.post("/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/duplicate",
+ response_model=schemas.Dynamips,
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Duplicate a router.
@@ -284,15 +344,24 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep
@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)) -> None:
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: Router = Depends(dep_node)
+
+) -> None:
"""
Console WebSocket.
"""
- await node.start_websocket_console(websocket)
+ if websocket:
+ await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: Router = Depends(dep_node)) -> None:
await node.reset_console()
diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py
index 5b74acc9..1b456a97 100644
--- a/gns3server/api/routes/compute/iou_nodes.py
+++ b/gns3server/api/routes/compute/iou_nodes.py
@@ -30,6 +30,8 @@ from gns3server import schemas
from gns3server.compute.iou import IOU
from gns3server.compute.iou.iou_vm import IOUVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
router = APIRouter(responses=responses)
@@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM:
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU:
"""
@@ -82,7 +85,11 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.IOU)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.IOU,
+ dependencies=[Depends(compute_authentication)]
+)
def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Return an IOU node.
@@ -91,7 +98,11 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.IOU)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.IOU,
+ dependencies=[Depends(compute_authentication)]
+)
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Update an IOU node.
@@ -112,7 +123,11 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Delete an IOU node.
@@ -121,7 +136,12 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await IOU.instance().delete_node(node.id)
-@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/duplicate",
+ response_model=schemas.IOU,
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
async def duplicate_iou_node(
destination_node_id: UUID = Body(..., embed=True),
node: IOUVM = Depends(dep_node)
@@ -134,7 +154,11 @@ async def duplicate_iou_node(
return new_node.asdict()
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None:
"""
Start an IOU node.
@@ -148,7 +172,11 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Stop an IOU node.
@@ -157,7 +185,11 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Suspend an IOU node.
@@ -167,7 +199,11 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
pass
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Reload an IOU node.
@@ -180,6 +216,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
+ dependencies=[Depends(compute_authentication)]
)
async def create_iou_node_nio(
adapter_number: int,
@@ -200,6 +237,7 @@ async def create_iou_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
+ dependencies=[Depends(compute_authentication)]
)
async def update_iou_node_nio(
adapter_number: int,
@@ -218,7 +256,11 @@ async def update_iou_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@@ -227,7 +269,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM
await node.adapter_remove_nio_binding(adapter_number, port_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_iou_node_capture(
adapter_number: int,
port_number: int,
@@ -244,7 +289,9 @@ async def start_iou_node_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
@@ -254,7 +301,10 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU
await node.stop_capture(adapter_number, port_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@@ -269,16 +319,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)) -> None:
+@router.websocket(
+ "/{node_id}/console/ws",
+)
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: IOUVM = Depends(dep_node)
+) -> None:
"""
Console WebSocket.
"""
- await node.start_websocket_console(websocket)
+ if websocket:
+ await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: IOUVM = Depends(dep_node)) -> None:
await node.reset_console()
diff --git a/gns3server/api/routes/compute/notifications.py b/gns3server/api/routes/compute/notifications.py
index 26a04b61..47b30f00 100644
--- a/gns3server/api/routes/compute/notifications.py
+++ b/gns3server/api/routes/compute/notifications.py
@@ -18,14 +18,13 @@
API routes for compute notifications.
"""
-import base64
-import binascii
-from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException
-from fastapi.security.utils import get_authorization_scheme_param
+from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
+from typing import Union
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.compute.notification_manager import NotificationManager
+from .dependencies.authentication import ws_compute_authentication
import logging
@@ -35,53 +34,27 @@ router = APIRouter()
@router.websocket("/notifications/ws")
-async def project_ws_notifications(websocket: WebSocket) -> None:
+async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(ws_compute_authentication)) -> None:
"""
Receive project notifications about the project from WebSocket.
"""
- await websocket.accept()
-
- # handle basic HTTP authentication
- invalid_user_credentials_exc = HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
- headers={"WWW-Authenticate": "Basic"},
- )
-
- try:
- authorization = websocket.headers.get("Authorization")
- scheme, param = get_authorization_scheme_param(authorization)
- if not authorization or scheme.lower() != "basic":
- raise invalid_user_credentials_exc
+ if websocket:
+ log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
- data = base64.b64decode(param).decode("ascii")
- except (ValueError, UnicodeDecodeError, binascii.Error):
- raise invalid_user_credentials_exc
- username, separator, password = data.partition(":")
- if not separator:
- raise invalid_user_credentials_exc
- except invalid_user_credentials_exc as e:
- websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
- f"compute WebSocket: {e.detail}"}}
- await websocket.send_json(websocket_error)
- return await websocket.close(code=1008)
-
- log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
- try:
- with NotificationManager.instance().queue() as queue:
- while True:
- notification = await queue.get_json(5)
- await websocket.send_text(notification)
- except (ConnectionClosed, WebSocketDisconnect):
- log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
- except WebSocketException as e:
- log.warning(f"Error while sending to controller event to WebSocket client: {e}")
- finally:
- try:
- await websocket.close()
- except OSError:
- pass # ignore OSError: [Errno 107] Transport endpoint is not connected
+ with NotificationManager.instance().queue() as queue:
+ while True:
+ notification = await queue.get_json(5)
+ await websocket.send_text(notification)
+ except (ConnectionClosed, WebSocketDisconnect):
+ log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
+ except WebSocketException as e:
+ log.warning(f"Error while sending to controller event to WebSocket client: {e}")
+ finally:
+ try:
+ await websocket.close()
+ except OSError:
+ pass # ignore OSError: [Errno 107] Transport endpoint is not connected
if __name__ == "__main__":
diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py
index b1b95417..1689fabe 100644
--- a/gns3server/api/routes/compute/qemu_nodes.py
+++ b/gns3server/api/routes/compute/qemu_nodes.py
@@ -20,15 +20,17 @@ API routes for Qemu nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
+from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
+from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_vm import QemuVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
@@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM:
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu:
"""
@@ -78,7 +81,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.Qemu)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.Qemu,
+ dependencies=[Depends(compute_authentication)]
+)
def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Return a Qemu node.
@@ -87,7 +94,11 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.Qemu)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.Qemu,
+ dependencies=[Depends(compute_authentication)]
+)
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Update a Qemu node.
@@ -103,7 +114,11 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Delete a Qemu node.
@@ -112,7 +127,12 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await Qemu.instance().delete_node(node.id)
-@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/duplicate",
+ response_model=schemas.Qemu,
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
async def duplicate_qemu_node(
destination_node_id: UUID = Body(..., embed=True),
node: QemuVM = Depends(dep_node)
@@ -127,7 +147,8 @@ async def duplicate_qemu_node(
@router.post(
"/{node_id}/disk_image/{disk_name}",
- status_code=status.HTTP_204_NO_CONTENT
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def create_qemu_disk_image(
disk_name: str,
@@ -144,7 +165,8 @@ async def create_qemu_disk_image(
@router.put(
"/{node_id}/disk_image/{disk_name}",
- status_code=status.HTTP_204_NO_CONTENT
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def update_qemu_disk_image(
disk_name: str,
@@ -161,7 +183,8 @@ async def update_qemu_disk_image(
@router.delete(
"/{node_id}/disk_image/{disk_name}",
- status_code=status.HTTP_204_NO_CONTENT
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_disk_image(
disk_name: str,
@@ -174,7 +197,11 @@ async def delete_qemu_disk_image(
node.delete_disk_image(disk_name)
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Start a Qemu node.
@@ -183,7 +210,11 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Stop a Qemu node.
@@ -192,7 +223,11 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Reload a Qemu node.
@@ -201,7 +236,11 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.reload()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Suspend a Qemu node.
@@ -210,7 +249,11 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.suspend()
-@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/resume",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Resume a Qemu node.
@@ -223,6 +266,7 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node_nio(
*,
@@ -245,6 +289,7 @@ async def create_qemu_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_qemu_node_nio(
*,
@@ -267,7 +312,11 @@ async def update_qemu_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_qemu_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -281,7 +330,10 @@ async def delete_qemu_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_qemu_node_capture(
*,
adapter_number: int,
@@ -300,7 +352,9 @@ async def start_qemu_node_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_qemu_node_capture(
adapter_number: int,
@@ -315,7 +369,10 @@ async def stop_qemu_node_capture(
await node.stop_capture(adapter_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -330,16 +387,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)) -> None:
+@router.websocket(
+ "/{node_id}/console/ws"
+)
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: QemuVM = Depends(dep_node)
+) -> None:
"""
Console WebSocket.
"""
- await node.start_websocket_console(websocket)
+ if websocket:
+ await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: QemuVM = Depends(dep_node)) -> None:
await node.reset_console()
diff --git a/gns3server/api/routes/compute/virtualbox_nodes.py b/gns3server/api/routes/compute/virtualbox_nodes.py
index f457cbff..f676025e 100644
--- a/gns3server/api/routes/compute/virtualbox_nodes.py
+++ b/gns3server/api/routes/compute/virtualbox_nodes.py
@@ -20,16 +20,19 @@ API routes for VirtualBox nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
+from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
+from typing import Union
from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
router = APIRouter(responses=responses, deprecated=True)
@@ -50,6 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
response_model=schemas.VirtualBox,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox:
"""
@@ -82,7 +86,11 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.VirtualBox)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.VirtualBox,
+ dependencies=[Depends(compute_authentication)]
+)
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox:
"""
Return a VirtualBox node.
@@ -91,7 +99,11 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.VirtualBox)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.VirtualBox,
+ dependencies=[Depends(compute_authentication)]
+)
async def update_virtualbox_node(
node_data: schemas.VirtualBoxUpdate,
node: VirtualBoxVM = Depends(dep_node)
@@ -136,7 +148,11 @@ async def update_virtualbox_node(
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Delete a VirtualBox node.
@@ -145,7 +161,11 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await VirtualBox.instance().delete_node(node.id)
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Start a VirtualBox node.
@@ -154,7 +174,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Stop a VirtualBox node.
@@ -163,7 +187,11 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Suspend a VirtualBox node.
@@ -172,7 +200,11 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non
await node.suspend()
-@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/resume",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Resume a VirtualBox node.
@@ -181,7 +213,11 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await node.resume()
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Reload a VirtualBox node.
@@ -194,6 +230,7 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node_nio(
*,
@@ -216,6 +253,7 @@ async def create_virtualbox_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_virtualbox_node_nio(
*,
@@ -238,7 +276,11 @@ async def update_virtualbox_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_virtualbox_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -252,7 +294,10 @@ async def delete_virtualbox_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_virtualbox_node_capture(
*,
adapter_number: int,
@@ -271,7 +316,9 @@ async def start_virtualbox_node_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_virtualbox_node_capture(
adapter_number: int,
@@ -286,7 +333,10 @@ async def stop_virtualbox_node_capture(
await node.stop_capture(adapter_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -302,8 +352,13 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)) -> None:
+@router.websocket(
+ "/{node_id}/console/ws"
+)
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: VirtualBoxVM = Depends(dep_node)
+) -> None:
"""
Console WebSocket.
"""
@@ -311,7 +366,11 @@ async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node
await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.reset_console()
diff --git a/gns3server/api/routes/compute/vmware_nodes.py b/gns3server/api/routes/compute/vmware_nodes.py
index d7c38844..f1e3752c 100644
--- a/gns3server/api/routes/compute/vmware_nodes.py
+++ b/gns3server/api/routes/compute/vmware_nodes.py
@@ -20,16 +20,18 @@ API routes for VMware nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
+from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
+from typing import Union
from gns3server import schemas
from gns3server.compute.vmware import VMware
-from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.vmware.vmware_vm import VMwareVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses, deprecated=True)
@@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
response_model=schemas.VMware,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware:
"""
@@ -76,7 +79,11 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.VMware)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.VMware,
+ dependencies=[Depends(compute_authentication)]
+)
def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Return a VMware node.
@@ -85,7 +92,11 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.VMware)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.VMware,
+ dependencies=[Depends(compute_authentication)]
+)
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Update a VMware node.
@@ -102,7 +113,11 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Delete a VMware node.
@@ -111,7 +126,11 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await VMware.instance().delete_node(node.id)
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Start a VMware node.
@@ -120,7 +139,11 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Stop a VMware node.
@@ -129,7 +152,11 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Suspend a VMware node.
@@ -138,7 +165,11 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.suspend()
-@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/resume",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Resume a VMware node.
@@ -147,7 +178,11 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.resume()
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Reload a VMware node.
@@ -160,6 +195,7 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node_nio(
*,
@@ -182,6 +218,7 @@ async def create_vmware_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_vmware_node_nio(
*,
@@ -202,7 +239,11 @@ async def update_vmware_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_vmware_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -216,7 +257,10 @@ async def delete_vmware_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_vmware_node_capture(
*,
adapter_number: int,
@@ -235,7 +279,9 @@ async def start_vmware_node_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_vmware_node_capture(
adapter_number: int,
@@ -250,7 +296,10 @@ async def stop_vmware_node_capture(
await node.stop_capture(adapter_number)
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@@ -266,7 +315,11 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/interfaces/vmnet",
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
"""
Allocate a VMware VMnet interface on the server.
@@ -280,16 +333,23 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)) -> None:
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: VMwareVM = Depends(dep_node)
+) -> None:
"""
Console WebSocket.
"""
- await node.start_websocket_console(websocket)
+ if websocket:
+ await node.start_websocket_console(websocket)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reset_console(node: VMwareVM = Depends(dep_node)) -> None:
await node.reset_console()
-
diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py
index df4f82c3..143ceef8 100644
--- a/gns3server/api/routes/compute/vpcs_nodes.py
+++ b/gns3server/api/routes/compute/vpcs_nodes.py
@@ -20,15 +20,18 @@ API routes for VPCS nodes.
import os
-from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
+from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
+from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
+from .dependencies.authentication import compute_authentication, ws_compute_authentication
+
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses)
@@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM:
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
+ dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS:
"""
@@ -69,7 +73,11 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
return vm.asdict()
-@router.get("/{node_id}", response_model=schemas.VPCS)
+@router.get(
+ "/{node_id}",
+ response_model=schemas.VPCS,
+ dependencies=[Depends(compute_authentication)]
+)
def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Return a VPCS node.
@@ -78,7 +86,11 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
return node.asdict()
-@router.put("/{node_id}", response_model=schemas.VPCS)
+@router.put(
+ "/{node_id}",
+ response_model=schemas.VPCS,
+ dependencies=[Depends(compute_authentication)]
+)
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Update a VPCS node.
@@ -92,7 +104,11 @@ def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_n
return node.asdict()
-@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Delete a VPCS node.
@@ -101,7 +117,12 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await VPCS.instance().delete_node(node.id)
-@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/{node_id}/duplicate",
+ response_model=schemas.VPCS,
+ status_code=status.HTTP_201_CREATED,
+ dependencies=[Depends(compute_authentication)]
+)
async def duplicate_vpcs_node(
destination_node_id: UUID = Body(..., embed=True),
node: VPCSVM = Depends(dep_node)) -> None:
@@ -113,7 +134,11 @@ async def duplicate_vpcs_node(
return new_node.asdict()
-@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/start",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Start a VPCS node.
@@ -122,7 +147,11 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.start()
-@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Stop a VPCS node.
@@ -131,7 +160,11 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.stop()
-@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/suspend",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Suspend a VPCS node.
@@ -141,7 +174,11 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
pass
-@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
+@router.post(
+ "/{node_id}/reload",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Reload a VPCS node.
@@ -154,6 +191,7 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node_nio(
*,
@@ -176,6 +214,7 @@ async def create_vpcs_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
+ dependencies=[Depends(compute_authentication)]
)
async def update_vpcs_node_nio(
*,
@@ -196,7 +235,11 @@ async def update_vpcs_node_nio(
return nio.asdict()
-@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
async def delete_vpcs_node_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
@@ -211,7 +254,10 @@ async def delete_vpcs_node_nio(
await node.port_remove_nio_binding(port_number)
-@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
+@router.post(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
+ dependencies=[Depends(compute_authentication)]
+)
async def start_vpcs_node_capture(
*,
adapter_number: int = Path(..., ge=0, le=0),
@@ -230,7 +276,9 @@ async def start_vpcs_node_capture(
@router.post(
- "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
)
async def stop_vpcs_node_capture(
*,
@@ -246,13 +294,10 @@ async def stop_vpcs_node_capture(
await node.stop_capture(port_number)
-@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
-async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
-
- await node.reset_console()
-
-
-@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
+@router.get(
+ "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
+ dependencies=[Depends(compute_authentication)]
+)
async def stream_pcap_file(
*,
adapter_number: int = Path(..., ge=0, le=0),
@@ -269,10 +314,24 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
-@router.websocket("/{node_id}/console/ws")
-async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)) -> None:
+@router.websocket(
+ "/{node_id}/console/ws"
+)
+async def console_ws(
+ websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
+ node: VPCSVM = Depends(dep_node)) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
+
+
+@router.post(
+ "/{node_id}/console/reset",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(compute_authentication)]
+)
+async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
+
+ await node.reset_console()
diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py
index ce49ab0a..05e4d4ae 100644
--- a/gns3server/api/routes/controller/dependencies/authentication.py
+++ b/gns3server/api/routes/controller/dependencies/authentication.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import re
+import logging
from fastapi import Request, Query, Depends, HTTPException, WebSocket, status
from fastapi.security import OAuth2PasswordBearer
@@ -26,6 +26,7 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services import auth_service
from .database import get_repository
+log = logging.getLogger(__name__)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/access/users/login", auto_error=False)
@@ -108,7 +109,9 @@ async def get_current_active_user_from_websocket(
return user
except HTTPException as e:
- websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
- f"WebSocket: {e.detail}"}}
+ err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}"
+ websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
- await websocket.close(code=1008)
+ log.error(err_msg)
+ return await websocket.close(code=1008)
+
diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py
index 865ad683..13e84450 100644
--- a/gns3server/api/routes/controller/nodes.py
+++ b/gns3server/api/routes/controller/nodes.py
@@ -29,6 +29,7 @@ from typing import List, Callable
from uuid import UUID
from gns3server.controller import Controller
+from gns3server.config import Config
from gns3server.controller.node import Node
from gns3server.controller.project import Project
from gns3server.utils import force_unix_path
@@ -510,16 +511,22 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
# FIXME: response with correct status code (from compute)
-@router.websocket("/{node_id}/console/ws", dependencies=[Depends(has_privilege_on_websocket("Node.Console"))])
-async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None:
+@router.websocket("/{node_id}/console/ws")
+async def ws_console(
+ websocket: WebSocket,
+ current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
+ node: Node = Depends(dep_node)
+) -> None:
"""
WebSocket console.
Required privilege: Node.Console
"""
+ if current_user is None:
+ return
+
compute = node.compute
- await websocket.accept()
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
)
@@ -557,9 +564,20 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
try:
# receive WebSocket data from compute console WebSocket and forward to client.
- async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute:
- asyncio.ensure_future(ws_receive(ws_console_compute))
- async for msg in ws_console_compute:
+ log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'")
+ server_config = Config.instance().settings.Server
+ user = server_config.compute_username
+ password = server_config.compute_password
+ if not user:
+ raise ControllerForbiddenError("Compute username is not set")
+ user = user.strip()
+ if user and password:
+ auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
+ else:
+ auth = aiohttp.BasicAuth(user, "")
+ async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth) as ws:
+ asyncio.ensure_future(ws_receive(ws))
+ async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await websocket.send_text(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py
index c877f5ff..20f437be 100644
--- a/gns3server/compute/base_node.py
+++ b/gns3server/compute/base_node.py
@@ -485,6 +485,11 @@ class BaseNode:
:param ws: Websocket object
"""
+ log.info(
+ f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
+ f" console WebSocket"
+ )
+
if self.status != "started":
raise NodeError(f"Node {self.name} is not started")
@@ -492,20 +497,13 @@ class BaseNode:
raise NodeError(f"Node {self.name} console type is not telnet")
try:
- (telnet_reader, telnet_writer) = await asyncio.open_connection(
- self._manager.port_manager.console_host, self.console
- )
+ host = self._manager.port_manager.console_host
+ port = self.console
+ (telnet_reader, telnet_writer) = await asyncio.open_connection(host, port)
+ log.info(f"Connected to local Telnet server {host}:{port}")
except ConnectionError as e:
raise NodeError(f"Cannot connect to node {self.name} telnet server: {e}")
- log.info("Connected to Telnet server")
-
- await websocket.accept()
- log.info(
- f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
- f" console WebSocket"
- )
-
async def ws_forward(telnet_writer):
try:
diff --git a/tests/api/routes/test_routes.py b/tests/api/routes/test_routes.py
index 7a7313ed..b960d6f4 100644
--- a/tests/api/routes/test_routes.py
+++ b/tests/api/routes/test_routes.py
@@ -20,6 +20,10 @@ from fastapi import FastAPI, status
from fastapi.routing import APIRoute, APIWebSocketRoute
from starlette.routing import Mount
from httpx import AsyncClient
+from httpx_ws import aconnect_ws
+from httpx_ws.transport import ASGIWebSocketTransport
+
+
pytestmark = pytest.mark.asyncio
@@ -37,6 +41,7 @@ ALLOWED_CONTROLLER_ENDPOINTS = [
("/v3/symbols/default_symbols", "GET")
]
+
# Controller endpoints have a OAuth2 bearer token authentication
async def test_controller_endpoints_require_authentication(app: FastAPI, unauthorized_client: AsyncClient) -> None:
@@ -47,7 +52,14 @@ async def test_controller_endpoints_require_authentication(app: FastAPI, unautho
response = await getattr(unauthorized_client, method.lower())(route.path)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
elif isinstance(route, APIWebSocketRoute):
- pass # TODO: test websocket route authentication
+ params = {"token": "wrong_token"}
+ async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app)) as client:
+ async with aconnect_ws(route.path, client, params=params) as ws:
+ json_notification = await ws.receive_json()
+ assert json_notification['event'] == {
+ 'message': 'Could not authenticate while connecting to controller WebSocket: Could not validate credentials'
+ }
+
# Compute endpoints have a basic HTTP authentication
async def test_compute_endpoints_require_authentication(app: FastAPI, unauthorized_client: AsyncClient) -> None:
@@ -55,9 +67,14 @@ async def test_compute_endpoints_require_authentication(app: FastAPI, unauthoriz
for route in app.routes:
if isinstance(route, Mount):
for compute_route in route.routes:
- if isinstance(compute_route, APIRoute): # APIWebSocketRoute
+ if isinstance(compute_route, APIRoute):
for method in list(compute_route.methods):
response = await getattr(unauthorized_client, method.lower())(route.path + compute_route.path)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
elif isinstance(compute_route, APIWebSocketRoute):
- pass # TODO: test websocket route authentication
+ async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app)) as client:
+ async with aconnect_ws(route.path + compute_route.path, client, auth=("wrong_user", "password123")) as ws:
+ json_notification = await ws.receive_json()
+ assert json_notification['event'] == {
+ 'message': 'Could not authenticate while connecting to compute WebSocket: Could not validate credentials'
+ }