Compare commits

..

No commits in common. "main" and "swee-patch-1" have entirely different histories.

9 changed files with 473 additions and 784 deletions

View file

@ -7,4 +7,4 @@ jobs:
with: with:
additional: python3 additional: python3
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: python -m compileall . - run: python -m py_compile server.py

View file

@ -9,6 +9,9 @@ host: 127.0.0.1
# The identifier for this server, such as the location (Used in whois) # The identifier for this server, such as the location (Used in whois)
identifier: somewhere in the universe identifier: somewhere in the universe
# The nickserv account to automatically be IRC opped
admin-nick: admin
# The path of the data file to be used by NickServ, ChanServ, etc # The path of the data file to be used by NickServ, ChanServ, etc
# This MUST be a file path. # This MUST be a file path.
# Recommended to use a .db extension because it is an SQLite database # Recommended to use a .db extension because it is an SQLite database
@ -42,21 +45,9 @@ ban-provider: /path/to/bans.txt
# ban-provider: sql # ban-provider: sql
# Mail server settings for PawServ
smtp_host: smtp.example.com
smtp_port: 25
smtp_starttls: off
smtp_username: pawserv@example.com
smtp_password: examplePassword
# If you setup a webchat, enter the passphrase for it.
webirc_pass: helloworld
# Use of modules in the /modules folder, or in an absolute path specified. # Use of modules in the /modules folder, or in an absolute path specified.
# You want your protection modules BEFORE the ban engine. # You want your protection modules BEFORE the ban engine.
modules: modules:
- sqlite_local - sqlite_local
- botnet_protect - botnet_protect
- ban_engine - ban_engine
- pawserv

View file

@ -2,13 +2,10 @@ import threading
__ircat_type__ = "allsocket" __ircat_type__ = "allsocket"
__ircat_requires__ = ["ban-provider"] __ircat_requires__ = ["ban-provider"]
__ircat_giveme__ = ["sql"] # Only command and allsocket have these. __ircat_giveme__ = ["sql"] # Only command and allsocket have these.
__ircat_fakechannels__ = {"#IRCATSUCKS": "B0tn3t pr0t3ct10n, do not join."} # Fake channels that plugins control. __ircat_fakechannels__ = {"#IRCATSUCKS": "WHATEVER YOU DO, DO NOT JOIN IF YOU ARE HUMAN"}
class IRCatModule: class IRCatModule:
sus_strings = [ sus_strings = [
# Known SupernetS botnet texts " .''." # Latest Supernets spambot!
# Contribute here: https://discuss.swee.codes/t/61
" .''.", # 2025 new year
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀" # "The United States of America" LATEST
] ]
memory = {} # {nick: {channel: trustlevel}} one can also be {nick: True} if it is whitelisted for the session. memory = {} # {nick: {channel: trustlevel}} one can also be {nick: True} if it is whitelisted for the session.
useSQLengine = False useSQLengine = False
@ -18,28 +15,28 @@ class IRCatModule:
self.useSQLengine = True self.useSQLengine = True
self.SQLengine = sql self.SQLengine = sql
def ban(self, ip): def ban(self, ip):
del self.memory[ip] # Forget this all happened del self.memory[ip]
# Add the ban
if self.useSQLengine: if self.useSQLengine:
self.SQLengine.addban(ip, "Botnet detected!") # Use the SQL provider if {'ban-provider': 'sql'} cur = self.SQLengine.conn.cursor()
else: else:
open(self.ban_provider, "a").write(f"\n{ip} Botnet detected!") # Else, write on the banfile. open(self.ban_provider, "a").write(f"\n{ip} Botnet detected!")
raise Exception("Botnet detected!") # Kill the connection raise Exception("Botnet detected!")
def onSocket(self, ip, socket, value, cachedNick=None, validated=False): def onSocket(self, ip, socket, value, cachedNick=None, validated=False):
if cachedNick != None: if cachedNick != None:
print(value) print(value)
if "JOIN" in value: if "JOIN" in value:
target = value.split(" ")[1] target = value.split(" ")[1]
self.memory[ip] = 1 # 1: Just joined the channel, continue observing. self.memory[ip] = 1 # 1: Just joined the channel, continue observing.
print("Autoban> Somebody joined " + target)
if target.lower() == "#ircatsucks": if target.lower() == "#ircatsucks":
self.ban(ip) # Ruh roh self.ban(ip)
elif "PRIVMSG" in value: elif "PRIVMSG" in value:
if not (ip in self.memory and self.memory[ip] == 0): # Continue observing if not (ip in self.memory and self.memory[ip] == 0):
target = value.split(" ")[1] target = value.split(" ")[1]
content = " ".join(value.split(" ")[2:])[1:] content = " ".join(value.split(" ")[2:])[1:]
if content in self.sus_strings: if content in self.sus_strings:
if ip in self.memory: # Hey stinky! YOU'RE BANNED if ip in self.memory:
if self.memory[ip] == 1: if self.memory[ip] == 1:
self.ban(ip) self.ban(ip)
else: else:
self.memory[ip] = 0 # 0: Trust the connection :3 self.memory[ip] = 0 # 0: Trust the connection :3

