Skip to content
Open
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
12 changes: 12 additions & 0 deletions browser_devtools/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM node:lts-alpine

WORKDIR /usr/app

COPY websockets.js ./

RUN npm init -y
RUN npm install ws

EXPOSE 8080

CMD ["node", "websockets.js"]
2 changes: 2 additions & 0 deletions browser_devtools/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ class BrowserDevtoolsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "browser_devtools"
has_tasks = True
use_docker = True
container_protocol = "ws"
display_name = "Browser Devtools"
3 changes: 3 additions & 0 deletions browser_devtools/static/browser_devtools/courier_intercept.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const wsUri = `ws://${window.location.host}/envs/browser_devtools/`;
console.log(wsUri);
const websocket = new WebSocket(wsUri);
14 changes: 14 additions & 0 deletions browser_devtools/templates/browser_devtools/courier_intercept.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "ctef_web/task.html" %}

{% block head %}
{% load static %}
<script src="{% static 'browser_devtools/courier_intercept.js' %}"></script>
{% endblock %}

{% block task %}

{% load task %}

<p>Test!</p>

{% endblock %}
4 changes: 4 additions & 0 deletions browser_devtools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
@define_task(name="Watch your head!", clues="clues/watch_your_head.md")
def watch_your_head(request: HttpRequest, context):
return render(request, "browser_devtools/watch_your_head.html", context)

@define_task(name="Courier intercept")
def courier_intercept(request: HttpRequest, context):
return render(request, "browser_devtools/courier_intercept.html", context)
13 changes: 13 additions & 0 deletions browser_devtools/websockets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.on('error', console.error);

ws.on('message', function message(data) {
console.log('received: %s', data);
});

ws.send('something');
});
9 changes: 8 additions & 1 deletion ctef/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@
asgi_application = get_asgi_application()

from ctef_core.routing import websocket_urlpatterns
from . import urls

# Add the websocket url patterns from the containers
websocket_urlpatterns = websocket_urlpatterns + urls.wspatterns
print(websocket_urlpatterns)

# TODO: Adapt this to forward websocket connections to container proxy as well
# https://channels.readthedocs.io/en/latest/topics/routing.html#
application = ProtocolTypeRouter(
{
"http": asgi_application,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
)
),
}
)
16 changes: 14 additions & 2 deletions ctef/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
path("admin/", admin.site.urls),
]

wspatterns = []

# Import and register all task views in installed CTF modules.
from ctef_core.common import fetch_ctf_modules
from ctef_web.views import config_hints_view
Expand All @@ -49,15 +51,25 @@
container = client.containers.get(container_name)
IPAddress = container.attrs["NetworkSettings"]["IPAddress"]

urlpatterns.append(
try:
if ctf_module.container_protocol == "ws":
pattern_collection = wspatterns
else:
pattern_collection = urlpatterns
except:
pattern_collection = urlpatterns

# TODO: Maybe just writing a Proxy consumer would be the easiest.
# Or maybe extend ProxyView?
pattern_collection.append(
re_path(
f"envs/{module_name}/(?P<path>.*)",
ProxyView.as_view(upstream=f"http://{IPAddress}:8080"),
)
)
except (AttributeError, docker.errors.NotFound) as e:
pass

# Import MODULE_TASKS after being populated by the previous section imports.
from ctef_core.decorators import MODULE_TASKS

Expand Down
47 changes: 45 additions & 2 deletions ctef_core/consumers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
from channels.generic.websocket import WebsocketConsumer
import json, websockets, asyncio

from channels.exceptions import DenyConnection
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer

from .models import Task
from .process import CTFProcess
Expand Down Expand Up @@ -47,3 +49,44 @@ def receive(self, text_data=None, bytes_data=None):
def send_bytes(self, bytes: bytes):
decoded = bytes.decode()
self.send(decoded)


class ProxyConsumer(AsyncWebsocketConsumer):
"""
Django Channels websockets consumer that forwards websocket connections to a Docker container.
Based on https://gist.github.com/brianglass/e3184341afe63ed348144753ee62dce5
"""

def __init__(self, endpoint: str, *args: object, **kwargs: object) -> None:
"""Create a new proxy consumer for a websocket server at the specified endpoint"""
super().__init__(*args, **kwargs)
self.endpoint = endpoint
self.upstream_socket = None
self.forward_upstream_task = None

async def connect(self) -> None:
# Attempt to connect to the endpoint to proxy
try:
self.upstream_socket = await websockets.connect(self.endpoint)
except websockets.InvalidURI:
print("The container endpoint was not reachable.")
raise DenyConnection()

# Accept the incoming connection with the same subprotocol.
await self.accept(self.upstream_socket.subprotocol)

self.forward_upstream_task = asyncio.create_task(self.forward_upstream())

async def forward_upstream(self):
try:
async for data in self.upstream_socket:
if hasattr(data, "decode"):
await self.send(bytes_data=data)
else:
await self.send(text_data=data)
except asyncio.exceptions.CancelledError:
# This is triggered by the consumer itself when the client connection is terminating.
await self.upstream_socket.close()
except websockets.ConnectionClosedError:
# The target probably closed the connection.
await self.close()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ Twisted==24.7.0
txaio==23.1.1
typing_extensions==4.12.2
urllib3==2.5.0
websockets==15.0.1
wheel==0.44.0
zope.interface==7.0.3