Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix compute authentication for websocket endpoints #2300

Merged
merged 4 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ flake8==6.1.0
pytest-timeout==2.2.0
pytest-asyncio==0.21.1
requests==2.31.0
httpx==0.25.0
httpx==0.24.1 # version 0.24.1 is required by httpx_ws
httpx_ws==0.4.2
7 changes: 0 additions & 7 deletions gns3server/api/routes/compute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,12 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):

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"]
)
Expand Down Expand Up @@ -234,7 +232,6 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):

compute_api.include_router(
iou_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/iou/nodes",
tags=["IOU nodes"])

Expand All @@ -247,28 +244,24 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):

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"]
)
50 changes: 48 additions & 2 deletions gns3server/api/routes/compute/dependencies/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

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()


Expand All @@ -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
102 changes: 84 additions & 18 deletions gns3server/api/routes/compute/docker_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@

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)
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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()
Loading