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

Refactored the control panel and workspace reload #489

Merged
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
2 changes: 2 additions & 0 deletions dojo_plugin/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .v1.scoreboard import scoreboard_namespace
from .v1.ssh_key import ssh_key_namespace
from .v1.workspace_tokens import workspace_tokens_namespace
from .v1.workspace import workspace_namespace


api = Blueprint("pwncollege_api", __name__)
Expand All @@ -24,3 +25,4 @@
api_v1.add_namespace(scoreboard_namespace, "/scoreboard")
api_v1.add_namespace(ssh_key_namespace, "/ssh_key")
api_v1.add_namespace(workspace_tokens_namespace, "/workspace_tokens")
api_v1.add_namespace(workspace_namespace, "/workspace")
69 changes: 69 additions & 0 deletions dojo_plugin/api/v1/workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from flask_restx import Namespace, Resource
from flask import request, render_template, url_for, abort
from CTFd.utils.user import get_current_user
from CTFd.utils.decorators import authed_only
from ...utils import get_current_container, container_password
from ...utils.workspace import exec_run, start_on_demand_service


workspace_namespace = Namespace(
"workspace", description="Endpoint to manage workspace iframe urls"
)


@workspace_namespace.route("")
class view_desktop(Resource):
@authed_only
def get(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably add a service = request.args.get("service") or something like that.

user_id = request.args.get("user")
password = request.args.get("password")
service = request.args.get("service")

if not service:
return { "active": False }


if user_id and not password and not is_admin():
abort(403)

user = get_current_user() if not user_id else Users.query.filter_by(id=int(user_id)).first_or_404()
container = get_current_container(user)
if not container:
return { "active": False }


if service == "desktop":
interact_password = container_password(container, "desktop", "interact")
view_password = container_password(container, "desktop", "view")

if user_id and password:
if not hmac.compare_digest(password, interact_password) and not hmac.compare_digest(password, view_password):
abort(403)
password = password[:8]
else:
password = interact_password[:8]

view_only = user_id is not None
service_param = "~".join(("desktop", str(user.id), container_password(container, "desktop")))

vnc_params = {
"autoconnect": 1,
"reconnect": 1,
"reconnect_delay": 200,
"resize": "remote",
"path": url_for("pwncollege_workspace.forward_workspace", service=service_param, service_path="websockify"),
"view_only": int(view_only),
"password": password,
}
iframe_src = url_for("pwncollege_workspace.forward_workspace", service=service_param, service_path="vnc.html", **vnc_params)
else:
iframe_src = f"/workspace/{service}/"

if start_on_demand_service(user, service) is False:
return { "active": False }

return {
"iframe_src": iframe_src,
"service": service,
"active": True
}
1 change: 1 addition & 0 deletions dojo_plugin/pages/dojo.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def active_module():
"challenge_id": challs['current'].challenge_id,
"challenge_name": challs['current'].name,
"challenge_reference_id": challs['current'].id,
"description": render_markdown(challs['current'].description).strip(),
},
"c_next": {
"module_name": challs['next'].module.name if challs['next'] else None,
Expand Down
120 changes: 3 additions & 117 deletions dojo_plugin/pages/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from CTFd.plugins import bypass_csrf_protection

from ..models import Dojos
from ..utils import redirect_user_socket, get_current_container
from ..utils import redirect_user_socket, get_current_container, container_password
from ..utils.dojo import get_current_dojo_challenge
from ..utils.workspace import exec_run
from ..utils.workspace import exec_run, start_on_demand_service


workspace = Blueprint("pwncollege_workspace", __name__)
Expand All @@ -20,126 +20,12 @@
"desktop": 6080,
"desktop-windows": 6082,
}
on_demand_services = { "code", "desktop", "desktop-windows" }

def container_password(container, *args):
key = container.labels["dojo.auth_token"].encode()
message = "-".join(args).encode()
return hmac.HMAC(key, message, "sha256").hexdigest()

def start_on_demand_service(user, service_name):
if service_name not in on_demand_services:
return
try:
exec_run(
f"/run/dojo/bin/dojo-{service_name}",
workspace_user="hacker",
user_id=user.id,
assert_success=True,
)
except docker.errors.NotFound:
return False
return True

@workspace.route("/workspace/desktop")
@authed_only
def view_desktop():
user_id = request.args.get("user")
password = request.args.get("password")

if user_id and not password and not is_admin():
abort(403)

