server alive

This commit is contained in:
Tykayn 2025-09-15 23:54:04 +02:00 committed by tykayn
parent 1c17b57d8a
commit fab0e979d5
11 changed files with 353 additions and 28 deletions

86
DB_CONNECTION_FIX.md Normal file
View file

@ -0,0 +1,86 @@
# PostgreSQL Authentication Fix
## Issue Description
The server was failing to connect to the PostgreSQL database with the following error:
```
[OEDB] [ERROR] Failed to connect to PostgreSQL database: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "cipherbliss"
```
This error occurs because the application was trying to connect to PostgreSQL using Unix socket connections, which default to "peer" authentication (using the operating system username). However, the database was configured to use password authentication, as evidenced by the fact that DBeaver could connect using the password from the `.env` file.
## Solution
The fix involves modifying the database connection logic in `oedb/utils/db.py` to:
1. Force TCP/IP connections instead of Unix socket connections by setting the host to "localhost" when it's empty
2. Add appropriate connection parameters for local connections
3. Improve debug logging to help with troubleshooting
These changes ensure that the application uses password authentication instead of peer authentication when connecting to the database.
## Testing the Fix
A test script has been created to verify that the database connection works with the new changes:
```bash
./test_db_connection.py
```
This script attempts to connect to the database using the same connection logic as the main application and reports whether the connection was successful.
## Technical Details
### Changes Made
1. Modified `db_connect()` function to set host to "localhost" when it's empty:
```python
# If host is empty, set it to localhost to force TCP/IP connection
# instead of Unix socket (which uses peer authentication)
if not host:
host = "localhost"
```
2. Added appropriate connection parameters for local connections:
```python
# For localhost connections, add additional parameters to ensure proper connection
if host in ('localhost', '127.0.0.1'):
logger.debug("Using TCP/IP connection with additional parameters")
return psycopg2.connect(
dbname=dbname,
host=host,
password=password,
user=user,
options="-c client_encoding=utf8 -c statement_timeout=3000",
connect_timeout=3,
application_name="oedb",
# Disable SSL for local connections
sslmode='disable')
```
3. Improved debug logging to help with troubleshooting:
```python
logger.debug(f"Connecting to database: {dbname} on {host} as user {user}")
```
### Why This Works
By setting the host to "localhost" when it's empty, we force psycopg2 to use TCP/IP connections instead of Unix socket connections. TCP/IP connections typically use password authentication, which is what the database is configured to use.
The additional connection parameters for local connections help ensure that the connection is established correctly and provide useful information for debugging.
## Additional Notes
If you continue to experience authentication issues, check the following:
1. Make sure your `.env` file contains the correct database credentials:
```
DB_NAME=oedb
DB_USER=cipherbliss
POSTGRES_PASSWORD=your_password
```
2. Verify that your PostgreSQL server is configured to allow password authentication for the specified user.
3. Check the PostgreSQL logs for more detailed error messages.

View file

