├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── contrib └── docker │ ├── Dockerfile │ ├── README.md │ └── entrypoint.sh ├── data └── .gitignore ├── output └── .gitignore ├── pyproject.toml ├── pyseeder.py ├── pyseeder ├── __init__.py ├── actions.py ├── cli.py ├── crypto.py ├── routerinfo.py ├── server.py ├── su3file.py ├── transport.py ├── transports │ ├── __init__.py │ ├── dropbox.py │ ├── git.py │ └── github.py └── utils.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── transports.ini.example └── yggdrasil ├── README.md ├── err.html ├── reseed_nginx.conf ├── seed.html ├── wild-putin.png └── y2r.sh /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | build/ 4 | pyseeder.egg-info/ 5 | venv/ 6 | transports.ini 7 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements: python3 and python cryptography package, requests for transports 5 | 6 | 7 | Example for Ubuntu/Debian 8 | ------------------------- 9 | 10 | 11 | Basic python packages & packages for building python cryptography package: 12 | 13 | sudo apt-get install python3 python3-pip python3-virtualenv build-essential libssl-dev libffi-dev python-dev 14 | 15 | 16 | Clone repo: 17 | 18 | git clone https://github.com/PurpleI2P/pyseeder.git 19 | 20 | 21 | Configure new python virtual environment: 22 | 23 | cd pyseeder 24 | virtualenv --python=python3 venv 25 | . venv/bin/activate 26 | pip3 install . 27 | 28 | 29 | Thats it! Next time you will need to run pyseeder, don't forget to activate 30 | python virtual environment as followed: 31 | 32 | . venv/bin/activate 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Darknet Villain 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyseeder 2 | ======== 3 | 4 | Reseed data managment tools for I2P 5 | 6 | * Generate reseed signing keypair 7 | * Make reseed data files (su3) 8 | * Download su3 files from official servers for mirroring 9 | * Upload reseed data to different places (with plugins) 10 | * Run HTTPS reseed server 11 | 12 | Reseed transports are implemented so that users can bootstrap their I2P nodes 13 | without needing to connect to "official" I2P reseeds. This makes I2P more 14 | invisible for firewalls. 15 | 16 | Installation 17 | ------------ 18 | 19 | $ pip3 install https://github.com/PurpleI2P/pyseeder/zipball/master 20 | 21 | [Detailed installation instructions](INSTALL.md) 22 | 23 | 24 | Usage 25 | ----- 26 | 27 | $ pyseeder --help 28 | $ pyseeder keygen --help 29 | 30 | 31 | Generating keypair 32 | ------------------ 33 | 34 | 35 | $ pyseeder keygen --cert data/user_at_mail.i2p.crt --private-key data/priv_key.pem --signer-id user@mail.i2p 36 | 37 | This will generate certificate (user\_at\_mail.i2p.crt) and private RSA key 38 | (priv\_key.pem) in data folder. E-mail is used as certificate identifier. 39 | 40 | Script will prompt for private key password. 41 | 42 | 43 | Generating reseed data 44 | ---------------------- 45 | 46 | 47 | $ YOUR_PASSWORD="Pa55w0rd" 48 | $ echo $YOUR_PASSWORD | pyseeder reseed --netdb /path/to/netDb --private-key data/priv_key.pem --outfile output/i2pseeds.su3 --signer-id user@mail.i2p 49 | 50 | This will generate file i2pseeds.su3 in output folder, using user@mail.i2p as 51 | certificate identifier. 52 | 53 | Note: you'll have to enter your private key password to stdin, the above 54 | is one of the ways to do it (for cron and scripts). 55 | 56 | 57 | Download su3 file from official servers 58 | --------------------------------------- 59 | 60 | $ pyseeder transport.pull --urls https://reseed.i2p-projekt.de/ https://reseed.i2p.vzaws.com:8443/ --outfile output/i2pseeds.su3 61 | 62 | Note: --urls parameter is optional, defaults are "official" I2P reseeds. 63 | 64 | 65 | Upload su3 file with pluggable transports 66 | ----------------------------------------- 67 | 68 | $ pyseeder transport.push --config transports.ini --file output/i2pseeds.su3 69 | 70 | All parameters are optional. Copy file transports.ini.example to 71 | transports.ini. Edit your settings in this new file. 72 | 73 | 74 | Run HTTPS reseed server 75 | ----------------------- 76 | 77 | $ pyseeder serve --port 8443 --host 127.0.0.1 --private-key data/priv_key.pem --cert data/user_at_mail.i2p.crt --file output/i2pseeds.su3 78 | 79 | Note: this server is fine for testing, but for "production" environments please 80 | use nginx webserver. 81 | -------------------------------------------------------------------------------- /contrib/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL authors "Darknet Villain " 3 | 4 | ENV PYSEEDER_HOME="/home/pyseeder" 5 | ENV DATA_DIR="${PYSEEDER_HOME}/data" 6 | 7 | RUN mkdir -p "$PYSEEDER_HOME" "$DATA_DIR" "$DATA_DIR"/data "$DATA_DIR"/output \ 8 | && adduser -S -h "$PYSEEDER_HOME" pyseeder \ 9 | && chown -R pyseeder:nobody "$PYSEEDER_HOME" \ 10 | && chmod -R 700 "$DATA_DIR" 11 | 12 | RUN apk --no-cache add python3 py3-pip build-base git openssl-dev musl-dev python3-dev libffi-dev \ 13 | && pip3 install --no-cache-dir https://github.com/PurpleI2P/pyseeder/zipball/master \ 14 | && apk --purge del python3 py3-pip build-base git openssl-dev musl-dev python3-dev libffi-dev 15 | 16 | # 2. Adding required libraries to run i2pd to ensure it will run. 17 | RUN apk --no-cache add python3 openssl 18 | 19 | RUN mkdir /netDb 20 | 21 | VOLUME "$DATA_DIR" 22 | 23 | COPY entrypoint.sh /entrypoint.sh 24 | RUN chmod a+x /entrypoint.sh 25 | 26 | EXPOSE 8443 27 | 28 | USER pyseeder 29 | 30 | ENTRYPOINT [ "/entrypoint.sh" ] 31 | 32 | -------------------------------------------------------------------------------- /contrib/docker/README.md: -------------------------------------------------------------------------------- 1 | Pyseeder docker container 2 | ========================= 3 | 4 | Using it with i2pd container: 5 | 6 | 1) Run i2pd docker container and locate it's datadir volume with `docker inspect ` 7 | 8 | It will show something like that: 9 | 10 | ... 11 | 12 | "Mounts": [ 13 | { 14 | "Name": "69c5fb747b09701acebcf22478f99719e87ac65b3ac11464cd949bebe03a744a", 15 | "Source": "/var/lib/docker/volumes/69c5fb747b09701acebcf22478f99719e87ac65b3ac11464cd949bebe03a744a/_data", 16 | "Destination": "/home/i2pd/data", 17 | "Driver": "local", 18 | "Mode": "", 19 | "RW": true, 20 | "Propagation": "" 21 | } 22 | ], 23 | 24 | ... 25 | 26 | 2) Run pyseeder image with that volume mounted at `/i2pd_data`: 27 | 28 | docker run --name=test10 -P -dt -v 69c5fb747b09701acebcf22478f99719e87ac65b3ac11464cd949bebe03a744a:/i2pd_data pyseeder 29 | 30 | 3) Enjoy your reseed. 31 | -------------------------------------------------------------------------------- /contrib/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | COMMAND="pyseeder" 3 | 4 | if [ "$1" = "--help" ]; then 5 | $COMMAND --help 6 | else 7 | # TODO 8 | # make configurable SIGNER_ID and cert, key etc 9 | 10 | DATA_DIR="/home/pyseeder/data" 11 | SIGNER_ID="test@mail.i2p" 12 | PRIVKEY_FILE="$DATA_DIR/data/private_key.pem" 13 | CERT_FILE=` echo $DATA_DIR/data/$SIGNER_ID.crt | sed 's/@/_at_/' ` 14 | RESEED_FILE="$DATA_DIR/output/i2pseeds.su3" 15 | NETDB_DIR="/i2pd_data/netDb" 16 | if [ ! -d $NETDB_DIR ]; then 17 | NETDB_DIR="/netDb" 18 | fi 19 | 20 | $COMMAND keygen --signer-id $SIGNER_ID --no-encrypt \ 21 | --private-key $PRIVKEY_FILE --cert $CERT_FILE 22 | $COMMAND reseed --netdb $NETDB_DIR --signer-id $SIGNER_ID --no-encrypt \ 23 | --private-key $PRIVKEY_FILE --outfile $RESEED_FILE 24 | $COMMAND serve --private-key $PRIVKEY_FILE --cert $CERT_FILE --file $RESEED_FILE 25 | fi 26 | 27 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.pem 3 | -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | *.su3 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | 4 | [tools.setuptools] 5 | license_files = [] 6 | -------------------------------------------------------------------------------- /pyseeder.py: -------------------------------------------------------------------------------- 1 | pyseeder/cli.py -------------------------------------------------------------------------------- /pyseeder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PurpleI2P/pyseeder/4dbdf65f46496425f6f7293e45f4a8c0aeea3762/pyseeder/__init__.py -------------------------------------------------------------------------------- /pyseeder/actions.py: -------------------------------------------------------------------------------- 1 | """Action functions for argparser""" 2 | import pyseeder.transport 3 | import pyseeder.actions 4 | from pyseeder.utils import PyseederException, check_readable, check_writable 5 | 6 | def keygen(args): 7 | """Sub-command to generate keys""" 8 | if not args.cert: 9 | args.cert = "data/{}.crt".format(args.signer_id.replace("@", "_at_")) 10 | 11 | for f in [args.cert, args.private_key]: check_writable(f) 12 | 13 | from pyseeder.crypto import keygen 14 | 15 | if args.no_encryption: 16 | priv_key_password = None 17 | else: 18 | from getpass import getpass 19 | priv_key_password = getpass("Set private key password: ").encode("utf-8") 20 | 21 | keygen(args.cert, args.private_key, args.signer_id, priv_key_password) 22 | 23 | def reseed(args): 24 | """Sub-command to generate reseed file""" 25 | check_writable(args.outfile) 26 | for f in [args.netdb, args.private_key]: check_readable(f) 27 | 28 | from pyseeder.su3file import SU3File 29 | 30 | if args.no_encryption: 31 | priv_key_password = None 32 | else: 33 | priv_key_password = input().encode("utf-8") 34 | 35 | su3file = SU3File(args.signer_id) 36 | su3file.reseed(args.netdb, args.yggseeds) 37 | su3file.write(args.outfile, args.private_key, priv_key_password) 38 | 39 | def transport_pull(args): 40 | """Sub-command for downloading su3 file""" 41 | import random 42 | random.shuffle(args.urls) 43 | 44 | for u in args.urls: 45 | if pyseeder.transport.download(u, args.outfile): 46 | return True 47 | 48 | raise PyseederException("Failed to download su3 file") 49 | 50 | def transport_push(args): 51 | """Sub-command for uploading su3 file with transports""" 52 | check_readable(args.config) 53 | 54 | import configparser 55 | config = configparser.ConfigParser() 56 | config.read(args.config) 57 | pyseeder.transport.upload(args.file, config) 58 | 59 | def serve(args): 60 | """Sub-command to start HTTPS reseed server""" 61 | for f in [args.private_key, args.cert, args.file]: check_readable(f) 62 | 63 | import pyseeder.server 64 | pyseeder.server.run_server(args.host, args.port, args.private_key, 65 | args.cert, args.file) 66 | -------------------------------------------------------------------------------- /pyseeder/cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import os 3 | import sys 4 | import argparse 5 | import logging 6 | 7 | import pyseeder.transport 8 | import pyseeder.actions 9 | from pyseeder.utils import PyseederException 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('--loglevel', default=logging.INFO, help="Log level", 16 | choices=[logging.CRITICAL, logging.ERROR, logging.WARNING, 17 | logging.INFO, logging.DEBUG]) 18 | 19 | subparsers = parser.add_subparsers(title="actions", 20 | help="Command to execute") 21 | 22 | kg_parser = subparsers.add_parser( 23 | "keygen", 24 | description="Generates keypair for your reseed", 25 | usage=""" 26 | %(prog)s --cert data/user_at_mail.i2p.crt \\ 27 | --private-key data/priv_key.pem --signer-id user@mail.i2p""" 28 | ) 29 | kg_parser.add_argument("--signer-id", required=True, 30 | help="Identifier of certificate (example: user@mail.i2p)") 31 | kg_parser.add_argument("--private-key", default="data/priv_key.pem", 32 | help="RSA private key (default: data/priv_key.pem)") 33 | kg_parser.add_argument("--cert", default=None, 34 | help="Certificate (example: data/user_at_mail.i2p.crt)") 35 | kg_parser.add_argument("--no-encryption", action="store_true", 36 | help="Disable private key encryption") 37 | kg_parser.set_defaults(func=pyseeder.actions.keygen) 38 | 39 | 40 | rs_parser = subparsers.add_parser( 41 | "reseed", 42 | description="Creates su3 reseed file", 43 | usage=""" 44 | echo $YOUR_PASSWORD | %(prog)s --netdb /path/to/netDb \\ 45 | --private-key data/priv_key.pem --outfile output/i2pseeds.su3 \\ 46 | --signer-id user@mail.i2p""" 47 | ) 48 | rs_parser.add_argument("--signer-id", required=True, 49 | help="Identifier of certificate (example: user@mail.i2p)") 50 | rs_parser.add_argument("--private-key", default="data/priv_key.pem", 51 | help="RSA private key (default: data/priv_key.pem)") 52 | rs_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3", 53 | help="Output file (default: output/i2pseeds.su3)") 54 | rs_parser.add_argument("--netdb", required=True, 55 | help="Path to netDb folder (example: ~/.i2pd/netDb)") 56 | rs_parser.add_argument("--no-encryption", action="store_true", 57 | help="Disable private key encryption") 58 | rs_parser.add_argument("--yggseeds", type=int, default=0, 59 | help="Amount of yggdrasil seeds to include to reseed") 60 | rs_parser.set_defaults(func=pyseeder.actions.reseed) 61 | 62 | 63 | tpull_parser = subparsers.add_parser( 64 | "transport.pull", 65 | description="Download su3 file from random reseed server", 66 | usage=""" 67 | %(prog)s --urls https://reseed.i2p-projekt.de/ \\ 68 | https://reseed.i2p.vzaws.com:8443/ \\ 69 | --outfile output/i2pseeds.su3""" 70 | ) 71 | tpull_parser.add_argument("--urls", default=pyseeder.transport.RESEED_URLS, 72 | nargs="*", help="""Reseed URLs separated by space, default are 73 | mainline I2P (like https://reseed.i2p-projekt.de/)""") 74 | tpull_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3", 75 | help="Output file (default: output/i2pseeds.su3)") 76 | tpull_parser.set_defaults(func=pyseeder.actions.transport_pull) 77 | 78 | 79 | tpush_parser = subparsers.add_parser( 80 | "transport.push", 81 | description="Upload su3 file with transports", 82 | usage="%(prog)s --config transports.ini --file output/i2pseeds.su3" 83 | ) 84 | tpush_parser.add_argument("--config", default="transports.ini", 85 | help="Transports config file (default: transports.ini)") 86 | tpush_parser.add_argument("-f", "--file", default="output/i2pseeds.su3", 87 | help=".su3 file (default: output/i2pseeds.su3)") 88 | tpush_parser.set_defaults(func=pyseeder.actions.transport_push) 89 | 90 | 91 | serve_parser = subparsers.add_parser( 92 | "serve", 93 | description="""Run HTTPS reseeding server 94 | (in production use nginx instead, please). 95 | Will ask for a private key password""", 96 | usage="""%(prog)s --port 8443 --host 127.0.0.1 \\ 97 | --private-key data/priv_key.pem \\ 98 | --cert data/user_at_mail.i2p.crt \\ 99 | --file output/i2pseeds.su3""" 100 | ) 101 | serve_parser.add_argument("--host", default="0.0.0.0", 102 | help="Host listening for clients (default: 0.0.0.0)") 103 | serve_parser.add_argument("--port", default=8443, 104 | help="Port listening for clients (default: 8443)") 105 | serve_parser.add_argument("--private-key", default="data/priv_key.pem", 106 | help="RSA private key (default: data/priv_key.pem)") 107 | serve_parser.add_argument("--cert", required=True, 108 | help="Certificate (example: data/user_at_mail.i2p.crt)") 109 | serve_parser.add_argument("-f", "--file", default="output/i2pseeds.su3", 110 | help=".su3 file (default: output/i2pseeds.su3)") 111 | serve_parser.set_defaults(func=pyseeder.actions.serve) 112 | 113 | 114 | args = parser.parse_args() 115 | 116 | logging.basicConfig(level=args.loglevel, 117 | format='%(levelname)-8s %(message)s') 118 | 119 | if hasattr(args, "func"): 120 | try: 121 | args.func(args) 122 | except PyseederException as pe: 123 | log.critical("Pyseeder error: {}".format(pe)) 124 | sys.exit(1) 125 | else: 126 | parser.print_help() 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /pyseeder/crypto.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import sys 4 | import datetime 5 | 6 | from pyseeder.utils import PyseederException 7 | 8 | from cryptography.hazmat.backends import default_backend 9 | from cryptography.hazmat.primitives import serialization 10 | from cryptography.hazmat.primitives.asymmetric import rsa 11 | from cryptography import x509 12 | from cryptography.x509.oid import NameOID 13 | from cryptography.hazmat.primitives import hashes 14 | from cryptography.hazmat.primitives.asymmetric import padding 15 | 16 | def keygen(pub_key, priv_key, user_id, priv_key_password=None): 17 | """Generate new private key and certificate RSA_SHA512_4096""" 18 | # Generate our key 19 | key = rsa.generate_private_key(public_exponent=65537, key_size=4096, 20 | backend=default_backend()) 21 | 22 | if priv_key_password: 23 | ea = serialization.BestAvailableEncryption(priv_key_password) 24 | else: 25 | ea = serialization.NoEncryption() 26 | 27 | # Write our key to disk for safe keeping 28 | with open(priv_key, "wb") as f: 29 | f.write(key.private_bytes( 30 | encoding=serialization.Encoding.PEM, 31 | format=serialization.PrivateFormat.TraditionalOpenSSL, 32 | encryption_algorithm=ea, 33 | )) 34 | 35 | # Various details about who we are. For a self-signed certificate the 36 | # subject and issuer are always the same. 37 | subject = issuer = x509.Name([ 38 | x509.NameAttribute(NameOID.COUNTRY_NAME, "XX"), 39 | x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "XX"), 40 | x509.NameAttribute(NameOID.LOCALITY_NAME, "XX"), 41 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, "I2P Anonymous Network"), 42 | x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "I2P"), 43 | x509.NameAttribute(NameOID.COMMON_NAME, user_id), 44 | ]) 45 | 46 | cert = x509.CertificateBuilder() \ 47 | .subject_name(subject) \ 48 | .issuer_name(issuer) \ 49 | .public_key(key.public_key()) \ 50 | .not_valid_before(datetime.datetime.utcnow()) \ 51 | .not_valid_after( 52 | datetime.datetime.utcnow() + datetime.timedelta(days=365*10) 53 | ) \ 54 | .serial_number(random.randrange(1000000000, 2000000000)) \ 55 | .add_extension( 56 | x509.SubjectKeyIdentifier.from_public_key(key.public_key()), 57 | critical=False, 58 | ).sign(key, hashes.SHA512(), default_backend()) 59 | 60 | with open(pub_key, "wb") as f: 61 | f.write(cert.public_bytes(serialization.Encoding.PEM)) 62 | 63 | 64 | def get_signature(contents, priv_key, priv_key_password=None): 65 | """Calculate signature for prepared reseed file""" 66 | """Singing with NoneWithRSA algorithm: https://stackoverflow.com/a/68301530""" 67 | import rsa as pyrsa 68 | 69 | with open(priv_key, "rb") as kf: 70 | pk = pyrsa.PrivateKey.load_pkcs1( 71 | serialization.load_pem_private_key( 72 | kf.read(), password=priv_key_password, backend=default_backend() 73 | ).private_bytes( 74 | serialization.Encoding.PEM, 75 | serialization.PrivateFormat.TraditionalOpenSSL, 76 | serialization.NoEncryption() 77 | ) 78 | ) 79 | 80 | digest = hashes.Hash(hashes.SHA512(), default_backend()) 81 | digest.update(contents) 82 | h = digest.finalize() 83 | 84 | keylength = pyrsa.pkcs1.common.byte_size(pk.n) 85 | padded = pyrsa.pkcs1._pad_for_signing(h, keylength) 86 | payload = pyrsa.pkcs1.transform.bytes2int(padded) 87 | encrypted = pk.blinded_encrypt(payload) 88 | sig = pyrsa.pkcs1.transform.int2bytes(encrypted, keylength) 89 | 90 | return sig 91 | -------------------------------------------------------------------------------- /pyseeder/routerinfo.py: -------------------------------------------------------------------------------- 1 | class RouterInfo: 2 | 3 | def __init__(self, filename): 4 | self.yggdrasil = False 5 | with open(filename, 'rb') as f: 6 | buf = f.read() 7 | if len(buf) < 387: 8 | return 9 | offset = 384 # crypto and signing keys 10 | offset += int.from_bytes(buf[offset + 1: offset + 3], byteorder='big') # size from certificate 11 | offset += 3 # certificate 12 | self.timestamp = int.from_bytes(buf[offset: offset + 8], byteorder='big') # 8 bytes timestamp 13 | offset += 8 14 | offset += self.readaddresses(buf[offset:]) # addresses 15 | offset += buf[offset]*32 + 1; # peers 16 | self.properties = {} 17 | offset += self.readproperties(buf[offset:]) # properties 18 | 19 | def readaddresses(self, buf): 20 | numaddresses = buf[0] 21 | offset = 1 22 | for i in range(0, numaddresses): 23 | offset += 1 # cost 24 | offset += 8 # date 25 | style = self.readstring(buf[offset:]) # transport style 26 | offset += len(style) + 1 # style string 27 | size = int.from_bytes(buf[offset:offset+2], byteorder='big') # size of properties 28 | offset += 2 29 | if not self.yggdrasil and style == 'NTCP2': # possible yggdrasil? 30 | r = offset 31 | while r < size + offset: 32 | key = self.readstring(buf[r:]) 33 | r += len(key) + 2 # length and = 34 | if key == 'host': 35 | value = self.readstring(buf[r:]) 36 | firstcolon = value.find(':') 37 | if firstcolon > 0: # ipv6 address 38 | first = int(value[0:firstcolon], 16) # first segment of ipv6 address 39 | if first >= 0x0200 and first <= 0x03FF: # yggdrasil range 40 | self.yggdrasil = True 41 | break 42 | r += len(self.readstring(buf[r:])) + 2 # length and ; 43 | offset += size 44 | return offset 45 | 46 | def readproperties(self, buf): 47 | size = int.from_bytes(buf[0:2], byteorder='big') 48 | r = 2 49 | while r < size + 2: 50 | key = self.readstring(buf[r:]) 51 | r += len(key) + 2 # length and = 52 | value = self.readstring(buf[r:]) 53 | r += len(value) + 2 # length and ; 54 | self.properties[key] = value 55 | return size 56 | 57 | def readstring(self, buf): 58 | l = buf[0] 59 | return buf[1:l+1].decode('utf-8') 60 | 61 | def getversion(self): 62 | if 'router.version' in self.properties: 63 | v = 0 64 | for c in self.properties['router.version']: 65 | if c >= '0' and c <= '9': 66 | v *= 10 67 | v += (ord(c) - ord('0')) 68 | return v 69 | else: 70 | return 0 71 | 72 | def hasinvalidcaps(self): 73 | invalidcaps = set('UDEG') 74 | return any((c in invalidcaps) for c in self.properties['caps']) 75 | 76 | def isvalid(self): 77 | return self.getversion() >= 959 and not self.hasinvalidcaps() # version >= 0.9.59 and no 'U', 'D', 'E' or 'G' caps 78 | 79 | def isyggdrasil(self): 80 | return self.yggdrasil 81 | 82 | -------------------------------------------------------------------------------- /pyseeder/server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import urllib.parse 3 | import ssl 4 | import os 5 | 6 | class ReseedHandler(http.server.SimpleHTTPRequestHandler): 7 | """Handles reseeding requests""" 8 | i2pseeds_file = "" 9 | server_version = "Pyseeder Server" 10 | sys_version = "" 11 | 12 | def do_GET(self): 13 | path = urllib.parse.urlparse(self.path).path 14 | if path == "/i2pseeds.su3": 15 | self.send_response(200) 16 | self.send_header("Content-Type", "application/octet-stream") 17 | self.send_header("Content-Length", 18 | os.path.getsize(self.i2pseeds_file)) 19 | self.end_headers() 20 | with open(self.i2pseeds_file, 'rb') as f: 21 | self.wfile.write(f.read()) 22 | else: 23 | self.send_error(404, "Not found") 24 | 25 | def run_server(host, port, priv_key, cert, i2pseeds_file): 26 | """Start HTTPS server""" 27 | Handler = ReseedHandler 28 | Handler.i2pseeds_file = i2pseeds_file 29 | 30 | httpd = http.server.HTTPServer((host, int(port)), Handler) 31 | httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True, 32 | keyfile=priv_key, certfile=cert, ssl_version=ssl.PROTOCOL_TLSv1) 33 | 34 | try: 35 | httpd.serve_forever() 36 | except KeyboardInterrupt: 37 | exit() 38 | -------------------------------------------------------------------------------- /pyseeder/su3file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time, datetime 3 | import random 4 | import io 5 | 6 | from zipfile import ZipFile, ZIP_DEFLATED 7 | 8 | import pyseeder.crypto 9 | from pyseeder.routerinfo import RouterInfo 10 | from pyseeder.utils import PyseederException 11 | 12 | 13 | class SU3File: 14 | """SU3 file format""" 15 | 16 | def __init__(self, signer_id): 17 | self.SIGNER_ID = signer_id 18 | self.SIGNER_ID_LENGTH = len(self.SIGNER_ID) 19 | self.SIGNATURE_TYPE = 0x0006 20 | self.SIGNATURE_LENGTH = 512 21 | self.VERSION_LENGTH = 0x10 22 | self.FILE_TYPE = None 23 | self.CONTENT_TYPE = None 24 | self.CONTENT = None 25 | self.CONTENT_LENGTH = None 26 | self.VERSION = str(int(time.time())).encode("utf-8") 27 | #self.keytype = "RSA_SHA512_4096" 28 | self.OUTPUT = None 29 | 30 | def write(self, filename, priv_key, priv_key_password=None): 31 | """Write file to disc""" 32 | nullbyte = bytes([0]) 33 | 34 | self.OUTPUT = "I2Psu3".encode("utf-8") 35 | self.OUTPUT += bytes([0,0]) 36 | self.OUTPUT += self.SIGNATURE_TYPE.to_bytes(2, "big") 37 | self.OUTPUT += self.SIGNATURE_LENGTH.to_bytes(2, "big") 38 | self.OUTPUT += nullbyte 39 | self.OUTPUT += bytes([self.VERSION_LENGTH]) 40 | self.OUTPUT += nullbyte 41 | self.OUTPUT += bytes([self.SIGNER_ID_LENGTH]) 42 | self.OUTPUT += self.CONTENT_LENGTH.to_bytes(8, "big") 43 | self.OUTPUT += nullbyte 44 | self.OUTPUT += bytes([self.FILE_TYPE]) 45 | self.OUTPUT += nullbyte 46 | self.OUTPUT += bytes([self.CONTENT_TYPE]) 47 | self.OUTPUT += bytes([0 for _ in range(12)]) 48 | self.OUTPUT += self.VERSION + bytes( 49 | [0 for _ in range(16 - len(self.VERSION))]) 50 | self.OUTPUT += self.SIGNER_ID.encode("utf-8") 51 | self.OUTPUT += self.CONTENT 52 | 53 | signature = pyseeder.crypto.get_signature(self.OUTPUT, priv_key, priv_key_password) 54 | self.OUTPUT += signature 55 | 56 | with open(filename, "wb") as f: 57 | f.write(self.OUTPUT) 58 | 59 | def reseed(self, netdb, yggseeds): 60 | """Compress netdb entries and set content""" 61 | seeds = 75 62 | zip_file = io.BytesIO() 63 | dat_files = [] 64 | dat_yggfiles = [] 65 | 66 | timelimit = time.time() - float(3600 * 10) # current time minus 10 hours 67 | 68 | for root, dirs, files in os.walk(netdb): 69 | for f in files: 70 | if f.endswith(".dat"): 71 | path = os.path.join(root, f) 72 | 73 | if os.path.getmtime(path) < timelimit: # modified time older than 10h 74 | continue 75 | 76 | ri = RouterInfo(path) 77 | if ri.isvalid(): 78 | ygg_file_added = False 79 | if yggseeds > 0 and ri.isyggdrasil(): 80 | dat_yggfiles.append(path) 81 | ygg_file_added = True 82 | 83 | if not ygg_file_added: 84 | dat_files.append(path) 85 | 86 | 87 | if yggseeds > 0: 88 | if len(dat_yggfiles) == 0: 89 | raise PyseederException("Can't get enough netDb entries with yggdrasil addresses") 90 | elif len(dat_yggfiles) > yggseeds: 91 | dat_yggfiles = random.sample(dat_yggfiles, yggseeds) 92 | seeds = seeds - len(dat_yggfiles) 93 | 94 | if len(dat_files) == 0: 95 | raise PyseederException("Can't get enough netDb entries") 96 | elif len(dat_files) > seeds: 97 | dat_files = random.sample(dat_files, seeds) 98 | 99 | dat_files.extend(dat_yggfiles) 100 | 101 | with ZipFile(zip_file, "w", compression=ZIP_DEFLATED) as zf: 102 | for f in dat_files: 103 | zf.write(f, arcname=os.path.split(f)[1]) 104 | 105 | self.FILE_TYPE = 0x00 106 | self.CONTENT_TYPE = 0x03 107 | self.CONTENT = zip_file.getvalue() 108 | self.CONTENT_LENGTH = len(self.CONTENT) 109 | -------------------------------------------------------------------------------- /pyseeder/transport.py: -------------------------------------------------------------------------------- 1 | """Module for managing transport tasks""" 2 | import urllib.request 3 | from urllib.error import URLError 4 | import os 5 | import importlib 6 | import logging 7 | 8 | from pyseeder.utils import PyseederException, TransportException 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | RESEED_URLS = [ 13 | "https://reseed.i2p-projekt.de/", 14 | "https://i2p.mooo.com/netDb/", 15 | "https://reseed.i2p2.no/", 16 | "https://reseed-fr.i2pd.xyz/", 17 | "https://reseed.memcpy.io/", 18 | "https://reseed.onion.im/", 19 | "https://i2pseed.creativecowpat.net:8443/", 20 | "https://reseed.i2pgit.org/", 21 | "https://i2p.novg.net/", 22 | ] 23 | 24 | def download(url, filename): 25 | """Download .su3 file, return True on success""" 26 | USER_AGENT = "Wget/1.11.4" 27 | 28 | url = "{}i2pseeds.su3".format(url) 29 | req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) 30 | 31 | try: 32 | with urllib.request.urlopen(req) as resp: 33 | with open(filename, 'wb') as f: 34 | f.write(resp.read()) 35 | 36 | if os.stat(filename).st_size > 0: 37 | return True 38 | else: 39 | return False 40 | except URLError as e: 41 | return False 42 | 43 | 44 | def upload(filename, config): 45 | """Upload .su3 file with transports""" 46 | if "transports" in config and "enabled" in config["transports"]: 47 | for t in config["transports"]["enabled"].split(): 48 | if t in config: 49 | tconf = config[t] 50 | else: 51 | tconf = None 52 | 53 | try: 54 | importlib.import_module("pyseeder.transports.{}".format(t)) \ 55 | .run(filename, tconf) 56 | except ImportError: 57 | raise PyseederException( 58 | "{} transport can't be loaded".format(t)) 59 | except TransportException as e: 60 | log.error("Transport error: {}".format(e)) 61 | 62 | -------------------------------------------------------------------------------- /pyseeder/transports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PurpleI2P/pyseeder/4dbdf65f46496425f6f7293e45f4a8c0aeea3762/pyseeder/transports/__init__.py -------------------------------------------------------------------------------- /pyseeder/transports/dropbox.py: -------------------------------------------------------------------------------- 1 | 2 | def run(filename, config): 3 | print("dummy dropbox plugin") 4 | -------------------------------------------------------------------------------- /pyseeder/transports/git.py: -------------------------------------------------------------------------------- 1 | """Git transport plugin""" 2 | import subprocess 3 | import os 4 | from shutil import copyfile 5 | from pyseeder.utils import TransportException 6 | 7 | TRANSPORT_NAME = "git" 8 | 9 | # Push to github repo witout prompting password. 10 | # Set up SSH keys or change origin URL like that: 11 | # git remote set-url origin https://$USERNAME:$PASSWORD@github.com/$USERNAME/$REPO.git 12 | 13 | def run(filename, config): 14 | if "folder" not in config: 15 | raise TransportException("git: No folder specified in config") 16 | else: 17 | REPO_FOLDER = config["folder"] 18 | 19 | REPO_FILE = os.path.split(filename)[1] 20 | 21 | if not os.access(REPO_FOLDER, os.W_OK): 22 | raise TransportException("git: {} access forbidden" \ 23 | .format(REPO_FOLDER)) 24 | 25 | if not os.path.isfile(filename): 26 | raise TransportException("git: input file not found") 27 | 28 | copyfile(filename, os.path.join(REPO_FOLDER, REPO_FILE)) 29 | 30 | commands = [ 31 | "git add {}".format(REPO_FILE), 32 | "git commit -m 'update'", 33 | "git push origin master" 34 | ] 35 | 36 | cwd = os.getcwd() 37 | os.chdir(REPO_FOLDER) 38 | for c in commands: subprocess.call(c, shell=True) 39 | os.chdir(cwd) 40 | -------------------------------------------------------------------------------- /pyseeder/transports/github.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # (re)uploads reseed file to github repository releases as an asset 3 | import requests 4 | 5 | from urllib.parse import urljoin 6 | from mimetypes import guess_type 7 | import sys 8 | import os 9 | 10 | from pyseeder.utils import TransportException 11 | 12 | TRANSPORT_NAME = "github" 13 | 14 | def run(filename, config): 15 | API_URL = "https://api.github.com/" 16 | asset_name = os.path.split(filename)[-1] 17 | content_type = guess_type(asset_name)[0] or "application/zip" 18 | creds = (config["username"], config["token"]) 19 | release_info_url = urljoin(API_URL, "/repos/{}/releases/tags/{}".format( 20 | config["repo"], config["release_tag"])) 21 | 22 | # get release info 23 | try: 24 | release = requests.get(release_info_url, auth=creds) 25 | except: 26 | raise TransportException("Failed to connect to GitHub API") 27 | 28 | if release.status_code is not 200: 29 | raise TransportException("Check your GitHub API auth settings") 30 | 31 | # fetch release assets 32 | try: 33 | assets = requests.get(release.json()["assets_url"], auth=creds) 34 | except: 35 | raise TransportException("Unable get release assets") 36 | 37 | # delete old asset 38 | for x in assets.json(): 39 | if x["name"] == asset_name: 40 | r = requests.delete(x["url"], auth=creds) 41 | if r.status_code is not 204: 42 | raise TransportException("Failed to delete asset from GitHub") 43 | 44 | # upload new asset 45 | upload_url = release.json()["upload_url"].split("{")[0] # wat 46 | headers = {'Content-Type': content_type} 47 | params = {'name': asset_name} 48 | 49 | data = open(filename, 'rb').read() 50 | r = requests.post(upload_url, headers=headers, params=params, auth=creds, 51 | data=data) 52 | 53 | if r.status_code is not 201: 54 | raise TransportException("Failed to upload asset to GitHub API : code %d" % (r.status_code)) 55 | 56 | -------------------------------------------------------------------------------- /pyseeder/utils.py: -------------------------------------------------------------------------------- 1 | """Various code""" 2 | import os 3 | 4 | class PyseederException(Exception): 5 | pass 6 | 7 | class TransportException(PyseederException): 8 | pass 9 | 10 | def check_readable(f): 11 | """Checks if path exists and readable""" 12 | if not os.path.exists(f) or not os.access(f, os.R_OK): 13 | raise PyseederException("Error accessing path: {}".format(f)) 14 | 15 | def check_writable(f): 16 | """Checks if path is writable""" 17 | if not os.access(os.path.dirname(f) or ".", os.W_OK): 18 | raise PyseederException("Path is not writable: {}".format(f)) 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography>=1.4 2 | rsa 3 | requests 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | with open("README.md") as readme: 6 | long_description = readme.read() 7 | 8 | with open("requirements.txt") as f: 9 | install_requires = f.read().split() 10 | 11 | setup( 12 | name='pyseeder', 13 | version='0.2', 14 | description='Python reseed utilities for I2P', 15 | long_description=long_description, 16 | author='Darnet Villain', 17 | author_email='supervillain@riseup.net', 18 | maintainer='R4SAS', 19 | maintainer_email='r4sas@i2pmail.org', 20 | url='https://github.com/PurpleI2P/pyseeder/', 21 | keywords='i2p reseed', 22 | license='MIT', 23 | classifiers=[ 24 | 'Development Status :: 4 - Beta', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3', 27 | 'Topic :: Utilities', 28 | ], 29 | packages=['pyseeder', 'pyseeder.transports'], 30 | install_requires=install_requires, 31 | entry_points={ 32 | 'console_scripts': [ 33 | 'pyseeder=pyseeder.cli:main', 34 | ], 35 | }, 36 | project_urls={ 37 | 'Bug Reports': 'https://github.com/PurpleI2P/pyseeder/issues', 38 | 'Source': 'https://github.com/PurpleI2P/pyseeder/', 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /transports.ini.example: -------------------------------------------------------------------------------- 1 | [transports] 2 | ; enabled transports separated by space 3 | enabled=github 4 | 5 | [git] 6 | ; Folder with git repository to use 7 | folder=/home/user/reseed-data-repo 8 | 9 | [dropbox] 10 | ; todo 11 | 12 | [github] 13 | ; GitHub username 14 | username=username 15 | ; GitHub API token 16 | ; Generate token for this script here --> https://github.com/settings/tokens 17 | ; Check scope: public_repo (shall be enough) 18 | token=token 19 | ; Repository 20 | repo=username/repo-name 21 | ; Repository tag to which asset is being uploaded 22 | release_tag=v1.0 23 | -------------------------------------------------------------------------------- /yggdrasil/README.md: -------------------------------------------------------------------------------- 1 | # Yggdrasil I2P Reseed creator 2 | ## bash script for creating reseed with Yggdrasil routers 3 | 4 | 5 | Also add `y2r.sh` to cron for automatically updating. 6 | Example:`/etc/cron.hourly/i2preseed` 7 | 8 | #!/bin/bash 9 | /path/to/y2r.sh 10 | -------------------------------------------------------------------------------- /yggdrasil/err.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Access denied 6 | 17 | 18 |
19 |

