This commit is contained in:
rald 2023-08-03 17:19:04 +08:00
commit 002c027f30
10 changed files with 353 additions and 0 deletions

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# CGI Example Project
The goal of this project is to give a working example of a basic Gemini CGI application. User identities are driven by TLS Client Certificates. This project will serve as a reference for CGI authors and be explained in detail in an instructional video.
## Features
* (DONE) New TLS certificates automatically create a new user account
* (DONE) Users can set data on their own account (a name field for example)
* (DONE) Users can add additional certificates to the same account by using a special code
## Setup
Create database with:
``` sqlite command to create database
sqlite3 db/cgi-example.sqlite3 < create_schema.sql
```
Then give write permission to the both the database and the containing directory (`db`) to the user that your Gemini server runs as. It is important that the containing folder write permissions are in place or you will see `50` errors returned when trying to write to the database.

26
add-cert.cgi Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# modules are all in the parent directory
import sys
sys.path.append('..')
# import helpers from modules
from helpers import get_client_cert, get_query_string
from db import add_cert_to_user
tls_client_hash = get_client_cert(False)
key_code = get_query_string("What is your auth code?")
success = add_cert_to_user(key_code, tls_client_hash)
print("# Add Cert Page")
print()
if success:
print("Your new key has been added to this account. Congratulations!")
else:
print("Either your auth code is incorrect or something else has gone wrong.")
print()
print("=> login.cgi Back to main page")
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

BIN
cgi-example.sqlite3 Normal file

Binary file not shown.

24
change-name.cgi Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# modules are all in the parent directory
import sys
# sys.path.append('..')
# import helpers from modules
from helpers import get_query_string, get_client_cert
from db import check_hash, set_name
TLS_CLIENT_HASH = get_client_cert(False) # don't default to response 20
user_name = get_query_string("What is your name?")
user_id = check_hash(TLS_CLIENT_HASH)
set_name(user_id, user_name)
print("# Name Change Page")
print()
print("Your name has been changed.")
print()
print("=> login.cgi Back to main page")
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

14
create_schema.sql Normal file
View File

@ -0,0 +1,14 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "certs" (
"hash" TEXT NOT NULL UNIQUE,
"user_id" INTEGER,
PRIMARY KEY("hash"),
FOREIGN KEY("user_id") REFERENCES "users"("id")
);
CREATE TABLE IF NOT EXISTS "users" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT,
"add_key_code" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
COMMIT;

159
db.py Normal file
View File

@ -0,0 +1,159 @@
import sqlite3
from sqlite3 import Error
import os
import urllib.parse
dir_name = os.path.dirname(__file__)
db_name = r"cgi-example.sqlite3"
db_file = os.path.join(dir_name, db_name)
def create_connection():
""" create a database connection to the SQLite database
specified by db_file
:return: Connection object or None
"""
conn = None
try:
conn = sqlite3.connect(db_file)
except Error as e:
print(e)
return conn
def add_user(conn):
"""
Create a new user record
:param conn: database connection object
:return: user id
"""
sql = ''' INSERT INTO users DEFAULT VALUES '''
cur = conn.cursor()
try:
cur.execute(sql)
conn.commit()
except:
return 'error in insert'
return cur.lastrowid
def add_hash(conn, tls_client_hash, user_id):
"""
Create a new record for this cert
:param conn: database connection object
:param tls_client_hash:
:param user_id:
:return: certificate id
"""
sql = ''' INSERT OR REPLACE INTO certs(hash,user_id) VALUES(?,?) '''
cur = conn.cursor()
cur.execute(sql, (tls_client_hash, user_id))
conn.commit()
return cur.lastrowid
def get_user(conn, tls_client_hash):
"""
Get user id matching tls_client_hash if it exists
:param conn: database connection object
:param tls_client_hash:
:return: user id or None
"""
cur = conn.cursor()
cur.execute("SELECT user_id FROM certs WHERE hash=?", (tls_client_hash,))
row = cur.fetchone()
if row is None:
return None
else:
return row[0] # user_id
def get_user_by_keycode(conn, key_code):
"""
Get user id of tls_client_hash if it exists
:param conn: database connection object
:param key_code: authorization key code
:return: user id or None
"""
cur = conn.cursor()
cur.execute("SELECT id FROM users WHERE add_key_code=?", (key_code,))
row = cur.fetchone()
if row is None:
return None
else:
return row[0] # user_id
def check_hash(tls_client_hash):
"""
Check for existing user with hash or add a new one
:param conn:
:param tls_client_hash:
:return: user id
"""
conn = create_connection()
with conn:
user_id = get_user(conn, tls_client_hash)
if (user_id is None):
user_id = add_user(conn)
add_hash(conn, tls_client_hash, user_id)
return user_id
def get_name(user_id):
"""
Get user name if it exists
:param user_id:
:return: user name
"""
conn = create_connection()
with conn:
cur = conn.cursor()
cur.execute("SELECT name FROM users WHERE id=?", (user_id,))
row = cur.fetchone()
if row is None:
return None
else:
return row[0] # user_name
def set_name(user_id, user_name):
"""
Update or set name on user
:param user_id:
:param user_name:
"""
conn = create_connection()
with conn:
sql = ''' UPDATE users SET name=(?) WHERE id=? '''
cur = conn.cursor()
cur.execute(sql, (urllib.parse.unquote(user_name), user_id))
conn.commit()
return cur.lastrowid
def set_add_key_code(user_id, key_code):
"""
Update or set key code on user
:param user_id:
:param key_code:
"""
conn = create_connection()
with conn:
""" There should probably be a check here to ensure no user
has this key_code already
"""
sql = ''' UPDATE users SET add_key_code=(?) WHERE id=? '''
cur = conn.cursor()
cur.execute(sql, (key_code, user_id))
conn.commit()
return cur.lastrowid
def add_cert_to_user(key_code, tls_client_hash):
"""
Check for existing user with hash or add a new one
:param conn:
:param key_code:
:param tls_client_hash:
:return: true if added, false if not found
"""
conn = create_connection()
with conn:
user_id = get_user_by_keycode(conn, key_code)
if (user_id is None):
return False
add_hash(conn, tls_client_hash, user_id)
return True
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