@ -20,6 +20,7 @@ from oedb.middleware.headers import HeaderMiddleware
from oedb.resources.event import event
from oedb.resources.stats import StatsResource
from oedb.resources.search import EventSearch
from oedb.resources.root import root
def create_app():
"""
@ -43,10 +44,11 @@ def create_app():
# Add routes
logger.info("Setting up API routes")
app.add_route('/', root) # Handle root requests
app.add_route('/event/search', event_search) # Handle event search requests
app.add_route('/event/{id}', event) # Handle single event requests
app.add_route('/event', event) # Handle event collection requests
app.add_route('/stats', stats) # Handle stats requests
app.add_route('/event/search', event_search) # Handle event search requests
logger.success("Application initialized successfully")
return app

View file

@ -7,6 +7,7 @@ import re
import falcon
import psycopg2
import psycopg2.extras
import psycopg2.errors
from oedb.models.event import BaseEvent
from oedb.utils.db import db_connect
from oedb.utils.serialization import dumps
@ -219,7 +220,7 @@ class EventResource(BaseEvent):
event_sort=event_sort, limit=limit)
logger.debug(f"Executing SQL: {sql}")
cur.execute(sql)
resp.body = dumps(self.rows_to_collection(cur.fetchall(), geom_only))
resp.text = dumps(self.rows_to_collection(cur.fetchall(), geom_only))
resp.status = falcon.HTTP_200
logger.success(f"Successfully processed GET request to /event")
else:
@ -229,16 +230,24 @@ class EventResource(BaseEvent):
e = cur.fetchone()
if e is not None:
resp.body = dumps(self.row_to_feature(e))
resp.text = dumps(self.row_to_feature(e))
resp.status = falcon.HTTP_200
logger.success(f"Successfully processed GET request to /event/{id}")
else:
resp.status = falcon.HTTP_404
logger.warning(f"Event not found: {id}")
except psycopg2.errors.InsufficientPrivilege as e:
logger.error(f"Permission denied for database table: {e}")
resp.status = falcon.HTTP_500
resp.text = dumps({
"error": "Database permission error",
"message": "The server does not have permission to access the required database tables. Please contact the administrator to fix this issue.",
"details": str(e)
})
except Exception as e:
logger.error(f"Error processing GET request: {e}")
resp.status = falcon.HTTP_500
resp.body = dumps({"error": str(e)})
resp.text = dumps({"error": str(e)})
finally:
db.close()
@ -260,29 +269,29 @@ class EventResource(BaseEvent):
j = json.loads(body)
except Exception as e:
logger.error(f"Invalid JSON or bad encoding: {e}")
resp.body = 'invalid json or bad encoding'
resp.text = 'invalid json or bad encoding'
resp.status = falcon.HTTP_400
return
resp.body = ''
resp.text = ''
if "properties" not in j:
resp.body = resp.body + "missing 'properties' elements\n"
resp.text = resp.text + "missing 'properties' elements\n"
j['properties'] = dict()
if "geometry" not in j:
resp.body = resp.body + "missing 'geometry' elements\n"
resp.text = resp.text + "missing 'geometry' elements\n"
j['geometry'] = None
if "when" not in j['properties'] and ("start" not in j['properties'] or "stop" not in j['properties']):
resp.body = resp.body + "missing 'when' or 'start/stop' in properties\n"
resp.text = resp.text + "missing 'when' or 'start/stop' in properties\n"
j['properties']['when'] = None
if "type" not in j['properties']:
resp.body = resp.body + "missing 'type' of event in properties\n"
resp.text = resp.text + "missing 'type' of event in properties\n"
j['properties']['type'] = None
if "what" not in j['properties']:
resp.body = resp.body + "missing 'what' in properties\n"
resp.text = resp.text + "missing 'what' in properties\n"
j['properties']['what'] = None
if "type" in j and j['type'] != 'Feature':
resp.body = resp.body + 'geojson must be "type":"Feature" only\n'
if id is None and resp.body != '':
resp.text = resp.text + 'geojson must be "type":"Feature" only\n'
if id is None and resp.text != '':
resp.status = falcon.HTTP_400
resp.set_header('Content-type', 'text/plain')
return
@ -319,7 +328,7 @@ class EventResource(BaseEvent):
geometry = dumps(j['geometry'])
h = self.maybe_insert_geometry(geometry, cur)
if len(h) > 1 and h[1] is False:
resp.body = "invalid geometry: %s\n" % h[2]
resp.text = "invalid geometry: %s\n" % h[2]
resp.status = falcon.HTTP_400
resp.set_header('Content-type', 'text/plain')
return
@ -361,11 +370,11 @@ class EventResource(BaseEvent):
AND e.events_when=tstzrange(coalesce(%s, lower(s.events_when)),coalesce(%s,upper(s.events_when)),%s) AND e.events_geo=coalesce(%s, s.events_geo);""",
(id, j['properties']['what'], event_start, event_stop, bounds, h[0]))
dupe = cur.fetchone()
resp.body = """{"duplicate":"%s"}""" % (dupe[0])
resp.text = """{"duplicate":"%s"}""" % (dupe[0])
resp.status = '409 Conflict with event %s' % dupe[0]
logger.warning(f"Duplicate event: {dupe[0]}")
else:
resp.body = """{"id":"%s"}""" % (e[0])
resp.text = """{"id":"%s"}""" % (e[0])
if id is None:
resp.status = falcon.HTTP_201
logger.success(f"Event created with ID: {e[0]}")
@ -455,7 +464,7 @@ class EventResource(BaseEvent):
except Exception as e:
logger.error(f"Error deleting event {id}: {e}")
resp.status = falcon.HTTP_500
resp.body = dumps({"error": str(e)})
resp.text = dumps({"error": str(e)})
db.rollback()
finally:
cur.close()

