mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
dns/ddclient - move checkip properties from general settings to account and implement interface selection using curl. closes https://github.com/opnsense/plugins/issues/2863
o adds migration taking previous use_interface toggle into account o embed fetch address into new script /usr/local/opnsense/scripts/ddclient/checkip o guess protocol as the result can't contain two addresses anyway (service determines protocol) o add OVH, provided by @toxic0berliner
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
PLUGIN_NAME= ddclient
|
||||
PLUGIN_VERSION= 1.2
|
||||
PLUGIN_VERSION= 1.3
|
||||
PLUGIN_DEPENDS= ddclient
|
||||
PLUGIN_COMMENT= Dynamic DNS client
|
||||
PLUGIN_MAINTAINER= ad@opnsense.org
|
||||
|
||||
@@ -6,6 +6,11 @@ WWW: https://github.com/ddclient/ddclient
|
||||
Plugin Changelog
|
||||
================
|
||||
|
||||
1.3
|
||||
|
||||
* Add checkip settings per account using selected source interface when provided
|
||||
* Add OVH DynHost to the DynDNS providers
|
||||
|
||||
1.2
|
||||
|
||||
* Add Loopia (contributed by Johan Lilja)
|
||||
|
||||
-4
@@ -45,10 +45,6 @@ class AccountsController extends ApiMutableModelControllerBase
|
||||
"description"
|
||||
);
|
||||
foreach ($result['rows'] as &$row) {
|
||||
if ($row['use_interface'] == "0") {
|
||||
$row['interface'] = "";
|
||||
}
|
||||
unset($row['use_interface']);
|
||||
if ($row['service'] == 'Custom') {
|
||||
$row['service'] = 'Custom (' . $row['protocol'] . ')';
|
||||
}
|
||||
|
||||
+10
-4
@@ -60,14 +60,20 @@
|
||||
<help>Hostname to update</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>account.use_interface</id>
|
||||
<label>Use interface IP</label>
|
||||
<id>account.checkip</id>
|
||||
<label>Check ip method</label>
|
||||
<type>dropdown</type>
|
||||
<help>How to determine the address to use for this host</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>account.force_ssl</id>
|
||||
<label>Force SSL</label>
|
||||
<type>checkbox</type>
|
||||
<help>Use the IP of a specified interface for the update</help>
|
||||
<help>Force update using HTTPS</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>account.interface</id>
|
||||
<label>Interface</label>
|
||||
<label>Interface to monitor</label>
|
||||
<type>dropdown</type>
|
||||
</field>
|
||||
<field>
|
||||
|
||||
@@ -12,29 +12,10 @@
|
||||
<advanced>true</advanced>
|
||||
<help>Enable verbose logging</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>ddclient.general.force_ssl</id>
|
||||
<label>Force SSL</label>
|
||||
<type>checkbox</type>
|
||||
<advanced>true</advanced>
|
||||
<help>Force update using HTTPS</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>ddclient.general.daemon_delay</id>
|
||||
<label>Interval</label>
|
||||
<type>text</type>
|
||||
<help>Interval in seconds to check for address changes</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>ddclient.general.checkip</id>
|
||||
<label>Check ip method</label>
|
||||
<type>dropdown</type>
|
||||
<help>How to determine the address to use for this host</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>ddclient.general.interface</id>
|
||||
<label>Interface</label>
|
||||
<type>dropdown</type>
|
||||
<style>optional_setting checkip_if</style>
|
||||
</field>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<model>
|
||||
<mount>//OPNsense/DynDNS</mount>
|
||||
<version>1.1.0</version>
|
||||
<version>1.2.0</version>
|
||||
<description>
|
||||
Dynamic DNS client
|
||||
</description>
|
||||
@@ -14,44 +14,12 @@
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</verbose>
|
||||
<force_ssl type="BooleanField">
|
||||
<default>1</default>
|
||||
<Required>Y</Required>
|
||||
</force_ssl>
|
||||
<daemon_delay type="IntegerField">
|
||||
<default>300</default>
|
||||
<Required>Y</Required>
|
||||
<MinimumValue>1</MinimumValue>
|
||||
<MaximumValue>86400</MaximumValue>
|
||||
</daemon_delay>
|
||||
<checkip type="OptionField">
|
||||
<Required>Y</Required>
|
||||
<default>web_dyndns</default>
|
||||
<ValidationMessage>An IP service type is required.</ValidationMessage>
|
||||
<OptionValues>
|
||||
<web_dyndns>dyndns</web_dyndns>
|
||||
<web_freedns>freedns</web_freedns>
|
||||
<web_googledomains>googledomains</web_googledomains>
|
||||
<web_he>he</web_he>
|
||||
<web_ip4only_me value="web_ip4only.me">ip4only.me</web_ip4only_me>
|
||||
<web_ip6only_me value="web_ip6only.me">ip6only.me</web_ip6only_me>
|
||||
<web_ipify_ipv4 value="web_ipify-ipv4">ipify-ipv4</web_ipify_ipv4>
|
||||
<web_ipify_ipv6 value="web_ipify-ipv6">ipify-ipv6</web_ipify_ipv6>
|
||||
<web_loopia>loopia</web_loopia>
|
||||
<web_myonlineportal>myonlineportal</web_myonlineportal>
|
||||
<web_noip_ipv4 value="web_noip-ipv4">noip-ipv4</web_noip_ipv4>
|
||||
<web_noip_ipv6 value="web_noip-ipv6">noip-ipv6</web_noip_ipv6>
|
||||
<web_nsupdate_info_ipv4 value="web_nsupdate.info-ipv4">nsupdate.info-ipv4</web_nsupdate_info_ipv4>
|
||||
<web_nsupdate_info_ipv6 value="web_nsupdate.info-ipv6">nsupdate.info-ipv6</web_nsupdate_info_ipv6>
|
||||
<web_zoneedit>zoneedit</web_zoneedit>
|
||||
<if>Interface</if>
|
||||
</OptionValues>
|
||||
</checkip>
|
||||
<interface type="InterfaceField">
|
||||
<Required>N</Required>
|
||||
<multiple>N</multiple>
|
||||
<default>wan</default>
|
||||
</interface>
|
||||
</general>
|
||||
<accounts>
|
||||
<account type="ArrayField">
|
||||
@@ -86,6 +54,7 @@
|
||||
<spdyn>spDYN</spdyn>
|
||||
<strato>STRATO</strato>
|
||||
<zoneedit1>Zoneedit</zoneedit1>
|
||||
<ovh>OVH DynHost</ovh>
|
||||
<custom>Custom</custom>
|
||||
</OptionValues>
|
||||
</service>
|
||||
@@ -127,10 +96,33 @@
|
||||
<Required>N</Required>
|
||||
<IpAllowed>N</IpAllowed>
|
||||
</zone>
|
||||
<use_interface type="BooleanField">
|
||||
<default>0</default>
|
||||
<checkip type="OptionField">
|
||||
<Required>Y</Required>
|
||||
</use_interface>
|
||||
<default>web_dyndns</default>
|
||||
<ValidationMessage>An IP service type is required.</ValidationMessage>
|
||||
<OptionValues>
|
||||
<web_dyndns>dyndns</web_dyndns>
|
||||
<web_freedns>freedns</web_freedns>
|
||||
<web_googledomains>googledomains</web_googledomains>
|
||||
<web_he>he</web_he>
|
||||
<web_ip4only_me value="web_ip4only.me">ip4only.me</web_ip4only_me>
|
||||
<web_ip6only_me value="web_ip6only.me">ip6only.me</web_ip6only_me>
|
||||
<web_ipify_ipv4 value="web_ipify-ipv4">ipify-ipv4</web_ipify_ipv4>
|
||||
<web_ipify_ipv6 value="web_ipify-ipv6">ipify-ipv6</web_ipify_ipv6>
|
||||
<web_loopia>loopia</web_loopia>
|
||||
<web_myonlineportal>myonlineportal</web_myonlineportal>
|
||||
<web_noip_ipv4 value="web_noip-ipv4">noip-ipv4</web_noip_ipv4>
|
||||
<web_noip_ipv6 value="web_noip-ipv6">noip-ipv6</web_noip_ipv6>
|
||||
<web_nsupdate_info_ipv4 value="web_nsupdate.info-ipv4">nsupdate.info-ipv4</web_nsupdate_info_ipv4>
|
||||
<web_nsupdate_info_ipv6 value="web_nsupdate.info-ipv6">nsupdate.info-ipv6</web_nsupdate_info_ipv6>
|
||||
<web_zoneedit>zoneedit</web_zoneedit>
|
||||
<if>Interface</if>
|
||||
</OptionValues>
|
||||
</checkip>
|
||||
<force_ssl type="BooleanField">
|
||||
<default>1</default>
|
||||
<Required>Y</Required>
|
||||
</force_ssl>
|
||||
<interface type="InterfaceField">
|
||||
<Required>N</Required>
|
||||
<multiple>N</multiple>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2022 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
namespace OPNsense\DynDNS\Migrations;
|
||||
|
||||
use OPNsense\Base\BaseModelMigration;
|
||||
use OPNsense\Core\Config;
|
||||
|
||||
class M1_2_0 extends BaseModelMigration
|
||||
{
|
||||
/**
|
||||
* Migrate older models into shared model
|
||||
* @param $model
|
||||
*/
|
||||
public function run($model)
|
||||
{
|
||||
$config = Config::getInstance()->object();
|
||||
|
||||
if (empty($config->OPNsense->DynDNS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// migration will move these settings, extract datapoints from raw config
|
||||
$checkip = (string)$config->OPNsense->DynDNS->general->checkip;
|
||||
$interface = $checkip == "if" ? (string)$config->OPNsense->DynDNS->general->interface : "";
|
||||
$force_ssl = (string)$config->OPNsense->DynDNS->general->force_ssl;
|
||||
$pre_account = [];
|
||||
if (!empty($config->OPNsense->DynDNS->accounts->account)) {
|
||||
foreach ($config->OPNsense->DynDNS->accounts->account as $account) {
|
||||
$pre_account[(string)$account->attributes()['uuid']] = [
|
||||
"checkip" => !empty($account->use_interface) ? "if" : $checkip,
|
||||
"interface" => !empty($account->use_interface) ? (string)$account->interface : $interface
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// update accounts
|
||||
foreach ($model->accounts->account->iterateItems() as $account) {
|
||||
$uuid = $account->getAttributes()['uuid'];
|
||||
$account->checkip = $pre_account[$uuid]['checkip'];
|
||||
$account->interface = $pre_account[$uuid]['interface'];
|
||||
$account->force_ssl = $force_ssl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,19 +67,8 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#account\\.use_interface").change(function(){
|
||||
if ($(this).is(':checked')) {
|
||||
$("#account\\.interface").prop( "disabled", false );
|
||||
$("#account\\.interface").closest("tr").show();
|
||||
} else {
|
||||
$("#account\\.interface").closest("tr").hide();
|
||||
$("#account\\.interface").prop( "disabled", true );
|
||||
}
|
||||
$('#account\\.interface').selectpicker('refresh');
|
||||
});
|
||||
$('#DialogAccount').on('shown.bs.modal', function (e) {
|
||||
$("#account\\.service").change();
|
||||
$("#account\\.use_interface").change();
|
||||
});
|
||||
|
||||
$("#ddclient\\.general\\.checkip").change(function(){
|
||||
@@ -113,6 +102,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
|
||||
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
|
||||
<th data-column-id="service" data-type="string">{{ lang._('Service') }}</th>
|
||||
<th data-column-id="hostnames" data-type="string">{{ lang._('Hostnames') }}</th>
|
||||
<th data-column-id="username" data-type="string">{{ lang._('Username') }}</th>
|
||||
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
|
||||
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/local/bin/python3
|
||||
|
||||
"""
|
||||
Copyright (c) 2022 Ad Schellevis <ad@opnsense.org>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
import argparse
|
||||
import subprocess
|
||||
import re
|
||||
import ipaddress
|
||||
|
||||
service_list = {
|
||||
'dyndns': '%s://checkip.dyndns.org/',
|
||||
'freedns': '%s://freedns.afraid.org/dynamic/check.php',
|
||||
'googledomains': '%s://domains.google.com/checkip',
|
||||
'he': '%s://checkip.dns.he.net/',
|
||||
'ip4only.me': '%s://ip4only.me/api/',
|
||||
'ip6only.me': '%s://ip6only.me/api/',
|
||||
'ipify-ipv4': '%s://api.ipify.org/',
|
||||
'ipify-ipv6': '%s://api6.ipify.org/',
|
||||
'loopia': '%s://dns.loopia.se/checkip/checkip.php',
|
||||
'myonlineportal': '%s://myonlineportal.net/checkip',
|
||||
'noip-ipv4': '%s://ip1.dynupdate.no-ip.com/',
|
||||
'noip-ipv6': '%s://ip1.dynupdate6.no-ip.com/',
|
||||
'nsupdate.info-ipv4': '%s://ipv4.nsupdate.info/myip',
|
||||
'nsupdate.info-ipv6': '%s://ipv6.nsupdate.info/myip',
|
||||
'zoneedit': '%s://dynamic.zoneedit.com/checkip.html'
|
||||
}
|
||||
|
||||
|
||||
def extract_address(txt):
|
||||
""" Extract first IPv4 or IPv6 address from provided string
|
||||
:param txt: text blob
|
||||
:return: str
|
||||
"""
|
||||
for regexp in [r'[^a-fA-F0-9\:]', r'[^F0-9\.]']:
|
||||
for line in re.sub(regexp, ' ', txt).split():
|
||||
if line.count('.') == 3 or line.count(':') > 4:
|
||||
try:
|
||||
ipaddress.ip_address(line)
|
||||
return line
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
# handle parameters
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-s', '--service', help='service name', choices=service_list.keys(), required=True)
|
||||
parser.add_argument('-i', '--interface', help='interface', type=str, default='')
|
||||
parser.add_argument('-t', '--tls', help='enforce tls', choices=['0', '1'], default='0')
|
||||
inputargs = parser.parse_args()
|
||||
|
||||
# use curl to fetch data, so we can optionally use "--interface"
|
||||
params = ['/usr/local/bin/curl']
|
||||
if inputargs.interface.strip() != "":
|
||||
params.append("--interface")
|
||||
params.append(inputargs.interface)
|
||||
|
||||
proto = 'http' if inputargs.tls == "0" else 'https'
|
||||
params.append(service_list[inputargs.service] % proto)
|
||||
|
||||
result = subprocess.run(params, capture_output=True, text=True).stdout
|
||||
print (extract_address(result))
|
||||
@@ -2,58 +2,22 @@
|
||||
daemon={{OPNsense.DynDNS.general.daemon_delay|default('300')}}
|
||||
syslog=yes # log update msgs to syslog
|
||||
pid=/var/run/ddclient.pid # record PID in file.
|
||||
{% if not helpers.empty('OPNsense.DynDNS.general.force_ssl') %}
|
||||
ssl=yes
|
||||
{% endif %}
|
||||
{% if not helpers.empty('OPNsense.DynDNS.general.verbose') %}
|
||||
verbose=yes
|
||||
{% endif %}
|
||||
|
||||
#
|
||||
# setup how we expect to retrieve an IP address
|
||||
#
|
||||
{% if not helpers.empty('OPNsense.DynDNS.general.checkip') %}
|
||||
{% set checkip = OPNsense.DynDNS.general.checkip %}
|
||||
{% if checkip == 'if' and OPNsense.DynDNS.general.interface|default('') != '' %}
|
||||
use=if, if={{physical_interface(OPNsense.DynDNS.general.interface)}}
|
||||
{% elif checkip == 'web_dyndns' %}
|
||||
use=web, web=http://checkip.dyndns.org/, web-skip="Current IP Address:"
|
||||
{% elif checkip == 'web_freedns' %}
|
||||
use=web, web=https://freedns.afraid.org/dynamic/check.php
|
||||
{% elif checkip == 'web_googledomains' %}
|
||||
use=web, web=https://domains.google.com/checkip
|
||||
{% elif checkip == 'web_he' %}
|
||||
use=web, web=http://checkip.dns.he.net/
|
||||
{% elif checkip == 'web_ip4only.me' %}
|
||||
use=web, web=http://ip4only.me/api/
|
||||
{% elif checkip == 'web_ip6only.me' %}
|
||||
use=web, web=http://ip6only.me/api/
|
||||
{% elif checkip == 'web_ipify-ipv4' %}
|
||||
use=web, web=https://api.ipify.org/
|
||||
{% elif checkip == 'web_ipify-ipv6' %}
|
||||
use=web, web=https://api6.ipify.org/
|
||||
{% elif checkip == 'web_loopia' %}
|
||||
use=web, web=http://dns.loopia.se/checkip/checkip.php, web-skip="Current IP Address:"
|
||||
{% elif checkip == 'web_myonlineportal' %}
|
||||
use=web, web=https://myonlineportal.net/checkip
|
||||
{% elif checkip == 'web_noip-ipv4' %}
|
||||
use=web, web=http://ip1.dynupdate.no-ip.com/
|
||||
{% elif checkip == 'web_noip-ipv6' %}
|
||||
use=web, web=http://ip1.dynupdate6.no-ip.com/
|
||||
{% elif checkip == 'web_nsupdate.info-ipv4' %}
|
||||
use=web, web=https://ipv4.nsupdate.info/myip
|
||||
{% elif checkip == 'web_nsupdate.info-ipv6' %}
|
||||
use=web, web=https://ipv6.nsupdate.info/myip
|
||||
{% elif checkip == 'web_zoneedit' %}
|
||||
use=web, web=http://dynamic.zoneedit.com/checkip.html
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if helpers.exists('OPNsense.DynDNS.accounts.account') %}
|
||||
{% for account in helpers.toList('OPNsense.DynDNS.accounts.account') %}
|
||||
{% if account.enabled|default('0') == '1' %}
|
||||
{% if account.use_interface|default('0') == '1' %}
|
||||
{% if account.checkip == 'if' %}
|
||||
use=if, if={{physical_interface(account.interface)}}, \
|
||||
{% elif account.checkip.startswith('web_') %}
|
||||
{% if account.interface %}
|
||||
use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -i {{physical_interface(account.interface)}} -t {{account.force_ssl}} -s {{account.checkip[4:]}}",
|
||||
{% else %}
|
||||
use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -t {{account.force_ssl}} -s {{account.checkip[4:]}}",
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if account.service == 'custom' %}
|
||||
protocol={{account.protocol}}, \
|
||||
@@ -96,6 +60,9 @@ server=update.spdyn.de, \
|
||||
{% elif account.service == 'strato' %}
|
||||
protocol=dyndns2, \
|
||||
server=dyndns.strato.com, \
|
||||
{% elif account.service == 'ovh' %}
|
||||
protocol=dyndns2, \
|
||||
server=www.ovh.com, \
|
||||
{% else %}
|
||||
protocol={{account.service}}, \
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user