mirror of https://github.com/PurpleI2P/pyseeder
libre-net-society
8 years ago
commit
03af5634c1
15 changed files with 491 additions and 0 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
__pycache__ |
||||
*.pyc |
||||
venv/ |
||||
transports.ini |
@ -0,0 +1,70 @@
@@ -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. |
@ -0,0 +1,125 @@
@@ -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,0 +1,82 @@
@@ -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) |
@ -0,0 +1,84 @@
@@ -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) |
@ -0,0 +1,58 @@
@@ -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,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
|
||||
def run(filename, config): |
||||
print("dummy dropbox plugin") |
@ -0,0 +1,39 @@
@@ -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) |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
"""Various code""" |
||||
|
||||
class PyseederException(Exception): |
||||
pass |
||||
|
||||
class TransportException(PyseederException): |
||||
pass |
@ -0,0 +1,6 @@
@@ -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 |
Loading…
Reference in new issue