diff --git a/roles/1-prep/templates/.iiab.env.j2.un~ b/roles/1-prep/templates/.iiab.env.j2.un~ new file mode 100644 index 000000000..870f3493e Binary files /dev/null and b/roles/1-prep/templates/.iiab.env.j2.un~ differ diff --git a/roles/1-prep/templates/iiab.env.j2 b/roles/1-prep/templates/iiab.env.j2 index 49458cd9b..93176035b 100644 --- a/roles/1-prep/templates/iiab.env.j2 +++ b/roles/1-prep/templates/iiab.env.j2 @@ -4,7 +4,10 @@ IIAB_BASE_PATH={{ iiab_base }} IIAB_DIR={{ iiab_dir }} IIAB_RELEASE={{ iiab_base_ver }} IIAB_REVISION={{ iiab_revision }} +IIAB_GATEWAY_ENABLED={{ iiab_gateway_enabled }} +LAN_IP={{ lan_ip }} OS={{ ansible_local.local_facts.os }} OS_VER={{ ansible_local.local_facts.os_ver }} WWWROOT={{ doc_root }} STAGE=1 +FQDN={{ iiab_fqdn }} diff --git a/roles/httpd/templates/010-iiab.conf.j2 b/roles/httpd/templates/010-iiab.conf.j2 index 5b83bb5f5..c56977490 100755 --- a/roles/httpd/templates/010-iiab.conf.j2 +++ b/roles/httpd/templates/010-iiab.conf.j2 @@ -159,8 +159,6 @@ UseCanonicalName Off # so it doesn't move in some future version. # DocumentRoot "{{ doc_root }}" -ErrorLog {{ apache_log_dir }}/error.log -CustomLog {{ apache_log_dir }}/access.log combined # # Each directory to which Apache has access can be configured with respect @@ -175,12 +173,17 @@ CustomLog {{ apache_log_dir }}/access.log combined AllowOverride None - - Options Indexes FollowSymLinks - AllowOverride None - Require all granted - - + + ErrorLog /var/log/apache2/error.log + CustomLog /var/log/apache2/access.log combined + ServerName {{ iiab_hostname }} + ServerAlias iiab-server.lan + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + # # UserDir: The name of the directory that is appended onto a user's home # directory if a ~user request is received. diff --git a/roles/network/files/mac.template b/roles/network/files/mac.template new file mode 100644 index 000000000..c430d1958 --- /dev/null +++ b/roles/network/files/mac.template @@ -0,0 +1,54 @@ + + + + + Success + + + + Success +
+
+ +

Welcome to IIAB

+

+ {{ message }} +
+

+

+

+

+ {{ btn1 }} +
+ + diff --git a/roles/network/files/simple.template b/roles/network/files/simple.template new file mode 100644 index 000000000..ec140a058 --- /dev/null +++ b/roles/network/files/simple.template @@ -0,0 +1,100 @@ + + + + Success + + + + + + + + {% if success_token is defined %} + Success + {% endif %} +
+
+ +

Welcome to IIAB

+

+ {{ message }} +
+ {% if btn2 is defined %} + {{ btn2 }} + {% endif %} +

+

+

+

