Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ lib/__pycache__/milenage.cpython-36.pyc
.gitignore
.vscode/settings.json
.cspell/custom-dictionary-workspace.txt
pyhss.egg-info
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ The underlying library - ``diameter.py`` can be easily worked with to add suppor

## Usage

Basic configuration is set in the ``config.yaml`` file,
Basic configuration is set in the `config.yaml` file, which gets loaded from:
* The path in the `PYHSS_CONFIG` environment variable (if set)
* `/etc/pyhss/config.yaml`
* `/usr/share/pyhss/config.yaml`
* The same directory as this `README.md`

You will need to set the IP address to bind to (IPv4 or IPv6), the Diameter hostname, realm, your PLMN and transport type to use (SCTP or TCP).

Expand Down
13 changes: 1 addition & 12 deletions lib/S6a_crypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,7 @@
import logging
import os
import sys
sys.path.append(os.path.realpath('../'))
import yaml

try:
with open("../config.yaml", 'r') as stream:
config = (yaml.safe_load(stream))
except:
with open("config.yaml", 'r') as stream:
config = (yaml.safe_load(stream))

# logtool = logtool.LogTool()
# logtool.setup_logger('CryptoLogger', yaml_config['logging']['logfiles']['database_logging_file'], level=yaml_config['logging']['level'])

CryptoLogger = logging.getLogger('CryptoLogger')

CryptoLogger.info("Initialised Diameter Logger, importing database")
Expand Down
98 changes: 43 additions & 55 deletions lib/database.py

Large diffs are not rendered by default.

70 changes: 34 additions & 36 deletions lib/diameter.py

Large diffs are not rendered by default.

15 changes: 6 additions & 9 deletions lib/diameterAsync.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#Diameter Packet Decoder / Encoder & Tools
import math
import asyncio
import yaml
import uuid
import socket
import traceback
import binascii
from messagingAsync import RedisMessagingAsync
from pyhss_config import config


class DiameterAsync:
Expand Down Expand Up @@ -50,13 +50,10 @@ def __init__(self, logTool):
{"commandCode": 8388622, "applicationId": 16777291, "responseMethod": self.Answer_16777291_8388622, "failureResultCode": 4100 ,"requestAcronym": "LRR", "responseAcronym": "LRA", "requestName": "LCS Routing Info Request", "responseName": "LCS Routing Info Answer"},
]

with open("../config.yaml", 'r') as stream:
self.config = (yaml.safe_load(stream))

self.redisUseUnixSocket = self.config.get('redis', {}).get('useUnixSocket', False)
self.redisUnixSocketPath = self.config.get('redis', {}).get('unixSocketPath', '/var/run/redis/redis-server.sock')
self.redisHost = self.config.get('redis', {}).get('host', 'localhost')
self.redisPort = self.config.get('redis', {}).get('port', 6379)
self.redisUseUnixSocket = config.get('redis', {}).get('useUnixSocket', False)
self.redisUnixSocketPath = config.get('redis', {}).get('unixSocketPath', '/var/run/redis/redis-server.sock')
self.redisHost = config.get('redis', {}).get('host', 'localhost')
self.redisPort = config.get('redis', {}).get('port', 6379)
self.redisMessaging = RedisMessagingAsync(host=self.redisHost, port=self.redisPort, useUnixSocket=self.redisUseUnixSocket, unixSocketPath=self.redisUnixSocketPath)

self.logTool = logTool
Expand Down Expand Up @@ -469,4 +466,4 @@ async def Answer_16777236_274(self):
pass

async def Answer_16777238_258(self):
pass
pass
6 changes: 4 additions & 2 deletions lib/logtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import os, sys, time
import socket
from datetime import datetime
sys.path.append(os.path.realpath('../'))

sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/../lib"))

import asyncio
from messagingAsync import RedisMessagingAsync
from messaging import RedisMessaging
Expand Down Expand Up @@ -97,4 +99,4 @@ def setupFileLogger(self, loggerName: str, logFilePath: str):
rolloverHandler.setFormatter(formatter)
fileLogger.addHandler(rolloverHandler)
fileLogger.setLevel(logging.DEBUG)
return fileLogger
return fileLogger
1 change: 0 additions & 1 deletion lib/milenage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import logging
import os
import sys
sys.path.append(os.path.realpath('../'))

CryptoLogger = logging.getLogger('CryptoLogger')

Expand Down
54 changes: 54 additions & 0 deletions lib/pyhss_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Copyright (C) 2025 sysmocom - s.f.m.c. GmbH <info@sysmocom.de>