View file

@ -1,55 +1,12 @@
import os, traceback import requests, os
from cryptography.fernet import Fernet
from cloudflare import Cloudflare # Please make sure you install this module from pip, not package manager.
__ircat_type__ = "sql.provider" # The type of module __ircat_type__ = "sql.provider" # The type of module
__ircat_requires__ = ["cf_accountid", "cf_apitoken", "cf_d1database", "fernet-key"] # The required config.yml entries. __ircat_requires__ = ["cf_accountid", "cf_apitoken", "cf_d1database"] # The required config.yml entries.
class broker: class broker:
def __init__(self, cf_accountid:str, cf_apitoken:str, cf_d1database:str, fernet_key:str): def __init__(self, cf_accountid, cf_apitoken, cf_d1database):
self.account_id = cf_accountid self.account_id = cf_accountid
self.api_token = cf_apitoken self.api_token = cf_apitoken
self.database = cf_d1database self.base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/d1/database"
self.client = Cloudflare(api_token=cf_apitoken) self.headers = {
self.fnet = Fernet(fernet_key) "Content-Type": "application/json",
self.client.d1.database.query( "Authorization": f"Bearer {self.api_token}"
database_id=self.database, }
account_id=self.account_id,
sql="CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255)); CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), email varchar(255)); CREATE table IF NOT EXISTS groups (name varchar(255), owner varchar(255)); CREATE table IF NOT EXISTS chanserv (name varchar(255), modes varchar(255), params varchar(255), owner varchar(255), usermodes varchar(255), optimodes varchar(255))",
)
def cfexec(self, command:str, params:list):
query = self.client.d1.database.query(
database_id=self.database,
account_id=self.account_id,
sql=command,
params=params
)
return query[0].results
def parse2sqlite(self, results):
temp = []
for k, v in results.items():
temp.append(v)
return temp
def nickserv_identify(self, nick, password:str):
f = self.cfexec("SELECT * FROM groups WHERE name=?;", [nick])
if len(f) != 0:
nick = f[0]["owner"]
e = self.cfexec("SELECT * FROM nickserv WHERE user=?;", [nick])
if len(e) == 0:
return False
else:
try:
return self.parse2sqlite(e[0]) if self.fnet.decrypt(bytes(e[0]["hash"], "UTF-8")).decode() == password else False
except:
print(traceback.format_exc())
return False
def nickserv_register(self, nick, password, email):
hashed = self.fnet.encrypt(bytes(password, "UTF-8")).decode()
e = self.cfexec("INSERT INTO nickserv values(?, 'iw', ?, ?);", [nick, hashed, email])
def nickserv_isexist(self, nick):
e = self.cfexec("SELECT * FROM nickserv WHERE user=?;", [nick])
f = self.cfexec("SELECT * FROM groups WHERE name=?;", [nick])
return len(e) != 0 or len(f) != 0
def nickserv_group(self, nick, account):
self.cfexec("INSERT INTO groups VALUES (?, ?);", [nick.lower(), account.lower()])
def nickserv_drop(self, account):
self.cfexec("DELETE FROM nickserv WHERE user=?;", [account.lower()])
self.cfexec("DELETE FROM groups WHERE owner=?;", [account.lower()])