+ + {{ btn1 }} +
+ + diff --git a/roles/network/tasks/captive_portal.yml b/roles/network/tasks/captive_portal.yml index 033314121..1474a7f0d 100644 --- a/roles/network/tasks/captive_portal.yml +++ b/roles/network/tasks/captive_portal.yml @@ -1,47 +1,107 @@ -- name: Create directory for Captive Portal script +- name: Download & install python-dateutil, sqlite3 + package: + name: "{{ item }}" + state: present + with_items: + - python-dateutil + - sqlite3 # @georgehunt hopes to move this to 2-common (or more like stage 3-base-server, alongside MySQL) in October 2018 + +- name: Create directory /opt/iiab/captive-portal for scripts & templates file: path: /opt/iiab/captive-portal state: directory - when: py_captive_portal_install -- name: Copy Captive Portal script +- name: 'Copy scripts: checkurls, capture-wsgi.py' template: - src: roles/network/templates/captive_portal/captive_portal.py.j2 - dest: /opt/iiab/captive-portal/captive_portal.py + src: "{{ item.src }}" + dest: /opt/iiab/captive-portal/ + mode: "{{ item.mode }}" + with_items: + - { src: roles/network/templates/captive-portal/checkurls, mode: '0644' } + - { src: roles/network/templates/captive-portal/capture-wsgi.py, mode: '0755' } + +- name: 'Copy templates: simple.template, mac.template' + copy: + src: "{{ item }}" + dest: /opt/iiab/captive-portal/ + with_items: + - roles/network/files/simple.template + - roles/network/files/mac.template + +- name: Copy iiab-catch & iiab-uncatch into /usr/bin/ + template: + src: "{{ item }}" + dest: /usr/bin/ owner: root group: root - mode: 0740 - when: py_captive_portal_install + mode: 0755 + with_items: + - roles/network/templates/captive-portal/iiab-catch + - roles/network/templates/captive-portal/iiab-uncatch -- name: Copy Captive Portal service file +- name: Run iiab-uncatch to generate diversion lists for dnsmasq and apache2 + shell: /usr/bin/iiab-uncatch + +- name: Install systemd unit file py-captive-portal.service from template template: - src: roles/network/templates/captive_portal/captive_portal.service.j2 - dest: /etc/systemd/system/captive_portal.service + src: roles/network/templates/captive-portal/py-captive-portal.service.j2 + dest: /etc/systemd/system/py-captive-portal.service owner: root group: root mode: 0644 - when: py_captive_portal_install -- name: Enable captive_portal after copying files - service: - name: captive_portal.service +- name: Install Apache's captive-portal.conf from template if py_captive_portal_enabled + template: + src: roles/network/templates/captive-portal/captive-portal.conf + dest: /etc/{{ apache_config_dir }}/captive-portal.conf + owner: root + group: root + mode: 0740 + when: py_captive_portal_enabled + +- name: Enable Apache's captive-portal.conf if py_captive_portal_enabled (debuntu) + file: + src: /etc/apache2/sites-available/captive-portal.conf + path: /etc/apache2/sites-enabled/captive-portal.conf + state: link + when: py_captive_portal_enabled and is_debuntu + +- name: Enable Apache's default-ssl.conf if py_captive_portal_enabled (debuntu) + file: + src: /etc/apache2/sites-available/default-ssl.conf + path: /etc/apache2/sites-enabled/default-ssl.conf + state: link + when: py_captive_portal_enabled and is_debuntu + +- name: Enable & Start systemd service py-captive-portal.service if py_captive_portal_enabled + systemd: + name: py-captive-portal.service + daemon-reload: yes enabled: yes - when: py_captive_portal_install and py_captive_portal_enabled - -- name: Start captive_portal after copying files - service: - name: captive_portal.service state: started - when: py_captive_portal_install and py_captive_portal_enabled + when: py_captive_portal_enabled -- name: Disable captive_portal after copying files - service: - name: captive_portal.service +- name: Disable & Stop py-captive-portal.service if not py_captive_portal_enabled + systemd: + name: py-captive-portal.service enabled: no - when: py_captive_portal_install and py_captive_portal_enabled + state: stopped + when: not py_captive_portal_enabled -- name: Stop captive_portal after copying files - service: - name: captive_portal.service - state: started - when: py_captive_portal_install and py_captive_portal_enabled +- name: Disable Apache's captive-portal.conf if not py_captive_portal_enabled (debuntu) + file: + path: /etc/apache2/sites-enabled/captive-portal.conf + state: absent + when: not py_captive_portal_enabled and is_debuntu + +- name: Disable Apache's default-ssl.conf if not py_captive_portal_enabled (debuntu) + file: + path: /etc/apache2/sites-enabled/default-ssl.conf + state: absent + when: not py_captive_portal_enabled and is_debuntu + +- name: Make sure dnsmasq is not diverting if not py_captive_portal_enabled + file: + path: /etc/dnsmasq.d/capture + state: absent + when: not py_captive_portal_enabled diff --git a/roles/network/tasks/hosts.yml b/roles/network/tasks/hosts.yml index 0b3d55673..f0ad8f456 100644 --- a/roles/network/tasks/hosts.yml +++ b/roles/network/tasks/hosts.yml @@ -1,24 +1,24 @@ #TODO: Use vars instead of hardcoded values - name: Remove FQDN with 172.18.96.1 in /etc/hosts without LAN (if iiab_lan_iface == "none" and not installing) lineinfile: - dest: /etc/hosts + path: /etc/hosts regexp: '^172\.18\.96\.1' state: absent when: iiab_lan_iface == "none" and not installing - name: Configure FQDN with 172.18.96.1 in /etc/hosts with LAN (if iiab_lan_iface != "none" and not installing) lineinfile: - dest: /etc/hosts + path: /etc/hosts regexp: '^172\.18\.96\.1' - line: '172.18.96.1 {{ iiab_hostname }}.{{ iiab_domain }} {{ iiab_hostname }} box' + line: '172.18.96.1 {{ iiab_hostname }}.{{ iiab_domain }} {{ iiab_hostname }} box' state: present when: iiab_lan_iface != "none" and not installing - name: Configure FQDN with 127.0.0.1 in /etc/hosts appliance mode (if iiab_lan_iface == "none" and not installing) lineinfile: - dest: /etc/hosts + path: /etc/hosts regexp: '^127\.0\.0\.1' - line: '127.0.0.1 localhost.localdomain localhost {{ iiab_hostname }}.{{ iiab_domain }} {{ iiab_hostname }} box ' + line: '127.0.0.1 localhost.localdomain localhost {{ iiab_hostname }}.{{ iiab_domain }} {{ iiab_hostname }} box' owner: root group: root mode: 0644 diff --git a/roles/network/templates/captive-portal/captive-portal.conf b/roles/network/templates/captive-portal/captive-portal.conf new file mode 100644 index 000000000..6c5a8a012 --- /dev/null +++ b/roles/network/templates/captive-portal/captive-portal.conf @@ -0,0 +1,14 @@ + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName iiab.io + Include /etc/apache2/capture + ProxyPreserveHost On + ProxyPass / http://box.lan:{{ py_captive_portal_port }}/ + ProxyPassReverse / http://box.lan:{{ py_captive_portal_port }}/ + diff --git a/roles/network/templates/captive_portal/captive_portal.py.j2 b/roles/network/templates/captive-portal/captive-portal.py.j2 similarity index 100% rename from roles/network/templates/captive_portal/captive_portal.py.j2 rename to roles/network/templates/captive-portal/captive-portal.py.j2 diff --git a/roles/network/templates/captive-portal/capture-wsgi.py b/roles/network/templates/captive-portal/capture-wsgi.py new file mode 100755 index 000000000..e7e43d0fb --- /dev/null +++ b/roles/network/templates/captive-portal/capture-wsgi.py @@ -0,0 +1,550 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# using Python's bundled WSGI server + +from wsgiref.simple_server import make_server +import subprocess +from dateutil.tz import * +import datetime +import logging +from logging.handlers import RotatingFileHandler +import os +import sys +from jinja2 import Environment, FileSystemLoader +import sqlite3 +import re + +# Notes on timeout strategy +# every client timestamp is recorded into current_ts +# When splash page is clicked , return 204 timeout starts (via ajax call), +# Return 204 is android (may be different for different versions) +# captive portal redirect is triggered after inactivity timeout, +# which needs to be longer than period of normal connecetivity checks by OS +# + +# Create the jinja2 environment. +CAPTIVE_PORTAL_BASE = "/opt/iiab/captive-portal" +j2_env = Environment(loader=FileSystemLoader(CAPTIVE_PORTAL_BASE),trim_blocks=True) + +# Define time outs +INACTIVITY_TO = 30 +PORTAL_TO = 0 # delay after triggered by ajax upon click of link to home page +# I had hoped that returning 204 status after some delay +# would dispense with android's "sign-in to network" (no work) + + +# Get the IIAB variables +sys.path.append('/etc/iiab/') +from iiab_env import get_iiab_env +doc_root = get_iiab_env("WWWROOT") + +# make a way to find new URLs queried by new clients +# CATCH substitues this server for apache at port 80 +CATCH = False +if len(sys.argv) > 1 and sys.argv[1] == '-d': + CATCH = True + PORT=80 +else: + PORT=9090 + +# set up some logging -- selectable for diagnostics +# Create dummy iostream to capture stderr and stdout +class StreamToLogger(object): + """ + Fake file-like stream object that redirects writes to a logger instance. + """ + def __init__(self, logger, log_level=logging.INFO): + self.logger = logger + self.log_level = log_level + self.linebuf = '' + + def write(self, buf): + for line in buf.rstrip().splitlines(): + self.logger.log(self.log_level, line.rstrip()) + +if len(sys.argv) > 1 and sys.argv[1] == '-l': + loggingLevel = logging.DEBUG +else: + loggingLevel = logging.ERROR +logging.basicConfig(filename='/var/log/apache2/portal.log',format='%(asctime)s.%(msecs)03d:%(name)s:%(message)s', datefmt='%M:%S',level=loggingLevel) + + +logger = logging.getLogger('/var/log/apache2/portal.log') +handler = RotatingFileHandler("/var/log/apache2/portal.log", maxBytes=100000, backupCount=2) +logger.addHandler(handler) + + +# divert stdout and stderr to logger +stdout_logger = logging.getLogger('STDOUT') +sl = StreamToLogger(stdout_logger, logging.ERROR) +#sys.stdout = sl + +stderr_logger = logging.getLogger('STDERR') +sl = StreamToLogger(stderr_logger, logging.ERROR) +sys.stderr = sl + + +# Define globals +MAC_SUCCESS=False +ANDROID_TRIGGERED=False + +logger.debug("") +logger.debug('##########################################') +# what language are we speaking? +lang = os.environ['LANG'][0:2] +logger.debug('speaking: %s'%lang) + +def tstamp(dtime): + '''return a UNIX style seconds since 1970 for datetime input''' + epoch = datetime.datetime(1970, 1, 1,tzinfo=tzutc()) + newdtime = dtime.astimezone(tzutc()) + since_epoch_delta = newdtime - epoch + return since_epoch_delta.total_seconds() + +# ##########database operations ############## +# Use a sqlite database to store per client information +user_db = os.path.join(CAPTIVE_PORTAL_BASE,"users.sqlite") +conn = sqlite3.connect(user_db) +if not os.path.exists(user_db): + conn.close() + conn = sqlite3.connect(user_db) +c = conn.cursor() +c.row_factory = sqlite3.Row +c.execute( """create table IF NOT EXISTS users + (ip text PRIMARY KEY, mac text, current_ts integer, + lasttimestamp integer, send204after integer, + os text, os_version text, + ymd text)""") + +def update_user(ip, mac, system, system_version, ymd): + sql = "SELECT * FROM users WHERE ip = ?" + c.execute(sql,(ip,)) + row = c.fetchone() + if row == None: + sql = "INSERT INTO users (ip,mac,os,os_version,ymd) VALUES (?,?,?,?,?)" + c.execute(sql,(ip, mac, system, system_version, ymd )) + else: + sql = "UPDATE users SET (mac,os,os_version,ymd) = ( ?, ?, ?, ? ) WHERE ip = ?" + c.execute(sql,(mac, system, system_version, ymd, ip,)) + conn.commit() + +def platform_info(ip): + sql = "select * FROM users WHERE ip = ?" + c.execute(sql,(ip,)) + row = c.fetchone() + if row is None: return ('','',) + return (row['os'],row['os_version']) + +def timeout_info(ip): + sql = "select * FROM users WHERE ip = ?" + c.execute(sql,(ip,)) + row = c.fetchone() + if row is None: return (0,0,0,) + return [row['current_ts'],row['lasttimestamp'],row['send204after']] + +def is_inactive(ip): + ts=tstamp(datetime.datetime.now(tzutc())) + current_ts, last_ts, send204after = timeout_info(ip) + if not last_ts: + return True + if ts - int(last_ts) > INACTIVITY_TO: + return True + else: + return False + +def is_after204_timeout(ip): + ts=tstamp(datetime.datetime.now(tzutc())) + current_ts, last_ts, send204after = timeout_info(ip) + if send204after == 0: return False + logger.debug("function: is_after204_timeout send204after:%s current: %s"%(send204after,ts,)) + if not send204after: + return False + if ts - int(send204after) > 0: + return True + else: + return False + +def set_204after(ip,value): + global ANDROID_TRIGGERED + ts=tstamp(datetime.datetime.now(tzutc())) + sql = 'UPDATE users SET send204after = ? where ip = ?' + c.execute(sql,(ts + value,ip,)) + conn.commit() + ANDROID_TRIGGERED = False + +def set_lasttimestamp(ip): + ts=tstamp(datetime.datetime.now(tzutc())) + sql = 'UPDATE users SET lasttimestamp = ? where ip = ?' + c.execute(sql,(ts,ip,)) + conn.commit() + +# ################### Action routines based on OS ################3 +def microsoft(environ,start_response): + #logger.debug("sending microsoft response") + en_txt={ 'message':"Click on the button to go to the IIAB home page",\ + 'btn1':"GO TO IIAB HOME PAGE",'doc_root':get_iiab_env("WWWROOT")} + es_txt={ 'message':"Haga clic en el botón para ir a la página de inicio de IIAB",\ + 'btn1':"IIAB",'doc_root':get_iiab_env("WWWROOT")} + if lang == "en": + txt = en_txt + elif lang == "es": + txt = es_txt + response_body = str(j2_env.get_template("simple.template").render(**txt)) + status = '200 OK' + response_headers = [('Content-type','text/html'), + ('Content-Length',str(len(response_body)))] + start_response(status, response_headers) + return [response_body] + +def android(environ, start_response): + global ANDROID_TRIGGERED + ip = environ['HTTP_X_FORWARDED_FOR'].strip() + system,system_version = platform_info(ip) + if system_version[0:1] < '6': + logger.debug("system < 6:%s"%system_version) + location = '/android_splash' + set_204after(ip,0) + else: + set_204after(ip,20) + location = '/android_https' + agent = environ['HTTP_USER_AGENT'] + response_body = "hello" + status = '302 Moved Temporarily' + response_headers = [('Location',location)] + start_response(status, response_headers) + return [response_body] + +def android_splash(environ, start_response): + en_txt={ 'message':"Click on the button to go to the IIAB home page",\ + 'btn1':"GO TO IIAB HOME PAGE", \ + 'doc_root':get_iiab_env("WWWROOT") } + es_txt={ 'message':"Haga clic en el botón para ir a la página de inicio de IIAB",\ + 'btn1':"IIAB",'doc_root':get_iiab_env("WWWROOT")} + if lang == "en": + txt = en_txt + elif lang == "es": + txt = es_txt + response_body = str(j2_env.get_template("simple.template").render(**txt)) + status = '200 OK' + response_headers = [('Content-type','text/html'), + ('Content-Length',str(len(response_body)))] + start_response(status, response_headers) + return [response_body] + +def android_https(environ, start_response): + en_txt={ 'message':"""Please ignore the SECURITY warning which appears after clicking the first button""",\ + 'btn2':'Click this first Go to the browser we need',\ + 'btn1':'Then click this to go to IIAB home page',\ + 'doc_root':get_iiab_env("WWWROOT") } + es_txt={ 'message':"Haga clic en el botón para ir a la página de inicio de IIAB",\ + 'btn1':"IIAB",'doc_root':get_iiab_env("WWWROOT")} + if lang == "en": + txt = en_txt + elif lang == "es": + txt = es_txt + response_body = str(j2_env.get_template("simple.template").render(**txt)) + status = '200 OK' + response_headers = [('Content-type','text/html'), + ('Content-Length',str(len(response_body)))] + start_response(status, response_headers) + return [response_body] + +def mac_splash(environ,start_response): + logger.debug("in function mac_splash") + en_txt={ 'message':"Click on the button to go to the IIAB home page",\ + 'btn1':"GO TO IIAB HOME PAGE",'success_token': 'Success', + 'doc_root':get_iiab_env("WWWROOT")} + es_txt={ 'message':"Haga clic en el botón para ir a la página de inicio de IIAB",\ + 'btn1':"IIAB",'doc_root':get_iiab_env("WWWROOT")} + if lang == "en": + txt = en_txt + elif lang == "es": + txt = es_txt + set_lasttimestamp(ip) + response_body = str(j2_env.get_template("mac.template").render(**txt)) + status = '200 Success' + response_headers = [('Content-type','text/html'), + ('Content-Length',str(len(response_body)))] + start_response(status, response_headers) + return [response_body] + +def macintosh(environ, start_response): + global ip + logger.debug("in function mcintosh") + if not is_inactive(ip): + set_lasttimestamp(ip) + return success(environ,start_response) + # determine if it is time to redirect again + if is_after204_timeout(ip): + set_204after(ip,10) + response_body = """""" + status = '302 Moved Temporarily' + response_headers = [('content','text/html')] + start_response(status, response_headers) + return [response_body] + else: + return mac_splash(environ,start_response) + +def microsoft_connect(environ,start_response): + status = '200 ok' + headers = [('Content-type', 'text/html')] + start_response(status, headers) + return ["Microsoft Connect Test"] + +# ============= Return html pages ============================ +def banner(environ, start_response): + status = '200 OK' + headers = [('Content-type', 'image/png')] + start_response(status, headers) + image = open("%s/iiab-menu/menu-files/images/iiab_banner6.png"%doc_root, "rb").read() + return [image] + +def bootstrap(environ, start_response): + logger.debug("in bootstrap") + status = '200 OK' + headers = [('Content-type', 'text/javascript')] + start_response(status, headers) + boot = open("%s/common/js/bootstrap.min.js"%doc_root, "rb").read() + return [boot] + +def jquery(environ, start_response): + logger.debug("in jquery") + status = '200 OK' + headers = [('Content-type', 'text/javascript')] + start_response(status, headers) + boot = open("%s/common/js/jquery.min.js"%doc_root, "rb").read() + return [boot] + +def bootstrap_css(environ, start_response): + logger.debug("in bootstrap_css") + status = '200 OK' + headers = [('Content-type', 'text/css')] + start_response(status, headers) + boot = open("%s/common/css/bootstrap.min.css"%doc_root, "rb").read() + return [boot] + +def null(environ, start_response): + status = '200 ok' + headers = [('Content-type', 'text/html')] + start_response(status, headers) + return [""] + +def success(environ, start_response): + status = '200 ok' + html = 'SuccessSuccess' + headers = [('Content-type', 'text/html')] + start_response(status, headers) + return [html] + +def put_204(environ, start_response): + status = '204 No Data' + response_body = '' + response_headers = [('Content-type','text/html'), + ('Content-Length',str(len(response_body)))] + start_response(status, response_headers) + logger.debug("in function put_204: sending 204 html response") + return [response_body] + +def parse_agent(agent): + system = '' + system_version = '' + match = re.search(r"(Android)\s([.\d]*)",agent) + if match: + system = match.group(1) + system_version = match.group(2) + match = re.search(r"(OS X)\s([\d_]*)",agent) + if match: + system = match.group(1) + system_version = match.group(2) + match = re.search(r"(iPhone OS)\s([\d_]*)",agent) + if match: + system = match.group(1) + system_version = match.group(2) + match = re.search(r"(Windows NT)\s([\d.]*)",agent) + if match: + system = match.group(1) + system_version = match.group(2) + return (system, system_version) + +# +# ================== Start serving the wsgi application ================= +def application (environ, start_response): + global ip + global CATCH + global LIST + global INACTIVITY_TO + global ANDROID_TRIGGERED + + # Log the URLs that are not in checkurls + # This "CATCH" mode substitutes this server for apache at port 80 + # CATCH mode is started by "iiab-catch" and turned off by "iiab-uncath". + if CATCH: + logger.debug("Checking for url %s. USER_AGENT:%s"%(environ['HTTP_HOST'],\ + environ['HTTP_USER_AGENT'],)) + if environ['HTTP_HOST'] == '/box.lan': + return + if 'HTTP_X_FORWARDED_FOR' in environ: + ip = environ['HTTP_X_FORWARDED_FOR'].strip() + else: + ip = environ['HTTP_HOST'].strip() + cmd="arp -an %s|gawk \'{print $4}\'" % ip + mac = subprocess.check_output(cmd, shell=True) + data = [] + data.append("host: %s\n"%environ['HTTP_HOST']) + data.append("path: %s\n"%environ['PATH_INFO']) + data.append("query: %s\n"%environ['QUERY_STRING']) + data.append("ip: %s\n"%ip) + agent = environ['HTTP_USER_AGENT'] + data.append("AGENT: %s\n"%agent) + #print(data) + found = False + url_list = os.path.join(CAPTIVE_PORTAL_BASE,"checkurls") + if os.path.exists(url_list): + with open(url_list,"r") as checkers: + for line in checkers: + if line.find(environ['HTTP_HOST']) > -1: + found = True + break + if not found: + with open(url_list,"a") as checkers: + outstr ="%s\n" % (environ['HTTP_HOST']) + checkers.write(outstr) + data = ['%s: %s\n' % (key, value) for key, value in sorted(environ.items()) ] + logger.debug("This url was missing from checkurls:%s"%data) + + # Normal query for captive portal + else: + if 'HTTP_X_FORWARDED_FOR' in environ: + ip = environ['HTTP_X_FORWARDED_FOR'].strip() + else: + data = ['%s: %s\n' % (key, value) for key, value in sorted(environ.items()) ] + #logger.debug("need the correct ip:%s"%data) + ip = environ['REMOTE_ADDR'].strip() + cmd="arp -an %s|gawk \'{print $4}\'" % ip + mac = subprocess.check_output(cmd, shell=True) + data = [] + data.append("host: %s\n"%environ['HTTP_HOST']) + data.append("path: %s\n"%environ['PATH_INFO']) + data.append("query: %s\n"%environ['QUERY_STRING']) + data.append("ip: %s\n"%ip) + agent = environ['HTTP_USER_AGENT'] + data.append("AGENT: %s\n"%agent) + logger.debug(data) + #print(data) + found = False + return_204_flag = "False" + + # record the activity with this ip + ts=tstamp(datetime.datetime.now(tzutc())) + sql = "INSERT or IGNORE INTO users (current_ts,ip) VALUES (?,?)" + c.execute(sql,(ts,ip,)) + sql = "UPDATE users SET current_ts = ? where ip = ?" + c.execute(sql,(ts,ip,)) + if c.rowcount == 0: + logger.debug("failed UPDATE users SET current_ts = %s WHERE ip = %s"%(ts,ip,)) + conn.commit() + ymd=datetime.datetime.today().strftime("%y%m%d-%H%M") + + system,system_version = parse_agent(agent) + if system != '': + update_user(ip, mac, system, system_version, ymd) + +####### Return pages based upon PATH ############### + # do more specific stuff first + if environ['PATH_INFO'] == "/iiab_banner6.png": + return banner(environ, start_response) + + if environ['PATH_INFO'] == "/bootstrap.min.js": + return bootstrap(environ, start_response) + + if environ['PATH_INFO'] == "/bootstrap.min.css": + return bootstrap_css(environ, start_response) + + if environ['PATH_INFO'] == "/jquery.min.js": + return jquery(environ, start_response) + + if environ['PATH_INFO'] == "/favicon.ico": + return null(environ, start_response) + + if environ['PATH_INFO'] == "/home_selected": + # the js link to home page triggers this ajax url + # mark the sign-in conversation completed, return 204 or Success or Success + ANDROID_TRIGGERED = True + #data = ['%s: %s\n' % (key, value) for key, value in sorted(environ.items()) ] + #logger.debug("need the correct ip:%s"%data) + logger.debug("function: home_selected. Setting flag to return_204") + #print("setting flag to return_204") + set_204after(ip,PORTAL_TO) + set_lasttimestamp(ip) + status = '200 OK' + headers = [('Content-type', 'text/html')] + start_response(status, headers) + return [""] + +#### parse OS platform based upon URL ################## + # mac + if environ['PATH_INFO'] == "/mac_splash": + return mac_splash(environ, start_response) + + if environ['PATH_INFO'] == "/step2": + return step2(environ, start_response) + + if environ['HTTP_HOST'] == "captive.apple.com" or\ + environ['HTTP_HOST'] == "appleiphonecell.com" or\ + environ['HTTP_HOST'] == "detectportal.firefox.com" or\ + environ['HTTP_HOST'] == "*.apple.com.edgekey.net" or\ + environ['HTTP_HOST'] == "gsp1.apple.com" or\ + environ['HTTP_HOST'] == "apple.com" or\ + environ['HTTP_HOST'] == "www.apple.com": + current_ts, last_ts, send204after = timeout_info(ip) + if not send204after: + # take care of uninitialized state + set_204after(ip,0) + return macintosh(environ, start_response) + + # android + if environ['PATH_INFO'] == "/android_splash": + return android_splash(environ, start_response) + if environ['PATH_INFO'] == "/android_https": + return android_https(environ, start_response) + if environ['HTTP_HOST'] == "clients3.google.com" or\ + environ['HTTP_HOST'] == "mtalk.google.com" or\ + environ['HTTP_HOST'] == "alt7-mtalk.google.com" or\ + environ['HTTP_HOST'] == "alt6-mtalk.google.com" or\ + environ['HTTP_HOST'] == "connectivitycheck.android.com" or\ + environ['HTTP_HOST'] == "connectivitycheck.gstatic.com": + current_ts, last_ts, send204after = timeout_info(ip) + logger.debug("current_ts: %s laat_ts: %s send204after: %s"%(current_ts, last_ts, send204after,)) + if not last_ts or (ts - int(last_ts) > INACTIVITY_TO): + return android(environ, start_response) + elif is_after204_timeout(ip): + return put_204(environ,start_response) + return null(environ,start_response) #return without doing anything + + # microsoft + if environ['PATH_INFO'] == "/connecttest.txt" and not is_inactive(ip): + return microsoft_connect(environ, start_response) + if environ['HTTP_HOST'] == "ipv6.msftncsi.com" or\ + environ['HTTP_HOST'] == "ipv6.msftncsi.com.edgesuite.net" or\ + environ['HTTP_HOST'] == "www.msftncsi.com" or\ + environ['HTTP_HOST'] == "www.msftncsi.com.edgesuite.net" or\ + environ['HTTP_HOST'] == "www.msftconnecttest.com" or\ + environ['HTTP_HOST'] == "teredo.ipv6.microsoft.com" or\ + environ['HTTP_HOST'] == "teredo.ipv6.microsoft.com.nsatc.net": + return microsoft(environ, start_response) + + logger.debug("executing the defaut 204 response. [%s"%data) + return put_204(environ,start_response) + +# Instantiate the server +httpd = make_server ( + "", # The host name + PORT, # A port number where to wait for the request + application # The application object name, in this case a function +) + +httpd.serve_forever() +#vim: tabstop=3 expandtab shiftwidth=3 softtabstop=3 background=dark + diff --git a/roles/network/templates/captive-portal/checkurls b/roles/network/templates/captive-portal/checkurls new file mode 100755 index 000000000..445022872 --- /dev/null +++ b/roles/network/templates/captive-portal/checkurls @@ -0,0 +1,21 @@ +clients3.google.com +connectivitycheck.gstatic.com +detectportal.firefox.com +*.akamaitechnologies.com +appleiphonecell.com +thinkdifferent.us +*.apple.com.edgekey.net +ipv6.msftncsi.com +ipv6.msftncsi.com.edgesuite.net +www.msftncsi.com +www.msftncsi.com.edgesuite.net +www.msftconnecttest.com +teredo.ipv6.microsoft.com +teredo.ipv6.microsoft.com.nsatc.net +captive.apple.com +init-p01st.push.apple.com +mtalk.google.com +connectivitycheck.android.com +alt7-mtalk.google.com +alt6-mtalk.google.com +captive.lan diff --git a/roles/network/templates/captive-portal/iiab-catch b/roles/network/templates/captive-portal/iiab-catch new file mode 100755 index 000000000..59bf32b65 --- /dev/null +++ b/roles/network/templates/captive-portal/iiab-catch @@ -0,0 +1,9 @@ +#!/bin/bash -x +# substitute our own server to catch OS connectivity checking URL's + +systemctl stop apache2 +systemctl stop py-captive-portal +echo address=/#/172.18.96.1 > /etc/dnsmasq.d/capture +/opt/iiab/captive-portal/capture-wsgi.py -d & +# write the pid just started +echo $! > /opt/iiab/captive-portal/pid diff --git a/roles/network/templates/captive-portal/iiab-uncatch b/roles/network/templates/captive-portal/iiab-uncatch new file mode 100755 index 000000000..bfea48d55 --- /dev/null +++ b/roles/network/templates/captive-portal/iiab-uncatch @@ -0,0 +1,13 @@ +#!/bin/bash -x +# Turn off URL recording mode, and return to serving with apache2 + +kill $(cat /opt/iiab/captive-portal/pid) +# during testing, I start capture by hand -- recorded pid may be stale +pid=$(ps aux | grep "capture-wsgi.py -d" | grep -v grep | awk '{print $2}') +if [ -n "$pid" ]; then + kill $pid +fi +awk '{print("address=/" $1 "/172.18.96.1")}' /opt/iiab/captive-portal/checkurls > /etc/dnsmasq.d/capture +awk '{print("ServerAlias ",$1)}' /opt/iiab/captive-portal/checkurls > /etc/apache2/capture +systemctl start py-captive-portal +systemctl start apache2 diff --git a/roles/network/templates/captive_portal/captive_portal.service.j2 b/roles/network/templates/captive-portal/py-captive-portal.service.j2 similarity index 80% rename from roles/network/templates/captive_portal/captive_portal.service.j2 rename to roles/network/templates/captive-portal/py-captive-portal.service.j2 index 03f3c33d5..f1c398bef 100644 --- a/roles/network/templates/captive_portal/captive_portal.service.j2 +++ b/roles/network/templates/captive-portal/py-captive-portal.service.j2 @@ -7,7 +7,7 @@ Type=simple User=root Group=root WorkingDirectory=/opt/iiab/captive-portal -ExecStart=/opt/iiab/captive-portal/captive_portal.py +ExecStart=/opt/iiab/captive-portal/capture-wsgi.py StandardOutput=syslog StandardError=syslog diff --git a/roles/network/templates/gateway/iiab-gen-iptables b/roles/network/templates/gateway/iiab-gen-iptables index fdd91f56d..1494a2bee 100755 --- a/roles/network/templates/gateway/iiab-gen-iptables +++ b/roles/network/templates/gateway/iiab-gen-iptables @@ -9,7 +9,7 @@ IPTABLES_DATA=/etc/sysconfig/iptables {% endif %} LANIF=$IIAB_LAN_DEVICE WANIF=$IIAB_WAN_DEVICE -MODE=`grep iiab_network_mode_applied /etc/iiab/iiab.ini | gawk '{print $3}'` +MODE=`grep iiab_network_mode_applied /etc/iiab/iiab.ini | gawk '{print $3}'` clear_fw() { $IPTABLES -F @@ -26,7 +26,7 @@ $IPTABLES -A INPUT -p udp --dport 111 -j DROP # mysql $IPTABLES -A INPUT -p tcp --dport 3306 -j DROP $IPTABLES -A INPUT -p udp --dport 3306 -j DROP -# postgre - not needed listens on lo only +# postgres - not needed listens on lo only $IPTABLES -A INPUT -p tcp --dport 5432 -j DROP $IPTABLES -A INPUT -p udp --dport 5432 -j DROP # couchdb @@ -34,7 +34,7 @@ $IPTABLES -A INPUT -p tcp --dport 5984 -j DROP $IPTABLES -A INPUT -p udp --dport 5984 -j DROP } -if [ "x$WANIF" == "xnone" ] || [ "$MODE" == 'Appliance' ]; then +if [ "x$WANIF" == "xnone" ] || [ "$MODE" == "Appliance" ]; then clear_fw # save the rule set {% if is_debuntu %} @@ -62,7 +62,6 @@ transmission_http_port={{ transmission_http_port }} transmission_peer_port={{ transmission_peer_port }} sugarizer_port={{ sugarizer_port }} block_DNS={{ block_DNS }} -captive_portal_enabled={{ captive_portal_enabled }} py_captive_portal_enabled={{ py_captive_portal_enabled }} echo "LAN is $lan and WAN is $wan" @@ -112,28 +111,23 @@ if [ "$gw_block_https" == "True" ]; then fi # Allow outgoing connections from the LAN side. -if ! [ "$py_captive_portal_enabled" == "True" ];then +if ! [ "$py_captive_portal_enabled" == "True" ]; then $IPTABLES -A FORWARD -i $lan -o $wan -j ACCEPT fi # Don't forward from the outside to the inside. $IPTABLES -A FORWARD -i $wan -o $lan -j DROP $IPTABLES -A INPUT -i $wan -j DROP -if [ "$block_DNS" == "True" ];then +if [ "$block_DNS" == "True" ]; then $IPTABLES -t nat -A PREROUTING -i $lan -p tcp --dport 53 ! -d {{ lan_ip }} -j DNAT --to {{ lan_ip }}:53 $IPTABLES -t nat -A PREROUTING -i $lan -p udp --dport 53 ! -d {{ lan_ip }} -j DNAT --to {{ lan_ip }}:53 fi -if [ "$captive_portal_enabled" == "True" ];then - $IPTABLES -t mangle -N internet - $IPTABLES -t mangle -A PREROUTING -i {{ iiab_lan_iface }} -p tcp -m tcp --dport 80 -j internet - $IPTABLES -t mangle -A internet -j MARK --set-mark 99 - $IPTABLES -t nat -A PREROUTING -i {{ iiab_lan_iface }} -p tcp -m mark --mark 99 -m tcp --dport 80 -j DNAT --to-destination {{ lan_ip }} - -elif [ "py_$captive_portal_enabled" == "True" ];then +if [ "$py_captive_portal_enabled" == "True" ]; then $IPTABLES -t nat -A PREROUTING -i $lan -p tcp --dport 80 ! -d {{ lan_ip }} -j DNAT --to {{ lan_ip }}:{{ py_captive_portal_port }} +fi -elif [ "$HTTPCACHE_ON" == "True" ]; then +if [ "$HTTPCACHE_ON" == "True" ]; then $IPTABLES -t nat -A PREROUTING -i $lan -p tcp --dport 80 ! -d {{ lan_ip }} -j DNAT --to {{ lan_ip }}:3128 fi diff --git a/vars/default_vars.yml b/vars/default_vars.yml index ac666d5a3..06ebea839 100644 --- a/vars/default_vars.yml +++ b/vars/default_vars.yml @@ -103,23 +103,27 @@ dhcpd_enabled: False # named named_install: True -named_enabled: True +named_enabled: False block_DNS: False # dnsmasq dnsmasq_install: True -dnsmasq_enabled: False +dnsmasq_enabled: True # Enable in local_vars.yml AFTER installing IIAB! Then run "cd /opt/iiab/iiab; ./iiab-network" dns_jail_enabled: False -# For @tim-moody's Nodogsplash approach to Captive Portal? High experimental as of June 2018: github.com/iiab/iiab/issues/608 -captive_portal_install: False -captive_portal_enabled: False - -# Simple python Captive Portal, that @m-anish & @jvonau are experimenting with in July 2018: github.com/iiab/iiab/pull/870 +# Python-based Captive Portal, that @m-anish & @jvonau experimented with in +# July 2018 (https://github.com/iiab/iiab/pull/870) and that @georgejhunt +# extensively refined in Sept 2018 (https://github.com/iiab/iiab/pull/1179) py_captive_portal_install: True -py_captive_portal_enabled: False +py_captive_portal_enabled: True +# In a pinch, disable it by running: systemctl disable py-captive-portal + +# For @tim-moody's Nodogsplash approach to Captive Portal? +# Highly experimental as of June 2018: https://github.com/iiab/iiab/issues/608 +# captive_portal_install: False +# captive_portal_enabled: False # Squid squid_install: False diff --git a/vars/local_vars_big.yml b/vars/local_vars_big.yml index 243f2dc62..ec2e751dd 100644 --- a/vars/local_vars_big.yml +++ b/vars/local_vars_big.yml @@ -47,14 +47,17 @@ iiab_gateway_enabled: False # dnsmasq dnsmasq_install: True -dnsmasq_enabled: False +dnsmasq_enabled: True # Enable AFTER installing IIAB! Then run "cd /opt/iiab/iiab; ./iiab-network" dns_jail_enabled: False -# Simple python Captive Portal, that @m-anish & @jvonau are experimenting with in July 2018: github.com/iiab/iiab/pull/870 +# Python-based Captive Portal, that @m-anish & @jvonau experimented with in +# July 2018 (https://github.com/iiab/iiab/pull/870) and that @georgejhunt +# extensively refined in Sept 2018 (https://github.com/iiab/iiab/pull/1179) py_captive_portal_install: True -py_captive_portal_enabled: False +py_captive_portal_enabled: True +# In a pinch, disable it by running: systemctl disable py-captive-portal # Stages 3 & 4 must be run (using iiab-install or runrole) if changing these: squid_install: True diff --git a/vars/local_vars_medium.yml b/vars/local_vars_medium.yml index 7d04368f5..4cc70fd06 100644 --- a/vars/local_vars_medium.yml +++ b/vars/local_vars_medium.yml @@ -47,14 +47,17 @@ iiab_gateway_enabled: False # dnsmasq dnsmasq_install: True -dnsmasq_enabled: False +dnsmasq_enabled: True # Enable AFTER installing IIAB! Then run "cd /opt/iiab/iiab; ./iiab-network" dns_jail_enabled: False -# Simple python Captive Portal, that @m-anish & @jvonau are experimenting with in July 2018: github.com/iiab/iiab/pull/870 +# Python-based Captive Portal, that @m-anish & @jvonau experimented with in +# July 2018 (https://github.com/iiab/iiab/pull/870) and that @georgejhunt +# extensively refined in Sept 2018 (https://github.com/iiab/iiab/pull/1179) py_captive_portal_install: True -py_captive_portal_enabled: False +py_captive_portal_enabled: True +# In a pinch, disable it by running: systemctl disable py-captive-portal # Stages 3 & 4 must be run (using iiab-install or runrole) if changing these: squid_install: False diff --git a/vars/local_vars_min.yml b/vars/local_vars_min.yml index a61104c56..b48d9a460 100644 --- a/vars/local_vars_min.yml +++ b/vars/local_vars_min.yml @@ -47,14 +47,17 @@ iiab_gateway_enabled: False # dnsmasq dnsmasq_install: True -dnsmasq_enabled: False +dnsmasq_enabled: True # Enable AFTER installing IIAB! Then run "cd /opt/iiab/iiab; ./iiab-network" dns_jail_enabled: False -# Simple python Captive Portal, that @m-anish & @jvonau are experimenting with in July 2018: github.com/iiab/iiab/pull/870 +# Python-based Captive Portal, that @m-anish & @jvonau experimented with in +# July 2018 (https://github.com/iiab/iiab/pull/870) and that @georgejhunt +# extensively refined in Sept 2018 (https://github.com/iiab/iiab/pull/1179) py_captive_portal_install: True -py_captive_portal_enabled: False +py_captive_portal_enabled: True +# In a pinch, disable it by running: systemctl disable py-captive-portal # Stages 3 & 4 must be run (using iiab-install or runrole) if changing these: squid_install: False