Fork project
This commit is contained in:
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
150
utils/cache.py
Normal file
150
utils/cache.py
Normal file
@ -0,0 +1,150 @@
|
||||
import ipaddress
|
||||
import warnings
|
||||
from CTFd.cache import cache
|
||||
from CTFd.utils import get_config
|
||||
from flask_redis import FlaskRedis
|
||||
from redis.exceptions import LockError
|
||||
|
||||
from .db import DBContainer
|
||||
|
||||
|
||||
class CacheProvider:
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
if app.config['CACHE_TYPE'] == 'redis':
|
||||
self.provider = RedisCacheProvider(app, *args, **kwargs)
|
||||
elif app.config['CACHE_TYPE'] in ['filesystem', 'simple']:
|
||||
if not hasattr(CacheProvider, 'cache'):
|
||||
CacheProvider.cache = {}
|
||||
self.provider = FilesystemCacheProvider(app, *args, **kwargs)
|
||||
self.init_port_sets()
|
||||
|
||||
def init_port_sets(self):
|
||||
self.clear()
|
||||
|
||||
containers = DBContainer.get_all_container()
|
||||
used_port_list = []
|
||||
for container in containers:
|
||||
if container.port != 0:
|
||||
used_port_list.append(container.port)
|
||||
for port in range(int(get_config("whale:frp_direct_port_minimum", 29000)),
|
||||
int(get_config("whale:frp_direct_port_maximum", 28000)) + 1):
|
||||
if port not in used_port_list:
|
||||
self.add_available_port(port)
|
||||
|
||||
from .docker import get_docker_client
|
||||
client = get_docker_client()
|
||||
|
||||
docker_subnet = get_config("whale:docker_subnet", "174.1.0.0/16")
|
||||
docker_subnet_new_prefix = int(
|
||||
get_config("whale:docker_subnet_new_prefix", "24"))
|
||||
|
||||
exist_networks = []
|
||||
available_networks = []
|
||||
|
||||
for network in client.networks.list(filters={'label': 'prefix'}):
|
||||
exist_networks.append(str(network.attrs['Labels']['prefix']))
|
||||
|
||||
for network in list(ipaddress.ip_network(docker_subnet).subnets(new_prefix=docker_subnet_new_prefix)):
|
||||
if str(network) not in exist_networks:
|
||||
available_networks.append(str(network))
|
||||
|
||||
self.add_available_network_range(*set(available_networks))
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.provider.__getattribute__(name)
|
||||
|
||||
|
||||
class FilesystemCacheProvider:
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
warnings.warn(
|
||||
'\n[CTFd Whale] Warning: looks like you are using filesystem cache. '
|
||||
'\nThis is for TESTING purposes only, DO NOT USE on production sites.',
|
||||
RuntimeWarning
|
||||
)
|
||||
self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
|
||||
self.global_port_key = "ctfd_whale-port-set"
|
||||
self.global_network_key = "ctfd_whale-network-set"
|
||||
|
||||
def clear(self):
|
||||
cache.set(self.global_port_key, set())
|
||||
cache.set(self.global_network_key, set())
|
||||
|
||||
def add_available_network_range(self, *ranges):
|
||||
s = cache.get(self.global_network_key)
|
||||
s.update(ranges)
|
||||
cache.set(self.global_network_key, s)
|
||||
|
||||
def get_available_network_range(self):
|
||||
try:
|
||||
s = cache.get(self.global_network_key)
|
||||
r = s.pop()
|
||||
cache.set(self.global_network_key, s)
|
||||
return r
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def add_available_port(self, port):
|
||||
s = cache.get(self.global_port_key)
|
||||
s.add(port)
|
||||
cache.set(self.global_port_key, s)
|
||||
|
||||
def get_available_port(self):
|
||||
try:
|
||||
s = cache.get(self.global_port_key)
|
||||
r = s.pop()
|
||||
cache.set(self.global_port_key, s)
|
||||
return r
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def acquire_lock(self):
|
||||
# for testing purposes only, so no need to set this limit
|
||||
return True
|
||||
|
||||
def release_lock(self):
|
||||
return True
|
||||
|
||||
|
||||
class RedisCacheProvider(FlaskRedis):
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(app)
|
||||
self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
|
||||
self.current_lock = None
|
||||
self.global_port_key = "ctfd_whale-port-set"
|
||||
self.global_network_key = "ctfd_whale-network-set"
|
||||
|
||||
def clear(self):
|
||||
self.delete(self.global_port_key)
|
||||
self.delete(self.global_network_key)
|
||||
|
||||
def add_available_network_range(self, *ranges):
|
||||
self.sadd(self.global_network_key, *ranges)
|
||||
|
||||
def get_available_network_range(self):
|
||||
return self.spop(self.global_network_key).decode()
|
||||
|
||||
def add_available_port(self, port):
|
||||
self.sadd(self.global_port_key, str(port))
|
||||
|
||||
def get_available_port(self):
|
||||
return int(self.spop(self.global_port_key))
|
||||
|
||||
def acquire_lock(self):
|
||||
lock = self.lock(name=self.key, timeout=10)
|
||||
|
||||
if not lock.acquire(blocking=True, blocking_timeout=2.0):
|
||||
return False
|
||||
|
||||
self.current_lock = lock
|
||||
return True
|
||||
|
||||
def release_lock(self):
|
||||
if self.current_lock is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.current_lock.release()
|
||||
|
||||
return True
|
||||
except LockError:
|
||||
return False
|
||||
50
utils/checks.py
Normal file
50
utils/checks.py
Normal file
@ -0,0 +1,50 @@
|
||||
from docker.errors import DockerException, TLSParameterError, APIError, requests
|
||||
|
||||
from CTFd.utils import get_config
|
||||
|
||||
from .docker import get_docker_client
|
||||
from .routers import Router, _routers
|
||||
|
||||
|
||||
class WhaleChecks:
|
||||
@staticmethod
|
||||
def check_docker_api():
|
||||
try:
|
||||
client = get_docker_client()
|
||||
except TLSParameterError as e:
|
||||
return f'Docker TLS Parameters incorrect ({e})'
|
||||
except DockerException as e:
|
||||
return f'Docker API url incorrect ({e})'
|
||||
try:
|
||||
client.ping()
|
||||
except (APIError, requests.RequestException):
|
||||
return f'Unable to connect to Docker API, check your API connectivity'
|
||||
|
||||
credentials = get_config("whale:docker_credentials")
|
||||
if credentials and credentials.count(':') == 1:
|
||||
try:
|
||||
client.login(*credentials.split(':'))
|
||||
except DockerException:
|
||||
return f'Unable to log into docker registry, check your credentials'
|
||||
swarm = client.info()['Swarm']
|
||||
if not swarm['ControlAvailable']:
|
||||
return f'Docker swarm not available. You should initialize a swarm first. ($ docker swarm init)'
|
||||
|
||||
@staticmethod
|
||||
def check_frp_connection():
|
||||
router_conftype = get_config("whale:router_type", "frp")
|
||||
if router_conftype not in _routers:
|
||||
return "invalid router type: " + router_conftype
|
||||
ok, msg = _routers[router_conftype]().check_availability()
|
||||
if not ok:
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
def perform():
|
||||
errors = []
|
||||
for attr in dir(WhaleChecks):
|
||||
if attr.startswith('check_'):
|
||||
err = getattr(WhaleChecks, attr)()
|
||||
if err:
|
||||
errors.append(err)
|
||||
return errors
|
||||
61
utils/control.py
Normal file
61
utils/control.py
Normal file
@ -0,0 +1,61 @@
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from CTFd.utils import get_config
|
||||
from .db import DBContainer, db
|
||||
from .docker import DockerUtils
|
||||
from .routers import Router
|
||||
|
||||
|
||||
class ControlUtil:
|
||||
@staticmethod
|
||||
def try_add_container(user_id, challenge_id):
|
||||
container = DBContainer.create_container_record(user_id, challenge_id)
|
||||
try:
|
||||
DockerUtils.add_container(container)
|
||||
except Exception as e:
|
||||
DBContainer.remove_container_record(user_id)
|
||||
print(traceback.format_exc())
|
||||
return False, 'Docker Creation Error'
|
||||
ok, msg = Router.register(container)
|
||||
if not ok:
|
||||
DockerUtils.remove_container(container)
|
||||
DBContainer.remove_container_record(user_id)
|
||||
return False, msg
|
||||
return True, 'Container created'
|
||||
|
||||
@staticmethod
|
||||
def try_remove_container(user_id):
|
||||
container = DBContainer.get_current_containers(user_id=user_id)
|
||||
if not container:
|
||||
return False, 'No such container'
|
||||
for _ in range(3): # configurable? as "onerror_retry_cnt"
|
||||
try:
|
||||
ok, msg = Router.unregister(container)
|
||||
if not ok:
|
||||
return False, msg
|
||||
DockerUtils.remove_container(container)
|
||||
DBContainer.remove_container_record(user_id)
|
||||
return True, 'Container destroyed'
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
return False, 'Failed when destroying instance, please contact admin!'
|
||||
|
||||
@staticmethod
|
||||
def try_renew_container(user_id):
|
||||
container = DBContainer.get_current_containers(user_id)
|
||||
if not container:
|
||||
return False, 'No such container'
|
||||
timeout = int(get_config("whale:docker_timeout", "3600"))
|
||||
container.start_time = container.start_time + \
|
||||
datetime.timedelta(seconds=timeout)
|
||||
if container.start_time > datetime.datetime.now():
|
||||
container.start_time = datetime.datetime.now()
|
||||
# race condition? useless maybe?
|
||||
# useful when docker_timeout < poll timeout (10 seconds)
|
||||
# doesn't make any sense
|
||||
else:
|
||||
return False, 'Invalid container'
|
||||
container.renew_count += 1
|
||||
db.session.commit()
|
||||
return True, 'Container Renewed'
|
||||
104
utils/db.py
Normal file
104
utils/db.py
Normal file
@ -0,0 +1,104 @@
|
||||
import datetime
|
||||
|
||||
from CTFd.models import db
|
||||
from CTFd.utils import get_config
|
||||
from ..models import WhaleContainer, WhaleRedirectTemplate
|
||||
|
||||
|
||||
class DBContainer:
|
||||
@staticmethod
|
||||
def create_container_record(user_id, challenge_id):
|
||||
container = WhaleContainer(user_id=user_id, challenge_id=challenge_id)
|
||||
db.session.add(container)
|
||||
db.session.commit()
|
||||
|
||||
return container
|
||||
|
||||
@staticmethod
|
||||
def get_current_containers(user_id):
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(WhaleContainer.user_id == user_id)
|
||||
return q.first()
|
||||
|
||||
@staticmethod
|
||||
def get_container_by_port(port):
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(WhaleContainer.port == port)
|
||||
return q.first()
|
||||
|
||||
@staticmethod
|
||||
def remove_container_record(user_id):
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(WhaleContainer.user_id == user_id)
|
||||
q.delete()
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_all_expired_container():
|
||||
timeout = int(get_config("whale:docker_timeout", "3600"))
|
||||
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(
|
||||
WhaleContainer.start_time <
|
||||
datetime.datetime.now() - datetime.timedelta(seconds=timeout)
|
||||
)
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_all_alive_container():
|
||||
timeout = int(get_config("whale:docker_timeout", "3600"))
|
||||
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(
|
||||
WhaleContainer.start_time >=
|
||||
datetime.datetime.now() - datetime.timedelta(seconds=timeout)
|
||||
)
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_all_container():
|
||||
q = db.session.query(WhaleContainer)
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_all_alive_container_page(page_start, page_end):
|
||||
timeout = int(get_config("whale:docker_timeout", "3600"))
|
||||
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(
|
||||
WhaleContainer.start_time >=
|
||||
datetime.datetime.now() - datetime.timedelta(seconds=timeout)
|
||||
)
|
||||
q = q.slice(page_start, page_end)
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_all_alive_container_count():
|
||||
timeout = int(get_config("whale:docker_timeout", "3600"))
|
||||
|
||||
q = db.session.query(WhaleContainer)
|
||||
q = q.filter(
|
||||
WhaleContainer.start_time >=
|
||||
datetime.datetime.now() - datetime.timedelta(seconds=timeout)
|
||||
)
|
||||
return q.count()
|
||||
|
||||
|
||||
class DBRedirectTemplate:
|
||||
@staticmethod
|
||||
def get_all_templates():
|
||||
return WhaleRedirectTemplate.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_template(name, access_template, frp_template):
|
||||
if WhaleRedirectTemplate.query.filter_by(key=name).first():
|
||||
return # already existed
|
||||
db.session.add(WhaleRedirectTemplate(
|
||||
name, access_template, frp_template
|
||||
))
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def delete_template(name):
|
||||
WhaleRedirectTemplate.query.filter_by(key=name).delete()
|
||||
db.session.commit()
|
||||
202
utils/docker.py
Normal file
202
utils/docker.py
Normal file
@ -0,0 +1,202 @@
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
|
||||
import docker
|
||||
from flask import current_app
|
||||
|
||||
from CTFd.utils import get_config
|
||||
|
||||
from .cache import CacheProvider
|
||||
from .exceptions import WhaleError
|
||||
|
||||
|
||||
def get_docker_client():
|
||||
if get_config("whale:docker_use_ssl", False):
|
||||
tls_config = docker.tls.TLSConfig(
|
||||
verify=True,
|
||||
ca_cert=get_config("whale:docker_ssl_ca_cert") or None,
|
||||
client_cert=(
|
||||
get_config("whale:docker_ssl_client_cert"),
|
||||
get_config("whale:docker_ssl_client_key")
|
||||
),
|
||||
)
|
||||
return docker.DockerClient(
|
||||
base_url=get_config("whale:docker_api_url"),
|
||||
tls=tls_config,
|
||||
)
|
||||
else:
|
||||
return docker.DockerClient(base_url=get_config("whale:docker_api_url"))
|
||||
|
||||
|
||||
class DockerUtils:
|
||||
@staticmethod
|
||||
def init():
|
||||
try:
|
||||
DockerUtils.client = get_docker_client()
|
||||
# docker-py is thread safe: https://github.com/docker/docker-py/issues/619
|
||||
except Exception:
|
||||
raise WhaleError(
|
||||
'Docker Connection Error\n'
|
||||
'Please ensure the docker api url (first config item) is correct\n'
|
||||
'if you are using unix:///var/run/docker.sock, check if the socket is correctly mapped'
|
||||
)
|
||||
credentials = get_config("whale:docker_credentials")
|
||||
if credentials and credentials.count(':') == 1:
|
||||
try:
|
||||
DockerUtils.client.login(*credentials.split(':'))
|
||||
except Exception:
|
||||
raise WhaleError('docker.io failed to login, check your credentials')
|
||||
|
||||
@staticmethod
|
||||
def add_container(container):
|
||||
if container.challenge.docker_image.startswith("{"):
|
||||
DockerUtils._create_grouped_container(DockerUtils.client, container)
|
||||
else:
|
||||
DockerUtils._create_standalone_container(DockerUtils.client, container)
|
||||
|
||||
@staticmethod
|
||||
def _create_standalone_container(client, container):
|
||||
dns = get_config("whale:docker_dns", "").split(",")
|
||||
node = DockerUtils.choose_node(
|
||||
container.challenge.docker_image,
|
||||
get_config("whale:docker_swarm_nodes", "").split(",")
|
||||
)
|
||||
|
||||
client.services.create(
|
||||
image=container.challenge.docker_image,
|
||||
name=f'{container.user_id}-{container.uuid}',
|
||||
env={'FLAG': container.flag}, dns_config=docker.types.DNSConfig(nameservers=dns),
|
||||
networks=[get_config("whale:docker_auto_connect_network", "ctfd_frp-containers")],
|
||||
resources=docker.types.Resources(
|
||||
mem_limit=DockerUtils.convert_readable_text(
|
||||
container.challenge.memory_limit),
|
||||
cpu_limit=int(container.challenge.cpu_limit * 1e9)
|
||||
),
|
||||
labels={
|
||||
'whale_id': f'{container.user_id}-{container.uuid}'
|
||||
}, # for container deletion
|
||||
constraints=['node.labels.name==' + node],
|
||||
endpoint_spec=docker.types.EndpointSpec(mode='dnsrr', ports={})
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_grouped_container(client, container):
|
||||
range_prefix = CacheProvider(app=current_app).get_available_network_range()
|
||||
|
||||
ipam_pool = docker.types.IPAMPool(subnet=range_prefix)
|
||||
ipam_config = docker.types.IPAMConfig(
|
||||
driver='default', pool_configs=[ipam_pool])
|
||||
network_name = f'{container.user_id}-{container.uuid}'
|
||||
network = client.networks.create(
|
||||
network_name, internal=True,
|
||||
ipam=ipam_config, attachable=True,
|
||||
labels={'prefix': range_prefix},
|
||||
driver="overlay", scope="swarm"
|
||||
)
|
||||
|
||||
dns = []
|
||||
containers = get_config("whale:docker_auto_connect_containers", "").split(",")
|
||||
for c in containers:
|
||||
if not c:
|
||||
continue
|
||||
network.connect(c)
|
||||
if "dns" in c:
|
||||
network.reload()
|
||||
for name in network.attrs['Containers']:
|
||||
if network.attrs['Containers'][name]['Name'] == c:
|
||||
dns.append(network.attrs['Containers'][name]['IPv4Address'].split('/')[0])
|
||||
|
||||
has_processed_main = False
|
||||
try:
|
||||
images = json.loads(
|
||||
container.challenge.docker_image,
|
||||
object_pairs_hook=OrderedDict
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
raise WhaleError(
|
||||
"Challenge Image Parse Error\n"
|
||||
"plase check the challenge image string"
|
||||
)
|
||||
for name, image in images.items():
|
||||
if has_processed_main:
|
||||
container_name = f'{container.user_id}-{uuid.uuid4()}'
|
||||
else:
|
||||
container_name = f'{container.user_id}-{container.uuid}'
|
||||
node = DockerUtils.choose_node(image, get_config("whale:docker_swarm_nodes", "").split(","))
|
||||
has_processed_main = True
|
||||
client.services.create(
|
||||
image=image, name=container_name, networks=[
|
||||
docker.types.NetworkAttachmentConfig(network_name, aliases=[name])
|
||||
],
|
||||
env={'FLAG': container.flag},
|
||||
dns_config=docker.types.DNSConfig(nameservers=dns),
|
||||
resources=docker.types.Resources(
|
||||
mem_limit=DockerUtils.convert_readable_text(
|
||||
container.challenge.memory_limit
|
||||
),
|
||||
cpu_limit=int(container.challenge.cpu_limit * 1e9)),
|
||||
labels={
|
||||
'whale_id': f'{container.user_id}-{container.uuid}'
|
||||
}, # for container deletion
|
||||
hostname=name, constraints=['node.labels.name==' + node],
|
||||
endpoint_spec=docker.types.EndpointSpec(mode='dnsrr', ports={})
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_container(container):
|
||||
whale_id = f'{container.user_id}-{container.uuid}'
|
||||
|
||||
for s in DockerUtils.client.services.list(filters={'label': f'whale_id={whale_id}'}):
|
||||
s.remove()
|
||||
|
||||
networks = DockerUtils.client.networks.list(names=[whale_id])
|
||||
if len(networks) > 0: # is grouped containers
|
||||
auto_containers = get_config("whale:docker_auto_connect_containers", "").split(",")
|
||||
redis_util = CacheProvider(app=current_app)
|
||||
for network in networks:
|
||||
for container in auto_containers:
|
||||
try:
|
||||
network.disconnect(container, force=True)
|
||||
except Exception:
|
||||
pass
|
||||
redis_util.add_available_network_range(network.attrs['Labels']['prefix'])
|
||||
network.remove()
|
||||
|
||||
@staticmethod
|
||||
def convert_readable_text(text):
|
||||
lower_text = text.lower()
|
||||
|
||||
if lower_text.endswith("k"):
|
||||
return int(text[:-1]) * 1024
|
||||
|
||||
if lower_text.endswith("m"):
|
||||
return int(text[:-1]) * 1024 * 1024
|
||||
|
||||
if lower_text.endswith("g"):
|
||||
return int(text[:-1]) * 1024 * 1024 * 1024
|
||||
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def choose_node(image, nodes):
|
||||
win_nodes = []
|
||||
linux_nodes = []
|
||||
for node in nodes:
|
||||
if node.startswith("windows"):
|
||||
win_nodes.append(node)
|
||||
else:
|
||||
linux_nodes.append(node)
|
||||
try:
|
||||
tag = image.split(":")[1:]
|
||||
if len(tag) and tag[0].startswith("windows"):
|
||||
return random.choice(win_nodes)
|
||||
return random.choice(linux_nodes)
|
||||
except IndexError:
|
||||
raise WhaleError(
|
||||
'No Suitable Nodes.\n'
|
||||
'If you are using Whale for the first time, \n'
|
||||
'Please Setup Swarm Nodes Correctly and Lable Them with\n'
|
||||
'docker node update --label-add "name=linux-1" $(docker node ls -q)'
|
||||
)
|
||||
8
utils/exceptions.py
Normal file
8
utils/exceptions.py
Normal file
@ -0,0 +1,8 @@
|
||||
class WhaleError(Exception):
|
||||
def __init__(self, msg):
|
||||
super().__init__(msg)
|
||||
self.message = msg
|
||||
|
||||
|
||||
class WhaleWarning(Warning):
|
||||
pass
|
||||
34
utils/routers/__init__.py
Normal file
34
utils/routers/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
from CTFd.utils import get_config
|
||||
|
||||
from .frp import FrpRouter
|
||||
from .trp import TrpRouter
|
||||
|
||||
_routers = {
|
||||
'frp': FrpRouter,
|
||||
'trp': TrpRouter,
|
||||
}
|
||||
|
||||
|
||||
def instanciate(cls):
|
||||
return cls()
|
||||
|
||||
|
||||
@instanciate
|
||||
class Router:
|
||||
_name = ''
|
||||
_router = None
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
router_conftype = get_config("whale:router_type", "frp")
|
||||
if Router._name != router_conftype:
|
||||
Router._router = _routers[router_conftype]()
|
||||
Router._name = router_conftype
|
||||
return getattr(Router._router, name)
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
Router._name = ''
|
||||
Router._router = None
|
||||
|
||||
|
||||
__all__ = ["Router"]
|
||||
25
utils/routers/base.py
Normal file
25
utils/routers/base.py
Normal file
@ -0,0 +1,25 @@
|
||||
import typing
|
||||
|
||||
from ...models import WhaleContainer
|
||||
|
||||
|
||||
class BaseRouter:
|
||||
name = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def access(self, container: WhaleContainer):
|
||||
pass
|
||||
|
||||
def register(self, container: WhaleContainer):
|
||||
pass
|
||||
|
||||
def unregister(self, container: WhaleContainer):
|
||||
pass
|
||||
|
||||
def reload(self):
|
||||
pass
|
||||
|
||||
def check_availability(self) -> typing.Tuple[bool, str]:
|
||||
pass
|
||||
132
utils/routers/frp.py
Normal file
132
utils/routers/frp.py
Normal file
@ -0,0 +1,132 @@
|
||||
import warnings
|
||||
|
||||
from flask import current_app
|
||||
from requests import session, RequestException
|
||||
|
||||
from CTFd.models import db
|
||||
from CTFd.utils import get_config, set_config, logging
|
||||
|
||||
from .base import BaseRouter
|
||||
from ..cache import CacheProvider
|
||||
from ..db import DBContainer
|
||||
from ..exceptions import WhaleError, WhaleWarning
|
||||
from ...models import WhaleContainer
|
||||
|
||||
|
||||
class FrpRouter(BaseRouter):
|
||||
name = "frp"
|
||||
types = {
|
||||
'direct': 'tcp',
|
||||
'http': 'http',
|
||||
}
|
||||
|
||||
class FrpRule:
|
||||
def __init__(self, name, config):
|
||||
self.name = name
|
||||
self.config = config
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'[{self.name}]\n' + '\n'.join(f'{k} = {v}' for k, v in self.config.items())
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ses = session()
|
||||
self.url = get_config("whale:frp_api_url").rstrip("/")
|
||||
self.common = ''
|
||||
try:
|
||||
CacheProvider(app=current_app).init_port_sets()
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"cache initialization failed",
|
||||
WhaleWarning
|
||||
)
|
||||
|
||||
def reload(self, exclude=None):
|
||||
rules = []
|
||||
for container in DBContainer.get_all_alive_container():
|
||||
if container.uuid == exclude:
|
||||
continue
|
||||
name = f'{container.challenge.redirect_type}_{container.user_id}_{container.uuid}'
|
||||
config = {
|
||||
'type': self.types[container.challenge.redirect_type],
|
||||
'local_ip': f'{container.user_id}-{container.uuid}',
|
||||
'local_port': container.challenge.redirect_port,
|
||||
'use_compression': 'true',
|
||||
}
|
||||
if config['type'] == 'http':
|
||||
config['subdomain'] = container.http_subdomain
|
||||
elif config['type'] == 'tcp':
|
||||
config['remote_port'] = container.port
|
||||
rules.append(self.FrpRule(name, config))
|
||||
|
||||
try:
|
||||
if not self.common:
|
||||
common = get_config("whale:frp_config_template", '')
|
||||
if '[common]' in common:
|
||||
self.common = common
|
||||
else:
|
||||
remote = self.ses.get(f'{self.url}/api/config')
|
||||
assert remote.status_code == 200
|
||||
set_config("whale:frp_config_template", remote.text)
|
||||
self.common = remote.text
|
||||
config = self.common + '\n' + '\n'.join(str(r) for r in rules)
|
||||
assert self.ses.put(
|
||||
f'{self.url}/api/config', config, timeout=5
|
||||
).status_code == 200
|
||||
assert self.ses.get(
|
||||
f'{self.url}/api/reload', timeout=5
|
||||
).status_code == 200
|
||||
except (RequestException, AssertionError) as e:
|
||||
raise WhaleError(
|
||||
'\nfrpc request failed\n' +
|
||||
(f'{e}\n' if str(e) else '') +
|
||||
'please check the frp related configs'
|
||||
) from None
|
||||
|
||||
def access(self, container: WhaleContainer):
|
||||
if container.challenge.redirect_type == 'direct':
|
||||
return f'nc {get_config("whale:frp_direct_ip_address", "127.0.0.1")} {container.port}'
|
||||
elif container.challenge.redirect_type == 'http':
|
||||
host = get_config("whale:frp_http_domain_suffix", "")
|
||||
port = get_config("whale:frp_http_port", "80")
|
||||
host += f':{port}' if port != 80 else ''
|
||||
return f'<a target="_blank" href="http://{container.http_subdomain}.{host}/">Link to the Challenge</a>'
|
||||
return ''
|
||||
|
||||
def register(self, container: WhaleContainer):
|
||||
if container.challenge.redirect_type == 'direct':
|
||||
if not container.port:
|
||||
port = CacheProvider(app=current_app).get_available_port()
|
||||
if not port:
|
||||
return False, 'No available ports. Please wait for a few minutes.'
|
||||
container.port = port
|
||||
db.session.commit()
|
||||
elif container.challenge.redirect_type == 'http':
|
||||
# config['subdomain'] = container.http_subdomain
|
||||
pass
|
||||
self.reload()
|
||||
return True, 'success'
|
||||
|
||||
def unregister(self, container: WhaleContainer):
|
||||
if container.challenge.redirect_type == 'direct':
|
||||
try:
|
||||
redis_util = CacheProvider(app=current_app)
|
||||
redis_util.add_available_port(container.port)
|
||||
except Exception as e:
|
||||
logging.log(
|
||||
'whale', 'Error deleting port from cache',
|
||||
name=container.user.name,
|
||||
challenge_id=container.challenge_id,
|
||||
)
|
||||
return False, 'Error deleting port from cache'
|
||||
self.reload(exclude=container.uuid)
|
||||
return True, 'success'
|
||||
|
||||
def check_availability(self):
|
||||
try:
|
||||
resp = self.ses.get(f'{self.url}/api/status', timeout=2.0)
|
||||
except RequestException as e:
|
||||
return False, 'Unable to access frpc admin api'
|
||||
if resp.status_code == 401:
|
||||
return False, 'frpc admin api unauthorized'
|
||||
return True, 'Available'
|
||||
69
utils/routers/trp.py
Normal file
69
utils/routers/trp.py
Normal file
@ -0,0 +1,69 @@
|
||||
import traceback
|
||||
from requests import session, RequestException, HTTPError
|
||||
|
||||
from CTFd.utils import get_config
|
||||
from .base import BaseRouter
|
||||
from ..db import DBContainer, WhaleContainer
|
||||
|
||||
|
||||
class TrpRouter(BaseRouter):
|
||||
name = "trp"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ses = session()
|
||||
self.url = get_config('whale:trp_api_url', '').rstrip("/")
|
||||
self.common = ''
|
||||
for container in DBContainer.get_all_alive_container():
|
||||
self.register(container)
|
||||
|
||||
@staticmethod
|
||||
def get_domain(container: WhaleContainer):
|
||||
domain = get_config('whale:trp_domain_suffix', '127.0.0.1.nip.io').lstrip('.')
|
||||
domain = f'{container.uuid}.{domain}'
|
||||
return domain
|
||||
|
||||
def access(self, container: WhaleContainer):
|
||||
ch_type = container.challenge.redirect_type
|
||||
domain = self.get_domain(container)
|
||||
port = get_config('whale:trp_listening_port', 1443)
|
||||
if ch_type == 'direct':
|
||||
return f'from pwn import *<br>remote("{domain}", {port}, ssl=True).interactive()'
|
||||
elif ch_type == 'http':
|
||||
return f'https://{domain}' + (f':{port}' if port != 443 else '')
|
||||
else:
|
||||
return f'[ssl] {domain} {port}'
|
||||
|
||||
def register(self, container: WhaleContainer):
|
||||
try:
|
||||
resp = self.ses.post(f'{self.url}/rule/{self.get_domain(container)}', json={
|
||||
'target': f'{container.user_id}-{container.uuid}:{container.challenge.redirect_port}',
|
||||
'source': None,
|
||||
})
|
||||
resp.raise_for_status()
|
||||
return True, 'success'
|
||||
except HTTPError as e:
|
||||
return False, e.response.text
|
||||
except RequestException as e:
|
||||
print(traceback.format_exc())
|
||||
return False, 'unable to access trp Api'
|
||||
|
||||
def unregister(self, container: WhaleContainer):
|
||||
try:
|
||||
resp = self.ses.delete(f'{self.url}/rule/{self.get_domain(container)}')
|
||||
resp.raise_for_status()
|
||||
return True, 'success'
|
||||
except HTTPError as e:
|
||||
return False, e.response.text
|
||||
except RequestException as e:
|
||||
print(traceback.format_exc())
|
||||
return False, 'unable to access trp Api'
|
||||
|
||||
def check_availability(self):
|
||||
try:
|
||||
resp = self.ses.get(f'{self.url}/rules').json()
|
||||
except RequestException as e:
|
||||
return False, 'Unable to access trp admin api'
|
||||
except Exception as e:
|
||||
return False, 'Unknown trp error'
|
||||
return True, 'Available'
|
||||
60
utils/setup.py
Normal file
60
utils/setup.py
Normal file
@ -0,0 +1,60 @@
|
||||
from CTFd.utils import set_config
|
||||
|
||||
from ..models import WhaleRedirectTemplate, db
|
||||
|
||||
|
||||
def setup_default_configs():
|
||||
for key, val in {
|
||||
'setup': 'true',
|
||||
'docker_api_url': 'unix:///var/run/docker.sock',
|
||||
'docker_credentials': '',
|
||||
'docker_dns': '127.0.0.1',
|
||||
'docker_max_container_count': '100',
|
||||
'docker_max_renew_count': '5',
|
||||
'docker_subnet': '174.1.0.0/16',
|
||||
'docker_subnet_new_prefix': '24',
|
||||
'docker_swarm_nodes': 'linux-1',
|
||||
'docker_timeout': '3600',
|
||||
'frp_api_url': 'http://frpc:7400',
|
||||
'frp_http_port': '8080',
|
||||
'frp_http_domain_suffix': '127.0.0.1.nip.io',
|
||||
'frp_direct_port_maximum': '10100',
|
||||
'frp_direct_port_minimum': '10000',
|
||||
'template_http_subdomain': '{{ container.uuid }}',
|
||||
'template_chall_flag': '{{ "flag{"+uuid.uuid4()|string+"}" }}',
|
||||
}.items():
|
||||
set_config('whale:' + key, val)
|
||||
db.session.add(WhaleRedirectTemplate(
|
||||
'http',
|
||||
'http://{{ container.http_subdomain }}.'
|
||||
'{{ get_config("whale:frp_http_domain_suffix", "") }}'
|
||||
'{% if get_config("whale:frp_http_port", "80") != 80 %}:{{ get_config("whale:frp_http_port") }}{% endif %}/',
|
||||
'''
|
||||
[http_{{ container.user_id|string }}-{{ container.uuid }}]
|
||||
type = http
|
||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
|
||||
local_port = {{ container.challenge.redirect_port }}
|
||||
subdomain = {{ container.http_subdomain }}
|
||||
use_compression = true
|
||||
'''
|
||||
))
|
||||
db.session.add(WhaleRedirectTemplate(
|
||||
'direct',
|
||||
'nc {{ get_config("whale:frp_direct_ip_address", "127.0.0.1") }} {{ container.port }}',
|
||||
'''
|
||||
[direct_{{ container.user_id|string }}-{{ container.uuid }}]
|
||||
type = tcp
|
||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
|
||||
local_port = {{ container.challenge.redirect_port }}
|
||||
remote_port = {{ container.port }}
|
||||
use_compression = true
|
||||
|
||||
[direct_{{ container.user_id|string }}-{{ container.uuid }}_udp]
|
||||
type = udp
|
||||
local_ip = {{ container.user_id|string }}-{{ container.uuid }}
|
||||
local_port = {{ container.challenge.redirect_port }}
|
||||
remote_port = {{ container.port }}
|
||||
use_compression = true
|
||||
'''
|
||||
))
|
||||
db.session.commit()
|
||||
Reference in New Issue
Block a user