2021-06-02 12:36:59 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
import os
|
|
|
|
import socket
|
|
|
|
from datetime import datetime
|
|
|
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
|
|
from prometheus_client import Counter
|
2021-06-04 12:51:42 +00:00
|
|
|
from pymongo import ReturnDocument
|
2021-06-02 12:36:59 +00:00
|
|
|
from sanic import Sanic, response
|
|
|
|
from sanic.exceptions import InvalidUsage, NotFound
|
|
|
|
from sanic_prometheus import monitor
|
|
|
|
from sanic_wtf import SanicForm
|
|
|
|
from wtforms import IntegerField, StringField
|
|
|
|
from wtforms.validators import DataRequired, IPAddress, NumberRange
|
|
|
|
|
2021-06-14 19:02:30 +00:00
|
|
|
submit_count = Counter(
|
|
|
|
"pinecrypt_gateway_lease_updates",
|
|
|
|
"Client IP address updates.",
|
|
|
|
["service"])
|
|
|
|
flush_count = Counter(
|
|
|
|
"pinecrypt_gateway_lease_flushes",
|
|
|
|
"Client IP address flushes.",
|
|
|
|
["service"])
|
|
|
|
migration_count = Counter(
|
|
|
|
"pinecrypt_gateway_lease_migrations",
|
|
|
|
"Client migrations to this replica.",
|
|
|
|
["replica"])
|
|
|
|
not_found_count = Counter(
|
|
|
|
"pinecrypt_gateway_lease_not_found",
|
|
|
|
"Invalid connection attempts.",
|
|
|
|
["service"])
|
2021-06-02 12:36:59 +00:00
|
|
|
|
|
|
|
class LeaseUpdateForm(SanicForm):
|
|
|
|
service = StringField("Service name")
|
|
|
|
internal_addr = StringField("Internal IP address", validators=[DataRequired(), IPAddress(ipv4=True, ipv6=True)])
|
|
|
|
remote_addr = StringField("Remote IP address", validators=[DataRequired(), IPAddress(ipv4=True, ipv6=True)])
|
|
|
|
remote_port = IntegerField(validators=[NumberRange(min=0, max=65534)])
|
|
|
|
|
|
|
|
app = Sanic("lease")
|
|
|
|
app.config["WTF_CSRF_ENABLED"] = False
|
|
|
|
|
|
|
|
MONGO_URI = os.getenv("MONGO_URI", "mongodb://127.0.0.1:27017/default")
|
|
|
|
FQDN = socket.getfqdn()
|
|
|
|
|
|
|
|
|
|
|
|
@app.listener('before_server_start')
|
|
|
|
async def setup_db(app, loop):
|
|
|
|
app.ctx.db = AsyncIOMotorClient(MONGO_URI).get_default_database()
|
|
|
|
|
|
|
|
|
|
|
|
async def submit(request, q):
|
|
|
|
q["status"] = "signed"
|
|
|
|
# TODO: add expiration check
|
|
|
|
|
|
|
|
form = LeaseUpdateForm(request)
|
|
|
|
if not form.validate():
|
|
|
|
raise InvalidUsage("Invalid form input")
|
|
|
|
|
2021-06-04 12:51:42 +00:00
|
|
|
instance = "%s-%s" % (FQDN, form.service.data)
|
|
|
|
doc = await app.ctx.db.certidude_certificates.find_one_and_update(q, {
|
2021-06-02 12:36:59 +00:00
|
|
|
"$set": {
|
|
|
|
"last_seen": datetime.utcnow(),
|
2021-06-04 12:51:42 +00:00
|
|
|
"instance": instance,
|
2021-06-02 12:36:59 +00:00
|
|
|
"remote.port": form.remote_port.data,
|
|
|
|
"remote.addr": form.remote_addr.data,
|
|
|
|
},
|
|
|
|
"$addToSet": {
|
|
|
|
"ip": form.internal_addr.data
|
|
|
|
}
|
2021-06-04 12:51:42 +00:00
|
|
|
}, return_document=ReturnDocument.BEFORE)
|
|
|
|
if doc:
|
2021-06-02 12:36:59 +00:00
|
|
|
submit_count.labels(form.service.data).inc()
|
2021-06-06 12:14:01 +00:00
|
|
|
if doc.get("instance") != instance:
|
|
|
|
migration_count.labels(FQDN).inc()
|
2021-06-04 12:51:42 +00:00
|
|
|
return response.text('Client lease info updated')
|
2021-06-02 12:36:59 +00:00
|
|
|
else:
|
|
|
|
not_found_count.labels(form.service.data).inc()
|
2021-06-04 12:51:42 +00:00
|
|
|
raise NotFound("Client not found")
|
2021-06-02 12:36:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/by-serial/<serial_number:int>", methods=["GET"])
|
|
|
|
async def get_by_serial(request, serial_number):
|
|
|
|
obj = await app.ctx.db.certidude_certificates.find_one({
|
|
|
|
"serial_number": "%x" % serial_number,
|
|
|
|
"status": "signed"})
|
|
|
|
# TODO: Add expiration check
|
|
|
|
if obj:
|
|
|
|
return response.text("Certificate valid")
|
|
|
|
else:
|
|
|
|
raise NotFound("Certificate not found or not valid")
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/by-dn/<distinguished_name:string>", methods=["POST"])
|
|
|
|
async def submit_by_dn(request, distinguished_name):
|
|
|
|
return await submit(request, {"distinguished_name": distinguished_name.replace("%20", " ")})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/by-serial/<serial_number:int>", methods=["POST"])
|
|
|
|
async def submit_by_serial(request, serial_number):
|
|
|
|
return await submit(request, {"serial_number": "%x" % serial_number})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/by-service/<service:string>", methods=["DELETE"])
|
|
|
|
async def flush(request, service):
|
|
|
|
"""
|
|
|
|
Flush IP addresses assigned by this instance as it was restarted
|
|
|
|
"""
|
|
|
|
await app.ctx.db.certidude_certificates.update_many({
|
|
|
|
"instance": "%s-%s" % (FQDN, service),
|
|
|
|
}, {
|
|
|
|
"$unset": {
|
|
|
|
"ip": "",
|
|
|
|
"instance": "",
|
|
|
|
}
|
|
|
|
})
|
|
|
|
flush_count.labels(service).inc()
|
|
|
|
return response.text('Leases flushed')
|
|
|
|
|
|
|
|
|
|
|
|
monitor(app).expose_endpoint()
|
|
|
|
app.run(port=2001)
|