Skip to content

Commit

Permalink
Public Repo Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
yantisj committed Sep 18, 2016
0 parents commit 0ff4ee4
Show file tree
Hide file tree
Showing 13 changed files with 849 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.vscode
*.pyc
__pycache__
api.log
api.db
venv
4 changes: 4 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[MASTER]
disable=invalid-name
ignored-modules=flask_sqlalchemy
disable=E1101
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Overview

Python Flask-based API server template for creating a secure REST API. This
server has everything you need properly configured out of the box to create your
own API. You can use it to call local scripts, relay to other APIs, or do
anything you can do in Python. Clients can be anything that supports REST calls
(curl, bash, powershell etc). Currently apisrv only supports local authentation,
but it could be extended with LDAP support and auth tokens. Please contact me at
([yantisj](https://github.com/yantisj)) for any help.

## Requirements
* Python 3.4+
* VirtualEnv
* SSL Certificates (can test with non-HTTPS but should not be used in production)

## Features
* Built-in SSL server
* Example functions for creating your own secure API
* Sample Rest Client
* Strong Password Hashing (stored in sqlite DB)
* Rate limiting for safety
* JSONified error handling
* Extensive logging to both stdout and log files

## Installing
* Make sure you have virtualenv and Python3 installed on your system (pip3 install virtualenv)
* Clone the repo: ```git clone https://git.musc.edu/yantisj/apisrv.git```
* Install the virtualenv: ```cd apisrv; virtualenv venv```
* Activate the virtual environment: ```source venv/bin/activate```
* Install Python Requirements: ```pip3 install -r requirements.txt```

## Testing
* You must activate the virtualenv before starting the server
* Initialize DB and Add a user: ```./ctlsrv.py --init; ./ctlsrv.py --add testuser```
* Start the server: ```./runsrv.py```
* Test the info request with a browser: [/test/api/v1.0/info?testvar=testing](http://localhost:5000/test/api/v1.0/info?testvar=testing)
* Test via CURL: ```curl -u testuser:testpass http://localhost:5000/test/api/v1.0/info?var1=test```
* Test via the Sample Rest Client: ```./test/rest-client.py```
* Get the system uptime/load: [/test/api/v1.0/uptime](http://localhost:5000/test/api/v1.0/uptime)

## Customizing For Use
* See [api.conf](api.conf)
* Enable https and add a certificate to enable listening beyond localhost
* Add custom methods to [apisrv/views.py](apisrv/views.py)
* Make sure to add the @auth.login_required decorator to each method for security
* Be careful when making system calls with user input
* Check the API rate limits in [apisrv/\_\_init\_\_.py](apisrv/__init__.py)

## Using VirtualEnv with Visual Studio Code
* Make sure you have the latest version of the Python Extension installed
* Use Command+Shift+P: Python: Select Workspace Interpreter and choose python under venv/bin

## Further Reading
* [Creating a REST API](https://realpython.com/blog/python/api-integration-in-python/)
18 changes: 18 additions & 0 deletions api.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Flask API Server Configuration File

[apisrv]
app_name = test
port = 5000

# Debug mode logs to stdout and enable Flask debugging
# Set to 0 for production!
debug = 1

# HTTPS must not be set to 0 for production server
https = 0
logfile = api.log
database = ../api.db

# SSL Certificate and Key, required for enabling HTTPS
ssl_crt = ssl.crt
ssl_key = ssl.key
147 changes: 147 additions & 0 deletions apisrv/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env python
# Copyright (c) 2016 "Jonathan Yantis"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the GNU Affero General Public License in all respects
# for all of the code used other than as permitted herein. If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so. If you do not
# wish to do so, delete this exception statement from your version. If you
# delete this exception statement from all source files in the program,
# then also delete it in the license file.
#
#
"""
Flask API Server
"""
import sys
import os
import re
import logging
from logging.handlers import RotatingFileHandler
import configparser
import sqlite3
from flask import Flask, jsonify, request, g, make_response
from flask_httpauth import HTTPBasicAuth
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_sqlalchemy import SQLAlchemy

logger = logging.getLogger(__name__)

# Populated from config file
debug = 0

# Flask Limits for Safety
flask_limits = ["1000 per day", "100 per hour", "5 per minute"]

# Initialize Configuration
config_file = 'api.conf'
config = configparser.ConfigParser()
config.read(config_file)

# Check for sane config file
if 'apisrv' not in config:
print("Could not parse config file: " + config_file)
sys.exit(1)

# Logging Configuration, default level INFO
logger = logging.getLogger('')
logger.setLevel(logging.INFO)
lformat = logging.Formatter('%(asctime)s %(name)s:%(levelname)s: %(message)s')

# Debug mode
if 'debug' in config['apisrv'] and int(config['apisrv']['debug']) != 0:
debug = int(config['apisrv']['debug'])
logger.setLevel(logging.DEBUG)
logging.debug('Enabled Debug mode')

# Enable logging to file if configured
if 'logfile' in config['apisrv']:
lfh = RotatingFileHandler(config['apisrv']['logfile'], maxBytes=(1048576*5), backupCount=3)
lfh.setFormatter(lformat)
logger.addHandler(lfh)

# STDOUT Logging defaults to Warning
if not debug:
lsh = logging.StreamHandler(sys.stdout)
lsh.setFormatter(lformat)
lsh.setLevel(logging.WARNING)
logger.addHandler(lsh)

# Create Flask APP
app = Flask(__name__)
app.config.from_object(__name__)
app.config.update(dict(
DATABASE=os.path.join(app.root_path, config['apisrv']['database']),
SQLALCHEMY_DATABASE_URI='sqlite:///' + os.path.join(app.root_path, config['apisrv']['database']),
SQLALCHEMY_TRACK_MODIFICATIONS=False,
#SECRET_KEY='development key',
#USERNAME='dbuser',
#PASSWORD='dbpass'
))

# Auth module
auth = HTTPBasicAuth()

# Database module
db = SQLAlchemy(app)

# Apply Rate limiting
limiter = Limiter(
app,
key_func=get_remote_address,
global_limits=flask_limits
)

# Helper Functions
def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db

def connect_db():
"""Connects to the specific database."""
conn = sqlite3.connect(app.config['DATABASE'])
conn.row_factory = sqlite3.Row
return conn

def initialize_db():
""" Initialize the database structure"""
conn = connect_db()
c = conn.cursor()
c.execute('''CREATE TABLE user
(username text, password text, token text) ''')
conn.commit()
conn.close()

@app.teardown_appcontext
def close_db(error):
"""Closes the database again at the end of the request."""
if hasattr(g, 'sqlite_db'):
g.sqlite_db.close()

# Safe circular imports per Flask guide
import apisrv.errors
import apisrv.views
import apisrv.user


76 changes: 76 additions & 0 deletions apisrv/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python
# Copyright (c) 2016 "Jonathan Yantis"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the GNU Affero General Public License in all respects
# for all of the code used other than as permitted herein. If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so. If you do not
# wish to do so, delete this exception statement from your version. If you
# delete this exception statement from all source files in the program,
# then also delete it in the license file.
#
"""
API Errors Jsonified
"""
import logging
from flask import Flask, jsonify, request, g, make_response
from apisrv import app, auth

logger = logging.getLogger(__name__)

@app.errorhandler(404)
def not_found(error=None):
message = {
'status': 404,
'message': 'Not Found: ' + request.url,
}
resp = jsonify(message)
resp.status_code = 404
return resp

@app.errorhandler(429)
def ratelimit_handler(e):
return make_response(
jsonify(error="ratelimit exceeded %s" % e.description)
, 429
)

@auth.error_handler
def auth_failed(error=None):
message = {
'status': 401,
'message': 'Authentication Failed: ' + request.url
}
resp = jsonify(message)
resp.status_code = 401

return resp

@app.errorhandler(400)
def bad_request(error):
print("Bad Request")
message = {
'status': 400,
'message': 'Bad Request: ' + request.url,
}
resp = jsonify(message)
resp.status_code = 400
return resp


Loading

0 comments on commit 0ff4ee4

Please sign in to comment.