├── .gitignore ├── LICENSE ├── README.md ├── pirania-app └── files │ ├── etc │ └── pirania │ │ └── content.json │ ├── usr │ ├── libexec │ │ └── rpcd │ │ │ └── pirania-app │ └── share │ │ └── rpcd │ │ └── acl.d │ │ └── pirania-app.json │ └── www │ ├── cgi-bin │ ├── error_handler │ └── pirania │ │ ├── client_ip │ │ ├── handle_auth │ │ ├── handle_voucher │ │ └── portal │ └── portal │ ├── admin.html │ ├── auth.html │ ├── authenticated.html │ ├── css │ ├── loader2.css │ ├── main.css │ ├── mobile-icons.css │ └── normalize.css │ ├── fail.html │ ├── index.html │ ├── info.html │ └── js │ ├── admin.js │ ├── content.js │ ├── int.js │ ├── tabs.js │ ├── ubusFetch.js │ └── voucher.js └── pirania └── files ├── etc ├── config │ └── pirania ├── init.d │ ├── pirania │ ├── pirania-dnsmasq │ └── pirania-uhttpd └── pirania │ └── db.csv ├── usr ├── bin │ ├── captive-portal │ └── voucher ├── lib │ └── lua │ │ └── voucher │ │ ├── config.lua │ │ ├── db.lua │ │ ├── debugtools.lua │ │ ├── functools.lua │ │ ├── hooks.lua │ │ ├── logic.lua │ │ └── utils.lua ├── libexec │ └── rpcd │ │ └── pirania └── share │ └── rpcd │ └── acl.d │ └── pirania.json └── www └── pirania-redirect └── redirect /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .DS_Store 3 | build-router.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 LibreMesh.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated! Moved to https://github.com/libremesh/lime-packages/tree/master/packages 2 | 3 | ![PIRANHA](https://i.imgur.com/kHWUNOu.png) 4 | 5 | ## Voucher and Captive Portal solution for community networks 6 | 7 | *ALPHA Software, don't use in production* 8 | 9 | 10 | This tool allows an administrator to manage a voucher system to get through the gateway. 11 | 12 | It could be used in a community that wants to share an Internet connection and for that the user's pay a fraction each, but needs the payment from everyone. So the vouchers allows to control the payments via the control of the access to Internet. 13 | 14 | ## Features 15 | 16 | This are the currently implemented features: 17 | * Runs directly from the OpenWRT/LEDE router: no need for extra hardware 18 | * Integrates it's administration with Ubus 19 | * Has a command-line interface for listing, creating and removing vouchers 20 | * Voucher database is shared among nodes in the network 21 | 22 | All planned features are accesible at: https://github.com/libremesh/voucher/issues 23 | 24 | ## Prerequisites 25 | 26 | This software assumes that will be running on a OpenWRT/LEDE distribution (because uses uci for config). Needs `ip6tables-mod-nat` and `ipset` packages installed. 27 | 28 | ## Install 29 | 30 | Not clear yet, but would be something like: 31 | * add the libremesh software feed to opkg 32 | * opkg install pirania 33 | * opkg install pirania-app 34 | 35 | # How it works 36 | 37 | It uses iptables rules to filter inbound connections outside the mesh network. 38 | 39 | ## General overview of file hierarchy and function 40 | 41 | ``` 42 | files/ 43 | /etc/config/pirania is the UCI config 44 | /etc/pirania/db.csv (default path) contains the database of vouchers 45 | /etc/init.d/pirania-uhttpd starts a uhttpd on port 59080 that replies any request with a redirect towards a preset URL 46 | 47 | /usr/lib/lua/voucher/ contains lua libraries used by /usr/bin/voucher 48 | /usr/bin/voucher is a CLI to manage the db (has functions add_voucher, add_many_vouchers, auth_voucher, get_valid_macs, list_vouchers, remove_voucher and url) 49 | /usr/bin/captive-portal sets up iptables rules to capture traffic 50 | 51 | /usr/libexec/rpcd/pirania ubus pirania API (this is used by the web frontend) 52 | /usr/share/rpcd/acl.d/pirania.json ACL for the ubus pirania API 53 | 54 | luasrc/ contains the luci-app to manage vouchers 55 | ``` 56 | -------------------------------------------------------------------------------- /pirania-app/files/etc/pirania/content.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Pirania", 3 | "backgroundColor": "#f0e5de", 4 | "welcome": "Welcome, this is a captive portal", 5 | "body": "In order to have access to Interet you must enter a valid voucher. In the meantime you can continue using the local network services.", 6 | "logo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAABrCAMAAACi04sXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgTWFjaW50b3NoIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjc0MjEyOEY5NUEyOTExRTlBNTMwOTU0MjBCQTA4RDNCIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjc0MjEyOEZBNUEyOTExRTlBNTMwOTU0MjBCQTA4RDNCIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzQyMTI4Rjc1QTI5MTFFOUE1MzA5NTQyMEJBMDhEM0IiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzQyMTI4Rjg1QTI5MTFFOUE1MzA5NTQyMEJBMDhEM0IiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4ENYiSAAAAM1BMVEUQEBBAQEDv7+9/f3+/v79gYGDPz8+fn5/f398wMDAgICBwcHCvr6+Pj49QUFAAAAD///9/TN52AAAAEXRSTlP/////////////////////ACWtmWIAAAttSURBVHjazFvZguMoDOQGA8b9/1+7SOL0Fbund6b90JvJJnahoyQVhH1993J8y9cSzNf/cLFvf1Nu5eLql8HSEi22afF7YNltc19fwvqMi62/BpZgm8YXEYAl8UtgfYVtiwQwv9q8+SWwXHZesZHJBmPqd8CCoOfVoykbTP4OWF/LtoX6eoWM/B2wDKvhlS+V/2H/GSwlpVU17WIPLwJp/xGsQMzupankxb/+B1xvYbFacjaW1mw1PiIx+W3zT2BlJliV1R3dlIDZeMz9C1jgKKg0ppTDbfZb5oxF/IuQB1ze4qONlYHLOZrSz9DEe4IwCwTWFXWKhaz593lLSH9DnWBN83dhOSuBtZRKNymXuWz5q7D0Nl7qhtrkX4RlJlT8MuGEf8WquV1j9k+sBbG+SOOyF2+jR71hL3KB+wNYQiKL8mtMgJhCjz+85wojyjEi3mWiWJEdjv4TKvLJx9v6yF4O6NchLDPWsdcEEQ/RLqLcQaIrxUcuNGCxfF/whP8erBjAWl6M9muQFk7soaxMnvqMT5FPY8qSGQWq6QLwXsMqobXx7h5XOIPLuAs4tyIyHz9FlgJH2kzCi1t6RD6HBWUFAPTnKwTlwwWDKbTj7aTmwW0hByvLLlAD3z2HlZsDFoY4NvhUfmcOBAaTWnY1XFIcAl4CNs3BanrgCfYq1rvgINBSy6chDAKZGbfU1nHd+9AANg4l1o119kVsxYSxZepg+KhVANcvgE3LdCgOemM0MpEvhzL7LhPXQsio1izmeUhib4gJwsxYNhJ2aODCHF78u3Tq0UIl+C9iWWkO1uGr6M5fxum7BZDAu4ELAq7UfhOWRfcjKhbvR6PeKsqxLKBPh9ppABvelHUufQtLggsR1aUQgvm6KhVDaRXd3BZCeyH7Z/Nfjh+bjfUalkFUl2NE7MEDSajhqXP3pXq/wDGapG4MplaZONfqvRPTLSoxmgbaZ20PvWpoRDAAzr1cCG3K0+9gCfriNXHriTQMO2tj4SaiQFFjiiNR8BTAzXtYbtXJ3jV4dwOX2JlGbWNXMITCWqCI4b025+Xw4+zgpruGWN+rRWrfxfNtkJrGHotGymV8j9u+uMT239jYdfB80rDk3mNgriOTcDITG9dg3HgXy47hqNjFw+0nZS0NbmkIYlOgRH+KwqRVFwGcO4r9XVRJoW+gyllebqdUJwxyouDdBxRcYWNXa5Nf7GxxcTsZqQBVus/UOlnwFp6ivkVVtN2J+pmLEpq73wOsUCHEI6oPYowp38YE5LMB+SDuKFifOos6KppmT6eyrkjvE9t8RtUSMQyjXyov0iCFKQCtz3woa4vBDg93tVeIOzt+HElVMQdZZp2SU86wgjiJU9ynIe+zQ+cSaqxq6kJzs6ujvRMdBli+w5L1PrI5dms9xSoP2kocN4/YoWD5KSiW1qeEz7Vpo4kqzJaRdcF1ZTlCItvP3WhO7c47CNfiknZ0oDgngvZAeyyfigMsO7zoabClg/H5NOmxIwPzIa2oJcKN1gfaYyVP37OuE79KQQzKDz7FTZk+pBQ7YSfXZ7gaAtdlZ61PIwy6BlJJkOWoz5GP4V09Kkh6xMVOhPdQPyaIYmqTEi6E1HY3U31tZFFG3JGDqTuSpfAPjhtxMRz48kDg+lpYH8S/moR2vkWR31x8f7Df21QfG5JQux0SCoahYGgZWWotoZuDnnLINpVUzcNUfW+Fb5ghBOxRQBxHN1tbwwDGWcEJYuhQdN8sCSQElUmlamaCvKYbGjtrNeU++BE+1NkBF84TaZqmbSWbEuIwcLN2aEHXvGXkM1JjcFVtBiB4EAolMvUhTiR+xHUoOICEYSpc5s6TyD7jyTFRkkKO+koqK2SVP4WsFaNuXnLydOwkdNgKIP7IeP3Uv4OEIlDPwRgmESWP+4Fkr2x9+FglLlcEqoXrRk+sVxXMKi1a0HN6JpUNHAzdfisA+INHsGhrNMUok9dhxPpZOgdUcpLz2FxMmO7hj3dcnClINcGC6lgdqXYzRIaVb2hK1tbVF6NsXu10RMYxivl60mcqGhLLQpgbidJieJbBt3go/0fgDjmcDZG7VWaDSJjl+ZylLj/jRNM1ehgFP2wXyPFDZbTD0C4MT76EGTkHCjbaM285ny2YQ/3jgQPLKd3j8nn7hZUpqz6Lzhmhh6mc1dkP0iqN1NI2UhS8BZ5P7pPwStqhYx9le1botjPeygpTESxbsx9IqOSpGTsOT/MSpluy530Geq9LnOERLCoDrdfJ0Sd68Q9U5pwtNItu5mN1sWXDmtLtqMY73GPwg3Cqn8EirX7eusSDM1JBiciL9X1CMGn8JJoLSNSijEkEZbuypuRyUOgfOrErtG6nLVJnVDYEdL115FrM5jKt5kRd9w7gogTy84k4uLV8BqvEessmHNc6ss3rOCWFHJfuqRaGJq4M5MlGIb/dmomnsCjWa3EjEcpZFPVA9YaIxq0xjC3vJkq1VAp62pjMW3CtR9JQy5PDEmMbiPK5rXwkanOUQVHBRDZb9mo8qZ7jEHY3HOlte7KrN3WnQOasdA++9sprUeFBXT/w1myuD/sHZl0eUfwAy6yhhLbqTU2OG6bKnK+wGOHmzJnEgkLgcn2uEgkGIeVwkA9hDRUek6xQO00yawln2F/TZ50zFkqU+8nX6x64kS17/CSZ3sKigpOHZynLESiHswIUYjrHZgsTiNNqRuaSsIZSUpdQgl0oGwq/sKCwAzdPzkkwyi2227xC6WaF+w+laJkbmLm/odBPm02dG6aTAKV1C+tRTT2DBai4O9FuV7f1bU0S3PnFLT302TBAr3kwgGcPlJdJH/KZlUbX+yeDMIOMLx26spLXPUtkBviz2C64i6skol0XKEG4tZtNCsuQUa0YTZimsZ03jk9g0UThaghUssZbtFmJQj1eyjZoLhgbymliHN+apgtNLeCW2Oc+8GGGhaJSjYZla9vYduh7FcWG7nvcp+bKsD0FvVGUKGoryp8ElGnLVePR+VRWiZDqvti6SEfxgQbjNKPdVH40F46kKMfAkQtyH9QLOH+RrZZv7oR8dD6CwUCUL7PfkEEsnmaeosHFm+pC5spLYYwB4fAN9US55TvamNfKNh0f+e9EGpnO9oGJsBFbE1FNOJ6F2ZtLod8xhaVEhdThFAKvNv78INwEqx78a7DKqeBCov5uMlBkLjK3RK4H0c+jWpDfi9ubY5Zs7hrH9fA2kFF6mvvzTyX+qFHFfQfa6Yylr2XPzwNNsKA/wwIdeFIUJ2SHMgKHe6FSEXiavPNfh2SnaHB3uI3vvgMrkGRgq/7UVkf6Z35I+iRROooEFr+MwRwptIIay/riRCobV8sqKoBlmkcd3ld+ksALuY1CWCwK7+uj9GyeFTAXSTdZeyh5KkSfdmp5ef5efFPvD+6yQb3OxsLNnoCwUqcD+Uw4aMJJPKv+34MFCY5jmaXN+Km1WtkjXO28GmlS5gdgSapnzOJmhAG7rbMj1u2TDUwftXAhfnU/AKtsT1Hx90dKx2yQdyQx1K5yCM0nqTn3ueTGb8Gi+a/0oXz8+cdOhb87WSdGlZwOGQ8jcPxOyJtaryV1OuwsNEgOUndRP37PRFD48rUktt1V1PtS3WrQBar8P9P9GTd7+c31zZH1My4SmrHrX6vRkb9LV9rt5Cxw6d/YH8H6dBlNA5a5wnU+CIbL3vZnYDUp/dyXUNrPzp3I/x0WGKVIbCfhj2fHjqZM/7MTm/7OrpDpE0e6Pw3551ckod2ve9tAVfTqrO/5G7CaYrWxZN2czXO6uvTqNwd/DAt+Z1rGTJ/WoYdBImEBWi6n9KuW+UdgkXrFmiAS5EoSupl+vPHqZ6g/A4uEuzT+hCvjq0ozgnrXoP4cLNqCkpqzw4F++bq/+VlYvUJLTleSMn7jR2X/CTAA8COGKzk+uG4AAAAASUVORK5CYII=", 7 | "rules": "

Welcome to our community network. It's a communication network owned and managed by the community. In case of any problems first learn how to use our app which can help you find any problems and check the status of the whole network.