The reseed is available for download from an I2P router only!

20 | 21 |

22 | 23 | -------------------------------------------------------------------------------- /yggdrasil/reseed_nginx.conf: -------------------------------------------------------------------------------- 1 | limit_req_zone $binary_remote_addr zone=reseed:512k rate=10r/m; 2 | 3 | server { 4 | root /srv/pyseeder/output; 5 | listen [324:9de3:fea4:f6ac::ace]:7070; 6 | index seed.html; 7 | location /i2pseeds.su3 { 8 | limit_req zone=reseed burst=5; 9 | error_page 403 /err.html; 10 | if ($http_user_agent !~* "Wget/1.11.4" ) { return 403; } 11 | } 12 | location /acetone_at_mail.i2p.crt { 13 | limit_req zone=reseed; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /yggdrasil/seed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | I2P Reseed in Yggdrasil. 6 | 13 | 14 |
15 |

Number of Yggdrasil routers in the I2P network.

16 |

Data from the current reseed. Updated hourly.

17 |

99

18 |
19 | 20 | -------------------------------------------------------------------------------- /yggdrasil/wild-putin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PurpleI2P/pyseeder/4dbdf65f46496425f6f7293e45f4a8c0aeea3762/yggdrasil/wild-putin.png -------------------------------------------------------------------------------- /yggdrasil/y2r.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # The script finds routers with a Yggdrasil IPv6 4 | # and mixes 30% of regular routers with them. 5 | # 6 | # If Yggdrasil < 25 then regular routers 25; 7 | # 8 | # Путь до папки netDb, которая будет скопирована 9 | netdb=/var/lib/i2pd/netDb 10 | # Путь до выходной папки 11 | outdb=/srv/pyseeder/transitoutput 12 | # 13 | ### 14 | temp=/tmp/yggreseed 15 | success=/tmp/yggreseed.success 16 | padding=/tmp/yggreseed.padding 17 | minimum=25 # Минимальное количество роутеров с Ygg 18 | timestart=$(date '+%Y-%m-%d %H:%M:%S') 19 | ### 20 | 21 | # Проверка рабочих директорий: 22 | echo -n $netdb... 23 | cd $netdb &> /dev/null 24 | if [[ $? != 0 ]]; then 25 | echo "X" 26 | exit 1 27 | else 28 | echo "OK" 29 | fi 30 | ls 31 | 32 | echo -n $outdb... 33 | cd $outdb &> /dev/null 34 | if [[ $? != 0 ]]; then 35 | echo "X" 36 | exit 2 37 | else 38 | echo "OK" 39 | fi 40 | ls 41 | 42 | # Очистка выходной директории 43 | echo "Clearing the output directory..." 44 | rm -r $outdb/* &> /dev/null 45 | 46 | maketemp () { # Создание временной папки первичной сортировки 47 | echo "Creating temp directory..." 48 | rm -r $temp &> /dev/null 49 | mkdir $temp &> /dev/null 50 | if [[ $? != 0 ]]; then 51 | echo "Error. Exiting." 52 | exit 3 53 | fi 54 | } 55 | maketemp 56 | 57 | copy () { # Копирование базы роутера в первичную временную папку 58 | echo "Copying the router base to the temp directory..." 59 | cp -r $netdb/* $temp/ &> /dev/null 60 | if [[ $? != 0 ]]; then 61 | echo "Error. Exiting." 62 | exit 4 63 | fi 64 | } 65 | copy 66 | 67 | # Main section 68 | 69 | echo "Finding the Yggdrasil routers:" 70 | cd $temp 71 | yggaddr=0 72 | count=1 73 | 74 | for((;;)); do # Поиск Yggdrasil-роутеров и их копирование в выходную директорию 75 | dir=$(ls -lh | head -n 2 | tail -n 1 | grep -o r.$) 76 | if [[ $? != 0 ]]; then 77 | break 78 | fi 79 | 80 | cd $dir 81 | for((;;)); do 82 | dat=$(ls -lh | head -n 2 | tail -n 1 | grep -E -w -o routerInfo.*.dat$) 83 | if [[ $? != 0 ]]; then # Если файлов в папке не осталось, выходим и удаляем ее 84 | cd $temp 85 | rmdir $dir 86 | break 87 | fi 88 | 89 | echo -n "[$count] " 90 | echo -n "$dat [" 91 | cat $dat | grep '=.[23]..:' &> /dev/null # Поиск host=200: или host=300: 92 | 93 | if [[ $? == 0 ]]; then # Успех, забираем 94 | echo "+]" 95 | let yggaddr++ 96 | mkdir $outdb/$dir &> /dev/null 97 | mv ./$dat $outdb/$dir 98 | echo $dat >> $success 99 | else # Не успех, удаляем 100 | echo ".]" 101 | rm ./$dat 102 | fi 103 | let count++ 104 | done 105 | done 106 | 107 | echo -e "\n================================================= YGGDRASIL *" 108 | cat $success 109 | rm $success 110 | echo -e "===========================================================" 111 | 112 | echo -e "Reseed building..." 113 | echo -n "Need " 114 | if [[ $yggvolume < $minimum ]]; then # Если ygg-роутеров меньше minimum, докладываем 25 обычных роутеров 115 | echo -n "25 " 116 | paddingcount=25 117 | else 118 | paddingcount=$(($yggaddr / 10 * 3)) 119 | echo -n "$paddingcount " 120 | fi 121 | echo "regular routers" 122 | 123 | cd $temp 124 | maketemp 125 | copy 126 | echo -n "Padding status: " 127 | realpadding=0 128 | for((i=0; $i < $paddingcount; i++)); do 129 | 130 | rand=$(( $RANDOM % 10 )) 131 | for((j=0;$j!=$rand;j++)); do 132 | cd $temp 133 | dir=$(ls -lh | head -n 2 | tail -n 1 | grep -o r.$) 134 | if [[ $? != 0 ]]; then 135 | echo "Dir error! PADDING-RAND-FOR" 136 | exit 5 137 | fi 138 | cd $dir 139 | dat=$(ls -lh | head -n 2 | tail -n 1 | grep -E -w -o routerInfo.*.dat$) 140 | if [[ $? != 0 ]]; then # Если файлов в папке не осталось, выходим и удаляем ее 141 | cd $temp 142 | rmdir $dir 143 | else 144 | rm ./$dat 145 | fi 146 | cd $temp 147 | done 148 | 149 | padselect () { # Дополнение ресида случайными роутерами 150 | cd $temp 151 | dir=$(ls -lh | head -n 2 | tail -n 1 | grep -o r.$) 152 | if [[ $? != 0 ]]; then 153 | echo "." 154 | padselect 155 | fi 156 | cd $dir 157 | dat=$(ls -lh | head -n 2 | tail -n 1 | grep -E -w -o routerInfo.*.dat$) 158 | if [[ $? != 0 ]]; then # Если файлов в папке не осталось, выходим и удаляем ее 159 | cd $temp 160 | rmdir $dir 161 | padselect 162 | fi 163 | } 164 | padselect 165 | 166 | mkdir $outdb/$dir &> /dev/null 167 | mv ./$dat $outdb/$dir 168 | echo $dat >> $padding 169 | let realpadding++ 170 | echo -n "*" 171 | done 172 | 173 | echo -e "\n=================================================== PADDING *" 174 | cat $padding 175 | rm $padding 176 | echo -e "===========================================================" 177 | echo -e "\nstarted: $timestart\nfinished: $(date '+%Y-%m-%d %H:%M:%S')" 178 | echo -e "total routers: $count" 179 | echo -e "reseed build: $(($yggaddr+$realpadding)) ($yggaddr/$realpadding)\n" 180 | echo -e "Yggdrasil I2P reseed creator | acetone, 2021\n" 181 | 182 | # Update web page 183 | 184 | sed -i "s/>[0-9]\{1,1000\}$yggaddr