Commit f0551177 authored by rgaudin's avatar rgaudin
Browse files

Fixed #193 and #200: Changed Captive Portal behavior

- Captive portal has been slightly redisgned as a low-end Onboarding
- Connectivity is checked
- DNS is spoofed if we have no connectivity otherwise not
- Default nginx server (not ideascube hostname) is the portal
- When spoofing, wrong URIs go to default nginx
- When not spoofing, iptables redirects to default nginx (IP)
- Idle for capture is set to 15mn (was 5mn)
- Supports different modes (Offline, Gateway, Extended, Resource -- see #193)
- New master integrating those changes
parent 6eed28c8
......@@ -5,6 +5,9 @@
* Better SD-card writer (faster, error-handling, cancellable)
* Image is considered successful even if SD-writing failed
* Etcher (external SD writer) menu entry advertised in case of SD-writing failure
* Revamped Capture Portal behavior
* Supports Internet Gateway mode
* Supports Extended WiFi mode
2.0-rc5
* Domain tld changed to .hotstop (was .lan)
......
......@@ -4,3 +4,4 @@
name: "{{ service }}"
enabled: no
state: stopped
tags: always
......@@ -4,3 +4,4 @@
name: "{{ service }}"
enabled: yes
state: restarted
tags: always
......@@ -71,7 +71,6 @@ sites_fqdn: sites.{{ fqdn }}
time_fqdn: time.{{ fqdn }}
# content & features
captive_portal: true
has_custom_branding: false
custom_branding_path: /tmp
kalite_languages: []
......
......@@ -40,7 +40,6 @@
# master, rename
- role: captive_portal
when: ansible_local.config.captive_portal|bool
# content setup (install, dependencies)
- role: wikifundi_setup
......
......@@ -6,14 +6,11 @@ passlist=$(iptables -t nat -nL CAPTIVE_PASSLIST | grep ACCEPT | awk '{print $4}'
for ip in $passlist
do
## Count ESTABLISHED connections from/to $ip
is_connected=`conntrack -L | grep $ip | grep ESTABLISHED | wc -l`
## If no connections then remove iptables rule to bypass redirection
if [ $is_connected -eq 0 ]
then
/sbin/iptables -t nat -D CAPTIVE_PASSLIST -s $ip -j ACCEPT
/sbin/iptables -t nat -D CAPTIVE_PASSLIST -s $ip -j ACCEPT
fi
done
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
def is_connected(hostname="www.google.com"):
try:
socket.create_connection((socket.gethostbyname(hostname), 80), timeout=2)
return True
except Exception:
pass
return False
if __name__ == "__main__":
print("yes" if is_connected() else "no")
# the upstream component nginx needs to connect to
server {
## Captive portal listening on port 82 for HTTP and 83 for HTTPS
listen 82;
listen [::]:82;
listen 83 ssl;
## SSL certificate only used to allow forwarding of HTTPS requests.
# Captive portal responding all non-specified names
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
# SSL certificate only used to allow forwarding of HTTPS requests.
ssl_certificate /etc/nginx/ssl/ideascube.crt;
ssl_certificate_key /etc/nginx/ssl/ideascube.key;
keepalive_timeout 0;
......@@ -19,4 +16,8 @@ server {
include uwsgi_params;
uwsgi_pass 127.0.0.1:3031;
}
location /ideascube-static/ { alias /var/ideascube/static/; expires 1y; }
location /502.html { internal; root /var/www; }
error_page 502 /502.html;
}
#!/bin/bash
touch /tmp/has_internet
previous=`cat /tmp/has_internet`
current=$(/usr/local/bin/connectivity-check)
echo "dnsmasq dispatcher connectivity check: $current"
echo "$current" > /tmp/has_internet
if [ "$current" != "$previous" ];
then
if [ $current = "no" ]; then spoof="-spoof" ; else spoof="" ; fi
sed -i "s/^DNSMASQ_OPTS=.*/DNSMASQ_OPTS=\"--conf-file=\/etc\/dnsmasq${spoof}.conf --local-ttl=300\"/g" /etc/default/dnsmasq;
systemctl restart dnsmasq.service
fi
......@@ -10,6 +10,15 @@
- uwsgi-plugin-python
tags: master
- name: Copy connectivity check script
copy:
src: connectivity-check
dest: /usr/local/bin/connectivity-check
owner: root
group: root
mode: 0755
tags: master
- name: Create /var/www/captiveportal folder
file:
path: /var/www/captiveportal
......@@ -58,7 +67,7 @@
lineinfile:
dest: /etc/default/dnsmasq
regexp: 'DNSMASQ_OPTS='
line: 'DNSMASQ_OPTS="--conf-file=/etc/dnsmasq-spoof.conf --local-ttl=600"'
line: 'DNSMASQ_OPTS="--conf-file=/etc/dnsmasq-spoof.conf --local-ttl=300"'
state: present
tags: master
......@@ -83,12 +92,6 @@
command: openssl x509 -req -sha256 -days 3650 -in /etc/nginx/ssl/ideascube.csr -signkey /etc/nginx/ssl/ideascube.key -out /etc/nginx/ssl/ideascube.crt
tags: ['master', 'rename']
- name: Copy new catch-all redirect 301 vhost
template:
src: redirect.j2
dest: /etc/nginx/sites-enabled/redirect
tags: ['master', 'rename']
- name: Add UWSGI specific configuration file
copy:
src: captive.ini
......@@ -127,17 +130,32 @@
mode: 0755
tags: master
- name: Copy dnsmasq script for NetworkManager
template:
src: dnsmasq.j2
dest: /etc/NetworkManager/dispatcher.d/03dnsmasq
- name: Copy pibox-mode-switcher script for NetworkManager
copy:
src: pibox-mode-switcher
dest: /usr/local/bin/pibox-mode-switcher
mode: 0755
tags: master
- name: Link mode-switcher script to NetworkManager
file:
src: /usr/local/bin/pibox-mode-switcher
dest: /etc/NetworkManager/dispatcher.d/03dnsmasq
state: link
notify: restart network manager
tags: ['master', 'rename']
tags: master
- name: Copy clear_iptables.sh to /usr/bin/local
- name: Add a cron entry to check connectivity
cron:
name: "Check connectivity status"
minute: "*/5"
job: "/usr/local/bin/pibox-mode-switcher"
state: present
tags: master
- name: Copy clean_iptables.sh to /usr/local/bin
copy:
src: clear_iptables.sh
src: clean_iptables.sh
dest: /usr/local/bin/clean_iptables.sh
owner: root
group: root
......@@ -147,7 +165,7 @@
- name: Add a cron entry to clean iptables rules
cron:
name: "Clear dead connections from CAPTIVE_PASSLIST"
minute: "*/5"
minute: "*/15"
job: "/usr/local/bin/clean_iptables.sh"
state: present
tags: master
......@@ -189,24 +207,24 @@
jump: CAPTIVE_PASSLIST
tags: master
- name: DNAT HTTP to hotspot_ip:82
- name: DNAT HTTP to hotspot_ip:80
iptables:
table: nat
chain: CAPTIVE_HTTP
protocol: tcp
jump: DNAT
to_destination: "{{ hotspot_ip }}:82"
comment: Redirect HTTP traffic to server port 82
to_destination: "{{ hotspot_ip }}:80"
comment: Redirect HTTP traffic to server port 80
tags: master
- name: DNAT HTTPS to hotspot_ip:83
- name: DNAT HTTPS to hotspot_ip:443
iptables:
table: nat
chain: CAPTIVE_HTTPS
protocol: tcp
jump: DNAT
to_destination: "{{ hotspot_ip }}:83"
comment: Redirect HTTPS traffic to server port 83
to_destination: "{{ hotspot_ip }}:443"
comment: Redirect HTTPS traffic to server port 443
tags: master
- name: Last CAPTIVE_PASSLIST rules RETURN to calling chain
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
import subprocess
def has_internet():
try:
with open("/tmp/has_internet", "r") as f:
return f.read().strip() == "yes"
except Exception:
return False
TRANSLATIONS = {
'fr': {
'button': u"Ok",
'message': u"Vous allez maintenant être redirigé vers {{ fqdn }}",
"fr": {
"banner": u"Bienvenue sur «<em>{{ hotspot_name }}</em>» "
u"(<a href=\"http://{{ fqdn }}/\">{{ fqdn }}</a>)",
"title": u"<em>{{ project_name }}</em> est une ressource de "
u"contenus libres d'accès.",
"reminder": u"Merci de vous rappeler des points suivants :",
"access": u"L'accès à ces contenus ne se fait "
u"<strong>qu'à travers <em>http://{{ fqdn }}/</em></strong>. "
u"<u>Souvenez-vous en !</u>",
"internet": u"Cette ressource peut fournir un accès à Internet. "
u"Dans ce cas, il faudra vous <em>identifier</em> pour y accèder.",
"prompt": u"Cette page vous sera alors présenté <strong>après 15mn</strong> "
u"d'inactivité.",
"https": u"Surfant sur un site sécurisé, le navigateur pourrait vous afficher "
u"un <em>avertissement de sécurité</em> au lieu d'afficher cette page. "
u"Ouvrez-en simplement une nouvelle "
u"sur un site non-sécurisé et identifiez-vous à nouveau.",
"go_button": u"Aller à {{ fqdn }}",
"internet_button": u"M'identifier et accèder à Internet",
},
'en': {
'button': u"Ok",
'message': u"You will now be redirected to {{ fqdn }}",
"en": {
"banner": u"Entering “<em>{{ hotspot_name }}</em>” "
u"(<a href=\"http://{{ fqdn }}/\">{{ fqdn }}</a>)",
"title": u"<em>{{ project_name }}</em> is a free-to-use, content-enabled box.",
"reminder": u"Please, keep the following in mind:",
"access": u"Access to this box's content is done "
u"<strong>solely via <em>http://{{ fqdn }}/</em></strong>. "
u"<u>Remember-it !</u>",
"internet": u"This box may also provide access to Internet. "
u"You need to <em>log-in</em> to the network to access it.",
"prompt": u"You will then be prompted with this page <strong>after "
u"every 15mn</strong> of inactivity.",
"https": u"When browsing a secure website, your browser might display "
u"a <em>Security Warning</em> instead of displaying this page. "
u"Just open a new page onto a non-secure website to log-in again.",
"go_button": u"Go to {{ fqdn }}",
"internet_button": u"Log me in and access Internet",
},
}
header = u"""
header = u"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { width: 100%; height: 100%; position: relative; }
.myButton {
border: none;
border-radius: 2px;
padding: 0 10px;
text-decoration: none;
color: #fff;
font-size: 1.1em;
background-color: #6C7A89;
display: inline-block;
margin-top: 10px;
height: 50px;
line-height: 50px;
text-align: center;
cursor: pointer;
}
.myButton:hover {
background-color: #95A5A6;
color: #fff;
}
.myButton:active {
position:relative;
top:1px;
}
.center {
position: absolute; top: calc(50% - 60px);left: calc(50% - 260px);
width: 500px; height: 100px; border: 0px; padding: 10px;
text-align:center;
}
</style>
</head>"""
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{ hotspot_name }}</title>
<link rel="icon" href="/ideascube-static/branding/favicon.png">
<link rel="stylesheet" href="/ideascube-static/ideascube/vendor/font-awesome/css/font-awesome.min.css" />
<!--[if lte IE 8]>
<link rel="stylesheet" type="text/css" href="/ideascube-static/ideascube/vendor/purecss/pure-css-grids-old-ie-min.css">
<![endif]-->
<!--[if gt IE 8]><!-->
<link rel="stylesheet" type="text/css" href="/ideascube-static/ideascube/vendor/purecss/pure-css-grids-min.css">
<!--<![endif]-->
<link rel="stylesheet" type="text/css" href="/ideascube-static/ideascube/main.css" />
<link rel="stylesheet" type="text/css" href="/ideascube-static/branding/style.css" />
</head>"""
body = u"""
<body>
<div class="center">
<h3>{message}</h3>
<a href="/letmein.html" class="myButton">{button}</a>
</div>
</body>
"""
parta = u"""
<body class="">
<header>
<section>
<a href="/">
<h1>
<img src="/ideascube-static/branding/header-logo.png"></img>
</h1>
</a>
<span class="boxid">{{ project_name }}!</span>
</section>
</header>
<ul class="messages">
<li class="success">{banner}</li>
</ul>
<div class="content">
<h2>{title}</h2>
<p style="text-decoration: underline;">{reminder}</p>
<ul><li>{access}</li>"""
footer = u"</html>"
partb = u"""<li>{internet}</li><li>{prompt}</li>"""
partc = u"""<li>{https}</li></ul>
<p><a class="button" href="http://{{ fqdn }}">{go_button}</a>"""
def get_translation_for(accept_lang_header, default='en'):
partd = u""" <a class="button neutral" href="/register">{internet_button}</a>"""
parte = u"""</p></div>"""
footer = u"""
<footer>
<div class="row">
<div class="col third i18n_switch"></div>
<div class="col half">
<img src="/ideascube-static/ideascube/img/logoBSF.png" />
</div>
</div>
</footer>
</body>
</html>"""
def get_translation_for(accept_lang_header, default="en"):
chosen_lang = default
langs = [lang_item.replace('_', '-').split('-')[0].lower()
langs = [lang_item.replace("_", "-").split("-")[0].lower()
for lang_code in accept_lang_header.split(",")
for lang_item in lang_code.split(";")
if 'q=' not in lang_item]
if "q=" not in lang_item]
for lang in langs:
if lang in TRANSLATIONS.keys():
......@@ -79,18 +129,29 @@ def get_translation_for(accept_lang_header, default='en'):
def allow_host(ipaddress):
passlist_output = subprocess.check_output(
"sudo iptables -t nat -nL CAPTIVE_PASSLIST", shell=True)
passlist = [re.split(r"\s+", line)[3]
for line in passlist_output.split("\n") if "--" in line]
if ipaddress in passlist:
return
subprocess.check_output(
"sudo iptables -t nat -I CAPTIVE_PASSLIST 1 -s {ip} -j ACCEPT"
.format(ip=ipaddress), shell=True)
def application(env, start_response):
if env['REQUEST_URI'] == "/letmein.html":
allow_host(env['REMOTE_ADDR'])
start_response('302 Found', [('Location', "http://{{ fqdn }}")])
return ['1']
start_response('200 OK', [('Content-Type', 'text/html')])
translation = get_translation_for(env.get('HTTP_ACCEPT_LANGUAGE'))
page = header + body.format(**translation) + footer
return [page.encode('utf-8')]
if env["REQUEST_URI"] == "/register":
allow_host(env["REMOTE_ADDR"])
start_response("302 Found", [("Location", "http://wikipedia.org")])
return ["1"]
start_response("200 OK", [("Content-Type", "text/html")])
translation = get_translation_for(env.get("HTTP_ACCEPT_LANGUAGE"))
if has_internet():
parts = [parta, partb, partc, partd, parte]
else:
parts = [parta, partc, parte]
page = header + "\n".join([part.format(**translation) for part in parts]) + footer
return [page.encode("utf-8")]
#!/bin/bash
if [ $1 == "{{ lan_interface }}" ] || [ $1 == "{{ lan_interface2 }}" ] || [ $1 == "{{ client_interface }}" ]
then
case "$2" in
## set dnsmasq.conf as default configuration
up) sed -i 's/^DNSMASQ_OPTS=.*/DNSMASQ_OPTS="--conf-file=\/etc\/dnsmasq.conf"/g' /etc/default/dnsmasq;;
## set dnsmasq-spoof.conf as default configuration
down) sed -i 's/^DNSMASQ_OPTS=.*/DNSMASQ_OPTS="--conf-file=\/etc\/dnsmasq-spoof.conf --local-ttl=600"/g' /etc/default/dnsmasq;;
*) exit 0;;
esac
systemctl restart dnsmasq.service
fi
server {
listen 80 default_server;
listen 443 ssl default_server;
ssl_certificate /etc/nginx/ssl/ideascube.crt;
ssl_certificate_key /etc/nginx/ssl/ideascube.key;
keepalive_timeout 0;
server_name _;
rewrite ^ http://{{ fqdn }};
}
interface={{ hotspot_interface }}
except-interface={{ lan_interface }}
interface={{ lan_interface }}
except-interface={{ lan_interface2 }}
except-interface={{ client_interface }}
dhcp-range=192.168.2.100,192.168.2.240,255.255.255.0,12h
no-dhcp-interface={{ lan_interface }}
expand-hosts
bogus-priv
domain={{ fqdn }},192.168.2.0/24,local
address=/{{ fqdn }}/{{ hotspot_ip }}
address=/{{ hostname }}/{{ hotspot_ip }}
address=/#/{{ hotspot_ip }}
no-hosts
interface={{ hotspot_interface }}
except-interface={{ lan_interface }}
interface={{ lan_interface }}
except-interface={{ lan_interface2 }}
except-interface={{ client_interface }}
dhcp-range=192.168.2.100,192.168.2.240,255.255.255.0,12h
no-dhcp-interface={{ lan_interface }}
expand-hosts
bogus-priv
......
......@@ -6,3 +6,7 @@
autoremove: yes
autoclean: yes
tags: master
- name: Setup eth0 connection
command: nmcli connection add ifname eth0 type ethernet autoconnect yes save yes
tags: master
......@@ -69,6 +69,9 @@
- include_tasks: disable_service.yml service=memcached
tags: setup
- include_tasks: disable_service.yml service=redis-server
tags: setup
- import_tasks: clean_apt.yml
tags: setup
......
......@@ -8,11 +8,11 @@
"expanded_size": 1862270976
},
"pibox_base_image": {
"url": "{mirror}/plug/base/plug-master_2018-08-31.img.zip",
"name": "plug-master_2018-08-31.img.zip",
"url": "{mirror}/plug/base/plug-master_2018-09-05.img.zip",
"name": "plug-master_2018-09-05.img.zip",
"root_partition_size": 7000000000,
"checksum": "ad6bb658e351b888fd932a81fa8802401cc65caab73a2517a40c83f0abd24ce6",
"archive_size": 2590744278,
"checksum": "e07f1fa3f7636ede35a24cbb851edae011f8fa3ce0303b72a26652c689fb2959",
"archive_size": 2891982051,
"expanded_size": 8000000000
},
"aflatoun_content": {
......
......@@ -121,7 +121,6 @@ def build_extra_vars(name, timezone, language, language_name, wifi_pwd,
'edupi': edupi,
'edupi_has_resources': bool(edupi_resources),
'packages': packages,
'captive_portal': True,
'custom_branding_path': '/tmp',
'admin_account': "admin",
'admin_password': "admin-password",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment