├── LICENSE ├── LicenseServer.licenseheader ├── LicenseServer.pyproj ├── README.md ├── app.py ├── auth.py ├── database.py ├── insert.py ├── make_db.py ├── requirements.txt ├── tables.py ├── utilities.py └── validate.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 6 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 7 | subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 14 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 15 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 16 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LicenseServer.licenseheader: -------------------------------------------------------------------------------- 1 | extensions: designer.cs generated.cs 2 | extensions: .cs .cpp .h 3 | // MIT Licensed. Copyright (c) 2017 4 | extensions: .aspx .ascx 5 | <%-- 6 | MIT Licensed. Copyright (c) 2017 7 | --%> 8 | extensions: .vb 9 | 'MIT Licensed. Copyright (c) 2017 10 | extensions: .xml .config .xsd .xaml 11 | 14 | extensions: .py 15 | # MIT Licensed. Copyright (c) 2017 -------------------------------------------------------------------------------- /LicenseServer.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | a3249779-d23a-4699-b6ae-f4f7187a6640 7 | . 8 | app.py 9 | 10 | 11 | . 12 | . 13 | LicenseServer 14 | LicenseServer 15 | {4d01393b-b261-4e3a-8855-b827e67498dd} 16 | 3.5 17 | False 18 | Standard Python launcher 19 | dev 20 | False 21 | 22 | 23 | true 24 | false 25 | 26 | 27 | true 28 | false 29 | 30 | 31 | 32 | 33 | Code 34 | 35 | 36 | Code 37 | 38 | 39 | Code 40 | 41 | 42 | 43 | Code 44 | 45 | 46 | Code 47 | 48 | 49 | Code 50 | 51 | 52 | 53 | 54 | {4d01393b-b261-4e3a-8855-b827e67498dd} 55 | {9a7a9026-48c1-4688-9d5d-e5699d47d074} 56 | 3.5 57 | env (Python 64-bit 3.5) 58 | Scripts\python.exe 59 | Scripts\pythonw.exe 60 | Lib\ 61 | PYTHONPATH 62 | Amd64 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 10.0 73 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | This daemon allows for license management by simply keeping a record of license signatures and how many times they are allowed to be used (1, 2..unlimited). 3 | It does no formal verification of signatures so any program you use this with will still need to verify the signature. 4 | 5 | This is simply a secondary safety to prevent people from sharing their license keys with others (unless you allow them to do so). You can also revoke signatures (by setting their InstallLimit to 0) 6 | 7 | Originally developed to be used with Portable.Licensing although the layout is so simple it should work with any licensing software that can provide a Signature and a reasonably unique UserID/HardwareID type of identifier. 8 | 9 | # Configuration 10 | 1. Run `python auth.py USERNAME PASSWORD` to create a new administrator user. This user will be allowed to insert Signatures into the database via the `/insert` endpoint, using Basic Auth headers. 11 | 1. Run `python make_db.py` to create the SQLite3 database. 12 | 1. To run it in dev mode you can run `python app.py dev`, and for production/live mode run `python app.py live` 13 | 14 | Dev mode runs on port 8080 by default and logs errors to stdout. 15 | 16 | Live mode runs on port 9090 and logs errors to a log file. Though this appears broken at the moment so best to log stdout to a file via: `python app.py live >> stdout 2>&1 &` 17 | 18 | # Usage 19 | This is a RESTful application so send your requests via HTTP Post with a JSON string as the body. 20 | 21 | You can use endpoints such as `http://HOSTNAME:PORT/ENDPOINT` but it's suggested to use `http://HOSTNAME:PORT/licenseserver/v1/ENDPOINT` in case future versions change the protocol. 22 | 23 | ### /insert 24 | Insert a new signature and define its allowed usage. 25 | 26 | Requires HTTP Basic Auth headers that match an admin user which you setup previously. 27 | 28 | The JSON request must contain a Signature (string), and one of InstallLimit (int), or UnlimitedInstalls (bool) 29 | 30 | If UnlimitedInstalls isn't specified then it's assumed to be false by the server. 31 | 32 | Example that limits a Signature to 2 uses: 33 | 34 | ``` 35 | {"Signature": "YOURSIGNATURE", 36 | "InstallLimit" : "2"} 37 | ``` 38 | 39 | And one that allows unlimited Signature usage (such as for trials): 40 | ``` 41 | {"Signature": "YOURSIGNATURE", 42 | "UnlimitedInstalls" : "True"} 43 | ``` 44 | 45 | Trying to insert a signature which already exists will result in the program ignoring the addition and returning an error code. If you need to do this at the moment then it's best to open the database on the server with `sqlite3` and make your adjustments manually. 46 | 47 | ### /validate 48 | Asks the server if a given Signature is valid and records information about the user validating the signature. 49 | 50 | If the Signature is valid then it will record the User's UserID (which should be a reasonably unique ID), Name, Email, Company (optional), the date/time of install, and the validated signature. 51 | 52 | Example request: 53 | 54 | ``` 55 | {"Signature":"YOURSIGNATURE", 56 | "Name":"Max", 57 | "Email":"max@steel.com", 58 | "UserID":"0xfe5712d89a"} 59 | ``` 60 | 61 | Example response: 62 | ``` 63 | {"signatureValid":"True"} 64 | ``` 65 | 66 | On the first request the new User will be stored in the User table, and the Signature's InstallCount will be incremented by 1. On subsequent requests, if there's a User with matching Signature and UserID then the User's "InstallDateTime" entry will be updated but no further change will be made to the Signature's InstallCount. 67 | 68 | ### UserIDs 69 | UserIDs are entirely up to you, this program will accept any string and makes no attempt to validate the ID. Pick your ID according to exactly how reliable you need it to be. 70 | 71 | You should also hash the IDs before sending them out. 72 | 73 | # Note 74 | Don't expect too many updates to this software as I'm self-employed and don't have too much free time to dedicate to adding functionality that I don't need. However, I am very happy to merge commits which fix bugs and add functionality. 75 | 76 | # MIT License 77 | Copyright 2017 78 | 79 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 80 | associated documentation files (the "Software"), to deal in the Software without restriction, 81 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 82 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 83 | subject to the following conditions: 84 | 85 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 88 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 89 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 90 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 91 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 92 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import falcon 3 | import validate 4 | import insert 5 | import sys 6 | 7 | from waitress import serve 8 | import logging 9 | 10 | api = application = falcon.API() 11 | 12 | validate = validate.Resource() 13 | api.add_route('/validate', validate) 14 | 15 | insert = insert.Resource() 16 | api.add_route('/insert', insert) 17 | 18 | if __name__ == '__main__': 19 | 20 | if sys.argv[1] == 'live': 21 | # TODO: Logging may not actually be working right 22 | logger = logging.getLogger('waitress') 23 | logfile = logging.FileHandler('license_server.log') 24 | logger.setLevel(logging.WARN) 25 | logger.addHandler(logfile) 26 | 27 | serve(api, url_prefix='licenseserver/v1', listen='*:9090') 28 | else: 29 | serve(api, url_prefix='licenseserver/v1', listen='*:8080') -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import sys 3 | 4 | from sqlalchemy.orm import Session 5 | 6 | from database import engine, Base 7 | from tables import * 8 | 9 | import random 10 | from hashlib import sha256 11 | from base64 import standard_b64decode 12 | 13 | def authenticate(req): 14 | session = Session(engine) 15 | 16 | if req.content_length == 0: 17 | return False 18 | 19 | authrequest = standard_b64decode(req.auth.split(" ")[1]).split(b":") 20 | username = authrequest[0] 21 | 22 | if username is None: 23 | return False 24 | 25 | stored_user = session.query(AdminsTable).get(username) 26 | 27 | if stored_user is not None: 28 | stored_key = stored_user.Key.split("#")[0] 29 | salt = stored_user.Key.split("#")[1] 30 | 31 | key = authrequest[1].decode("utf-8") + salt 32 | key = key.encode("utf-8") 33 | key = sha256(key).hexdigest() 34 | 35 | return key == stored_key 36 | 37 | return False 38 | 39 | session.close() 40 | 41 | def make_admin(username, key): 42 | 43 | session = Session(engine) 44 | ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 45 | 46 | username = username.encode("utf-8") 47 | 48 | salt = ''.join(random.choice(ALPHABET) for i in range(16)) 49 | key = key + salt 50 | 51 | encrypted_key = sha256(key.encode("utf-8")).hexdigest() 52 | encrypted_key = encrypted_key + "#" + salt 53 | 54 | newRow = AdminsTable(Username=username, Key=encrypted_key) 55 | 56 | session.add(newRow) 57 | 58 | session.commit() 59 | session.close() 60 | 61 | if __name__ == '__main__': 62 | make_admin(sys.argv[1], sys.argv[2]) -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import sqlite3 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.ext.automap import automap_base 5 | 6 | # Setup our connection to the database 7 | engine = create_engine('sqlite+pysqlite:///license_database.sqlite3', module=sqlite3) 8 | Base = automap_base() 9 | Base.prepare(engine, reflect=True) -------------------------------------------------------------------------------- /insert.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import falcon 3 | 4 | from sqlalchemy.orm import Session 5 | from sqlalchemy import exists, and_ 6 | from database import engine, Base 7 | from tables import UsersTable, SignaturesTable 8 | from time import ctime 9 | 10 | from utilities import getSignatureQuery, getJson, to_bool 11 | from auth import authenticate 12 | 13 | def recordUser(session, valueDict): 14 | userRow = None 15 | requiredValues = ["Signature", "Name", "Email", "UserID"] 16 | 17 | for value in requiredValues: 18 | if value not in valueDict.keys(): 19 | return None 20 | #resp.body = "Error, missing key {}".format(value) 21 | #resp.status = falcon.HTTP_400 22 | 23 | # User table values 24 | signature = valueDict.get("Signature") 25 | name = valueDict.get("Name") 26 | email = valueDict.get("Email") 27 | company = valueDict.get("Company") 28 | userID = valueDict.get("UserID") 29 | 30 | userExists = True 31 | userRow = session.query(UsersTable).get((signature, userID)) 32 | 33 | if userRow is None: 34 | userExists = False 35 | userRow = UsersTable(Signature=signature, Name=name, Email=email, Company=company, UserID=userID, InstallDateTime=ctime()) 36 | 37 | return userRow, userExists 38 | 39 | def createSignatureRow(session, valueDict): 40 | signatureRow = None 41 | signature = valueDict.get("Signature") 42 | 43 | # Signature table values 44 | installLimit = valueDict.get("InstallLimit") 45 | unlimitedInstalls = to_bool(valueDict.get("UnlimitedInstalls")) 46 | 47 | if unlimitedInstalls is True: 48 | signatureRow = SignaturesTable(PrimaryKey=signature, InstallCount=0, InstallLimit=0, UnlimitedInstalls=unlimitedInstalls) 49 | print("Created Signature: {}, with UnlimitedInstalls".format(signature)) 50 | elif installLimit is not None: 51 | signatureRow = SignaturesTable(PrimaryKey=signature, InstallCount=0, InstallLimit=installLimit, UnlimitedInstalls=False) 52 | print("Created Signature: {}, with InstallLimit of: {}".format(signature, installLimit)) 53 | else: 54 | print("Error creating row with Signature: {}. No valid UnlimitedInstalls or InstallLimit provided".format(signature)) 55 | 56 | return signatureRow 57 | 58 | class Resource(object): 59 | 60 | def on_get(self, req, resp): 61 | resp.body = "Accepted!" 62 | resp.status = falcon.HTTP_200 63 | 64 | def on_post(self, req, resp): 65 | validRequest = authenticate(req) 66 | 67 | if not validRequest: 68 | resp.body = "Invalid username/password" 69 | resp.status = falcon.HTTP_401 70 | return 71 | 72 | session = Session(engine) 73 | valueDict = getJson(req) 74 | 75 | signatureQuery = getSignatureQuery(req, session) 76 | 77 | message = "Unable to add Signature" 78 | resp.status = falcon.HTTP_400 79 | 80 | if "Signature" in valueDict.keys() and signatureQuery is None: 81 | signatureRow = createSignatureRow(session, valueDict) 82 | message = "Unable to create signature row" 83 | if signatureRow is not None: 84 | session.add(signatureRow) 85 | message = "Added signature to database: {}".format(signatureRow.PrimaryKey) 86 | resp.status = falcon.HTTP_200 87 | 88 | elif "Signature" in valueDict.keys(): 89 | message = "Unable to add Signature, already exists in database" 90 | 91 | resp.body = message 92 | print(message) 93 | 94 | session.commit() 95 | session.close() -------------------------------------------------------------------------------- /make_db.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | # coding: utf-8 3 | # Run this script to produce the database this program uses 4 | from sqlalchemy import CheckConstraint, Column, Integer, Table, Text, text, create_engine 5 | from sqlalchemy.sql.sqltypes import NullType 6 | from sqlalchemy.ext.declarative import declarative_base 7 | import os 8 | 9 | Base = declarative_base() 10 | metadata = Base.metadata 11 | 12 | class Admin(Base): 13 | __tablename__ = 'Admins' 14 | 15 | Username = Column(Text, primary_key=True) 16 | Key = Column(Text, nullable=False) 17 | 18 | class Signature(Base): 19 | __tablename__ = 'Signatures' 20 | __table_args__ = ( 21 | CheckConstraint('UnlimitedInstalls IN ( 0 , 1 )'), 22 | ) 23 | 24 | PrimaryKey = Column(Text, primary_key=True) 25 | InstallCount = Column(Integer, nullable=False) 26 | InstallLimit = Column(Integer, nullable=False) 27 | UnlimitedInstalls = Column(Integer, nullable=False, server_default=text("0")) 28 | 29 | class User(Base): 30 | __tablename__ = 'Users' 31 | 32 | Signature = Column(Text, primary_key=True, nullable=False) 33 | UserID = Column(Text, primary_key=True, nullable=False) 34 | Name = Column(Text, nullable=False) 35 | Email = Column(Text, nullable=False) 36 | Company = Column(Text) 37 | InstallDateTime = Column(Text, nullable=False) 38 | 39 | def run_main(): 40 | db_file = 'license_database.sqlite3' 41 | if os.path.isfile(db_file): 42 | print("Database already exists.") 43 | return 44 | else: 45 | engine = create_engine('sqlite+pysqlite:///'+db_file, echo=True) 46 | Base.metadata.create_all(engine) 47 | 48 | if __name__ == '__main__': 49 | run_main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | falcon==1.2.0 2 | python-mimeparse==1.6.0 3 | six==1.10.0 4 | waitress==1.0.2 5 | SQLAlchemy==1.1.9 6 | passlib==1.7.1 7 | pbr==3.0.0 8 | talons==0.3 9 | -------------------------------------------------------------------------------- /tables.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | from database import Base 3 | 4 | # Our tables 5 | SignaturesTable = Base.classes.Signatures 6 | UsersTable = Base.classes.Users 7 | AdminsTable = Base.classes.Admins -------------------------------------------------------------------------------- /utilities.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import json 3 | 4 | from tables import * 5 | 6 | def to_bool(value): 7 | 8 | if type(value) == bool: 9 | return value 10 | 11 | valid = {'true': True, 't': True, '1': True, 12 | 'false': False, 'f': False, '0': False,} 13 | 14 | if not isinstance(value, str): 15 | return False # not a string so return false 16 | 17 | if value.lower() in valid: 18 | return valid[value.lower()] 19 | else: 20 | return False 21 | 22 | def getJson(req): 23 | body = None 24 | 25 | if req.content_length != 0: 26 | req.stream.seek(0) 27 | data = req.stream.read(req.content_length or 0).decode('utf-8') 28 | body = json.loads(data) 29 | 30 | return body 31 | 32 | def getSignatureQuery(req, session): 33 | clientIDQuery = None 34 | signatureQuery = None 35 | 36 | body = getJson(req) 37 | 38 | if "Signature" in body.keys(): 39 | signature = body.get("Signature") 40 | 41 | if signature: 42 | print("Received Signature: {}".format(signature)) 43 | signatureQuery = session.query(SignaturesTable).get(signature) 44 | 45 | return signatureQuery 46 | -------------------------------------------------------------------------------- /validate.py: -------------------------------------------------------------------------------- 1 | # MIT Licensed. Copyright (c) 2017 2 | import falcon 3 | import json 4 | 5 | from sqlalchemy.orm import Session 6 | from database import engine, Base 7 | from tables import * 8 | 9 | from utilities import getSignatureQuery, getJson 10 | from insert import recordUser 11 | 12 | class Resource(object): 13 | 14 | def on_get(self, req, resp): 15 | resp.body = "Accepted!" 16 | resp.status = falcon.HTTP_200 17 | 18 | def on_post(self, req, resp): 19 | session = Session(engine) 20 | valueDict = getJson(req) 21 | signatureQuery = getSignatureQuery(req, session) 22 | 23 | signatureValid = False 24 | 25 | if signatureQuery: 26 | print("Found signature: {}".format(signatureQuery.PrimaryKey)) 27 | 28 | userRow, userExists = recordUser(session, valueDict) 29 | availableSignature = signatureQuery.UnlimitedInstalls or signatureQuery.InstallCount <= signatureQuery.InstallLimit 30 | 31 | if userRow is not None and availableSignature: 32 | signatureValid = True 33 | 34 | # If the user didn't exist already we up the install count 35 | # If they did exist then we leave the install count as-is (probably did a reinstall) 36 | if not userExists: 37 | signatureQuery.InstallCount = signatureQuery.InstallCount + 1 38 | 39 | session.add(userRow) 40 | print("Valid signature: Unlimited - {}, Install Count - {}, Existing User - {}".format(bool(signatureQuery.UnlimitedInstalls), signatureQuery.InstallCount, userExists)) 41 | 42 | response = {'signatureValid': signatureValid} 43 | status = None 44 | if signatureValid: 45 | status = falcon.HTTP_200 46 | else: 47 | status = falcon.HTTP_400 48 | 49 | resp.body = json.dumps(response) 50 | resp.status = status 51 | 52 | session.commit() 53 | session.close() 54 | --------------------------------------------------------------------------------