Compare commits
15 Commits
0b4322d671
...
handle_cor
| Author | SHA1 | Date | |
|---|---|---|---|
|
8ec34cfbeb
|
|||
|
479f990044
|
|||
|
078514b69e
|
|||
|
7754b77b82
|
|||
|
87a9b1dd5f
|
|||
|
98643f9a69
|
|||
|
e77e4dc174
|
|||
| a95dfaae2b | |||
|
4eb906f6d4
|
|||
|
878a2b6bbf
|
|||
|
d090e90ee2
|
|||
|
38416f103f
|
|||
|
484e548c46
|
|||
|
1fa64feefc
|
|||
|
f78ed7592d
|
17
.gitea/workflows/bandit.yml
Normal file
17
.gitea/workflows/bandit.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Bandit
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
|
||||||
|
# XXX need to do stuff with uv
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: freebsd
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Analyse code with Bandit
|
||||||
|
run: |
|
||||||
|
bandit -r .
|
||||||
17
.gitea/workflows/flake8.yml
Normal file
17
.gitea/workflows/flake8.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Flake8
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
|
||||||
|
# XXX need to do stuff with uv
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: freebsd
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Analyse code with Flake8
|
||||||
|
run: |
|
||||||
|
flake8 $(git ls-files '*.py')
|
||||||
17
.gitea/workflows/mypy.yml
Normal file
17
.gitea/workflows/mypy.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Mypy
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
|
||||||
|
# XXX need to do stuff with uv
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: freebsd
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Analyse code with Mypy
|
||||||
|
run: |
|
||||||
|
mypy --install-types --non-interactive $(git ls-files '*.py')
|
||||||
23
.gitea/workflows/pip-audit.yml
Normal file
23
.gitea/workflows/pip-audit.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: pip-audit
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # Weekly on Sunday
|
||||||
|
|
||||||
|
# XXX need to do stuff with uv
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: freebsd
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Check vulnerable components with pip-audit
|
||||||
|
run: |
|
||||||
|
pip-audit .
|
||||||
|
|
||||||
17
.gitea/workflows/pylint.yml
Normal file
17
.gitea/workflows/pylint.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Pylint
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
|
||||||
|
# XXX need to do stuff with uv
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: freebsd
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Analyse code with Pylint
|
||||||
|
run: |
|
||||||
|
pylint $(git ls-files '*.py')
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -160,3 +160,5 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.sw?
|
||||||
|
|||||||
54
app/main.py
54
app/main.py
@ -1,18 +1,41 @@
|
|||||||
'''
|
'''
|
||||||
Simple Geolocation with FastAPI
|
Simple Geolocation with FastAPI
|
||||||
'''
|
'''
|
||||||
|
import os
|
||||||
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
|
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
|
||||||
from typing import Annotated, Optional, Union
|
from typing import Annotated, Optional, Union
|
||||||
|
|
||||||
import geoip2.database
|
import geoip2.database
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import Body, FastAPI, Path, Request, Response, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
from geoip2.errors import AddressNotFoundError
|
from geoip2.errors import AddressNotFoundError
|
||||||
from fastapi import FastAPI, Path, Body, Response, status
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
GEOLITE2_ASN_DB = '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb'
|
# Configure CORS from environment variables
|
||||||
GEOLITE2_CITY_DB = '/usr/local/share/GeoIP/GeoLite2-City.mmdb'
|
cors_origins = os.getenv('CORS_ALLOW_ORIGINS', 'http://localhost')
|
||||||
|
allow_origins = [origin.strip() for origin in cors_origins.split(',')
|
||||||
|
if origin.strip()]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=allow_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
GEOLITE2_ASN_DB = os.getenv('GEOLITE2_ASN_DB',
|
||||||
|
'/usr/local/share/GeoIP/GeoLite2-ASN.mmdb')
|
||||||
|
GEOLITE2_CITY_DB = os.getenv('GEOLITE2_CITY_DB',
|
||||||
|
'/usr/local/share/GeoIP/GeoLite2-City.mmdb')
|
||||||
|
|
||||||
|
|
||||||
class IPAddressParam(BaseModel):
|
class IPAddressParam(BaseModel):
|
||||||
'''
|
'''
|
||||||
@ -20,6 +43,7 @@ class IPAddressParam(BaseModel):
|
|||||||
'''
|
'''
|
||||||
ip: Union[IPv6Address, IPv4Address]
|
ip: Union[IPv6Address, IPv4Address]
|
||||||
|
|
||||||
|
|
||||||
class Locality(BaseModel):
|
class Locality(BaseModel):
|
||||||
'''
|
'''
|
||||||
Locality data
|
Locality data
|
||||||
@ -29,6 +53,7 @@ class Locality(BaseModel):
|
|||||||
continent: Optional[str] = None
|
continent: Optional[str] = None
|
||||||
is_eu: bool = False
|
is_eu: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GeoLocation(BaseModel):
|
class GeoLocation(BaseModel):
|
||||||
'''
|
'''
|
||||||
Geolocation data model
|
Geolocation data model
|
||||||
@ -39,9 +64,12 @@ class GeoLocation(BaseModel):
|
|||||||
network: Optional[Union[IPv6Network, IPv4Network]] = None
|
network: Optional[Union[IPv6Network, IPv4Network]] = None
|
||||||
locality: Locality = Locality()
|
locality: Locality = Locality()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/")
|
@app.post("/")
|
||||||
async def root_post(ipaddresses: Annotated[list[IPAddressParam],
|
async def root_post(ipaddresses: Annotated[
|
||||||
Body(title="The IPAddresses to geolocate")]
|
list[IPAddressParam],
|
||||||
|
Body(title="The IPAddresses to geolocate")],
|
||||||
|
response: Response
|
||||||
) -> list[GeoLocation]:
|
) -> list[GeoLocation]:
|
||||||
'''
|
'''
|
||||||
Return GeoLocation item(s) for a list of IPAddressParam objects
|
Return GeoLocation item(s) for a list of IPAddressParam objects
|
||||||
@ -71,19 +99,31 @@ async def root_post(ipaddresses: Annotated[list[IPAddressParam],
|
|||||||
ip=ipaddress.ip
|
ip=ipaddress.ip
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if geolocations:
|
||||||
|
response.headers['Cache-Control'] = 'private, max-age=604800'
|
||||||
return geolocations
|
return geolocations
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{ipaddress}")
|
@app.get("/{ipaddress}")
|
||||||
async def root_get(ipaddress: Annotated[Union[IPv4Address,IPv6Address],
|
async def root_get(ipaddress: Annotated[
|
||||||
|
Union[IPv4Address, IPv6Address],
|
||||||
Path(title="The IPAddress to geolocate")],
|
Path(title="The IPAddress to geolocate")],
|
||||||
response: Response
|
response: Response
|
||||||
) -> GeoLocation:
|
) -> GeoLocation:
|
||||||
'''
|
'''
|
||||||
Look up geolocation for ip in path parameter
|
Look up geolocation for ip in path parameter
|
||||||
'''
|
'''
|
||||||
locations = await root_post([IPAddressParam(ip=ipaddress)])
|
locations = await root_post([IPAddressParam(ip=ipaddress)], response)
|
||||||
if locations:
|
if locations:
|
||||||
|
response.headers['Cache-Control'] = 'private, max-age=604800'
|
||||||
return locations.pop()
|
return locations.pop()
|
||||||
response.status_code = status.HTTP_404_NOT_FOUND
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
return GeoLocation()
|
return GeoLocation()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root_redirect(req: Request) -> RedirectResponse:
|
||||||
|
'''
|
||||||
|
Redirect empty request using REMOTE_ADDR
|
||||||
|
'''
|
||||||
|
return RedirectResponse(url=str(req.url) + str(req.client.host))
|
||||||
|
|||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
geoip2==4.7.0
|
||||||
|
fastapi==0.115.6
|
||||||
0
run/.gitkeep
Normal file
0
run/.gitkeep
Normal file
24
start.sh
Executable file
24
start.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
NAME=ismijnipverweg
|
||||||
|
DIR=/opt/apps/ismijnipverweg/app
|
||||||
|
USER=www
|
||||||
|
GROUP=nobody
|
||||||
|
WORKERS=3
|
||||||
|
WORKER_CLASS=uvicorn.workers.UvicornWorker
|
||||||
|
#VENV=$DIR/.venv/bin/activate
|
||||||
|
BIND=unix:$DIR/../run/gunicorn.sock
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
cd $DIR
|
||||||
|
# source $VENV
|
||||||
|
|
||||||
|
exec /usr/local/bin/gunicorn main:app \
|
||||||
|
--name $NAME \
|
||||||
|
--workers $WORKERS \
|
||||||
|
--worker-class $WORKER_CLASS \
|
||||||
|
--user=$USER \
|
||||||
|
--group=$GROUP \
|
||||||
|
--bind=$BIND \
|
||||||
|
--log-level=$LOG_LEVEL \
|
||||||
|
--log-file=-
|
||||||
Reference in New Issue
Block a user