", 8 | "mediaUrl": "" 9 | } -------------------------------------------------------------------------------- /pirania-app/files/usr/libexec/rpcd/pirania-app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | Copyright 2018 Marcos Gutierrez 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-3.0 8 | ]]-- 9 | 10 | require "ubus" 11 | local json = require 'luci.json' 12 | local path = '/etc/pirania/content.json' 13 | 14 | local function printJson (obj) 15 | print(json.encode(obj)) 16 | end 17 | 18 | local conn = ubus.connect() 19 | if not conn then 20 | error("Failed to connect to ubus") 21 | end 22 | 23 | local function shell(command) 24 | local handle = io.popen(command) 25 | local result = handle:read("*a") 26 | handle:close() 27 | return result 28 | end 29 | 30 | local function get_clients(msg) 31 | local result = {} 32 | result.clients = {} 33 | local output = shell("for ip in $(ip n show | grep -v IP | awk '{print $1}' | sort -ut '|' -k 1,2); do grep $ip /tmp/dhcp.leases; done") 34 | for line in output:gmatch("[^\n]+") do 35 | local words = {} 36 | for w in line:gmatch("%S+") do if w ~= "" then table.insert(words, w) end end 37 | local mac = words[2] 38 | local ip = words[3] 39 | local station = words[4] 40 | table.insert(result.clients, { station=station, ip=ip, mac=mac }) 41 | end 42 | printJson(result); 43 | end 44 | 45 | local function read_content(msg) 46 | local contents = "" 47 | local myTable = {} 48 | local file = io.open( path, "r" ) 49 | 50 | if file then 51 | -- read all contents of file into a string 52 | local contents = file:read( "*a" ) 53 | myTable = json.decode(contents) 54 | io.close( file ) 55 | end 56 | printJson(myTable) 57 | end 58 | 59 | local function write_content(msg) 60 | local file = io.open(path, "w") 61 | if file then 62 | local contents = json.encode(msg) 63 | file:write( contents ) 64 | io.close( file ) 65 | read_content() 66 | end 67 | end 68 | 69 | local methods = { 70 | read_content = { no_params = 0 }, 71 | get_clients = { no_params = 0 }, 72 | write_content = { 73 | title = 'value', 74 | backgroundColor = 'value', 75 | welcome = 'value', 76 | body = 'value', 77 | logo = 'value', 78 | rules = 'value' 79 | } 80 | } 81 | 82 | if arg[1] == 'list' then 83 | printJson(methods) 84 | end 85 | 86 | if arg[1] == 'call' then 87 | local msg = io.read() 88 | msg = json.decode(msg) 89 | if arg[2] == 'read_content' then read_content(msg) 90 | elseif arg[2] == 'get_clients' then get_clients(msg) 91 | elseif arg[2] == 'write_content' then write_content(msg) 92 | else printJson({ error = "Method not found" }) 93 | end 94 | end -------------------------------------------------------------------------------- /pirania-app/files/usr/share/rpcd/acl.d/pirania-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "unauthenticated": { 3 | "description": "Pirania public access", 4 | "write": { 5 | "ubus": { 6 | "pirania-app": [ "get_clients", "read_content" ] 7 | } 8 | } 9 | }, 10 | "root": { 11 | "description": "Pirania administration access", 12 | "write": { 13 | "ubus": { 14 | "pirania-app": ["*"] 15 | } 16 | }, 17 | "read": { 18 | "ubus": { 19 | "pirania-app": ["*"] 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /pirania-app/files/www/cgi-bin/error_handler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | print ("Content-type: text/html\n\n") 4 | 5 | print([[ 6 | 7 | 8 | 9 | Pirania 10 | 11 | 12 | 15 | 16 | 17 | 18 |

ENTER

19 | 20 | 21 | ]]) -------------------------------------------------------------------------------- /pirania-app/files/www/cgi-bin/pirania/client_ip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local json = require 'luci.json' 4 | local voucher = require('voucher.logic') 5 | 6 | print ("Content-type: application/json\n") 7 | 8 | 9 | local ipv4AndMac = voucher.getIpv4AndMac() 10 | 11 | 12 | local response = {} 13 | response.ip = ipv4AndMac.ip 14 | response.mac = ipv4AndMac.mac 15 | response.valid = voucher.check_mac_validity(ipv4AndMac.mac) > 0 16 | 17 | print(json.encode(response)) 18 | -------------------------------------------------------------------------------- /pirania-app/files/www/cgi-bin/pirania/handle_auth: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local http = require('luci.http') 4 | local logic = require('voucher.logic') 5 | local dba = require('voucher.db') 6 | local query_string = os.getenv("QUERY_STRING") 7 | local db = dba.load(config.db) 8 | local redirect_page = require('voucher.utils').redirect_page 9 | local uci_cursor = require('uci').cursor() 10 | 11 | local url_authenticated = uci_cursor:get("pirania", "base_config", "url_authenticated") 12 | local url_fail = uci_cursor:get("pirania", "base_config", "url_fail") 13 | 14 | print("Content-type: text/html \n\n") 15 | 16 | local res = logic.getIpv4AndMac() 17 | params = http.urldecode_params(query_string) 18 | local voucher = params['voucher'] 19 | local mac = res.mac 20 | local prevUrl = params['prev'] 21 | local valid = logic.check_mac_validity(res.mac) 22 | 23 | local url = url_fail 24 | local output 25 | local success = false 26 | 27 | if (voucher and mac) then 28 | local res = logic.auth_voucher(db, mac, voucher) 29 | if (res.success == true or valid > 0) then 30 | if (res.success == true) then 31 | success = true 32 | end 33 | if (prevUrl ~= nil) then 34 | url = 'http://'..prevUrl 35 | else 36 | url = url_authenticated 37 | end 38 | end 39 | end 40 | 41 | print(redirect_page(url)) 42 | 43 | 44 | if (success == true) then 45 | dba.save(config.db, db) 46 | end -------------------------------------------------------------------------------- /pirania-app/files/www/cgi-bin/pirania/handle_voucher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local http = require('luci.http') 4 | local logic = require('voucher.logic') 5 | local redirect_page = require('voucher.utils').redirect_page 6 | local uci_cursor = require('uci').cursor() 7 | local dba = require('voucher.db') 8 | local query_string = os.getenv("QUERY_STRING") 9 | -- send("Status: 302 \r\n") 10 | print("Content-type: text/html \n\n") 11 | params = http.urldecode_params(query_string) 12 | local res = logic.getIpv4AndMac() 13 | local valid = logic.check_mac_validity(res.mac) 14 | local noJs = params['nojs'] 15 | 16 | local url_authenticated = uci_cursor:get("pirania", "base_config", "url_authenticated") 17 | local url_fail = uci_cursor:get("pirania", "base_config", "url_fail") 18 | local url_info = uci_cursor:get("pirania", "base_config", "url_info") 19 | 20 | local url 21 | 22 | if (valid < 1) then 23 | local db = dba.load(config.db) 24 | local output 25 | local mac = res.mac 26 | local voucher = params['voucher'] 27 | local prevUrl = params['prev'] 28 | if (noJs == 'true') then 29 | local authCommand = "voucher auth_voucher " .. mac .. " ".. voucher 30 | ac = io.popen(authCommand, 'r') 31 | output = ac:read('*l') 32 | ac:close() 33 | local validationOut = tonumber(output) 34 | if (validationOut and validationOut > 0) then 35 | url = url_authenticated 36 | else 37 | url = url_fail 38 | end 39 | else 40 | local setParams = prevUrl and '?voucher='..voucher..'&prev='..prevUrl or'?voucher='..voucher 41 | local voucherValid = logic.check_voucher_validity(voucher, db) 42 | if (voucherValid.valid) then 43 | url = url_info..setParams 44 | else 45 | url = url_fail 46 | end 47 | end 48 | 49 | 50 | 51 | else 52 | url = url_authenticated 53 | end 54 | 55 | print(redirect_page(url)) 56 | 57 | -------------------------------------------------------------------------------- /pirania-app/files/www/cgi-bin/pirania/portal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local http = require('luci.http') 4 | local logic = require('voucher.logic') 5 | local redirect_page = require('voucher.utils').redirect_page 6 | local uci_cursor = require('uci').cursor() 7 | 8 | local query_string = os.getenv("QUERY_STRING") 9 | print("Content-type: text/html \n\n") 10 | params = http.urldecode_params(query_string) 11 | local prevUrl = params['prev'] 12 | 13 | local url_auth = uci_cursor:get("pirania", "base_config", "url_auth") 14 | local url_authenticated = uci_cursor:get("pirania", "base_config", "url_authenticated") 15 | 16 | local res = logic.getIpv4AndMac() 17 | local valid = logic.check_mac_validity(res.mac) 18 | 19 | local url 20 | local setParams = prevUrl and '?prev='..prevUrl or '' 21 | 22 | if (valid > 0) then 23 | url = url_authenticated..setParams 24 | else 25 | url = url_auth..setParams 26 | end 27 | 28 | print(redirect_page(url)) -------------------------------------------------------------------------------- /pirania-app/files/www/portal/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pirania | Admin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 26 | 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pirania 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 25 |
26 | 27 | 28 | 29 |

30 |

31 |

32 |
33 |
34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/authenticated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pirania 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 25 |
26 | 27 | 28 | 29 |

30 |

31 |

32 |

Authenticated!

