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 @@
© {% now "Y" %} {{ site_conf.footer | markdown | safe }}
-{{ site_conf.footer_extended | markdown | safe }}
+ + {% if site_conf.show_version %}