49
oedb/resources/root.py Normal file
View file

@ -0,0 +1,49 @@
"""
Root resource for the OpenEventDatabase.
"""
import falcon
from oedb.utils.serialization import dumps
from oedb.utils.logging import logger
class RootResource:
"""
Resource for the root endpoint.
Handles the / endpoint.
"""
def on_get(self, req, resp):
"""
Handle GET requests to the / endpoint.
Returns a JSON response with available routes and a welcome message.
Args:
req: The request object.
resp: The response object.
"""
logger.info("Processing GET request to /")
try:
# Create a response with available routes and a welcome message
response = {
"message": "Welcome to the OpenEventDatabase API!",
"available_routes": {
"/": "This endpoint - provides information about available routes",
"/event": "Get events matching specified criteria",
"/event/{id}": "Get a specific event by ID",
"/event/search": "Search for events using a GeoJSON geometry",
"/stats": "Get statistics about the database"
}
}
# Set the response body and status
resp.text = dumps(response)
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /")
except Exception as e:
logger.error(f"Error processing GET request to /: {e}")
resp.status = falcon.HTTP_500
resp.text = dumps({"error": str(e)})
# Create a global instance of RootResource
root = RootResource()

View file

@ -33,7 +33,7 @@ class EventSearch(BaseEvent):
if 'geometry' not in j:
logger.warning("Request body missing 'geometry' field")
resp.status = falcon.HTTP_400
resp.body = json.dumps({"error": "Request body must contain a 'geometry' field"})
resp.text = json.dumps({"error": "Request body must contain a 'geometry' field"})
return
# Pass the query with the geometry to event.on_get
@ -44,8 +44,8 @@ class EventSearch(BaseEvent):
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON: {e}")
resp.status = falcon.HTTP_400
resp.body = json.dumps({"error": "Invalid JSON in request body"})
resp.text = json.dumps({"error": "Invalid JSON in request body"})
except Exception as e:
logger.error(f"Error processing POST request to /event/search: {e}")
resp.status = falcon.HTTP_500
resp.body = json.dumps({"error": str(e)})
resp.text = json.dumps({"error": str(e)})

View file

@ -45,13 +45,13 @@ class StatsResource:
cur.execute("SELECT row_to_json(stat) from (SELECT events_what as what, left(max(upper(events_when))::text,19) as last, count(*) as count, array_agg(distinct(regexp_replace(regexp_replace(events_tags ->> 'source','^(http://|https://)',''),'/.*',''))) as source from (select * from events order by lastupdate desc limit 10000) as last group by 1 order by 2 desc) as stat;")
recent = cur.fetchall()
resp.body = dumps(dict(events_count=count, last_updated=last, uptime=uptime, db_uptime=pg_uptime, recent=recent))
resp.text = dumps(dict(events_count=count, last_updated=last, uptime=uptime, db_uptime=pg_uptime, recent=recent))
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /stats")
except Exception as e:
logger.error(f"Error processing GET request to /stats: {e}")
resp.status = falcon.HTTP_500
resp.body = dumps({"error": str(e)})
resp.text = dumps({"error": str(e)})
finally:
cur.close()
db.close()

View file

