diff --git a/dcc/driver/admin.py b/dcc/driver/admin.py index 7d2acc9..57d2e02 100644 --- a/dcc/driver/admin.py +++ b/dcc/driver/admin.py @@ -3,4 +3,27 @@ from solo.admin import SingletonModelAdmin from driver.models import DriverConfiguration -admin.site.register(DriverConfiguration, SingletonModelAdmin) + +@admin.register(DriverConfiguration) +class DriverConfigurationAdmin(SingletonModelAdmin): + fieldsets = ( + ( + "Remote DCC-EX configuration", + { + "fields": ( + "remote_host", + "remote_port", + "timeout", + ) + }, + ), + ( + "Firewall setting", + { + "fields": ( + "network", + "subnet_mask", + ) + }, + ), + ) diff --git a/dcc/driver/migrations/0002_driverconfiguration_network_and_more.py b/dcc/driver/migrations/0002_driverconfiguration_network_and_more.py new file mode 100644 index 0000000..baeba91 --- /dev/null +++ b/dcc/driver/migrations/0002_driverconfiguration_network_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.3 on 2022-04-10 15:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('driver', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='driverconfiguration', + name='network', + field=models.GenericIPAddressField(default='192.168.4.0', protocol='IPv4'), + ), + migrations.AddField( + model_name='driverconfiguration', + name='subnet_mask', + field=models.GenericIPAddressField(default='255.255.255.0', protocol='IPv4'), + ), + ] diff --git a/dcc/driver/migrations/0003_alter_driverconfiguration_network_and_more.py b/dcc/driver/migrations/0003_alter_driverconfiguration_network_and_more.py new file mode 100644 index 0000000..d10a8cd --- /dev/null +++ b/dcc/driver/migrations/0003_alter_driverconfiguration_network_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.3 on 2022-04-10 16:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('driver', '0002_driverconfiguration_network_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='driverconfiguration', + name='network', + field=models.GenericIPAddressField(blank=True, default='192.168.4.0', null=True, protocol='IPv4'), + ), + migrations.AlterField( + model_name='driverconfiguration', + name='remote_host', + field=models.GenericIPAddressField(blank=True, default='192.168.4.1', null=True, protocol='IPv4'), + ), + migrations.AlterField( + model_name='driverconfiguration', + name='subnet_mask', + field=models.GenericIPAddressField(blank=True, default='255.255.255.0', null=True, protocol='IPv4'), + ), + ] diff --git a/dcc/driver/migrations/0004_alter_driverconfiguration_remote_host.py b/dcc/driver/migrations/0004_alter_driverconfiguration_remote_host.py new file mode 100644 index 0000000..56deee7 --- /dev/null +++ b/dcc/driver/migrations/0004_alter_driverconfiguration_remote_host.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-04-10 16:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('driver', '0003_alter_driverconfiguration_network_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='driverconfiguration', + name='remote_host', + field=models.GenericIPAddressField(default='192.168.4.1', protocol='IPv4'), + ), + ] diff --git a/dcc/driver/models.py b/dcc/driver/models.py index f5a72df..ddb7838 100644 --- a/dcc/driver/models.py +++ b/dcc/driver/models.py @@ -1,16 +1,41 @@ from django.db import models +from django.core.exceptions import ValidationError +from ipaddress import IPv4Address, IPv4Network from solo.models import SingletonModel class DriverConfiguration(SingletonModel): remote_host = models.GenericIPAddressField( - protocol="IPv4", default="192.168.4.1" + protocol="IPv4", + default="192.168.4.1" ) remote_port = models.SmallIntegerField(default=2560) timeout = models.SmallIntegerField(default=250) + network = models.GenericIPAddressField( + protocol="IPv4", + default="192.168.4.0", + blank=True, + null=True + ) + subnet_mask = models.GenericIPAddressField( + protocol="IPv4", + default="255.255.255.0", + blank=True, + null=True + ) + def __str__(self): return "Configuration" + def clean(self, *args, **kwargs): + if self.network: + try: + IPv4Network( + "{0}/{1}".format(self.network, self.subnet_mask)) + except ValueError as e: + raise ValidationError(e) + super().clean(*args, **kwargs) + class Meta: verbose_name = "Configuration" diff --git a/dcc/driver/views.py b/dcc/driver/views.py index ed51f7a..4a50b9d 100644 --- a/dcc/driver/views.py +++ b/dcc/driver/views.py @@ -1,10 +1,17 @@ +from ipaddress import IPv4Address, IPv4Network from django.http import Http404 from django.utils.decorators import method_decorator from rest_framework import status, serializers from rest_framework.views import APIView from rest_framework.response import Response +from rest_framework.permissions import ( + IsAuthenticated, + BasePermission, + SAFE_METHODS +) from dcc.parsers import PlainTextParser +from driver.models import DriverConfiguration from driver.connector import Connector from driver.serializers import ( FunctionSerializer, @@ -27,12 +34,35 @@ def addresschecker(f): return addresslookup +class Firewall(BasePermission): + def has_permission(self, request, view): + config = DriverConfiguration.get_solo() + if not config.network: + return request.method in SAFE_METHODS + + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = IPv4Address(x_forwarded_for.split(',')[0]) + else: + ip = IPv4Address(request.META.get("REMOTE_ADDR")) + + network = IPv4Network("{0}/{1}".format( + config.network, + config.subnet_mask + )) + + # accept IP configured is settings or localhost + if ip in network or ip in IPv4Network("127.0.0.0/8"): + return request.method in SAFE_METHODS + + class Test(APIView): """ Send a test command """ parser_classes = [PlainTextParser] + permission_classes = [IsAuthenticated | Firewall] def get(self, request): response = Connector().passthrough("") @@ -47,6 +77,7 @@ class SendCommand(APIView): """ parser_classes = [PlainTextParser] + permission_classes = [IsAuthenticated | Firewall] def put(self, request): data = request.data @@ -70,6 +101,7 @@ class Function(APIView): """ Send "Function" commands to a valid DCC address """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request, address): serializer = FunctionSerializer(data=request.data) @@ -85,6 +117,7 @@ class Cab(APIView): """ Send "Cab" commands to a valid DCC address """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request, address): serializer = CabSerializer(data=request.data) @@ -99,6 +132,7 @@ class Infra(APIView): """ Send "Infra" commands to a valid DCC address """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request): serializer = InfraSerializer(data=request.data) @@ -113,6 +147,7 @@ class Emergency(APIView): """ Send an "Emergency" stop, no matter the HTTP method used """ + permission_classes = [IsAuthenticated | Firewall] def put(self, request): Connector().emergency() diff --git a/dcc/portal/static/css/main.css b/dcc/portal/static/css/main.css index d06f61a..20f73a5 100644 --- a/dcc/portal/static/css/main.css +++ b/dcc/portal/static/css/main.css @@ -1,3 +1,7 @@ .card > a > img { width: 100%; } + +#footer > p { + display: inline; +} diff --git a/dcc/portal/templates/footer.html b/dcc/portal/templates/footer.html index 5e85072..67cdc67 100644 --- a/dcc/portal/templates/footer.html +++ b/dcc/portal/templates/footer.html @@ -5,8 +5,12 @@

Back to top

-

© {% now "Y" %} {{ site_conf.footer | markdown | safe }}

-

{{ site_conf.footer_extended | markdown | safe }}

+ + {% if site_conf.show_version %}