You can ask basically anyone who manages multiple Linux (and even Windows) servers what they use to monitor their systems and its a high possibility that they’ll say “Nagios” or a variant of it (i.e.: OpsView, Icinga, Centreon, etc…). There’s no doubt it has a strong hold in the market and there are plenty of positives to it, but is it for everyone?
Nagios requires the core itself installed on the main/master server as well as a daemon installed on every server to be monitored. The core then parses config files and performs checks to make sure stats are correct. While this isn’t painstaking the process of installing both can be troublesome, especially if you’re doing it by hand (not using an auto-installer script).
Editing the configuration of Nagios (core or daemon) is a bit of a challenge. While there is documentation, it would seem like trying to solve a rubic cube would be simpler until you really understand whats going on. I can see it being very beneficial when Nagios was first starting out but now with it branching out so much into such a more sophisticated piece of software the configuration is convoluted.
Luckily Nagios is pretty much a set it and forget solution (unless you want to add more plugins to it).
This is my biggest gripe with Nagios personally: there’s virtually no customization. The program itself is a compiled Perl/CGI script. About the only thing you can customize are the plugins for monitoring. While I can see some points for making it closed-source, given some of the obscure warnings it can spit out I think opening it up even a little bit would be far more beneficial.
This is a short list, but Nagios does what its supposed to do and doesn’t offer much fluff. The web UI is pretty horrid (looks like its from 1990) but it presents information you need. The alert system is nice but could be easier to work with, and while there are numerous frontend wrappers for it, they all still require Nagios itself.
The biggest compliment I can give it in the end is that it uses perf data to return information about a plugin, which is pretty universal as to how its formatted.
Again an easy but essential requirement for our two-factor system. This will be another Flask web route and mostly database driven. Lets look at the flow of how things will transpire first for this project:
SMS: POST number to /sms -> URI generates token and sends to number via SMS -> User enters their number and token to website and submits -> Site POSTs number and token to /auth/#/token -> HTTP 200 for authenticated, 403 for failure
Voice: POST number to /who -> URI generates token and sends to number via call -> User enters their number and token to website and submits -> Site POSTs number and token to /auth/#/token -> HTTP 200 for authenticated, 403 for failure
The only difference between the two is how the user receives their token. We’ll use that to our advantage. Here is the auth URI:
@app.route("/auth//", methods=['GET','POST']) def auth(number, token): valid = False up = phone.select(phone.id).where(phone.digits==number).get() records = SelectQuery(tokens).where((tokens.token==token) & (tokens.phone==up.id)).count() if records: valid = True if valid: tokens.update(token="").where(tokens.phone==up.id).execute() return make_response("", 200) return make_response("", 403)
If you think its pretty simple that’s because it is. We get the phone ID by looking up the numbers and then check to see if there’s a token ready for the phone number (phone ID has to match as well as token). If the authentication is valid we set the token to “” so no one can use it again for that number (this is one reason why the generate_token method is flawed…its too easy to figure out), and return HTTP/200 (OK) to the user. Otherwise, we return HTTP/403 (Forbidden).
We can definitely make this more intricate, however, and I’ll showcase some of that next time. But this is a good start for anyone wanting to make their own two-factor authentication system.
We’re almost there! Now we need to save the tokens we’ve generated as well as the phone number requesting it. While can be done anywhere I chose to plop it into the generate_token() method because we’d have to write the code twice otherwise. Luckily its a small fix, and we’ll finally be able to use our database stuff now.
Just before the “return token” line in the method add these lines:
try: up = phone.select().where(phone.digits==number).get() except: up = phone.create(digits=number) records = SelectQuery(tokens).where(tokens.phone==up.id).count() if records: tokens.update(token=token).where(tokens.phone==up.id).execute() else: tokens.create(token=token,phone=up.id)
The first try/except block attempts to get the phone information from the database and creates a record of it instead if nothing exists. We then try to update the token for the phone number and if that doesn’t work then we create a new record of it. Very simple and easy but is also vital to our service.
Every incoming and outgoing request to the number (voice and SMS) goes against your balance. Unfortunately there’s nothing you can do to stop people from trying to spam your SMS inbox. There is a silver lining though with voice calls.
On your Twilio dashboard click “Numbers” near the top, then click on your number. Here you’ll be presented with some options. The “Voice Request URL” is what we’re interested in. Remember our /voice URI? We’ll use that to make things fun. So change the URL to http://ipaddress/voice and change the method from POST to GET. Save and leave it be for a bit, we don’t need to change anything else there.
Now we will make a change to our voice method, which also extends what we learned while making outgoing calls. We will be using TwiML again. Replace your @app.route() to the end of the method with this:
@app.route("/voice") def voice(): resp = twiml.Response() resp.reject("rejected") return make_response(str(resp), 200)
Twilio has two methods of rejecting someone: “rejected” and “busy”. When reject() is set to “busy” a busy tone will be played, whereas when “rejected” is used a “this number is not in service” type of message is played.
Now, for the caveat. If resp.reject() is not called first in the line of creating TwiML, your account will be hit with usage. The only way to make it so when incoming calls don’t affect your usage is to call reject() first before any other. However, after reject() is handled via Twilio anything after that is ignored as well. Something else to keep in mind.
Save and now try to call your number. See what happens.
Now we’ll go into making outbound calls. This is pretty similar to SMS but does get more advanced pretty quickly. Now is the time where being Internet-reachable is a necessity.
First, how to create a call. For this I’m going to make the functions easy again:
@app.route("/voice/") def voice(number): try: client.calls.create(url="http://ip_address:5000/who/%d" % number, to="+1%d" % number, from_=TWILIO_NUMBER) return "Call sent." except TwilioRestException, e: return "Unable to send. Reason: %s" % (e.msg)
This is very similar to our SMS method with the exception being “client.calls.create” instead of “client.sms.messages.create”. Also, instead of passing a message to the user we are passing a URL to forward them to. While you don’t have to pass a URL, there does have to be something for Twilio to process (which will be covered shortly). So, basically, when the call is created Twilio will read special XML generated at /who/[number] and go from there (there’s a few options, we’ll cover the basic message reading first). Here’s the method for /who:
@app.route("/who/", methods=['POST']) def who(number): token = generate_token(number) xml = TwiML.Response() xml.say("Your token is: %s" % ". ".join([i for i in token])) return make_response(str(xml), 200)
Twilio only POSTs when itself is referencing a URI/URL, so that is why we only support POST for /who. I won’t go into the basics of TwiML (Twilio’s XML), but basically what’s going on is we are creating a reference to our response XML, and then passing it what we want it to read back to the user. From my experience, if HTTP response is not 200 then you will run into issues, so that is why we force a 200 (HTTP OK) response regardless.
The reason why we rejoin our token like we do is that if we gave it as one long strong then Twilio’s text-to-speech service would jumble it all up. If you just space the characters apart it reads still a little too fast. So we instead make Twilio read each character as if they are a sentence by themselves.
Visit http://ip_address/voice/verified_number (replacing “verified_number” with your verified number like with SMS) and give it a try!
This will be a small blurb compared to the rest but this is rather essential. This isn’t the best way to generate tokens and in no way do I recommend you use this in a practical case. Here is the code, however (place it just below the app = … line):
def generate_token(number): from time import time charlist = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890+/=" num = str(number+time()).split(".") info = [num[i:i+2] for i in range(0, len(num), 2)] token = "" for i in info: index = int(i)+int(i) token = "%s%s" % (token, str(charlist[index])) return token
Basically what this does is add the current time to the number provided (and since time() returns a decimal we only want the whole number). Then we split the number into multiple pairs and add both digits in the pairs, and that is our place in the token list/charlist. It’s pretty simplistic but it does our job just fine.
When I wrote the original code I made things way too complicated for it. So, I’ll steer you in the direction of NOT doing that, and we’ll make this simple!
After our import lines, add these:
TWILIO_SID = "xxx" TWILIO_AUTH = "yyy" TWILIO_NUMBER = "zzz"
The xxx and yyy will be replaced by whatever values you noted down/get from the Twilio dashboard. You also need to set your Twilio number as well so the service will know who to set the “From” field to.
Now we need to create a reference to Twilio’s API, which also requires us to send the SID and AUTH values:
client = TwilioRestClient(TWILIO_SID, TWILIO_AUTH)
We’ll work on SMS first since its cheaper and easier to work with. First what we need to do is create a route for SMS:
@app.route("/sms", methods=['GET', 'POST']) def sms(): try: client.sms.messages.create(body="Huzzah!", to="+1xxxyyyzzzz", from_=TWILIO_NUMBER) return "SMS sent." except TwilioRestException, e: return "Unable to send. Reason: %s" % (e.msg)
For sake of this project right now replace the hard-coded number “+1xxxyyyzzzz” with the number you signed up for with Twilio (demo accounts can’t send to just any number, paid accounts can) and visit /sms to receive a text message. Pretty cool, huh?! Lets change this a little bit and make it so when a GET request is made to the page it’s sent to a number specified. Here’s the new code block:
@app.route("/sms/") def sms(number): try: client.sms.messages.create(body="Huzzah!", to="+1%d" % number, from_=TWILIO_NUMBER) return "SMS sent." except TwilioRestException, e: return "Unable to send. Reason: %s" % (e.msg)
We made some changes here. First, we removed the ‘methods’ argument to app.route. This was done because we want to just focus on handling GET requests right now, which Flask defaults to when methods is not passed. Next is the route. Now we need to visit /sms/10-digit-number-here to make it work (replacing the “10-digit-number-here” part with the verified number). We still need to pass that info to the method itself so we can use it, and also modify the ‘to’ field so it still sends properly. Now, try again and it should still work!
What we need is something to handle HTTP requests. Thankfully Flask does this for us and is extremely easy to use!
First thing we’ll do is import all of the stuff we need/want:
from flask import Flask, request, redirect, make_response from twilio import twiml as TwiML, TwilioRestException from twilio.rest import TwilioRestClient from db import * from peewee import SelectQuery
This imports the important Flask and Twilio options as well as our database reference. Now we need to initialize Flask before we get too happy:
app = Flask(__name__)
In a way you can think of this as like using logging.Logger(), you typically pass it __name__ so that it has a reference.
Now, lets make it so when we go to http://ip_address:5000/ we see a message:
@app.route("/", methods=['GET', 'POST']) def main(): return "You sure?!"
The method name is irrelevant (though has to be unique per route). The “/” is the same as when visiting http://ip_address:5000/, and we will let the main handler support both GET and POST requests. Now, when you visit the URL you’ll receive “You sure?!” in your browser.
After, we need this two-liner:
if __name__ == "__main__": app.run(host='192.168.0.102', debug=True)
However, change the IP in host to one that is reachable from the Internet (or set up port forwarding) so later on Twilio will be able to connect to the web service. Now, open up your browser once you run the script and you should see the message.
This was short but, then again, there’s not a whole lot that needed to be covered to get Flask up and running by itself. Next, however, will be when the fun starts and we work with Twilio’s helper library.
Lets figure out what we need to make this magical wonderland happen. We need a user’s phone number, a field to state if the phone is usable or not (if we should concern ourselves with requests from it) and storage for the active token. If we want to make it more advanced than we will but for now this is good. So we will use two tables: phone and tokens. The token will be based on the phone number and current timestamp as sort of a salt (not the safest but again, template for the future ).
id – primary key
digits – small integer (15-digit number)
id – primary key
token – variable char (length 0f 10)
phone_id – foreign key to phone->id
Simple, eh? Now lets create it in Python!
from peewee import *
database = SqliteDatabase(“2fa.db”)
We import everything of the Peewee library to make things easier on us. We also specify we want a SQLite database.
database = database
We create an abstract base class (ABC) so everything can share the database instance. This is mostly done to save typing.
id = PrimaryKeyField()
digits = IntegerField()
id = PrimaryKeyField()
token = CharField(max_length=10)
phone = ForeignKeyField(Phone)
Specify the table structure of the phone and tokens tables, subclassing the mentioned ABC class.
phone = Phone
tokens = Tokens
Easy references for the tables (which will be needed quite a bit).
Connect/load the SQLite database.
Create the tables in the SQLite file.
Save the above code fragments to a file called “db.py” (otherwise make the adjustments in later parts). Now we have the database laid out properly and easy to work with!
For the point of this guide we are going to be using just our test account number. The only difference between that and a purchased number is that every SMS or call is prefixed with a “Thank you for using Twilio” kind of message. For demo’ing everything its a small price to pay. I’ll also be walking through this with you as well by creating a new account.
- Sign up for Twilio here: https://www.twilio.com/try-twilio and enter the appropriate information.
- It’ll prompt you for a phone number so they can verify who you are (i.e.: a bot wasn’t programmed just to sign up for the service), enter your phone number. I used Google Voice and had no issues. You’ll receive a text message with a verification code. Enter that.
- Twilio will generate a phone number for you, you can either use the one provided or search their database for a specific number.
- Once you pick a number it’ll let you test some of its basic features. Its helpful to make sure everything works fine at least.
- Click on “Go To Your Account” and you’ll be presented with a pretty nicely done dashboard
In the dashboard make a note of your account SID and auth token (hover over the little padlock icon and click it to make it visible). You’ll need these later, SID being your account ID and the token being the SID’s password (so keep it a secret!). There’s a lot of useful information Twilio gives you and its awesome. But, lets continue on. You don’t need to do anything else in the dashboard yet. Now we’ll get started on our web service so we can make magic happen!