33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/css/loader2.css: -------------------------------------------------------------------------------- 1 | .lds-ring { 2 | display: inline-block; 3 | /* position: relative; */ 4 | top: -6.5vh; 5 | width: 25px; 6 | height: 25px; 7 | margin: 0 auto; 8 | text-align: center; 9 | margin-bottom: -6.5vh; 10 | left: 200px; 11 | } 12 | 13 | .lds-ring div { 14 | box-sizing: border-box; 15 | display: block; 16 | position: absolute; 17 | width: 20px; 18 | height: 20px; 19 | margin: 2px; 20 | border: 2px solid #000; 21 | border-radius: 50%; 22 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 23 | border-color: #000 transparent transparent transparent; 24 | } 25 | 26 | .lds-ring div:nth-child(1) { 27 | animation-delay: -0.45s; 28 | } 29 | 30 | .lds-ring div:nth-child(2) { 31 | animation-delay: -0.3s; 32 | } 33 | 34 | .lds-ring div:nth-child(3) { 35 | animation-delay: -0.15s; 36 | } 37 | 38 | @keyframes lds-ring { 39 | 0% { 40 | transform: rotate(0deg); 41 | } 42 | 43 | 100% { 44 | transform: rotate(360deg); 45 | } 46 | } -------------------------------------------------------------------------------- /pirania-app/files/www/portal/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | border: 10px solid white; 3 | } 4 | 5 | a { 6 | text-decoration: none; 7 | } 8 | 9 | input[type=number] { 10 | /*for absolutely positioning spinners*/ 11 | position: relative; 12 | padding: 5px; 13 | padding-right: 25px; 14 | } 15 | 16 | input[type=number]::-webkit-inner-spin-button, 17 | input[type=number]::-webkit-outer-spin-button { 18 | opacity: 1; 19 | } 20 | 21 | 22 | input[type=number]::-webkit-outer-spin-button, 23 | input[type=number]::-webkit-inner-spin-button { 24 | -webkit-appearance: inner-spin-button !important; 25 | width: 25px; 26 | position: absolute; 27 | top: 0; 28 | right: 0; 29 | height: 100%; 30 | } 31 | 32 | 33 | input[type=text], 34 | input[type=number] { 35 | padding: 5px; 36 | border: 2px solid #ccc; 37 | -webkit-border-radius: 15px; 38 | border-radius: 15px; 39 | } 40 | 41 | input[type=text]:focus, 42 | input[type=number]:focus { 43 | border-color: #333; 44 | } 45 | 46 | input[type=submit], 47 | button { 48 | padding: 10px 25px; 49 | background: #ccc; 50 | border: 0 none; 51 | cursor: pointer; 52 | -webkit-border-radius: 5px; 53 | border-radius: 5px; 54 | } 55 | 56 | input, 57 | textarea { 58 | max-width: 100%; 59 | padding: 5px; 60 | border: 2px solid #ccc; 61 | -webkit-border-radius: 15px; 62 | border-radius: 15px; 63 | } 64 | 65 | #link input { 66 | margin-top: 15vh; 67 | } 68 | 69 | video { 70 | min-width: 100%; 71 | } 72 | 73 | #content-media { 74 | margin: 0 auto; 75 | padding: 5vh 0; 76 | } 77 | 78 | #content-media img { 79 | max-width: 300px; 80 | } 81 | 82 | #user-valid { 83 | font-size: 45px; 84 | } 85 | 86 | select { 87 | display: block; 88 | font-family: sans-serif; 89 | color: #444; 90 | line-height: 1.3; 91 | padding: .6em 1.4em .5em .8em; 92 | width: 100%; 93 | max-width: 100%; 94 | box-sizing: border-box; 95 | margin: 0; 96 | border: 1px solid #aaa; 97 | box-shadow: 0 1px 0 1px rgba(0, 0, 0, .04); 98 | border-radius: .5em; 99 | -moz-appearance: none; 100 | -webkit-appearance: none; 101 | appearance: none; 102 | background-color: #fff; 103 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-6.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), 104 | linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%); 105 | background-repeat: no-repeat, repeat; 106 | background-position: right .7em top 50%, 0 0; 107 | background-size: .65em auto, 100%; 108 | } 109 | 110 | select::-ms-expand { 111 | display: none; 112 | } 113 | 114 | select:hover { 115 | border-color: #888; 116 | } 117 | 118 | select:focus { 119 | border-color: #aaa; 120 | box-shadow: 0 0 1px 3px rgba(59, 153, 252, .7); 121 | box-shadow: 0 0 0 3px -moz-mac-focusring; 122 | color: #222; 123 | outline: none; 124 | } 125 | 126 | select option { 127 | font-weight: normal; 128 | } 129 | 130 | .main { 131 | margin: 0 auto; 132 | max-width: 968px; 133 | padding: 5%; 134 | text-align: center; 135 | animation: ease-in; 136 | min-height: 90vh; 137 | } 138 | 139 | /* Tabs */ 140 | .tab-nav { 141 | margin: 0; 142 | padding: 0; 143 | } 144 | 145 | .tab-link { 146 | margin-right: 5px; 147 | padding: 10px 20px; 148 | color: #fff; 149 | display: inline-block; 150 | background-color: #b3b3b3; 151 | } 152 | 153 | .tab-link:last-child { 154 | margin-right: 0; 155 | } 156 | 157 | .tab-link:hover { 158 | color: #6d6d6d; 159 | } 160 | 161 | .tab-link.is-active { 162 | color: lightcoral; 163 | background-color: #e7e7e7; 164 | } 165 | 166 | /* Tab Content */ 167 | .tab-contents svg { 168 | height: 15px; 169 | width: 15px; 170 | } 171 | 172 | .tab-content { 173 | display: none; 174 | /* background-color: #e7e7e7; */ 175 | } 176 | 177 | .tab-content.is-active { 178 | display: block; 179 | padding: 20px; 180 | } 181 | 182 | .content { 183 | max-width: 968px; 184 | margin: 0 auto; 185 | } 186 | 187 | .login { 188 | position: absolute; 189 | top: 15px; 190 | left: 15px; 191 | text-decoration: none; 192 | color: black; 193 | } 194 | 195 | .voucher { 196 | padding-top: 30px; 197 | } 198 | 199 | .voucher form { 200 | display: flex; 201 | flex-flow: column; 202 | align-items: center; 203 | justify-content: space-around; 204 | height: 33vh; 205 | } 206 | 207 | .voucher form * { 208 | width: 250px; 209 | } 210 | 211 | .admin-login, 212 | #tabs { 213 | margin-top: 50px; 214 | padding: 10px; 215 | } 216 | 217 | .admin-login { 218 | border: 1px solid #444; 219 | } 220 | 221 | .admin-login form, 222 | .admin-create form, 223 | .admin-content form, 224 | .admin-create-many form { 225 | display: flex; 226 | flex-flow: column; 227 | align-items: center; 228 | align-content: center; 229 | justify-content: space-around; 230 | margin: 0 auto; 231 | max-width: 300px; 232 | min-height: 20vh; 233 | } 234 | 235 | .admin-create form { 236 | min-height: 35vh; 237 | } 238 | 239 | .admin-create-many form { 240 | min-height: 45vh; 241 | } 242 | 243 | .admin-content form { 244 | min-height: 100vh; 245 | } 246 | 247 | .voucher-item svg { 248 | width: 12px; 249 | height: 12px; 250 | } 251 | 252 | .voucher-item { 253 | position: relative; 254 | display: flex; 255 | flex-flow: row nowrap; 256 | justify-content: space-between; 257 | align-items: center; 258 | align-content: flex-start; 259 | padding: 15px 0; 260 | } 261 | 262 | .voucher-item-name, 263 | .voucher-item-voucher { 264 | width: 30% 265 | } 266 | 267 | .voucher-item-expires, 268 | .voucher-item-remove, 269 | .voucher-item-renew { 270 | width: 10%; 271 | } 272 | 273 | .voucher-item-voucher { 274 | display: none; 275 | font-family: monospace; 276 | color: #6d6d6d; 277 | } 278 | 279 | .voucher-item-mq { 280 | position: absolute; 281 | left: -20px; 282 | background: #444; 283 | border-radius: 50%; 284 | padding: 0 5px; 285 | color: white; 286 | } 287 | 288 | .voucher-item-macs { 289 | display: none; 290 | } 291 | 292 | .voucher-item-name:hover~.voucher-item-macs { 293 | display: block; 294 | position: absolute; 295 | bottom: -5px; 296 | } 297 | 298 | #voucher-list-button { 299 | position: fixed; 300 | top: 5%; 301 | right: 5%; 302 | border-radius: 25px; 303 | z-index: 999; 304 | } 305 | 306 | #voucher-list-loader { 307 | position: absolute; 308 | top: 50%; 309 | left: 50%; 310 | } 311 | 312 | #error, .error { 313 | margin: 0 auto; 314 | text-align: center; 315 | padding: 10px 0; 316 | background: darkseagreen; 317 | border-radius: 8px; 318 | background: indianred; 319 | } 320 | 321 | .hidden { 322 | position: absolute !important; 323 | top: -9999px !important; 324 | left: -9999px !important; 325 | z-index: -1; 326 | } 327 | 328 | #other-devices { 329 | position: absolute; 330 | top: 30px; 331 | right: 30px; 332 | margin: 0 auto; 333 | border-radius: 25px; 334 | height: 45px; 335 | width: 45px; 336 | display: flex; 337 | flex-flow: row nowrap; 338 | text-align: center; 339 | justify-content: center; 340 | } 341 | 342 | #other-devices span { 343 | font-size: 20px; 344 | position: relative; 345 | right: 10px; 346 | } 347 | 348 | @media only screen and (max-width: 480px) { 349 | .voucher-item-name:hover~.voucher-item-voucher { 350 | display: block; 351 | position: absolute; 352 | top: -5px; 353 | } 354 | } 355 | 356 | @media only screen and (min-width: 480px) { 357 | .tab-contents svg { 358 | height: 32px; 359 | width: 32px; 360 | } 361 | 362 | .voucher-item svg { 363 | width: 20px; 364 | height: 20px; 365 | } 366 | 367 | .voucher-item-voucher { 368 | display: block; 369 | } 370 | } -------------------------------------------------------------------------------- /pirania-app/files/www/portal/css/mobile-icons.css: -------------------------------------------------------------------------------- 1 | .mobile.icon { 2 | color: #000; 3 | position: absolute; 4 | margin-left: 4px; 5 | margin-top: 0px; 6 | width: 12px; 7 | height: 19px; 8 | border-radius: 2px; 9 | border: solid 1px currentColor; 10 | } 11 | 12 | .mobile.icon:before { 13 | content: ''; 14 | position: absolute; 15 | left: 5px; 16 | top: 1px; 17 | width: 2px; 18 | height: 1px; 19 | background-color: currentColor; 20 | } 21 | 22 | .mobile.icon:after { 23 | content: ''; 24 | position: absolute; 25 | bottom: 1px; 26 | left: 5px; 27 | height: 2px; 28 | width: 2px; 29 | border-radius: 50%; 30 | background-color: currentColor; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; 13 | /* 1 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Render the `main` element consistently in IE. 31 | */ 32 | 33 | main { 34 | display: block; 35 | } 36 | 37 | /** 38 | * Correct the font size and margin on `h1` elements within `section` and 39 | * `article` contexts in Chrome, Firefox, and Safari. 40 | */ 41 | 42 | h1 { 43 | font-size: 2em; 44 | margin: 0.67em 0; 45 | } 46 | 47 | /* Grouping content 48 | ========================================================================== */ 49 | 50 | /** 51 | * 1. Add the correct box sizing in Firefox. 52 | * 2. Show the overflow in Edge and IE. 53 | */ 54 | 55 | hr { 56 | box-sizing: content-box; 57 | /* 1 */ 58 | height: 0; 59 | /* 1 */ 60 | overflow: visible; 61 | /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; 71 | /* 1 */ 72 | font-size: 1em; 73 | /* 2 */ 74 | } 75 | 76 | /* Text-level semantics 77 | ========================================================================== */ 78 | 79 | /** 80 | * Remove the gray background on active links in IE 10. 81 | */ 82 | 83 | a { 84 | background-color: transparent; 85 | } 86 | 87 | /** 88 | * 1. Remove the bottom border in Chrome 57- 89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 90 | */ 91 | 92 | abbr[title] { 93 | border-bottom: none; 94 | /* 1 */ 95 | text-decoration: underline; 96 | /* 2 */ 97 | text-decoration: underline dotted; 98 | /* 2 */ 99 | } 100 | 101 | /** 102 | * Add the correct font weight in Chrome, Edge, and Safari. 103 | */ 104 | 105 | b, 106 | strong { 107 | font-weight: bolder; 108 | } 109 | 110 | /** 111 | * 1. Correct the inheritance and scaling of font size in all browsers. 112 | * 2. Correct the odd `em` font sizing in all browsers. 113 | */ 114 | 115 | code, 116 | kbd, 117 | samp { 118 | font-family: monospace, monospace; 119 | /* 1 */ 120 | font-size: 1em; 121 | /* 2 */ 122 | } 123 | 124 | /** 125 | * Add the correct font size in all browsers. 126 | */ 127 | 128 | small { 129 | font-size: 80%; 130 | } 131 | 132 | /** 133 | * Prevent `sub` and `sup` elements from affecting the line height in 134 | * all browsers. 135 | */ 136 | 137 | sub, 138 | sup { 139 | font-size: 75%; 140 | line-height: 0; 141 | position: relative; 142 | vertical-align: baseline; 143 | } 144 | 145 | sub { 146 | bottom: -0.25em; 147 | } 148 | 149 | sup { 150 | top: -0.5em; 151 | } 152 | 153 | /* Embedded content 154 | ========================================================================== */ 155 | 156 | /** 157 | * Remove the border on images inside links in IE 10. 158 | */ 159 | 160 | img { 161 | border-style: none; 162 | } 163 | 164 | /* Forms 165 | ========================================================================== */ 166 | 167 | /** 168 | * 1. Change the font styles in all browsers. 169 | * 2. Remove the margin in Firefox and Safari. 170 | */ 171 | 172 | button, 173 | input, 174 | optgroup, 175 | select, 176 | textarea { 177 | font-family: inherit; 178 | /* 1 */ 179 | font-size: 100%; 180 | /* 1 */ 181 | line-height: 1.15; 182 | /* 1 */ 183 | margin: 0; 184 | /* 2 */ 185 | } 186 | 187 | /** 188 | * Show the overflow in IE. 189 | * 1. Show the overflow in Edge. 190 | */ 191 | 192 | button, 193 | input { 194 | /* 1 */ 195 | overflow: visible; 196 | } 197 | 198 | /** 199 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 200 | * 1. Remove the inheritance of text transform in Firefox. 201 | */ 202 | 203 | button, 204 | select { 205 | /* 1 */ 206 | text-transform: none; 207 | } 208 | 209 | /** 210 | * Correct the inability to style clickable types in iOS and Safari. 211 | */ 212 | 213 | button, 214 | [type="button"], 215 | [type="reset"], 216 | [type="submit"] { 217 | -webkit-appearance: button; 218 | } 219 | 220 | /** 221 | * Remove the inner border and padding in Firefox. 222 | */ 223 | 224 | button::-moz-focus-inner, 225 | [type="button"]::-moz-focus-inner, 226 | [type="reset"]::-moz-focus-inner, 227 | [type="submit"]::-moz-focus-inner { 228 | border-style: none; 229 | padding: 0; 230 | } 231 | 232 | /** 233 | * Restore the focus styles unset by the previous rule. 234 | */ 235 | 236 | button:-moz-focusring, 237 | [type="button"]:-moz-focusring, 238 | [type="reset"]:-moz-focusring, 239 | [type="submit"]:-moz-focusring { 240 | outline: 1px dotted ButtonText; 241 | } 242 | 243 | /** 244 | * Correct the padding in Firefox. 245 | */ 246 | 247 | fieldset { 248 | padding: 0.35em 0.75em 0.625em; 249 | } 250 | 251 | /** 252 | * 1. Correct the text wrapping in Edge and IE. 253 | * 2. Correct the color inheritance from `fieldset` elements in IE. 254 | * 3. Remove the padding so developers are not caught out when they zero out 255 | * `fieldset` elements in all browsers. 256 | */ 257 | 258 | legend { 259 | box-sizing: border-box; 260 | /* 1 */ 261 | color: inherit; 262 | /* 2 */ 263 | display: table; 264 | /* 1 */ 265 | max-width: 100%; 266 | /* 1 */ 267 | padding: 0; 268 | /* 3 */ 269 | white-space: normal; 270 | /* 1 */ 271 | } 272 | 273 | /** 274 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 275 | */ 276 | 277 | progress { 278 | vertical-align: baseline; 279 | } 280 | 281 | /** 282 | * Remove the default vertical scrollbar in IE 10+. 283 | */ 284 | 285 | textarea { 286 | overflow: auto; 287 | } 288 | 289 | /** 290 | * 1. Add the correct box sizing in IE 10. 291 | * 2. Remove the padding in IE 10. 292 | */ 293 | 294 | [type="checkbox"], 295 | [type="radio"] { 296 | box-sizing: border-box; 297 | /* 1 */ 298 | padding: 0; 299 | /* 2 */ 300 | } 301 | 302 | /** 303 | * Correct the cursor style of increment and decrement buttons in Chrome. 304 | */ 305 | 306 | [type="number"]::-webkit-inner-spin-button, 307 | [type="number"]::-webkit-outer-spin-button { 308 | height: auto; 309 | } 310 | 311 | /** 312 | * 1. Correct the odd appearance in Chrome and Safari. 313 | * 2. Correct the outline style in Safari. 314 | */ 315 | 316 | [type="search"] { 317 | -webkit-appearance: textfield; 318 | /* 1 */ 319 | outline-offset: -2px; 320 | /* 2 */ 321 | } 322 | 323 | /** 324 | * Remove the inner padding in Chrome and Safari on macOS. 325 | */ 326 | 327 | [type="search"]::-webkit-search-decoration { 328 | -webkit-appearance: none; 329 | } 330 | 331 | /** 332 | * 1. Correct the inability to style clickable types in iOS and Safari. 333 | * 2. Change font properties to `inherit` in Safari. 334 | */ 335 | 336 | ::-webkit-file-upload-button { 337 | -webkit-appearance: button; 338 | /* 1 */ 339 | font: inherit; 340 | /* 2 */ 341 | } 342 | 343 | /* Interactive 344 | ========================================================================== */ 345 | 346 | /* 347 | * Add the correct display in Edge, IE 10+, and Firefox. 348 | */ 349 | 350 | details { 351 | display: block; 352 | } 353 | 354 | /* 355 | * Add the correct display in all browsers. 356 | */ 357 | 358 | summary { 359 | display: list-item; 360 | } 361 | 362 | /* Misc 363 | ========================================================================== */ 364 | 365 | /** 366 | * Add the correct display in IE 10+. 367 | */ 368 | 369 | template { 370 | display: none; 371 | } 372 | 373 | /** 374 | * Add the correct display in IE 10. 375 | */ 376 | 377 | [hidden] { 378 | display: none; 379 | } -------------------------------------------------------------------------------- /pirania-app/files/www/portal/fail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pirania 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 25 |
26 | 27 | 28 | 29 |

30 |

31 |

32 |
33 |
34 | 35 | 36 | 37 | 38 |
39 |

invalid

40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirect 5 | 6 | 7 | 8 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pirania 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |

19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 85 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/admin.js: -------------------------------------------------------------------------------- 1 | let session = null 2 | let uploadedLogo = null 3 | 4 | var errorElem = document.getElementById('error') 5 | 6 | Date.daysBetween = function( date1, date2 ) { //Get 1 day in milliseconds 7 | var one_day=1000*60*60*24 // Convert both dates to milliseconds 8 | var date1_ms = date1.getTime() 9 | var date2_ms = date2.getTime() // Calculate the difference in milliseconds 10 | var difference_ms = date2_ms - date1_ms // Convert back to days and return 11 | return Math.round(difference_ms/one_day) 12 | } 13 | 14 | 15 | function xDaysFromNow (days) { 16 | let date = new Date() 17 | let newDate = date.setDate(date.getDate() + parseInt(days)) 18 | return newDate.toString() 19 | } 20 | 21 | function makeid(length) { 22 | var text = "" 23 | var possible = "abcdefghijklmnopqrstuvwxyz0123456789" 24 | for (var i = 0; i < length; i++) 25 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 26 | return text 27 | } 28 | 29 | function compress(e) { 30 | const fileName = e.target.files[0].name 31 | const reader = new FileReader() 32 | reader.readAsDataURL(e.target.files[0]) 33 | reader.onload = event => { 34 | const img = new Image() 35 | img.src = event.target.result 36 | img.onload = () => { 37 | const elem = document.createElement('canvas') 38 | const width = 50 39 | const scaleFactor = width / img.width 40 | elem.width = width 41 | elem.height = img.height * scaleFactor 42 | const ctx = elem.getContext('2d') 43 | ctx.drawImage(img, 0, 0, width, img.height * scaleFactor) 44 | ctx.canvas.toBlob((blob) => { 45 | const file = new File([blob], fileName, { 46 | type: 'image/jpeg', 47 | lastModified: Date.now() 48 | }) 49 | }, 'image/jpeg', 1) 50 | console.log(ctx.canvas.toDataURL("image/jpeg")) 51 | uploadedLogo = ctx.canvas.toDataURL("image/jpeg") 52 | document.getElementById("logo-upload").appendChild(elem) 53 | }, 54 | reader.onerror = error => console.log(error) 55 | 56 | } 57 | } 58 | 59 | function createManyVouchers () { 60 | const createLoader = document.getElementById('voucher-create-loader') 61 | const key = document.getElementById('adminManyInputKey').value 62 | const days = document.getElementById('adminManyInputDays').value 63 | const numberVouchers = document.getElementById('adminManyInputVouchers').value 64 | const epoc = xDaysFromNow(days) 65 | const vouchers = [] 66 | for (let index = 0; index < numberVouchers; index++) { 67 | vouchers.push({ 68 | key: `${key}-${makeid(3)}`, 69 | voucher: makeid(8), 70 | epoc, 71 | }) 72 | } 73 | console.log('VOUCHERS GO', JSON.stringify(vouchers)) 74 | hide(errorElem) 75 | show(createLoader) 76 | ubusFetch('pirania', 'add_many_vouchers', { vouchers }, session) 77 | .then(res => { 78 | console.log('RESPONSE', res) 79 | if (res.success) { 80 | listVouchers() 81 | console.log('VOUCHERS SUCCESS', res.success) 82 | document.getElementById('adminManyInputKey').value = '' 83 | document.getElementById('adminManyInputDays').value = 1 84 | document.getElementById('adminManyInputVouchers').value = 1 85 | document.getElementById('many-result').innerHTML = 'Sucesso!' 86 | } 87 | }) 88 | .catch(err => { 89 | console.log('Can find this error ', err) 90 | listVouchers() 91 | document.getElementById('adminManyInputKey').value = '' 92 | document.getElementById('adminManyInputDays').value = 1 93 | document.getElementById('adminManyInputVouchers').value = 1 94 | document.getElementById('many-result').innerHTML = 'Sucesso!' 95 | // show(errorElem) 96 | hide(createLoader) 97 | }) 98 | } 99 | 100 | function removeVoucher (name) { 101 | ubusFetch('pirania', 'remove_voucher', { name }, session) 102 | .then(res => { 103 | console.log(res) 104 | listVouchers() 105 | }) 106 | .catch(err => { 107 | console.log(err) 108 | show(errorElem) 109 | }) 110 | } 111 | 112 | /* Needs to be implemented in shared-state */ 113 | // function renewVoucher (name) { 114 | // console.log(name, xDaysFromNow(30)) 115 | // ubusFetch('pirania', 'renew_voucher', { name, date: xDaysFromNow(30) }, session) 116 | // .then(res => console.log(res)) 117 | // .catch(err => { 118 | // console.log(err) 119 | // show(errorElem) 120 | // }) 121 | // } 122 | 123 | function getVoucherName (name) { 124 | return name.split('-')[0] 125 | } 126 | 127 | function listVouchers () { 128 | let voucherList = document.getElementById("voucher-list") 129 | let listLoader = document.getElementById('voucher-list-loader') 130 | hide(errorElem) 131 | voucherList.innerHTML = '' 132 | show(listLoader) 133 | ubusFetch('pirania', 'list_vouchers', {}, session) 134 | .then(res => { 135 | const vouchers = res.vouchers 136 | // document.getElementById('voucher-list-button').style.display = 'none' 137 | vouchers 138 | .sort((a, b) => { 139 | if(parseInt(a.expires) > parseInt(b.expires)) { return -1; } 140 | if(parseInt(a.expires) < parseInt(b.expires)) { return 1; } 141 | return 0; 142 | }) 143 | .sort((a, b) => { 144 | if(getVoucherName(a.name) < getVoucherName(b.name)) { return -1; } 145 | if(getVoucherName(a.name) > getVoucherName(b.name)) { return 1; } 146 | return 0; 147 | }) 148 | .map(v => { 149 | const date = new Date (parseInt(v.expires)) 150 | const dateDiff = Date.daysBetween(new Date(), date) 151 | let container = document.createElement('div') 152 | container.className = 'voucher-item' 153 | let vQuantity = document.createElement('div') 154 | vQuantity.className = 'voucher-item-mq' 155 | vQuantity.innerHTML = v.macs.length > 0 ? v.macs.length : '' 156 | container.appendChild(vQuantity) 157 | let name = document.createElement('div') 158 | name.className = 'voucher-item-name' 159 | name.innerHTML = v.name 160 | container.appendChild(name) 161 | let voucher = document.createElement('div') 162 | voucher.className = 'voucher-item-voucher' 163 | voucher.innerHTML = v.voucher 164 | container.appendChild(voucher) 165 | /* Needs to be implemented in shared-state */ 166 | // let renew = document.createElementNS("http://www.w3.org/2000/svg", "svg") 167 | // renew.setAttribute ("viewBox", "0 0 32 32" ) 168 | // renew.setAttribute ("stroke", "currentcolor" ) 169 | // renew.setAttribute ("stroke-linecap", "round" ) 170 | // renew.setAttribute ("stroke-linejoin", "round" ) 171 | // renew.setAttribute ("stroke-width", "2" ) 172 | // renew.setAttribute ("fill", "none" ) 173 | // renew.className = 'voucher-item-renew' 174 | // renew.onclick = () => renewVoucher(v.name) 175 | // let renewPath = document.createElementNS("http://www.w3.org/2000/svg", "path") 176 | // renewPath.setAttribute('d', 'M29 16 C29 22 24 29 16 29 8 29 3 22 3 16 3 10 8 3 16 3 21 3 25 6 27 9 M20 10 L27 9 28 2') 177 | // renew.appendChild(renewPath) 178 | // container.appendChild(renew) 179 | let remove = document.createElementNS("http://www.w3.org/2000/svg", "svg") 180 | remove.setAttribute ("viewBox", "0 0 32 32" ) 181 | remove.setAttribute ("stroke", "currentcolor" ) 182 | remove.setAttribute ("stroke-linecap", "round" ) 183 | remove.setAttribute ("stroke-linejoin", "round" ) 184 | remove.setAttribute ("stroke-width", "2" ) 185 | remove.setAttribute ("fill", "none" ) 186 | remove.className = 'voucher-item-remove' 187 | remove.onclick = () => removeVoucher(v.name) 188 | let removePath = document.createElementNS("http://www.w3.org/2000/svg", "path") 189 | removePath.setAttribute('d', 'M28 6 L6 6 8 30 24 30 26 6 4 6 M16 12 L16 24 M21 12 L20 24 M11 12 L12 24 M12 6 L13 2 19 2 20 6') 190 | remove.appendChild(removePath) 191 | container.appendChild(remove) 192 | let expires = document.createElement('div') 193 | expires.className = 'voucher-item-expires' 194 | expires.innerHTML = dateDiff > 0 ? dateDiff +' '+ int[lang].days : 0 195 | container.appendChild(expires) 196 | let macs = document.createElement('div') 197 | macs.innerHTML = v.macs.toString() 198 | macs.className = 'voucher-item-macs' 199 | listLoader.className = 'hidden' 200 | container.appendChild(macs) 201 | voucherList.appendChild(container) 202 | document.getElementById('voucher-list-button').classList.remove('hidden') 203 | }) 204 | }) 205 | .catch(err => { 206 | console.log(err) 207 | hide(listLoader) 208 | show(document.getElementById('voucher-list-button')) 209 | errorElem.innerHTML = int[lang].error 210 | show(errorElem) 211 | }) 212 | } 213 | 214 | function updateContent () { 215 | const backgroundColor = document.getElementById('adminInputBackground').value 216 | const title = document.getElementById('adminInputTitle').value 217 | const welcome = document.getElementById('adminInputWelcome').value 218 | const body = document.getElementById('adminInputBody').value 219 | let logo = uploadedLogo || content.logo 220 | ubusFetch( 221 | 'pirania-app', 222 | 'write_content', 223 | { 224 | backgroundColor, 225 | title, 226 | welcome, 227 | body, 228 | logo, 229 | }, 230 | session 231 | ) 232 | .then(res => { 233 | content = res 234 | const { backgroundColor, title, welcome, body, logo } = content 235 | document.body.style.backgroundColor = backgroundColor 236 | const contentLogo = document.getElementById('content-logo') 237 | const contentTitle = document.getElementById('content-title') 238 | const contentWelcome = document.getElementById('content-welcome') 239 | const contentBody = document.getElementById('content-body') 240 | if (contentLogo) contentLogo.src = logo 241 | if (contentTitle) contentTitle.innerHTML = title 242 | if (contentWelcome) contentWelcome.innerHTML = welcome 243 | if (contentBody) contentBody.innerHTML = body 244 | }) 245 | .catch(err => { 246 | errorElem.innerHTML = int[lang].error 247 | show(errorElem) 248 | }) 249 | } 250 | 251 | function adminAuth () { 252 | const password = document.getElementById('adminInput').value 253 | ubusFetch( 254 | 'session', 255 | 'login', 256 | { 257 | username: 'root', 258 | password, 259 | timeout: 5000, 260 | } 261 | ) 262 | .then(res => { 263 | hide(errorElem) 264 | session = res.ubus_rpc_session 265 | document.querySelector('.admin-login').style.display = 'none' 266 | document.querySelector('#tabs').classList.remove('hidden') 267 | const adminContent = document.querySelector('.admin-content') 268 | const { backgroundColor, title, welcome, body } = content 269 | document.getElementById('adminInputTitle').value = title 270 | document.getElementById('adminInputWelcome').value = welcome 271 | document.getElementById('adminInputBody').value = body 272 | document.getElementById('adminInputBackground').value = backgroundColor 273 | listVouchers() 274 | }) 275 | .catch(err => { 276 | console.log(err) 277 | show(errorElem) 278 | errorElem.innerHTML = int[lang].wrongPassword 279 | }) 280 | } 281 | 282 | document.getElementById("logo-file").addEventListener("change", function (event) { 283 | compress(event) 284 | }) -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/content.js: -------------------------------------------------------------------------------- 1 | let loader = document.createElement('div') 2 | loader.className = 'lds-ring' 3 | loader.appendChild(document.createElement('div')) 4 | loader.appendChild(document.createElement('div')) 5 | loader.appendChild(document.createElement('div')) 6 | loader.appendChild(document.createElement('div')) 7 | 8 | const show = elem => elem.classList.remove('hidden') 9 | const hide = elem => (elem.className += ' hidden') 10 | 11 | var nojsElem = document.getElementById('nojs') 12 | if (nojsElem) { 13 | nojsElem.setAttribute('value', false) 14 | } 15 | 16 | var param = '?prev=' 17 | var prevUrl = window.location.search.split(param)[1] 18 | if (prevUrl) { 19 | var prevElem = document.createElement('input') 20 | prevElem.setAttribute('value', prevUrl) 21 | prevElem.setAttribute('id', 'prev') 22 | prevElem.setAttribute('name', 'prev') 23 | document.getElementById('voucher').appendChild(prevElem) 24 | } 25 | 26 | let content = { 27 | backgroundColor: 'white', 28 | title: '', 29 | welcome: '', 30 | body: '', 31 | logo: '', 32 | rules: '', 33 | mediaUrl: '' 34 | } 35 | 36 | function getContent () { 37 | ubusFetch('pirania-app', 'read_content') 38 | .then(res => { 39 | content = res 40 | const { backgroundColor, title, welcome, body, logo, rules, mediaUrl } = content 41 | document.body.style.backgroundColor = backgroundColor 42 | const contentLogo = document.getElementById('content-logo') 43 | const contentTitle = document.getElementById('content-title') 44 | const contentWelcome = document.getElementById('content-welcome') 45 | const contentBody = document.getElementById('content-body') 46 | const contentRules = document.getElementById('content-rules') 47 | const contentMedia = document.getElementById('content-media') 48 | 49 | if (contentLogo) { 50 | show(contentLogo) 51 | contentLogo.src = logo 52 | } 53 | if (contentTitle) contentTitle.innerHTML = title 54 | if (contentWelcome) contentWelcome.innerHTML = welcome 55 | if (contentBody) contentBody.innerHTML = body 56 | if (contentRules) contentRules.innerHTML = rules 57 | if (contentMedia) { 58 | var mediaType = mediaUrl.split('.')[mediaUrl.split('.').length -1] 59 | if (mediaType === 'mp4' || mediaType === 'webm' || mediaType === 'avi') { 60 | var videoContainerElem = document.createElement('video') 61 | var videoElem = document.createElement('source') 62 | contentMedia.append(videoContainerElem) 63 | videoElem.setAttribute('src', mediaUrl) 64 | videoElem.setAttribute('type', 'video/'+mediaType) 65 | videoContainerElem.appendChild(videoElem) 66 | } else if (mediaType === 'jpg' || mediaType === 'png' || mediaType === 'jpeg' || mediaType === 'gif' || mediaType === 'svg') { 67 | var imageElem = document.createElement('img') 68 | contentMedia.append(imageElem) 69 | imageElem.setAttribute('src', mediaUrl) 70 | imageElem.setAttribute('type', 'image/'+mediaType) 71 | } 72 | } 73 | }) 74 | .catch(err => { 75 | document.getElementById('error').innerHTML = int[lang].error 76 | }) 77 | } 78 | 79 | getContent() 80 | -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/int.js: -------------------------------------------------------------------------------- 1 | const userLang = navigator.language || navigator.userLanguage 2 | const lang = userLang.split('-')[0] || userLang || 'en' 3 | 4 | const int = { 5 | pt: { 6 | selectVoucher: 'Entre o voucher', 7 | createNewVoucher: 'Criar novo voucher', 8 | createManyVouchers: 'Criar muitos vouchers', 9 | changeContent: 'Mudar o conteúdo', 10 | title: 'Título', 11 | welcome: 'Bem vindo', 12 | body: 'Texto principal', 13 | backgroundColor: 'Cor de fundo', 14 | rules: 'Regras da rede', 15 | listVouchers: 'Listar vouchers', 16 | success: 'Sucesso', 17 | error: 'Erro', 18 | invalid: 'Código incorreto', 19 | wrongPassword: 'Senha incorreta', 20 | name: 'Nome', 21 | days: 'Dias', 22 | numberOfVouchers: 'Número de vouchers', 23 | authenticated: 'Seu dispositivo está autenticado', 24 | wait: 'Aguarde', 25 | continue: 'Continuar', 26 | info: 'Mais informações', 27 | }, 28 | es: { 29 | selectVoucher: 'Entre el voucher', 30 | createNewVoucher: 'Crear nuevo voucher', 31 | createManyVouchers: 'Crear muchos vouchers', 32 | changeContent: 'Cambiar el contenido', 33 | title: 'Título', 34 | welcome: 'Bienvenido', 35 | body: 'Texto principal', 36 | backgroundColor: 'Color de fondo', 37 | rules: 'Reglas de la rede', 38 | listVouchers: 'Listar vouchers', 39 | success: 'Sucesso', 40 | error: 'Erro', 41 | invalid: 'Código incorrecto', 42 | wrongPassword: 'Contraseña incorrecta', 43 | name: 'Nombre', 44 | days: 'Dias', 45 | numberOfVouchers: 'Cantidad de vouchers', 46 | authenticated: 'Tu dispositivo esta autenticado', 47 | wait: 'Espere', 48 | continue: 'Seguir', 49 | info: 'Mas informaciones', 50 | }, 51 | en: { 52 | selectVoucher: 'Enter a voucher', 53 | createNewVoucher: 'Create new voucher', 54 | createManyVouchers: 'Create many vouchers', 55 | changeContent: 'Change content', 56 | title: 'Title', 57 | welcome: 'Welcome text', 58 | body: 'Main text', 59 | backgroundColor: 'Background color', 60 | rules: 'Network rules', 61 | listVouchers: 'List vouchers', 62 | success: 'Success', 63 | error: 'Error', 64 | invalid: 'Invalid voucher', 65 | wrongPassword: 'Wrong password', 66 | name: 'Nome', 67 | days: 'Dias', 68 | numberOfVouchers: 'Number of vouchers', 69 | authenticated: "You're device is authenticated!", 70 | wait: 'Wait', 71 | continue: 'Continue', 72 | info: 'More information', 73 | } 74 | } 75 | 76 | Object.keys(int[lang]).map(text => { 77 | Array.from(document.getElementsByClassName(`int-${text}`)).map( 78 | element => { 79 | if (element.tagName === 'INPUT') { 80 | element.value = int[lang][text] 81 | } else { 82 | element.innerHTML = int[lang][text] 83 | } 84 | } 85 | ) 86 | }) -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/tabs.js: -------------------------------------------------------------------------------- 1 | // Start tabs.js 2 | (function() { 3 | 4 | 'use strict'; 5 | 6 | /** 7 | * tabs 8 | * 9 | * @description The Tabs component. 10 | * @param {Object} options The options hash 11 | */ 12 | var tabs = function(options) { 13 | 14 | var el = document.querySelector(options.el); 15 | var tabNavigationLinks = el.querySelectorAll(options.tabNavigationLinks); 16 | var tabContentContainers = el.querySelectorAll(options.tabContentContainers); 17 | var activeIndex = 0; 18 | var initCalled = false; 19 | 20 | /** 21 | * init 22 | * 23 | * @description Initializes the component by removing the no-js class from 24 | * the component, and attaching event listeners to each of the nav items. 25 | * Returns nothing. 26 | */ 27 | var init = function() { 28 | if (!initCalled) { 29 | initCalled = true; 30 | el.classList.remove('no-js'); 31 | 32 | for (var i = 0; i < tabNavigationLinks.length; i++) { 33 | var link = tabNavigationLinks[i]; 34 | handleClick(link, i); 35 | } 36 | } 37 | }; 38 | 39 | /** 40 | * handleClick 41 | * 42 | * @description Handles click event listeners on each of the links in the 43 | * tab navigation. Returns nothing. 44 | * @param {HTMLElement} link The link to listen for events on 45 | * @param {Number} index The index of that link 46 | */ 47 | var handleClick = function(link, index) { 48 | link.addEventListener('click', function(e) { 49 | e.preventDefault(); 50 | goToTab(index); 51 | }); 52 | }; 53 | 54 | /** 55 | * goToTab 56 | * 57 | * @description Goes to a specific tab based on index. Returns nothing. 58 | * @param {Number} index The index of the tab to go to 59 | */ 60 | var goToTab = function(index) { 61 | if (index !== activeIndex && index >= 0 && index <= tabNavigationLinks.length) { 62 | tabNavigationLinks[activeIndex].classList.remove('is-active'); 63 | tabNavigationLinks[index].classList.add('is-active'); 64 | tabContentContainers[activeIndex].classList.remove('is-active'); 65 | tabContentContainers[index].classList.add('is-active'); 66 | activeIndex = index; 67 | } 68 | }; 69 | 70 | /** 71 | * Returns init and goToTab 72 | */ 73 | return { 74 | init: init, 75 | goToTab: goToTab 76 | }; 77 | 78 | }; 79 | 80 | /** 81 | * Attach to global namespace 82 | */ 83 | window.tabs = tabs; 84 | 85 | })(); 86 | // End tabs.js 87 | 88 | // Initialise at bottom of HTML 89 | var myTabs = tabs({ 90 | el: '#tabs', 91 | tabNavigationLinks: '.tab-link', 92 | tabContentContainers: '.tab-content' 93 | }); 94 | 95 | myTabs.init(); -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/ubusFetch.js: -------------------------------------------------------------------------------- 1 | const url = 'http://thisnode.info/ubus' 2 | let ubusError = false 3 | 4 | function parseJSON(response) { 5 | return response.json() 6 | } 7 | 8 | const ubusFetch = (call, action, params, session) => new Promise ((resolve, reject) => { 9 | const form = { 10 | id: 99, 11 | jsonrpc: '2.0', 12 | method: 'call', 13 | params:[ 14 | session || '00000000000000000000000000000000', 15 | call, 16 | action, 17 | params || {}, 18 | ] 19 | } 20 | fetch(url, { 21 | method: 'POST', 22 | body: JSON.stringify(form), 23 | headers: { 24 | 'Access-Control-Allow-Origin': 'http://thisnode.info' 25 | }, 26 | }) 27 | .then(parseJSON) 28 | .then((res) => { 29 | if (res && res.result[1]) { 30 | resolve(res.result[1]) 31 | } else { 32 | ubusError = true 33 | reject(int[lang].error) 34 | } 35 | }) 36 | .catch((err) => { 37 | console.log('Ubus error ', err) 38 | ubusError = true 39 | reject(int[lang].error) 40 | }) 41 | }) -------------------------------------------------------------------------------- /pirania-app/files/www/portal/js/voucher.js: -------------------------------------------------------------------------------- 1 | var validMacs = [] 2 | var userIp = null 3 | var userMac = null 4 | var userIsValid = null 5 | 6 | var voucherButton = document.getElementById('voucherInput-submit') 7 | let voucherElem = document.getElementById('voucher') 8 | 9 | const validMacsForm = { 10 | id: 99, 11 | jsonrpc: '2.0', 12 | method: 'call', 13 | params: [ 14 | '00000000000000000000000000000000', 15 | 'pirania', 16 | 'print_valid_macs', 17 | {} 18 | ] 19 | } 20 | 21 | const validGetClients = { 22 | id: 99, 23 | jsonrpc: '2.0', 24 | method: 'call', 25 | params: [ 26 | '00000000000000000000000000000000', 27 | 'pirania-app', 28 | 'get_clients', 29 | {} 30 | ] 31 | } 32 | 33 | async function loadAsyncData () { 34 | await getIp() 35 | await getValidClients() 36 | await getValidMacs() 37 | } 38 | 39 | function init () { 40 | console.log('Welcome to Pirania!') 41 | // Add responses 42 | var error = document.createElement('h4') 43 | var result = document.createElement('p') 44 | var form = document.getElementsByClassName('voucher')[0] 45 | form.append(error) 46 | form.append(result) 47 | error.setAttribute('id', 'error') 48 | result.setAttribute('id', 'result') 49 | error.className = 'hidden' 50 | result.className = 'hidden' 51 | 52 | // Add list 53 | var deviceList = document.createElement('button') 54 | var add = document.createElement('span') 55 | var icon = document.createElement('div') 56 | document.body.appendChild(deviceList) 57 | deviceList.appendChild(add) 58 | deviceList.appendChild(icon) 59 | icon.className = 'mobile icon' 60 | add.innerHTML = '+' 61 | deviceList.setAttribute('id', 'other-devices') 62 | deviceList.addEventListener('click', async function (e) { 63 | e.preventDefault() 64 | showingList = !showingList 65 | if (showingList) { 66 | show(stationList) 67 | show(voucherButton) 68 | show(voucherElem) 69 | deviceList.style.backgroundColor = '#A593E0' 70 | } else { 71 | hide(stationList) 72 | deviceList.style.backgroundColor = '' 73 | } 74 | await loadAsyncData() 75 | }) 76 | } 77 | 78 | init() 79 | 80 | var showingList = false 81 | var stationList = document.getElementById('station-list') 82 | var errorElem = document.getElementById('error') 83 | var resultElem = document.getElementById('result') 84 | 85 | function prepareResult (res) { 86 | if (res.error) { 87 | console.log(res.error) 88 | errorElem.innerHTML = res.error 89 | show(errorElem) 90 | ubusError = true 91 | } else if (res && res.result[1]) return res.result[1] 92 | else return false 93 | } 94 | 95 | function authVoucher () { 96 | if (!userMac) return 97 | let mac 98 | if (showingList) { 99 | mac = document.getElementById('stations').value || userMac 100 | } else { 101 | mac = userMac 102 | } 103 | let voucher = voucherElem.value.toLowerCase() 104 | voucherElem.after(loader) 105 | show(loader) 106 | const authVoucherForm = { 107 | id: 99, 108 | jsonrpc: '2.0', 109 | method: 'call', 110 | params: [ 111 | '00000000000000000000000000000000', 112 | 'pirania', 113 | 'auth_voucher', 114 | { 115 | voucher, 116 | mac 117 | } 118 | ] 119 | } 120 | fetch(url, { 121 | method: 'POST', 122 | body: JSON.stringify(authVoucherForm), 123 | headers: { 124 | 'Access-Control-Allow-Origin': 'http://thisnode.info' 125 | } 126 | }) 127 | .then(parseJSON) 128 | .then(prepareResult) 129 | .then(res => { 130 | hide(loader) 131 | show(voucherButton) 132 | if (res && res.success) { 133 | result.innerHTML = int[lang].success 134 | show(result) 135 | hide(errorElem) 136 | loadAsyncData() 137 | } else if (res && !res.success) { 138 | errorElem.innerHTML = int[lang].invalid 139 | show(errorElem) 140 | } 141 | voucherElem.value = '' 142 | }) 143 | .catch(err => { 144 | console.log('UBUS error:', err) 145 | errorElem.innerHTML = err 146 | show(errorElem) 147 | ubusError = true 148 | }) 149 | } 150 | 151 | function getIp () { 152 | return fetch('/cgi-bin/pirania/client_ip', { 153 | headers: { 154 | 'Access-Control-Allow-Origin': 'http://thisnode.info' 155 | } 156 | }) 157 | .then(async i => { 158 | const res = await i.json() 159 | userIp = res.ip 160 | userMac = res.mac 161 | userIsValid = res.valid 162 | }) 163 | .catch(err => { 164 | console.log('Error fetching mac:', err) 165 | ubusError = true 166 | }) 167 | } 168 | 169 | function getValidClients () { 170 | if (!ubusError) { 171 | const myDiv = document.getElementById('station-list') 172 | const exists = document.getElementById('stations') 173 | if (!exists) { 174 | const select = document.createElement('select') 175 | select.id = 'stations' 176 | myDiv.appendChild(select) 177 | } 178 | } 179 | return fetch(url, { 180 | method: 'POST', 181 | body: JSON.stringify(validGetClients), 182 | headers: { 183 | 'Access-Control-Allow-Origin': 'http://thisnode.info' 184 | } 185 | }) 186 | .then(parseJSON) 187 | .then(prepareResult) 188 | .then(res => { 189 | if (res && !ubusError) { 190 | document.getElementById('stations').innerHTML = '' 191 | res.clients.map(i => { 192 | const valid = validMacs.filter(valid => i.mac === valid).length > 0 193 | const node = document.createElement('option') 194 | let textnode = document.createTextNode('') 195 | if (userIp === i.ip) { 196 | userMac = i.mac 197 | node.selected = true 198 | } 199 | const isIp = userIp === i.ip ? '📱 ' : '' 200 | textnode.nodeValue = valid 201 | ? isIp + i.station + ' ✅' 202 | : isIp + i.station 203 | node.value = i.mac 204 | node.appendChild(textnode) 205 | return document.getElementById('stations').appendChild(node) 206 | }) 207 | } 208 | }) 209 | .catch(err => { 210 | console.log(int[lang].error, err) 211 | errorElem.innerHTML = int[lang].error 212 | show(errorElem) 213 | ubusError = true 214 | }) 215 | } 216 | 217 | function getValidMacs () { 218 | fetch(url, { 219 | method: 'POST', 220 | body: JSON.stringify(validMacsForm), 221 | headers: { 222 | 'Access-Control-Allow-Origin': 'http://thisnode.info' 223 | } 224 | }) 225 | .then(parseJSON) 226 | .then(res => { 227 | if (res && res.result[1]) { 228 | validMacs = res.result[1].macs 229 | if (validMacs.length > 0) { 230 | getValidClients() 231 | } 232 | } else if (res.error) { 233 | console.log(res.error) 234 | errorElem.innerHTML = int[lang].error 235 | ubusError = true 236 | } 237 | }) 238 | .catch(err => { 239 | console.log(int[lang].error, err) 240 | errorElem.innerHTML = int[lang].error 241 | ubusError = true 242 | }) 243 | } 244 | 245 | voucherButton.addEventListener('click', function (e) { 246 | hide(voucherButton) 247 | show(loader) 248 | if (showingList) { 249 | e.preventDefault() 250 | authVoucher() 251 | } 252 | }) 253 | -------------------------------------------------------------------------------- /pirania/files/etc/config/pirania: -------------------------------------------------------------------------------- 1 | config base_config 'base_config' 2 | option enabled '0' 3 | option portal_url 'http://thisnode.info/portal/' 4 | option url_auth '/portal/auth.html' 5 | option url_authenticated '/portal/authenticated.html' 6 | option url_info '/portal/info.html' 7 | option url_fail '/portal/fail.html' 8 | option db_path '/etc/pirania/db.csv' 9 | option elections_path '/etc/pirania/elections.json' 10 | option hooks_path '/etc/pirania/hooks/' 11 | option uploadlimit '10000000' 12 | option downloadlimit '50000' 13 | option append_ipt_rules '0' # if set to 1, iptables rules will be Appended instead of Inserted 14 | list whitelist_ipv4 '10.0.0.0/8' 15 | list whitelist_ipv4 '172.16.0.0/12' 16 | list whitelist_ipv4 '192.168.0.0/16' 17 | list whitelist_ipv6 '2a00:1508:0a00::/40' 18 | list catch_interfaces 'br-lan' 19 | list catch_interfaces 'anygw' 20 | -------------------------------------------------------------------------------- /pirania/files/etc/init.d/pirania: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | NAME=pirania 5 | PROG=/usr/bin/captive-portal 6 | 7 | start() { 8 | if [ $(uci get pirania.base_config.enabled) == 1 ]; then 9 | logger -t pirania "Running portal captive" 10 | $PROG 11 | /usr/lib/lua/voucher/hooks.lua start 12 | else 13 | logger -t pirania "Portal captive is disabled" 14 | fi 15 | } 16 | 17 | stop () { 18 | /usr/lib/lua/voucher/hooks.lua stop 19 | $PROG stop 20 | logger -t pirania "Portal captive stopped" 21 | } 22 | -------------------------------------------------------------------------------- /pirania/files/etc/init.d/pirania-dnsmasq: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=50 4 | 5 | USE_PROCD=1 6 | 7 | DNSMASQ_BIN="/usr/sbin/dnsmasq" 8 | 9 | start_service() { 10 | procd_open_instance 11 | procd_set_param command $DNSMASQ_BIN \ 12 | --keep-in-foreground \ 13 | --port=59053 \ 14 | --no-resolv \ 15 | --server=/$(uci get dhcp.@dnsmasq[0].domain)/$(uci get network.lm_net_br_lan_anygw_if.ipaddr) \ 16 | --addn-hosts=/var/hosts/shared-state-dnsmasq_hosts \ 17 | --address=/thisnode.info/$(uci get network.lm_net_br_lan_anygw_if.ipaddr) \ 18 | --address=/\#/1.2.3.4 19 | 20 | # respawn automatically if something died, be careful if you have an alternative process supervisor 21 | # if process dies sooner than respawn_threshold, it is considered crashed and after 5 retries the service is stopped 22 | procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} 23 | 24 | procd_set_param stderr 1 # forward stderr of the command to logd 25 | procd_close_instance 26 | } -------------------------------------------------------------------------------- /pirania/files/etc/init.d/pirania-uhttpd: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=50 4 | 5 | USE_PROCD=1 6 | 7 | UHTTPD_BIN="/usr/sbin/uhttpd" 8 | 9 | start_service() { 10 | procd_open_instance 11 | procd_set_param command $UHTTPD_BIN -f -h /www/pirania-redirect/ -E / -l / -L /www/pirania-redirect/redirect -n 20 -p 59080 12 | # respawn automatically if something died, be careful if you have an alternative process supervisor 13 | # if process dies sooner than respawn_threshold, it is considered crashed and after 5 retries the service is stopped 14 | procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} 15 | 16 | procd_set_param stderr 1 # forward stderr of the command to logd 17 | procd_close_instance 18 | } 19 | -------------------------------------------------------------------------------- /pirania/files/etc/pirania/db.csv: -------------------------------------------------------------------------------- 1 | key,voucher,expiretime,uploadlimit,downloadlimit,amountofmacsallowed,usedmacs, 2 | -------------------------------------------------------------------------------- /pirania/files/usr/bin/captive-portal: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # requires ip6tables-mod-nat and ipset 3 | 4 | clean_tables () { 5 | echo "Cleaning captive-portal rules" 6 | for ipvX in ipv4 ipv6 ; do 7 | if [ "$ipvX" = "ipv4" ] ; then 8 | iptables=iptables 9 | family=inet 10 | ipaddr=ipaddr 11 | else 12 | iptables=ip6tables 13 | family=inet6 14 | ipaddr=ip6addr 15 | fi 16 | 17 | ### Cleanup 18 | for interface in br-lan anygw; do 19 | $iptables -t mangle -D PREROUTING -i $interface -j pirania 20 | done 21 | 22 | $iptables -t nat -D PREROUTING -j pirania 23 | $iptables -t filter -D FORWARD -j pirania 24 | 25 | for table in mangle nat filter; do 26 | $iptables -t $table -F pirania 27 | $iptables -t $table -X pirania 28 | done 29 | done 30 | } 31 | 32 | clean_sets () { 33 | ipset flush pirania-auth-macs 34 | for ipvX in ipv4 ipv6 ; do 35 | ipset flush pirania-whitelist-$ipvX 36 | done 37 | } 38 | 39 | set_iptables () { 40 | echo "Apply captive-portal rules" 41 | 42 | append_ipt_rules=$(uci get pirania.base_config.append_ipt_rules 2> /dev/null) 43 | if [ "$append_ipt_rules" = "1" ] ; then 44 | AorI="A" 45 | else 46 | AorI="I" 47 | fi 48 | 49 | for ipvX in ipv4 ipv6 ; do 50 | if [ "$ipvX" = "ipv4" ] ; then 51 | iptables=iptables 52 | family=inet 53 | ipaddr=ipaddr 54 | anygw=$(uci get network.lm_net_br_lan_anygw_if.ipaddr) 55 | else 56 | iptables=ip6tables 57 | family=inet6 58 | ipaddr=ip6addr 59 | anygw=[$(uci get network.lan.ip6addr | cut -d/ -f1)] 60 | fi 61 | 62 | ### Buildup 63 | for table in mangle nat filter; do 64 | $iptables -t $table -N pirania 65 | done 66 | 67 | $iptables -t nat -$AorI PREROUTING -j pirania 68 | $iptables -t filter -$AorI FORWARD -j pirania 69 | 70 | for interface in $(uci get pirania.base_config.catch_interfaces); do 71 | $iptables -t mangle -$AorI PREROUTING -i $interface -j pirania 72 | done 73 | 74 | $iptables -t nat -A pirania -p udp -m set ! --match-set pirania-whitelist-$ipvX src -m set ! --match-set pirania-auth-macs src --dport 53 -j DNAT --to-destination $anygw:59053 75 | 76 | $iptables -t mangle -A pirania -m set --match-set pirania-auth-macs src -j RETURN 77 | $iptables -t mangle -A pirania -m set --match-set pirania-whitelist-$ipvX dst -j RETURN 78 | $iptables -t mangle -A pirania -j MARK --set-mark 0x66/0xff # everything not auth nor whitelisted will be marked for REJECT 79 | $iptables -t mangle -A pirania -p tcp -m tcp --dport 80 -j MARK --set-mark 0x80/0xff # unless is dport 80, re-set mark for REDIRECT 80 | 81 | $iptables -t nat -A pirania -p tcp -m tcp -m mark --mark 0x80/0xff -j REDIRECT --to-ports 59080 82 | $iptables -t filter -A pirania -p tcp -m mark --mark 0x66/0xff -j REJECT --reject-with tcp-reset 83 | $iptables -t filter -A pirania -m mark --mark 0x66/0xff -j REJECT 84 | done 85 | } 86 | 87 | set_ipsets () { 88 | ipset -exist create pirania-auth-macs hash:mac timeout 0 89 | for mac in $(voucher print_valid_macs) ; do 90 | ipset -exist add pirania-auth-macs $mac 91 | done 92 | for ipvX in ipv4 ipv6 ; do 93 | if [ "$ipvX" = "ipv4" ] ; then 94 | family=inet 95 | else 96 | family=inet6 97 | fi 98 | ipset -exist create pirania-whitelist-$ipvX hash:net family $family 99 | for item in $(uci get pirania.base_config.whitelist_$ipvX); do 100 | ipset -exist add pirania-whitelist-$ipvX $item 101 | done 102 | done 103 | } 104 | 105 | # check if captive-portal is enabled in /etc/config/pirania 106 | enabled=$(uci get pirania.base_config.enabled) 107 | 108 | if [ "$1" = "start" ]; then 109 | echo "Running captive-portal" 110 | clean_tables 111 | clean_sets 112 | set_ipsets 113 | set_iptables 114 | exit 115 | elif [ "$1" = "update" ] ; then 116 | clean_sets 117 | set_ipsets 118 | exit 119 | elif [ "$1" = "clean" ] || [ "$1" = "stop" ] ; then 120 | clean_tables 121 | clean_sets 122 | exit 123 | elif [ "$enabled" = "1" ]; then 124 | clean_tables 125 | clean_sets 126 | set_ipsets 127 | set_iptables 128 | exit 129 | else 130 | echo "Pirania captive-portal is disabled. Try running captive-portal start" 131 | exit 132 | fi 133 | 134 | -------------------------------------------------------------------------------- /pirania/files/usr/bin/voucher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local config = require('voucher.config') 4 | local dba = require('voucher.db') 5 | local logic = require('voucher.logic') 6 | local ft = require('voucher.functools') 7 | local utils = require('voucher.utils') 8 | local uci = require('uci') 9 | local json = require 'luci.json' 10 | 11 | local uci_cursor = uci.cursor() 12 | local arguments 13 | local action 14 | local context 15 | 16 | captive_portal = {} 17 | 18 | local function printJson (obj) 19 | print(json.encode(obj)) 20 | end 21 | 22 | local function shell(command) 23 | local handle = io.popen(command) 24 | local result = handle:read("*a") 25 | handle:close() 26 | return result 27 | end 28 | 29 | local function split(inputstr, sep) 30 | if sep == nil then 31 | sep = "+" 32 | end 33 | local t={} 34 | for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 35 | table.insert(t, str) 36 | end 37 | return t 38 | end 39 | 40 | -- Show or change portal url 41 | captive_portal.url = function(context) 42 | local url = '' 43 | if (context[1] == 'show') then 44 | url = uci_cursor:get("pirania", "base_config", "portal_url") 45 | else 46 | url = context[1] and context[1] or "http://thisnode.info/portal" 47 | uci_cursor:set("pirania", "base_config", "portal_url", url) 48 | uci_cursor:commit("pirania") 49 | end 50 | print(url) 51 | end 52 | 53 | -- Update voucher date 54 | local function updateVoucher(name, date) 55 | local result = {} 56 | local db = dba.load(config.db) 57 | local toRenew = {} 58 | for _, voucher in pairs (db.data) do 59 | if (voucher[1] == name) then 60 | toRenew = voucher 61 | end 62 | end 63 | if (#toRenew > 0) then 64 | result.success = true 65 | toRenew[3] = tonumber(date) 66 | local validVouchers = ft.filter(function(row, index) return row[1] ~= name end, db.data) 67 | local newDb = db 68 | newDb.data = validVouchers 69 | table.insert(newDb.data, toRenew) 70 | dba.save(config.db, newDb) 71 | else result.success = false 72 | end 73 | return result 74 | end 75 | 76 | 77 | --[[ 78 | --Checks if a context(defined by a mac and a voucher code) 79 | --is authorized to be used, and associates the mac to the 80 | --voucher code if needed. 81 | --]] 82 | captive_portal.auth_voucher = function(context) 83 | local mac = context[1] 84 | local voucher = context[2] 85 | 86 | local db = dba.load(config.db) 87 | local res = logic.auth_voucher(db, mac, voucher) 88 | print ( res.limit[1], res.limit[2], res.limit[3], res.limit[4] ) 89 | if (res.success == true) then 90 | print('Saving to DB') 91 | print('...') 92 | dba.save(config.db, db) 93 | print('saved!') 94 | end 95 | end 96 | 97 | --[[ 98 | --Checks if the mac of the given context is allowed to browse. 99 | --]] 100 | captive_portal.status = function(context) 101 | local mac = context[1] 102 | 103 | local db = dba.load(config.db) 104 | print ( logic.status(db, mac) ) 105 | end 106 | 107 | -- List all 108 | captive_portal.list_vouchers = function() 109 | local result = {} 110 | local vName = 1 111 | local vSecret = 2 112 | local vExpire = 3 113 | local vMacsAllowed = 6 114 | local usedMacs = 7 115 | local db = dba.load(config.db) 116 | for _, v in pairs(db.data) do 117 | result[_] = {} 118 | result[_].name = v[vName] 119 | result[_].expires = v[vExpire] 120 | result[_].voucher = v[vSecret] 121 | result[_].macsAllowed = v[vMacsAllowed] 122 | result[_].macs = split(v[usedMacs]) 123 | end 124 | printJson(result) 125 | end 126 | 127 | -- Renew voucher 128 | captive_portal.renew_voucher = function(context) 129 | local voucherName = context[1] 130 | local renewDate = context[2] 131 | printJson(updateVoucher(voucherName, renewDate)) 132 | end 133 | 134 | -- Remove voucher 135 | captive_portal.remove_voucher = function(context) 136 | local voucherName = context[1] 137 | printJson(updateVoucher(voucherName, 0)) 138 | end 139 | 140 | --[[ 141 | --Adds a voucher to the db with the params defined by context. 142 | --]] 143 | captive_portal.add_voucher = function(context) 144 | local key = context[1] 145 | local voucher = context[2] 146 | local epoc = context[3] 147 | local upload = context[4] and context[4] or '0' 148 | local download = context[5] and context[5] or '0' 149 | local amountofmacsallowed = context[6] and context[6] or '0' 150 | local exists = false 151 | 152 | local db = dba.load(config.db) 153 | for _, v in pairs (db.data) do 154 | if (v[1] == key) then 155 | exists = true 156 | end 157 | end 158 | if exists == false then 159 | local retval = { logic.add_voucher(db, key, voucher, epoc, upload, download, amountofmacsallowed)} 160 | dba.save(config.db, db) 161 | print ( unpack(retval) ) 162 | else 163 | print(0) 164 | end 165 | end 166 | 167 | captive_portal.add_many_vouchers = function(context, data) 168 | local result = {} 169 | local table = utils.split(data, "\n") 170 | local db = dba.load(config.db) 171 | local error = false 172 | for _,val in pairs (table) do 173 | local jdata = json.decode(val) 174 | local upload = jdata.upload and jdata.upload or '10' 175 | local download = jdata.download and jdata.download or '10' 176 | local amountofmacsallowed = jdata.amountofmacsallowed and jdata.amountofmacsallowed or '1' 177 | local res = logic.add_voucher(db, jdata.key, jdata.voucher, jdata.epoc, upload, download, amountofmacsallowed) 178 | if (res == nil) then error = true end 179 | end 180 | result.success = not error 181 | dba.save(config.db, db) 182 | printJson(result) 183 | end 184 | 185 | -- TODO refactor eliminate bash portion awk sed bash-isms 186 | captive_portal.print_valid_macs = function() 187 | local db = dba.load(config.db) 188 | local macs = logic.valid_macs(db) 189 | for _, mac in ipairs(macs) do 190 | print ( mac ) 191 | end 192 | end 193 | 194 | -- if is main 195 | if debug.getinfo(2).name == nil then 196 | arguments = { ... } 197 | action = arguments[1] 198 | context = ft.filter(function(row, index) return index > 1 end, arguments) 199 | if (action == 'add_many_vouchers') then stdin = io.stdin:read("*all") end 200 | captive_portal[action](context, stdin) 201 | end 202 | 203 | return captive_portal 204 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/config.lua: -------------------------------------------------------------------------------- 1 | local uci = require("uci") 2 | local pirania_config = 'pirania' 3 | 4 | ucicursor = uci.cursor() 5 | 6 | config = { 7 | db = ucicursor:get(pirania_config, 'base_config', 'db_path'), 8 | uploadlimit = ucicursor:get(pirania_config, 'base_config', 'uploadlimit'), 9 | downloadlimit = ucicursor:get(pirania_config, 'base_config', 'downloadlimit'), 10 | hooksDir = ucicursor:get(pirania_config, 'base_config', 'hooks_path') 11 | } 12 | 13 | return config 14 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/db.lua: -------------------------------------------------------------------------------- 1 | #!/bin/lua 2 | 3 | local utils = require('voucher.utils') 4 | local ft = require('voucher.functools') 5 | local hooks = require('voucher.hooks') 6 | 7 | dba = {} 8 | 9 | local function read_db_from_csv(dbinfo) 10 | local rawtable = utils.from_csv_to_table(dbinfo); 11 | if rawtable == nil then 12 | fho,err = io.open(dbinfo, "w") 13 | fho:write('key,voucher,expiretime,uploadlimit,downloadlimit,amountofmacsallowed,usedmacs,') 14 | rawtable = utils.from_csv_to_table(dbinfo); 15 | end 16 | local table = { 17 | headers = rawtable[1], 18 | data = ft.filter(function(row, index) return index > 1 end, rawtable) 19 | } 20 | return table 21 | end 22 | 23 | local function write_db_to_csv(csvname, db) 24 | data = {} 25 | data[1] = db.headers 26 | local idx = 2 27 | 28 | for _, v in pairs(db.data) do 29 | data[idx] = v 30 | idx = idx + 1 31 | end 32 | 33 | utils.from_table_to_csv(csvname, data) 34 | hooks("db_change") 35 | end 36 | 37 | function dba.get_vouchers_by_voucher(db, voucherid) 38 | local voucher_column = functools.search(function(val) return val == 'voucher' end, db.headers) 39 | return ft.filter(function(voucher) return voucher[voucher_column] == voucherid end, db.data) 40 | end 41 | 42 | function dba.get_vouchers_by_mac(db, mac) 43 | local mac_column = functools.search(function(val) return val == 'usedmacs' end, db.headers) 44 | return ft.filter(function(maclist) 45 | if(maclist[mac_column]) then 46 | local search_result = string.find(maclist[mac_column], mac) 47 | return search_result ~= nil 48 | end 49 | return false 50 | end, db.data) 51 | end 52 | 53 | function dba.get_all_vouchers(db) 54 | return db.data 55 | end 56 | 57 | function dba.describe_values(db, row) 58 | local described_row = {} 59 | for i, v in pairs(db.headers) do 60 | described_row[v] = row[i] 61 | end 62 | 63 | return described_row 64 | end 65 | 66 | function dba.add_voucher(db, key, voucher, epoc, upload, download, amountofmacsallowed) 67 | local data = {key, voucher, epoc, upload, download, amountofmacsallowed, ''} 68 | table.insert(db.data, data) 69 | return data 70 | end 71 | 72 | dba.load = read_db_from_csv 73 | dba.save = write_db_to_csv 74 | 75 | return dba 76 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/debugtools.lua: -------------------------------------------------------------------------------- 1 | #!/bin/lua 2 | 3 | debugtools = {} 4 | 5 | debugtools.print_r = function ( t ) 6 | local print_r_cache={} 7 | local function sub_print_r(t,indent) 8 | if (print_r_cache[tostring(t)]) then 9 | print(indent.."*"..tostring(t)) 10 | else 11 | print_r_cache[tostring(t)]=true 12 | if (type(t)=="table") then 13 | for pos,val in pairs(t) do 14 | if (type(val)=="table") then 15 | print(indent.."["..pos.."] => "..tostring(t).." {") 16 | sub_print_r(val,indent..string.rep(" ",string.len(pos)+8)) 17 | print(indent..string.rep(" ",string.len(pos)+6).."}") 18 | elseif (type(val)=="string") then 19 | print(indent.."["..pos..'] => "'..val..'"') 20 | else 21 | print(indent.."["..pos.."] => "..tostring(val)) 22 | end 23 | end 24 | else 25 | print(indent..tostring(t)) 26 | end 27 | end 28 | end 29 | if (type(t)=="table") then 30 | print(tostring(t).." {") 31 | sub_print_r(t," ") 32 | print("}") 33 | else 34 | sub_print_r(t," ") 35 | end 36 | print() 37 | end 38 | 39 | return debugtools 40 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/functools.lua: -------------------------------------------------------------------------------- 1 | functools = {} 2 | 3 | -- Lua implementation of the curry function 4 | -- Developed by tinylittlelife.org 5 | -- released under the WTFPL (http://sam.zoy.org/wtfpl/) 6 | 7 | -- curry(func, num_args) : take a function requiring a tuple for num_args arguments 8 | -- and turn it into a series of 1-argument functions 9 | -- e.g.: you have a function dosomething(a, b, c) 10 | -- curried_dosomething = curry(dosomething, 3) -- we want to curry 3 arguments 11 | -- curried_dosomething (a1) (b1) (c1) -- returns the result of dosomething(a1, b1, c1) 12 | -- partial_dosomething1 = curried_dosomething (a_value) -- returns a function 13 | -- partial_dosomething2 = partial_dosomething1 (b_value) -- returns a function 14 | -- partial_dosomething2 (c_value) -- returns the result of dosomething(a_value, b_value, c_value) 15 | function functools.curry(func, num_args) 16 | 17 | -- currying 2-argument functions seems to be the most popular application 18 | num_args = num_args or 2 19 | 20 | -- helper 21 | local function curry_h(argtrace, n) 22 | if 0 == n then 23 | -- reverse argument list and call function 24 | return func(functools.reverse(argtrace())) 25 | else 26 | -- "push" argument (by building a wrapper function) and decrement n 27 | return function (onearg) 28 | return curry_h(function () return onearg, argtrace() end, n - 1) 29 | end 30 | end 31 | end 32 | 33 | -- no sense currying for 1 arg or less 34 | if num_args > 1 then 35 | return curry_h(function () return end, num_args) 36 | else 37 | return func 38 | end 39 | end 40 | 41 | -- reverse(...) : take some tuple and return a tuple of elements in reverse order 42 | -- 43 | -- e.g. "reverse(1,2,3)" returns 3,2,1 44 | function functools.reverse(...) 45 | 46 | --reverse args by building a function to do it, similar to the unpack() example 47 | local function reverse_h(acc, v, ...) 48 | if 0 == select('#', ...) then 49 | return v, acc() 50 | else 51 | return reverse_h(function () return v, acc() end, ...) 52 | end 53 | end 54 | 55 | -- initial acc is the end of the list 56 | return reverse_h(function () return end, ...) 57 | end 58 | 59 | 60 | function functools.map(func, tbl) 61 | local newtbl = {} 62 | for i,v in pairs(tbl) do 63 | newtbl[i] = func(v) 64 | end 65 | return newtbl 66 | end 67 | 68 | function functools.filter(func, tbl) 69 | local newtbl= {} 70 | local index=1; 71 | for i,v in pairs(tbl) do 72 | if func(v, i) then 73 | newtbl[index]=v 74 | index = index + 1 75 | end 76 | end 77 | return newtbl 78 | end 79 | 80 | function functools.search(func, tbl) 81 | for i,v in pairs(tbl) do 82 | if func(v, i) then 83 | return i 84 | end 85 | end 86 | 87 | return 0 88 | end 89 | 90 | return functools 91 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/hooks.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local config = require('voucher.config') 4 | local fs = require("nixio.fs") 5 | 6 | local hooks = function(action) 7 | local hookPath = config.hooksDir..action..'/' 8 | local files = fs.dir(hookPath) or pairs({}) 9 | for file in files do 10 | os.execute(hookPath..file) 11 | end 12 | 13 | end 14 | 15 | if debug.getinfo(2).name == nil then 16 | arguments = { ... } 17 | if (arguments ~= nil and arguments[1] ~= nil) then 18 | hooks(arguments[1]) 19 | end 20 | end 21 | 22 | return hooks -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/logic.lua: -------------------------------------------------------------------------------- 1 | #!/bin/lua 2 | 3 | local dba = require('voucher.db') 4 | local config = require('voucher.config') 5 | 6 | logic = {} 7 | 8 | local function shell(command) 9 | local handle = io.popen(command) 10 | local result = handle:read("*a") 11 | handle:close() 12 | return result 13 | end 14 | 15 | local function isMac(string) 16 | local string = string:match("%w%w:%w%w:%w%w:%w%w:%w%w:%w%w") 17 | if string then 18 | return true 19 | else 20 | return false 21 | end 22 | end 23 | 24 | 25 | local function dateNow() 26 | local output = shell('date +%s000') 27 | local parsed = string.gsub(output, "%s+", "") 28 | local dateNow = tonumber(parsed) 29 | return dateNow 30 | end 31 | 32 | 33 | 34 | local function use_voucher(db, voucher, mac) 35 | macs = voucher[7] 36 | 37 | if (string.find(macs, mac) == nil) then 38 | if (macs == '') then 39 | voucher[7] = mac 40 | else 41 | voucher[7] = macs .. '+' .. mac 42 | end 43 | end 44 | end 45 | 46 | local function get_valid_rawvoucher(db, rawvouchers) 47 | local voucher, expiretime, uploadlimit, downloadlimit 48 | 49 | for _, rawvoucher in ipairs( rawvouchers ) do 50 | if (logic.check_valid_voucher(db, rawvoucher)) then 51 | return rawvoucher 52 | end 53 | end 54 | 55 | return 56 | end 57 | 58 | local function get_limit_from_rawvoucher(db, rawvoucher) 59 | local voucher, expiretime, uploadlimit, downloadlimit 60 | 61 | if (rawvoucher ~= nil) then 62 | voucher = dba.describe_values (db, rawvoucher) 63 | 64 | if tonumber(voucher.expiretime) ~= nil then 65 | expiretime = tostring( tonumber( voucher.expiretime ) - dateNow()) 66 | uploadlimit = voucher.uploadlimit ~= '0' and voucher.uploadlimit or config.uploadlimit 67 | downloadlimit = voucher.downloadlimit ~= '0' and voucher.downloadlimit or config.downloadlimit 68 | currentmacs = table.getn(utils.string_split(voucher.usedmacs, '+')) 69 | valid = currentmacs < tonumber(voucher.amountofmacsallowed) and '1' or '0' 70 | return expiretime, uploadlimit, downloadlimit, valid 71 | end 72 | end 73 | 74 | return '0', '0', '0', '0' 75 | end 76 | 77 | local function checkIfIpv4(ip) 78 | if ip == nil or type(ip) ~= "string" then 79 | return 0 80 | end 81 | -- check for format 1.11.111.111 for ipv4 82 | local chunks = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")} 83 | if (#chunks == 4) then 84 | for _,v in pairs(chunks) do 85 | if (tonumber(v) < 0 or tonumber(v) > 255) then 86 | return 0 87 | end 88 | end 89 | return true 90 | else 91 | return false 92 | end 93 | end 94 | 95 | function logic.getIpv4AndMac() 96 | local address = os.getenv('REMOTE_ADDR') 97 | local isIpv4 = checkIfIpv4(address) 98 | if (isIpv4) then 99 | local ipv4macCommand = "cat /proc/net/arp | grep "..address.." | awk -F ' ' '{print $4}' | head -n 1" 100 | fd = io.popen(ipv4macCommand, 'r') 101 | ipv4mac = fd:read('*l') 102 | fd:close() 103 | local res = {} 104 | res.ip = address 105 | res.mac = ipv4mac 106 | return res 107 | else 108 | local ipv6macCommand = "ip neighbor | grep "..address.." | awk -F ' ' '{print $5}' | head -n 1" 109 | fd6 = io.popen(ipv6macCommand, 'r') 110 | ipv6mac = fd6:read('*l') 111 | fd6:close() 112 | local ipv4Command = "cat /proc/net/arp | grep "..ipv6mac.." | awk -F ' ' '{print $1}' | head -n 1" 113 | fd4 = io.popen(ipv4Command, 'r') 114 | ipv4 = fd4:read('*l') 115 | fd4:close() 116 | local res = {} 117 | res.ip = ipv4 118 | res.mac = ipv6mac 119 | return res 120 | end 121 | end 122 | 123 | function logic.check_valid_voucher(db, row) 124 | local expireDate = tonumber(dba.describe_values(db, row).expiretime) or 0 125 | if (expireDate ~= nil) then 126 | return expireDate > dateNow() 127 | end 128 | end 129 | 130 | function logic.check_voucher_validity(voucherid, db) 131 | local res = {} 132 | res.valid = false 133 | local rawvouchers = dba.get_vouchers_by_voucher(db, voucherid) 134 | if (rawvouchers ~= nil) then 135 | local voucher = get_valid_rawvoucher(db, rawvouchers) 136 | local expiretime, uploadlimit, downloadlimit, valid = get_limit_from_rawvoucher(db, voucher) 137 | if(voucher ~= nil and valid == '1' and tonumber(expiretime) > 0) then 138 | res.valid = true 139 | res.voucher = voucher 140 | end 141 | end 142 | return res 143 | end 144 | 145 | local function setIpset(mac, expiretime) 146 | -- ipset only supports timeout up to 4294967 147 | if tonumber(expiretime) > 4294967 then expiretime = 4294967 end 148 | os.execute("ipset -exist add pirania-auth-macs " .. mac .. " timeout ".. expiretime) 149 | end 150 | 151 | function logic.auth_voucher(db, mac, voucherid) 152 | local response = { 153 | success=false, 154 | limit={'0', '0', '0', '0'} 155 | } 156 | local res = logic.check_voucher_validity(voucherid, db) 157 | if (res.valid) then 158 | use_voucher(db, res.voucher, mac) 159 | setIpset(mac, res.voucher[3]) 160 | response.limit={ res.voucher[3], res.voucher[4], res.voucher[5], res.voucher[6] } 161 | response.success=true 162 | end 163 | return response 164 | end 165 | 166 | function logic.check_mac_validity(mac) 167 | local command = 'voucher print_valid_macs | grep -o '..mac..' | wc -l | grep "[^[:blank:]]"' 168 | fd = io.popen(command, 'r') 169 | local output = fd:read('*all') 170 | fd:close() 171 | return tonumber(output) 172 | end 173 | 174 | function logic.add_voucher(db, key, voucher, epoc, upload, download, amountofmacsallowed) 175 | local rawvoucher = dba.add_voucher(db, key, voucher, epoc, upload, download, amountofmacsallowed) 176 | 177 | return get_limit_from_rawvoucher(db, rawvoucher) 178 | end 179 | 180 | function logic.valid_macs(db) 181 | local rawvouchers, rawvoucher, macs, currentmacs 182 | macs = {} 183 | rawvouchers = dba.get_all_vouchers(db) 184 | 185 | for _, rawvoucher in ipairs( rawvouchers ) do 186 | if logic.check_valid_voucher(db, rawvoucher) then 187 | local voucher = dba.describe_values(db, rawvoucher) 188 | currentmacs = utils.string_split(voucher.usedmacs, '+') 189 | for _, mac in ipairs( currentmacs ) do 190 | local intAmount = tonumber(voucher.amountofmacsallowed) 191 | local amountofmacs = 0 192 | if (intAmount ~= nil) then 193 | amountofmacs = intAmount 194 | end 195 | if (_ <= amountofmacs and isMac(mac)) then 196 | table.insert(macs, mac) 197 | end 198 | end 199 | end 200 | end 201 | 202 | return macs 203 | end 204 | 205 | function logic.status(db, mac) 206 | local rawvouchers, rawvoucher 207 | rawvouchers = dba.get_vouchers_by_mac(db, mac) 208 | 209 | for _, rawvoucher in ipairs( rawvouchers ) do 210 | if logic.check_valid_voucher(db, rawvoucher) then 211 | return get_limit_from_rawvoucher(db, rawvoucher) 212 | end 213 | end 214 | 215 | return '0', '0', '0', '0' 216 | end 217 | 218 | return logic 219 | -------------------------------------------------------------------------------- /pirania/files/usr/lib/lua/voucher/utils.lua: -------------------------------------------------------------------------------- 1 | #!/bin/lua 2 | 3 | utils = {} 4 | 5 | -- Used to escape "'s by toCSV 6 | local function escapeCSV (s) 7 | if string.find(s, '[,"]') then 8 | s = '"' .. string.gsub(s, '"', '""') .. '"' 9 | end 10 | return s 11 | end 12 | 13 | -- Convert from CSV string to table (converts a single line of a CSV file) 14 | local function fromCSV (s) 15 | s = s .. ',' -- ending comma 16 | local t = {} -- table to collect fields 17 | local fieldstart = 1 18 | repeat 19 | -- next field is quoted? (start with `"'?) 20 | if string.find(s, '^"', fieldstart) then 21 | local a, c 22 | local i = fieldstart 23 | repeat 24 | -- find closing quote 25 | a, i, c = string.find(s, '"("?)', i+1) 26 | until c ~= '"' -- quote not followed by quote? 27 | if not i then error('unmatched "') end 28 | local f = string.sub(s, fieldstart+1, i-1) 29 | table.insert(t, (string.gsub(f, '""', '"'))) 30 | fieldstart = string.find(s, ',', i) + 1 31 | else -- unquoted; find next comma 32 | local nexti = string.find(s, ',', fieldstart) 33 | table.insert(t, string.sub(s, fieldstart, nexti-1)) 34 | fieldstart = nexti + 1 35 | end 36 | until fieldstart > string.len(s) 37 | return t 38 | end 39 | 40 | -- Convert from table to CSV string 41 | local function toCSV (tt) 42 | local s = "" 43 | for _,p in ipairs(tt) do 44 | s = s .. "," .. escapeCSV(p) 45 | end 46 | return string.sub(s, 2) -- remove first comma 47 | end 48 | 49 | utils.from_csv_to_table = function(filename) 50 | local line, lines, fh, err 51 | 52 | lines = {} 53 | 54 | fh, err = io.open(filename) 55 | if err then print("OOps"); return; end 56 | 57 | while true do 58 | line = fh:read() 59 | if line == nil or line == '' then break end 60 | 61 | table.insert(lines, fromCSV(line)) 62 | end 63 | 64 | fh:close() 65 | 66 | return lines 67 | end 68 | 69 | utils.from_table_to_csv = function(filename, table) 70 | local fho, err 71 | -- Open a file for write 72 | fho,err = io.open(filename, "w") 73 | 74 | for _, line in pairs( table ) do 75 | fho:write(toCSV(line)) 76 | fho:write('\n') 77 | end 78 | 79 | fho:close() 80 | end 81 | 82 | utils.string_split = function(inputstr, sep) 83 | if sep == nil then 84 | sep = "%s" 85 | end 86 | local t={} ; i=1 87 | for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 88 | t[i] = str 89 | i = i + 1 90 | end 91 | return t 92 | end 93 | 94 | utils.split = function(string, sep) 95 | local ret = {} 96 | for token in string.gmatch(string, "[^"..sep.."]+") do table.insert(ret, token) end 97 | return ret 98 | end 99 | 100 | utils.redirect_page = function(url) 101 | return string.format([[ 102 | 103 | 104 | 105 | Redirect 106 | 107 | 108 | 109 | 112 | 113 | 114 | 115 |

ENTER

116 | 117 | 118 | ]], url, url , url) 119 | end 120 | 121 | return utils 122 | -------------------------------------------------------------------------------- /pirania/files/usr/libexec/rpcd/pirania: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | Copyright 2018 Marcos Gutierrez 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-3.0 8 | ]]-- 9 | 10 | require "ubus" 11 | local json = require 'luci.json' 12 | local ft = require('voucher.functools') 13 | local utils = require('voucher.utils') 14 | local logic = require('voucher.logic') 15 | local dba = require('voucher.db') 16 | 17 | local uci = require('uci') 18 | 19 | local uci_cursor = uci.cursor() 20 | 21 | local function printJson (obj) 22 | print(json.encode(obj)) 23 | end 24 | 25 | local conn = ubus.connect() 26 | if not conn then 27 | error("Failed to connect to ubus") 28 | end 29 | 30 | local function shell(command) 31 | -- TODO(nicoechaniz): sanitize or evaluate if this is a security risk 32 | local handle = io.popen(command) 33 | local result = handle:read("*a") 34 | handle:close() 35 | return result 36 | end 37 | 38 | local function list_vouchers(msg) 39 | local result = {} 40 | result = json.decode(shell('voucher list_vouchers')) 41 | printJson({ vouchers = result }) 42 | end 43 | 44 | local function renew_voucher(msg) 45 | local name = msg.name 46 | local date = msg.date 47 | local result = {} 48 | result = json.decode(shell('voucher renew_voucher '..name..' '..date)) 49 | printJson(result) 50 | end 51 | 52 | local function remove_voucher(msg) 53 | local name = msg.name 54 | local result = {} 55 | result = json.decode(shell('voucher remove_voucher '..name)) 56 | printJson(result) 57 | end 58 | 59 | local function add_many_vouchers(msg) 60 | local result = {} 61 | local vouchers = ft.map(function(val) return json.encode(val) end, msg.vouchers) 62 | local voucher = io.popen("voucher add_many_vouchers", 'w') 63 | result.success = assert(voucher:write(table.concat(vouchers, '\n'))) 64 | printJson(result) 65 | end 66 | 67 | local function add_voucher(msg) 68 | local result = {} 69 | local key = msg.key 70 | local secret = msg.secret 71 | local epoc = msg.epoc 72 | local upload = msg.upload 73 | local download = msg.download 74 | local amountofmacsallowed = msg.amountofmacsallowed 75 | local output = shell('voucher add_voucher '..key..' '..secret..' '..epoc..' '..download..' '..upload..' '..amountofmacsallowed) 76 | local parsed = string.gsub(output, "%s+", "") 77 | if tonumber(parsed) == 0 then 78 | result.success = false 79 | else 80 | result.key = key 81 | result.secret = secret 82 | result.epoc = epoc 83 | result.upload = upload 84 | result.download = download 85 | result.amountofmacsallowed = amountofmacsallowed 86 | end 87 | printJson(result) 88 | end 89 | 90 | local function auth_voucher(msg) 91 | local result = {} 92 | local mac = msg.mac 93 | local voucher = msg.voucher 94 | result.mac = mac 95 | result.voucher = voucher 96 | local db = dba.load(config.db) 97 | local res = logic.auth_voucher(db, mac, voucher) 98 | result.success = res.success 99 | -- BUG: Saving to DB causes ubus to timeout 100 | dba.save(config.db, db) 101 | printJson(result) 102 | end 103 | 104 | local function print_valid_macs(msg) 105 | local result = {} 106 | result.macs = {} 107 | local output = shell('voucher print_valid_macs') 108 | for line in output:gmatch("[^\n]+") do 109 | -- local words = {} 110 | -- for w in line:gmatch("%S+") do if w ~= "" then table.insert(words, w) end end 111 | -- local mac = words[2] 112 | table.insert(result.macs, line) 113 | end 114 | printJson(result); 115 | end 116 | 117 | local function show_url(msg) 118 | local result = {} 119 | local url = uci_cursor:get("pirania", "base_config", "portal_url") 120 | result.url = url 121 | printJson(result); 122 | end 123 | 124 | local function change_url(msg) 125 | local result = {} 126 | local url = msg.url 127 | uci_cursor:set("pirania", "base_config", "portal_url", url) 128 | uci_cursor:commit("pirania") 129 | result.url = url 130 | printJson(result); 131 | end 132 | 133 | 134 | local methods = { 135 | add_voucher = { 136 | key = 'value', 137 | secret = 'value', 138 | epoc = 0, 139 | upload = 0, 140 | download = 0, 141 | amountofmacsallowed = 0 142 | }, 143 | add_many_vouchers = { vouchers = 'value' }, 144 | auth_voucher = { mac = 'value', voucher = 'value' }, 145 | print_valid_macs = { no_params = 0 }, 146 | list_vouchers = { no_params = 0 }, 147 | remove_voucher = { name = 'value' }, 148 | renew_voucher = { name = 'value', date = 0 }, 149 | show_url = { no_params = 0 }, 150 | change_url = { url = 'value' }, 151 | } 152 | 153 | if arg[1] == 'list' then 154 | printJson(methods) 155 | end 156 | 157 | if arg[1] == 'call' then 158 | local msg = io.read() 159 | msg = json.decode(msg) 160 | if arg[2] == 'add_voucher' then add_voucher(msg) 161 | elseif arg[2] == 'add_many_vouchers' then add_many_vouchers(msg) 162 | elseif arg[2] == 'auth_voucher' then auth_voucher(msg) 163 | elseif arg[2] == 'print_valid_macs' then print_valid_macs(msg) 164 | elseif arg[2] == 'list_vouchers' then list_vouchers(msg) 165 | elseif arg[2] == 'remove_voucher' then remove_voucher(msg) 166 | elseif arg[2] == 'renew_voucher' then renew_voucher(msg) 167 | elseif arg[2] == 'show_url' then show_url(msg) 168 | elseif arg[2] == 'change_url' then change_url(msg) 169 | else printJson({ error = "Method not found" }) 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /pirania/files/usr/share/rpcd/acl.d/pirania.json: -------------------------------------------------------------------------------- 1 | { 2 | "unauthenticated": { 3 | "description": "pirania voucher public access", 4 | "write": { 5 | "ubus": { 6 | "pirania": [ "auth_voucher", "get_clients", "print_valid_macs", "show_url" ] 7 | } 8 | } 9 | }, 10 | "root": { 11 | "description": "pirania administration access", 12 | "write": { 13 | "ubus": { 14 | "pirania": ["*"] 15 | } 16 | }, 17 | "read": { 18 | "ubus": { 19 | "pirania": ["*"] 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pirania/files/www/pirania-redirect/redirect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local uci = require('uci') 4 | local json = require('luci.json') 5 | local uci_cursor = uci.cursor() 6 | local redirect_page = require('voucher.utils').redirect_page 7 | 8 | function handle_request (env) 9 | local origin_url = env.HTTP_HOST 10 | local portal_url = uci_cursor:get("pirania", "base_config", "portal_url") 11 | if origin_url ~= portal_url then 12 | local setParams = prevUrl and '?prev='..origin_url or '' 13 | local url = portal_url..setParams 14 | local send = uhttpd.send 15 | send("Status: 302 \r\n") 16 | send("Content-type: text/html \n\n") 17 | send(print(redirect_page(url))) 18 | end 19 | end 20 | --------------------------------------------------------------------------------