35
helpers.py Normal file
View File

@ -0,0 +1,35 @@
import os
import string
import random
def get_client_cert(ok_if_found = True):
TLS_CLIENT_HASH = os.getenv('TLS_CLIENT_HASH')
if (TLS_CLIENT_HASH is None):
show_header_cert_required()
elif ok_if_found:
show_header_ok()
return TLS_CLIENT_HASH
def get_query_string(msg, ok_if_found = True):
QUERY_STRING = os.getenv('QUERY_STRING')
if(QUERY_STRING is None):
show_query_string_required(msg)
elif ok_if_found:
show_header_ok()
return QUERY_STRING
def show_header_ok():
print("20 text/gemini; charset=utf-8", end = "\r\n")
def show_header_cert_required():
print("60 text/gemini; charset=utf-8", end = "\r\n")
quit()
def show_query_string_required(msg):
print("10 " + msg, end = "\r\n")
quit()
def id_generator(size=12, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

16
index.cgi Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
from helpers import show_header_ok
show_header_ok()
with open('cgi-bin/README.md') as f:
contents = f.read()
print(contents)
print("## Project Source:")
print("=> https://tildegit.org/tomasino/gemspace/src/branch/master/cgi-example CGI Example Source on tildegit.org")
print("")
print("## Get Started")
print("=> login.cgi Login with your client certificate to begin")
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

32
login.cgi Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# modules are all in the parent directory
import sys
# sys.path.append('..')
# import helpers from modules
from helpers import get_client_cert
from db import *
TLS_CLIENT_HASH = get_client_cert()
user_id = check_hash(TLS_CLIENT_HASH)
user_name = get_name(user_id) or "[Not Set]"
print("# Welcome: User Logged In")
print()
print("## Your USER ID:")
print(user_id)
print()
print("## Your USER NAME:")
print(user_name)
print("=> change-name.cgi Change User Name")
print()
print("## TLS Certs associated with account")
print("=> update-auth-code.cgi Generate an auth code to add another cert to your account")
print("=> add-cert.cgi Add another cert to your account (with auth code)")
print()
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai

28
update-auth-code.cgi Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# modules are all in the parent directory
import sys
# sys.path.append('..')
# import helpers from modules
from helpers import get_client_cert, id_generator
from db import check_hash, set_add_key_code
TLS_CLIENT_HASH = get_client_cert()
user_id = check_hash(TLS_CLIENT_HASH)
key_code = id_generator()
set_add_key_code(user_id, key_code)
print("# Add Cert Page")
print()
print("Your authorization code to add new client certificates has been updated. Visit the 'Add Cert' page from another device or with another key and enter your authorization code to connect the accounts.")
print()
print("## AUTH CODE")
print(key_code)
print("=> add-cert.cgi?" + key_code + " Link to Add Cert page with Auth Code")
print()
print("=> login.cgi Back to main page")
#vim:fenc=utf-8:ts=4:sw=4:sta:noet:sts=4:fdm=marker:ai