@ -8,6 +8,20 @@ import psycopg2
import psycopg2.extras
from oedb.utils.logging import logger
def load_env_from_file():
"""
Load environment variables from .env file.
This ensures that database connection parameters are properly set.
"""
if os.path.exists('.env'):
logger.info("Loading environment variables from .env file...")
with open('.env', 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
key, value = line.split('=', 1)
os.environ[key] = value
def db_connect():
"""
Connect to the PostgreSQL database using environment variables.
@ -15,11 +29,43 @@ def db_connect():
Returns:
psycopg2.connection: A connection to the database.
"""
return psycopg2.connect(
dbname=os.getenv("DB_NAME", "oedb"),
host=os.getenv("DB_HOST", ""),
password=os.getenv("POSTGRES_PASSWORD", None),
user=os.getenv("DB_USER", ""))
# Load environment variables from .env file
load_env_from_file()
# Get connection parameters from environment variables
dbname = os.getenv("DB_NAME", "oedb")
host = os.getenv("DB_HOST", "")
password = os.getenv("POSTGRES_PASSWORD", None)
user = os.getenv("DB_USER", "")
# If host is empty, set it to localhost to force TCP/IP connection
# instead of Unix socket (which uses peer authentication)
if not host:
host = "localhost"
logger.debug(f"Connecting to database: {dbname} on {host} as user {user}")
# For localhost connections, add additional parameters to ensure proper connection
if host in ('localhost', '127.0.0.1'):
logger.debug("Using TCP/IP connection with additional parameters")
return psycopg2.connect(
dbname=dbname,
host=host,
password=password,
user=user,
options="-c client_encoding=utf8 -c statement_timeout=3000",
connect_timeout=3,
application_name="oedb",
# Disable SSL for local connections
sslmode='disable')
else:
# For remote connections, use standard parameters
logger.debug("Using standard connection parameters")
return psycopg2.connect(
dbname=dbname,
host=host,
password=password,
user=user)
def check_db_connection():
"""

View file

@ -1,4 +1,4 @@
falcon>=3.1.0
falcon
psycopg2-binary
geojson
gunicorn

22
run_tests.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
# Script to start the server in the background and run tests
echo "Starting the server in the background..."
python3 backend.py > server.log 2>&1 &
SERVER_PID=$!
# Give the server a moment to start
sleep 2
echo "Running tests..."
python3 test_api.py
TEST_RESULT=$?
echo "Stopping the server (PID: $SERVER_PID)..."
kill $SERVER_PID
# Wait for the server to stop
sleep 1
echo "Test completed with exit code: $TEST_RESULT"
exit $TEST_RESULT

87
test_api.py Executable file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
Test script to verify API endpoints are accessible.
"""
import requests
import sys
import json
import time
BASE_URL = "http://127.0.0.1:8080"
def test_endpoint(endpoint, method="GET", data=None):
"""Test an API endpoint and print the result."""
url = f"{BASE_URL}{endpoint}"
print(f"Testing {method} {url}...")
try:
if method == "GET":
response = requests.get(url)
elif method == "POST":
headers = {"Content-Type": "application/json"}
response = requests.post(url, data=json.dumps(data), headers=headers)
print(f"Status code: {response.status_code}")
if response.status_code < 400:
print("Success!")
else:
print(f"Error: {response.text}")
print("-" * 50)
return response.status_code < 400
except Exception as e:
print(f"Exception: {e}")
print("-" * 50)
return False
def main():
"""Run tests for all endpoints."""
# Wait for server to start
print("Waiting for server to start...")
max_retries = 5
retries = 0
while retries < max_retries:
try:
requests.get(f"{BASE_URL}/")
print("Server is running!")
break
except requests.exceptions.ConnectionError:
print(f"Server not ready yet, retrying in 2 seconds... ({retries+1}/{max_retries})")
retries += 1
time.sleep(2)
if retries == max_retries:
print("Could not connect to server after multiple attempts.")
print("Please make sure the server is running on http://127.0.0.1:8080")
return 1
success = True
# Test root endpoint
success = test_endpoint("/") and success
# Test event endpoint
success = test_endpoint("/event") and success
# Test event/search endpoint with POST
search_data = {
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566] # Paris coordinates
}
}
success = test_endpoint("/event/search", method="POST", data=search_data) and success
# Test stats endpoint
success = test_endpoint("/stats") and success
if success:
print("All tests passed!")
return 0
else:
print("Some tests failed!")
return 1
if __name__ == "__main__":
sys.exit(main())

24
test_db_connection.py Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""
Test script to verify database connection.
This script attempts to connect to the PostgreSQL database using the same
connection logic as the main application.
"""
import sys
from oedb.utils.db import check_db_connection
from oedb.utils.logging import logger
def main():
"""Test database connection and print the result."""
logger.info("Testing database connection...")
if check_db_connection():
logger.success("Database connection successful!")
return 0
else:
logger.error("Database connection failed. Check your .env file and PostgreSQL configuration.")
return 1
if __name__ == "__main__":
sys.exit(main())