View file

@ -1,123 +0,0 @@
# Replacement for services bots.
import traceback, smtplib, uuid, ssl
__ircat_type__ = "command"
__ircat_requires__ = ["name", "smtp_host", "smtp_port", "smtp_starttls", "smtp_username", "smtp_password", "host"]
__ircat_giveme__ = ["sql"] # Only command and allsocket have these.
__ircat_fakeusers__ = {
"NickServ": {
"host": "PawServ",
"username": "Meow",
"realname": "PawServ plugin - Identification bot",
"modes": "iw",
"away": False,
"identified": False,
"ssl": False
},
"ChanServ": {
"host": "PawServ",
"username": "Meow",
"realname": "PawServ plugin - Channel management bot",
"modes": "iw",
"away": False,
"identified": False,
"ssl": False
}
}
class IRCatModule:
def __init__(self, sql, smtp_host, smtp_port, smtp_starttls, smtp_username, smtp_password, name, host):
self.sql = sql
self.smtp_server = smtp_host
self.smtp_port = smtp_port
self.smtp_starttls = smtp_starttls
self.smtp_username = smtp_username
self.smtp_password = smtp_password
self.net_name = name
self.hostname = host
self.memory = {} # {nick: [authtoken, password, email]}
print("PawServ loaded!")
def command(self, command, args, ip, nick, connection, user):
try:
if command == "NICKSERV" or (command == "PRIVMSG" and args[0].lower() == "nickserv") or command == "PASS":
if command == "PASS":
command = "NICKSERV"
args = ["IDENTIFY", args[0]]
if command == "PRIVMSG":
args = args[1:]
args[0] = args[0][1:] if args[0][0] == ":" else args[0]
if len(args) > 0 and args[0].lower() == "verify":
if len(args) == 3:
if args[1].lower() in self.memory:
if args[2] == self.memory[args[1].lower()][0]:
self.sql.nickserv_register(nick=args[1].lower(), password=self.memory[args[1].lower()][1], email=self.memory[args[1].lower()][2])
nck = args[1].lower()
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Done, you may now identify as {nck}.\r\n", "UTF-8"))
del self.memory[args[1].lower()]
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid verification.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Nickname doesn't exist, try registering again?\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid verification.\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "group":
if len(args) == 1:
if user["identified"]:
if not self.sql.nickserv_isexist(nick.lower()):
self.sql.nickserv_group(nick, user["identusername"])
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Nickname {nick} already exists.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You are not logged in.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Does not requre arguments\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "register":
if not user["identified"]:
if len(args) == 3:
if not self.sql.nickserv_isexist(nick.lower()):
if not nick in self.memory:
context = ssl.create_default_context()
token = str(uuid.uuid4())
message = f"Subject: {self.net_name} Account Verification\n\nHi,\nIt appears you have tried to register an account ({nick}) with this email on {self.net_name},\nIf you did not register an account, feel free to delete this email.\nIf you did, use this command:\n/nickserv verify {nick} {token}"
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
server.ehlo()
if self.smtp_starttls:
server.starttls(context=context)
server.ehlo()
server.login(self.smtp_username, self.smtp_password)
server.sendmail(self.smtp_username, args[2], message)
self.memory[nick.lower()] = [token, args[1], args[2]]
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Email sent, please verify.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :A verification is already pending.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :The user {nick} already exists.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Needs 3 arguments, nickname, password, and email.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You're already logged in.\r\n", "UTF-8"))
elif len(args) > 0 and args[0].lower() == "identify":
if not user["identified"]:
nck = nick if len(args) == 2 else args[2]
temp = self.sql.nickserv_identify(nick=nck.lower(), password=args[1])
if temp != False:
hostmask = user["host"]
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You are now identified as {nck}.\r\n", "UTF-8"))
connection.sendall(bytes(f":{self.hostname} 900 {nick} {hostmask} {nck} :You are now logged in as {nck}.\r\n", "UTF-8"))
return {"success": True, "identify": temp}
else:
if nick.lower() in self.memory:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Your account isn't verified, please verify now.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :Invalid username/password.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :You're already logged in.\r\n", "UTF-8"))
else:
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :NickServ Usage:\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :IDENTIFY pass <nick> - Identifies your nickname\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :REGISTER pass email - Register your nickname\r\n","UTF-8"))
connection.sendall(bytes(f":NickServ!Meow@PawServ NOTICE {nick} :GROUP - Allows you to sign in to your account with different nicknames\r\n","UTF-8"))
return {"success": True}
else:
return {"success": False}
except:
print(traceback.format_exc())
return {"success": False}