SPDX-License-Identifier: AGPL-3.0-or-later

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

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 <https://www.gnu.org/licenses/>.
"""
import os
import sys
import yaml
from pathlib import Path

config = None


def load_config():
global config

if "PYHSS_CONFIG" in os.environ:
paths = [os.environ["PYHSS_CONFIG"]]
if not os.path.exists(paths[0]):
print(f"ERROR: PYHSS_CONFIG is set, but file does not exist: {paths[0]}")
sys.exit(1)
else:
paths = [
"/etc/pyhss/config.yaml",
"/usr/share/pyhss/config.yaml",
Path(__file__).resolve().parent.parent / "config.yaml",
]

for path in paths:
if os.path.exists(path):
with open(path, "r") as stream:
config = yaml.safe_load(stream)
return

print("ERROR: failed to find PyHSS config, tried these paths:")
for path in paths:
print(f" * {path}")
sys.exit(1)


load_config()
30 changes: 30 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "pyhss"
version = "1.0.2"

[project.scripts]
pyhss_api = "pyhss.services.apiService:main"
pyhss_database = "pyhss.services.databaseService:main"
pyhss_diameter = "pyhss.services.diameterService:main"
pyhss_geored = "pyhss.services.georedService:main"
pyhss_gsup = "pyhss.services.gsupService:main"
pyhss_hss = "pyhss.services.hssService:main"
pyhss_log = "pyhss.services.logService:main"
pyhss_metric = "pyhss.services.metricService:main"

[tool.setuptools]
packages = [
"pyhss.lib",
"pyhss.lib.gsup",
"pyhss.lib.gsup.controller",
"pyhss.lib.gsup.protocol",
"pyhss.services",
"pyhss.tools",
]

[tool.setuptools.package-dir]
pyhss = ""
14 changes: 9 additions & 5 deletions services/apiService.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from werkzeug.middleware.proxy_fix import ProxyFix
from functools import wraps
import os
sys.path.append(os.path.realpath('../lib'))

sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/../lib"))

import time
import requests
import traceback
Expand All @@ -15,10 +17,8 @@
from diameter import Diameter
from messaging import RedisMessaging
import database
import yaml
from pyhss_config import config

with open("../config.yaml", 'r') as stream:
config = (yaml.safe_load(stream))

siteName = config.get("hss", {}).get("site_name", "")
originHostname = socket.gethostname()
Expand Down Expand Up @@ -2372,6 +2372,10 @@ def put(self, imsi):
response_json = {'result': 'Failed', 'Reason' : "Unable to send CLR: " + str(E)}
return response_json

if __name__ == '__main__':

def main():
apiService.run(debug=False, host='0.0.0.0', port=8080)


if __name__ == '__main__':
main()
43 changes: 22 additions & 21 deletions services/databaseService.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import os, sys, json, yaml
import os, sys, json
import uuid, time
import asyncio
import socket
import datetime
import traceback
sys.path.append(os.path.realpath('../lib'))

sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/../lib"))

from messagingAsync import RedisMessagingAsync
from banners import Banners
from logtool import LogTool
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker
from sqlalchemy import MetaData, Table
from pyhss_config import config

class DatabaseService:
"""
Expand All @@ -20,35 +23,29 @@ class DatabaseService:
"""

def __init__(self, redisHost: str='127.0.0.1', redisPort: int=6379):
try:
with open("../config.yaml", "r") as self.configFile:
self.config = yaml.safe_load(self.configFile)
except:
print(f"[Database] Fatal Error - config.yaml not found, exiting.")
quit()
self.logTool = LogTool(self.config)
self.logTool = LogTool(config)
self.banners = Banners()

self.redisUseUnixSocket = self.config.get('redis', {}).get('useUnixSocket', False)
self.redisUnixSocketPath = self.config.get('redis', {}).get('unixSocketPath', '/var/run/redis/redis-server.sock')
self.redisHost = self.config.get('redis', {}).get('host', 'localhost')
self.redisPort = self.config.get('redis', {}).get('port', 6379)
self.redisUseUnixSocket = config.get('redis', {}).get('useUnixSocket', False)
self.redisUnixSocketPath = config.get('redis', {}).get('unixSocketPath', '/var/run/redis/redis-server.sock')
self.redisHost = config.get('redis', {}).get('host', 'localhost')
self.redisPort = config.get('redis', {}).get('port', 6379)
self.redisDatabaseReadMessaging = RedisMessagingAsync(host=self.redisHost, port=self.redisPort, useUnixSocket=self.redisUseUnixSocket, unixSocketPath=self.redisUnixSocketPath)
self.redisLogMessaging = RedisMessagingAsync(host=self.redisHost, port=self.redisPort, useUnixSocket=self.redisUseUnixSocket, unixSocketPath=self.redisUnixSocketPath)
self.hostname = socket.gethostname()

supportedDatabaseTypes = ["mysql", "postgresql"]
self.databaseType = self.config.get('database', {}).get('db_type', 'mysql').lower()
self.databaseType = config.get('database', {}).get('db_type', 'mysql').lower()
if not self.databaseType in supportedDatabaseTypes:
print(f"[Database] Fatal Error - unsupported database type: {self.databaseType}. Supported database types are: {supportedDatabaseTypes}, exiting.")
quit()

self.databaseHost = self.config.get('database', {}).get('server', '')
self.databaseUsername = self.config.get('database', {}).get('username', '')
self.databasePassword = self.config.get('database', {}).get('password', '')
self.database = self.config.get('database', {}).get('database', '')
self.readCacheEnabled = self.config.get('database', {}).get('readCacheEnabled', True)
self.cacheReadInterval = int(self.config.get('database', {}).get('cacheReadInterval', 60))
self.databaseHost = config.get('database', {}).get('server', '')
self.databaseUsername = config.get('database', {}).get('username', '')
self.databasePassword = config.get('database', {}).get('password', '')
self.database = config.get('database', {}).get('database', '')
self.readCacheEnabled = config.get('database', {}).get('readCacheEnabled', True)
self.cacheReadInterval = int(config.get('database', {}).get('cacheReadInterval', 60))

self.sqlAlchemyEngine = create_engine(
f"{self.databaseType}://{self.databaseUsername}:{self.databasePassword}@{self.databaseHost}/{self.database}"
Expand Down Expand Up @@ -139,6 +136,10 @@ async def startService(self):
pass


if __name__ == '__main__':
def main():
databaseService = DatabaseService()
asyncio.run(databaseService.startService())


if __name__ == '__main__':
main()
Loading