diff --git a/.gitignore b/.gitignore index eaf482c..8540dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ ENV/ # more key/ +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9eef6f1..e2e12f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ +repos: - repo: git://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + rev: v2.2.1 hooks: - id: check-added-large-files - id: check-docstring-first @@ -10,12 +11,14 @@ args: - --exclude=__init__.py language_version: python3 - - id: autopep8-wrapper - language_version: python3 - id: requirements-txt-fixer - id: trailing-whitespace +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: 'v1.4.4' + hooks: + - id: autopep8 - repo: git://github.com/asottile/reorder_python_imports - sha: v0.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports language_version: python3 diff --git a/.travis.yml b/.travis.yml index bd531f2..7a4848f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ sudo: false +dist: xenial language: python python: - - "3.4" - - "3.5" - - "3.6" +- '3.6' +- '3.7' install: pip install tox-travis pre-commit script: - - pre-commit run --all-files - - tox +- pre-commit run --all-files +- tox diff --git a/Dockerfile b/Dockerfile index 0e6a4fb..10e1506 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,43 @@ -FROM alpine +FROM alpine:latest +ARG tor_version ENV HOME /var/lib/tor -RUN apk add --no-cache git libevent-dev openssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils && \ - mkdir -p /usr/local/src/ && \ - git clone https://git.torproject.org/tor.git /usr/local/src/tor && \ - cd /usr/local/src/tor && \ - git checkout $(git branch -a | grep 'release' | sort -V | tail -1) && \ - ./autogen.sh && \ - ./configure \ - --disable-asciidoc \ - --sysconfdir=/etc \ - --disable-unittests && \ - make && make install && \ - cd .. && \ - rm -rf tor && \ - apk add --no-cache python3 python3-dev && \ - python3 -m ensurepip && \ - rm -r /usr/lib/python*/ensurepip && \ - pip3 install --upgrade pip setuptools pycrypto && \ - apk del git libevent-dev openssl-dev make automake python3-dev gcc autoconf musl-dev coreutils && \ - apk add --no-cache libevent openssl +RUN apk add --no-cache git libevent-dev openssl-dev gcc make automake ca-certificates autoconf musl-dev coreutils zlib-dev && \ + mkdir -p /usr/local/src/ && \ + git clone https://git.torproject.org/tor.git /usr/local/src/tor && \ + cd /usr/local/src/tor && \ + git checkout tor-$tor_version && \ + ./autogen.sh && \ + ./configure \ + --disable-asciidoc \ + --sysconfdir=/etc \ + --disable-unittests && \ + make && make install && \ + cd .. && \ + rm -rf tor && \ + apk add --no-cache python3 python3-dev && \ + python3 -m ensurepip && \ + rm -r /usr/lib/python*/ensurepip && \ + pip3 install --upgrade pip setuptools && \ + apk del git libevent-dev openssl-dev make automake python3-dev autoconf musl-dev coreutils && \ + apk add --no-cache libevent openssl RUN mkdir -p /etc/tor/ -ADD assets/entrypoint-config.yml / -ADD assets/onions /usr/local/src/onions -ADD assets/torrc /var/local/tor/torrc.tpl +COPY assets/onions /usr/local/src/onions +COPY assets/torrc /var/local/tor/torrc.tpl -RUN cd /usr/local/src/onions && python3 setup.py install +RUN cd /usr/local/src/onions && apk add --no-cache openssl-dev libffi-dev gcc python3-dev libc-dev && \ + python3 setup.py install && \ + apk del libffi-dev gcc python3-dev libc-dev openssl-dev RUN mkdir -p ${HOME}/.tor && \ - addgroup -S -g 107 tor && \ - adduser -S -G tor -u 104 -H -h ${HOME} tor + addgroup -S -g 107 tor && \ + adduser -S -G tor -u 104 -H -h ${HOME} tor + +COPY assets/entrypoint-config.yml / VOLUME ["/var/lib/tor/hidden_service/"] diff --git a/Makefile b/Makefile index afd18f5..5d272cd 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,28 @@ +.EXPORT_ALL_VARIABLES: + +TOR_VERSION = $(shell bash last_tor_version.sh) + test: tox +tag: + git tag v$(TOR_VERSION) + check: pre-commit run --all-files build: - docker-compose build + - echo build with tor version $(TOR_VERSION) + docker-compose -f docker-compose.build.yml build -run: build - docker-compose up +rebuild: + docker-compose -f docker-compose.build.yml build --no-cache -build-v2: - docker-compose -f docker-compose.v2.yml build +run: build + docker-compose -f docker-compose-v1.yml up -run-v2: build-v2 +run-v2: build docker-compose -f docker-compose.v2.yml up -build-v3: - docker-compose -f docker-compose.v3.yml build - -run-v3: build-v3 +run-v3: build docker-compose -f docker-compose.v3.yml up diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..cdf4aa5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +tox = "*" + +[dev-packages] +tox = "*" +pre-commit = "*" +ptpython = "*" +cryptography = "*" +pylint = "*" +autopep8 = "*" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..3aeffd5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,415 @@ +{ + "_meta": { + "hash": { + "sha256": "98040ea608cfb470b37bd65b2b0092dc483e9d7781b613c1f526285eb992d3a0" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "filelock": { + "hashes": [ + "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", + "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6" + ], + "version": "==3.0.10" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:69620e19de33a6b7ee8aeda5478791b3618ff58f0b869dbd0319fb71aa903deb", + "sha256:e5cdb1653aa27b3e46b5c390de6b6d51d31afcfdbd9d1222d82d76b82ad03d9b" + ], + "index": "pypi", + "version": "==3.8.6" + }, + "virtualenv": { + "hashes": [ + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + ], + "version": "==16.4.3" + } + }, + "develop": { + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, + "aspy.yaml": { + "hashes": [ + "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", + "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" + ], + "version": "==1.2.0" + }, + "astroid": { + "hashes": [ + "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", + "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + ], + "version": "==2.2.5" + }, + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "cffi": { + "hashes": [ + "sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f", + "sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11", + "sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d", + "sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891", + "sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf", + "sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c", + "sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed", + "sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b", + "sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a", + "sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585", + "sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea", + "sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f", + "sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33", + "sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145", + "sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a", + "sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3", + "sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f", + "sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd", + "sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804", + "sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d", + "sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92", + "sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f", + "sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84", + "sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb", + "sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7", + "sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7", + "sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35", + "sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889" + ], + "version": "==1.12.2" + }, + "cfgv": { + "hashes": [ + "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef", + "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172" + ], + "version": "==1.6.0" + }, + "cryptography": { + "hashes": [ + "sha256:066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1", + "sha256:210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705", + "sha256:26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6", + "sha256:2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1", + "sha256:2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8", + "sha256:409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151", + "sha256:45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d", + "sha256:48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659", + "sha256:6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537", + "sha256:6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e", + "sha256:8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb", + "sha256:9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c", + "sha256:9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9", + "sha256:9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5", + "sha256:acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad", + "sha256:c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a", + "sha256:d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460", + "sha256:d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd", + "sha256:e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6" + ], + "index": "pypi", + "version": "==2.6.1" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "filelock": { + "hashes": [ + "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", + "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6" + ], + "version": "==3.0.10" + }, + "identify": { + "hashes": [ + "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd", + "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171" + ], + "version": "==1.4.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de", + "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca" + ], + "version": "==0.9" + }, + "isort": { + "hashes": [ + "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", + "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" + ], + "version": "==4.3.17" + }, + "jedi": { + "hashes": [ + "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b", + "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c" + ], + "version": "==0.13.3" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "parso": { + "hashes": [ + "sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33", + "sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376" + ], + "version": "==0.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "pre-commit": { + "hashes": [ + "sha256:75a9110eae00d009c913616c0fc8a6a02e7716c4a29a14cac9b313d2c7338ab0", + "sha256:f882c65316eb5b705fe4613e92a7c91055c1800102e4d291cfd18912ec9cf90e" + ], + "index": "pypi", + "version": "==1.15.1" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", + "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", + "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" + ], + "version": "==2.0.9" + }, + "ptpython": { + "hashes": [ + "sha256:51a74abe931f692360a32d650c2ba1ca329c08f3ed9b1de8abcd1164e0b0a6a7", + "sha256:938ee050e37d61c138dbbeb21383dfef8b9ed4ffb453a5f34041f42025bf5042", + "sha256:ebe9d68ea7532ec8ab306d4bdc7ec393701cd9bbd6eff0aa3067c821f99264d4" + ], + "index": "pypi", + "version": "==2.0.4" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "version": "==5.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:69620e19de33a6b7ee8aeda5478791b3618ff58f0b869dbd0319fb71aa903deb", + "sha256:e5cdb1653aa27b3e46b5c390de6b6d51d31afcfdbd9d1222d82d76b82ad03d9b" + ], + "index": "pypi", + "version": "==3.8.6" + }, + "typed-ast": { + "hashes": [ + "sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23", + "sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15", + "sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3", + "sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d", + "sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6", + "sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60", + "sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773", + "sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424", + "sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287", + "sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99", + "sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23", + "sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8", + "sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699", + "sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1", + "sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463", + "sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6", + "sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0", + "sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0", + "sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6" + ], + "markers": "implementation_name == 'cpython'", + "version": "==1.3.1" + }, + "virtualenv": { + "hashes": [ + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + ], + "version": "==16.4.3" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + }, + "zipp": { + "hashes": [ + "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", + "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" + ], + "version": "==0.3.3" + } + } +} diff --git a/README.md b/README.md index 927b01b..9ed6a0d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,161 @@ [![Build Status](https://travis-ci.org/cmehay/docker-tor-hidden-service.svg?branch=master)](https://travis-ci.org/cmehay/docker-tor-hidden-service) -Create a tor hidden service with a link +## Setup + +### Setup hosts + +From 2019, new conf to handle tor v3 address has been added. Here an example with `docker-compose` v2+: + +```yaml +version: "2" + +services: + tor: + image: goldy/tor-hidden-service:0.3.5.8 + links: + - hello + - world + - again + environment: + # Set mapping ports + HELLO_TOR_SERVICE_HOSTS: 80:hello:80,800:hello:80,8888:hello:80 + # Set private key + HELLO_TOR_SERVIVE_KEY: | + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C + NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH + dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB + AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK + 8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj + GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL + 1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C + k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr + +qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY + t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9 + AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX + cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y + FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ + -----END RSA PRIVATE KEY----- + + # hello and again will share the same onion v3 address + FOO_TOR_SERVICE_HOSTS: 88:again:80,8000:world:80 + FOO_TOR_SERVICE_VERSION: '3' + # tor v3 address private key base 64 encoded + FOO_TOR_SERVICE_KEY: | + PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++ + j96H1X/gq14NwLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM + + hello: + image: tutum/hello-world + hostname: hello + + world: + image: tutum/hello-world + hostname: world + + again: + image: tutum/hello-world + hostname: again +``` + +This configuration will output: + +``` +foo: xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:88, xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:8000 +hello: 5azvyr7dvvr4cldn.onion:80, 5azvyr7dvvr4cldn.onion:800, 5azvyr7dvvr4cldn.onion:8888 +``` + +#### Environment variables + +##### `{SERVICE}_TOR_SERVICE_HOSTS` + +The config patern for this variable is: `{exposed_port}:{hostname}:{port}}` + +For example `80:hello:8080` will expose a onion service on port 80 to the port 8080 of hello hostname. + +You can concatenate services using comas. + +##### `{SERVICE}_TOR_SERVICE_VERSION` + +Can be `2` or `3`. Set the tor address type. + +`2` gives short addresses `5azvyr7dvvr4cldn.onion` and `3` long addresses `xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion` + + +##### `{SERVICE}_TOR_SERVICE_KEY` + +You can set the private key for the current service. + +Tor v2 addresses uses RSA PEM keys like: +``` +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C +NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH +dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB +AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK +8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj +GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL +1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C +k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr ++qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY +t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9 +AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX +cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y +FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ +-----END RSA PRIVATE KEY----- +``` + +Tor v3 addresses uses ed25519 binary keys. It should be base64 encoded: +``` +PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++j96H1X/gq14NwLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM +``` +#### Secrets + +Secret key can be set through docker `secrets`, see `docker-compose.v3.yml` for example. + + +### Tools + +A command line tool `onions` is available in container to get `.onion` url when container is running. + +```sh +# Get services +$ docker exec -ti torhiddenproxy_tor_1 onions +hello: vegm3d7q64gutl75.onion:80 +world: b2sflntvdne63amj.onion:80 + +# Get json +$ docker exec -ti torhiddenproxy_tor_1 onions --json +{"hello": ["b2sflntvdne63amj.onion:80"], "world": ["vegm3d7q64gutl75.onion:80"]} +``` + +### Auto reload + +Changing `/etc/tor/torrc` file triggers a `SIGHUP` signal to `tor` to reload configuration. + +To disable this behavior, add `ENTRYPOINT_DISABLE_RELOAD` in environment. + +### Versions + +Container version will follow tor release versions. + +### pyentrypoint + +This container uses [`pyentrypoint`](https://github.com/cmehay/pyentrypoint) to generate its setup. + +If you need to use the legacy version, please checkout the `legacy` branch or pull `goldy/tor-hidden-service:legacy`. + +### pytor + +This containner uses [`pytor`](https://github.com/cmehay/pytor) to mannages tor cryptography, generate keys and compute onion urls. + + +# Legacy deprecated doc + +ALL THE DOC BELLOW IS LEGACY, IT'S STILL WORKING BUT IT'S NOT RECOMMENDED ANYMORE AND COULD BE DROPPED IN FUTURE RELEASES. + +### Create a tor hidden service with a link ```sh # run a container with a network application @@ -22,8 +176,6 @@ $ docker run -ti --link something --volume /path/to/keys:/var/lib/tor/hidden_ser Look at the `docker-compose.yml` file to see how to use it. -## Setup - ### Set private key Private key is settable by environment or by copying file in `hostname/private_key` in docket volume (`hostname` is the link name). @@ -127,35 +279,3 @@ Links setting are required when using docker-compose v2. See `docker-compose.v2. ### Copose v3 support and secrets Links setting are required when using docker-compose v3. See `docker-compose.v3.yml` for example. - -#### Secrets - -Secret key can be set through docker `secrets`, see `docker-compose.v3.yml` for example. - -### Tools - -A command line tool `onions` is available in container to get `.onion` url when container is running. - -```sh -# Get services -$ docker exec -ti torhiddenproxy_tor_1 onions -hello: vegm3d7q64gutl75.onion:80 -world: b2sflntvdne63amj.onion:80 - -# Get json -$ docker exec -ti torhiddenproxy_tor_1 onions --json -{"hello": ["b2sflntvdne63amj.onion:80"], "world": ["vegm3d7q64gutl75.onion:80"]} -``` - -### Auto reload - -Changing `/etc/tor/torrc` file trigger a `SIGHUP` signal to `tor` to reload configuration. - -To disable this behavior, add `ENTRYPOINT_DISABLE_RELOAD` in environment. - - -### pyentrypoint - -This container is using [`pyentrypoint`](https://github.com/cmehay/pyentrypoint) to generate its setup. - -If you need to use the legacy version, please checkout the `legacy` branch or pull `goldy/tor-hidden-service:legacy`. diff --git a/assets/entrypoint-config.yml b/assets/entrypoint-config.yml index 0266ec9..04798ad 100644 --- a/assets/entrypoint-config.yml +++ b/assets/entrypoint-config.yml @@ -7,11 +7,13 @@ secret_env: - '*_KEY' - '*_PORTS' - '*_SERVICE_NAME' + - '*_TOR_SERVICE_*' pre_conf_commands: - onions --setup-hosts post_conf_commands: + - chmod -R 700 $HOME - chown -R tor:tor $HOME reload: diff --git a/assets/onions/onions/Onions.py b/assets/onions/onions/Onions.py index ed51ea7..5ad2fa7 100644 --- a/assets/onions/onions/Onions.py +++ b/assets/onions/onions/Onions.py @@ -3,6 +3,7 @@ import argparse import logging import os import sys +from base64 import b64decode from json import dumps from re import match @@ -46,6 +47,10 @@ class Setup(object): assert len(key) > 800 self.setup[host]['key'] = key + def _load_keys_in_services(self): + for service in self.services: + service.load_key() + def _get_service(self, host, service): self._add_host(host) self.setup[host]['service'] = service @@ -66,30 +71,39 @@ class Setup(object): if service: return service - def add_empty_group(self, name): + def add_empty_group(self, name, version=None): if self.find_group_by_name(name): raise Exception('Group {name} already exists'.format(name=name)) - group = ServicesGroup(name=name) + group = ServicesGroup(name=name, version=version) self.services.append(group) return group - def add_new_service(self, host, name=None, ports=None, key=None): + def add_new_service(self, + host, + name=None, + ports=None, + key=None): group = self.find_group_by_name(name) - service = self.find_service_by_host(host) + if group: + service = group.get_service_by_host(host) + else: + service = self.find_service_by_host(host) if not service: service = Service(host=host) if not group: group = ServicesGroup( service=service, name=name, - hidden_service_dir=self.hidden_service_dir + hidden_service_dir=self.hidden_service_dir, ) else: group.add_service(service) if group not in self.services: self.services.append(group) + elif group and service not in group.services: + group.add_service(service) else: - group = self.find_group_by_service(service) + self.find_group_by_service(service) if key: group.add_key(key) if ports: @@ -108,22 +122,69 @@ class Setup(object): self.add_new_service(host=host, ports=ports) def _set_key(self, host, key): - self.add_new_service(host=host, key=key) + self.add_new_service(host=host, key=key.encode()) - def _setup_from_env(self): - match_map = ( - (r'([A-Z0-9]*)_PORTS', self._set_ports), - (r'([A-Z0-9]*)_KEY', self._set_key), - ) - for key, val in os.environ.items(): - for reg, call in match_map: + def _setup_from_env(self, match_map): + for reg, call in match_map: + for key, val in os.environ.items(): m = match(reg, key) if m: call(m.groups()[0].lower(), val) + def _setup_keys_and_ports_from_env(self): + self._setup_from_env( + ( + (r'([A-Z0-9]+)_PORTS', self._set_ports), + (r'([A-Z0-9]+)_KEY', self._set_key), + ) + ) + + def get_or_create_empty_group(self, name, version=None): + group = self.find_group_by_name(name) + if group: + if version: + group.set_version(version) + return group + return self.add_empty_group(name, version) + + def _set_group_version(self, name, version): + 'Setup groups with version' + group = self.get_or_create_empty_group(name, version=version) + group.set_version(version) + + def _set_group_key(self, name, key): + 'Set key for service group' + group = self.get_or_create_empty_group(name) + if group.version == 3: + group.add_key(b64decode(key)) + else: + group.add_key(key) + + def _set_group_hosts(self, name, hosts): + 'Set services for service groups' + self.get_or_create_empty_group(name) + for host_map in hosts.split(','): + host_map = host_map.strip() + port_from, host, port_dest = host_map.split(':', 2) + if host == 'unix' and port_dest.startswith('/'): + self.add_new_service(host=name, name=name, ports=host_map) + else: + ports = '{frm}:{dst}'.format(frm=port_from, dst=port_dest) + self.add_new_service(host=host, name=name, ports=ports) + + def _setup_services_from_env(self): + self._setup_from_env( + ( + (r'([A-Z0-9]+)_TOR_SERVICE_VERSION', self._set_group_version), + (r'([A-Z0-9]+)_TOR_SERVICE_KEY', self._set_group_key), + (r'([A-Z0-9]+)_TOR_SERVICE_HOSTS', self._set_group_hosts), + ) + ) + def _get_setup_from_env(self): self._set_service_names() - self._setup_from_env() + self._setup_keys_and_ports_from_env() + self._setup_services_from_env() def _get_setup_from_links(self): containers = DockerLinks().to_containers() @@ -162,6 +223,7 @@ class Setup(object): self.setup = {} self._get_setup_from_env() self._get_setup_from_links() + self._load_keys_in_services() self.check_services() self.apply_conf() @@ -201,38 +263,65 @@ class Onions(Setup): def torrc_parser(self): + self.torrc_dict = {} + def parse_dir(line): _, path = line.split() group_name = os.path.basename(path) - group = (self.find_group_by_name(group_name) - or self.add_empty_group(group_name)) - return group + self.torrc_dict[group_name] = { + 'services': [], + } + return group_name - def parse_port(line, service_group): + def parse_port(line, name): _, port_from, dest = line.split() service_host, port = dest.split(':') ports_str = '{port_from}:{dest}' - name = service_host ports_param = ports_str.format(port_from=port_from, dest=port) if port.startswith('/'): - name = service_group.name + service_host = name ports_param = ports_str.format(port_from=port_from, dest=dest) - service = (service_group.get_service_by_host(name) - or Service(name)) - service.add_ports(ports_param) - if service not in service_group.services: - service_group.add_service(service) + self.torrc_dict[name]['services'].append({ + 'host': service_host, + 'ports': ports_param, + }) + + def parse_version(line, name): + _, version = line.split() + self.torrc_dict[name]['version'] = int(version) + + def setup_services(): + for name, setup in self.torrc_dict.items(): + version = setup.get('version', 2) + group = (self.find_group_by_name(name) + or self.add_empty_group(name, version=version)) + for service_dict in setup.get('services', []): + host = service_dict['host'] + service = (group.get_service_by_host(host) + or Service(host)) + service.add_ports(service_dict['ports']) + if service not in group.services: + group.add_service(service) + self._load_keys_in_services() if not os.path.exists(self.torrc): return - with open(self.torrc, 'r') as f: - for line in f.readlines(): - if line.startswith('HiddenServiceDir'): - service_group = parse_dir(line) - if line.startswith('HiddenServicePort'): - parse_port(line, service_group) + try: + with open(self.torrc, 'r') as f: + for line in f.readlines(): + if line.startswith('HiddenServiceDir'): + name = parse_dir(line) + if line.startswith('HiddenServicePort'): + parse_port(line, name) + if line.startswith('HiddenServiceVersion'): + parse_version(line, name) + except BaseException: + raise Exception( + 'Fail to parse torrc file. Please check the file' + ) + setup_services() def __str__(self): if not self.services: @@ -257,6 +346,7 @@ def main(): help='Setup hosts') args = parser.parse_args() + logging.getLogger().setLevel(logging.WARNING) try: onions = Onions() if args.setup: @@ -264,6 +354,7 @@ def main(): else: onions.torrc_parser() except BaseException as e: + logging.exception(e) error_msg = str(e) else: error_msg = None @@ -271,7 +362,6 @@ def main(): if error_msg: print(dumps({'error': error_msg})) sys.exit(1) - logging.getLogger().setLevel(logging.ERROR) print(onions.to_json()) else: if error_msg: diff --git a/assets/onions/onions/Service.py b/assets/onions/onions/Service.py index 6b0d605..0db98aa 100644 --- a/assets/onions/onions/Service.py +++ b/assets/onions/onions/Service.py @@ -1,42 +1,67 @@ 'This class define a service link' +import base64 +import binascii import logging import os +import pathlib import re -from base64 import b32encode -from hashlib import sha1 -from Crypto.PublicKey import RSA +from pytor import OnionV2 +from pytor import OnionV3 class ServicesGroup(object): name = None - _priv_key = None - _key_in_secrets = False - - hidden_service_dir = "/var/lib/tor/hidden_service/" - - def __init__(self, name=None, service=None, hidden_service_dir=None): + version = None + imported_key = False + _default_version = 2 + _imported_key = False + _onion = None + _hidden_service_dir = "/var/lib/tor/hidden_service/" + + def __init__(self, + name=None, + service=None, + version=None, + hidden_service_dir=None): name_regex = r'^[a-zA-Z0-9-_]+$' - self.hidden_service_dir = hidden_service_dir or self.hidden_service_dir + self.onion_map = { + 2: OnionV2, + 3: OnionV3, + } + if not name and not service: raise Exception( 'Init service group with a name or service at least' ) self.services = [] self.name = name or service.host + if hidden_service_dir: + self._hidden_service_dir = hidden_service_dir if not re.match(name_regex, self.name): raise Exception( 'Group {name} has invalid name'.format(name=self.name) ) if service: self.add_service(service) + self.set_version(version or self._default_version) + self.gen_key() + + def set_version(self, version): + version = int(version) + if version not in self.onion_map: + raise Exception( + 'Url version {version} is not supported'.format(version) + ) + self.version = version + self._onion = self.onion_map[version]() - self.load_key() - if not self._priv_key: - self.gen_key() + @property + def hidden_service_dir(self): + return os.path.join(self._hidden_service_dir, self.name) def add_service(self, service): if service not in self.services: @@ -50,15 +75,22 @@ class ServicesGroup(object): return service def add_key(self, key): - if self._key_in_secrets: + if self._imported_key: logging.warning('Secret key already set, overriding') - self._priv_key = key - self._key_in_secrets = False + # Try to decode key from base64 encoding + # import the raw data if the input cannot be decoded as base64 + try: + key = base64.b64decode(key) + except binascii.Error: + pass + self._onion.set_private_key(key) + self._imported_key = True def __iter__(self): yield 'name', self.name yield 'onion', self.onion_url yield 'urls', list(self.urls) + yield 'version', self.version def __str__(self): return '{name}: {urls}'.format(name=self.name, @@ -66,16 +98,7 @@ class ServicesGroup(object): @property def onion_url(self): - "Get onion url from private key" - - # Convert private RSA to public DER - priv = RSA.importKey(self._priv_key.strip()) - der = priv.publickey().exportKey("DER") - - # hash key, keep first half of sha1, base32 encode - onion = b32encode(sha1(der[22:]).digest()[:10]) - - return '{onion}.onion'.format(onion=onion.decode().lower()) + return self._onion.onion_hostname @property def urls(self): @@ -88,30 +111,17 @@ class ServicesGroup(object): 'Write key on disk and set tor service' if not hidden_service_dir: hidden_service_dir = self.hidden_service_dir - serv_dir = os.path.join(hidden_service_dir, self.name) - os.makedirs(serv_dir, exist_ok=True) - os.chmod(serv_dir, 0o700) - with open(os.path.join(serv_dir, 'private_key'), 'w') as f: - f.write(self._priv_key) - os.fchmod(f.fileno(), 0o600) - with open(os.path.join(serv_dir, 'hostname'), 'w') as f: - f.write(self.onion_url) + if not os.path.isdir(hidden_service_dir): + pathlib.Path(hidden_service_dir).mkdir(parents=True) + self._onion.write_hidden_service(hidden_service_dir, force=True) def _load_key(self, key_file): - if os.path.exists(key_file): - with open(key_file, 'r') as f: - key = f.read().encode() - if not len(key): - return - try: - rsa = RSA.importKey(key) - self._priv_key = rsa.exportKey("PEM").decode() - except BaseException: - raise('Fail to load key for {name} services'.format( - name=self.name - )) - - def load_key(self): + with open(key_file, 'rb') as f: + self._onion.set_private_key_from_file(f) + + def load_key(self, override=False): + if self._imported_key and not override: + return self.load_key_from_secrets() self.load_key_from_conf() @@ -122,8 +132,9 @@ class ServicesGroup(object): return try: self._load_key(secret_file) - self._key_in_secrets = True - except BaseException: + self._imported_key = True + except BaseException as e: + logging.exception(e) logging.warning('Fail to load key from secret, ' 'check the key or secret name collision') @@ -131,16 +142,17 @@ class ServicesGroup(object): 'Load key from disk if exists' if not hidden_service_dir: hidden_service_dir = self.hidden_service_dir - key_file = os.path.join(hidden_service_dir, - self.name, - 'private_key') - self._load_key(key_file) + if not os.path.isdir(hidden_service_dir): + return + self._onion.load_hidden_service(hidden_service_dir) def gen_key(self): - 'Generate new 1024 bits RSA key for hidden service' - self._priv_key = RSA.generate( - bits=1024, - ).exportKey("PEM").decode() + self.imported_key = False + return self._onion.gen_new_private_key() + + @property + def _priv_key(self): + return self._onion.get_private_key() class Ports: diff --git a/assets/onions/setup.py b/assets/onions/setup.py index 2141b32..670b1db 100644 --- a/assets/onions/setup.py +++ b/assets/onions/setup.py @@ -6,7 +6,7 @@ from setuptools import setup setup( name='onions', - version='0.4.1', + version='0.5.0', packages=find_packages(), @@ -31,10 +31,9 @@ setup( "Topic :: System :: Installation/Setup", ], - install_requires=['pyentrypoint==0.5.1', - 'Jinja2>=2.8', - 'pycrypto', ], - + install_requires=['pyentrypoint==0.5.2', + 'Jinja2>=2.10', + 'pytor>=0.1.2'], entry_points={ 'console_scripts': [ 'onions = onions:main', diff --git a/assets/onions/tests/onions_test.py b/assets/onions/tests/onions_test.py index 9ac85c5..acc62b9 100644 --- a/assets/onions/tests/onions_test.py +++ b/assets/onions/tests/onions_test.py @@ -2,6 +2,7 @@ import json import os import re from base64 import b32encode +from base64 import b64decode from hashlib import sha1 import pytest @@ -9,8 +10,9 @@ from Crypto.PublicKey import RSA from onions import Onions -def get_key_and_onion(): - key = ''' +def get_key_and_onion(version=2): + key = {} + key[2] = ''' -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCsMP4gl6g1Q313miPhb1GnDr56ZxIWGsO2PwHM1infkbhlBakR 6DGQfpE31L1ZKTUxY0OexKbW088v8qCOfjD9Zk1i80JP4xzfWQcwFZ5yM/0fkhm3 @@ -27,24 +29,40 @@ La/7Syrnobngsh/vX90CQB+PSSBqiPSsK2yPz6Gsd6OLCQ9sdy2oRwFTasH8sZyl bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8= -----END RSA PRIVATE KEY----- ''' - - onion = b32encode( + onion = {} + pub = {} + onion[2] = b32encode( sha1( RSA.importKey( - key.strip() + key[2].strip() ).publickey().exportKey( "DER" )[22:] ).digest()[:10] ).decode().lower() + '.onion' - return key.strip(), onion + key[3] = ''' +PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++j96H1X/gq14N +wLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM + ''' + + pub[3] = ''' +PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAC9kzftiea/kb+TWlCEVNpfUJLVk+rFIoMG +m9/hW13isA== + ''' + + onion[3] = 'xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion' + + return key[version].strip(), onion[version] def get_torrc_template(): return r''' {% for service_group in services %} -HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}} +HiddenServiceDir {{service_group.hidden_service_dir}} +{% if service_group.version == 3 %} +HiddenServiceVersion 3 +{% endif %} {% for service in service_group.services %} {% for port in service.ports %} {% if port.is_socket %} @@ -155,7 +173,7 @@ ff02::2 ip6-allrouters 172.17.0.2 compose_service1_1 bf447f22cdba '''.strip() - fs.CreateFile('/etc/hosts', contents=etc_host) + fs.create_file('/etc/hosts', contents=etc_host) monkeypatch.setattr(os, 'environ', env) @@ -190,34 +208,57 @@ def test_key(monkeypatch): assert onion.services[0].onion_url == onion_url +def test_key_v3(monkeypatch): + key, onion_url = get_key_and_onion(version=3) + env = { + 'GROUP1_TOR_SERVICE_HOSTS': '80:service1:80,81:service2:80', + 'GROUP1_TOR_SERVICE_VERSION': '3', + 'GROUP1_TOR_SERVICE_KEY': key, + } + + monkeypatch.setattr(os, 'environ', env) + + onion = Onions() + onion._get_setup_from_env() + onion._load_keys_in_services() + + assert len(os.environ) == 3 + assert len(onion.services) == 1 + + assert onion.services[0].onion_url == onion_url + + def test_key_in_secret(fs, monkeypatch): env = { - 'SERVICE1_SERVICE_NAME': 'group1', - 'SERVICE2_SERVICE_NAME': 'group1', - 'SERVICE3_SERVICE_NAME': 'group2', - 'SERVICE1_PORTS': '80:80', - 'SERVICE2_PORTS': '81:80,82:8000', - 'SERVICE3_PORTS': '80:unix://unix.socket', + 'GROUP1_TOR_SERVICE_HOSTS': '80:service1:80', + 'GROUP2_TOR_SERVICE_HOSTS': '80:service2:80', + 'GROUP3_TOR_SERVICE_HOSTS': '80:service3:80', + 'GROUP3_TOR_SERVICE_VERSION': '3', } monkeypatch.setattr(os, 'environ', env) - key, onion_url = get_key_and_onion() + key_v2, onion_url_v2 = get_key_and_onion() + key_v3, onion_url_v3 = get_key_and_onion(version=3) - fs.CreateFile('/run/secrets/group1', contents=key) + fs.create_file('/run/secrets/group1', contents=key_v2) + fs.create_file('/run/secrets/group3', contents=b64decode(key_v3)) onion = Onions() onion._get_setup_from_env() + onion._load_keys_in_services() group1 = onion.find_group_by_name('group1') group2 = onion.find_group_by_name('group2') + group3 = onion.find_group_by_name('group3') + + assert group1.onion_url == onion_url_v2 + assert group2.onion_url not in [onion_url_v2, onion_url_v3] + assert group3.onion_url == onion_url_v3 - # assert group._priv_key == key - assert group1.onion_url == onion_url - assert group2.onion_url != onion_url +def test_configuration(fs, monkeypatch, tmpdir): -def test_configuration(fs, monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', 'SERVICE2_SERVICE_NAME': 'group1', @@ -225,44 +266,72 @@ def test_configuration(fs, monkeypatch): 'SERVICE1_PORTS': '80:80', 'SERVICE2_PORTS': '81:80,82:8000', 'SERVICE3_PORTS': '80:unix://unix.socket', + 'GROUP3_TOR_SERVICE_VERSION': '2', + 'GROUP3_TOR_SERVICE_HOSTS': '80:service4:888,81:service5:8080', + 'GROUP4_TOR_SERVICE_VERSION': '3', + 'GROUP4_TOR_SERVICE_HOSTS': '81:unix://unix2.sock', + 'GROUP3V3_TOR_SERVICE_VERSION': '3', + 'GROUP3V3_TOR_SERVICE_HOSTS': '80:service4:888,81:service5:8080', + 'SERVICE5_TOR_SERVICE_HOSTS': '80:service5:80' } + hidden_dir = '/var/lib/tor/hidden_service' + monkeypatch.setattr(os, 'environ', env) monkeypatch.setattr(os, 'fchmod', lambda x, y: None) - key, onion_url = get_key_and_onion() torrc_tpl = get_torrc_template() - fs.CreateFile('/var/local/tor/torrc.tpl', contents=torrc_tpl) - fs.CreateFile('/etc/tor/torrc') + fs.create_file('/var/local/tor/torrc.tpl', contents=torrc_tpl) + fs.create_file('/etc/tor/torrc') + fs.create_dir(hidden_dir) onion = Onions() onion._get_setup_from_env() + onion._load_keys_in_services() onion.apply_conf() + onions_urls = {} + for dir in os.listdir(hidden_dir): + with open(os.path.join(hidden_dir, dir, 'hostname'), 'r') as f: + onions_urls[dir] = f.read().strip() + with open('/etc/tor/torrc', 'r') as f: torrc = f.read() + print(torrc) assert 'HiddenServiceDir /var/lib/tor/hidden_service/group1' in torrc assert 'HiddenServicePort 80 service1:80' in torrc assert 'HiddenServicePort 81 service2:80' in torrc assert 'HiddenServicePort 82 service2:8000' in torrc assert 'HiddenServiceDir /var/lib/tor/hidden_service/group2' in torrc assert 'HiddenServicePort 80 unix://unix.socket' in torrc + assert 'HiddenServiceDir /var/lib/tor/hidden_service/group3' in torrc + assert 'HiddenServiceDir /var/lib/tor/hidden_service/group4' in torrc + assert 'HiddenServiceDir /var/lib/tor/hidden_service/group3v3' in torrc + assert 'HiddenServiceDir /var/lib/tor/hidden_service/service5' in torrc + assert torrc.count('HiddenServicePort 80 service4:888') == 2 + assert torrc.count('HiddenServicePort 81 service5:8080') == 2 + assert torrc.count('HiddenServicePort 80 service5:80') == 1 + assert torrc.count('HiddenServicePort 81 unix://unix2.sock') == 1 + assert torrc.count('HiddenServiceVersion 3') == 2 # Check parser onion2 = Onions() onion2.torrc_parser() - assert len(onion2.services) == 2 + assert len(onion2.services) == 6 assert set( group.name for group in onion2.services - ) == set(['group1', 'group2']) + # ) == set(['group1', 'group2']) + ) == set(['group1', 'group2', 'group3', 'group4', 'group3v3', 'service5']) for group in onion2.services: if group.name == 'group1': assert len(group.services) == 2 + assert group.version == 2 + assert group.onion_url == onions_urls[group.name] assert set( service.host for service in group.services ) == set(['service1', 'service2']) @@ -279,6 +348,8 @@ def test_configuration(fs, monkeypatch): ) == set([(81, 80), (82, 8000)]) if group.name == 'group2': assert len(group.services) == 1 + assert group.version == 2 + assert group.onion_url == onions_urls[group.name] assert set( service.host for service in group.services ) == set(['group2']) @@ -288,6 +359,53 @@ def test_configuration(fs, monkeypatch): (port.port_from, port.dest) for port in service.ports ) == set([(80, 'unix://unix.socket')]) + if group.name in ['group3', 'group3v3']: + assert len(group.services) == 2 + assert group.version == 2 if group.name == 'group3' else 3 + assert group.onion_url == onions_urls[group.name] + assert set( + service.host for service in group.services + ) == set(['service4', 'service5']) + for service in group.services: + if service.host == 'service4': + assert len(service.ports) == 1 + assert set( + (port.port_from, port.dest) for port in service.ports + ) == set([(80, 888)]) + if service.host == 'service5': + assert len(service.ports) == 1 + assert set( + (port.port_from, port.dest) for port in service.ports + ) == set([(81, 8080)]) + + if group.name == 'group4': + assert len(group.services) == 1 + assert group.version == 3 + assert group.onion_url == onions_urls[group.name] + assert set( + service.host for service in group.services + ) == set(['group4']) + for service in group.services: + assert service.host == 'group4' + assert len(service.ports) == 1 + assert set( + (port.port_from, port.dest) for port in service.ports + ) == set([(81, 'unix://unix2.sock')]) + + if group.name == 'service5': + assert len(group.services) == 1 + assert group.version == 2 + assert group.onion_url == onions_urls[group.name] + assert set( + service.host for service in group.services + ) == set(['service5']) + for service in group.services: + assert service.host == 'service5' + assert len(service.ports) == 1 + assert set( + (port.port_from, port.dest) for port in service.ports + ) == set([(80, 80)]) + def test_groups(monkeypatch): env = { diff --git a/assets/torrc b/assets/torrc index 76eaeea..3128362 100644 --- a/assets/torrc +++ b/assets/torrc @@ -1,5 +1,8 @@ {% for service_group in services %} -HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}} +HiddenServiceDir {{service_group.hidden_service_dir}} +{% if service_group.version == 3 %} +HiddenServiceVersion 3 +{% endif %} {% for service in service_group.services %} {% for port in service.ports %} {% if port.is_socket %} diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 0000000..16863f5 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,11 @@ +# docker version 3 builder + +version: "3.1" + +services: + tor: + image: goldy/tor-hidden-service:$TOR_VERSION + build: + context: . + args: + tor_version: $TOR_VERSION diff --git a/docker-compose.yml b/docker-compose.v1.yml similarity index 74% rename from docker-compose.yml rename to docker-compose.v1.yml index c5b3971..84a9209 100644 --- a/docker-compose.yml +++ b/docker-compose.v1.yml @@ -1,7 +1,9 @@ # docker-compose.yml example +# LEGACY CONFIGURATION +# SEE README FOR INFORMATIONS tor: - image: goldy/tor-hidden-service + image: goldy/tor-hidden-service:$TOR_VERSION links: - hello - world diff --git a/docker-compose.v2.legacy.yml b/docker-compose.v2.legacy.yml new file mode 100644 index 0000000..29726a2 --- /dev/null +++ b/docker-compose.v2.legacy.yml @@ -0,0 +1,59 @@ +# docker version 2 example + +version: "2" + +services: + tor: + image: goldy/tor-hidden-service:$TOR_VERSION + links: + - hello + - world + - again + environment: + # Set mapping ports + HELLO_PORTS: 80:80,800:80,8888:80 + # Set private key + HELLO_KEY: | + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C + NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH + dnFnHEcsllSEqD1hPAAvMUWwSMJaNmBEFtl8DUMS9tPX5fWGX4w5Xx8dZwIDAQAB + AoGBAMb20jMHxaZHWg2qTRYYJa8LdHgS0BZxkWYefnBUbZn7dOz7mM+tddpX6raK + 8OSqyQu3Tc1tB9GjPLtnVr9KfVwhUVM7YXC/wOZo+u72bv9+4OMrEK/R8xy30XWj + GePXEu95yArE4NucYphxBLWMMu2E4RodjyJpczsl0Lohcn4BAkEA+XPaEKnNA3AL + 1DXRpSpaa0ukGUY/zM7HNUFMW3UP00nxNCpWLSBmrQ56Suy7iSy91oa6HWkDD/4C + k0HslnMW5wJBANdz4ehByMJZmJu/b5y8wnFSqep2jmJ1InMvd18BfVoBTQJwGMAr + +qwSwNXXK2YYl9VJmCPCfgN0o7h1AEzvdYECQAM5UxUqDKNBvHVmqKn4zShb1ugY + t1RfS8XNbT41WhoB96MT9P8qTwlniX8UZiwUrvNp1Ffy9n4raz8Z+APNwvsCQQC9 + AuaOsReEmMFu8VTjNh2G+TQjgvqKmaQtVNjuOgpUKYv7tYehH3P7/T+62dcy7CRX + cwbLaFbQhUUUD2DCHdkBAkB6CbB+qhu67oE4nnBCXllI9EXktXgFyXv/cScNvM9Y + FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ + -----END RSA PRIVATE KEY----- + + WORLD_PORTS: 8000:80 + + AGAIN_PORTS: 88:80 + + # hello and again will share the same onion_adress + AGAIN_SERVICE_NAME: foo + HELLO_SERVICE_NAME: foo + + # Keep keys in volumes + volumes: + - tor-keys:/var/lib/tor/hidden_service/ + + hello: + image: tutum/hello-world + hostname: hello + + world: + image: tutum/hello-world + hostname: world + + again: + image: tutum/hello-world + hostname: again + +volumes: + tor-keys: + driver: local diff --git a/docker-compose.v2.socket.yml b/docker-compose.v2.socket.yml index 8f27bca..d3c4400 100644 --- a/docker-compose.v2.socket.yml +++ b/docker-compose.v2.socket.yml @@ -4,7 +4,7 @@ version: "2" services: tor: - image: goldy/tor-hidden-service + image: goldy/tor-hidden-service:$TOR_VERSION build: . links: - world diff --git a/docker-compose.v2.yml b/docker-compose.v2.yml index 709154c..ed60dc0 100644 --- a/docker-compose.v2.yml +++ b/docker-compose.v2.yml @@ -4,17 +4,16 @@ version: "2" services: tor: - image: goldy/tor-hidden-service - build: . + image: goldy/tor-hidden-service:$TOR_VERSION links: - hello - world - again environment: # Set mapping ports - HELLO_PORTS: 80:80,800:80,8888:80 + HELLO_TOR_SERVICE_HOSTS: 80:hello:80,800:hello:80,8888:hello:80 # Set private key - HELLO_KEY: | + HELLO_TOR_SERVIVE_KEY: | -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDR8TdQF9fDlGhy1SMgfhMBi9TaFeD12/FK27TZE/tYGhxXvs1C NmFJy1hjVxspF5unmUsCk0yEsvEdcAdp17Vynz6W41VdinETU9yXHlUJ6NyI32AH @@ -31,13 +30,13 @@ services: FDzzNAAfVc5Nmbmx28Nw+0w6pnpe/3m0Tudbq3nHdHfQ -----END RSA PRIVATE KEY----- - WORLD_PORTS: 8000:80 - - AGAIN_PORTS: 88:80 - # hello and again will share the same onion_adress - AGAIN_SERVICE_NAME: foo - HELLO_SERVICE_NAME: foo + FOO_TOR_SERVICE_HOSTS: 88:again:80,8000:world:80 + FOO_TOR_SERVICE_VERSION: '3' + # tor v3 address private key base 64 encoded + FOO_TOR_SERVICE_KEY: | + PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAACArobDQYyZAWXei4QZwr++ + j96H1X/gq14NwLRZ2O5DXuL0EzYKkdhZSILY85q+kfwZH8z4ceqe7u1F+0pQi/sM # Keep keys in volumes volumes: diff --git a/docker-compose.v3.yml b/docker-compose.v3.yml index 293c902..00aa3ed 100644 --- a/docker-compose.v3.yml +++ b/docker-compose.v3.yml @@ -4,23 +4,19 @@ version: "3.1" services: tor: - image: goldy/tor-hidden-service - build: . + image: goldy/tor-hidden-service:$TOR_VERSION links: - hello - world - again environment: - # Set mapping ports - HELLO_PORTS: 80:80,800:80,8888:80 + # Set version 3 on BAR group + BAR_TOR_SERVICE_HOSTS: '80:hello:80,88:world:80' + BAR_TOR_SERVICE_VERSION: '3' - WORLD_PORTS: 8000:80 + # hello and again will share the same v2 onion_adress + FOO_TOR_SERVICE_HOSTS: '88:again:80,80:hello:80,800:hello:80,8888:hello:80' - AGAIN_PORTS: 88:80 - - # hello and again will share the same onion_adress - AGAIN_SERVICE_NAME: foo - HELLO_SERVICE_NAME: foo # Keep keys in volumes volumes: @@ -31,6 +27,9 @@ services: - source: foo target: foo mode: 0400 + - source: bar + target: bar + mode: 0400 hello: image: tutum/hello-world @@ -50,4 +49,6 @@ volumes: secrets: foo: - file: ./foo_private_key + file: ./private_key_foo_v2 + bar: + file: ./private_key_bar_v3 diff --git a/last_tor_version.sh b/last_tor_version.sh new file mode 100644 index 0000000..f46c1c2 --- /dev/null +++ b/last_tor_version.sh @@ -0,0 +1,2 @@ +#!/bin/bash +git ls-remote --tags https://git.torproject.org/tor.git | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 diff --git a/private_key_bar_v3 b/private_key_bar_v3 new file mode 100644 index 0000000..b634f52 Binary files /dev/null and b/private_key_bar_v3 differ diff --git a/foo_private_key b/private_key_foo_v2 similarity index 100% rename from foo_private_key rename to private_key_foo_v2 diff --git a/tox.ini b/tox.ini index 7ddf9e0..4d78040 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, py36 +envlist = py34, py35, py36, py37 changedir=assets/onions/ setupdir=assets/onions/ skip_missing_interpreters = true