mirror of
https://github.com/PurpleI2P/pyseeder
synced 2025-01-29 16:14:16 +00:00
initialize repository
This commit is contained in:
commit
03af5634c1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
venv/
|
||||
transports.ini
|
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
||||
pyseeder
|
||||
========
|
||||
|
||||
Reseed data managment tools for I2P
|
||||
|
||||
* Generate reseed signing keypair
|
||||
* Make reseed data files (su3)
|
||||
* Download su3 files from official servers for mirroring
|
||||
* Upload reseed data to different places (with plugins)
|
||||
|
||||
Reseed transports are implemented so that users can bootstrap their I2P nodes
|
||||
without needing to connect to "official" I2P reseeds. This makes I2P more
|
||||
invisible for firewalls.
|
||||
|
||||
|
||||
Install requirements
|
||||
--------------------
|
||||
|
||||
# apt-get install python3 python3-pip
|
||||
# pip3 install -r requirements.txt
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
$ python3 pyseeder.py --help
|
||||
$ python3 pyseeder.py keygen --help
|
||||
|
||||
|
||||
Generating keypair
|
||||
------------------
|
||||
|
||||
|
||||
$ pyhon3 pyseeder.py keygen --cert data/user_at_mail.i2p.crt --private-key data/priv_key.pem --signer-id user@mail.i2p
|
||||
|
||||
This will generate certificate (user\_at\_mail.i2p.crt) and private RSA key
|
||||
(priv\_key.pem) in data folder. E-mail is used as certificate identifier.
|
||||
|
||||
Script will prompt for private key password.
|
||||
|
||||
|
||||
Generating reseed data
|
||||
----------------------
|
||||
|
||||
|
||||
$ YOUR_PASSWORD="Pa55w0rd"
|
||||
$ echo $YOUR_PASSWORD | python3 pyseeder.py reseed --netdb /path/to/netDb --private-key data/priv_key.pem --outfile output/i2pseeds.su3 --signer-id user@mail.i2p
|
||||
|
||||
This will generate file i2pseeds.su3 in output folder, using user@mail.i2p as
|
||||
certificate identifier.
|
||||
|
||||
Note: you'll have to enter your private key password to stdin, the above
|
||||
is one of the ways to do it (for cron and scripts).
|
||||
|
||||
|
||||
Download su3 file from official servers
|
||||
---------------------------------------
|
||||
|
||||
$ python3 pyseeder.py transport.pull --urls https://reseed.i2p-projekt.de/ https://reseed.i2p.vzaws.com:8443/ --outfile output/i2pseeds.su3
|
||||
|
||||
Note: --urls parameter is optional, defaults are "official" I2P reseeds.
|
||||
|
||||
|
||||
Upload su3 file with pluggable transports
|
||||
-----------------------------------------
|
||||
|
||||
$ python3 pyseeder.py transport.push --config transports.ini --file output/i2pseeds.su3
|
||||
|
||||
All parameters are optional. Copy file transports.ini.example to
|
||||
transports.ini. Edit your settings in this new file.
|
2
data/.gitignore
vendored
Normal file
2
data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.crt
|
||||
*.pem
|
1
output/.gitignore
vendored
Normal file
1
output/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.su3
|
125
pyseeder.py
Executable file
125
pyseeder.py
Executable file
@ -0,0 +1,125 @@
|
||||
#! /usr/bin/env python3
|
||||
import os, os.path
|
||||
import sys
|
||||
from getpass import getpass
|
||||
import argparse
|
||||
|
||||
from pyseeder.crypto import keygen
|
||||
from pyseeder.su3file import SU3File
|
||||
import pyseeder.transport
|
||||
|
||||
from pyseeder.utils import PyseederException
|
||||
|
||||
def keygen_action(args):
|
||||
"""Sub-command to generate keys"""
|
||||
priv_key_password = getpass("Set private key password: ").encode("utf-8")
|
||||
keygen(args.cert, args.private_key, priv_key_password, args.signer_id)
|
||||
|
||||
def reseed_action(args):
|
||||
"""Sub-command to generate reseed file"""
|
||||
priv_key_password = input().encode("utf-8")
|
||||
su3file = SU3File(args.signer_id)
|
||||
su3file.reseed(args.netdb)
|
||||
su3file.write(args.outfile, args.private_key, priv_key_password)
|
||||
|
||||
def transport_pull_action(args):
|
||||
"""Sub-command for downloading su3 file"""
|
||||
import random
|
||||
random.shuffle(args.urls)
|
||||
|
||||
for u in args.urls:
|
||||
if pyseeder.transport.download(u, args.outfile):
|
||||
return True
|
||||
|
||||
raise PyseederException("Failed to download su3 file")
|
||||
|
||||
def transport_push_action(args):
|
||||
"""Sub-command for uploading su3 file with transports"""
|
||||
if not os.path.isfile(args.config):
|
||||
raise PyseederException("Can't read transports config file")
|
||||
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
config.read(args.config)
|
||||
pyseeder.transport.upload(args.file, config)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(title="actions",
|
||||
help="Command to execute")
|
||||
|
||||
kg_parser = subparsers.add_parser(
|
||||
"keygen",
|
||||
description="Generates keypair for your reseed",
|
||||
usage="""
|
||||
%(prog)s --cert data/user_at_mail.i2p.crt \\
|
||||
--private-key data/priv_key.pem --signer-id user@mail.i2p"""
|
||||
)
|
||||
kg_parser.add_argument("--signer-id", required=True,
|
||||
help="Identifier of certificate (example: user@mail.i2p)")
|
||||
kg_parser.add_argument("--private-key", default="data/priv_key.pem",
|
||||
help="RSA private key (default: data/priv_key.pem)")
|
||||
kg_parser.add_argument("--cert", required=True,
|
||||
help="Certificate (example: output/user_at_mail.i2p.crt)")
|
||||
kg_parser.set_defaults(func=keygen_action)
|
||||
|
||||
|
||||
rs_parser = subparsers.add_parser(
|
||||
"reseed",
|
||||
description="Creates su3 reseed file",
|
||||
usage="""
|
||||
echo $YOUR_PASSWORD | %(prog)s --netdb /path/to/netDb \\
|
||||
--private-key data/priv_key.pem --outfile output/i2pseeds.su3 \\
|
||||
--signer-id user@mail.i2p"""
|
||||
)
|
||||
rs_parser.add_argument("--signer-id", required=True,
|
||||
help="Identifier of certificate (example: user@mail.i2p)")
|
||||
rs_parser.add_argument("--private-key", default="data/priv_key.pem",
|
||||
help="RSA private key (default: data/priv_key.pem)")
|
||||
rs_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3",
|
||||
help="Output file (default: output/i2pseeds.su3)")
|
||||
rs_parser.add_argument("--netdb", required=True,
|
||||
help="Path to netDb folder (example: ~/.i2pd/netDb)")
|
||||
rs_parser.set_defaults(func=reseed_action)
|
||||
|
||||
|
||||
tpull_parser = subparsers.add_parser(
|
||||
"transport.pull",
|
||||
description="Download su3 file from random reseed server",
|
||||
usage="""
|
||||
%(prog)s --urls https://reseed.i2p-projekt.de/ \\
|
||||
https://reseed.i2p.vzaws.com:8443/ \\
|
||||
--outfile output/i2pseeds.su3"""
|
||||
)
|
||||
tpull_parser.add_argument("--urls", default=pyseeder.transport.RESEED_URLS,
|
||||
nargs="*", help="""Reseed URLs separated by space, default are
|
||||
mainline I2P (like https://reseed.i2p-projekt.de/)""")
|
||||
tpull_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3",
|
||||
help="Output file (default: output/i2pseeds.su3)")
|
||||
tpull_parser.set_defaults(func=transport_pull_action)
|
||||
|
||||
|
||||
tpush_parser = subparsers.add_parser(
|
||||
"transport.push",
|
||||
description="Upload su3 file with transports",
|
||||
usage="%(prog)s --config transports.ini --file output/i2pseeds.su3"
|
||||
)
|
||||
tpush_parser.add_argument("--config", default="transports.ini",
|
||||
help="Transports config file (default: transports.ini)")
|
||||
tpush_parser.add_argument("-f", "--file", default="output/i2pseeds.su3",
|
||||
help=".su3 file (default: output/i2pseeds.su3)")
|
||||
tpush_parser.set_defaults(func=transport_push_action)
|
||||
|
||||
args = parser.parse_args()
|
||||
if hasattr(args, "func"):
|
||||
try:
|
||||
args.func(args)
|
||||
except PyseederException as pe:
|
||||
print("Pyseeder error: {}".format(pe))
|
||||
sys.exit(1)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
0
pyseeder/__init__.py
Normal file
0
pyseeder/__init__.py
Normal file
82
pyseeder/crypto.py
Normal file
82
pyseeder/crypto.py
Normal file
@ -0,0 +1,82 @@
|
||||
import os, os.path
|
||||
import random
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
from pyseeder.utils import PyseederException
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
def keygen(pub_key, priv_key, priv_key_password, user_id):
|
||||
"""Generate new private key and certificate RSA_SHA512_4096"""
|
||||
for f in [pub_key, priv_key]:
|
||||
if not os.access(os.path.dirname(f) or ".", os.W_OK):
|
||||
raise PyseederException("Can't write {}, access forbidden").format(f)
|
||||
|
||||
# Generate our key
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=4096,
|
||||
backend=default_backend())
|
||||
|
||||
# Write our key to disk for safe keeping
|
||||
with open(priv_key, "wb") as f:
|
||||
f.write(key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.BestAvailableEncryption(
|
||||
priv_key_password),
|
||||
))
|
||||
|
||||
# Various details about who we are. For a self-signed certificate the
|
||||
# subject and issuer are always the same.
|
||||
subject = issuer = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "XX"),
|
||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "XX"),
|
||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "XX"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "I2P Anonymous Network"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "I2P"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, user_id),
|
||||
])
|
||||
|
||||
cert = x509.CertificateBuilder() \
|
||||
.subject_name(subject) \
|
||||
.issuer_name(issuer) \
|
||||
.public_key(key.public_key()) \
|
||||
.not_valid_before(datetime.datetime.utcnow()) \
|
||||
.not_valid_after(
|
||||
datetime.datetime.utcnow() + datetime.timedelta(days=365*10)
|
||||
) \
|
||||
.serial_number(random.randrange(1000000000, 2000000000)) \
|
||||
.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
|
||||
critical=False,
|
||||
).sign(key, hashes.SHA512(), default_backend())
|
||||
|
||||
with open(pub_key, "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||
|
||||
|
||||
def append_signature(target_file, priv_key, priv_key_password):
|
||||
"""Append signature to the end of file"""
|
||||
if not os.path.exists(priv_key):
|
||||
raise PyseederException("Wrong private key path")
|
||||
|
||||
if not os.access(priv_key, os.R_OK):
|
||||
raise PyseederException("Can't read private key, access forbidden")
|
||||
|
||||
with open(target_file, "rb") as f:
|
||||
contents = f.read()
|
||||
|
||||
with open(priv_key, "rb") as kf:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
kf.read(), password=priv_key_password, backend=default_backend())
|
||||
|
||||
signature = private_key.sign(contents, padding.PKCS1v15(), hashes.SHA512())
|
||||
|
||||
with open(target_file, "ab") as f:
|
||||
f.write(signature)
|
84
pyseeder/su3file.py
Normal file
84
pyseeder/su3file.py
Normal file
@ -0,0 +1,84 @@
|
||||
import os, os.path
|
||||
import time, datetime
|
||||
import random
|
||||
import io
|
||||
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
import pyseeder.crypto
|
||||
from pyseeder.utils import PyseederException
|
||||
|
||||
|
||||
class SU3File:
|
||||
"""SU3 file format"""
|
||||
|
||||
def __init__(self, signer_id):
|
||||
self.SIGNER_ID = signer_id
|
||||
self.SIGNER_ID_LENGTH = len(self.SIGNER_ID)
|
||||
self.SIGNATURE_TYPE = 0x0006
|
||||
self.SIGNATURE_LENGTH = 512
|
||||
self.VERSION_LENGTH = 0x10
|
||||
self.FILE_TYPE = None
|
||||
self.CONTENT_TYPE = None
|
||||
self.CONTENT = None
|
||||
self.CONTENT_LENGTH = None
|
||||
self.VERSION = str(int(time.time())).encode("utf-8")
|
||||
#self.keytype = "RSA_SHA512_4096"
|
||||
|
||||
def write(self, filename, priv_key, priv_key_password):
|
||||
"""Write file to disc"""
|
||||
if not os.access(os.path.dirname(filename) or ".", os.W_OK):
|
||||
raise PyseederException("Can't write su3 file, access forbidden")
|
||||
|
||||
nullbyte = bytes([0])
|
||||
with open(filename, "wb") as f:
|
||||
f.write("I2Psu3".encode("utf-8"))
|
||||
f.write(bytes([0,0]))
|
||||
f.write(self.SIGNATURE_TYPE.to_bytes(2, "big"))
|
||||
f.write(self.SIGNATURE_LENGTH.to_bytes(2, "big"))
|
||||
f.write(nullbyte)
|
||||
f.write(bytes([self.VERSION_LENGTH]))
|
||||
f.write(nullbyte)
|
||||
f.write(bytes([self.SIGNER_ID_LENGTH]))
|
||||
f.write(self.CONTENT_LENGTH.to_bytes(8, "big"))
|
||||
f.write(nullbyte)
|
||||
f.write(bytes([self.FILE_TYPE]))
|
||||
f.write(nullbyte)
|
||||
f.write(bytes([self.CONTENT_TYPE]))
|
||||
f.write(bytes([0 for _ in range(12)]))
|
||||
f.write(self.VERSION + bytes(
|
||||
[0 for _ in range(16 - len(self.VERSION))]))
|
||||
f.write(self.SIGNER_ID.encode("utf-8"))
|
||||
f.write(self.CONTENT)
|
||||
|
||||
pyseeder.crypto.append_signature(filename, priv_key, priv_key_password)
|
||||
|
||||
def reseed(self, netdb):
|
||||
"""Compress netdb entries and set content"""
|
||||
zip_file = io.BytesIO()
|
||||
dat_files = []
|
||||
|
||||
if not os.path.exists(netdb):
|
||||
raise PyseederException("Wrong netDb path")
|
||||
|
||||
if not os.access(netdb, os.R_OK):
|
||||
raise PyseederException("Can't read netDb, access forbidden")
|
||||
|
||||
for root, dirs, files in os.walk(netdb):
|
||||
for f in files:
|
||||
if f.endswith(".dat"):
|
||||
# TODO check modified time
|
||||
# may be not older than 10h
|
||||
dat_files.append(os.path.join(root, f))
|
||||
|
||||
if len(dat_files) < 100:
|
||||
raise PyseederException("Can't get enough netDb entries. Wrong netDb path?")
|
||||
|
||||
with ZipFile(zip_file, "w", compression=ZIP_DEFLATED) as zf:
|
||||
for f in random.sample(dat_files, 75):
|
||||
zf.write(f, arcname=os.path.split(f)[1])
|
||||
|
||||
self.FILE_TYPE = 0x00
|
||||
self.CONTENT_TYPE = 0x03
|
||||
self.CONTENT = zip_file.getvalue()
|
||||
self.CONTENT_LENGTH = len(self.CONTENT)
|
58
pyseeder/transport.py
Normal file
58
pyseeder/transport.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Module for managing transport tasks"""
|
||||
import urllib.request
|
||||
from urllib.error import URLError
|
||||
import os, os.path
|
||||
import importlib
|
||||
|
||||
from pyseeder.utils import PyseederException
|
||||
|
||||
|
||||
RESEED_URLS = [
|
||||
"https://reseed.i2p-projekt.de/",
|
||||
"https://i2p.mooo.com/netDb/",
|
||||
"https://netdb.i2p2.no/",
|
||||
"https://us.reseed.i2p2.no:444/",
|
||||
"https://uk.reseed.i2p2.no:444/",
|
||||
"https://i2p.manas.ca:8443/",
|
||||
"https://i2p-0.manas.ca:8443/",
|
||||
"https://reseed.i2p.vzaws.com:8443/",
|
||||
"https://user.mx24.eu/",
|
||||
"https://download.xxlspeed.com/",
|
||||
]
|
||||
|
||||
def download(url, filename):
|
||||
"""Download .su3 file, return True on success"""
|
||||
USER_AGENT = "Wget/1.11.4"
|
||||
|
||||
url = "{}i2pseeds.su3".format(url)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(resp.read())
|
||||
|
||||
if os.stat(filename).st_size > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except URLError as e:
|
||||
return False
|
||||
|
||||
|
||||
def upload(filename, config):
|
||||
"""Upload .su3 file with transports"""
|
||||
if "transports" in config and "enabled" in config["transports"]:
|
||||
for t in config["transports"]["enabled"].split():
|
||||
if t in config:
|
||||
tconf = config[t]
|
||||
else:
|
||||
tconf = None
|
||||
|
||||
try:
|
||||
importlib.import_module("pyseeder.transports.{}".format(t)) \
|
||||
.run(filename, tconf)
|
||||
except ImportError:
|
||||
raise PyseederException(
|
||||
"{} transport can't be loaded".format(t))
|
||||
|
0
pyseeder/transports/__init__.py
Normal file
0
pyseeder/transports/__init__.py
Normal file
3
pyseeder/transports/dropbox.py
Normal file
3
pyseeder/transports/dropbox.py
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
def run(filename, config):
|
||||
print("dummy dropbox plugin")
|
39
pyseeder/transports/git.py
Normal file
39
pyseeder/transports/git.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Git transport plugin"""
|
||||
import subprocess
|
||||
import os, os.path
|
||||
from shutil import copyfile
|
||||
from pyseeder.utils import TransportException
|
||||
|
||||
TRANSPORT_NAME = "git"
|
||||
|
||||
# Push to github repo witout prompting password.
|
||||
# Set up SSH keys or change origin URL like that:
|
||||
# git remote set-url origin https://$USERNAME:$PASSWORD@github.com/$USERNAME/$REPO.git
|
||||
|
||||
def run(filename, config):
|
||||
if "folder" not in config:
|
||||
raise TransportException("git: No folder specified in config")
|
||||
else:
|
||||
REPO_FOLDER = config["folder"]
|
||||
|
||||
REPO_FILE = os.path.split(filename)[1]
|
||||
|
||||
if not os.access(REPO_FOLDER, os.W_OK):
|
||||
raise TransportException("git: {} access forbidden" \
|
||||
.format(REPO_FOLDER))
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
raise TransportException("git: input file not found")
|
||||
|
||||
copyfile(filename, os.path.join(REPO_FOLDER, REPO_FILE))
|
||||
|
||||
commands = [
|
||||
"git add {}".format(REPO_FILE),
|
||||
"git commit -m 'update'",
|
||||
"git push origin master"
|
||||
]
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(REPO_FOLDER)
|
||||
for c in commands: subprocess.call(c, shell=True)
|
||||
os.chdir(cwd)
|
7
pyseeder/utils.py
Normal file
7
pyseeder/utils.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""Various code"""
|
||||
|
||||
class PyseederException(Exception):
|
||||
pass
|
||||
|
||||
class TransportException(PyseederException):
|
||||
pass
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
cffi==1.7.0
|
||||
cryptography==1.4
|
||||
idna==2.1
|
||||
pyasn1==0.1.9
|
||||
pycparser==2.14
|
||||
six==1.10.0
|
10
transports.ini.example
Normal file
10
transports.ini.example
Normal file
@ -0,0 +1,10 @@
|
||||
[transports]
|
||||
; enabled transports separated by space
|
||||
enabled=git
|
||||
|
||||
[git]
|
||||
; Folder with git repository to use
|
||||
folder=/home/user/reseed-data-repo
|
||||
|
||||
[dropbox]
|
||||
; todo
|
Loading…
x
Reference in New Issue
Block a user