-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0ff4ee4
Showing
13 changed files
with
849 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.vscode | ||
*.pyc | ||
__pycache__ | ||
api.log | ||
api.db | ||
venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[MASTER] | ||
disable=invalid-name | ||
ignored-modules=flask_sqlalchemy | ||
disable=E1101 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
Oops, something went wrong.