View file

@ -1,6 +1,6 @@
# IRCat module for local SQLite database (default) # IRCat module for local SQLite database (default)
import sqlite3, os, traceback import sqlite3, os, traceback
from cryptography.fernet import Fernet from cryptography import Fernet
__ircat_type__ = "sql.provider" # The type of module __ircat_type__ = "sql.provider" # The type of module
__ircat_requires__ = ["data-path", "fernet-key"] # The required config.yml entries. __ircat_requires__ = ["data-path", "fernet-key"] # The required config.yml entries.
class broker: class broker:
@ -8,46 +8,22 @@ class broker:
if not os.path.isfile(data_path): if not os.path.isfile(data_path):
print("Creating database file...") print("Creating database file...")
open(data_path, "w").write("") open(data_path, "w").write("")
self.conn = sqlite3.connect(data_path, check_same_thread=False) self.conn = sqlite3.connect(data_path)
self.fnet = Fernet(fernet_key) self.fnet = Fernet(fernet_key)
db = self.conn.cursor() db = self.conn.cursor()
db.execute("""CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255))""") db.execute("""CREATE table IF NOT EXISTS bans (ip varchar(255), reason varchar(255)""")
db.execute("""CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), email varchar(255))""") db.execute("""CREATE table IF NOT EXISTS nickserv (user varchar(255), modes varchar(255), hash varchar(255), cloak varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS groups (name varchar(255), owner varchar(255))""") db.execute("""CREATE table IF NOT EXISTS groups (name varchar(255), owner varchar(255))""")
db.execute("""CREATE table IF NOT EXISTS chanserv (name varchar(255), modes varchar(255), params varchar(255), owner varchar(255), usermodes varchar(255), optimodes varchar(255))""") db.execute("""CREATE table IF NOT EXISTS chanserv (name varchar(255), modes varchar(255), params varchar(255), owner varchar(255), usermodes varchar(255), optimodes varchar(255))""")
def nickserv_identify(self, nick, password:str): def nickserv_identify(self, nick, password:str):
db = self.conn.cursor() db = self.conn.cursor()
db.execute("SELECT * FROM groups WHERE name=?", [nick])
f = db.fetchall()
if f != []:
nick = f[0][1]
db.execute("SELECT * FROM nickserv WHERE user=?;", [nick]) db.execute("SELECT * FROM nickserv WHERE user=?;", [nick])
e = db.fetchall() e = db.fetchall()
if e == []: if e == []:
return False return False
else: else:
try: try:
return e[0] if self.fnet.decrypt(bytes(e[0][2], "UTF-8")).decode() == password else False return e[0] if self.fnet.decrypt(e[0][2]) == password else False
except: except:
print(traceback.format_exc()) print(traceback.format_exc())
return False return False
def nickserv_register(self, nick, password, email):
hashed = self.fnet.encrypt(bytes(password, "UTF-8")).decode()
db = self.conn.cursor()
db.execute("INSERT INTO nickserv values(?, 'iw', ?, ?);", [nick, hashed, email])
self.conn.commit()
def nickserv_isexist(self, nick):
db = self.conn.cursor()
db.execute("SELECT * FROM nickserv WHERE user=?;", [nick.lower()])
e = db.fetchall()
db.execute("SELECT * FROM groups WHERE name=?;", [nick.lower()])
f = db.fetchall()
return e != [] or f != []
def nickserv_group(self, nick, account):
db = self.conn.cursor()
db.execute("INSERT INTO groups VALUES (?, ?);", [nick.lower(), account.lower()])
self.conn.commit()
def nickserv_drop(self, account):
db = self.conn.cursor()
db.execute("DELETE FROM nickserv WHERE user=?;", [account.lower()])
db.execute("DELETE FROM groups WHERE owner=?;", [account.lower()])

View file

@ -1,4 +1,2 @@
cloudflare>=4.0.0
requests requests
PyOpenSSL
pyyaml pyyaml

963
server.py

File diff suppressed because it is too large Load diff

30
todo.md
View file

@ -12,11 +12,12 @@
- [x] Send PING and wait for PONG - [x] Send PING and wait for PONG
- [x] Reply PONG if received PING - [x] Reply PONG if received PING
- [x] [Change of nicknames](https://mastodon.swee.codes/@swee/113642104470536887) - [x] [Change of nicknames](https://mastodon.swee.codes/@swee/113642104470536887)
- [ ] Change of hostnames
- [x] Away - [x] Away
- [ ] Multi-server support - [ ] Multi-server support
- [x] `LIST` - [ ] `LIST`
- [ ] `TOPIC` - [ ] `TOPIC`
- [ ] [Database support](https://discuss.swee.codes/t/41) - [ ] [Data file with SQLite](https://discuss.swee.codes/t/41/2)
- [ ] User Flags - [ ] User Flags
- [ ] i (invisible) - [ ] i (invisible)
- [ ] o (IRCOP) - [ ] o (IRCOP)
@ -39,29 +40,30 @@
- [ ] Destructive features for IRCOPS - [ ] Destructive features for IRCOPS
- [ ] `KILL <user> <comment>` - [ ] `KILL <user> <comment>`
- [ ] `MODE <external user>` - [ ] `MODE <external user>`
- [x] `RESTART` - [ ] `RESTART`
- [ ] Extra commands - [ ] Extra commands
- [x] `NAMES` - [ ] `USERS`
- [x] `WHOIS` - [x] `WHOIS`
- [ ] `WHOWAS` - [ ] `WHOWAS`
- [ ] [Implement services.](modules/pawserv.py) - [ ] Implement services.
- [ ] Nickserv - [ ] Nickserv
- [ ] ChanServ - [ ] ChanServ
- [x] CatServ (Outside of PawServ) - [x] GitServ (Custom user for pull)
- [x] Link `PRIVMSG *serv` to `*serv` - [ ] Link `PRIVMSG *serv` to `*serv`
- [x] Extra ~~(not planned)~~ features - [ ] Extra (not planned) features
- [x] ident support - [ ] ident support
- [ ] Authentication - [ ] Authentication
- [x] Store credentials in an SQLite3 file. - [ ] Make the server able to change the client's host
- [x] Map NickServ IDENTIFY - [ ] Store credentials in an SQLite3 file.
- [ ] Map NickServ IDENTIFY
- [ ] Map PASS - [ ] Map PASS
- [x] SSL/TLS - [ ] Mock SASL PLAIN
- [ ] SSL/TLS
- [x] [Use a thread to accept connections on SSL port 6697](https://mastodon.swee.codes/@swee/113762525145710774) - [x] [Use a thread to accept connections on SSL port 6697](https://mastodon.swee.codes/@swee/113762525145710774)
- [x] Automatically reload the certificate ~~if defined in config.~~ - [ ] Automatically reload the certificate if defined in config.
- [ ] Add IRCv3 features. - [ ] Add IRCv3 features.
- [x] List capabilities (`CAP LS 302`) - [x] List capabilities (`CAP LS 302`)
- [ ] `away-notify` - [ ] `away-notify`
- [ ] `tls` (STARTTLS) - [ ] `tls` (STARTTLS)
- [ ] `sasl`
- Will research later. - Will research later.
I am going to fully read [RFC 1459](https://datatracker.ietf.org/doc/html/rfc1459) soon and add each part to the TODO. I am going to fully read [RFC 1459](https://datatracker.ietf.org/doc/html/rfc1459) soon and add each part to the TODO.