Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
O
OpnSense
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kulya
OpnSense
Commits
6e850f00
Commit
6e850f00
authored
Sep 28, 2015
by
Ad Schellevis
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
(captiveportal/new) add script base, work in progress
parent
93b9605f
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
489 additions
and
0 deletions
+489
-0
allow.py
src/opnsense/scripts/OPNsense/CaptivePortal/allow.py
+77
-0
__init__.py
src/opnsense/scripts/OPNsense/CaptivePortal/lib/__init__.py
+0
-0
arp.py
src/opnsense/scripts/OPNsense/CaptivePortal/lib/arp.py
+74
-0
db.py
src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py
+121
-0
ipfw.py
src/opnsense/scripts/OPNsense/CaptivePortal/lib/ipfw.py
+85
-0
listARPtable.py
src/opnsense/scripts/OPNsense/CaptivePortal/listARPtable.py
+44
-0
listClients.py
src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py
+63
-0
init.sql
src/opnsense/scripts/OPNsense/CaptivePortal/sql/init.sql
+25
-0
No files found.
src/opnsense/scripts/OPNsense/CaptivePortal/allow.py
0 → 100755
View file @
6e850f00
#!/usr/local/bin/python2.7
"""
Copyright (c) 2015 Deciso B.V. - Ad Schellevis
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.
--------------------------------------------------------------------------------------
allow user/host to captive portal
"""
import
sys
import
ujson
from
lib.db
import
DB
from
lib.arp
import
ARP
from
lib.ipfw
import
IPFW
# parse input parameters
parameters
=
{
'username'
:
''
,
'ip_address'
:
None
,
'zoneid'
:
None
,
'output_type'
:
'plain'
}
current_param
=
None
for
param
in
sys
.
argv
[
1
:]:
if
param
[
0
]
==
'/'
:
current_param
=
param
[
1
:]
.
lower
()
elif
current_param
is
not
None
:
if
current_param
in
parameters
:
parameters
[
current_param
]
=
param
.
strip
()
current_param
=
None
# create new session
if
parameters
[
'ip_address'
]
is
not
None
and
parameters
[
'zoneid'
]
is
not
None
:
cpDB
=
DB
()
cpIPFW
=
IPFW
()
arp_entry
=
ARP
()
.
get_by_ipaddress
(
parameters
[
'ip_address'
])
if
arp_entry
is
not
None
:
mac_address
=
arp_entry
[
'mac'
]
else
:
mac_address
=
None
response
=
cpDB
.
add_client
(
zoneid
=
parameters
[
'zoneid'
],
username
=
parameters
[
'username'
],
ip_address
=
parameters
[
'ip_address'
],
mac_address
=
mac_address
)
# check if address is not already registered before adding it to the ipfw table
if
not
cpIPFW
.
ip_or_net_in_table
(
table_number
=
parameters
[
'zoneid'
],
address
=
parameters
[
'ip_address'
]):
cpIPFW
.
add_to_table
(
table_number
=
parameters
[
'zoneid'
],
address
=
parameters
[
'ip_address'
])
response
[
'state'
]
=
'AUTHORIZED'
else
:
response
=
{
'state'
:
'UNKNOWN'
}
# output result as plain text or json
if
parameters
[
'output_type'
]
!=
'json'
:
for
item
in
response
:
print
'
%20
s
%
s'
%
(
item
,
response
[
item
])
else
:
print
(
ujson
.
dumps
(
response
))
src/opnsense/scripts/OPNsense/CaptivePortal/lib/__init__.py
0 → 100644
View file @
6e850f00
src/opnsense/scripts/OPNsense/CaptivePortal/lib/arp.py
0 → 100644
View file @
6e850f00
"""
Copyright (c) 2015 Ad Schellevis
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
tempfile
import
subprocess
class
ARP
(
object
):
def
__init__
(
self
):
""" construct new arp helper
:return: None
"""
self
.
_arp_table
=
dict
()
self
.
_fetch_arp_table
()
def
_fetch_arp_table
(
self
):
""" parse system arp table and store result in this object
:return: None
"""
# parse arp table
self
.
_arp_table
=
dict
()
with
tempfile
.
NamedTemporaryFile
()
as
output_stream
:
subprocess
.
check_call
([
'/usr/sbin/arp'
,
'-an'
],
stdout
=
output_stream
,
stderr
=
subprocess
.
STDOUT
)
output_stream
.
seek
(
0
)
for
line
in
output_stream
.
read
()
.
split
(
'
\n
'
):
if
line
.
find
(
'('
)
>
-
1
and
line
.
find
(
')'
)
>
-
1
:
address
=
line
.
split
(
')'
)[
0
]
.
split
(
'('
)[
-
1
]
mac
=
line
.
split
(
'at'
)[
-
1
]
.
split
(
'on'
)[
0
]
.
strip
()
physical_intf
=
line
.
split
(
'on'
)[
-
1
]
.
strip
()
.
split
(
' '
)[
0
]
if
address
in
self
.
_arp_table
:
self
.
_arp_table
[
address
][
'intf'
]
.
append
(
physical_intf
)
else
:
self
.
_arp_table
[
address
]
=
{
'mac'
:
mac
,
'intf'
:
[
physical_intf
]}
def
list_items
(
self
):
""" return parsed arp list
:return: dict
"""
return
self
.
_arp_table
def
get_by_ipaddress
(
self
,
address
):
""" search arp entry by ip address
:param address: ip address
:return: dict or None (if not found)
"""
if
address
in
self
.
_arp_table
:
return
self
.
_arp_table
[
address
]
else
:
return
None
src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py
0 → 100644
View file @
6e850f00
"""
Copyright (c) 2015 Ad Schellevis
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
os
import
base64
import
time
import
sqlite3
class
DB
(
object
):
database_filename
=
'/tmp/captiveportal.sqlite'
def
__init__
(
self
):
""" construct new database connection
:return:
"""
self
.
_connection
=
sqlite3
.
connect
(
self
.
database_filename
)
self
.
create
()
def
create
(
self
,
force_recreate
=
False
):
""" create/initialize new database
:param force_recreate: if database already exists, remove old one first
:return: None
"""
if
force_recreate
:
if
os
.
path
.
isfile
(
self
.
database_filename
):
os
.
remove
(
self
.
database_filename
)
self
.
_connection
=
sqlite3
.
connect
(
self
.
database_filename
)
cur
=
self
.
_connection
.
cursor
()
cur
.
execute
(
'SELECT count(*) FROM sqlite_master'
)
if
cur
.
fetchall
()[
0
][
0
]
==
0
:
# empty database, initialize database
init_script_filename
=
'
%
s/../sql/init.sql'
%
os
.
path
.
dirname
(
os
.
path
.
abspath
(
__file__
))
cur
.
executescript
(
open
(
init_script_filename
,
'rb'
)
.
read
())
cur
.
close
()
def
add_client
(
self
,
zoneid
,
username
,
ip_address
,
mac_address
):
""" add a new client to the captive portal administration
:param zoneid: cp zone number
:param username: username, maybe empty
:param ip_address: ip address (to unlock)
:param mac_address: physical address of this ip
:return: dictionary with session info
"""
response
=
dict
()
response
[
'zoneid'
]
=
zoneid
response
[
'username'
]
=
username
response
[
'ip_address'
]
=
ip_address
response
[
'mac_address'
]
=
mac_address
response
[
'created'
]
=
time
.
time
()
# record creation = sign-in time
response
[
'sessionid'
]
=
base64
.
b64encode
(
os
.
urandom
(
16
))
# generate a new random session id
cur
=
self
.
_connection
.
cursor
()
# update cp_clients in case there's already a user logged-in at this ip address.
# places an implicit lock on this client.
cur
.
execute
(
"""update cp_clients
set created = :created
, username = :username
, mac_address = :mac_address
where zoneid = :zoneid
and ip_address = :ip_address
"""
,
response
)
# normal operation, new user at this ip, add to host
if
cur
.
rowcount
==
0
:
cur
.
execute
(
"""insert into cp_clients(zoneid, sessionid, username, ip_address, mac_address, created)
values (:zoneid, :sessionid, :username, :ip_address, :mac_address, :created)
"""
,
response
)
self
.
_connection
.
commit
()
return
response
def
list_clients
(
self
,
zoneid
):
""" return list of (administrative) connected clients
:param zoneid: zone id
:return: list of clients
"""
result
=
list
()
fieldnames
=
list
()
cur
=
self
.
_connection
.
cursor
()
cur
.
execute
(
"select * from cp_clients where zoneid = :zoneid"
,
{
'zoneid'
:
zoneid
})
while
True
:
# fetch field names
if
len
(
fieldnames
)
==
0
:
for
fields
in
cur
.
description
:
fieldnames
.
append
(
fields
[
0
])
row
=
cur
.
fetchone
()
if
row
is
None
:
break
else
:
record
=
dict
()
for
idx
in
range
(
len
(
row
)):
record
[
fieldnames
[
idx
]]
=
row
[
idx
]
result
.
append
(
record
)
return
result
src/opnsense/scripts/OPNsense/CaptivePortal/lib/ipfw.py
0 → 100644
View file @
6e850f00
"""
Copyright (c) 2015 Ad Schellevis
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
os
import
tempfile
import
subprocess
class
IPFW
(
object
):
def
__init__
(
self
):
pass
def
list_table
(
self
,
table_number
):
""" list ipfw table
:param table_number: ipfw table number
:return: list
"""
DEVNULL
=
open
(
os
.
devnull
,
'w'
)
result
=
list
()
with
tempfile
.
NamedTemporaryFile
()
as
output_stream
:
subprocess
.
check_call
([
'/sbin/ipfw'
,
'table'
,
table_number
,
'list'
],
stdout
=
output_stream
,
stderr
=
DEVNULL
)
output_stream
.
seek
(
0
)
for
line
in
output_stream
.
read
()
.
split
(
'
\n
'
):
result
.
append
(
line
.
split
(
' '
)[
0
])
return
result
def
ip_or_net_in_table
(
self
,
table_number
,
address
):
""" check if address or net is in this zone's table
:param table_number: ipfw table number to query
:param address: ip address or net
:return: boolean
"""
ipfw_tbl
=
self
.
list_table
(
table_number
)
if
address
.
find
(
'.'
)
>
-
1
and
address
.
find
(
'/'
)
==
-
1
:
# address given, search for /32 net in ipfw rules
if
'
%
s/32'
%
address
.
strip
()
in
ipfw_tbl
:
return
True
elif
address
.
strip
()
in
ipfw_tbl
:
return
True
return
False
def
add_to_table
(
self
,
table_number
,
address
):
""" add new entry to ipfw table
:param table_number: ipfw table number
:param address: ip address or net to add to table
:return:
"""
DEVNULL
=
open
(
os
.
devnull
,
'w'
)
subprocess
.
call
([
'/sbin/ipfw'
,
'table'
,
table_number
,
'add'
,
address
],
stdout
=
DEVNULL
,
stderr
=
DEVNULL
)
def
delete_from_table
(
self
,
table_number
,
address
):
""" remove entry from ipfw table
:param table_number: ipfw table number
:param address: ip address or net to add to table
:return:
"""
DEVNULL
=
open
(
os
.
devnull
,
'w'
)
subprocess
.
call
([
'/sbin/ipfw'
,
'table'
,
table_number
,
'delete'
,
address
],
stdout
=
DEVNULL
,
stderr
=
DEVNULL
)
src/opnsense/scripts/OPNsense/CaptivePortal/listARPtable.py
0 → 100644
View file @
6e850f00
#!/usr/local/bin/python2.7
"""
Copyright (c) 2015 Deciso B.V. - Ad Schellevis
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.
--------------------------------------------------------------------------------------
return a list of all available rrd files including additional definition data
"""
from
lib.arp
import
ARP
import
sys
import
ujson
arp_list
=
ARP
()
.
list_items
()
if
len
(
sys
.
argv
)
>
1
and
sys
.
argv
[
1
]
.
trim
()
.
lower
()
==
'json'
:
# dump as json
print
(
ujson
.
dumps
(
arp_list
))
else
:
print
(
'------------------------- ARP table content -------------------------'
)
for
address
in
arp_list
:
print
(
'[
%10
s]
%-20
s
%-20
s'
%
(
arp_list
[
address
][
'intf'
],
address
,
arp_list
[
address
][
'mac'
]))
src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py
0 → 100755
View file @
6e850f00
#!/usr/local/bin/python2.7
"""
Copyright (c) 2015 Deciso B.V. - Ad Schellevis
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.
--------------------------------------------------------------------------------------
list connected clients for a captive portal zone
"""
import
sys
import
ujson
from
lib.db
import
DB
# parse input parameters
parameters
=
{
'zoneid'
:
None
,
'output_type'
:
'plain'
}
current_param
=
None
for
param
in
sys
.
argv
[
1
:]:
if
param
[
0
]
==
'/'
:
current_param
=
param
[
1
:]
.
lower
()
elif
current_param
is
not
None
:
if
current_param
in
parameters
:
parameters
[
current_param
]
=
param
.
strip
()
current_param
=
None
if
parameters
[
'zoneid'
]
is
not
None
:
cpDB
=
DB
()
response
=
cpDB
.
list_clients
(
parameters
[
'zoneid'
])
else
:
response
=
[]
# output result as plain text or json
if
parameters
[
'output_type'
]
!=
'json'
:
heading
=
{
'sessionid'
:
'sessionid'
,
'username'
:
'username'
,
'ip_address'
:
'ip_address'
,
'mac_address'
:
'mac_address'
}
print
'
%(sessionid)-30
s
%(username)-20
s
%(ip_address)-20
s
%(mac_address)-20
s'
%
heading
for
item
in
response
:
print
'
%(sessionid)-30
s
%(username)-20
s
%(ip_address)-20
s
%(mac_address)-20
s'
%
item
else
:
print
(
ujson
.
dumps
(
response
))
src/opnsense/scripts/OPNsense/CaptivePortal/sql/init.sql
0 → 100644
View file @
6e850f00
--
-- create new Captive Portal database
--
-- connected clients
create
table
cp_clients
(
zoneid
int
,
sessionid
varchar
,
username
varchar
,
ip_address
varchar
,
mac_address
varchar
,
created
number
,
primary
key
(
zoneid
,
sessionid
)
);
create
index
cp_clients_ip
ON
cp_clients
(
ip_address
);
create
index
cp_clients_zone
ON
cp_clients
(
zoneid
);
-- session (accounting) info
create
table
session_info
(
zoneid
int
,
sessionid
varchar
,
primary
key
(
zoneid
,
sessionid
)
);
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment