Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jsteinberg1 committed May 5, 2020
0 parents commit 8ded0a4
Show file tree
Hide file tree
Showing 99 changed files with 565,403 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.venv
/.vscode
/__pycache__
Binary file added .github/jobstatus.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/phoneinfo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/phonescraper.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/settings.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/.venv
/.vscode
/data
__pycache__/
*.pyc
__pycache__
/*code*
todo
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.7

WORKDIR /fastapi

ADD requirements.txt /fastapi/requirements.txt
RUN pip install -r /fastapi/requirements.txt

COPY ./api /fastapi/api
COPY ./lib /fastapi/lib
COPY ./client/dist /fastapi/client/dist
COPY ./rq-workers /usr/src/workers

ENV PYTHONPATH="/fastapi:${PYTHONPATH}"

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Cisco VOIP Phone Info Web Application Server
The VOIP Phone Info server is a application that collects IP phone data from your Cisco VOIP environment.

The key features are:

* **Phone configuration data**: Pulls high level configuration data (MAC/Description/Device Pool/etc) from Cisco Unified Communications Manager (CUCM) via AXL
* **Phone registration data**: Pulls real-time registration data (IP address, firmware, registration time stamp, EM logins) from CUCM via Serviceability API
* **IP Phone webpage**: Collects data (Serial number, CDP/LLDP neighbor info, ITL, Network info) by scraping the IP phone's internal web server
* **Multiple cluster support**: supports integration with multiple CUCM clusters
* **Docker**: Runs easily in Docker containers

## Screenshots

### Phone Info Page
![Phone Info](.github/phoneinfo.jpg)
### Phone Scraper Page
![Phone Scraper](.github/phonescraper.jpg)
### Settings Page
![Settings](.github/settings.jpg)
### Job Status Page
![Job Status](.github/jobstatus.jpg)

## Requirements

Python 3.7+
and/or
Docker

## CUCM Prerequisites
* Create a new application user on your CUCM with the following roles:
* Standard AXL API Access
* Standard CCM Admin Users
* Standard CCMAdmin Read Only
* Standard SERVICEABILITY Read Only

## Docker Installation

* Install docker
* clone/download CUCM Phone Info from github
* Edit the Docker environment variables file **docker-fastapi-variables.env** in the project root folder. Create your own secret key and specify your timezone
* run command below from terminal/command line

<div class="termy">

```console
docker-compose up
```

</div>

## First time setup info

* This will start a HTTPS web server on port 8080
* Login to https://localhost:8080/ with username 'localadmin' and password 'setup'
* Configure your CUCM clusters on the server. The first CUCM server in the cluster will be used to authenticate future logins to the VOIP Phone Info server.
* Perform manual sync on the Job Status tab
* Set the scheduler to define the automatic update schedule
* All persistent data is stored in the 'data' bind mount. A default self signed certificate is created the first time this application is run. You can replace the files in the 'data/cert' folder with CA signed certificates and then restart the containers.

## Known issues & limitations
* phone scraper script isn't perfect - some models are not fully working (7940/7960, 7937, Codecs)
* ITL status is populated based on the IP Phone webpage's 'status messages' page. In some circumstances the phone will not have any recently reports of ITL status and therefore the ITL status is not reported.
* testing, has only been tested against CUCM 11.5
* schema files need to be updated with 12.5
* no redundancy for CUCM, if the CUCM node in the first cluster is down, logins will fail.
* The CUCM service account credentials are stored using symmetric encryption. You are responsible for limiting access to the 'data' bind mount directory.

## Third-party software credits

* This application was built using FastAPI for the backend and VueJS for the front end. This is my first application using both technologies, i'm sure there is room for alot of improvement.
* Handsontable Non-Commercial License - this application is using the Handsontable non-commercial license. This application is for personal use. Please see (https://github.com/handsontable/handsontable/blob/develop/handsontable-non-commercial-license.pdf) for more information.
* Phonescrape library originally from https://github.com/levensailor/phonescrape. Some modifications to parse additional data, support various models, and store results in object

95 changes: 95 additions & 0 deletions api/Auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os, hashlib
import jwt
import logging
from datetime import datetime, timedelta
from fastapi import HTTPException

from api.Config import config

from api.crud import settings_management

logger = logging.getLogger('api')

class Auth:
def __init__(self):
self.SECRET_KEY = os.getenv('SECRET_KEY')
self.ALGORITHM = "HS256"
self.ACCESS_TOKEN_EXPIRE_MINUTES = 120


def authenticate_user(self, username, password, cluster=None): # authenticate user against CUCM API
import requests

if username == "localadmin":
# authenticate to localadmin account
current_hashed_pw = settings_management.get_setting(name='localadmin')

supplied_hashed_pw = hashlib.sha512((password + str(config.salt)).encode()).hexdigest()

if supplied_hashed_pw == current_hashed_pw:
return True
else:
return False
else:
# authenticate against CUCM


authorized_cucm_users = settings_management.get_all_cucm_users() # get authorized users from DB

if username in [user_object.userid for user_object in authorized_cucm_users]:
# user is an authorized CUCM user, authenticate user against CUCM UDS interface for Cluster #1

cucm_clusters = settings_management.get_cucm_clusters()

if len(cucm_clusters) > 0:
logger.info(f"SSL verification status {cucm_clusters[0].ssl_verification}")

url = "https://" + cucm_clusters[0].server + ":8443/cucm-uds/user/" + username
session = requests.Session()

if cucm_clusters[0].ssl_verification == True and cucm_clusters[0].ssl_ca_trust_file != None:
session.verify = os.path.join(config.ca_certs_folder,cucm_clusters[0].ssl_ca_trust_file)

if cucm_clusters[0].ssl_verification == False or cucm_clusters[0].ssl_verification == '0':
session.verify = False

session.auth = (username, password)
try:
response = session.get(url)
except Exception as e:
logger.error(f"Auth failure {e}")
if response.status_code == 200:
return True

else:
return False

return False

def login(self, username, password):
result = self.authenticate_user(username, password)
if result == True:

expiration = datetime.utcnow() + timedelta(minutes=self.ACCESS_TOKEN_EXPIRE_MINUTES)

token = jwt.encode({
'sub': username,
'iat': datetime.utcnow(),
'exp': expiration
},
self.SECRET_KEY)

return {"status": "success", "user": username, "token": token.decode('utf-8'), "expiration": str(expiration)}
else:
return {"status": "error", "message": "username/password incorrect"}


def validate(self, token):
try:
data = jwt.decode(token, self.SECRET_KEY)
except Exception as e:
if "expired" in str(e):
raise HTTPException(status_code=401, detail={"status": "error", "message": "Token expired"})
else:
raise HTTPException(status_code=400, detail={"status": "error", "message": "Exception: " + str(e)})
return data
44 changes: 44 additions & 0 deletions api/Config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
import logging
from pathlib import Path
from cryptography.fernet import Fernet
from typing import List
from pydantic import BaseModel


logger = logging.getLogger('api')

class ApiConfig:
def __init__(self):
self.basedir = Path(os.path.abspath(__file__)).parents[1]
logger.info(f"Basedir is set to {self.basedir}")
self.datafolder = os.path.join(self.basedir, "data")
self.ca_certs_folder = os.path.join(self.datafolder, "ca_certs")
self.certs_folder = os.path.join(self.datafolder, "certs")
self.database_folder = os.path.join(self.datafolder, "database")

if not os.path.exists(self.datafolder):
os.mkdir(self.datafolder)
if not os.path.exists(self.ca_certs_folder):
os.mkdir(self.ca_certs_folder)
if not os.path.exists(self.certs_folder):
os.mkdir(self.certs_folder)
if not os.path.exists(self.database_folder):
os.mkdir(self.database_folder)

if not os.path.exists(os.path.join(self.datafolder, "settings.dat")):
key = Fernet.generate_key()
file = open(os.path.join(self.datafolder, "settings.dat"), 'wb')
file.write(key) # The key is type bytes still
file.close()


# settings.dat key
file = open(os.path.join(self.datafolder, "settings.dat"), 'rb')
key = file.read() # The key will be type bytes
self.salt = file.read()
self.key = Fernet(key)
file.close()


config = ApiConfig()
76 changes: 76 additions & 0 deletions api/Main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
import logging
from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import atexit

from api.Config import config
from api.db import database
from api.scheduler.Scheduler import scheduler

api = FastAPI()
api.secret_key = os.getenv('SECRET_KEY')

# Configure CORS to allow API requests from NPM
origins = [
"http://127.0.0.1:8081", # allow npm run serve development
]
api.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

api.add_middleware(GZipMiddleware, minimum_size=1000)

logger = logging.getLogger("api")

from .routes import auth, phone_data, settings_management

api.include_router(
auth.router,
prefix='/auth',
tags=["Authentication"]
)

api.include_router(
phone_data.router,
prefix='/phonedata',
tags=["Phone Data Functions"]
)

api.include_router(
settings_management.router,
prefix='/settings_management',
tags=["Settings Management Functions"]
)

api.mount("/home", StaticFiles(directory=os.path.join(config.basedir,"client","dist"), html=True), name="vue-client")

# redirect root to home static path
@api.get("/")
async def redirect():
response = RedirectResponse(url='/home/index.html')
return response

# FAST API Startup tasks

@api.on_event('startup')
async def startup_event():
scheduler.start()


# FAST API Shutdown tasks

@api.on_event("shutdown")
async def fastapi_stopped():
scheduler.shutdown()




Empty file added api/__init__.py
Empty file.
Empty file added api/crud/__init__.py
Empty file.
Loading

0 comments on commit 8ded0a4

Please sign in to comment.