diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..398fbff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.vscode
+*.pyc
+__pycache__
+api.log
+api.db
+venv
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..a53ea1d
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,4 @@
+[MASTER]
+disable=invalid-name
+ignored-modules=flask_sqlalchemy
+disable=E1101
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cb7a164
--- /dev/null
+++ b/README.md
@@ -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/)
diff --git a/api.conf b/api.conf
new file mode 100644
index 0000000..3d69596
--- /dev/null
+++ b/api.conf
@@ -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
diff --git a/apisrv/__init__.py b/apisrv/__init__.py
new file mode 100644
index 0000000..a4eef2c
--- /dev/null
+++ b/apisrv/__init__.py
@@ -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 .
+#
+# 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
+
+
diff --git a/apisrv/errors.py b/apisrv/errors.py
new file mode 100644
index 0000000..53dc3e6
--- /dev/null
+++ b/apisrv/errors.py
@@ -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 .
+#
+# 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
+
+
diff --git a/apisrv/user.py b/apisrv/user.py
new file mode 100644
index 0000000..6123dfb
--- /dev/null
+++ b/apisrv/user.py
@@ -0,0 +1,149 @@
+#!/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 .
+#
+# 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 User Functions
+"""
+import sys
+import logging
+import time
+from passlib.hash import sha256_crypt
+
+import sqlite3
+from flask_sqlalchemy import SQLAlchemy
+from apisrv import auth, config, get_db, connect_db, db
+
+logger = logging.getLogger(__name__)
+
+class User(db.Model):
+ """ SQL User Model"""
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(80), unique=True)
+ password = db.Column(db.String(120), unique=True)
+ role = db.Column(db.String(40))
+
+ def __init__(self, username, password, role='default'):
+ self.username = username
+ self.password = password
+ self.role = role
+
+ def __repr__(self):
+ return ''.format(self.username)
+
+@auth.verify_password
+def verify_password(username, password):
+ """API Password Verification"""
+
+ if authenticate_user(username, password):
+ return True
+ return False
+
+def authenticate_user(username, passwd):
+ """ Authenticate a user """
+
+ user = User.query.filter_by(username=username).first()
+
+ authenticated = False
+
+ if user:
+ authenticated = sha256_crypt.verify(passwd, user.password)
+ else:
+ time.sleep(1)
+ logger.info("Authentication Error: User not found in DB: %s", username)
+ return False
+
+ if authenticated:
+ logger.debug("Successfully Authenticated user: %s", username)
+ else:
+ logger.info("Authentication Failed: %s", username)
+
+ return authenticated
+
+
+def add_user(username, passwd):
+ """
+ Add a new user to the database
+ """
+
+ user = User.query.filter_by(username=username).first()
+
+ if user:
+ #print("Error: User already exists in DB", file=sys.stderr)
+ raise Exception("Error: User already exists in DB")
+ elif len(passwd) < 6:
+ print("Error: Password must be 6 or more characters", file=sys.stderr)
+ exit(1)
+ else:
+ logger.info("Adding new user to the database: %s", username)
+
+ phash = sha256_crypt.encrypt(passwd)
+
+ newuser = User(username, phash)
+ db.session.add(newuser)
+ db.session.commit()
+
+ return phash
+
+def update_password(username, passwd):
+ """ Update password for user """
+
+ user = User.query.filter_by(username=username).first()
+
+ if len(passwd) < 6:
+ print("Error: Password must be 6 or more characters", file=sys.stderr)
+ exit(1)
+ elif user:
+ logger.info("Updating password for user: %s", username)
+
+ phash = sha256_crypt.encrypt(passwd)
+
+ user.password = phash
+ db.session.commit()
+
+ return phash
+ else:
+
+ print("Error: User does not exists in DB", file=sys.stderr)
+ exit(1)
+
+def del_user(username):
+ """ Delete a user from the database """
+
+ user = User.query.filter_by(username=username).first()
+
+ if user:
+ logger.info("Deleting user: %s", username)
+
+ db.session.delete(user)
+ db.session.commit()
+
+ return True
+ else:
+
+ print("Error: User does not exists in DB", file=sys.stderr)
+ exit(1)
+
diff --git a/apisrv/views.py b/apisrv/views.py
new file mode 100644
index 0000000..35d892e
--- /dev/null
+++ b/apisrv/views.py
@@ -0,0 +1,103 @@
+#!/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 .
+#
+# 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 Views for serving user requests with examples
+Add your own methods here
+"""
+import logging
+import subprocess
+from flask import jsonify, request
+from apisrv import app, auth, config
+
+# Setup
+logger = logging.getLogger(__name__)
+app_name = config['apisrv']['app_name']
+
+# http://localhost:5000/test unauthenticated response of "Hello World"
+@app.route('/test')
+def app_test():
+ """ Test Method, no authentication required """
+ logger.info("Hello World Requested")
+ response = {"message": "Hello World!", "status": "200"}
+ return jsonify(response)
+
+# System Uptime, requires authentication
+@app.route('/' + app_name + '/api/v1.0/uptime')
+@auth.login_required
+def app_uptime():
+ """ Runs a system command to get the uptime"""
+ p = subprocess.Popen('uptime', stdout=subprocess.PIPE, universal_newlines=True)
+ uptime = p.stdout.readlines()[0].strip()
+ response = {"message": uptime, "status": "200"}
+ return jsonify(response)
+
+# Call echo shell cmd on message via /musc/api/v1.0/echo?message=testing
+@app.route('/' + app_name + '/api/v1.0/echo', methods=['GET'])
+@auth.login_required
+def app_echo():
+ """ Runs the echo command with input 'message'
+ Note: Input is validated and escaped properly
+ """
+ message = request.args.get('message', '')
+ if message:
+ p = subprocess.Popen(['echo', message], stdout=subprocess.PIPE, universal_newlines=True)
+ uptime = p.stdout.readlines()[0].strip()
+ response = {"message": uptime, "status": "200"}
+ return jsonify(response)
+ else:
+ return jsonify({"error": "Must provide message attribute via GET"})
+
+# Info method, Return Request Data back to client as JSON
+@app.route('/' + app_name + '/api/v1.0/info', methods=['POST', 'GET'])
+@auth.login_required
+def app_getinfo():
+ """ Returns Flask API Info """
+ response = dict()
+ response['message'] = "Flask API Data"
+ response['status'] = "200"
+ response['method'] = request.method
+ response['path'] = request.path
+ response['remote_addr'] = request.remote_addr
+ response['user_agent'] = request.headers.get('User-Agent')
+
+ # GET attributes
+ for key in request.args:
+ response['GET ' + key] = request.args.get(key, '')
+ # POST Attributes
+ for key in request.form.keys():
+ response['POST ' + key] = request.form[key]
+
+ return jsonify(response)
+
+@app.route('/')
+def app_index():
+ """Index identifying the server"""
+ response = {"message": app_name + \
+ " server: Authentication required for use",
+ "status": "200"}
+ return jsonify(response)
diff --git a/ctlsrv.py b/ctlsrv.py
new file mode 100755
index 0000000..d7858f5
--- /dev/null
+++ b/ctlsrv.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# 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 .
+#
+# 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.
+#
+"""
+Manage the API Server
+"""
+import sys
+import getpass
+import apisrv
+import argparse
+
+# Config File
+config = apisrv.config
+
+parser = argparse.ArgumentParser()
+
+parser = argparse.ArgumentParser(prog='ctlsrv.py',
+ description='Manage the API Server')
+
+parser.add_argument("--adduser", metavar="user", help="Add API User to DB", type=str)
+parser.add_argument("--newpass", metavar="user", help="Update Password", type=str)
+parser.add_argument("--testuser", metavar="user", help="Test Authentication", type=str)
+parser.add_argument("--deluser", metavar="user", help="Delete API User", type=str)
+parser.add_argument("--initdb", help="Initialize the Database", action="store_true")
+parser.add_argument("--debug", help="Set debugging level", type=int)
+parser.add_argument("-v", help="Verbose Output", action="store_true")
+
+args = parser.parse_args()
+
+verbose = 0
+if args.v:
+ verbose = 1
+if args.debug:
+ verbose = args.debug
+
+# Initialize the Database
+if args.initdb:
+ apisrv.db.create_all()
+ apisrv.db.session.commit()
+
+# Add user to DB
+elif args.adduser:
+
+ passwd = getpass.getpass('Password:')
+ verify = getpass.getpass('Verify Password:')
+
+ if passwd == verify:
+ phash = apisrv.user.add_user(args.adduser, passwd)
+ if phash:
+ print("Successfully Added User to Database")
+ else:
+ print("Error: Could not Add User to Database")
+ else:
+ print("Error: Passwords do not match")
+
+# Update User Password
+elif args.newpass:
+ passwd = getpass.getpass('New Password:')
+ verify = getpass.getpass('Verify Password:')
+
+ if passwd == verify:
+ phash = apisrv.user.update_password(args.newpass, passwd)
+ if phash:
+ print("Successfully Updated Password")
+ else:
+ print("Error: Could not Update Password")
+ else:
+ print("Error: Passwords do not match")
+
+# Delete a User
+elif args.deluser:
+ ucheck = apisrv.user.del_user(args.deluser)
+
+ if ucheck:
+ print("Successfully Deleted User")
+ else:
+ print("Username not found in DB")
+
+# Test Authentication
+elif args.testuser:
+ passwd = getpass.getpass('Password:')
+ phash = apisrv.user.authenticate_user(args.testuser, passwd)
+ if phash:
+ print("Successfully Authenticated")
+ else:
+ print("Authentication Failed")
+else:
+ parser.print_help()
+ print()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..a3b4526
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+pylint
+requests
+flask
+flask_httpauth
+flask_limiter
+passlib
+sqlalchemy
+flask_sqlalchemy
diff --git a/runsrv.py b/runsrv.py
new file mode 100755
index 0000000..92a5cd4
--- /dev/null
+++ b/runsrv.py
@@ -0,0 +1,47 @@
+#!/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 .
+#
+# 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.
+#
+#
+"""
+Run API Server
+"""
+import logging
+from apisrv import app, debug, config
+
+logger = logging.getLogger('runsrv')
+
+# Production server in HTTPS Mode
+if config['apisrv']['https'] != '0':
+ context = (config['apisrv']['ssl_crt'], config['apisrv']['ssl_key'])
+ app.run(host='0.0.0.0', port=int(config['apisrv']['port']), \
+ ssl_context=context, threaded=True, debug=debug)
+
+# Localhost development server
+else:
+ logger.warning("HTTPS is not configured, defaulting to localhost only")
+ app.run(debug=debug, port=int(config['apisrv']['port']))
diff --git a/test/rest-client.py b/test/rest-client.py
new file mode 100755
index 0000000..d8237e4
--- /dev/null
+++ b/test/rest-client.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# 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 .
+#
+# 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.
+#
+#
+"""
+REST API Sample Client
+"""
+import requests
+import json
+
+user = 'testuser'
+passwd = 'testpass'
+url = 'http://localhost:5000'
+
+# Make a request (don't verify cert if SSL)
+r = requests.get(url + '/test/api/v1.0/info?var=testvar', auth=(user, passwd), verify=False)
+
+# Check for proper response
+if r.status_code == 200:
+
+ # JSON Dict
+ response = r.json()
+
+ # Dump JSON in pretty format
+ print(json.dumps(response, sort_keys=True, indent=4, separators=(',', ': ')))
+else:
+ print("Request Error:", r.status_code, r.text)
diff --git a/testapi.py b/testapi.py
new file mode 100755
index 0000000..c9871aa
--- /dev/null
+++ b/testapi.py
@@ -0,0 +1,72 @@
+#!/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 .
+#
+# 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.
+#
+#
+"""
+Test the API Application
+"""
+import os
+import apisrv
+import unittest
+import tempfile
+from base64 import b64encode
+
+headers = {
+ 'Authorization': 'Basic %s' % b64encode(b"testuser:testpass").decode("ascii")
+}
+
+class APITestCase(unittest.TestCase):
+ """ API Testing Cases"""
+ def setUp(self):
+ apisrv.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + tempfile.mkstemp()[1]
+ apisrv.app.config['TESTING'] = True
+ self.app = apisrv.app.test_client()
+
+ with apisrv.app.app_context():
+ apisrv.db.create_all()
+ try:
+ apisrv.user.add_user("testuser", "testpass")
+ except Exception:
+ pass
+
+ def tearDown(self):
+ apisrv.db.drop_all()
+
+ def test_index(self):
+ rv = self.app.get('/')
+ assert b'Authentication required' in rv.data
+
+ def test_info(self):
+ rv = self.app.get('/test/api/v1.0/info?test=GETTEST', headers=headers)
+ assert b'GET test' in rv.data
+
+ def test_uptime(self):
+ rv = self.app.get('/test/api/v1.0/uptime', headers=headers)
+ assert b'load averages' in rv.data
+
+if __name__ == '__main__':
+ unittest.main()