user = get_current_user() if not user_id else Users.query.filter_by(id=int(user_id)).first_or_404()
container = get_current_container(user)
if not container:
return render_template("iframe.html", active=False)

interact_password = container_password(container, "desktop", "interact")
view_password = container_password(container, "desktop", "view")

if user_id and password:
if not hmac.compare_digest(password, interact_password) and not hmac.compare_digest(password, view_password):
abort(403)
password = password[:8]
else:
password = interact_password[:8]

view_only = user_id is not None
service = "~".join(("desktop", str(user.id), container_password(container, "desktop")))

vnc_params = {
"autoconnect": 1,
"reconnect": 1,
"reconnect_delay": 200,
"resize": "remote",
"path": url_for("pwncollege_workspace.forward_workspace", service=service, service_path="websockify"),
"view_only": int(view_only),
"password": password,
}
iframe_src = url_for("pwncollege_workspace.forward_workspace", service=service, service_path="vnc.html", **vnc_params)

share_urls = {
"Desktop (Interact)": url_for("pwncollege_workspace.view_desktop", user=user.id, password=interact_password, _external=True),
"Desktop (View)": url_for("pwncollege_workspace.view_desktop", user=user.id, password=view_password, _external=True),
}

if start_on_demand_service(user, "desktop") is False:
return render_template("iframe.html", active=False)

return render_template("iframe.html",
iframe_name="workspace",
iframe_src=iframe_src,
share_urls=share_urls,
active=True)

@workspace.route("/workspace/desktop-windows")
@authed_only
def view_desktop_win():
user_id = request.args.get("user")

if user_id and not is_admin():
abort(403)

user = get_current_user() if not user_id else Users.query.filter_by(id=int(user_id)).first_or_404()
container = get_current_container(user)
if not container:
return render_template("iframe.html", active=False)

interact_password = container_password(container, "desktop", "interact")
view_password = container_password(container, "desktop", "view")

view_only = user_id is not None
service = "~".join(("desktop", str(user.id), container_password(container, "desktop")))

vnc_params = {
"autoconnect": 1,
"reconnect": 1,
"reconnect_delay": 200,
"resize": "remote",
"path": url_for("pwncollege_workspace.forward_workspace", service=service, service_path="websockify"),
"view_only": int(view_only),
"password": "abcd",
}
iframe_src = url_for("pwncollege_workspace.forward_workspace", service=service, service_path="vnc.html", **vnc_params)

if start_on_demand_service(user, "desktop") is False:
return render_template("iframe.html", active=False)

return render_template("iframe.html",
iframe_name="workspace",
iframe_src=iframe_src,
share_urls={},
active=True)

@workspace.route("/workspace/<service>")
@authed_only
def view_workspace(service):
user = get_current_user()
active = bool(get_current_dojo_challenge())
if start_on_demand_service(user, service) is False:
return render_template("iframe.html", active=False)
return render_template("iframe.html", iframe_name="workspace", iframe_src=f"/workspace/{service}/", active=active)
return render_template("workspace.html", iframe_name="workspace", service=service)

@workspace.route("/workspace/<service>/", websocket=True)
@workspace.route("/workspace/<service>/<path:service_path>", websocket=True)
Expand Down
7 changes: 7 additions & 0 deletions dojo_plugin/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import hashlib
import hmac
import io
import logging
import os
Expand Down Expand Up @@ -39,6 +40,12 @@ def container_name(user):
return f"user_{user.id}"


def container_password(container, *args):
key = container.labels["dojo.auth_token"].encode()
message = "-".join(args).encode()
return hmac.HMAC(key, message, "sha256").hexdigest()


def get_current_container(user=None):
user = user or get_current_user()
if not user:
Expand Down
15 changes: 15 additions & 0 deletions dojo_plugin/utils/workspace.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import docker

on_demand_services = { "code", "desktop", "desktop-windows" }

def start_on_demand_service(user, service_name):
if service_name not in on_demand_services:
return
try:
exec_run(
f"/run/current-system/sw/bin/dojo-{service_name}",
workspace_user="hacker",
user_id=user.id,
assert_success=True,
)
except docker.errors.NotFound:
return False
return True

def exec_run(cmd, *, shell=False, assert_success=True, workspace_user="root", user_id=None, container=None, **kwargs):
# TODO: Cleanup this interface
Expand Down
46 changes: 39 additions & 7 deletions dojo_theme/static/js/dojo/navbar.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
const broadcast = new BroadcastChannel('broadcast');

