# SPDX-License-Identifier: AGPL-3.0-or-later """ Views for WireGuard application. """ from io import BytesIO import urllib.parse from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404, HttpResponse from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import FormView, TemplateView, View from plinth import network from plinth.modules.names.components import DomainName from plinth.views import AppView from . import forms, utils class WireguardView(AppView): """Serve configuration page.""" app_id = 'wireguard' diagnostics_module_name = 'wireguard' template_name = 'wireguard.html' def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) info = utils.get_info() server_info = info['my_server'] context['server'] = server_info context['client_peers'] = info['my_client']['servers'] context['server_endpoints'] = [] if server_info: domains = DomainName.list_names(filter_for_service='wireguard') listen_port = server_info.get('listen_port') context['server_endpoints'] = [ f'{domain}:{listen_port}' for domain in domains if not domain.endswith('.local') ] return context class AddClientView(SuccessMessageMixin, FormView): """View to add a client.""" form_class = forms.AddClientForm template_name = 'wireguard_add_client.html' success_url = reverse_lazy('wireguard:index') success_message = _('Added new client.') def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Add Allowed Client') # Show next available IP. try: connection = utils._server_connection() setting_name = utils.nm.SETTING_WIREGUARD_SETTING_NAME settings = connection.get_setting_by_name(setting_name) context['next_ip'] = utils._get_next_available_ip_address(settings) except Exception: context['next_ip'] = None return context def form_valid(self, form): """Add the client.""" public_key = form.cleaned_data.get('public_key') try: utils.add_client(public_key) except ValueError: messages.warning(self.request, _('Client with public key already exists')) return redirect('wireguard:index') return super().form_valid(form) class SessionClientDataMixin: """Shared session data loading for auto-client views.""" def get_session_client_data(self, request): """Extract client data from session.""" next_ip = request.session.get('next_ip') pubkey = request.session.get('client_pubkey') privkey = request.session.get('client_privkey') endpoint = request.session.get('endpoint') if not all([next_ip, privkey, pubkey, endpoint]): raise Http404("Session expired") return { 'next_ip': next_ip, 'privkey': privkey, 'pubkey': pubkey, 'endpoint': endpoint } def get_client_config(self, request): """Rebuild client config from session.""" data = self.get_session_client_data(request) return utils.build_client_config(data['next_ip'], data['privkey'], data['pubkey'], data['endpoint']) class ClientActionsView(SessionClientDataMixin, View): action = None def get(self, request): import segno config = self.get_client_config(request) if self.action == 'download': response = HttpResponse(config, content_type='text/plain') response['Content-Disposition'] = \ 'attachment; filename="wg-client.conf"' return response elif self.action == 'qr': qrcode = segno.make(config) buffer = BytesIO() qrcode.save(buffer, kind='svg', scale=5) return HttpResponse(buffer.getvalue(), content_type='image/svg+xml') raise Http404("Invalid action") class AutoAddClientView(SuccessMessageMixin, FormView): """View to add a client with keypair generation.""" form_class = forms.AutoAddClientForm template_name = 'wireguard_auto_add_client.html' success_url = reverse_lazy('wireguard:index') success_message = _('Added new client.') def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Add Allowed Client') context['domains'] = [] info = utils.get_info() server_info = info['my_server'] if server_info: domains = DomainName.list_names(filter_for_service='wireguard') filtered_domains = [ domain for domain in domains if not domain.endswith('.local') ] port = server_info.get('listen_port', 51820) endpoint = f"{filtered_domains[0]}:{port}" try: client_privkey, client_pubkey = utils.generate_client_keypair() # Get next IP connection = utils._server_connection() setting_name = utils.nm.SETTING_WIREGUARD_SETTING_NAME settings = connection.get_setting_by_name(setting_name) next_ip = utils._get_next_available_ip_address(settings) data = { 'next_ip': next_ip, 'client_privkey': client_privkey, 'client_pubkey': client_pubkey, 'endpoint': endpoint } # Add properties to template context context['domains'] = filtered_domains context.update(data) # Store info on instance for reuse self.request.session.update(data) except Exception as e: messages.warning(f"Client key generation failed: {e}") pass return context def form_valid(self, form): """Add client using generated public key.""" try: client_pubkey = self.request.session.pop('client_pubkey') utils.add_client(client_pubkey) except KeyError: messages.warning(self.request, _('Session expired. Please try again.')) return redirect('wireguard:auto-add-client') except ValueError: messages.warning(self.request, _('Client already exists')) return redirect('wireguard:index') return super().form_valid(form) class ShowClientView(SuccessMessageMixin, TemplateView): """View to show a client's details.""" template_name = 'wireguard_show_client.html' def get_context_data(self, **kwargs) -> dict[str, object]: """Return additional context data for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Allowed Client') public_key = urllib.parse.unquote(self.kwargs['public_key']) server_info = utils.get_info()['my_server'] if not server_info or public_key not in server_info['peers']: raise Http404 domains = DomainName.list_names(filter_for_service='wireguard') context['server'] = server_info context['client'] = server_info['peers'][public_key] context['endpoints'] = [ domain + ':' + str(server_info['listen_port']) for domain in domains if not domain.endswith('.local') ] return context class EditClientView(SuccessMessageMixin, FormView): """View to modify a client.""" form_class = forms.AddClientForm template_name = 'wireguard_edit_client.html' success_url = reverse_lazy('wireguard:index') success_message = _('Updated client.') def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Modify Client') return context def get_initial(self): """Get initial form data.""" initial = super().get_initial() initial['public_key'] = urllib.parse.unquote(self.kwargs['public_key']) return initial def form_valid(self, form): """Update the client.""" old_public_key = form.initial['public_key'] public_key = form.cleaned_data.get('public_key') if old_public_key != public_key: try: utils.add_client(public_key) except ValueError: messages.warning(self.request, _('Client with public key already exists')) utils.remove_client(old_public_key) return super().form_valid(form) class DeleteClientView(SuccessMessageMixin, TemplateView): """View to delete a client.""" template_name = 'wireguard_delete_client.html' def get_context_data(self, **kwargs): """Return additional context data for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Delete Allowed Client') context['public_key'] = urllib.parse.unquote(self.kwargs['public_key']) return context def post(self, request, public_key): """Delete the client.""" public_key = urllib.parse.unquote(public_key) try: utils.remove_client(public_key) messages.success(request, _('Client deleted.')) except KeyError: messages.error(request, _('Client not found')) return redirect('wireguard:index') class AddServerView(SuccessMessageMixin, FormView): """View to add a server.""" form_class = forms.AddServerForm template_name = 'wireguard_add_server.html' success_url = reverse_lazy('wireguard:index') success_message = _('Added new server.') def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Add Connection to Server') return context def form_valid(self, form): """Add the server.""" utils.add_server(form.get_settings()) return super().form_valid(form) class ShowServerView(SuccessMessageMixin, TemplateView): """View to show a server's details.""" template_name = 'wireguard_show_server.html' def get_context_data(self, **kwargs): """Return additional context data for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Connection to Server') interface = self.kwargs['interface'] info = utils.get_info() server = info['my_client']['servers'].get(interface) if not server: raise Http404 context['interface'] = interface context['server'] = server return context class EditServerView(SuccessMessageMixin, FormView): """View to modify a server.""" form_class = forms.AddServerForm template_name = 'wireguard_edit_server.html' success_url = reverse_lazy('wireguard:index') success_message = _('Updated server.') def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Modify Connection to Server') return context def get_initial(self): """Get initial form data.""" initial = super().get_initial() interface = self.kwargs['interface'] info = utils.get_nm_info() server = info.get(interface) if not server: raise Http404 initial['ip_address_and_network'] = server.get( 'ip_address_and_network') if server['peers']: peer = next(peer for peer in server['peers'].values()) initial['peer_endpoint'] = peer['endpoint'] initial['peer_public_key'] = peer['public_key'] initial['private_key'] = server['private_key'] initial['preshared_key'] = peer['preshared_key'] initial['default_route'] = server['default_route'] return initial def form_valid(self, form): """Update the server.""" interface = self.kwargs['interface'] utils.edit_server(interface, form.get_settings()) return super().form_valid(form) class DeleteServerView(SuccessMessageMixin, TemplateView): """View to delete a server.""" template_name = 'wireguard_delete_server.html' def get_context_data(self, **kwargs): """Return additional context data for rendering the template.""" context = super().get_context_data(**kwargs) context['title'] = _('Delete Connection to Server') interface = self.kwargs['interface'] info = utils.get_nm_info() server = info.get(interface) if not server: raise Http404 context['interface'] = interface if server['peers']: peer = next(peer for peer in server['peers'].values()) context['peer_endpoint'] = peer['endpoint'] context['peer_public_key'] = peer['public_key'] return context def post(self, request, interface): """Delete the server.""" connection = network.get_connection_by_interface_name(interface) network.delete_connection(connection.get_uuid()) messages.success(request, _('Server deleted.')) return redirect('wireguard:index') class EnableServerView(SuccessMessageMixin, View): """View to enable the WireGuard server.""" def post(self, request): """Create server interface.""" try: utils.setup_server() messages.success(request, _('WireGuard server started successfully.')) except Exception as error: messages.error( request, _('Failed to start WireGuard server: {}').format(error)) return redirect('wireguard:index')