broadcast.onmessage = (event) => {
if (event.data.msg === 'New challenge started') {
if (window.location.pathname === '/workspace/code') {
window.location.reload();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason code uses a window reload, while the desktop gets/sets the iframe url?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i cant figure out why reloading the iframe does not work. as the iframe url seams to be the same.
reloading the page seam to solve the problem. But i currently don't know why.
Any advice would be welcome.

}
else if (window.location.pathname === '/workspace/desktop') {
get_and_set_iframe_url()
}
}
};
function get_and_set_iframe_url() {
// check if the window location pathname starts with /workspace/ and set the rest of the path as an variable service
const service = window.location.pathname.startsWith('/workspace/') ? window.location.pathname.substring(11) : '';
fetch("/pwncollege_api/v1/workspace?service=" + service)
.then(response => response.json())
.then(data => {
if (data.active) {
const iframe = $("#workspace_iframe")[0];
if (iframe.src !== window.location.origin + data.iframe_src) {
iframe.src = data.iframe_src;
}
}
});
}
async function fetch_current_module() {
const response = await fetch('/active-module/');
const data = await response.json();
Expand All @@ -20,6 +46,7 @@ async function updateNavbarDropdown() {
$("#current #module").val(data.c_current.module_id);
$("#current #challenge").val(data.c_current.challenge_reference_id);
$("#current #challenge-id").val(data.c_current.challenge_id);
$("#dropdown-description").html(data.c_current.description);

if ("dojo_name" in data.c_previous) {
$("#previous").removeClass("invisible");
Expand Down Expand Up @@ -114,13 +141,11 @@ function DropdownStartChallenge(event) {
await updateNavbarDropdown();
$(".challenge-active").removeClass("challenge-active");
$(`.accordion-item input[value=${params.challenge}]`).closest(".accordion-item").find("h4.challenge-name").addClass("challenge-active");
if (window.location.href.includes('/workspace/desktop')) {
let iframe_html = await fetch('/workspace/desktop').then(response => response.text());
let iframe_src = $(iframe_html).find("iframe").attr("src");
if (iframe_src) {
$("main iframe").attr("src", iframe_src);
}
}
const broadcast_send = new BroadcastChannel('broadcast');
broadcast_send.postMessage({
time: new Date().getTime(),
msg: 'New challenge started'
});
}
else {
let message = "Error:";
Expand Down Expand Up @@ -190,6 +215,13 @@ function submitFlag(event) {
}
updateNavbarDropdown();
$(() => {
$("#show_description").click((event) =>{
$("#dropdown-description").toggle();
event.stopPropagation();
});
$("#dropdown-description").click((event) =>{
event.stopPropagation();
});
$(".close-link").on("click", () => {
$(".navbar")
.addClass("navbar-hiding")
Expand Down
25 changes: 18 additions & 7 deletions dojo_theme/templates/components/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,17 @@
</div>
<div class="dropdown-divider"></div>
<div class="container-fluid">
<div class="row mt-3">
<div class="row d-none d-md-block">
<div class="col-12"><p class="text-light dropdown-toggle" id="show_description">Description</p></div>
<div class="col-12" style="max-height: 50vh;min-width: 50vw;overflow-y: scroll;">
<p id="dropdown-description" class="text-light" style="display: none">
This is a description of the challenge.
</p>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 form-group">
<input id="dropdown-challenge-input" class="challenge-input form-control" type="text" name="answer" placeholder="Flag">
</div>
Expand All @@ -74,11 +84,6 @@
</div>
<div class="container-fluid">
<div id="dropdown-controls" class="row pb-2">
<div class="notification-row fixed-top">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
<div id="previous" class="col overflow-hidden ">
<button id="challenge-start" class="btn btn-dark text-truncate w-100">
<i class="fas fa-backward"></i>
Expand Down Expand Up @@ -115,6 +120,12 @@
<input id="challenge" type="hidden" value="xx">
<input id="challenge-id" type="hidden" value="xx">
</div>
<div class="w-100"></div>
<div class="notification-row col-12 mt-2">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
</div>
Expand All @@ -130,7 +141,7 @@
{% endif %}

<li class="nav-item">
<a class="nav-link close-link" href="javascript:void()">
<a class="nav-link close-link" href="javascript:void(0)">
<span class="d-block" data-toggle="tooltip" data-placement="bottom" title="Hide Navbar">
<i class="fas caret-up d-none d-md-block d-lg-none"></i>
</span>
Expand Down
Loading
Loading