diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100755 index 00000000..66ad2049 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +ko_fi: beardogkf + diff --git a/.gitignore b/.gitignore index 637243fe..7573da9c 100755 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ core venv/* onionr/fs* +*.dll +*.exe + # log files output.log *.log diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100755 new mode 100644 index ccd047ea..3b624005 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100755 new mode 100644 index f5a31c5d..9f083d3d --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ # Contributing to Onionr -One of the great things about open source projects is that they allow for many people to contribute to the project. +One of the great things about open source projects is that they allow for anyone to contribute to the project. This file serves to provide guidelines on how to successfully contribute to Onionr. ## Code of Conduct -See our [Code of Conduct](https://github.com/beardog108/onionr/blob/master/CODE_OF_CONDUCT.md) +Contributors in project-related spaces/contexts are expected to follow the [Code of Conduct](https://github.com/beardog108/onionr/blob/master/CODE_OF_CONDUCT.md) ## Reporting Bugs @@ -23,11 +23,13 @@ Please provide the following information when reporting a bug: If a bug is a security issue, please contact us privately. -And most importantly, please be patient. Onionr is an open source project done by volunteers. +And most importantly, please be patient. Onionr is a free open source project maintained by volunteers in their free time. ## Asking Questions -If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven non-profit project). +If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven project). + +Do your best to use good english. ## Contributing Code diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md old mode 100755 new mode 100644 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100755 new mode 100644 index 7d2d0a0e..f288702d --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,3 @@ -The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). - - GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 8b0de5c2..7f314023 --- a/README.md +++ b/README.md @@ -5,28 +5,30 @@

- Anonymous P2P storage network 🕵️ + Private P2P Communication Network 🕵️

(***pre-alpha & experimental, not well tested or easy to use yet***) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) - + - [Onionr.net](https://onionr.net/) - [.onion](http://onionr.onionkvc5ibm37bmxwr56bdxcdnb6w3wm4bdghh5qo6f6za7gn7styid.onion/)
**The main repository for this software is at https://gitlab.com/beardog/Onionr/** -# Summary +# About -Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption. +Onionr is a decentralized, peer-to-peer communication network, designed to be anonymous and resistant to (meta)data analysis, spam, and corruption. -Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by particular nodes (only inferred). Even if there is enough evidence to believe a particular node created a block, nodes still operate behind Tor or I2P and as such are not trivially known to be at a particular IP address. +Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by a particular user. Even if there is enough evidence to believe that a specific user created a block, nodes still operate behind Tor or I2P and as such cannot be trivially unmasked. -Users are identified by ed25519 public keys, which can be used to sign blocks or send encrypted data. +Users are identified by ed25519/curve25519 public keys, which can be used to sign blocks or send encrypted data. Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion. +The whitepaper (subject to change prior to alpha release) is available [here](docs/whitepaper.md). + ![Tor stinks slide image](docs/tor-stinks-02.png) ## Main Features @@ -34,17 +36,33 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure * [X] 🔒 End to end encryption of user data * [X] 📢 Optional non-encrypted blocks, useful for blog posts or public file sharing -* [X] 💻 Easy API system for integration to websites +* [X] 💻 Easy HTTP API for integration to websites * [X] 🕵️ Metadata analysis resistance and anonymity * [X] 📡 Transport agnosticism (no internet required) +## Software Suite + +Onionr ships with various application plugins ready for use out of the box: + +Currently usable: + +* Mail +* Public anonymous chat +* Simple webpage hosting - Will be greatly extended +* File sharing (Work in progress) + +Not yet usable: + +* Instant messaging +* Forum/BBS + **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** # Screenshots Node statistics page screenshot -Node statistics +Home screen Friend/contact manager screenshot @@ -52,22 +70,31 @@ Friend/contact manager Encrypted, metadata-masking mail application screenshot -Encrypted, metadata-masking mail application. +Encrypted, metadata-masking mail application. One of the first distributed mail systems to have basic forward secrecy. + +# Documentation + +More docs coming soon. + +* [Block specification](docs/specs/block-spec.md) +* [HTTP API](docs/http-api.md) # Install and Run on Linux The following applies to Ubuntu Bionic. Other distros may have different package or command names. -* Have python3.6+, python3-pip, Tor (daemon, not browser) installed (python3-dev recommended) +`$ sudo apt install python3-pip python3-dev tor` + +* Have python3.6+, python3-pip, Tor (daemon, not browser) installed. python3-dev is recommended. * Clone the git repo: `$ git clone https://gitlab.com/beardog/onionr` * cd into install direction: `$ cd onionr/` * Install the Python dependencies ([virtualenv strongly recommended](https://virtualenv.pypa.io/en/stable/userguide/)): `$ pip3 install --require-hashes -r requirements.txt` -(--require-hashes is intended to prevent exploitation via compromise of Pypi/CA certificates) +(--require-hashes is intended to prevent exploitation via compromise of PyPi/CA certificates) -## Help out +# Help out -Everyone is welcome to help out. Help is wanted for the following: +Everyone is welcome to contribute. Help is wanted for the following: * Development (Get in touch first) * Creation of a shared lib for use from other languages and faster proof-of-work @@ -75,26 +102,39 @@ Everyone is welcome to help out. Help is wanted for the following: * Windows and Mac support (already partially supported, testers needed) * General bug fixes and development of new features * Testing +* Translations/localizations * UI/UX design * Running stable nodes * Security review/audit * Automatic I2P setup -Contribute money: +## Contribute money: Donating at least $5 gets you cool Onionr stickers. Get in touch if you want them. -Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) (Contact us for privacy coins like Monero) +Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) (Contact us for a unique address or for other coins) + +Monero: 4B5BA24d1P3R5aWEpkGY5TP7buJJcn2aSGBVRQCHhpiahxeB4aWsu15XwmuTjC6VF62NApZeJGTS248RMVECP8aW73Uj2ax USD (Card/Paypal): [Ko-Fi](https://www.ko-fi.com/beardogkf) Note: probably not tax deductible -## Contact +# Contact -beardog [ at ] mailbox.org +Email: beardog [ at ] mailbox.org -## Disclaimer +Onionr Mail: TRH763JURNY47QPBTTQ4LLPYCYQK6Q5YA33R6GANKZK5C5DKCIGQ + +## Disclaimers and legal + +No matter how good Onionr and other software gets, there will always be ways for clever or well-funded adversaries to break your security. + +*Do not rely on Onionr or any other software to hold up if your life or liberty are at stake.* + +### Licenses and Branding + +Onionr is published under the GNU GPL v3 license, except for the logo. The Tor Project and I2P developers do not own, create, or endorse this project, and are not otherwise involved. @@ -102,8 +142,10 @@ Tor is a trademark for the Tor Project. We do not own it. The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License. -## Logo +## Onionr Logo The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without permission. \ No newline at end of file +If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use the project name or logo in a way that makes it seem like we endorse you without our permission. + +![node web illustration](docs/onionr-web.png) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..47a7f806 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +We welcome responsible and constructive security review. + +# Scope + +The Onionr software and any nodes you control are within scope. + +Avoid social engineering, volume-based denial of service and disrupting or harming the Onionr network. Do not attempt to exploit any machines/servers you do not own or otherwise have permission to do so. + +The following exploits are of particular interest: + +* Arbitrary code execution +* API authentication bypass (such as accessing local API from public interface) +* Deanonymization: + * Easily associating public keys with server addresses + * Discovering true server IPs when behind Tor/I2P (aside from Tor/i2p-level attacks) + * Easily discovering which nodes are the block creator +* XSS, CSRF, clickjacking, DNS rebinding +* Timing attacks against the local http server ([see blog post](https://www.chaoswebs.net/blog/timebleed-breaking-privacy-with-a-simple-timing-attack.html)) +* Discovering direct connection servers as a non participant. +* Cryptography/protocol issues +* Denying nodes access to the network by segmenting them out with Sybil nodes + +We do not consider non-network based same-machine attacks to be very significant, but we are still willing to listen. + +# Rewards + +Onionr is a student-owned hobby project, resources are not available for large rewards. + +Stickers or other small rewards are available. We reserve the right to refuse rewards for any reason. + +Public recognition can be given upon request. + +# Contact + +Email: beardog [ at ] mailbox.org + +PGP (optional): F61A 4DBB 0B3D F172 1F65 0EDF 0D41 4D0F E405 B63B diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..6c5f44e5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Onionr Documentation + +The Onionr [whitepaper](whitepaper.md) is the best place to start both for users and developers. + +## User Documentation + +* [Installation](usage/install.md) +* [First steps](usage/firststeps.md) +* [Using Onionr Mail](usage/mail.md) +* [Using Onionr web pages](usage/pages.md) +* [Staying safe/anonymous](usage/safety.md) + +## Developer Documentation + +* [Development environment setup](dev/setup.md) +* [Technical overview](dev/overview.md) +* [Project layout](dev/layout.md) +* [Plugin development guide](dev/plugins.md) +* [Testing](dev/testing.md) \ No newline at end of file diff --git a/docs/api.md b/docs/api.md deleted file mode 100755 index 52a55368..00000000 --- a/docs/api.md +++ /dev/null @@ -1,2 +0,0 @@ -HTTP API -TODO diff --git a/docs/dev/http-api.md b/docs/dev/http-api.md new file mode 100755 index 00000000..74745e1a --- /dev/null +++ b/docs/dev/http-api.md @@ -0,0 +1,118 @@ +# Onionr HTTP API + +All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server. + +## Client & Public difference + +The client API server is a locked down interface intended for authenticated local communication. + +The public API server is available only remotely from Tor & I2P. It is the interface in which peers use to communicate with one another. + +# Client API + +Please note: endpoints that simply provide static web app files are not documented here. + +* /serviceactive/pubkey + - Methods: GET + - Returns true or false based on if a given public key has an active direct connection service. +* /queueResponseAdd/key (DEPRECATED) + - Methods: POST + - Accepts form key 'data' to set queue response information from a plugin + - Returns success if no error occurs +* /queueResponse/key (DEPRECATED) + - Methods: GET + - Returns the queue response for a key. Returns failure with a 404 code if a code is not set. +* /ping + - Methods: GET + - Returns "pong!" +* /getblocksbytype/type + - Methods: GET + - Returns a list of stored blocks by a given type +* /getblockbody/hash + - Methods: GET + - Returns the main data section of a block +* /getblockdata/hash + - Methods: GET + - Returns the entire data contents of a block, including metadata. +* /getblockheader/hash + - Methods: GET + - Returns the header (metadata section) of a block. +* /hitcount + - Methods: GET + - Return the amount of requests the public api server has received this session +* /lastconnect + - Methods: GET + - Returns the epoch timestamp of when the last incoming connection to the public API server was logged +* /site/hash + - Methods: GET + - Returns HTML content out of a block +* /waitforshare/hash + - Methods: POST + - Prevents the public API server from listing or sharing a block until it has been uploaded to at least 1 peer. +* /shutdown + - Methods: GET + - Shutdown Onionr. You should probably use /shutdownclean instead. +* /shutdownclean + - Methods: GET + - Tells the communicator daemon to shutdown Onionr. Slower but cleaner. +* /getstats + - Methods: GET + - Returns some JSON serialized statistics +* /getuptime + - Methods: GET + - Returns uptime in seconds +* /getActivePubkey + - Methods: GET + - Returns the current active public key in base32 format +* /getHumanReadable/pubkey + - Methods: GET + - Echos the specified public key in mnemonic format +* /insertblock + - Methods: POST + - Accepts JSON data for creating a new block. 'message' contains the block data, 'to' specifies the peer's public key to encrypt the data to, 'sign' is a boolean for signing the message. + +# Public API + +v0 + +* / + - Methods: GET + - Returns a basic HTML informational banner describing Onionr. +* /getblocklist + - Methods: GET + - URI Parameters: + - date: unix epoch timestamp for offset + - Returns a list of block hashes stored on the node since an offset (all blocks if no timestamp is specified) +* /getdata/block-hash + - Methods: GET + - Returns data for a block based on a provided hash +* /www/file-path + - Methods: GET + - Returns file data. Intended for manually sharing file data directly from an Onionr node. +* /ping + - Methods: GET + - Returns 'pong!' +* /pex + - Methods: GET + - Returns a list of peer addresses reached within recent time +* /announce + - Methods: POST + - Accepts form data for 'node' (valid node address) and 'random' which is a nonce when hashed (blake2b_256) in the format `hash(peerAddress+serverAddress+nonce)`, begins with at least 5 zeros. + - Returns 200 with 'Success' if no error occurs. If the post is invalid, 'failure' with code 406 is returned. +* /upload + - Methods: POST + - Accepts form data for 'block' as a 'file' upload. + - Returns 200 with 'success' if no error occurs. If the block cannot be accepted, 'failure' with 400 is returned. + +# Direct Connection API + +These are constant endpoints available on direct connection servers. Plugin endpoints for direct connections are not documented here. + +* /ping + - Methods: GET + - Returns 200 with 'pong!' + +* /close + - Methods: GET + - Kills the direct connection server, destroying the onion address. + - Returns 200 with 'goodbye' \ No newline at end of file diff --git a/docs/dev/specs/block-spec.md b/docs/dev/specs/block-spec.md new file mode 100755 index 00000000..26867e42 --- /dev/null +++ b/docs/dev/specs/block-spec.md @@ -0,0 +1,63 @@ +# Onionr Block Spec v1.1.0 + +# Block Description + +Onionr Blocks are the primary means of sharing information in Onionr. Blocks are identified by a single hash value of their entire contents, using SHA3_256. + +Blocks contain a JSON metadata section followed by a line break, with the main data section comprising the rest. + +In the future, the specification will likely be updated to use flags and MessagePack instead of JSON with english keys. + +# Encryption and Signatures + +Onionr blocks may be encrypted or signed. In the reference client, this is done with libsodium, for both asymmetric and symmetric encryption. + +Unlike many similar projects, blocks may completely be in plaintext, making Onionr suitable for sharing information publicly. + +# Metadata Section + +The metadata section has the following fields. If a block contains any other field, it must be considered invalid. All metadata fields are technically optional, but many are useful and essentially necessary for most use cases. + +## meta + +Max byte size: 1000 + +Meta is a string field which can contain arbitrary sub fields. It is intended for applications and plugins to use it for arbitrary metadata information. In the reference client, if the data section is encrypted or signed, the meta section also is. + +Common meta fields, such as 'type' are used by the reference Onionr client to describe the type of a block. + +## sig + +Max byte size: 200 + +Sig is a field for storing public key signatures of the block, typically ed25519. In the reference client, this field is a base64 encoded signature of the meta field combined with the block data. (**Therefore, information outside of the meta and data fields cannot be trusted to be placed there by the signer, although it can still be assured that the particular block has not been modified.**) + +Note: the max field size is larger than a EdDSA signature (which is what is typically used) in order to allow other primitives for signing in alternative implementations or future versions. + +## signer + +Max byte size: 200 + +Signer is a field for specifying the public key which signed the block. In the reference client this is a base64 encoded ed25519 public key. + +## time + +Max byte size: 10 + +Time is an integer field for specifying the time of which a block was created. The trustworthiness of this field is based on one's trust of the block creator, however blocks with a time field set in the future (past a reasonable clock skew) are thrown out by the reference client. + +## expire + +Max byte size: 10 + +Expire is an integer field for specifying the time of which the block creator has indicated that the block should be deleted. The purpose of this is for voluntarily freeing the burden of unwanted blocks on the Onionr network, rather than security/privacy (since blocks could be trivially kept past expiration). Regardless, the reference client deletes blocks after a preset time if the expire field is either not set or longer than the preset time. + +## pow + +Max byte size: 1000 + +Pow is a field for placing the nonce found to make a block meet a target proof of work. In theory, a block could meet a target without a random token in this field. + +## encryptType + +encryptType is a field to specify the mode of encryption for a block. The values supported by Onionr are 'asym' and 'sym'. \ No newline at end of file diff --git a/docs/network-comparison.png b/docs/network-comparison.png deleted file mode 100644 index 0d1f42e6..00000000 Binary files a/docs/network-comparison.png and /dev/null differ diff --git a/docs/onionr-1.png b/docs/onionr-1.png index c498fe8f..fed98f28 100644 Binary files a/docs/onionr-1.png and b/docs/onionr-1.png differ diff --git a/docs/onionr-2.png b/docs/onionr-2.png index 1407d96d..c8bab626 100644 Binary files a/docs/onionr-2.png and b/docs/onionr-2.png differ diff --git a/docs/onionr-3.png b/docs/onionr-3.png index d31230d5..3c7673d2 100644 Binary files a/docs/onionr-3.png and b/docs/onionr-3.png differ diff --git a/docs/onionr-icon.png b/docs/onionr-icon.png index 861af295..65c279db 100755 Binary files a/docs/onionr-icon.png and b/docs/onionr-icon.png differ diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png index 31a95241..493095be 100755 Binary files a/docs/onionr-logo.png and b/docs/onionr-logo.png differ diff --git a/docs/tor-stinks-02.png b/docs/tor-stinks-02.png old mode 100644 new mode 100755 diff --git a/docs/usage/firststeps.md b/docs/usage/firststeps.md new file mode 100644 index 00000000..8733565d --- /dev/null +++ b/docs/usage/firststeps.md @@ -0,0 +1,7 @@ +# Onionr First Steps + +After installing Onionr, there are several things to do: + +1. Setup a [deterministic address](usage/deterministic.md) (optional) +2. Add friends' ids +3. Publish your id \ No newline at end of file diff --git a/docs/usage/install.md b/docs/usage/install.md new file mode 100644 index 00000000..f1fb6130 --- /dev/null +++ b/docs/usage/install.md @@ -0,0 +1,13 @@ +# Onionr Installation + +The following steps work broadly speaking for Windows, Mac, and Linux. + +1. Verify python3.6+ is installed: if its not see https://www.python.org/downloads/ + +2. Verify Tor is installed (does not need to be running, binary can be put into system path or Onionr directory) + +3. [Optional but recommended]: setup virtual environment using [virtualenv](https://virtualenv.pypa.io/en/latest/), activate the virtual environment + +4. Clone Onionr: git clone https://gitlab.com/beardog/onionr + +5. Install the Python module dependencies: pip3 install --require-hashes -r requirements.txt diff --git a/docs/whitepaper.md b/docs/whitepaper.md index ca2eb080..09760453 100755 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -39,11 +39,11 @@ When designing Onionr we had these main goals in mind: At its core, Onionr is merely a description for storing data in self-verifying packages ("blocks"). These blocks can be encrypted to a user (or for one's self), encrypted symmetrically, or not at all. Blocks can be signed by their creator, but regardless, they are self-verifying due to being identified by a sha3-256 hash value; once a block is created, it cannot be modified. -Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however this is configurable. Blocks do not rely on any particular order of receipt or transport mechanism. +Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however, this is configurable. Blocks do not rely on any particular order of receipt or transport mechanism. ## User IDs -User IDs are simply Ed25519 public keys. They are represented in Base32 format, or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list). +User IDs are simply Ed25519 public keys. They are represented in Base32 format or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list). Public keys can be generated deterministically with a password using a key derivation function (Argon2id). This password can be shared between many users in order to share data anonymously among a group, using only 1 password. This is useful in some cases, but is risky, as if one user causes the key to be compromised and does not notify the group or revoke the key, there is no way to know. @@ -53,7 +53,7 @@ Although Onionr is transport agnostic, the only supported transports in the refe ### Node Profiling -To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the amount of successful block transfers, speed, and reliability of a node, and reduces the score based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connection to 'friend' nodes. +To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the number of successful block transfers, speed, and reliability of a node, and reduces the score based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connections to 'friend' nodes. ## Block Format @@ -65,7 +65,7 @@ Optionally, a random token can be inserted into the metadata for use in Proof of ### Block Encryption -For encryption, Onionr uses ephemeral Curve25519 keys for key exchange and XSalsa20-Poly1305 as a symmetric cipher, or optionally using only XSalsa20-Poly1305 with a pre-shared key. +For encryption, Onionr uses ephemeral Curve25519 keys for key exchange and XSalsa20-Poly1305 as a symmetric cipher or optionally using only XSalsa20-Poly1305 with a pre-shared key. Regardless of encryption, blocks can be signed internally using Ed25519. @@ -91,7 +91,7 @@ In addition, randomness beacons such as the one operated by [NIST](https://beaco # Direct Connections -We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request connection. If the requested peer is available and wishes to accept the connection, Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket. +We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request a connection. If the requested peer is available and wishes to accept the connection, Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket. The benefits of such a system are increased privacy, and the ability to anonymously communicate from multiple devices at once. In a traditional onion service, one's online status can be monitored and more easily correlated. @@ -119,7 +119,9 @@ We seek to protect the following information: * Physical location/IP address of nodes on the network * All block data from tampering -### Data we cannot or do not protect +### Unprotected Data + +Onionr does not protect the following: * Data specifically inserted as plaintext is available to the public * The public key of signed plaintext blocks @@ -133,12 +135,6 @@ We assume that Tor onion services (v3) and I2P services cannot be trivially dean Once quantum safe algorithms are more mature and have decent high level libraries, they will be deployed. -# Comparisons to other P2P software - -Since Onionr is far from the first to implement many of these ideas (on their own), this section compares Onionr to other networks, using points we consider to be the most important. - -![network comparison image](network-comparison.png) - # Conclusion -If successful, Onionr will be a complete decentralized platform for anonymous computing, complete with limited metadata exposure, both node and user anonymity, and spam prevention \ No newline at end of file +If successful, Onionr will be a complete decentralized platform for anonymous computing, complete with limited metadata exposure, both node and user anonymity, and spam prevention diff --git a/install/onionr.service b/install/onionr.service old mode 100644 new mode 100755 diff --git a/onionr/api.py b/onionr/api.py deleted file mode 100755 index 79694238..00000000 --- a/onionr/api.py +++ /dev/null @@ -1,602 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - This file handles all incoming http requests to the client, using Flask -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -from gevent.pywsgi import WSGIServer, WSGIHandler -from gevent import Timeout -import flask, cgi, uuid -from flask import request, Response, abort, send_from_directory -import sys, random, threading, hmac, base64, time, os, json, socket -import core -from onionrblockapi import Block -import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config -import httpapi -from httpapi import friendsapi, simplecache -import onionr - -class FDSafeHandler(WSGIHandler): - '''Our WSGI handler. Doesn't do much non-default except timeouts''' - def handle(self): - timeout = Timeout(60, exception=Exception) - timeout.start() - try: - WSGIHandler.handle(self) - except Timeout as ex: - raise - -def setBindIP(filePath): - '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' - if config.get('general.random_bind_ip', True): - hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] - data = '.'.join(hostOctets) - # Try to bind IP. Some platforms like Mac block non normal 127.x.x.x - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.bind((data, 0)) - except OSError: - # if mac/non-bindable, show warning and default to 127.0.0.1 - logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.') - data = '127.0.0.1' - s.close() - else: - data = '127.0.0.1' - with open(filePath, 'w') as bindFile: - bindFile.write(data) - return data - -class PublicAPI: - ''' - The new client api server, isolated from the public api - ''' - def __init__(self, clientAPI): - assert isinstance(clientAPI, API) - app = flask.Flask('PublicAPI') - self.i2pEnabled = config.get('i2p.host', False) - self.hideBlocks = [] # Blocks to be denied sharing - self.host = setBindIP(clientAPI._core.publicApiHostFile) - self.torAdder = clientAPI._core.hsAddress - self.i2pAdder = clientAPI._core.i2pAddress - self.bindPort = config.get('client.public.port') - self.lastRequest = 0 - logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) - - @app.before_request - def validateRequest(): - '''Validate request has the correct hostname''' - # If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P) - if config.get('general.security_level', default=0) > 0: - abort(403) - if type(self.torAdder) is None and type(self.i2pAdder) is None: - # abort if our hs addresses are not known - abort(403) - if request.host not in (self.i2pAdder, self.torAdder): - # Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks - abort(403) - - @app.after_request - def sendHeaders(resp): - '''Send api, access control headers''' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header. - # CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed) - resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" - # Prevent click jacking - resp.headers['X-Frame-Options'] = 'deny' - # No sniff is possibly not needed - resp.headers['X-Content-Type-Options'] = "nosniff" - # Network API version - resp.headers['X-API'] = onionr.API_VERSION - # Close connections to limit FD use - resp.headers['Connection'] = "close" - self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5) - return resp - - @app.route('/') - def banner(): - # Display a bit of information to people who visit a node address in their browser - try: - with open('static-data/index.html', 'r') as html: - resp = Response(html.read(), mimetype='text/html') - except FileNotFoundError: - resp = Response("") - return resp - - @app.route('/getblocklist') - def getBlockList(): - # Provide a list of our blocks, with a date offset - dateAdjust = request.args.get('date') - bList = clientAPI._core.getBlockList(dateRec=dateAdjust) - for b in self.hideBlocks: - if b in bList: - # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block - bList.remove(b) - return Response('\n'.join(bList)) - - @app.route('/getdata/') - def getBlockData(name): - # Share data for a block if we have it - resp = '' - data = name - if clientAPI._utils.validateHash(data): - if data not in self.hideBlocks: - if data in clientAPI._core.getBlockList(): - block = clientAPI.getBlockData(data, raw=True) - try: - block = block.encode() - except AttributeError: - abort(404) - block = clientAPI._core._utils.strToBytes(block) - resp = block - #resp = base64.b64encode(block).decode() - if len(resp) == 0: - abort(404) - resp = "" - return Response(resp, mimetype='application/octet-stream') - - @app.route('/www/') - def wwwPublic(path): - # A way to share files directly over your .onion - if not config.get("www.public.run", True): - abort(403) - return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) - - @app.route('/ping') - def ping(): - # Endpoint to test if nodes are up - return Response("pong!") - - @app.route('/pex') - def peerExchange(): - response = ','.join(clientAPI._core.listAdders(recent=3600)) - if len(response) == 0: - response = '' - return Response(response) - - @app.route('/announce', methods=['post']) - def acceptAnnounce(): - resp = 'failure' - powHash = '' - randomData = '' - newNode = '' - ourAdder = clientAPI._core.hsAddress.encode() - try: - newNode = request.form['node'].encode() - except KeyError: - logger.warn('No node specified for upload') - pass - else: - try: - randomData = request.form['random'] - randomData = base64.b64decode(randomData) - except KeyError: - logger.warn('No random data specified for upload') - else: - nodes = newNode + clientAPI._core.hsAddress.encode() - nodes = clientAPI._core._crypto.blake2bHash(nodes) - powHash = clientAPI._core._crypto.blake2bHash(randomData + nodes) - try: - powHash = powHash.decode() - except AttributeError: - pass - if powHash.startswith('0000'): - newNode = clientAPI._core._utils.bytesToStr(newNode) - if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: - clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) - resp = 'Success' - else: - logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) - resp = Response(resp) - return resp - - @app.route('/upload', methods=['post']) - def upload(): - '''Accept file uploads. In the future this will be done more often than on creation - to speed up block sync - ''' - resp = 'failure' - try: - data = request.form['block'] - except KeyError: - logger.warn('No block specified for upload') - pass - else: - if sys.getsizeof(data) < 100000000: - try: - if blockimporter.importBlockFromData(data, clientAPI._core): - resp = 'success' - else: - logger.warn('Error encountered importing uploaded block') - except onionrexceptions.BlacklistedBlock: - logger.debug('uploaded block is blacklisted') - pass - if resp == 'failure': - abort(400) - resp = Response(resp) - return resp - - # Set instances, then startup our public api server - clientAPI.setPublicAPIInstance(self) - while self.torAdder == '': - clientAPI._core.refreshFirstStartVars() - self.torAdder = clientAPI._core.hsAddress - time.sleep(0.1) - self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler) - self.httpServer.serve_forever() - -class API: - ''' - Client HTTP api - ''' - - callbacks = {'public' : {}, 'private' : {}} - - def __init__(self, onionrInst, debug, API_VERSION): - ''' - Initialize the api server, preping variables for later use - - This initilization defines all of the API entry points and handlers for the endpoints and errors - This also saves the used host (random localhost IP address) to the data folder in host.txt - ''' - - self.debug = debug - self._core = onionrInst.onionrCore - self.startTime = self._core._utils.getEpoch() - self._crypto = onionrcrypto.OnionrCrypto(self._core) - self._utils = onionrutils.OnionrUtils(self._core) - app = flask.Flask(__name__) - bindPort = int(config.get('client.client.port', 59496)) - self.bindPort = bindPort - - # Be extremely mindful of this. These are endpoints available without a password - self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex', 'friends', 'friendsindex') - - self.clientToken = config.get('client.webpassword') - self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() - - self.publicAPI = None # gets set when the thread calls our setter... bad hack but kinda necessary with flask - #threading.Thread(target=PublicAPI, args=(self,)).start() - self.host = setBindIP(self._core.privateApiHostFile) - logger.info('Running api on %s:%s' % (self.host, self.bindPort)) - self.httpServer = '' - - self.pluginResponses = {} # Responses for plugin endpoints - self.queueResponse = {} - onionrInst.setClientAPIInst(self) - app.register_blueprint(friendsapi.friends) - app.register_blueprint(simplecache.simplecache) - httpapi.load_plugin_blueprints(app) - - @app.before_request - def validateRequest(): - '''Validate request has set password and is the correct hostname''' - # For the purpose of preventing DNS rebinding attacks - if request.host != '%s:%s' % (self.host, self.bindPort): - abort(403) - if request.endpoint in self.whitelistEndpoints: - return - try: - if not hmac.compare_digest(request.headers['token'], self.clientToken): - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - except KeyError: - if not hmac.compare_digest(request.form['token'], self.clientToken): - abort(403) - - @app.after_request - def afterReq(resp): - # Security headers - if request.endpoint == 'site': - resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" - else: - resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" - resp.headers['X-Frame-Options'] = 'deny' - resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['Server'] = '' - resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. - resp.headers['Connection'] = "close" - return resp - - @app.route('/board/', endpoint='board') - def loadBoard(): - return send_from_directory('static-data/www/board/', "index.html") - - @app.route('/mail/', endpoint='mail') - def loadMail(path): - return send_from_directory('static-data/www/mail/', path) - @app.route('/mail/', endpoint='mailindex') - def loadMailIndex(): - return send_from_directory('static-data/www/mail/', 'index.html') - - @app.route('/friends/', endpoint='friends') - def loadContacts(path): - return send_from_directory('static-data/www/friends/', path) - - @app.route('/friends/', endpoint='friendsindex') - def loadContacts(): - return send_from_directory('static-data/www/friends/', 'index.html') - - @app.route('/board/', endpoint='boardContent') - def boardContent(path): - return send_from_directory('static-data/www/board/', path) - @app.route('/shared/', endpoint='sharedContent') - def sharedContent(path): - return send_from_directory('static-data/www/shared/', path) - - @app.route('/www/', endpoint='www') - def wwwPublic(path): - if not config.get("www.private.run", True): - abort(403) - return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) - - @app.route('/queueResponseAdd/', methods=['post']) - def queueResponseAdd(name): - # Responses from the daemon. TODO: change to direct var access instead of http endpoint - self.queueResponse[name] = request.form['data'] - return Response('success') - - @app.route('/queueResponse/') - def queueResponse(name): - # Fetch a daemon queue response - resp = 'failure' - try: - resp = self.queueResponse[name] - except KeyError: - pass - else: - del self.queueResponse[name] - return Response(resp) - - @app.route('/ping') - def ping(): - # Used to check if client api is working - return Response("pong!") - - @app.route('/', endpoint='onionrhome') - def hello(): - # ui home - return send_from_directory('static-data/www/private/', 'index.html') - - @app.route('/getblocksbytype/') - def getBlocksByType(name): - blocks = self._core.getBlocksByType(name) - return Response(','.join(blocks)) - - @app.route('/getblockbody/') - def getBlockBodyData(name): - resp = '' - if self._core._utils.validateHash(name): - try: - resp = Block(name, decrypt=True).bcontent - #resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True) - except TypeError: - pass - else: - abort(404) - return Response(resp) - - @app.route('/getblockdata/') - def getData(name): - resp = "" - if self._core._utils.validateHash(name): - if name in self._core.getBlockList(): - try: - resp = self.getBlockData(name, decrypt=True) - except ValueError: - pass - else: - abort(404) - else: - abort(404) - return Response(resp) - - @app.route('/getblockheader/') - def getBlockHeader(name): - resp = self.getBlockData(name, decrypt=True, headerOnly=True) - return Response(resp) - - @app.route('/lastconnect') - def lastConnect(): - return Response(str(self.publicAPI.lastRequest)) - - @app.route('/site/', endpoint='site') - def site(name): - bHash = name - resp = 'Not Found' - if self._core._utils.validateHash(bHash): - try: - resp = Block(bHash).bcontent - except onionrexceptions.NoDataAvailable: - abort(404) - except TypeError: - pass - try: - resp = base64.b64decode(resp) - except: - pass - if resp == 'Not Found' or not resp: - abort(404) - return Response(resp) - - @app.route('/waitforshare/', methods=['post']) - def waitforshare(name): - '''Used to prevent the **public** api from sharing blocks we just created''' - assert name.isalnum() - if name in self.publicAPI.hideBlocks: - self.publicAPI.hideBlocks.remove(name) - return Response("removed") - else: - self.publicAPI.hideBlocks.append(name) - return Response("added") - - @app.route('/shutdown') - def shutdown(): - try: - self.publicAPI.httpServer.stop() - self.httpServer.stop() - except AttributeError: - pass - return Response("bye") - - @app.route('/shutdownclean') - def shutdownClean(): - # good for calling from other clients - self._core.daemonQueueAdd('shutdown') - return Response("bye") - - @app.route('/getstats') - def getStats(): - # returns node stats - #return Response("disabled") - while True: - try: - return Response(self._core.serializer.getStats()) - except AttributeError: - pass - - @app.route('/getuptime') - def showUptime(): - return Response(str(self.getUptime())) - - @app.route('/getActivePubkey') - def getActivePubkey(): - return Response(self._core._crypto.pubKey) - - @app.route('/getHumanReadable/') - def getHumanReadable(name): - return Response(self._core._utils.getHumanReadableID(name)) - - @app.route('/insertblock', methods=['POST']) - def insertBlock(): - encrypt = False - bData = request.get_json(force=True) - message = bData['message'] - subject = 'temp' - encryptType = '' - sign = True - meta = {} - to = '' - try: - if bData['encrypt']: - to = bData['to'] - encrypt = True - encryptType = 'asym' - except KeyError: - pass - try: - if not bData['sign']: - sign = False - except KeyError: - pass - try: - bType = bData['type'] - except KeyError: - bType = 'bin' - try: - meta = json.loads(bData['meta']) - except KeyError: - pass - threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() - return Response('success') - - @app.route('/apipoints/', methods=['POST', 'GET']) - def pluginEndpoints(subpath=''): - '''Send data to plugins''' - # TODO have a variable for the plugin to set data to that we can use for the response - pluginResponseCode = str(uuid.uuid4()) - resp = 'success' - responseTimeout = 20 - startTime = self._core._utils.getEpoch() - postData = {} - if request.method == 'POST': - postData = request.form['postData'] - if len(subpath) > 1: - data = subpath.split('/') - if len(data) > 1: - plName = data[0] - events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) - while True: - try: - resp = self.pluginResponses[pluginResponseCode] - except KeyError: - time.sleep(0.2) - if self._core._utils.getEpoch() - startTime > responseTimeout: - abort(504) - break - else: - break - else: - abort(404) - return Response(resp) - - self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler) - self.httpServer.serve_forever() - - def setPublicAPIInstance(self, inst): - assert isinstance(inst, PublicAPI) - self.publicAPI = inst - - def validateToken(self, token): - ''' - Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration - ''' - if len(self.clientToken) == 0: - logger.error("client password needs to be set") - return False - try: - if not hmac.compare_digest(self.clientToken, token): - return False - else: - return True - except TypeError: - return False - - def getUptime(self): - while True: - try: - return self._utils.getEpoch - startTime - except AttributeError: - # Don't error on race condition with startup - pass - - def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): - assert self._core._utils.validateHash(bHash) - bl = Block(bHash, core=self._core) - if decrypt: - bl.decrypt() - if bl.isEncrypted and not bl.decrypted: - raise ValueError - - if not raw: - if not headerOnly: - retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} - for x in list(retData.keys()): - try: - retData[x] = retData[x].decode() - except AttributeError: - pass - else: - validSig = False - signer = self._core._utils.bytesToStr(bl.signer) - #print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(signer)) - if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer): - validSig = True - bl.bheader['validSig'] = validSig - bl.bheader['meta'] = '' - retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} - return json.dumps(retData) - else: - return bl.raw diff --git a/onionr/apiservers/__init__.py b/onionr/apiservers/__init__.py new file mode 100755 index 00000000..c7c693ba --- /dev/null +++ b/onionr/apiservers/__init__.py @@ -0,0 +1,3 @@ +from . import public, private +PublicAPI = public.PublicAPI +ClientAPI = private.PrivateAPI \ No newline at end of file diff --git a/onionr/apiservers/private/__init__.py b/onionr/apiservers/private/__init__.py new file mode 100644 index 00000000..938ce384 --- /dev/null +++ b/onionr/apiservers/private/__init__.py @@ -0,0 +1,96 @@ +''' + Onionr - Private P2P Communication + + This file handles all incoming http requests to the client, using Flask +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64, os +import flask +from gevent.pywsgi import WSGIServer +import logger +from onionrutils import epoch +import httpapi +from . import register_private_blueprints +class PrivateAPI: + ''' + Client HTTP api + ''' + + callbacks = {'public' : {}, 'private' : {}} + + def __init__(self, onionrInst, debug, API_VERSION): + ''' + Initialize the api server, preping variables for later use + + This initialization defines all of the API entry points and handlers for the endpoints and errors + This also saves the used host (random localhost IP address) to the data folder in host.txt + ''' + config = onionrInst.config + self.config = config + self.debug = debug + self._core = onionrInst.onionrCore + self.startTime = epoch.get_epoch() + self._crypto = self._core._crypto + app = flask.Flask(__name__) + bindPort = int(config.get('client.client.port', 59496)) + self.bindPort = bindPort + + self.clientToken = config.get('client.webpassword') + self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() + + self.publicAPI = None # gets set when the thread calls our setter... bad hack but kinda necessary with flask + #threading.Thread(target=PublicAPI, args=(self,)).start() + self.host = httpapi.apiutils.setbindip.set_bind_IP(self._core.privateApiHostFile, self._core) + logger.info('Running api on %s:%s' % (self.host, self.bindPort)) + self.httpServer = '' + + self.queueResponse = {} + self.get_block_data = httpapi.apiutils.GetBlockData(self) + onionrInst.setClientAPIInst(self) + register_private_blueprints.register_private_blueprints(self, app) + httpapi.load_plugin_blueprints(app) + + self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=httpapi.fdsafehandler.FDSafeHandler) + self.httpServer.serve_forever() + + def setPublicAPIInstance(self, inst): + self.publicAPI = inst + + def validateToken(self, token): + ''' + Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration + ''' + if len(self.clientToken) == 0: + logger.error("client password needs to be set") + return False + try: + if not hmac.compare_digest(self.clientToken, token): + return False + else: + return True + except TypeError: + return False + + def getUptime(self): + while True: + try: + return epoch.get_epoch() - self.startTime + except (AttributeError, NameError): + # Don't error on race condition with startup + pass + + def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False): + return self.get_block_data.get_block_data(bHash, decrypt=decrypt, raw=raw, headerOnly=headerOnly) \ No newline at end of file diff --git a/onionr/apiservers/private/register_private_blueprints.py b/onionr/apiservers/private/register_private_blueprints.py new file mode 100644 index 00000000..c9960b88 --- /dev/null +++ b/onionr/apiservers/private/register_private_blueprints.py @@ -0,0 +1,33 @@ +''' + Onionr - Private P2P Communication + + This file registers blueprints for the private api server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os +from httpapi import security, friendsapi, profilesapi, configapi, insertblock, miscclientapi, onionrsitesapi, apiutils +def register_private_blueprints(private_api, app): + app.register_blueprint(security.client.ClientAPISecurity(private_api).client_api_security_bp) + app.register_blueprint(friendsapi.friends) + app.register_blueprint(profilesapi.profile_BP) + app.register_blueprint(configapi.config_BP) + app.register_blueprint(insertblock.ib) + app.register_blueprint(miscclientapi.getblocks.client_get_blocks) + app.register_blueprint(miscclientapi.endpoints.PrivateEndpoints(private_api).private_endpoints_bp) + app.register_blueprint(onionrsitesapi.site_api) + app.register_blueprint(apiutils.shutdown.shutdown_bp) + app.register_blueprint(miscclientapi.staticfiles.static_files_bp) + return app \ No newline at end of file diff --git a/onionr/apiservers/public/__init__.py b/onionr/apiservers/public/__init__.py new file mode 100644 index 00000000..b5594e6d --- /dev/null +++ b/onionr/apiservers/public/__init__.py @@ -0,0 +1,56 @@ +''' + Onionr - Private P2P Communication + + This file handles all incoming http requests to the public api server, using Flask +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time +import flask +from gevent.pywsgi import WSGIServer +from httpapi import apiutils, security, fdsafehandler, miscpublicapi +import logger, onionr +class PublicAPI: + ''' + The new client api server, isolated from the public api + ''' + def __init__(self, clientAPI): + config = clientAPI.config + app = flask.Flask('PublicAPI') + app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 + self.i2pEnabled = config.get('i2p.host', False) + self.hideBlocks = [] # Blocks to be denied sharing + self.host = apiutils.setbindip.set_bind_IP(clientAPI._core.publicApiHostFile, clientAPI._core) + self.torAdder = clientAPI._core.hsAddress + self.i2pAdder = clientAPI._core.i2pAddress + self.bindPort = config.get('client.public.port') + self.lastRequest = 0 + self.hitCount = 0 # total rec requests to public api since server started + self.config = config + self.clientAPI = clientAPI + self.API_VERSION = onionr.API_VERSION + logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) + + # Set instances, then startup our public api server + clientAPI.setPublicAPIInstance(self) + while self.torAdder == '': + clientAPI._core.refreshFirstStartVars() + self.torAdder = clientAPI._core.hsAddress + time.sleep(0.1) + + app.register_blueprint(security.public.PublicAPISecurity(self).public_api_security_bp) + app.register_blueprint(miscpublicapi.endpoints.PublicEndpoints(self).public_endpoints_bp) + self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=fdsafehandler.FDSafeHandler) + self.httpServer.serve_forever() \ No newline at end of file diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py index ce1cd1fe..067ec045 100755 --- a/onionr/blockimporter.py +++ b/onionr/blockimporter.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Import block data and save it ''' @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import core, onionrexceptions, logger +from onionrutils import validatemetadata, blockmetadata def importBlockFromData(content, coreInst): retData = False @@ -34,17 +35,17 @@ def importBlockFromData(content, coreInst): except AttributeError: pass - metas = coreInst._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata + metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata metadata = metas[0] - if coreInst._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid + if validatemetadata.validate_metadata(coreInst, metadata, metas[2]): # check if metadata is valid if coreInst._crypto.verifyPow(content): # check if POW is enough/correct - logger.info('Block passed proof, saving.') + logger.info('Block passed proof, saving.', terminal=True) try: blockHash = coreInst.setData(content) except onionrexceptions.DiskAllocationReached: pass else: coreInst.addToBlockDB(blockHash, dataSaved=True) - coreInst._utils.processBlockMetadata(blockHash) # caches block metadata values to block database + blockmetadata.process_block_metadata(coreInst, blockHash) # caches block metadata values to block database retData = True return retData \ No newline at end of file diff --git a/onionr/communicator.py b/onionr/communicator.py deleted file mode 100755 index 12fbabbc..00000000 --- a/onionr/communicator.py +++ /dev/null @@ -1,646 +0,0 @@ -#!/usr/bin/env python3 -''' - Onionr - P2P Anonymous Storage Network - - This file contains both the OnionrCommunicate class for communcating with peers - and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue) -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid, binascii -from dependencies import secrets -from utils import networkmerger -import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -from communicatorutils import onionrdaemontools -import onionrsockets, onionr, onionrproofs -from communicatorutils import onionrcommunicatortimers, proxypicker - -OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers - -config.reload() -class OnionrCommunicatorDaemon: - def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): - onionrInst.communicatorInst = self - # configure logger and stuff - onionr.Onionr.setupConfig('data/', self = self) - self.proxyPort = proxyPort - - self.isOnline = True # Assume we're connected to the internet - - # list of timer instances - self.timers = [] - - # initialize core with Tor socks port being 3rd argument - self.proxyPort = proxyPort - self._core = onionrInst.onionrCore - - self.blocksToUpload = [] - - # loop time.sleep delay in seconds - self.delay = 1 - - # lists of connected peers and peers we know we can't reach currently - self.onlinePeers = [] - self.offlinePeers = [] - self.cooldownPeer = {} - self.connectTimes = {} - self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) - self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable - - # amount of threads running by name, used to prevent too many - self.threadCounts = {} - - # set true when shutdown command received - self.shutdown = False - - # list of new blocks to download, added to when new block lists are fetched from peers - self.blockQueue = {} - - # list of blocks currently downloading, avoid s - self.currentDownloading = [] - - # timestamp when the last online node was seen - self.lastNodeSeen = None - - # Dict of time stamps for peer's block list lookup times, to avoid downloading full lists all the time - self.dbTimestamps = {} - - # Clear the daemon queue for any dead messages - if os.path.exists(self._core.queueDB): - self._core.clearDaemonQueue() - - # Loads in and starts the enabled plugins - plugins.reload() - - # daemon tools are misc daemon functions, e.g. announce to online peers - # intended only for use by OnionrCommunicatorDaemon - self.daemonTools = onionrdaemontools.DaemonTools(self) - - # time app started running for info/statistics purposes - self.startTime = self._core._utils.getEpoch() - - if developmentMode: - OnionrCommunicatorTimers(self, self.heartbeat, 30) - - # Set timers, function reference, seconds - # requiresPeer True means the timer function won't fire if we have no connected peers - peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1) - OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1) - OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1) - OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True, maxThreads=2) - OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) - blockCleanupTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65) - OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) - OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True) - OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1) - OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1) - OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1) - deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1) - - netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600) - if config.get('general.security_level') == 0: - announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 3600, requiresPeer=True, maxThreads=1) - announceTimer.count = (announceTimer.frequency - 120) - else: - logger.debug('Will not announce node.') - cleanupTimer = OnionrCommunicatorTimers(self, self.peerCleanup, 300, requiresPeer=True) - forwardSecrecyTimer = OnionrCommunicatorTimers(self, self.daemonTools.cleanKeys, 15, maxThreads=1) - - # set loop to execute instantly to load up peer pool (replaced old pool init wait) - peerPoolTimer.count = (peerPoolTimer.frequency - 1) - cleanupTimer.count = (cleanupTimer.frequency - 60) - deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) - blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) - #forwardSecrecyTimer.count = (forwardSecrecyTimer.frequency - 990) - - if config.get('general.socket_servers'): - self.socketServer = threading.Thread(target=onionrsockets.OnionrSocketServer, args=(self._core,)) - self.socketServer.start() - self.socketClient = onionrsockets.OnionrSocketClient(self._core) - - # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking - try: - while not self.shutdown: - for i in self.timers: - if self.shutdown: - break - i.processTimer() - time.sleep(self.delay) - # Debug to print out used FDs (regular and net) - #proc = psutil.Process() - #print(proc.open_files(), len(psutil.net_connections())) - except KeyboardInterrupt: - self.shutdown = True - pass - - logger.info('Goodbye.') - self._core.killSockets = True - self._core._utils.localCommand('shutdown') # shutdown the api - time.sleep(0.5) - - def lookupAdders(self): - '''Lookup new peer addresses''' - logger.info('Looking up new addresses...') - tryAmount = 1 - newPeers = [] - for i in range(tryAmount): - # Download new peer address list from random online peers - if len(newPeers) > 10000: - # Dont get new peers if we have too many queued up - break - peer = self.pickOnlinePeer() - newAdders = self.peerAction(peer, action='pex') - try: - newPeers = newAdders.split(',') - except AttributeError: - pass - else: - # Validate new peers are good format and not already in queue - invalid = [] - for x in newPeers: - x = x.strip() - if not self._core._utils.validateID(x) or x in self.newPeers or x == self._core.hsAddress: - invalid.append(x) - for x in invalid: - newPeers.remove(x) - self.newPeers.extend(newPeers) - self.decrementThreadCount('lookupAdders') - - def lookupBlocks(self): - '''Lookup new blocks & add them to download queue''' - logger.info('Looking up new blocks...') - tryAmount = 2 - newBlocks = '' - existingBlocks = self._core.getBlockList() - triedPeers = [] # list of peers we've tried this time around - maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion - lastLookupTime = 0 # Last time we looked up a particular peer's list - for i in range(tryAmount): - listLookupCommand = 'getblocklist' # This is defined here to reset it each time - if len(self.blockQueue) >= maxBacklog: - break - if not self.isOnline: - break - # check if disk allocation is used - if self._core._utils.storageCounter.isFull(): - logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') - break - peer = self.pickOnlinePeer() # select random online peer - # if we've already tried all the online peers this time around, stop - if peer in triedPeers: - if len(self.onlinePeers) == len(triedPeers): - break - else: - continue - triedPeers.append(peer) - - # Get the last time we looked up a peer's stamp to only fetch blocks since then. - # Saved in memory only for privacy reasons - try: - lastLookupTime = self.dbTimestamps[peer] - except KeyError: - lastLookupTime = 0 - else: - listLookupCommand += '?date=%s' % (lastLookupTime,) - try: - newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes - except Exception as error: - logger.warn('Could not get new blocks from %s.' % peer, error = error) - newBlocks = False - else: - self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60) - if newBlocks != False: - # if request was a success - for i in newBlocks.split('\n'): - if self._core._utils.validateHash(i): - # if newline seperated string is valid hash - if not i in existingBlocks: - # if block does not exist on disk and is not already in block queue - if i not in self.blockQueue: - if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): - if len(self.blockQueue) <= 1000000: - self.blockQueue[i] = [peer] # add blocks to download queue - else: - if peer not in self.blockQueue[i]: - if len(self.blockQueue[i]) < 10: - self.blockQueue[i].append(peer) - self.decrementThreadCount('lookupBlocks') - return - - def getBlocks(self): - '''download new blocks in queue''' - for blockHash in list(self.blockQueue): - triedQueuePeers = [] # List of peers we've tried for a block - try: - blockPeers = list(self.blockQueue[blockHash]) - except KeyError: - blockPeers = [] - removeFromQueue = True - if self.shutdown or not self.isOnline: - # Exit loop if shutting down or offline - break - # Do not download blocks being downloaded or that are already saved (edge cases) - if blockHash in self.currentDownloading: - #logger.debug('Already downloading block %s...' % blockHash) - continue - if blockHash in self._core.getBlockList(): - #logger.debug('Block %s is already saved.' % (blockHash,)) - try: - del self.blockQueue[blockHash] - except KeyError: - pass - continue - if self._core._blacklist.inBlacklist(blockHash): - continue - if self._core._utils.storageCounter.isFull(): - break - self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block - if len(blockPeers) == 0: - peerUsed = self.pickOnlinePeer() - else: - blockPeers = self._core._crypto.randomShuffle(blockPeers) - peerUsed = blockPeers.pop(0) - - if not self.shutdown and peerUsed.strip() != '': - logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed)) - content = self.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata) - if content != False and len(content) > 0: - try: - content = content.encode() - except AttributeError: - pass - - realHash = self._core._crypto.sha3Hash(content) - try: - realHash = realHash.decode() # bytes on some versions for some reason - except AttributeError: - pass - if realHash == blockHash: - content = content.decode() # decode here because sha3Hash needs bytes above - metas = self._core._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata - metadata = metas[0] - if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce - if self._core._crypto.verifyPow(content): # check if POW is enough/correct - logger.info('Attempting to save block %s...' % blockHash[:12]) - try: - self._core.setData(content) - except onionrexceptions.DiskAllocationReached: - logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash) - removeFromQueue = False - else: - self._core.addToBlockDB(blockHash, dataSaved=True) - self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database - else: - logger.warn('POW failed for block %s.' % blockHash) - else: - if self._core._blacklist.inBlacklist(realHash): - logger.warn('Block %s is blacklisted.' % (realHash,)) - else: - logger.warn('Metadata for block %s is invalid.' % blockHash) - self._core._blacklist.addToDB(blockHash) - else: - # if block didn't meet expected hash - tempHash = self._core._crypto.sha3Hash(content) # lazy hack, TODO use var - try: - tempHash = tempHash.decode() - except AttributeError: - pass - # Punish peer for sharing invalid block (not always malicious, but is bad regardless) - onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) - if tempHash != 'ed55e34cb828232d6c14da0479709bfa10a0923dca2b380496e6b2ed4f7a0253': - # Dumb hack for 404 response from peer. Don't log it if 404 since its likely not malicious or a critical error. - logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) - else: - removeFromQueue = False # Don't remove from queue if 404 - if removeFromQueue: - try: - del self.blockQueue[blockHash] # remove from block queue both if success or false - except KeyError: - pass - self.currentDownloading.remove(blockHash) - self.decrementThreadCount('getBlocks') - return - - def pickOnlinePeer(self): - '''randomly picks peer from pool without bias (using secrets module)''' - retData = '' - while True: - peerLength = len(self.onlinePeers) - if peerLength <= 0: - break - try: - # get a random online peer, securely. May get stuck in loop if network is lost or if all peers in pool magically disconnect at once - retData = self.onlinePeers[self._core._crypto.secrets.randbelow(peerLength)] - except IndexError: - pass - else: - break - return retData - - def decrementThreadCount(self, threadName): - '''Decrement amount of a thread name if more than zero, called when a function meant to be run in a thread ends''' - try: - if self.threadCounts[threadName] > 0: - self.threadCounts[threadName] -= 1 - except KeyError: - pass - - def clearOfflinePeer(self): - '''Removes the longest offline peer to retry later''' - try: - removed = self.offlinePeers.pop(0) - except IndexError: - pass - else: - logger.debug('Removed ' + removed + ' from offline list, will try them again.') - self.decrementThreadCount('clearOfflinePeer') - - def getOnlinePeers(self): - ''' - Manages the self.onlinePeers attribute list, connects to more peers if we have none connected - ''' - - logger.debug('Refreshing peer pool...') - maxPeers = int(config.get('peers.max_connect', 10)) - needed = maxPeers - len(self.onlinePeers) - - for i in range(needed): - if len(self.onlinePeers) == 0: - self.connectNewPeer(useBootstrap=True) - else: - self.connectNewPeer() - - if self.shutdown: - break - else: - if len(self.onlinePeers) == 0: - logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % self.daemonTools.humanReadableTime(time.time() - self.lastNodeSeen) if not self.lastNodeSeen is None else '')) - else: - self.lastNodeSeen = time.time() - self.decrementThreadCount('getOnlinePeers') - - def addBootstrapListToPeerList(self, peerList): - ''' - Add the bootstrap list to the peer list (no duplicates) - ''' - for i in self._core.bootstrapList: - if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress and len(str(i).strip()) > 0: - peerList.append(i) - self._core.addAddress(i) - - def connectNewPeer(self, peer='', useBootstrap=False): - '''Adds a new random online peer to self.onlinePeers''' - retData = False - tried = self.offlinePeers - if peer != '': - if self._core._utils.validateID(peer): - peerList = [peer] - else: - raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') - else: - peerList = self._core.listAdders() - - mainPeerList = self._core.listAdders() - peerList = onionrpeers.getScoreSortedPeerList(self._core) - - if len(peerList) < 8 or secrets.randbelow(4) == 3: - tryingNew = [] - for x in self.newPeers: - if x not in peerList: - peerList.append(x) - tryingNew.append(x) - for i in tryingNew: - self.newPeers.remove(i) - - if len(peerList) == 0 or useBootstrap: - # Avoid duplicating bootstrap addresses in peerList - self.addBootstrapListToPeerList(peerList) - - for address in peerList: - if not config.get('tor.v3onions') and len(address) == 62: - continue - if address == self._core.hsAddress: - continue - if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: - continue - if self.shutdown: - return - if self.peerAction(address, 'ping') == 'pong!': - time.sleep(0.1) - if address not in mainPeerList: - networkmerger.mergeAdders(address, self._core) - if address not in self.onlinePeers: - logger.info('Connected to ' + address) - self.onlinePeers.append(address) - self.connectTimes[address] = self._core._utils.getEpoch() - retData = address - - # add peer to profile list if they're not in it - for profile in self.peerProfiles: - if profile.address == address: - break - else: - self.peerProfiles.append(onionrpeers.PeerProfiles(address, self._core)) - break - else: - tried.append(address) - logger.debug('Failed to connect to ' + address) - return retData - - def removeOnlinePeer(self, peer): - '''Remove an online peer''' - try: - del self.connectTimes[peer] - except KeyError: - pass - try: - del self.dbTimestamps[peer] - except KeyError: - pass - try: - self.onlinePeers.remove(peer) - except ValueError: - pass - - def peerCleanup(self): - '''This just calls onionrpeers.cleanupPeers, which removes dead or bad peers (offline too long, too slow)''' - onionrpeers.peerCleanup(self._core) - self.decrementThreadCount('peerCleanup') - - def printOnlinePeers(self): - '''logs online peer list''' - if len(self.onlinePeers) == 0: - logger.warn('No online peers') - else: - logger.info('Online peers:') - for i in self.onlinePeers: - score = str(self.getPeerProfileInstance(i).score) - logger.info(i + ', score: ' + score) - - def peerAction(self, peer, action, data='', returnHeaders=False): - '''Perform a get request to a peer''' - if len(peer) == 0: - return False - #logger.debug('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort)) - url = 'http://%s/%s' % (peer, action) - if len(data) > 0: - url += '&data=' + data - - self._core.setAddressInfo(peer, 'lastConnectAttempt', self._core._utils.getEpoch()) # mark the time we're trying to request this peer - - retData = self._core._utils.doGetRequest(url, port=self.proxyPort) - # if request failed, (error), mark peer offline - if retData == False: - try: - self.getPeerProfileInstance(peer).addScore(-10) - self.removeOnlinePeer(peer) - if action != 'ping': - self.getOnlinePeers() # Will only add a new peer to pool if needed - except ValueError: - pass - else: - self._core.setAddressInfo(peer, 'lastConnect', self._core._utils.getEpoch()) - self.getPeerProfileInstance(peer).addScore(1) - return retData # If returnHeaders, returns tuple of data, headers. if not, just data string - - def getPeerProfileInstance(self, peer): - '''Gets a peer profile instance from the list of profiles, by address name''' - for i in self.peerProfiles: - # if the peer's profile is already loaded, return that - if i.address == peer: - retData = i - break - else: - # if the peer's profile is not loaded, return a new one. connectNewPeer adds it the list on connect - retData = onionrpeers.PeerProfiles(peer, self._core) - return retData - - def getUptime(self): - return self._core._utils.getEpoch() - self.startTime - - def heartbeat(self): - '''Show a heartbeat debug message''' - logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(self.getUptime())) - self.decrementThreadCount('heartbeat') - - def daemonCommands(self): - ''' - Process daemon commands from daemonQueue - ''' - cmd = self._core.daemonQueue() - response = '' - if cmd is not False: - events.event('daemon_command', onionr = None, data = {'cmd' : cmd}) - if cmd[0] == 'shutdown': - self.shutdown = True - elif cmd[0] == 'announceNode': - if len(self.onlinePeers) > 0: - self.announce(cmd[1]) - else: - logger.debug("No nodes connected. Will not introduce node.") - elif cmd[0] == 'runCheck': # deprecated - logger.debug('Status check; looks good.') - open(self._core.dataDir + '.runcheck', 'w+').close() - elif cmd[0] == 'connectedPeers': - response = '\n'.join(list(self.onlinePeers)).strip() - if response == '': - response = 'none' - elif cmd[0] == 'localCommand': - response = self._core._utils.localCommand(cmd[1]) - elif cmd[0] == 'pex': - for i in self.timers: - if i.timerFunction.__name__ == 'lookupAdders': - i.count = (i.frequency - 1) - elif cmd[0] == 'uploadBlock': - self.blocksToUpload.append(cmd[1]) - elif cmd[0] == 'startSocket': - # Create our own socket server - socketInfo = json.loads(cmd[1]) - socketInfo['id'] = uuid.uuid4() - self._core.startSocket = socketInfo - elif cmd[0] == 'addSocket': - # Socket server was created for us - socketInfo = json.loads(cmd[1]) - peer = socketInfo['peer'] - reason = socketInfo['reason'] - threading.Thread(target=self.socketClient.startSocket, args=(peer, reason)).start() - else: - logger.info('Recieved daemonQueue command:' + cmd[0]) - - if cmd[0] not in ('', None): - if response != '': - self._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) - response = '' - - self.decrementThreadCount('daemonCommands') - - def uploadBlock(self): - '''Upload our block to a few peers''' - # when inserting a block, we try to upload it to a few peers to add some deniability - triedPeers = [] - finishedUploads = [] - self.blocksToUpload = self._core._crypto.randomShuffle(self.blocksToUpload) - if len(self.blocksToUpload) != 0: - for bl in self.blocksToUpload: - if not self._core._utils.validateHash(bl): - logger.warn('Requested to upload invalid block') - self.decrementThreadCount('uploadBlock') - return - for i in range(min(len(self.onlinePeers), 6)): - peer = self.pickOnlinePeer() - if peer in triedPeers: - continue - triedPeers.append(peer) - url = 'http://' + peer + '/upload' - data = {'block': block.Block(bl).getRaw()} - proxyType = proxypicker.pick_proxy(peer) - logger.info("Uploading block to " + peer) - if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: - self._core._utils.localCommand('waitforshare/' + bl, post=True) - finishedUploads.append(bl) - for x in finishedUploads: - try: - self.blocksToUpload.remove(x) - except ValueError: - pass - self.decrementThreadCount('uploadBlock') - - def announce(self, peer): - '''Announce to peers our address''' - if self.daemonTools.announceNode() == False: - logger.warn('Could not introduce node.') - - def detectAPICrash(self): - '''exit if the api server crashes/stops''' - if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): - for i in range(8): - if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown: - break # break for loop - time.sleep(1) - else: - # This executes if the api is NOT detected to be running - events.event('daemon_crash', onionr = None, data = {}) - logger.error('Daemon detected API crash (or otherwise unable to reach API after long time), stopping...') - self.shutdown = True - self.decrementThreadCount('detectAPICrash') - - def runCheck(self): - if self.daemonTools.runCheck(): - logger.debug('Status check; looks good.') - - self.decrementThreadCount('runCheck') - -def startCommunicator(onionrInst, proxyPort): - OnionrCommunicatorDaemon(onionrInst, proxyPort) \ No newline at end of file diff --git a/onionr/communicator/__init__.py b/onionr/communicator/__init__.py new file mode 100755 index 00000000..ea471bf1 --- /dev/null +++ b/onionr/communicator/__init__.py @@ -0,0 +1,254 @@ +''' + Onionr - Private P2P Communication + + This file contains both the OnionrCommunicate class for communcating with peers + and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue) +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys, os, time +import core, config, logger, onionr +import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block +from . import onlinepeers +from communicatorutils import servicecreator, onionrcommunicatortimers +from communicatorutils import downloadblocks, lookupblocks, lookupadders +from communicatorutils import servicecreator, connectnewpeers, uploadblocks +from communicatorutils import daemonqueuehandler, announcenode, deniableinserts +from communicatorutils import cooldownpeer, housekeeping, netcheck +from onionrutils import localcommand, epoch +from etc import humanreadabletime +import onionrservices, onionr, onionrproofs +OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers + +config.reload() +class OnionrCommunicatorDaemon: + def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): + onionrInst.communicatorInst = self + # configure logger and stuff + onionr.Onionr.setupConfig('data/', self = self) + self.proxyPort = proxyPort + + self.isOnline = True # Assume we're connected to the internet + + # list of timer instances + self.timers = [] + + # initialize core with Tor socks port being 3rd argument + self.proxyPort = proxyPort + self._core = onionrInst.onionrCore + + self.blocksToUpload = [] + + # loop time.sleep delay in seconds + self.delay = 1 + + # lists of connected peers and peers we know we can't reach currently + self.onlinePeers = [] + self.offlinePeers = [] + self.cooldownPeer = {} + self.connectTimes = {} + self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) + self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable + self.announceProgress = {} + self.announceCache = {} + + # amount of threads running by name, used to prevent too many + self.threadCounts = {} + + # set true when shutdown command received + self.shutdown = False + + # list of new blocks to download, added to when new block lists are fetched from peers + self.blockQueue = {} + + # list of blocks currently downloading, avoid s + self.currentDownloading = [] + + # timestamp when the last online node was seen + self.lastNodeSeen = None + + # Dict of time stamps for peer's block list lookup times, to avoid downloading full lists all the time + self.dbTimestamps = {} + + # Clear the daemon queue for any dead messages + if os.path.exists(self._core.queueDB): + self._core.clearDaemonQueue() + + # Loads in and starts the enabled plugins + plugins.reload() + + # time app started running for info/statistics purposes + self.startTime = epoch.get_epoch() + + if developmentMode: + OnionrCommunicatorTimers(self, self.heartbeat, 30) + + # Set timers, function reference, seconds + # requiresPeer True means the timer function won't fire if we have no connected peers + peerPoolTimer = OnionrCommunicatorTimers(self, onlinepeers.get_online_peers, 60, maxThreads=1, myArgs=[self]) + OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1) + + # Timers to periodically lookup new blocks and download them + OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks', 25), requiresPeer=True, maxThreads=1) + OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks', 30), requiresPeer=True, maxThreads=2) + + # Timer to reset the longest offline peer so contact can be attempted again + OnionrCommunicatorTimers(self, onlinepeers.clear_offline_peer, 58, myArgs=[self]) + + # Timer to cleanup old blocks + blockCleanupTimer = OnionrCommunicatorTimers(self, housekeeping.clean_old_blocks, 65, myArgs=[self]) + + # Timer to discover new peers + OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) + + # Timer for adjusting which peers we actively communicate to at any given time, to avoid over-using peers + OnionrCommunicatorTimers(self, cooldownpeer.cooldown_peer, 30, myArgs=[self], requiresPeer=True) + + # Timer to read the upload queue and upload the entries to peers + OnionrCommunicatorTimers(self, uploadblocks.upload_blocks_from_communicator, 5, myArgs=[self], requiresPeer=True, maxThreads=1) + + # Timer to process the daemon command queue + OnionrCommunicatorTimers(self, daemonqueuehandler.handle_daemon_commands, 6, myArgs=[self], maxThreads=3) + + # Setup direct connections + if config.get('general.socket_servers', False): + self.services = onionrservices.OnionrServices(self._core) + self.active_services = [] + self.service_greenlets = [] + OnionrCommunicatorTimers(self, servicecreator.service_creator, 5, maxThreads=50, myArgs=[self]) + else: + self.services = None + + # This timer creates deniable blocks, in an attempt to further obfuscate block insertion metadata + if config.get('general.insert_deniable_blocks', True): + deniableBlockTimer = OnionrCommunicatorTimers(self, deniableinserts.insert_deniable_block, 180, myArgs=[self], requiresPeer=True, maxThreads=1) + deniableBlockTimer.count = (deniableBlockTimer.frequency - 175) + + # Timer to check for connectivity, through Tor to various high-profile onion services + netCheckTimer = OnionrCommunicatorTimers(self, netcheck.net_check, 600, myArgs=[self]) + + # Announce the public API server transport address to other nodes if security level allows + if config.get('general.security_level', 1) == 0 and config.get('general.announce_node', True): + # Default to high security level incase config breaks + announceTimer = OnionrCommunicatorTimers(self, announcenode.announce_node, 3600, myArgs=[self], requiresPeer=True, maxThreads=1) + announceTimer.count = (announceTimer.frequency - 120) + else: + logger.debug('Will not announce node.') + + # Timer to delete malfunctioning or long-dead peers + cleanupTimer = OnionrCommunicatorTimers(self, self.peerCleanup, 300, requiresPeer=True) + + # Timer to cleanup dead ephemeral forward secrecy keys + forwardSecrecyTimer = OnionrCommunicatorTimers(self, housekeeping.clean_keys, 15, myArgs=[self], maxThreads=1) + + # Adjust initial timer triggers + peerPoolTimer.count = (peerPoolTimer.frequency - 1) + cleanupTimer.count = (cleanupTimer.frequency - 60) + blockCleanupTimer.count = (blockCleanupTimer.frequency - 5) + + # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking + try: + while not self.shutdown: + for i in self.timers: + if self.shutdown: + break + i.processTimer() + time.sleep(self.delay) + # Debug to print out used FDs (regular and net) + #proc = psutil.Process() + #print(proc.open_files(), len(psutil.net_connections())) + except KeyboardInterrupt: + self.shutdown = True + pass + + logger.info('Goodbye. (Onionr is cleaning up, and will exit)', terminal=True) + try: + self.service_greenlets + except AttributeError: + pass + else: + for server in self.service_greenlets: + server.stop() + localcommand.local_command(self._core, 'shutdown') # shutdown the api + time.sleep(0.5) + + def lookupAdders(self): + '''Lookup new peer addresses''' + lookupadders.lookup_new_peer_transports_with_communicator(self) + + def lookupBlocks(self): + '''Lookup new blocks & add them to download queue''' + lookupblocks.lookup_blocks_from_communicator(self) + + def getBlocks(self): + '''download new blocks in queue''' + downloadblocks.download_blocks_from_communicator(self) + + def decrementThreadCount(self, threadName): + '''Decrement amount of a thread name if more than zero, called when a function meant to be run in a thread ends''' + try: + if self.threadCounts[threadName] > 0: + self.threadCounts[threadName] -= 1 + except KeyError: + pass + + def connectNewPeer(self, peer='', useBootstrap=False): + '''Adds a new random online peer to self.onlinePeers''' + connectnewpeers.connect_new_peer_to_communicator(self, peer, useBootstrap) + + def peerCleanup(self): + '''This just calls onionrpeers.cleanupPeers, which removes dead or bad peers (offline too long, too slow)''' + onionrpeers.peer_cleanup(self._core) + self.decrementThreadCount('peerCleanup') + + def getPeerProfileInstance(self, peer): + '''Gets a peer profile instance from the list of profiles, by address name''' + for i in self.peerProfiles: + # if the peer's profile is already loaded, return that + if i.address == peer: + retData = i + break + else: + # if the peer's profile is not loaded, return a new one. connectNewPeer adds it the list on connect + retData = onionrpeers.PeerProfiles(peer, self._core) + return retData + + def getUptime(self): + return epoch.get_epoch() - self.startTime + + def heartbeat(self): + '''Show a heartbeat debug message''' + logger.debug('Heartbeat. Node running for %s.' % humanreadabletime.human_readable_time(self.getUptime())) + self.decrementThreadCount('heartbeat') + + def announce(self, peer): + '''Announce to peers our address''' + if announcenode.announce_node(self) == False: + logger.warn('Could not introduce node.', terminal=True) + + def runCheck(self): + if run_file_exists(self): + logger.debug('Status check; looks good.') + + self.decrementThreadCount('runCheck') + +def startCommunicator(onionrInst, proxyPort): + OnionrCommunicatorDaemon(onionrInst, proxyPort) + +def run_file_exists(daemon): + if os.path.isfile(daemon._core.dataDir + '.runcheck'): + os.remove(daemon._core.dataDir + '.runcheck') + return True + return False \ No newline at end of file diff --git a/onionr/communicator/bootstrappeers.py b/onionr/communicator/bootstrappeers.py new file mode 100644 index 00000000..d57afd56 --- /dev/null +++ b/onionr/communicator/bootstrappeers.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + add bootstrap peers to the communicator peer list +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def add_bootstrap_list_to_peer_list(comm_inst, peerList): + ''' + Add the bootstrap list to the peer list (no duplicates) + ''' + for i in comm_inst._core.bootstrapList: + if i not in peerList and i not in comm_inst.offlinePeers and i != comm_inst._core.hsAddress and len(str(i).strip()) > 0: + peerList.append(i) + comm_inst._core.addAddress(i) \ No newline at end of file diff --git a/onionr/communicator/onlinepeers/__init__.py b/onionr/communicator/onlinepeers/__init__.py new file mode 100644 index 00000000..a3b107ee --- /dev/null +++ b/onionr/communicator/onlinepeers/__init__.py @@ -0,0 +1,6 @@ +from . import clearofflinepeer, onlinepeers, pickonlinepeers, removeonlinepeer + +clear_offline_peer = clearofflinepeer.clear_offline_peer +get_online_peers = onlinepeers.get_online_peers +pick_online_peer = pickonlinepeers.pick_online_peer +remove_online_peer = removeonlinepeer.remove_online_peer \ No newline at end of file diff --git a/onionr/communicator/onlinepeers/clearofflinepeer.py b/onionr/communicator/onlinepeers/clearofflinepeer.py new file mode 100644 index 00000000..31507878 --- /dev/null +++ b/onionr/communicator/onlinepeers/clearofflinepeer.py @@ -0,0 +1,29 @@ +''' + Onionr - Private P2P Communication + + clear offline peer in a communicator instance +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger +def clear_offline_peer(comm_inst): + '''Removes the longest offline peer to retry later''' + try: + removed = comm_inst.offlinePeers.pop(0) + except IndexError: + pass + else: + logger.debug('Removed ' + removed + ' from offline list, will try them again.') + comm_inst.decrementThreadCount('clear_offline_peer') \ No newline at end of file diff --git a/onionr/communicator/onlinepeers/onlinepeers.py b/onionr/communicator/onlinepeers/onlinepeers.py new file mode 100644 index 00000000..2eaa3d9d --- /dev/null +++ b/onionr/communicator/onlinepeers/onlinepeers.py @@ -0,0 +1,45 @@ +''' + Onionr - Private P2P Communication + + get online peers in a communicator instance +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time +from etc import humanreadabletime +import logger +def get_online_peers(comm_inst): + ''' + Manages the comm_inst.onlinePeers attribute list, connects to more peers if we have none connected + ''' + config = comm_inst._core.config + logger.debug('Refreshing peer pool...') + maxPeers = int(config.get('peers.max_connect', 10)) + needed = maxPeers - len(comm_inst.onlinePeers) + + for i in range(needed): + if len(comm_inst.onlinePeers) == 0: + comm_inst.connectNewPeer(useBootstrap=True) + else: + comm_inst.connectNewPeer() + + if comm_inst.shutdown: + break + else: + if len(comm_inst.onlinePeers) == 0: + logger.debug('Couldn\'t connect to any peers.' + (' Last node seen %s ago.' % humanreadabletime.human_readable_time(time.time() - comm_inst.lastNodeSeen) if not comm_inst.lastNodeSeen is None else ''), terminal=True) + else: + comm_inst.lastNodeSeen = time.time() + comm_inst.decrementThreadCount('get_online_peers') \ No newline at end of file diff --git a/onionr/communicator/onlinepeers/pickonlinepeers.py b/onionr/communicator/onlinepeers/pickonlinepeers.py new file mode 100644 index 00000000..3a6ac0aa --- /dev/null +++ b/onionr/communicator/onlinepeers/pickonlinepeers.py @@ -0,0 +1,34 @@ +''' + Onionr - Private P2P Communication + + pick online peers in a communicator instance +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def pick_online_peer(comm_inst): + '''randomly picks peer from pool without bias (using secrets module)''' + retData = '' + while True: + peerLength = len(comm_inst.onlinePeers) + if peerLength <= 0: + break + try: + # get a random online peer, securely. May get stuck in loop if network is lost or if all peers in pool magically disconnect at once + retData = comm_inst.onlinePeers[comm_inst._core._crypto.secrets.randbelow(peerLength)] + except IndexError: + pass + else: + break + return retData \ No newline at end of file diff --git a/onionr/communicator/onlinepeers/removeonlinepeer.py b/onionr/communicator/onlinepeers/removeonlinepeer.py new file mode 100644 index 00000000..fed600ab --- /dev/null +++ b/onionr/communicator/onlinepeers/removeonlinepeer.py @@ -0,0 +1,33 @@ +''' + Onionr - Private P2P Communication + + remove an online peer from the pool in a communicator instance +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def remove_online_peer(comm_inst, peer): + '''Remove an online peer''' + try: + del comm_inst.connectTimes[peer] + except KeyError: + pass + try: + del comm_inst.dbTimestamps[peer] + except KeyError: + pass + try: + comm_inst.onlinePeers.remove(peer) + except ValueError: + pass \ No newline at end of file diff --git a/onionr/communicator/peeraction.py b/onionr/communicator/peeraction.py new file mode 100644 index 00000000..e68f88d4 --- /dev/null +++ b/onionr/communicator/peeraction.py @@ -0,0 +1,53 @@ +''' + Onionr - Private P2P Communication + + This file implements logic for performing requests to Onionr peers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import streamedrequests +import logger +from onionrutils import epoch, basicrequests +from . import onlinepeers +def peer_action(comm_inst, peer, action, data='', returnHeaders=False, max_resp_size=5242880): + '''Perform a get request to a peer''' + penalty_score = -10 + if len(peer) == 0: + return False + url = 'http://%s/%s' % (peer, action) + if len(data) > 0: + url += '&data=' + data + + comm_inst._core.setAddressInfo(peer, 'lastConnectAttempt', epoch.get_epoch()) # mark the time we're trying to request this peer + try: + retData = basicrequests.do_get_request(comm_inst._core, url, port=comm_inst.proxyPort, max_size=max_resp_size) + except streamedrequests.exceptions.ResponseLimitReached: + logger.warn('Request failed due to max response size being overflowed', terminal=True) + retData = False + penalty_score = -100 + # if request failed, (error), mark peer offline + if retData == False: + try: + comm_inst.getPeerProfileInstance(peer).addScore(penalty_score) + onlinepeers.remove_online_peer(comm_inst, peer) + if action != 'ping' and not comm_inst.shutdown: + logger.warn('Lost connection to ' + peer, terminal=True) + onlinepeers.get_online_peers(comm_inst) # Will only add a new peer to pool if needed + except ValueError: + pass + else: + comm_inst._core.setAddressInfo(peer, 'lastConnect', epoch.get_epoch()) + comm_inst.getPeerProfileInstance(peer).addScore(1) + return retData # If returnHeaders, returns tuple of data, headers. if not, just data string diff --git a/onionr/communicatorutils/README.md b/onionr/communicatorutils/README.md new file mode 100755 index 00000000..393328c3 --- /dev/null +++ b/onionr/communicatorutils/README.md @@ -0,0 +1,33 @@ +# communicatorutils + +The files in this submodule handle various subtasks and utilities for the onionr communicator. + +## Files: + +announcenode.py: Uses a communicator instance to announce our transport address to connected nodes + +connectnewpeers.py: takes a communicator instance and has it connect to as many peers as needed, and/or to a new specified peer. + +cooldownpeer.py: randomly selects a connected peer in a communicator and disconnects them for the purpose of security and network balancing. + +daemonqueuehandler.py: checks for new commands in the daemon queue and processes them accordingly. + +deniableinserts.py: insert fake blocks with the communicator for plausible deniability + +downloadblocks.py: iterates a communicator instance's block download queue and attempts to download the blocks from online peers + +housekeeping.py: cleans old blocks and forward secrecy keys + +lookupadders.py: ask connected peers to share their list of peer transport addresses + +lookupblocks.py: lookup new blocks from connected peers from the communicator + +netcheck.py: check if the node is online based on communicator status and onion server ping results + +onionrcommunicataortimers.py: create a timer for a function to be launched on an interval. Control how many possible instances of a timer may be running a function at once and control if the timer should be ran in a thread or not. + +proxypicker.py: returns a string name for the appropriate proxy to be used with a particular peer transport address. + +servicecreator.py: iterate connection blocks and create new direct connection servers for them. + +uploadblocks.py: iterate a communicator's upload queue and upload the blocks to connected peers \ No newline at end of file diff --git a/onionr/communicatorutils/__init__.py b/onionr/communicatorutils/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/onionr/communicatorutils/announcenode.py b/onionr/communicatorutils/announcenode.py new file mode 100755 index 00000000..8b85c177 --- /dev/null +++ b/onionr/communicatorutils/announcenode.py @@ -0,0 +1,86 @@ +''' + Onionr - Private P2P Communication + + Use a communicator instance to announce our transport address to connected nodes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64 +import onionrproofs, logger +from etc import onionrvalues +from onionrutils import basicrequests, bytesconverter +from communicator import onlinepeers + +def announce_node(daemon): + '''Announce our node to our peers''' + ov = onionrvalues.OnionrValues() + retData = False + announceFail = False + + # Do not let announceCache get too large + if len(daemon.announceCache) >= 10000: + daemon.announceCache.popitem() + + if daemon._core.config.get('general.security_level', 0) == 0: + # Announce to random online peers + for i in daemon.onlinePeers: + if not i in daemon.announceCache and not i in daemon.announceProgress: + peer = i + break + else: + peer = onlinepeers.pick_online_peer(daemon) + + for x in range(1): + if x == 1 and daemon._core.config.get('i2p.host'): + ourID = daemon._core.config.get('i2p.own_addr').strip() + else: + ourID = daemon._core.hsAddress.strip() + + url = 'http://' + peer + '/announce' + data = {'node': ourID} + + combinedNodes = ourID + peer + if ourID != 1: + #TODO: Extend existingRand for i2p + existingRand = bytesconverter.bytes_to_str(daemon._core.getAddressInfo(peer, 'powValue')) + # Reset existingRand if it no longer meets the minimum POW + if type(existingRand) is type(None) or not existingRand.endswith('0' * ov.announce_pow): + existingRand = '' + + if peer in daemon.announceCache: + data['random'] = daemon.announceCache[peer] + elif len(existingRand) > 0: + data['random'] = existingRand + else: + daemon.announceProgress[peer] = True + proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=ov.announce_pow) + del daemon.announceProgress[peer] + try: + data['random'] = base64.b64encode(proof.waitForResult()[1]) + except TypeError: + # Happens when we failed to produce a proof + logger.error("Failed to produce a pow for announcing to " + peer) + announceFail = True + else: + daemon.announceCache[peer] = data['random'] + if not announceFail: + logger.info('Announcing node to ' + url) + if basicrequests.do_post_request(daemon._core, url, data) == 'Success': + logger.info('Successfully introduced node to ' + peer, terminal=True) + retData = True + daemon._core.setAddressInfo(peer, 'introduced', 1) + daemon._core.setAddressInfo(peer, 'powValue', data['random']) + daemon.decrementThreadCount('announce_node') + return retData \ No newline at end of file diff --git a/onionr/communicatorutils/connectnewpeers.py b/onionr/communicatorutils/connectnewpeers.py new file mode 100755 index 00000000..35cd3000 --- /dev/null +++ b/onionr/communicatorutils/connectnewpeers.py @@ -0,0 +1,89 @@ +''' + Onionr - Private P2P Communication + + Connect a new peer to our communicator instance. Does so randomly if no peer is specified +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time, sys, secrets +import onionrexceptions, logger, onionrpeers +from utils import networkmerger +from onionrutils import stringvalidators, epoch +from communicator import peeraction, bootstrappeers + +def connect_new_peer_to_communicator(comm_inst, peer='', useBootstrap=False): + config = comm_inst._core.config + retData = False + tried = comm_inst.offlinePeers + if peer != '': + if stringvalidators.validate_transport(peer): + peerList = [peer] + else: + raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') + else: + peerList = comm_inst._core.listAdders() + + mainPeerList = comm_inst._core.listAdders() + peerList = onionrpeers.get_score_sorted_peer_list(comm_inst._core) + + # If we don't have enough peers connected or random chance, select new peers to try + if len(peerList) < 8 or secrets.randbelow(4) == 3: + tryingNew = [] + for x in comm_inst.newPeers: + if x not in peerList: + peerList.append(x) + tryingNew.append(x) + for i in tryingNew: + comm_inst.newPeers.remove(i) + + if len(peerList) == 0 or useBootstrap: + # Avoid duplicating bootstrap addresses in peerList + bootstrappeers.add_bootstrap_list_to_peer_list(comm_inst, peerList) + + for address in peerList: + if not config.get('tor.v3onions') and len(address) == 62: + continue + # Don't connect to our own address + if address == comm_inst._core.hsAddress: + continue + # Don't connect to invalid address or if its already been tried/connected, or if its cooled down + if len(address) == 0 or address in tried or address in comm_inst.onlinePeers or address in comm_inst.cooldownPeer: + continue + if comm_inst.shutdown: + return + # Ping a peer, + if peeraction.peer_action(comm_inst, address, 'ping') == 'pong!': + time.sleep(0.1) + if address not in mainPeerList: + # Add a peer to our list if it isn't already since it successfully connected + networkmerger.mergeAdders(address, comm_inst._core) + if address not in comm_inst.onlinePeers: + logger.info('Connected to ' + address, terminal=True) + comm_inst.onlinePeers.append(address) + comm_inst.connectTimes[address] = epoch.get_epoch() + retData = address + + # add peer to profile list if they're not in it + for profile in comm_inst.peerProfiles: + if profile.address == address: + break + else: + comm_inst.peerProfiles.append(onionrpeers.PeerProfiles(address, comm_inst._core)) + break + else: + # Mark a peer as tried if they failed to respond to ping + tried.append(address) + logger.debug('Failed to connect to ' + address) + return retData \ No newline at end of file diff --git a/onionr/communicatorutils/cooldownpeer.py b/onionr/communicatorutils/cooldownpeer.py new file mode 100755 index 00000000..326af3a5 --- /dev/null +++ b/onionr/communicatorutils/cooldownpeer.py @@ -0,0 +1,53 @@ +''' + Onionr - Private P2P Communication + + Select a random online peer in a communicator instance and have them "cool down" +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from onionrutils import epoch +from communicator import onlinepeers +def cooldown_peer(comm_inst): + '''Randomly add an online peer to cooldown, so we can connect a new one''' + onlinePeerAmount = len(comm_inst.onlinePeers) + minTime = 300 + cooldownTime = 600 + toCool = '' + tempConnectTimes = dict(comm_inst.connectTimes) + + # Remove peers from cooldown that have been there long enough + tempCooldown = dict(comm_inst.cooldownPeer) + for peer in tempCooldown: + if (epoch.get_epoch() - tempCooldown[peer]) >= cooldownTime: + del comm_inst.cooldownPeer[peer] + + # Cool down a peer, if we have max connections alive for long enough + if onlinePeerAmount >= comm_inst._core.config.get('peers.max_connect', 10, save = True): + finding = True + + while finding: + try: + toCool = min(tempConnectTimes, key=tempConnectTimes.get) + if (epoch.get_epoch() - tempConnectTimes[toCool]) < minTime: + del tempConnectTimes[toCool] + else: + finding = False + except ValueError: + break + else: + onlinepeers.remove_online_peer(comm_inst, toCool) + comm_inst.cooldownPeer[toCool] = epoch.get_epoch() + + comm_inst.decrementThreadCount('cooldown_peer') \ No newline at end of file diff --git a/onionr/communicatorutils/daemonqueuehandler.py b/onionr/communicatorutils/daemonqueuehandler.py new file mode 100755 index 00000000..7c922aa1 --- /dev/null +++ b/onionr/communicatorutils/daemonqueuehandler.py @@ -0,0 +1,56 @@ +''' + Onionr - P2P Anonymous Storage Network + + Handle daemon queue commands in the communicator +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger +import onionrevents as events +from onionrutils import localcommand +def handle_daemon_commands(comm_inst): + cmd = comm_inst._core.daemonQueue() + response = '' + if cmd is not False: + events.event('daemon_command', onionr = comm_inst._core.onionrInst, data = {'cmd' : cmd}) + if cmd[0] == 'shutdown': + comm_inst.shutdown = True + elif cmd[0] == 'announceNode': + if len(comm_inst.onlinePeers) > 0: + comm_inst.announce(cmd[1]) + else: + logger.debug("No nodes connected. Will not introduce node.") + elif cmd[0] == 'runCheck': # deprecated + logger.debug('Status check; looks good.') + open(comm_inst._core.dataDir + '.runcheck', 'w+').close() + elif cmd[0] == 'connectedPeers': + response = '\n'.join(list(comm_inst.onlinePeers)).strip() + if response == '': + response = 'none' + elif cmd[0] == 'localCommand': + response = localcommand.local_command(comm_inst._core, cmd[1]) + elif cmd[0] == 'pex': + for i in comm_inst.timers: + if i.timerFunction.__name__ == 'lookupAdders': + i.count = (i.frequency - 1) + elif cmd[0] == 'uploadBlock': + comm_inst.blocksToUpload.append(cmd[1]) + + if cmd[0] not in ('', None): + if response != '': + localcommand.local_command(comm_inst._core, 'queueResponseAdd/' + cmd[4], post=True, postData={'data': response}) + response = '' + + comm_inst.decrementThreadCount('handle_daemon_commands') \ No newline at end of file diff --git a/onionr/communicatorutils/deniableinserts.py b/onionr/communicatorutils/deniableinserts.py new file mode 100755 index 00000000..571a6093 --- /dev/null +++ b/onionr/communicatorutils/deniableinserts.py @@ -0,0 +1,31 @@ +''' + Onionr - Private P2P Communication + + Use the communicator to insert fake mail messages +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import secrets +from etc import onionrvalues +def insert_deniable_block(comm_inst): + '''Insert a fake block in order to make it more difficult to track real blocks''' + fakePeer = '' + chance = 10 + if secrets.randbelow(chance) == (chance - 1): + # This assumes on the libsodium primitives to have key-privacy + fakePeer = onionrvalues.DENIABLE_PEER_ADDRESS + data = secrets.token_hex(secrets.randbelow(1024) + 1) + comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, disableForward=True, meta={'subject': 'foo'}) + comm_inst.decrementThreadCount('insert_deniable_block') \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks/__init__.py b/onionr/communicatorutils/downloadblocks/__init__.py new file mode 100755 index 00000000..46732728 --- /dev/null +++ b/onionr/communicatorutils/downloadblocks/__init__.py @@ -0,0 +1,116 @@ +''' + Onionr - Private P2P Communication + + Download blocks using the communicator instance +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import communicator, onionrexceptions +import logger, onionrpeers +from onionrutils import blockmetadata, stringvalidators, validatemetadata +from . import shoulddownload +from communicator import peeraction, onlinepeers + +def download_blocks_from_communicator(comm_inst): + assert isinstance(comm_inst, communicator.OnionrCommunicatorDaemon) + for blockHash in list(comm_inst.blockQueue): + if len(comm_inst.onlinePeers) == 0: + break + triedQueuePeers = [] # List of peers we've tried for a block + try: + blockPeers = list(comm_inst.blockQueue[blockHash]) + except KeyError: + blockPeers = [] + removeFromQueue = True + + if not shoulddownload.should_download(comm_inst, blockHash): + continue + + if comm_inst.shutdown or not comm_inst.isOnline or comm_inst._core.storage_counter.isFull(): + # Exit loop if shutting down or offline, or disk allocation reached + break + # Do not download blocks being downloaded + if blockHash in comm_inst.currentDownloading: + #logger.debug('Already downloading block %s...' % blockHash) + continue + + comm_inst.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block + if len(blockPeers) == 0: + peerUsed = onlinepeers.pick_online_peer(comm_inst) + else: + blockPeers = comm_inst._core._crypto.randomShuffle(blockPeers) + peerUsed = blockPeers.pop(0) + + if not comm_inst.shutdown and peerUsed.strip() != '': + logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed)) + content = peeraction.peer_action(comm_inst, peerUsed, 'getdata/' + blockHash, max_resp_size=3000000) # block content from random peer (includes metadata) + if content != False and len(content) > 0: + try: + content = content.encode() + except AttributeError: + pass + + realHash = comm_inst._core._crypto.sha3Hash(content) + try: + realHash = realHash.decode() # bytes on some versions for some reason + except AttributeError: + pass + if realHash == blockHash: + content = content.decode() # decode here because sha3Hash needs bytes above + metas = blockmetadata.get_block_metadata_from_data(content) # returns tuple(metadata, meta), meta is also in metadata + metadata = metas[0] + if validatemetadata.validate_metadata(comm_inst._core, metadata, metas[2]): # check if metadata is valid, and verify nonce + if comm_inst._core._crypto.verifyPow(content): # check if POW is enough/correct + logger.info('Attempting to save block %s...' % blockHash[:12]) + try: + comm_inst._core.setData(content) + except onionrexceptions.DataExists: + logger.warn('Data is already set for %s ' % (blockHash,)) + except onionrexceptions.DiskAllocationReached: + logger.error('Reached disk allocation allowance, cannot save block %s.' % (blockHash,)) + removeFromQueue = False + else: + comm_inst._core.addToBlockDB(blockHash, dataSaved=True) + blockmetadata.process_block_metadata(comm_inst._core, blockHash) # caches block metadata values to block database + else: + logger.warn('POW failed for block %s.' % (blockHash,)) + else: + if comm_inst._core._blacklist.inBlacklist(realHash): + logger.warn('Block %s is blacklisted.' % (realHash,)) + else: + logger.warn('Metadata for block %s is invalid.' % (blockHash,)) + comm_inst._core._blacklist.addToDB(blockHash) + else: + # if block didn't meet expected hash + tempHash = comm_inst._core._crypto.sha3Hash(content) # lazy hack, TODO use var + try: + tempHash = tempHash.decode() + except AttributeError: + pass + # Punish peer for sharing invalid block (not always malicious, but is bad regardless) + onionrpeers.PeerProfiles(peerUsed, comm_inst._core).addScore(-50) + if tempHash != 'ed55e34cb828232d6c14da0479709bfa10a0923dca2b380496e6b2ed4f7a0253': + # Dumb hack for 404 response from peer. Don't log it if 404 since its likely not malicious or a critical error. + logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) + else: + removeFromQueue = False # Don't remove from queue if 404 + if removeFromQueue: + try: + del comm_inst.blockQueue[blockHash] # remove from block queue both if success or false + logger.info('%s blocks remaining in queue' % [len(comm_inst.blockQueue)], terminal=True) + except KeyError: + pass + comm_inst.currentDownloading.remove(blockHash) + comm_inst.decrementThreadCount('getBlocks') \ No newline at end of file diff --git a/onionr/communicatorutils/downloadblocks/shoulddownload.py b/onionr/communicatorutils/downloadblocks/shoulddownload.py new file mode 100644 index 00000000..d126f548 --- /dev/null +++ b/onionr/communicatorutils/downloadblocks/shoulddownload.py @@ -0,0 +1,33 @@ +''' + Onionr - Private P2P Communication + + Check if a block should be downloaded (if we already have it or its blacklisted or not) +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def should_download(comm_inst, block_hash): + ret_data = True + if block_hash in comm_inst._core.getBlockList(): # Dont download block we have + ret_data = False + else: + if comm_inst._core._blacklist.inBlacklist(block_hash): # Dont download blacklisted block + ret_data = False + if ret_data is False: + # Remove block from communicator queue if it shouldnt be downloaded + try: + del comm_inst.blockQueue[block_hash] + except KeyError: + pass + return ret_data \ No newline at end of file diff --git a/onionr/communicatorutils/housekeeping.py b/onionr/communicatorutils/housekeeping.py new file mode 100755 index 00000000..3071a994 --- /dev/null +++ b/onionr/communicatorutils/housekeeping.py @@ -0,0 +1,60 @@ +''' + Onionr - Private P2P Communication + + Cleanup old Onionr blocks and forward secrecy keys using the communicator. Ran from a timer usually +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +import logger +from onionrusers import onionrusers +from onionrutils import epoch +def clean_old_blocks(comm_inst): + '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' + + # Delete expired blocks + for bHash in comm_inst._core.getExpiredBlocks(): + comm_inst._core._blacklist.addToDB(bHash) + comm_inst._core.removeBlock(bHash) + logger.info('Deleted block: %s' % (bHash,)) + + while comm_inst._core.storage_counter.isFull(): + oldest = comm_inst._core.getBlockList()[0] + comm_inst._core._blacklist.addToDB(oldest) + comm_inst._core.removeBlock(oldest) + logger.info('Deleted block: %s' % (oldest,)) + + comm_inst.decrementThreadCount('clean_old_blocks') + +def clean_keys(comm_inst): + '''Delete expired forward secrecy keys''' + conn = sqlite3.connect(comm_inst._core.peerDB, timeout=10) + c = conn.cursor() + time = epoch.get_epoch() + deleteKeys = [] + + for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): + logger.debug('Forward key: %s' % entry[1]) + deleteKeys.append(entry[1]) + + for key in deleteKeys: + logger.debug('Deleting forward key %s' % key) + c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) + conn.commit() + conn.close() + + onionrusers.deleteExpiredKeys(comm_inst._core) + + comm_inst.decrementThreadCount('clean_keys') \ No newline at end of file diff --git a/onionr/communicatorutils/lookupadders.py b/onionr/communicatorutils/lookupadders.py new file mode 100755 index 00000000..a4d3fd79 --- /dev/null +++ b/onionr/communicatorutils/lookupadders.py @@ -0,0 +1,49 @@ +''' + Onionr - Private P2P Communication + + Lookup new peer transport addresses using the communicator +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger +from onionrutils import stringvalidators +from communicator import peeraction, onlinepeers +def lookup_new_peer_transports_with_communicator(comm_inst): + logger.info('Looking up new addresses...') + tryAmount = 1 + newPeers = [] + for i in range(tryAmount): + # Download new peer address list from random online peers + if len(newPeers) > 10000: + # Don't get new peers if we have too many queued up + break + peer = onlinepeers.pick_online_peer(comm_inst) + newAdders = peeraction.peer_action(comm_inst, peer, action='pex') + try: + newPeers = newAdders.split(',') + except AttributeError: + pass + else: + # Validate new peers are good format and not already in queue + invalid = [] + for x in newPeers: + x = x.strip() + if not stringvalidators.validate_transport(x) or x in comm_inst.newPeers or x == comm_inst._core.hsAddress: + # avoid adding if its our address + invalid.append(x) + for x in invalid: + newPeers.remove(x) + comm_inst.newPeers.extend(newPeers) + comm_inst.decrementThreadCount('lookupAdders') \ No newline at end of file diff --git a/onionr/communicatorutils/lookupblocks.py b/onionr/communicatorutils/lookupblocks.py new file mode 100755 index 00000000..0e36753a --- /dev/null +++ b/onionr/communicatorutils/lookupblocks.py @@ -0,0 +1,89 @@ +''' + Onionr - Private P2P Communication + + Lookup new blocks with the communicator using a random connected peer +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger, onionrproofs +from onionrutils import stringvalidators, epoch +from communicator import peeraction, onlinepeers + +def lookup_blocks_from_communicator(comm_inst): + logger.info('Looking up new blocks...') + tryAmount = 2 + newBlocks = '' + existingBlocks = comm_inst._core.getBlockList() + triedPeers = [] # list of peers we've tried this time around + maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion + lastLookupTime = 0 # Last time we looked up a particular peer's list + new_block_count = 0 + for i in range(tryAmount): + listLookupCommand = 'getblocklist' # This is defined here to reset it each time + if len(comm_inst.blockQueue) >= maxBacklog: + break + if not comm_inst.isOnline: + break + # check if disk allocation is used + if comm_inst._core.storage_counter.isFull(): + logger.debug('Not looking up new blocks due to maximum amount of allowed disk space used') + break + peer = onlinepeers.pick_online_peer(comm_inst) # select random online peer + # if we've already tried all the online peers this time around, stop + if peer in triedPeers: + if len(comm_inst.onlinePeers) == len(triedPeers): + break + else: + continue + triedPeers.append(peer) + + # Get the last time we looked up a peer's stamp to only fetch blocks since then. + # Saved in memory only for privacy reasons + try: + lastLookupTime = comm_inst.dbTimestamps[peer] + except KeyError: + lastLookupTime = 0 + else: + listLookupCommand += '?date=%s' % (lastLookupTime,) + try: + newBlocks = peeraction.peer_action(comm_inst, peer, listLookupCommand) # get list of new block hashes + except Exception as error: + logger.warn('Could not get new blocks from %s.' % peer, error = error) + newBlocks = False + else: + comm_inst.dbTimestamps[peer] = epoch.get_rounded_epoch(roundS=60) + if newBlocks != False: + # if request was a success + for i in newBlocks.split('\n'): + if stringvalidators.validate_hash(i): + # if newline seperated string is valid hash + if not i in existingBlocks: + # if block does not exist on disk and is not already in block queue + if i not in comm_inst.blockQueue: + if onionrproofs.hashMeetsDifficulty(i) and not comm_inst._core._blacklist.inBlacklist(i): + if len(comm_inst.blockQueue) <= 1000000: + comm_inst.blockQueue[i] = [peer] # add blocks to download queue + new_block_count += 1 + else: + if peer not in comm_inst.blockQueue[i]: + if len(comm_inst.blockQueue[i]) < 10: + comm_inst.blockQueue[i].append(peer) + if new_block_count > 0: + block_string = "" + if new_block_count > 1: + block_string = "s" + logger.info('Discovered %s new block%s' % (new_block_count, block_string), terminal=True) + comm_inst.decrementThreadCount('lookupBlocks') + return \ No newline at end of file diff --git a/onionr/communicatorutils/netcheck.py b/onionr/communicatorutils/netcheck.py new file mode 100755 index 00000000..81369d58 --- /dev/null +++ b/onionr/communicatorutils/netcheck.py @@ -0,0 +1,41 @@ +''' + Onionr - Private P2P Communication + + Determine if our node is able to use Tor based on the status of a communicator instance + and the result of pinging onion http servers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger +from utils import netutils +from onionrutils import localcommand, epoch +def net_check(comm_inst): + '''Check if we are connected to the internet or not when we can't connect to any peers''' + rec = False # for detecting if we have received incoming connections recently + c = comm_inst._core + if len(comm_inst.onlinePeers) == 0: + try: + if (epoch.get_epoch() - int(localcommand.local_command(c, '/lastconnect'))) <= 60: + comm_inst.isOnline = True + rec = True + except ValueError: + pass + if not rec and not netutils.checkNetwork(c, torPort=comm_inst.proxyPort): + if not comm_inst.shutdown: + logger.warn('Network check failed, are you connected to the Internet, and is Tor working?') + comm_inst.isOnline = False + else: + comm_inst.isOnline = True + comm_inst.decrementThreadCount('net_check') \ No newline at end of file diff --git a/onionr/communicatorutils/onionrcommunicatortimers.py b/onionr/communicatorutils/onionrcommunicatortimers.py index c15f20fc..df3b06d0 100755 --- a/onionr/communicatorutils/onionrcommunicatortimers.py +++ b/onionr/communicatorutils/onionrcommunicatortimers.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file contains timer control for the communicator ''' @@ -20,7 +19,7 @@ ''' import threading, onionrexceptions, logger class OnionrCommunicatorTimers: - def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False): + def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False, myArgs=[]): self.timerFunction = timerFunction self.frequency = frequency self.threadAmount = threadAmount @@ -29,6 +28,7 @@ class OnionrCommunicatorTimers: self.daemonInstance = daemonInstance self.maxThreads = maxThreads self._core = self.daemonInstance._core + self.args = myArgs self.daemonInstance.timers.append(self) self.count = 0 @@ -52,10 +52,10 @@ class OnionrCommunicatorTimers: if self.makeThread: for i in range(self.threadAmount): if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads: - logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__) + logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__, terminal=True) else: self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 - newThread = threading.Thread(target=self.timerFunction) + newThread = threading.Thread(target=self.timerFunction, args=self.args, daemon=True) newThread.start() else: self.timerFunction() diff --git a/onionr/communicatorutils/onionrdaemontools.py b/onionr/communicatorutils/onionrdaemontools.py deleted file mode 100755 index b68c693d..00000000 --- a/onionr/communicatorutils/onionrdaemontools.py +++ /dev/null @@ -1,214 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Contains the CommunicatorUtils class which contains useful functions for the communicator daemon -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -import onionrexceptions, onionrpeers, onionrproofs, logger -import base64, sqlite3, os -from dependencies import secrets -from utils import netutils -from onionrusers import onionrusers - -class DaemonTools: - ''' - Class intended for use by Onionr Communicator - ''' - def __init__(self, daemon): - self.daemon = daemon - self.announceProgress = {} - self.announceCache = {} - - def announceNode(self): - '''Announce our node to our peers''' - retData = False - announceFail = False - - # Do not let announceCache get too large - if len(self.announceCache) >= 10000: - self.announceCache.popitem() - - if self.daemon._core.config.get('general.security_level', 0) == 0: - # Announce to random online peers - for i in self.daemon.onlinePeers: - if not i in self.announceCache and not i in self.announceProgress: - peer = i - break - else: - peer = self.daemon.pickOnlinePeer() - - for x in range(1): - if x == 1 and self.daemon._core.config.get('i2p.host'): - ourID = self.daemon._core.config.get('i2p.own_addr').strip() - else: - ourID = self.daemon._core.hsAddress.strip() - - url = 'http://' + peer + '/announce' - data = {'node': ourID} - - combinedNodes = ourID + peer - if ourID != 1: - #TODO: Extend existingRand for i2p - existingRand = self.daemon._core.getAddressInfo(peer, 'powValue') - if type(existingRand) is type(None): - existingRand = '' - - if peer in self.announceCache: - data['random'] = self.announceCache[peer] - elif len(existingRand) > 0: - data['random'] = existingRand - else: - self.announceProgress[peer] = True - proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) - del self.announceProgress[peer] - try: - data['random'] = base64.b64encode(proof.waitForResult()[1]) - except TypeError: - # Happens when we failed to produce a proof - logger.error("Failed to produce a pow for announcing to " + peer) - announceFail = True - else: - self.announceCache[peer] = data['random'] - if not announceFail: - logger.info('Announcing node to ' + url) - if self.daemon._core._utils.doPostRequest(url, data) == 'Success': - logger.info('Successfully introduced node to ' + peer) - retData = True - self.daemon._core.setAddressInfo(peer, 'introduced', 1) - self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) - self.daemon.decrementThreadCount('announceNode') - return retData - - def netCheck(self): - '''Check if we are connected to the internet or not when we can't connect to any peers''' - if len(self.daemon.onlinePeers) == 0: - if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): - if not self.daemon.shutdown: - logger.warn('Network check failed, are you connected to the internet?') - self.daemon.isOnline = False - else: - self.daemon.isOnline = True - self.daemon.decrementThreadCount('netCheck') - - def cleanOldBlocks(self): - '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' - - # Delete expired blocks - for bHash in self.daemon._core.getExpiredBlocks(): - self.daemon._core._blacklist.addToDB(bHash) - self.daemon._core.removeBlock(bHash) - logger.info('Deleted block: %s' % (bHash,)) - - while self.daemon._core._utils.storageCounter.isFull(): - oldest = self.daemon._core.getBlockList()[0] - self.daemon._core._blacklist.addToDB(oldest) - self.daemon._core.removeBlock(oldest) - logger.info('Deleted block: %s' % (oldest,)) - - self.daemon.decrementThreadCount('cleanOldBlocks') - - def cleanKeys(self): - '''Delete expired forward secrecy keys''' - conn = sqlite3.connect(self.daemon._core.peerDB, timeout=10) - c = conn.cursor() - time = self.daemon._core._utils.getEpoch() - deleteKeys = [] - - for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): - logger.debug('Forward key: %s' % entry[1]) - deleteKeys.append(entry[1]) - - for key in deleteKeys: - logger.debug('Deleting forward key %s' % key) - c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) - conn.commit() - conn.close() - - onionrusers.deleteExpiredKeys(self.daemon._core) - - self.daemon.decrementThreadCount('cleanKeys') - - def cooldownPeer(self): - '''Randomly add an online peer to cooldown, so we can connect a new one''' - onlinePeerAmount = len(self.daemon.onlinePeers) - minTime = 300 - cooldownTime = 600 - toCool = '' - tempConnectTimes = dict(self.daemon.connectTimes) - - # Remove peers from cooldown that have been there long enough - tempCooldown = dict(self.daemon.cooldownPeer) - for peer in tempCooldown: - if (self.daemon._core._utils.getEpoch() - tempCooldown[peer]) >= cooldownTime: - del self.daemon.cooldownPeer[peer] - - # Cool down a peer, if we have max connections alive for long enough - if onlinePeerAmount >= self.daemon._core.config.get('peers.max_connect', 10, save = True): - finding = True - - while finding: - try: - toCool = min(tempConnectTimes, key=tempConnectTimes.get) - if (self.daemon._core._utils.getEpoch() - tempConnectTimes[toCool]) < minTime: - del tempConnectTimes[toCool] - else: - finding = False - except ValueError: - break - else: - self.daemon.removeOnlinePeer(toCool) - self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch() - - self.daemon.decrementThreadCount('cooldownPeer') - - def runCheck(self): - if os.path.isfile(self.daemon._core.dataDir + '.runcheck'): - os.remove(self.daemon._core.dataDir + '.runcheck') - return True - - return False - - def humanReadableTime(self, seconds): - build = '' - - units = { - 'year' : 31557600, - 'month' : (31557600 / 12), - 'day' : 86400, - 'hour' : 3600, - 'minute' : 60, - 'second' : 1 - } - - for unit in units: - amnt_unit = int(seconds / units[unit]) - if amnt_unit >= 1: - seconds -= amnt_unit * units[unit] - build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' ' - - return build.strip() - - def insertDeniableBlock(self): - '''Insert a fake block in order to make it more difficult to track real blocks''' - fakePeer = '' - chance = 10 - if secrets.randbelow(chance) == (chance - 1): - fakePeer = 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====' - data = secrets.token_hex(secrets.randbelow(500) + 1) - self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) - self.daemon.decrementThreadCount('insertDeniableBlock') - return \ No newline at end of file diff --git a/onionr/communicatorutils/proxypicker.py b/onionr/communicatorutils/proxypicker.py old mode 100644 new mode 100755 index 7e1d1e38..a6461b5c --- a/onionr/communicatorutils/proxypicker.py +++ b/onionr/communicatorutils/proxypicker.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - Just picks a proxy + Just picks a proxy to use based on a peer's address ''' ''' This program is free software: you can redistribute it and/or modify @@ -22,4 +22,5 @@ def pick_proxy(peer_address): if peer_address.endswith('.onion'): return 'tor' elif peer_address.endswith('.i2p'): - return 'i2p' \ No newline at end of file + return 'i2p' + raise ValueError("Peer address was not string ending with acceptable value") \ No newline at end of file diff --git a/onionr/communicatorutils/servicecreator.py b/onionr/communicatorutils/servicecreator.py new file mode 100755 index 00000000..769f663b --- /dev/null +++ b/onionr/communicatorutils/servicecreator.py @@ -0,0 +1,40 @@ +''' + Onionr - Private P2P Communication + + Creates an onionr direct connection service by scanning all connection blocks +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import communicator, onionrblockapi +from onionrutils import stringvalidators, bytesconverter + +def service_creator(daemon): + assert isinstance(daemon, communicator.OnionrCommunicatorDaemon) + core = daemon._core + + # Find socket connection blocks + # TODO cache blocks and only look at recently received ones + con_blocks = core.getBlocksByType('con') + for b in con_blocks: + if not b in daemon.active_services: + bl = onionrblockapi.Block(b, core=core, decrypt=True) + bs = bytesconverter.bytes_to_str(bl.bcontent) + '.onion' + if stringvalidators.validate_pub_key(bl.signer) and stringvalidators.validate_transport(bs): + signer = bytesconverter.bytes_to_str(bl.signer) + daemon.active_services.append(b) + daemon.active_services.append(signer) + daemon.services.create_server(signer, bs) + + daemon.decrementThreadCount('service_creator') \ No newline at end of file diff --git a/onionr/communicatorutils/uploadblocks.py b/onionr/communicatorutils/uploadblocks.py new file mode 100755 index 00000000..6285263d --- /dev/null +++ b/onionr/communicatorutils/uploadblocks.py @@ -0,0 +1,57 @@ +''' + Onionr - Private P2P Communication + + Upload blocks in the upload queue to peers from the communicator +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import logger +from communicatorutils import proxypicker +import onionrblockapi as block +from onionrutils import localcommand, stringvalidators, basicrequests +from communicator import onlinepeers + +def upload_blocks_from_communicator(comm_inst): + # when inserting a block, we try to upload it to a few peers to add some deniability + TIMER_NAME = "upload_blocks_from_communicator" + + triedPeers = [] + finishedUploads = [] + core = comm_inst._core + comm_inst.blocksToUpload = core._crypto.randomShuffle(comm_inst.blocksToUpload) + if len(comm_inst.blocksToUpload) != 0: + for bl in comm_inst.blocksToUpload: + if not stringvalidators.validate_hash(bl): + logger.warn('Requested to upload invalid block', terminal=True) + comm_inst.decrementThreadCount(TIMER_NAME) + return + for i in range(min(len(comm_inst.onlinePeers), 6)): + peer = onlinepeers.pick_online_peer(comm_inst) + if peer in triedPeers: + continue + triedPeers.append(peer) + url = 'http://' + peer + '/upload' + data = {'block': block.Block(bl).getRaw()} + proxyType = proxypicker.pick_proxy(peer) + logger.info("Uploading block to " + peer, terminal=True) + if not basicrequests.do_post_request(core, url, data=data, proxyType=proxyType) == False: + localcommand.local_command(core, 'waitforshare/' + bl, post=True) + finishedUploads.append(bl) + for x in finishedUploads: + try: + comm_inst.blocksToUpload.remove(x) + except ValueError: + pass + comm_inst.decrementThreadCount(TIMER_NAME) \ No newline at end of file diff --git a/onionr/config.py b/onionr/config.py index fe093b57..98085828 100755 --- a/onionr/config.py +++ b/onionr/config.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with configuration management. ''' @@ -19,11 +19,9 @@ ''' import os, json, logger - +from utils import identifyhome # set data dir -dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) -if not dataDir.endswith('/'): - dataDir += '/' +dataDir = identifyhome.identify_home() _configfile = os.path.abspath(dataDir + 'config.json') _config = {} @@ -96,15 +94,8 @@ def check(): Checks if the configuration file exists, creates it if not ''' - try: - if not os.path.exists(os.path.dirname(get_config_file())): - os.path.mkdirs(os.path.dirname(get_config_file())) - if not os.path.isfile(get_config_file()): - open(get_config_file(), 'a', encoding="utf8").close() - save() - except: - pass - #logger.debug('Failed to check configuration file.') + if not os.path.exists(os.path.dirname(get_config_file())): + os.makedirs(os.path.dirname(get_config_file())) def save(): ''' @@ -115,14 +106,13 @@ def save(): try: with open(get_config_file(), 'w', encoding="utf8") as configfile: json.dump(get_config(), configfile, indent=2) - except: + except json.JSONDecodeError: logger.warn('Failed to write to configuration file.') def reload(): ''' Reloads the configuration data in memory from the file ''' - check() try: with open(get_config_file(), 'r', encoding="utf8") as configfile: diff --git a/onionr/core.py b/onionr/core.py index 1cbce026..b523c5ff 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Core Onionr library, useful for external programs. Handles peer & data processing ''' @@ -17,34 +17,35 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid +import os, sys, json +import logger, netcontroller, config from onionrblockapi import Block +import coredb import deadsimplekv as simplekv -import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions +import onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrblacklist from onionrusers import onionrusers +from onionrstorage import removeblock, setdata import dbcreator, onionrstorage, serializeddata, subprocesspow -from etc import onionrvalues - -if sys.version_info < (3, 6): - try: - import sha3 - except ModuleNotFoundError: - logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') - sys.exit(1) +from etc import onionrvalues, powchoice +from onionrutils import localcommand, stringvalidators, bytesconverter, epoch +from onionrutils import blockmetadata +from utils import identifyhome +import storagecounter class Core: def __init__(self, torPort=0): ''' Initialize Core Onionr library ''' - # set data dir - self.dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) - if not self.dataDir.endswith('/'): - self.dataDir += '/' + self.dataDir = identifyhome.identify_home() try: + self.usageFile = self.dataDir + 'disk-usage.txt' + self.config = config + self.maxBlockSize = 10000000 # max block size in bytes + self.onionrInst = None self.queueDB = self.dataDir + 'queue.db' self.peerDB = self.dataDir + 'peers.db' @@ -64,7 +65,8 @@ class Core: self.dbCreate = dbcreator.DBCreator(self) self.forwardKeysFile = self.dataDir + 'forward-keys.db' self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5) - + self.storage_counter = storagecounter.StorageCounter(self) + # Socket data, defined here because of multithreading constraints with gevent self.killSockets = False self.startSocket = {} @@ -72,11 +74,6 @@ class Core: self.socketReasons = {} self.socketServerResponseData = {} - self.usageFile = self.dataDir + 'disk-usage.txt' - self.config = config - - self.maxBlockSize = 10000000 # max block size in bytes - if not os.path.exists(self.dataDir): os.mkdir(self.dataDir) if not os.path.exists(self.dataDir + 'blocks/'): @@ -103,15 +100,15 @@ class Core: else: logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) - self._utils = onionrutils.OnionrUtils(self) + self.use_subprocess = powchoice.use_subprocess(self) # Initialize the crypto object self._crypto = onionrcrypto.OnionrCrypto(self) self._blacklist = onionrblacklist.OnionrBlackList(self) self.serializer = serializeddata.SerializedData(self) except Exception as error: - logger.error('Failed to initialize core Onionr library.', error=error) - logger.fatal('Cannot recover from error.') + logger.error('Failed to initialize core Onionr library.', error=error, terminal=True) + logger.fatal('Cannot recover from error.', terminal=True) sys.exit(1) return @@ -127,88 +124,19 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' - assert peerID not in self.listPeers() - - # This function simply adds a peer to the DB - if not self._utils.validatePubKey(peerID): - return False - - events.event('pubkey_add', data = {'key': peerID}, onionr = None) - - conn = sqlite3.connect(self.peerDB, timeout=30) - hashID = self._crypto.pubKeyHashID(peerID) - c = conn.cursor() - t = (peerID, name, 'unknown', hashID, 0) - - for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)): - try: - if i[0] == peerID: - conn.close() - return False - except ValueError: - pass - except IndexError: - pass - c.execute('INSERT INTO peers (id, name, dateSeen, hashID, trust) VALUES(?, ?, ?, ?, ?);', t) - conn.commit() - conn.close() - - return True + return coredb.keydb.addkeys.add_peer(self, peerID, name) def addAddress(self, address): ''' Add an address to the address database (only tor currently) ''' - - if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: - return False - if type(address) is None or len(address) == 0: - return False - if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - # check if address is in database - # this is safe to do because the address is validated above, but we strip some chars here too just in case - address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') - for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)): - try: - if i[0] == address: - conn.close() - return False - except ValueError: - pass - except IndexError: - pass - - t = (address, 1) - c.execute('INSERT INTO adders (address, type) VALUES(?, ?);', t) - conn.commit() - conn.close() - - events.event('address_add', data = {'address': address}, onionr = None) - - return True - else: - #logger.debug('Invalid ID: %s' % address) - return False + return coredb.keydb.addkeys.add_address(self, address) def removeAddress(self, address): ''' Remove an address from the address database ''' - - if self._utils.validateID(address): - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - t = (address,) - c.execute('Delete from adders where address=?;', t) - conn.commit() - conn.close() - - events.event('address_remove', data = {'address': address}, onionr = None) - return True - else: - return False + return coredb.keydb.removekeys.remove_address(self, address) def removeBlock(self, block): ''' @@ -216,16 +144,7 @@ class Core: **You may want blacklist.addToDB(blockHash) ''' - - if self._utils.validateHash(block): - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - t = (block,) - c.execute('Delete from hashes where hash=?;', t) - conn.commit() - conn.close() - dataSize = sys.getsizeof(onionrstorage.getData(self, block)) - self._utils.storageCounter.removeBytes(dataSize) + removeblock.remove_block(self, block) def createAddressDB(self): ''' @@ -251,67 +170,19 @@ class Core: Should be in hex format! ''' - - if not os.path.exists(self.blockDB): - raise Exception('Block db does not exist') - if self._utils.hasBlock(newHash): - return - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301) - if selfInsert or dataSaved: - selfInsert = 1 - else: - selfInsert = 0 - data = (newHash, currentTime, '', selfInsert) - c.execute('INSERT INTO hashes (hash, dateReceived, dataType, dataSaved) VALUES(?, ?, ?, ?);', data) - conn.commit() - conn.close() - - return - - def getData(self, hash): - ''' - Simply return the data associated to a hash - ''' - - data = onionrstorage.getData(self, hash) - - return data + coredb.blockmetadb.add.add_to_block_DB(self, newHash, selfInsert, dataSaved) def setData(self, data): ''' Set the data assciated with a hash ''' + return onionrstorage.setdata.set_data(self, data) - data = data - dataSize = sys.getsizeof(data) - - if not type(data) is bytes: - data = data.encode() - - dataHash = self._crypto.sha3Hash(data) - - if type(dataHash) is bytes: - dataHash = dataHash.decode() - blockFileName = self.blockDataLocation + dataHash + '.dat' - if os.path.exists(blockFileName): - pass # TODO: properly check if block is already saved elsewhere - #raise Exception("Data is already set for " + dataHash) - else: - if self._utils.storageCounter.addBytes(dataSize) != False: - onionrstorage.store(self, data, blockHash=dataHash) - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) - conn.commit() - conn.close() - with open(self.dataNonceFile, 'a') as nonceFile: - nonceFile.write(dataHash + '\n') - else: - raise onionrexceptions.DiskAllocationReached - - return dataHash + def getData(self, hash): + ''' + Simply return the data associated to a hash + ''' + return onionrstorage.getData(self, hash) def daemonQueue(self): ''' @@ -319,118 +190,31 @@ class Core: This function intended to be used by the client. Queue to exchange data between "client" and server. ''' - - retData = False - if not os.path.exists(self.queueDB): - self.dbCreate.createDaemonDB() - else: - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - try: - for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'): - retData = row - break - except sqlite3.OperationalError: - self.dbCreate.createDaemonDB() - else: - if retData != False: - c.execute('DELETE FROM commands WHERE id=?;', (retData[3],)) - conn.commit() - conn.close() - - events.event('queue_pop', data = {'data': retData}, onionr = None) - - return retData + return coredb.daemonqueue.daemon_queue(self) def daemonQueueAdd(self, command, data='', responseID=''): ''' Add a command to the daemon queue, used by the communication daemon (communicator.py) ''' - - retData = True - - date = self._utils.getEpoch() - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - t = (command, data, date, responseID) - - try: - c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) - conn.commit() - except sqlite3.OperationalError: - retData = False - self.daemonQueue() - events.event('queue_push', data = {'command': command, 'data': data}, onionr = None) - conn.close() - return retData + return coredb.daemonqueue.daemon_queue_add(self, command, data, responseID) def daemonQueueGetResponse(self, responseID=''): ''' Get a response sent by communicator to the API, by requesting to the API ''' - assert len(responseID) > 0 - resp = self._utils.localCommand('queueResponse/' + responseID) - return resp - - def daemonQueueWaitForResponse(self, responseID='', checkFreqSecs=1): - resp = 'failure' - while resp == 'failure': - resp = self.daemonQueueGetResponse(responseID) - time.sleep(1) - return resp - - def daemonQueueSimple(self, command, data='', checkFreqSecs=1): - ''' - A simplified way to use the daemon queue. Will register a command (with optional data) and wait, return the data - Not always useful, but saves time + LOC in some cases. - This is a blocking function, so be careful. - ''' - responseID = str(uuid.uuid4()) # generate unique response ID - self.daemonQueueAdd(command, data=data, responseID=responseID) - return self.daemonQueueWaitForResponse(responseID, checkFreqSecs) + return coredb.daemonqueue.daemon_queue_get_response(self, responseID) def clearDaemonQueue(self): ''' Clear the daemon queue (somewhat dangerous) ''' - conn = sqlite3.connect(self.queueDB, timeout=30) - c = conn.cursor() - - try: - c.execute('DELETE FROM commands;') - conn.commit() - except: - pass - - conn.close() - events.event('queue_clear', onionr = None) - - return + return coredb.daemonqueue.clear_daemon_queue(self) def listAdders(self, randomOrder=True, i2p=True, recent=0): ''' Return a list of addresses ''' - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - if randomOrder: - addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') - else: - addresses = c.execute('SELECT * FROM adders;') - addressList = [] - for i in addresses: - if len(i[0].strip()) == 0: - continue - addressList.append(i[0]) - conn.close() - testList = list(addressList) # create new list to iterate - for address in testList: - try: - if recent > 0 and (self._utils.getEpoch() - self.getAddressInfo(address, 'lastConnect')) > recent: - raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0 - except TypeError: - addressList.remove(address) - return addressList + return coredb.keydb.listkeys.list_adders(self, randomOrder, i2p, recent) def listPeers(self, randomOrder=True, getPow=False, trust=0): ''' @@ -439,35 +223,7 @@ class Core: randomOrder determines if the list should be in a random order trust sets the minimum trust to list ''' - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - payload = '' - - if trust not in (0, 1, 2): - logger.error('Tried to select invalid trust.') - return - - if randomOrder: - payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();' - else: - payload = 'SELECT * FROM peers WHERE trust >= ?;' - - peerList = [] - - for i in c.execute(payload, (trust,)): - try: - if len(i[0]) != 0: - if getPow: - peerList.append(i[0] + '-' + i[1]) - else: - peerList.append(i[0]) - except TypeError: - pass - - conn.close() - - return peerList + return coredb.keydb.listkeys.list_peers(self, randomOrder, getPow, trust) def getPeerInfo(self, peer, info): ''' @@ -480,46 +236,13 @@ class Core: trust int 4 hashID text 5 ''' - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - command = (peer,) - infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'trust': 4, 'hashID': 5} - info = infoNumbers[info] - iterCount = 0 - retVal = '' - - for row in c.execute('SELECT * FROM peers WHERE id=?;', command): - for i in row: - if iterCount == info: - retVal = i - break - else: - iterCount += 1 - - conn.close() - - return retVal + return coredb.keydb.userinfo.get_user_info(self, peer, info) def setPeerInfo(self, peer, key, data): ''' Update a peer for a key ''' - - conn = sqlite3.connect(self.peerDB, timeout=30) - c = conn.cursor() - - command = (data, peer) - - # TODO: validate key on whitelist - if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'): - raise Exception("Got invalid database key when setting peer info") - - c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) - conn.commit() - conn.close() - - return + return coredb.keydb.userinfo.set_peer_info(self, peer, key, data) def getAddressInfo(self, address, info): ''' @@ -536,117 +259,35 @@ class Core: trust 8 introduced 9 ''' - - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - - command = (address,) - infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} - info = infoNumbers[info] - iterCount = 0 - retVal = '' - - for row in c.execute('SELECT * FROM adders WHERE address=?;', command): - for i in row: - if iterCount == info: - retVal = i - break - else: - iterCount += 1 - conn.close() - - return retVal + return coredb.keydb.transportinfo.get_address_info(self, address, info) def setAddressInfo(self, address, key, data): ''' Update an address for a key ''' - - conn = sqlite3.connect(self.addressDB, timeout=30) - c = conn.cursor() - - command = (data, address) - - if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): - raise Exception("Got invalid database key when setting address info") - else: - c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) - conn.commit() - conn.close() - - return + return coredb.keydb.transportinfo.set_address_info(self, address, key, data) def getBlockList(self, dateRec = None, unsaved = False): ''' Get list of our blocks ''' - if dateRec == None: - dateRec = 0 - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' - args = (dateRec,) - rows = list() - for row in c.execute(execute, args): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.get_block_list(self, dateRec, unsaved) def getBlockDate(self, blockHash): ''' Returns the date a block was received ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' - args = (blockHash,) - for row in c.execute(execute, args): - for i in row: - return int(i) - conn.close() - return None + return coredb.blockmetadb.get_block_date(self, blockHash) def getBlocksByType(self, blockType, orderDate=True): ''' Returns a list of blocks by the type ''' - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - - if orderDate: - execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' - else: - execute = 'SELECT hash FROM hashes WHERE dataType=?;' - - args = (blockType,) - rows = list() - - for row in c.execute(execute, args): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.get_blocks_by_type(self, blockType, orderDate) def getExpiredBlocks(self): '''Returns a list of expired blocks''' - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - date = int(self._utils.getEpoch()) - - execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) - - rows = list() - for row in c.execute(execute): - for i in row: - rows.append(i) - conn.close() - return rows + return coredb.blockmetadb.expiredblocks.get_expired_blocks(self) def updateBlockInfo(self, hash, key, data): ''' @@ -663,18 +304,7 @@ class Core: dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is expire - expire date for a block ''' - - if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): - return False - - conn = sqlite3.connect(self.blockDB, timeout=30) - c = conn.cursor() - args = (data, hash) - c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) - conn.commit() - conn.close() - - return True + return coredb.blockmetadb.updateblockinfo.update_block_info(self, hash, key, data) def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False): ''' @@ -682,15 +312,17 @@ class Core: encryptType must be specified to encrypt a block ''' allocationReachedMessage = 'Cannot insert block, disk allocation reached.' - if self._utils.storageCounter.isFull(): + if self.storage_counter.isFull(): logger.error(allocationReachedMessage) return False retData = False - createTime = self._utils.getRoundedEpoch() + if type(data) is None: + raise ValueError('Data cannot be none') - # check nonce - dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) + createTime = epoch.get_epoch() + + dataNonce = bytesconverter.bytes_to_str(self._crypto.sha3Hash(data)) try: with open(self.dataNonceFile, 'r') as nonces: if dataNonce in nonces: @@ -763,14 +395,18 @@ class Core: signature = self._crypto.symmetricEncrypt(signature, key=symKey, returnEncoded=True).decode() signer = self._crypto.symmetricEncrypt(signer, key=symKey, returnEncoded=True).decode() elif encryptType == 'asym': - if self._utils.validatePubKey(asymPeer): + if stringvalidators.validate_pub_key(asymPeer): # Encrypt block data with forward secrecy key first, but not meta jsonMeta = json.dumps(meta) jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode() data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode() signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode() signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode() - onionrusers.OnionrUser(self, asymPeer, saveUser=True) + try: + onionrusers.OnionrUser(self, asymPeer, saveUser=True) + except ValueError: + # if peer is already known + pass else: raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') @@ -786,7 +422,10 @@ class Core: metadata['expire'] = expire # send block data (and metadata) to POW module to get tokenized block data - payload = subprocesspow.SubprocessPOW(data, metadata, self).start() + if self.use_subprocess: + payload = subprocesspow.SubprocessPOW(data, metadata, self).start() + else: + payload = onionrproofs.POW(metadata, data).waitForResult() if payload != False: try: retData = self.setData(payload) @@ -794,45 +433,29 @@ class Core: logger.error(allocationReachedMessage) retData = False else: - # Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult - self._utils.localCommand('/waitforshare/' + retData, post=True) + # Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult + if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': + if self.config.get('general.security_level', 1) == 0: + localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5) + self.daemonQueueAdd('uploadBlock', retData) + else: + pass self.addToBlockDB(retData, selfInsert=True, dataSaved=True) - #self.setBlockType(retData, meta['type']) - self._utils.processBlockMetadata(retData) - self.daemonQueueAdd('uploadBlock', retData) + blockmetadata.process_block_metadata(self, retData) if retData != False: - if plaintextPeer == 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====': - events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) + if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS: + events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True) else: - events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) + events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True) return retData def introduceNode(self): ''' Introduces our node into the network by telling X many nodes our HS address ''' - - if(self._utils.isCommunicatorRunning(timeout=30)): - announceAmount = 2 - nodeList = self.listAdders() - - if len(nodeList) == 0: - for i in self.bootstrapList: - if self._utils.validateID(i): - self.addAddress(i) - nodeList.append(i) - - if announceAmount > len(nodeList): - announceAmount = len(nodeList) - - for i in range(announceAmount): - self.daemonQueueAdd('announceNode', nodeList[i]) - - events.event('introduction', onionr = None) - - return True + if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!': + self.daemonQueueAdd('announceNode') + logger.info('Introduction command will be processed.', terminal=True) else: - logger.error('Onionr daemon is not running.') - return False - return + logger.warn('No running node detected. Cannot introduce.', terminal=True) \ No newline at end of file diff --git a/onionr/coredb/__init__.py b/onionr/coredb/__init__.py new file mode 100644 index 00000000..39c909ba --- /dev/null +++ b/onionr/coredb/__init__.py @@ -0,0 +1 @@ +from . import keydb, blockmetadb, daemonqueue \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/__init__.py b/onionr/coredb/blockmetadb/__init__.py new file mode 100644 index 00000000..409696dc --- /dev/null +++ b/onionr/coredb/blockmetadb/__init__.py @@ -0,0 +1,77 @@ +''' + Onionr - Private P2P Communication + + This module works with information relating to blocks stored on the node +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +from . import expiredblocks, updateblockinfo, add +def get_block_list(core_inst, dateRec = None, unsaved = False): + ''' + Get list of our blocks + ''' + if dateRec == None: + dateRec = 0 + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' + args = (dateRec,) + rows = list() + for row in c.execute(execute, args): + for i in row: + rows.append(i) + conn.close() + return rows + +def get_block_date(core_inst, blockHash): + ''' + Returns the date a block was received + ''' + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' + args = (blockHash,) + for row in c.execute(execute, args): + for i in row: + return int(i) + conn.close() + return None + +def get_blocks_by_type(core_inst, blockType, orderDate=True): + ''' + Returns a list of blocks by the type + ''' + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + + if orderDate: + execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' + else: + execute = 'SELECT hash FROM hashes WHERE dataType=?;' + + args = (blockType,) + rows = list() + + for row in c.execute(execute, args): + for i in row: + rows.append(i) + conn.close() + return rows \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/add.py b/onionr/coredb/blockmetadb/add.py new file mode 100644 index 00000000..cfd8305c --- /dev/null +++ b/onionr/coredb/blockmetadb/add.py @@ -0,0 +1,43 @@ +''' + Onionr - Private P2P Communication + + Add an entry to the block metadata database +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os, sqlite3 +from onionrutils import epoch, blockmetadata +def add_to_block_DB(core_inst, newHash, selfInsert=False, dataSaved=False): + ''' + Add a hash value to the block db + + Should be in hex format! + ''' + + if not os.path.exists(core_inst.blockDB): + raise Exception('Block db does not exist') + if blockmetadata.has_block(core_inst, newHash): + return + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + currentTime = epoch.get_epoch() + core_inst._crypto.secrets.randbelow(301) + if selfInsert or dataSaved: + selfInsert = 1 + else: + selfInsert = 0 + data = (newHash, currentTime, '', selfInsert) + c.execute('INSERT INTO hashes (hash, dateReceived, dataType, dataSaved) VALUES(?, ?, ?, ?);', data) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/expiredblocks.py b/onionr/coredb/blockmetadb/expiredblocks.py new file mode 100644 index 00000000..9e01d3ac --- /dev/null +++ b/onionr/coredb/blockmetadb/expiredblocks.py @@ -0,0 +1,35 @@ +''' + Onionr - Private P2P Communication + + Get a list of expired blocks still stored +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +from onionrutils import epoch +def get_expired_blocks(core_inst): + '''Returns a list of expired blocks''' + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + date = int(epoch.get_epoch()) + + execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) + + rows = list() + for row in c.execute(execute): + for i in row: + rows.append(i) + conn.close() + return rows \ No newline at end of file diff --git a/onionr/coredb/blockmetadb/updateblockinfo.py b/onionr/coredb/blockmetadb/updateblockinfo.py new file mode 100644 index 00000000..bbb5d969 --- /dev/null +++ b/onionr/coredb/blockmetadb/updateblockinfo.py @@ -0,0 +1,33 @@ +''' + Onionr - Private P2P Communication + + Update block information in the metadata database by a field name +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +import sqlite3 +def update_block_info(core_inst, hash, key, data): + if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): + return False + + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + args = (data, hash) + c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) + conn.commit() + conn.close() + + return True \ No newline at end of file diff --git a/onionr/coredb/daemonqueue/__init__.py b/onionr/coredb/daemonqueue/__init__.py new file mode 100644 index 00000000..b494a45a --- /dev/null +++ b/onionr/coredb/daemonqueue/__init__.py @@ -0,0 +1,97 @@ +''' + Onionr - Private P2P Communication + + Write and read the daemon queue, which is how messages are passed into the onionr daemon in a more + direct way than the http api +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3, os +import onionrevents as events +from onionrutils import localcommand, epoch + +def daemon_queue(core_inst): + ''' + Gives commands to the communication proccess/daemon by reading an sqlite3 database + + This function intended to be used by the client. Queue to exchange data between "client" and server. + ''' + + retData = False + if not os.path.exists(core_inst.queueDB): + core_inst.dbCreate.createDaemonDB() + else: + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + try: + for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'): + retData = row + break + except sqlite3.OperationalError: + core_inst.dbCreate.createDaemonDB() + else: + if retData != False: + c.execute('DELETE FROM commands WHERE id=?;', (retData[3],)) + conn.commit() + conn.close() + + events.event('queue_pop', data = {'data': retData}, onionr = core_inst.onionrInst) + + return retData + +def daemon_queue_add(core_inst, command, data='', responseID=''): + ''' + Add a command to the daemon queue, used by the communication daemon (communicator.py) + ''' + + retData = True + + date = epoch.get_epoch() + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + t = (command, data, date, responseID) + try: + c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t) + conn.commit() + except sqlite3.OperationalError: + retData = False + core_inst.daemonQueue() + events.event('queue_push', data = {'command': command, 'data': data}, onionr = core_inst.onionrInst) + conn.close() + return retData + +def daemon_queue_get_response(core_inst, responseID=''): + ''' + Get a response sent by communicator to the API, by requesting to the API + ''' + assert len(responseID) > 0 + resp = localcommand.local_command(core_inst, 'queueResponse/' + responseID) + return resp + +def clear_daemon_queue(core_inst): + ''' + Clear the daemon queue (somewhat dangerous) + ''' + conn = sqlite3.connect(core_inst.queueDB, timeout=30) + c = conn.cursor() + + try: + c.execute('DELETE FROM commands;') + conn.commit() + except: + pass + + conn.close() + events.event('queue_clear', onionr = core_inst.onionrInst) \ No newline at end of file diff --git a/onionr/coredb/keydb/__init__.py b/onionr/coredb/keydb/__init__.py new file mode 100644 index 00000000..16d602d5 --- /dev/null +++ b/onionr/coredb/keydb/__init__.py @@ -0,0 +1 @@ +from . import addkeys, listkeys, removekeys, userinfo, transportinfo \ No newline at end of file diff --git a/onionr/coredb/keydb/addkeys.py b/onionr/coredb/keydb/addkeys.py new file mode 100644 index 00000000..ca352d17 --- /dev/null +++ b/onionr/coredb/keydb/addkeys.py @@ -0,0 +1,91 @@ +''' + Onionr - Private P2P Communication + + add user keys or transport addresses +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +import onionrevents as events +from onionrutils import stringvalidators + +def add_peer(core_inst, peerID, name=''): + ''' + Adds a public key to the key database (misleading function name) + ''' + if peerID in core_inst.listPeers() or peerID == core_inst._crypto.pubKey: + raise ValueError("specified id is already known") + + # This function simply adds a peer to the DB + if not stringvalidators.validate_pub_key(peerID): + return False + + events.event('pubkey_add', data = {'key': peerID}, onionr = core_inst.onionrInst) + + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + hashID = core_inst._crypto.pubKeyHashID(peerID) + c = conn.cursor() + t = (peerID, name, 'unknown', hashID, 0) + + for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)): + try: + if i[0] == peerID: + conn.close() + return False + except ValueError: + pass + except IndexError: + pass + c.execute('INSERT INTO peers (id, name, dateSeen, hashID, trust) VALUES(?, ?, ?, ?, ?);', t) + conn.commit() + conn.close() + + return True + +def add_address(core_inst, address): + ''' + Add an address to the address database (only tor currently) + ''' + + if type(address) is None or len(address) == 0: + return False + if stringvalidators.validate_transport(address): + if address == core_inst.config.get('i2p.ownAddr', None) or address == core_inst.hsAddress: + return False + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + # check if address is in database + # this is safe to do because the address is validated above, but we strip some chars here too just in case + address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') + for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)): + try: + if i[0] == address: + conn.close() + return False + except ValueError: + pass + except IndexError: + pass + + t = (address, 1) + c.execute('INSERT INTO adders (address, type) VALUES(?, ?);', t) + conn.commit() + conn.close() + + events.event('address_add', data = {'address': address}, onionr = core_inst.onionrInst) + + return True + else: + return False diff --git a/onionr/coredb/keydb/listkeys.py b/onionr/coredb/keydb/listkeys.py new file mode 100644 index 00000000..50aff637 --- /dev/null +++ b/onionr/coredb/keydb/listkeys.py @@ -0,0 +1,83 @@ +''' + Onionr - Private P2P Communication + + get lists for user keys or transport addresses +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +import logger +from onionrutils import epoch +def list_peers(core_inst, randomOrder=True, getPow=False, trust=0): + ''' + Return a list of public keys (misleading function name) + + randomOrder determines if the list should be in a random order + trust sets the minimum trust to list + ''' + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + payload = '' + + if trust not in (0, 1, 2): + logger.error('Tried to select invalid trust.') + return + + if randomOrder: + payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();' + else: + payload = 'SELECT * FROM peers WHERE trust >= ?;' + + peerList = [] + + for i in c.execute(payload, (trust,)): + try: + if len(i[0]) != 0: + if getPow: + peerList.append(i[0] + '-' + i[1]) + else: + peerList.append(i[0]) + except TypeError: + pass + + conn.close() + + return peerList + +def list_adders(core_inst, randomOrder=True, i2p=True, recent=0): + ''' + Return a list of transport addresses + ''' + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + if randomOrder: + addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();') + else: + addresses = c.execute('SELECT * FROM adders;') + addressList = [] + for i in addresses: + if len(i[0].strip()) == 0: + continue + addressList.append(i[0]) + conn.close() + testList = list(addressList) # create new list to iterate + for address in testList: + try: + if recent > 0 and (epoch.get_epoch() - core_inst.getAddressInfo(address, 'lastConnect')) > recent: + raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0 + except TypeError: + addressList.remove(address) + return addressList \ No newline at end of file diff --git a/onionr/coredb/keydb/removekeys.py b/onionr/coredb/keydb/removekeys.py new file mode 100644 index 00000000..c0f1d7a6 --- /dev/null +++ b/onionr/coredb/keydb/removekeys.py @@ -0,0 +1,40 @@ +''' + Onionr - Private P2P Communication + + Remove a transport address but don't ban them +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +import onionrevents as events +from onionrutils import stringvalidators + +def remove_address(core_inst, address): + ''' + Remove an address from the address database + ''' + + if stringvalidators.validate_transport(address): + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + t = (address,) + c.execute('Delete from adders where address=?;', t) + conn.commit() + conn.close() + + events.event('address_remove', data = {'address': address}, onionr = core_inst.onionrInst) + return True + else: + return False \ No newline at end of file diff --git a/onionr/coredb/keydb/transportinfo.py b/onionr/coredb/keydb/transportinfo.py new file mode 100644 index 00000000..2fcd873b --- /dev/null +++ b/onionr/coredb/keydb/transportinfo.py @@ -0,0 +1,72 @@ +''' + Onionr - Private P2P Communication + + get or set transport address meta information +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +def get_address_info(core_inst, address, info): + ''' + Get info about an address from its database entry + + address text, 0 + type int, 1 + knownPeer text, 2 + speed int, 3 + success int, 4 + powValue 5 + failure int 6 + lastConnect 7 + trust 8 + introduced 9 + ''' + + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + + command = (address,) + infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9} + info = infoNumbers[info] + iterCount = 0 + retVal = '' + + for row in c.execute('SELECT * FROM adders WHERE address=?;', command): + for i in row: + if iterCount == info: + retVal = i + break + else: + iterCount += 1 + conn.close() + + return retVal + +def set_address_info(core_inst, address, key, data): + ''' + Update an address for a key + ''' + + conn = sqlite3.connect(core_inst.addressDB, timeout=30) + c = conn.cursor() + + command = (data, address) + + if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): + raise Exception("Got invalid database key when setting address info") + else: + c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/coredb/keydb/userinfo.py b/onionr/coredb/keydb/userinfo.py new file mode 100644 index 00000000..b7a43fd4 --- /dev/null +++ b/onionr/coredb/keydb/userinfo.py @@ -0,0 +1,69 @@ +''' + Onionr - Private P2P Communication + + get or set information about a user id +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +def get_user_info(core_inst, peer, info): + ''' + Get info about a peer from their database entry + + id text 0 + name text, 1 + adders text, 2 + dateSeen not null, 3 + trust int 4 + hashID text 5 + ''' + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + command = (peer,) + infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'trust': 4, 'hashID': 5} + info = infoNumbers[info] + iterCount = 0 + retVal = '' + + for row in c.execute('SELECT * FROM peers WHERE id=?;', command): + for i in row: + if iterCount == info: + retVal = i + break + else: + iterCount += 1 + + conn.close() + + return retVal + +def set_peer_info(core_inst, peer, key, data): + ''' + Update a peer for a key + ''' + + conn = sqlite3.connect(core_inst.peerDB, timeout=30) + c = conn.cursor() + + command = (data, peer) + + # TODO: validate key on whitelist + if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'): + raise Exception("Got invalid database key when setting peer info") + + c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) + conn.commit() + conn.close() \ No newline at end of file diff --git a/onionr/dbcreator.py b/onionr/dbcreator.py index b84794d3..6c26fa97 100755 --- a/onionr/dbcreator.py +++ b/onionr/dbcreator.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Data Storage & Sharing + Onionr - Private P2P Communication DBCreator, creates sqlite3 databases used by Onionr ''' diff --git a/onionr/dependencies/secrets.py b/onionr/dependencies/secrets.py deleted file mode 100755 index 9b2eb61b..00000000 --- a/onionr/dependencies/secrets.py +++ /dev/null @@ -1,331 +0,0 @@ -"""Generate cryptographically strong pseudo-random numbers suitable for -managing secrets such as account authentication, tokens, and similar. - -See PEP 506 for more information. -https://www.python.org/dev/peps/pep-0506/ - - -A. HISTORY OF THE SOFTWARE -========================== - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations, which became -Zope Corporation. In 2001, the Python Software Foundation (PSF, see -https://www.python.org/psf/) was formed, a non-profit organization -created specifically to own Python-related Intellectual Property. -Zope Corporation was a sponsoring member of the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases. - - Release Derived Year Owner GPL- - from compatible? (1) - - 0.9.0 thru 1.2 1991-1995 CWI yes - 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes - 1.6 1.5.2 2000 CNRI no - 2.0 1.6 2000 BeOpen.com no - 1.6.1 1.6 2001 CNRI yes (2) - 2.1 2.0+1.6.1 2001 PSF no - 2.0.1 2.0+1.6.1 2001 PSF yes - 2.1.1 2.1+2.0.1 2001 PSF yes - 2.1.2 2.1.1 2002 PSF yes - 2.1.3 2.1.2 2002 PSF yes - 2.2 and above 2.1.1 2001-now PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, - because its license has a choice of law clause. According to - CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 - is "not incompatible" with the GPL. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All -Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 -------------------------------------------- - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - -1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an -office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the -Individual or Organization ("Licensee") accessing and otherwise using -this software in source or binary form and its associated -documentation ("the Software"). - -2. Subject to the terms and conditions of this BeOpen Python License -Agreement, BeOpen hereby grants Licensee a non-exclusive, -royalty-free, world-wide license to reproduce, analyze, test, perform -and/or display publicly, prepare derivative works, distribute, and -otherwise use the Software alone or in any derivative version, -provided, however, that the BeOpen Python License is retained in the -Software, alone or in any derivative version prepared by Licensee. - -3. BeOpen is making the Software available to Licensee on an "AS IS" -basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE -SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS -AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY -DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -5. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -6. This License Agreement shall be governed by and interpreted in all -respects by the law of the State of California, excluding conflict of -law provisions. Nothing in this License Agreement shall be deemed to -create any relationship of agency, partnership, or joint venture -between BeOpen and Licensee. This License Agreement does not grant -permission to use BeOpen trademarks or trade names in a trademark -sense to endorse or promote products or services of Licensee, or any -third party. As an exception, the "BeOpen Python" logos available at -http://www.pythonlabs.com/logos.html may be used according to the -permissions granted on that web page. - -7. By copying, installing or otherwise using the software, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 ---------------------------------------- - -1. This LICENSE AGREEMENT is between the Corporation for National -Research Initiatives, having an office at 1895 Preston White Drive, -Reston, VA 20191 ("CNRI"), and the Individual or Organization -("Licensee") accessing and otherwise using Python 1.6.1 software in -source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, CNRI -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python 1.6.1 -alone or in any derivative version, provided, however, that CNRI's -License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) -1995-2001 Corporation for National Research Initiatives; All Rights -Reserved" are retained in Python 1.6.1 alone or in any derivative -version prepared by Licensee. Alternately, in lieu of CNRI's License -Agreement, Licensee may substitute the following text (omitting the -quotes): "Python 1.6.1 is made available subject to the terms and -conditions in CNRI's License Agreement. This Agreement together with -Python 1.6.1 may be located on the Internet using the following -unique, persistent identifier (known as a handle): 1895.22/1013. This -Agreement may also be obtained from a proxy server on the Internet -using the following URL: http://hdl.handle.net/1895.22/1013". - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python 1.6.1 or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python 1.6.1. - -4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" -basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. This License Agreement shall be governed by the federal -intellectual property law of the United States, including without -limitation the federal copyright law, and, to the extent such -U.S. federal law does not apply, by the law of the Commonwealth of -Virginia, excluding Virginia's conflict of law provisions. -Notwithstanding the foregoing, with regard to derivative works based -on Python 1.6.1 that incorporate non-separable material that was -previously distributed under the GNU General Public License (GPL), the -law of the Commonwealth of Virginia shall govern this License -Agreement only as to issues arising under or with respect to -Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this -License Agreement shall be deemed to create any relationship of -agency, partnership, or joint venture between CNRI and Licensee. This -License Agreement does not grant permission to use CNRI trademarks or -trade name in a trademark sense to endorse or promote products or -services of Licensee, or any third party. - -8. By clicking on the "ACCEPT" button where indicated, or by copying, -installing or otherwise using Python 1.6.1, Licensee agrees to be -bound by the terms and conditions of this License Agreement. - - ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 --------------------------------------------------- - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, -The Netherlands. All rights reserved. - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of Stichting Mathematisch -Centrum or CWI not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE -FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -""" - -__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom', - 'token_bytes', 'token_hex', 'token_urlsafe', - 'compare_digest', - ] - - -import base64 -import binascii -import os - -from hmac import compare_digest -from random import SystemRandom - -_sysrand = SystemRandom() - -randbits = _sysrand.getrandbits -choice = _sysrand.choice - -def randbelow(exclusive_upper_bound): - """Return a random int in the range [0, n).""" - if exclusive_upper_bound <= 0: - raise ValueError("Upper bound must be positive.") - return _sysrand._randbelow(exclusive_upper_bound) - -DEFAULT_ENTROPY = 32 # number of bytes to return by default - -def token_bytes(nbytes=None): - """Return a random byte string containing *nbytes* bytes. - - If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_bytes(16) #doctest:+SKIP - b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b' - - """ - if nbytes is None: - nbytes = DEFAULT_ENTROPY - return os.urandom(nbytes) - -def token_hex(nbytes=None): - """Return a random text string, in hexadecimal. - - The string has *nbytes* random bytes, each byte converted to two - hex digits. If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_hex(16) #doctest:+SKIP - 'f9bf78b9a18ce6d46a0cd2b0b86df9da' - - """ - return binascii.hexlify(token_bytes(nbytes)).decode('ascii') - -def token_urlsafe(nbytes=None): - """Return a random URL-safe text string, in Base64 encoding. - - The string has *nbytes* random bytes. If *nbytes* is ``None`` - or not supplied, a reasonable default is used. - - >>> token_urlsafe(16) #doctest:+SKIP - 'Drmhze6EPcv0fN_81Bj-nA' - - """ - tok = token_bytes(nbytes) - return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii') - diff --git a/onionr/etc/README.md b/onionr/etc/README.md new file mode 100755 index 00000000..9b310d28 --- /dev/null +++ b/onionr/etc/README.md @@ -0,0 +1,11 @@ +# etc + +Files that don't really fit anywhere else, but aren't used very frequently. + +## Files + +humanreadabletime.py: take integer seconds and return a human readable time string + +pgpwords.py: represent data using the pgp word list: https://en.wikipedia.org/wiki/PGP_word_list + +onionrvalues.py: spec values for onionr blocks and other things \ No newline at end of file diff --git a/onionr/etc/humanreadabletime.py b/onionr/etc/humanreadabletime.py new file mode 100755 index 00000000..7e3ff3bc --- /dev/null +++ b/onionr/etc/humanreadabletime.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + human_readable_time takes integer seconds and returns a human readable string +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def human_readable_time(seconds): + build = '' + + units = { + 'year' : 31557600, + 'month' : (31557600 / 12), + 'day' : 86400, + 'hour' : 3600, + 'minute' : 60, + 'second' : 1 + } + + for unit in units: + amnt_unit = int(seconds / units[unit]) + if amnt_unit >= 1: + seconds -= amnt_unit * units[unit] + build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' ' + + return build.strip() \ No newline at end of file diff --git a/onionr/etc/onionrvalues.py b/onionr/etc/onionrvalues.py index 84203d88..5df394db 100755 --- a/onionr/etc/onionrvalues.py +++ b/onionr/etc/onionrvalues.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file defines values and requirements used by Onionr ''' @@ -17,8 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +DENIABLE_PEER_ADDRESS = "OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====" class OnionrValues: def __init__(self): self.passwordLength = 20 - self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'powRandomToken': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed \ No newline at end of file + self.blockMetadataLengths = {'meta': 1000, 'sig': 200, 'signer': 200, 'time': 10, 'pow': 1000, 'encryptType': 4, 'expire': 14} #TODO properly refine values to minimum needed + self.default_expire = 2592000 + self.announce_pow = 5 \ No newline at end of file diff --git a/onionr/etc/pgpwords.py b/onionr/etc/pgpwords.py index d9738f3f..d045b65a 100755 --- a/onionr/etc/pgpwords.py +++ b/onionr/etc/pgpwords.py @@ -3,6 +3,14 @@ '''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license''' +''' +Changes made for Onionr by Kevin Froman in 2018-2019: +Minor changes such as slight word adjustment, line breaks + +CLI commands/usage function removed +hexify function added +''' + import os, re, sys, binascii _words = [ diff --git a/onionr/etc/powchoice.py b/onionr/etc/powchoice.py new file mode 100755 index 00000000..cf1c7990 --- /dev/null +++ b/onionr/etc/powchoice.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + This file does determinations for what proof of work module should be used +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import platform +def use_subprocess(core_inst): + use = True + if not core_inst.config.get('general.use_subprocess_pow_if_possible', True): + use = False + if 'Windows' == platform.system(): + use = False + return use \ No newline at end of file diff --git a/onionr/httpapi/README.md b/onionr/httpapi/README.md new file mode 100755 index 00000000..a4cf7eeb --- /dev/null +++ b/onionr/httpapi/README.md @@ -0,0 +1,13 @@ +# httpapi + +The httpapi contains collections of endpoints for the client and public API servers. + +## Files: + +configapi: manage onionr configuration from the client http api + +friendsapi: add, remove and list friends from the client http api + +miscpublicapi: misculanious onionr network interaction from the **public** httpapi, such as announcements, block fetching and uploading. + +profilesapi: work in progress in returning a profile page for an Onionr user \ No newline at end of file diff --git a/onionr/httpapi/__init__.py b/onionr/httpapi/__init__.py index 018d3abb..b3d6c3b3 100755 --- a/onionr/httpapi/__init__.py +++ b/onionr/httpapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file registers plugin's flask blueprints for the client http server ''' @@ -19,11 +19,11 @@ ''' import onionrplugins -def load_plugin_blueprints(flaskapp): +def load_plugin_blueprints(flaskapp, blueprint='flask_blueprint'): '''Iterate enabled plugins and load any http endpoints they have''' for plugin in onionrplugins.get_enabled_plugins(): plugin = onionrplugins.get_plugin(plugin) try: - flaskapp.register_blueprint(getattr(plugin, 'flask_blueprint')) + flaskapp.register_blueprint(getattr(plugin, blueprint)) except AttributeError: pass \ No newline at end of file diff --git a/onionr/httpapi/apiutils/__init__.py b/onionr/httpapi/apiutils/__init__.py new file mode 100644 index 00000000..cab13c98 --- /dev/null +++ b/onionr/httpapi/apiutils/__init__.py @@ -0,0 +1,3 @@ +from . import shutdown, setbindip, getblockdata + +GetBlockData = getblockdata.GetBlockData \ No newline at end of file diff --git a/onionr/httpapi/apiutils/getblockdata.py b/onionr/httpapi/apiutils/getblockdata.py new file mode 100644 index 00000000..881c0c89 --- /dev/null +++ b/onionr/httpapi/apiutils/getblockdata.py @@ -0,0 +1,42 @@ +import json +import core, onionrblockapi +from onionrutils import bytesconverter, stringvalidators +class GetBlockData: + def __init__(self, client_api_inst=None): + if client_api_inst is None: + self.client_api_inst = None + self.c = core.Core() + elif isinstance(client_api_inst, core.Core): + self.client_api_inst = None + self.c = client_api_inst + else: + self.client_api_Inst = client_api_inst + self.c = core.Core() + + def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False): + assert stringvalidators.validate_hash(bHash) + bl = onionrblockapi.Block(bHash, core=self.c) + if decrypt: + bl.decrypt() + if bl.isEncrypted and not bl.decrypted: + raise ValueError + + if not raw: + if not headerOnly: + retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} + for x in list(retData.keys()): + try: + retData[x] = retData[x].decode() + except AttributeError: + pass + else: + validSig = False + signer = bytesconverter.bytes_to_str(bl.signer) + if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer): + validSig = True + bl.bheader['validSig'] = validSig + bl.bheader['meta'] = '' + retData = {'meta': bl.bheader, 'metadata': bl.bmetadata} + return json.dumps(retData) + else: + return bl.raw \ No newline at end of file diff --git a/onionr/httpapi/apiutils/setbindip.py b/onionr/httpapi/apiutils/setbindip.py new file mode 100644 index 00000000..6a8e512b --- /dev/null +++ b/onionr/httpapi/apiutils/setbindip.py @@ -0,0 +1,25 @@ +import random, socket +import config, logger +def set_bind_IP(filePath='', core_inst=None): + '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' + if not core_inst is None: + config = core_inst.config + + if config.get('general.random_bind_ip', True): + hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] + data = '.'.join(hostOctets) + # Try to bind IP. Some platforms like Mac block non normal 127.x.x.x + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind((data, 0)) + except OSError: + # if mac/non-bindable, show warning and default to 127.0.0.1 + logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.') + data = '127.0.0.1' + s.close() + else: + data = '127.0.0.1' + if filePath != '': + with open(filePath, 'w') as bindFile: + bindFile.write(data) + return data \ No newline at end of file diff --git a/onionr/httpapi/apiutils/shutdown.py b/onionr/httpapi/apiutils/shutdown.py new file mode 100644 index 00000000..90cd33cb --- /dev/null +++ b/onionr/httpapi/apiutils/shutdown.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Shutdown the node either hard or cleanly +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, Response +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +shutdown_bp = Blueprint('shutdown', __name__) + +def shutdown(client_api_inst): + try: + client_api_inst.publicAPI.httpServer.stop() + client_api_inst.httpServer.stop() + except AttributeError: + pass + return Response("bye") + +@shutdown_bp.route('/shutdownclean') +def shutdown_clean(): + # good for calling from other clients + core.Core().daemonQueueAdd('shutdown') + return Response("bye") \ No newline at end of file diff --git a/onionr/httpapi/configapi/__init__.py b/onionr/httpapi/configapi/__init__.py new file mode 100755 index 00000000..d760ff7b --- /dev/null +++ b/onionr/httpapi/configapi/__init__.py @@ -0,0 +1,62 @@ +''' + Onionr - Private P2P Communication + + This file handles configuration setting and getting from the HTTP API +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import json +from flask import Blueprint, request, Response, abort +import config, onionrutils +config.reload() + +config_BP = Blueprint('config_BP', __name__) + +@config_BP.route('/config/get') +def get_all_config(): + '''Simply return all configuration as JSON string''' + return Response(json.dumps(config.get_config(), indent=4, sort_keys=True)) + +@config_BP.route('/config/get/') +def get_by_key(key): + '''Return a config setting by key''' + return Response(json.dumps(config.get(key))) + +@config_BP.route('/config/setall', methods=['POST']) +def set_all_config(): + '''Overwrite existing JSON config with new JSON string''' + try: + new_config = request.get_json(force=True) + except json.JSONDecodeError: + abort(400) + else: + config.set_config(new_config) + config.save() + return Response('success') + +@config_BP.route('/config/set/', methods=['POST']) +def set_by_key(key): + '''Overwrite/set only 1 config key''' + ''' + { + 'data': data + } + ''' + try: + data = json.loads(onionrutils.OnionrUtils.bytesToStr(request.data))['data'] + except (json.JSONDecodeError, KeyError): + abort(400) + config.set(key, data, True) + return Response('success') \ No newline at end of file diff --git a/onionr/httpapi/fdsafehandler.py b/onionr/httpapi/fdsafehandler.py new file mode 100644 index 00000000..ef45565e --- /dev/null +++ b/onionr/httpapi/fdsafehandler.py @@ -0,0 +1,21 @@ +from gevent.pywsgi import WSGIServer, WSGIHandler +from gevent import Timeout +class FDSafeHandler(WSGIHandler): + '''Our WSGI handler. Doesn't do much non-default except timeouts''' + def handle(self): + self.timeout = Timeout(120, Exception) + self.timeout.start() + try: + WSGIHandler.handle(self) + except Exception: + self.handle_error() + finally: + self.timeout.close() + + def handle_error(self): + if v is self.timeout: + self.result = [b"Timeout"] + self.start_response("200 OK", []) + self.process_result() + else: + WSGIHandler.handle_error(self) \ No newline at end of file diff --git a/onionr/httpapi/friendsapi/__init__.py b/onionr/httpapi/friendsapi/__init__.py index c935ded5..5b4a883b 100755 --- a/onionr/httpapi/friendsapi/__init__.py +++ b/onionr/httpapi/friendsapi/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file creates http endpoints for friend management ''' @@ -26,7 +26,8 @@ friends = Blueprint('friends', __name__) @friends.route('/friends/list') def list_friends(): pubkey_list = {} - friend_list = contactmanager.ContactManager.list_friends(core.Core()) + c = core.Core() + friend_list = contactmanager.ContactManager.list_friends(c) for friend in friend_list: pubkey_list[friend.publicKey] = {'name': friend.get_info('name')} return json.dumps(pubkey_list) diff --git a/onionr/httpapi/insertblock.py b/onionr/httpapi/insertblock.py new file mode 100644 index 00000000..d208bf70 --- /dev/null +++ b/onionr/httpapi/insertblock.py @@ -0,0 +1,63 @@ +''' + Onionr - Private P2P Communication + + Create blocks with the client api server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import json, threading +from flask import Blueprint, Response, request +import core + +ib = Blueprint('insertblock', __name__) + +@ib.route('/insertblock', methods=['POST']) +def client_api_insert_block(): + c = core.Core() + encrypt = False + bData = request.get_json(force=True) + message = bData['message'] + + # Detect if message (block body) is not specified + if type(message) is None: + return 'failure', 406 + + subject = 'temp' + encryptType = '' + sign = True + meta = {} + to = '' + try: + if bData['encrypt']: + to = bData['to'] + encrypt = True + encryptType = 'asym' + except KeyError: + pass + try: + if not bData['sign']: + sign = False + except KeyError: + pass + try: + bType = bData['type'] + except KeyError: + bType = 'bin' + try: + meta = json.loads(bData['meta']) + except KeyError: + pass + threading.Thread(target=c.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start() + return Response('success') \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/__init__.py b/onionr/httpapi/miscclientapi/__init__.py new file mode 100644 index 00000000..6ea8ea0f --- /dev/null +++ b/onionr/httpapi/miscclientapi/__init__.py @@ -0,0 +1 @@ +from . import getblocks, staticfiles, endpoints \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/endpoints.py b/onionr/httpapi/miscclientapi/endpoints.py new file mode 100644 index 00000000..8cbdd553 --- /dev/null +++ b/onionr/httpapi/miscclientapi/endpoints.py @@ -0,0 +1,112 @@ +''' + Onionr - Private P2P Communication + + Misc client API endpoints too small to need their own file and that need access to the client api inst +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Response, Blueprint, request, send_from_directory, abort +from httpapi import apiutils +class PrivateEndpoints: + def __init__(self, client_api): + private_endpoints_bp = Blueprint('privateendpoints', __name__) + self.private_endpoints_bp = private_endpoints_bp + config = client_api._core.config + + @private_endpoints_bp.route('/serviceactive/') + def serviceActive(pubkey): + try: + if pubkey in client_api._core.onionrInst.communicatorInst.active_services: + return Response('true') + except AttributeError as e: + pass + return Response('false') + + @private_endpoints_bp.route('/www/', endpoint='www') + def wwwPublic(path): + if not config.get("www.private.run", True): + abort(403) + return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) + + @private_endpoints_bp.route('/hitcount') + def get_hit_count(): + return Response(str(client_api.publicAPI.hitCount)) + + @private_endpoints_bp.route('/queueResponseAdd/', methods=['post']) + def queueResponseAdd(name): + # Responses from the daemon. TODO: change to direct var access instead of http endpoint + client_api.queueResponse[name] = request.form['data'] + return Response('success') + + @private_endpoints_bp.route('/queueResponse/') + def queueResponse(name): + # Fetch a daemon queue response + resp = 'failure' + try: + resp = client_api.queueResponse[name] + except KeyError: + pass + else: + del client_api.queueResponse[name] + if resp == 'failure': + return resp, 404 + else: + return resp + + @private_endpoints_bp.route('/ping') + def ping(): + # Used to check if client api is working + return Response("pong!") + + @private_endpoints_bp.route('/lastconnect') + def lastConnect(): + return Response(str(client_api.publicAPI.lastRequest)) + + @private_endpoints_bp.route('/waitforshare/', methods=['post']) + def waitforshare(name): + '''Used to prevent the **public** api from sharing blocks we just created''' + assert name.isalnum() + if name in client_api.publicAPI.hideBlocks: + client_api.publicAPI.hideBlocks.remove(name) + return Response("removed") + else: + client_api.publicAPI.hideBlocks.append(name) + return Response("added") + + @private_endpoints_bp.route('/shutdown') + def shutdown(): + return apiutils.shutdown.shutdown(client_api) + + @private_endpoints_bp.route('/getstats') + def getStats(): + # returns node stats + #return Response("disabled") + while True: + try: + return Response(client_api._core.serializer.getStats()) + except AttributeError: + pass + + @private_endpoints_bp.route('/getuptime') + def showUptime(): + return Response(str(client_api.getUptime())) + + @private_endpoints_bp.route('/getActivePubkey') + def getActivePubkey(): + return Response(client_api._core._crypto.pubKey) + + @private_endpoints_bp.route('/getHumanReadable/') + def getHumanReadable(name): + return Response(mnemonickeys.get_human_readable_ID(name)) \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/getblocks.py b/onionr/httpapi/miscclientapi/getblocks.py new file mode 100644 index 00000000..f9a6aa90 --- /dev/null +++ b/onionr/httpapi/miscclientapi/getblocks.py @@ -0,0 +1,66 @@ +''' + Onionr - Private P2P Communication + + Create blocks with the client api server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, Response, abort +import core, onionrblockapi +from httpapi import apiutils +from onionrutils import stringvalidators + +c = core.Core() + +client_get_block = apiutils.GetBlockData(c) + +client_get_blocks = Blueprint('miscclient', __name__) + +@client_get_blocks.route('/getblocksbytype/') +def getBlocksByType(name): + blocks = c.getBlocksByType(name) + return Response(','.join(blocks)) + +@client_get_blocks.route('/getblockbody/') +def getBlockBodyData(name): + resp = '' + if stringvalidators.validate_hash(name): + try: + resp = onionrblockapi.Block(name, decrypt=True, core=c).bcontent + except TypeError: + pass + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockdata/') +def getData(name): + resp = "" + if stringvalidators.validate_hash(name): + if name in c.getBlockList(): + try: + resp = client_get_block.get_block_data(name, decrypt=True) + except ValueError: + pass + else: + abort(404) + else: + abort(404) + return Response(resp) + +@client_get_blocks.route('/getblockheader/') +def getBlockHeader(name): + resp = client_get_block.get_block_data(name, decrypt=True, headerOnly=True) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/miscclientapi/staticfiles.py b/onionr/httpapi/miscclientapi/staticfiles.py new file mode 100644 index 00000000..a89f0cc1 --- /dev/null +++ b/onionr/httpapi/miscclientapi/staticfiles.py @@ -0,0 +1,70 @@ +''' + Onionr - Private P2P Communication + + Register static file routes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os +from flask import Blueprint, send_from_directory + +static_files_bp = Blueprint('staticfiles', __name__) + +root = os.getcwd() + '/static-data/www/' # should be set to onionr install directory from onionr startup + +@static_files_bp.route('/board/', endpoint='board') +def loadBoard(): + return send_from_directory(root + 'board/', "index.html") + +@static_files_bp.route('/mail/', endpoint='mail') +def loadMail(path): + return send_from_directory(root + 'mail/', path) + +@static_files_bp.route('/mail/', endpoint='mailindex') +def loadMailIndex(): + return send_from_directory(root + 'mail/', 'index.html') + +@static_files_bp.route('/friends/', endpoint='friends') +def loadContacts(path): + return send_from_directory(root + 'friends/', path) + +@static_files_bp.route('/friends/', endpoint='friendsindex') +def loadContacts(): + return send_from_directory(root + 'friends/', 'index.html') + +@static_files_bp.route('/profiles/', endpoint='profiles') +def loadContacts(path): + return send_from_directory(root + 'profiles/', path) + +@static_files_bp.route('/profiles/', endpoint='profilesindex') +def loadContacts(): + return send_from_directory(root + 'profiles/', 'index.html') + +@static_files_bp.route('/board/', endpoint='boardContent') +def boardContent(path): + return send_from_directory(root + 'board/', path) + +@static_files_bp.route('/shared/', endpoint='sharedContent') +def sharedContent(path): + return send_from_directory(root + 'shared/', path) + +@static_files_bp.route('/', endpoint='onionrhome') +def hello(): + # ui home + return send_from_directory(root + 'private/', 'index.html') + +@static_files_bp.route('/private/', endpoint='homedata') +def homedata(path): + return send_from_directory(root + 'private/', path) \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/__init__.py b/onionr/httpapi/miscpublicapi/__init__.py new file mode 100755 index 00000000..2f5f9c56 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/__init__.py @@ -0,0 +1,6 @@ +from . import announce, upload, getblocks, endpoints + +announce = announce.handle_announce # endpoint handler for accepting peer announcements +upload = upload.accept_upload # endpoint handler for accepting public uploads +public_block_list = getblocks.get_public_block_list # endpoint handler for getting block lists +public_get_block_data = getblocks.get_block_data # endpoint handler for responding to peers requests for block data \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/announce.py b/onionr/httpapi/miscpublicapi/announce.py new file mode 100755 index 00000000..060bead4 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/announce.py @@ -0,0 +1,65 @@ +''' + Onionr - Private P2P Communication + + Handle announcements to the public API server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64 +from flask import Response +import logger +from etc import onionrvalues +from onionrutils import stringvalidators, bytesconverter + +def handle_announce(clientAPI, request): + ''' + accept announcement posts, validating POW + clientAPI should be an instance of the clientAPI server running, request is a instance of a flask request + ''' + resp = 'failure' + powHash = '' + randomData = '' + newNode = '' + ourAdder = clientAPI._core.hsAddress.encode() + try: + newNode = request.form['node'].encode() + except KeyError: + logger.warn('No node specified for upload') + pass + else: + try: + randomData = request.form['random'] + randomData = base64.b64decode(randomData) + except KeyError: + logger.warn('No random data specified for upload') + else: + nodes = newNode + clientAPI._core.hsAddress.encode() + nodes = clientAPI._core._crypto.blake2bHash(nodes) + powHash = clientAPI._core._crypto.blake2bHash(randomData + nodes) + try: + powHash = powHash.decode() + except AttributeError: + pass + if powHash.startswith('0' * onionrvalues.OnionrValues().announce_pow): + newNode = bytesconverter.bytes_to_str(newNode) + if stringvalidators.validate_transport(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers: + clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode) + resp = 'Success' + else: + logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) + resp = Response(resp) + if resp == 'failure': + return resp, 406 + return resp \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/endpoints.py b/onionr/httpapi/miscpublicapi/endpoints.py new file mode 100644 index 00000000..05e824a6 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/endpoints.py @@ -0,0 +1,80 @@ +''' + Onionr - Private P2P Communication + + Misc public API endpoints too small to need their own file and that need access to the public api inst +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Response, Blueprint, request, send_from_directory, abort +from . import getblocks, upload, announce +class PublicEndpoints: + def __init__(self, public_api): + client_API = public_api.clientAPI + config = client_API._core.config + + public_endpoints_bp = Blueprint('publicendpoints', __name__) + self.public_endpoints_bp = public_endpoints_bp + + @public_endpoints_bp.route('/') + def banner(): + # Display a bit of information to people who visit a node address in their browser + try: + with open('static-data/index.html', 'r') as html: + resp = Response(html.read(), mimetype='text/html') + except FileNotFoundError: + resp = Response("") + return resp + + @public_endpoints_bp.route('/getblocklist') + def get_block_list(): + '''Get a list of blocks, optionally filtered by epoch time stamp, excluding those hidden''' + return getblocks.get_public_block_list(client_API, public_api, request) + + @public_endpoints_bp.route('/getdata/') + def get_block_data(name): + # Share data for a block if we have it and it isn't hidden + return getblocks.get_block_data(client_API, public_api, name) + + @public_endpoints_bp.route('/www/') + def www_public(path): + # A way to share files directly over your .onion + if not config.get("www.public.run", True): + abort(403) + return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) + + @public_endpoints_bp.route('/ping') + def ping(): + # Endpoint to test if nodes are up + return Response("pong!") + + @public_endpoints_bp.route('/pex') + def peer_exchange(): + response = ','.join(client_API._core.listAdders(recent=3600)) + if len(response) == 0: + response = '' + return Response(response) + + @public_endpoints_bp.route('/announce', methods=['post']) + def accept_announce(): + '''Accept announcements with pow token to prevent spam''' + resp = announce.handle_announce(client_API, request) + return resp + + @public_endpoints_bp.route('/upload', methods=['post']) + def upload_endpoint(): + '''Accept file uploads. In the future this will be done more often than on creation + to speed up block sync + ''' + return upload.accept_upload(client_API, request) \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/getblocks.py b/onionr/httpapi/miscpublicapi/getblocks.py new file mode 100755 index 00000000..8fe5cbf4 --- /dev/null +++ b/onionr/httpapi/miscpublicapi/getblocks.py @@ -0,0 +1,52 @@ +''' + Onionr - Private P2P Communication + + Public endpoints to get block data and lists +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Response, abort +import config +from onionrutils import bytesconverter, stringvalidators + +def get_public_block_list(clientAPI, publicAPI, request): + # Provide a list of our blocks, with a date offset + dateAdjust = request.args.get('date') + bList = clientAPI._core.getBlockList(dateRec=dateAdjust) + if clientAPI._core.config.get('general.hide_created_blocks', True): + for b in publicAPI.hideBlocks: + if b in bList: + # Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block + bList.remove(b) + return Response('\n'.join(bList)) + +def get_block_data(clientAPI, publicAPI, data): + '''data is the block hash in hex''' + resp = '' + if stringvalidators.validate_hash(data): + if not clientAPI._core.config.get('general.hide_created_blocks', True) or data not in publicAPI.hideBlocks: + if data in clientAPI._core.getBlockList(): + block = clientAPI.getBlockData(data, raw=True) + try: + block = block.encode() # Encode in case data is binary + except AttributeError: + abort(404) + block = bytesconverter.str_to_bytes(block) + resp = block + if len(resp) == 0: + abort(404) + resp = "" + # Has to be octet stream, otherwise binary data fails hash check + return Response(resp, mimetype='application/octet-stream') \ No newline at end of file diff --git a/onionr/httpapi/miscpublicapi/upload.py b/onionr/httpapi/miscpublicapi/upload.py new file mode 100755 index 00000000..5fd0f32c --- /dev/null +++ b/onionr/httpapi/miscpublicapi/upload.py @@ -0,0 +1,43 @@ +''' + Onionr - Private P2P Communication + + Accept block uploads to the public API server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys +from flask import Response, abort +import blockimporter, onionrexceptions, logger +def accept_upload(clientAPI, request): + resp = 'failure' + try: + data = request.form['block'] + except KeyError: + logger.warn('No block specified for upload') + pass + else: + if sys.getsizeof(data) < 100000000: + try: + if blockimporter.importBlockFromData(data, clientAPI._core): + resp = 'success' + else: + logger.warn('Error encountered importing uploaded block') + except onionrexceptions.BlacklistedBlock: + logger.debug('uploaded block is blacklisted') + pass + if resp == 'failure': + abort(400) + resp = Response(resp) + return resp \ No newline at end of file diff --git a/onionr/httpapi/onionrsitesapi/__init__.py b/onionr/httpapi/onionrsitesapi/__init__.py new file mode 100644 index 00000000..82805192 --- /dev/null +++ b/onionr/httpapi/onionrsitesapi/__init__.py @@ -0,0 +1,44 @@ +''' + Onionr - Private P2P Communication + + view and interact with onionr sites +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64 +from flask import Blueprint, Response, request, abort +import core, onionrblockapi, onionrexceptions +from onionrutils import stringvalidators + +site_api = Blueprint('siteapi', __name__) + +@site_api.route('/site/', endpoint='site') +def site(name): + bHash = name + resp = 'Not Found' + if stringvalidators.validate_hash(bHash): + try: + resp = onionrblockapi.Block(bHash).bcontent + except onionrexceptions.NoDataAvailable: + abort(404) + except TypeError: + pass + try: + resp = base64.b64decode(resp) + except: + pass + if resp == 'Not Found' or not resp: + abort(404) + return Response(resp) \ No newline at end of file diff --git a/onionr/httpapi/simplecache/__init__.py b/onionr/httpapi/profilesapi/__init__.py similarity index 71% rename from onionr/httpapi/simplecache/__init__.py rename to onionr/httpapi/profilesapi/__init__.py index 75a645a0..681212be 100755 --- a/onionr/httpapi/simplecache/__init__.py +++ b/onionr/httpapi/profilesapi/__init__.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This file creates http endpoints for friend management + This file creates http endpoints for user profile pages ''' ''' This program is free software: you can redistribute it and/or modify @@ -19,13 +19,10 @@ ''' import core from flask import Blueprint, Response, request, abort +from . import profiles -simplecache = Blueprint('simplecache', __name__) +profile_BP = Blueprint('profile_BP', __name__) -@simplecache.route('/get/') -def get_key(key): - return - -@simplecache.route('/set/', methods=['POST']) -def set_key(key): - return \ No newline at end of file +@profile_BP.route('/profile/get/', endpoint='profiles') +def get_profile_page(pubkey): + return Response(pubkey) \ No newline at end of file diff --git a/onionr/httpapi/profilesapi/profiles.py b/onionr/httpapi/profilesapi/profiles.py new file mode 100755 index 00000000..11a73b13 --- /dev/null +++ b/onionr/httpapi/profilesapi/profiles.py @@ -0,0 +1,2 @@ +def get_latest_user_profile(pubkey): + return '' \ No newline at end of file diff --git a/onionr/httpapi/security/__init__.py b/onionr/httpapi/security/__init__.py new file mode 100644 index 00000000..653f6af4 --- /dev/null +++ b/onionr/httpapi/security/__init__.py @@ -0,0 +1 @@ +from . import client, public \ No newline at end of file diff --git a/onionr/httpapi/security/client.py b/onionr/httpapi/security/client.py new file mode 100644 index 00000000..4191e67f --- /dev/null +++ b/onionr/httpapi/security/client.py @@ -0,0 +1,61 @@ +''' + Onionr - Private P2P Communication + + Process incoming requests to the client api server to validate they are legitimate +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import hmac +from flask import Blueprint, request, abort +from onionrservices import httpheaders +# Be extremely mindful of this. These are endpoints available without a password +whitelist_endpoints = ('siteapi.site', 'www', 'staticfiles.onionrhome', 'staticfiles.homedata', +'staticfiles.board', 'staticfiles.profiles', +'staticfiles.profilesindex', +'staticfiles.boardContent', 'staticfiles.sharedContent', +'staticfiles.mail', 'staticfiles.mailindex', 'staticfiles.friends', 'staticfiles.friendsindex', +'staticfiles.clandestine', 'staticfiles.clandestineIndex') + +class ClientAPISecurity: + def __init__(self, client_api): + client_api_security_bp = Blueprint('clientapisecurity', __name__) + self.client_api_security_bp = client_api_security_bp + self.client_api = client_api + + @client_api_security_bp.before_app_request + def validate_request(): + '''Validate request has set password and is the correct hostname''' + # For the purpose of preventing DNS rebinding attacks + if request.host != '%s:%s' % (client_api.host, client_api.bindPort): + abort(403) + if request.endpoint in whitelist_endpoints: + return + try: + if not hmac.compare_digest(request.headers['token'], client_api.clientToken): + if not hmac.compare_digest(request.form['token'], client_api.clientToken): + abort(403) + except KeyError: + if not hmac.compare_digest(request.form['token'], client_api.clientToken): + abort(403) + + @client_api_security_bp.after_app_request + def after_req(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + if request.endpoint == 'site': + resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + else: + resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'" + return resp \ No newline at end of file diff --git a/onionr/httpapi/security/public.py b/onionr/httpapi/security/public.py new file mode 100644 index 00000000..5e580792 --- /dev/null +++ b/onionr/httpapi/security/public.py @@ -0,0 +1,50 @@ +''' + Onionr - Private P2P Communication + + Process incoming requests to the public api server for certain attacks +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from flask import Blueprint, request, abort +from onionrservices import httpheaders +from onionrutils import epoch + +class PublicAPISecurity: + def __init__(self, public_api): + public_api_security_bp = Blueprint('publicapisecurity', __name__) + self.public_api_security_bp = public_api_security_bp + + @public_api_security_bp.before_app_request + def validate_request(): + '''Validate request has the correct hostname''' + # If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P) + if public_api.config.get('general.security_level', default=1) > 0: + abort(403) + if type(public_api.torAdder) is None and type(public_api.i2pAdder) is None: + # abort if our hs addresses are not known + abort(403) + if request.host not in (public_api.i2pAdder, public_api.torAdder): + # Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks + abort(403) + public_api.hitCount += 1 # raise hit count for valid requests + + @public_api_security_bp.after_app_request + def send_headers(resp): + '''Send api, access control headers''' + resp = httpheaders.set_default_onionr_http_headers(resp) + # Network API version + resp.headers['X-API'] = public_api.API_VERSION + public_api.lastRequest = epoch.get_rounded_epoch(roundS=5) + return resp \ No newline at end of file diff --git a/onionr/keymanager.py b/onionr/keymanager.py index ff89401b..a288d87e 100755 --- a/onionr/keymanager.py +++ b/onionr/keymanager.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Load, save, and delete the user's public key pairs (does not handle peer keys) ''' @@ -17,20 +17,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from onionrutils import bytesconverter import onionrcrypto class KeyManager: def __init__(self, crypto): assert isinstance(crypto, onionrcrypto.OnionrCrypto) self._core = crypto._core - self._utils = self._core._utils self.keyFile = crypto._keyFile self.crypto = crypto def addKey(self, pubKey=None, privKey=None): if type(pubKey) is type(None) and type(privKey) is type(None): pubKey, privKey = self.crypto.generatePubKey() - pubKey = self.crypto._core._utils.bytesToStr(pubKey) - privKey = self.crypto._core._utils.bytesToStr(privKey) + pubKey = bytesconverter.bytes_to_str(pubKey) + privKey = bytesconverter.bytes_to_str(privKey) try: if pubKey in self.getPubkeyList(): raise ValueError('Pubkey already in list: %s' % (pubKey,)) diff --git a/onionr/logger.py b/onionr/logger.py deleted file mode 100755 index 34dc4d06..00000000 --- a/onionr/logger.py +++ /dev/null @@ -1,250 +0,0 @@ -''' - Onionr - P2P Microblogging Platform & Social network - - This file handles all operations involving logging -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -import re, sys, time, traceback, os - -class colors: - ''' - This class allows you to set the color if ANSI codes are supported - ''' - reset='\033[0m' - bold='\033[01m' - disable='\033[02m' - underline='\033[04m' - reverse='\033[07m' - strikethrough='\033[09m' - invisible='\033[08m' - italics='\033[3m' - class fg: - black='\033[30m' - red='\033[31m' - green='\033[32m' - orange='\033[33m' - blue='\033[34m' - purple='\033[35m' - cyan='\033[36m' - lightgrey='\033[37m' - darkgrey='\033[90m' - lightred='\033[91m' - lightgreen='\033[92m' - yellow='\033[93m' - lightblue='\033[94m' - pink='\033[95m' - lightcyan='\033[96m' - class bg: - black='\033[40m' - red='\033[41m' - green='\033[42m' - orange='\033[43m' - blue='\033[44m' - purple='\033[45m' - cyan='\033[46m' - lightgrey='\033[47m' - @staticmethod - def filter(data): - return re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]').sub('', str(data)) - -''' - Use the bitwise operators to merge these settings -''' -USE_ANSI = 0b100 -if os.name == 'nt': - USE_ANSI = 0b000 -OUTPUT_TO_CONSOLE = 0b010 -OUTPUT_TO_FILE = 0b001 - -LEVEL_DEBUG = 1 -LEVEL_INFO = 2 -LEVEL_WARN = 3 -LEVEL_ERROR = 4 -LEVEL_FATAL = 5 -LEVEL_IMPORTANT = 6 - -_type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging -_level = LEVEL_DEBUG # the lowest level to log -_outputfile = 'data/onionr.log' # the file to log to - -def set_settings(type): - ''' - Set the settings for the logger using bitwise operators - ''' - - global _type - _type = type - -def get_settings(): - ''' - Get settings from the logger - ''' - - return _type - -def set_level(level): - ''' - Set the lowest log level to output - ''' - - global _level - _level = level - -def get_level(): - ''' - Get the lowest log level currently being outputted - ''' - - return _level - -def set_file(outputfile): - ''' - Set the file to output to, if enabled - ''' - - global _outputfile - _outputfile = outputfile - -def get_file(): - ''' - Get the file to output to - ''' - - return _outputfile - -def raw(data, fd = sys.stdout, sensitive = False): - ''' - Outputs raw data to console without formatting - ''' - - if get_settings() & OUTPUT_TO_CONSOLE: - ts = fd.write('%s\n' % data) - if get_settings() & OUTPUT_TO_FILE and not sensitive: - try: - with open(_outputfile, "a+") as f: - f.write(colors.filter(data) + '\n') - except OSError: - pass - -def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False): - ''' - Logs the data - prefix : The prefix to the output - data : The actual data to output - color : The color to output before the data - ''' - curTime = '' - if timestamp: - curTime = time.strftime("%m-%d %H:%M:%S") + ' ' - - output = colors.reset + str(color) + ('[' + colors.bold + str(prefix) + colors.reset + str(color) + '] ' if prompt is True else '') + curTime + str(data) + colors.reset - if not get_settings() & USE_ANSI: - output = colors.filter(output) - - raw(output, fd = fd, sensitive = sensitive) - -def readline(message = ''): - ''' - Takes in input from the console, not stored in logs - message: The message to display before taking input - ''' - - color = colors.fg.green + colors.bold - output = colors.reset + str(color) + '... ' + colors.reset + str(message) + colors.reset - - if not get_settings() & USE_ANSI: - output = colors.filter(output) - - sys.stdout.write(output) - - return input() - -def confirm(default = 'y', message = 'Are you sure %s? '): - ''' - Displays an "Are you sure" message, returns True for Y and False for N - message: The confirmation message, use %s for (y/n) - default: which to prefer-- y or n - ''' - - color = colors.fg.green + colors.bold - - default = default.lower() - confirm = colors.bold - if default.startswith('y'): - confirm += '(Y/n)' - else: - confirm += '(y/N)' - confirm += colors.reset + color - - output = colors.reset + str(color) + '... ' + colors.reset + str(message) + colors.reset - - if not get_settings() & USE_ANSI: - output = colors.filter(output) - - sys.stdout.write(output.replace('%s', confirm)) - - inp = input().lower() - - if 'y' in inp: - return True - if 'n' in inp: - return False - else: - return default == 'y' - -# debug: when there is info that could be useful for debugging purposes only -def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG): - if get_level() <= level: - log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive) - if not error is None: - debug('Error: ' + str(error) + parse_error()) - -# info: when there is something to notify the user of, such as the success of a process -def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO): - if get_level() <= level: - log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive) - -# warn: when there is a potential for something bad to happen -def warn(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_WARN): - if not error is None: - debug('Error: ' + str(error) + parse_error()) - if get_level() <= level: - log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive) - -# error: when only one function, module, or process of the program encountered a problem and must stop -def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR): - if get_level() <= level: - log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) - if not error is None: - debug('Error: ' + str(error) + parse_error()) - -# fatal: when the something so bad has happened that the program must stop -def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False, level = LEVEL_FATAL): - if not error is None: - debug('Error: ' + str(error) + parse_error(), sensitive = sensitive) - if get_level() <= level: - log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) - -# returns a formatted error message -def parse_error(): - details = traceback.extract_tb(sys.exc_info()[2]) - output = '' - - for line in details: - output += '\n ... module %s in %s:%i' % (line[2], line[0], line[1]) - - return output diff --git a/onionr/logger/__init__.py b/onionr/logger/__init__.py new file mode 100755 index 00000000..c975927d --- /dev/null +++ b/onionr/logger/__init__.py @@ -0,0 +1,71 @@ +''' + Onionr - Private P2P Communication + + This file handles all operations involving logging +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +import sys, traceback + +from . import colors, readline, log, raw, confirm, colors, settings +colors = colors.Colors +readline = readline.readline +log = log.log +raw = raw.raw +confirm = confirm.confirm + +# debug: when there is info that could be useful for debugging purposes only +def debug(data, error = None, timestamp = True, prompt = True, terminal = False, level = settings.LEVEL_DEBUG): + if settings.get_level() <= level: + log('/', data, timestamp = timestamp, prompt = prompt, terminal = terminal) + if not error is None: + debug('Error: ' + str(error) + parse_error()) + +# info: when there is something to notify the user of, such as the success of a process +def info(data, timestamp = False, prompt = True, terminal = False, level = settings.LEVEL_INFO): + if settings.get_level() <= level: + log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, terminal = terminal) + +# warn: when there is a potential for something bad to happen +def warn(data, error = None, timestamp = True, prompt = True, terminal = False, level = settings.LEVEL_WARN): + if not error is None: + debug('Error: ' + str(error) + parse_error()) + if settings.get_level() <= level: + log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, terminal = terminal) + +# error: when only one function, module, or process of the program encountered a problem and must stop +def error(data, error = None, timestamp = True, prompt = True, terminal = False, level = settings.LEVEL_ERROR): + if settings.get_level() <= level: + log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, terminal = terminal) + if not error is None: + debug('Error: ' + str(error) + parse_error()) + +# fatal: when the something so bad has happened that the program must stop +def fatal(data, error = None, timestamp=True, prompt = True, terminal = False, level = settings.LEVEL_FATAL): + if not error is None: + debug('Error: ' + str(error) + parse_error(), terminal = terminal) + if get_level() <= level: + log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, terminal = terminal) + +# returns a formatted error message +def parse_error(): + details = traceback.extract_tb(sys.exc_info()[2]) + output = '' + + for line in details: + output += '\n ... module %s in %s:%i' % (line[2], line[0], line[1]) + + return output diff --git a/onionr/logger/colors.py b/onionr/logger/colors.py new file mode 100644 index 00000000..03da5c70 --- /dev/null +++ b/onionr/logger/colors.py @@ -0,0 +1,60 @@ +''' + Onionr - Private P2P Communication + + class to access ANSI control codes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import re +class Colors: + ''' + This class allows you to set the color if ANSI codes are supported + ''' + reset='\033[0m' + bold='\033[01m' + disable='\033[02m' + underline='\033[04m' + reverse='\033[07m' + strikethrough='\033[09m' + invisible='\033[08m' + italics='\033[3m' + class fg: + black='\033[30m' + red='\033[31m' + green='\033[32m' + orange='\033[33m' + blue='\033[34m' + purple='\033[35m' + cyan='\033[36m' + lightgrey='\033[37m' + darkgrey='\033[90m' + lightred='\033[91m' + lightgreen='\033[92m' + yellow='\033[93m' + lightblue='\033[94m' + pink='\033[95m' + lightcyan='\033[96m' + class bg: + black='\033[40m' + red='\033[41m' + green='\033[42m' + orange='\033[43m' + blue='\033[44m' + purple='\033[45m' + cyan='\033[46m' + lightgrey='\033[47m' + @staticmethod + def filter(data): + return re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]').sub('', str(data)) \ No newline at end of file diff --git a/onionr/logger/confirm.py b/onionr/logger/confirm.py new file mode 100644 index 00000000..85fcd725 --- /dev/null +++ b/onionr/logger/confirm.py @@ -0,0 +1,54 @@ +''' + Onionr - Private P2P Communication + + confirm y/n cli prompt +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys +from . import colors, settings +colors = colors.Colors +def confirm(default = 'y', message = 'Are you sure %s? '): + ''' + Displays an "Are you sure" message, returns True for Y and False for N + message: The confirmation message, use %s for (y/n) + default: which to prefer-- y or n + ''' + + color = colors.fg.green + colors.bold + + default = default.lower() + confirm = colors.bold + if default.startswith('y'): + confirm += '(Y/n)' + else: + confirm += '(y/N)' + confirm += colors.reset + color + + output = colors.reset + str(color) + '... ' + colors.reset + str(message) + colors.reset + + if not get_settings() & settings.USE_ANSI: + output = colors.filter(output) + + sys.stdout.write(output.replace('%s', confirm)) + + inp = input().lower() + + if 'y' in inp: + return True + if 'n' in inp: + return False + else: + return default == 'y' \ No newline at end of file diff --git a/onionr/logger/log.py b/onionr/logger/log.py new file mode 100644 index 00000000..c0e2ba69 --- /dev/null +++ b/onionr/logger/log.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + god log function +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys, time +from . import colors, raw, settings +colors = colors.Colors +def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, terminal = False): + ''' + Logs the data + prefix : The prefix to the output + data : The actual data to output + color : The color to output before the data + ''' + curTime = '' + if timestamp: + curTime = time.strftime("%m-%d %H:%M:%S") + ' ' + + output = colors.reset + str(color) + ('[' + colors.bold + str(prefix) + colors.reset + str(color) + '] ' if prompt is True else '') + curTime + str(data) + colors.reset + if not settings.get_settings() & settings.USE_ANSI: + output = colors.filter(output) + + raw.raw(output, fd = fd, terminal = terminal) \ No newline at end of file diff --git a/onionr/logger/raw.py b/onionr/logger/raw.py new file mode 100644 index 00000000..f6c43b6f --- /dev/null +++ b/onionr/logger/raw.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Output raw data to file or terminal +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys +from . import settings, colors +colors = colors.Colors +def raw(data, fd = sys.stdout, terminal = False): + ''' + Outputs raw data to console without formatting + ''' + + if terminal and (settings.get_settings() & settings.OUTPUT_TO_CONSOLE): + try: + ts = fd.write('%s\n' % data) + except OSError: + pass + if settings.get_settings() & settings.OUTPUT_TO_FILE: + try: + with open(settings._outputfile, "a+") as f: + f.write(colors.filter(data) + '\n') + except OSError: + pass \ No newline at end of file diff --git a/onionr/logger/readline.py b/onionr/logger/readline.py new file mode 100644 index 00000000..73ed4f60 --- /dev/null +++ b/onionr/logger/readline.py @@ -0,0 +1,37 @@ +''' + Onionr - Private P2P Communication + + get a line of input from stdin +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys +from . import colors, settings +colors = colors.Colors +def readline(message = ''): + ''' + Takes in input from the console, not stored in logs + message: The message to display before taking input + ''' + + color = colors.fg.green + colors.bold + output = colors.reset + str(color) + '... ' + colors.reset + str(message) + colors.reset + + if not settings.get_settings() & settings.USE_ANSI: + output = colors.filter(output) + + sys.stdout.write(output) + + return input() \ No newline at end of file diff --git a/onionr/logger/settings.py b/onionr/logger/settings.py new file mode 100644 index 00000000..98f11357 --- /dev/null +++ b/onionr/logger/settings.py @@ -0,0 +1,85 @@ +''' + Onionr - Private P2P Communication + + logger settings +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os +from utils import identifyhome + +data_home = os.environ.get('ONIONR_LOG_DIR', identifyhome.identify_home()) +# Use the bitwise operators to merge these settings +USE_ANSI = 0b100 +if os.name == 'nt': + USE_ANSI = 0b000 +OUTPUT_TO_CONSOLE = 0b010 +OUTPUT_TO_FILE = 0b001 + +LEVEL_DEBUG = 1 +LEVEL_INFO = 2 +LEVEL_WARN = 3 +LEVEL_ERROR = 4 +LEVEL_FATAL = 5 +LEVEL_IMPORTANT = 6 + +_type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging +_level = LEVEL_DEBUG # the lowest level to log +_outputfile = '%s/onionr.log' % (data_home,) # the file to log to + +def set_settings(type): + ''' + Set the settings for the logger using bitwise operators + ''' + + global _type + _type = type + +def get_settings(): + ''' + Get settings from the logger + ''' + + return _type + +def set_level(level): + ''' + Set the lowest log level to output + ''' + + global _level + _level = level + +def get_level(): + ''' + Get the lowest log level currently being outputted + ''' + + return _level + +def set_file(outputfile): + ''' + Set the file to output to, if enabled + ''' + + global _outputfile + _outputfile = outputfile + +def get_file(): + ''' + Get the file to output to + ''' + + return _outputfile \ No newline at end of file diff --git a/onionr/netcontroller/__init__.py b/onionr/netcontroller/__init__.py new file mode 100755 index 00000000..c9a0337e --- /dev/null +++ b/onionr/netcontroller/__init__.py @@ -0,0 +1,4 @@ +from . import torbinary, getopenport, netcontrol +tor_binary = torbinary.tor_binary +get_open_port = getopenport.get_open_port +NetController = netcontrol.NetController \ No newline at end of file diff --git a/onionr/netcontroller/getopenport.py b/onionr/netcontroller/getopenport.py new file mode 100644 index 00000000..97d64ff7 --- /dev/null +++ b/onionr/netcontroller/getopenport.py @@ -0,0 +1,29 @@ +''' + Onionr - Private P2P Communication + + get an open port +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import socket +def get_open_port(): + # taken from (but modified) https://stackoverflow.com/a/2838309 by https://stackoverflow.com/users/133374/albert ccy-by-sa-3 https://creativecommons.org/licenses/by-sa/3.0/ + # changes from source: import moved to top of file, bind specifically to localhost + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("127.0.0.1",0)) + s.listen(1) + port = s.getsockname()[1] + s.close() + return port \ No newline at end of file diff --git a/onionr/netcontroller.py b/onionr/netcontroller/netcontrol.py old mode 100755 new mode 100644 similarity index 73% rename from onionr/netcontroller.py rename to onionr/netcontroller/netcontrol.py index 4f6281c9..e0b54804 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller/netcontrol.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication Netcontroller library, used to control/work with Tor/I2P and send requests through them ''' @@ -17,29 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - -import subprocess, os, random, sys, logger, time, signal, config, base64, socket -from stem.control import Controller -from onionrblockapi import Block -from dependencies import secrets -from shutil import which - -def getOpenPort(): - # taken from (but modified) https://stackoverflow.com/a/2838309 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("127.0.0.1",0)) - s.listen(1) - port = s.getsockname()[1] - s.close() - return port - -def torBinary(): - '''Return tor binary path or none if not exists''' - torPath = './tor' - if not os.path.exists(torPath): - torPath = which('tor') - return torPath - +import os, sys, base64, subprocess, signal +import config, logger +from . import getopenport +from utils import identifyhome +config.reload() class NetController: ''' This class handles hidden service setup on Tor and I2P @@ -47,13 +29,11 @@ class NetController: def __init__(self, hsPort, apiServerIP='127.0.0.1'): # set data dir - self.dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) - if not self.dataDir.endswith('/'): - self.dataDir += '/' + self.dataDir = identifyhome.identify_home() self.torConfigLocation = self.dataDir + 'torrc' self.readyState = False - self.socksPort = getOpenPort() + self.socksPort = getopenport.get_open_port() self.hsPort = hsPort self._torInstnace = '' self.myID = '' @@ -66,17 +46,6 @@ class NetController: else: self.torBinary = 'tor' - config.reload() - ''' - if os.path.exists(self.torConfigLocation): - torrc = open(self.torConfigLocation, 'r') - if not str(self.hsPort) in torrc.read(): - os.remove(self.torConfigLocation) - torrc.close() - ''' - - return - def generateTorrc(self): ''' Generate a torrc file for our tor instance @@ -84,7 +53,6 @@ class NetController: hsVer = '# v2 onions' if config.get('tor.v3onions'): hsVer = 'HiddenServiceVersion 3' - logger.debug('Using v3 onions') if os.path.exists(self.torConfigLocation): os.remove(self.torConfigLocation) @@ -94,7 +62,7 @@ class NetController: config.set('tor.controlpassword', plaintext, savefile=True) config.set('tor.socksport', self.socksPort, savefile=True) - controlPort = getOpenPort() + controlPort = getopenport.get_open_port() config.set('tor.controlPort', controlPort, savefile=True) @@ -104,13 +72,13 @@ class NetController: if 'warn' not in password: break - torrcData = '''SocksPort ''' + str(self.socksPort) + ''' + torrcData = '''SocksPort ''' + str(self.socksPort) + ''' OnionTrafficOnly DataDirectory ''' + self.dataDir + '''tordata/ CookieAuthentication 1 ControlPort ''' + str(controlPort) + ''' HashedControlPassword ''' + str(password) + ''' ''' - if config.get('general.security_level') == 0: + if config.get('general.security_level', 1) == 0: torrcData += '''\nHiddenServiceDir ''' + self.dataDir + '''hs/ \n''' + hsVer + '''\n HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) @@ -118,7 +86,6 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) torrc = open(self.torConfigLocation, 'w') torrc.write(torrcData) torrc.close() - return def startTor(self): @@ -138,14 +105,14 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) try: tor = subprocess.Popen([self.torBinary, '-f', self.torConfigLocation], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError: - logger.fatal("Tor was not found in your path or the Onionr directory. Please install Tor and try again.") + logger.fatal("Tor was not found in your path or the Onionr directory. Please install Tor and try again.", terminal=True) sys.exit(1) else: # Test Tor Version torVersion = subprocess.Popen([self.torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in iter(torVersion.stdout.readline, b''): if 'Tor 0.2.' in line.decode(): - logger.error('Tor 0.3+ required') + logger.fatal('Tor 0.3+ required', terminal=True) sys.exit(1) break torVersion.kill() @@ -153,18 +120,19 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) # wait for tor to get to 100% bootstrap try: for line in iter(tor.stdout.readline, b''): - if 'Bootstrapped 100%: Done' in line.decode(): + if 'bootstrapped 100' in line.decode().lower(): + logger.info(line.decode()) break - elif 'Opening Socks listener' in line.decode(): + elif 'opening socks listener' in line.decode().lower(): logger.debug(line.decode().replace('\n', '')) else: - logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') + logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open', terminal=True) return False except KeyboardInterrupt: - logger.fatal('Got keyboard interrupt.', timestamp = False, level = logger.LEVEL_IMPORTANT) + logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT, terminal=True) return False - logger.debug('Finished starting Tor.', timestamp=True) + logger.info('Finished starting Tor.', terminal=True) self.readyState = True try: @@ -198,7 +166,11 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort) return try: - os.kill(int(pidN), signal.SIGTERM) + try: + os.kill(int(pidN), signal.SIGTERM) + except PermissionError: + # seems to happen on win 10 + pass os.remove(self.dataDir + 'torPid.txt') except ProcessLookupError: pass diff --git a/onionr/proofofmemory.py b/onionr/netcontroller/torbinary.py similarity index 69% rename from onionr/proofofmemory.py rename to onionr/netcontroller/torbinary.py index 4b0b0fa7..fb61e2d6 100644 --- a/onionr/proofofmemory.py +++ b/onionr/netcontroller/torbinary.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This file handles proof of memory functionality + get the tor binary path ''' ''' This program is free software: you can redistribute it and/or modify @@ -17,13 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +import os +from shutil import which -class ProofOfMemory: - def __init__(self, commInst): - self.communicator = commInst - return - - def checkRandomPeer(self): - return - def checkPeer(self, peer): - return \ No newline at end of file +def tor_binary(): + '''Return tor binary path or none if not exists''' + tor_path = './tor' + if not os.path.exists(tor_path): + tor_path = which('tor') + return tor_path \ No newline at end of file diff --git a/onionr/onionr.py b/onionr/onionr.py index 532c87d5..f9b361ab 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - Onionr is the name for both the protocol and the original/reference software. + This file initializes Onionr when ran to be a daemon or with commands Run with 'help' for usage. ''' @@ -21,31 +21,34 @@ along with this program. If not, see . ''' import sys +ONIONR_TAGLINE = 'Private P2P Communication - GPLv3 - https://Onionr.net' +ONIONR_VERSION = '0.0.0' # for debugging and stuff +ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) +API_VERSION = '0' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. MIN_PY_VERSION = 6 if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: - print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) + sys.stderr.write('Error, Onionr requires Python 3.%s+\n' % (MIN_PY_VERSION,)) sys.exit(1) -import os, base64, random, getpass, shutil, time, platform, datetime, re, json, getpass, sqlite3 -import webbrowser, uuid, signal + +from utils import detectoptimization +if detectoptimization.detect_optimization(): + sys.stderr.write('Error, Onionr cannot be run in optimized mode\n') + sys.exit(1) + +import os, base64, random, shutil, time, platform, signal from threading import Thread -import api, core, config, logger, onionrplugins as plugins, onionrevents as events -import onionrutils -import netcontroller, onionrstorage +import core, config, logger, onionrplugins as plugins, onionrevents as events +import netcontroller from netcontroller import NetController from onionrblockapi import Block import onionrproofs, onionrexceptions, communicator, setupconfig -from onionrusers import onionrusers import onionrcommands as commands # Many command definitions are here +from utils import identifyhome try: from urllib3.contrib.socks import SOCKSProxyManager except ImportError: - raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") - -ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net' -ONIONR_VERSION = '0.0.0' # for debugging and stuff -ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) -API_VERSION = '0' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. + raise ImportError("You need the PySocks module (for use with socks5 proxy to use Tor)") class Onionr: def __init__(self): @@ -53,8 +56,10 @@ class Onionr: Main Onionr class. This is for the CLI program, and does not handle much of the logic. In general, external programs and plugins should not use this class. ''' + self.API_VERSION = API_VERSION self.userRunDir = os.getcwd() # Directory user runs the program from self.killed = False + self.config = config if sys.argv[0] == os.path.basename(__file__): try: @@ -63,24 +68,20 @@ class Onionr: pass # set data dir - self.dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) + self.dataDir = identifyhome.identify_home() if not self.dataDir.endswith('/'): self.dataDir += '/' - # set log file - logger.set_file(os.environ.get('LOG_DIR', 'data') + '/onionr.log') - # Load global configuration data - data_exists = Onionr.setupConfig(self.dataDir, self = self) + data_exists = Onionr.setupConfig(self.dataDir, self) - if netcontroller.torBinary() is None: - logger.error('Tor is not installed') + if netcontroller.tor_binary() is None: + logger.error('Tor is not installed', terminal=True) sys.exit(1) - # If data folder does not exist - if not data_exists: - if not os.path.exists(self.dataDir + 'blocks/'): - os.mkdir(self.dataDir + 'blocks/') + # If block data folder does not exist + if not os.path.exists(self.dataDir + 'blocks/'): + os.mkdir(self.dataDir + 'blocks/') # Copy default plugins into plugins folder if not os.path.exists(plugins.get_plugins_folder()): @@ -97,14 +98,14 @@ class Onionr: if not os.path.exists(plugins.get_plugin_data_folder(name)): try: os.mkdir(plugins.get_plugin_data_folder(name)) - except: + except Exception as e: + logger.warn('Error enabling plugin: ' + str(e)) plugins.disable(name, onionr = self, stop_event = False) self.communicatorInst = None self.onionrCore = core.Core() self.onionrCore.onionrInst = self #self.deleteRunFiles() - self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) self.clientAPIInst = '' # Client http api instance self.publicAPIInst = '' # Public http api instance @@ -119,14 +120,11 @@ class Onionr: if type(config.get('client.webpassword')) is type(None): config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) if type(config.get('client.client.port')) is type(None): - randomPort = netcontroller.getOpenPort() + randomPort = netcontroller.get_open_port() config.set('client.client.port', randomPort, savefile=True) if type(config.get('client.public.port')) is type(None): - randomPort = netcontroller.getOpenPort() - print(randomPort) + randomPort = netcontroller.get_open_port() config.set('client.public.port', randomPort, savefile=True) - if type(config.get('client.participate')) is type(None): - config.set('client.participate', True, savefile=True) if type(config.get('client.api_version')) is type(None): config.set('client.api_version', API_VERSION, savefile=True) @@ -143,14 +141,15 @@ class Onionr: command = '' finally: self.execute(command) - + + os.chdir(self.userRunDir) return def exitSigterm(self, signum, frame): self.killed = True def setupConfig(dataDir, self = None): - setupconfig.setup_config(dataDir, self) + return setupconfig.setup_config(dataDir, self) def cmdHeader(self): if len(sys.argv) >= 3: @@ -165,18 +164,7 @@ class Onionr: sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('A', '%s' % API_VERSION).replace('V', ONIONR_VERSION)) if not message is None: - logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n', sensitive=True) - - def doExport(self, bHash): - exportDir = self.dataDir + 'block-export/' - if not os.path.exists(exportDir): - if os.path.exists(self.dataDir): - os.mkdir(exportDir) - else: - logger.error('Onionr Not initialized') - data = onionrstorage.getData(self.onionrCore, bHash) - with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: - exportFile.write(data) + logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n', terminal=True) def deleteRunFiles(self): try: @@ -190,7 +178,7 @@ class Onionr: def get_hostname(self): try: - with open('./' + self.dataDir + 'hs/hostname', 'r') as hostname: + with open(self.dataDir + 'hs/hostname', 'r') as hostname: return hostname.read().strip() except FileNotFoundError: return "Not Generated" @@ -213,25 +201,17 @@ class Onionr: return columns ''' - THIS SECTION HANDLES THE COMMANDS + Handle command line commands ''' def exportBlock(self): - exportDir = self.dataDir + 'block-export/' - try: - assert self.onionrUtils.validateHash(sys.argv[2]) - except (IndexError, AssertionError): - logger.error('No valid block hash specified.') - sys.exit(1) - else: - bHash = sys.argv[2] - self.doExport(bHash) + commands.exportblocks.export_block(self) def showDetails(self): commands.onionrstatistics.show_details(self) def openHome(self): - commands.open_home(self) + commands.openwebinterface.open_home(self) def addID(self): commands.pubkeymanager.add_ID(self) @@ -249,23 +229,7 @@ class Onionr: commands.pubkeymanager.friend_command(self) def banBlock(self): - try: - ban = sys.argv[2] - except IndexError: - ban = logger.readline('Enter a block hash:') - if self.onionrUtils.validateHash(ban): - if not self.onionrCore._blacklist.inBlacklist(ban): - try: - self.onionrCore._blacklist.addToDB(ban) - self.onionrCore.removeBlock(ban) - except Exception as error: - logger.error('Could not blacklist block', error=error) - else: - logger.info('Block blacklisted') - else: - logger.warn('That block is already blacklisted') - else: - logger.error('Invalid block hash') + commands.banblocks.ban_block(self) def listConn(self): commands.onionrstatistics.show_peers(self) @@ -273,13 +237,13 @@ class Onionr: def listPeers(self): logger.info('Peer transport address list:') for i in self.onionrCore.listAdders(): - logger.info(i) + logger.info(i, terminal=True) def getWebPassword(self): return config.get('client.webpassword') def printWebPassword(self): - logger.info(self.getWebPassword(), sensitive = True) + logger.info(self.getWebPassword(), terminal=True) def getHelp(self): return self.cmdhelp @@ -304,13 +268,13 @@ class Onionr: if len(sys.argv) >= 4: config.reload() config.set(sys.argv[2], sys.argv[3], True) - logger.debug('Configuration file updated.') + logger.info('Configuration file updated.', terminal=True) elif len(sys.argv) >= 3: config.reload() - logger.info(logger.colors.bold + sys.argv[2] + ': ' + logger.colors.reset + str(config.get(sys.argv[2], logger.colors.fg.red + 'Not set.'))) + logger.info(logger.colors.bold + sys.argv[2] + ': ' + logger.colors.reset + str(config.get(sys.argv[2], logger.colors.fg.red + 'Not set.')), terminal=True) else: - logger.info(logger.colors.bold + 'Get a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') - logger.info(logger.colors.bold + 'Set a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') + logger.info(logger.colors.bold + 'Get a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) + logger.info(logger.colors.bold + 'Set a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) def execute(self, argument): ''' @@ -330,11 +294,11 @@ class Onionr: Displays the Onionr version ''' - function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION)) + function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION), terminal=True) if verbosity >= 1: - function(ONIONR_TAGLINE) + function(ONIONR_TAGLINE, terminal=True) if verbosity >= 2: - function('Running on %s %s' % (platform.platform(), platform.release())) + function('Running on %s %s' % (platform.platform(), platform.release()), terminal=True) def doPEX(self): '''make communicator do pex''' @@ -345,7 +309,7 @@ class Onionr: ''' Displays a list of keys (used to be called peers) (?) ''' - logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers()))) + logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers())), terminal=True) def addPeer(self): ''' @@ -388,19 +352,21 @@ class Onionr: Displays a "command not found" message ''' - logger.error('Command not found.', timestamp = False) + logger.error('Command not found.', timestamp = False, terminal=True) def showHelpSuggestion(self): ''' Displays a message suggesting help ''' if __name__ == '__main__': - logger.info('Do ' + logger.colors.bold + sys.argv[0] + ' --help' + logger.colors.reset + logger.colors.fg.green + ' for Onionr help.') + logger.info('Do ' + logger.colors.bold + sys.argv[0] + ' --help' + logger.colors.reset + logger.colors.fg.green + ' for Onionr help.', terminal=True) def start(self, input = False, override = False): ''' Starts the Onionr daemon ''' + if config.get('general.dev_mode', False): + override = True commands.daemonlaunch.start(self, input, override) def setClientAPIInst(self, inst): diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 63ecbb6b..03da61b4 100755 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles maintenence of a blacklist database, for blocks and peers ''' @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import sqlite3, os, logger +from onionrutils import epoch, bytesconverter class OnionrBlackList: def __init__(self, coreInst): self.blacklistDB = coreInst.dataDir + 'blacklist.db' @@ -28,7 +29,7 @@ class OnionrBlackList: return def inBlacklist(self, data): - hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) + hashed = bytesconverter.bytes_to_str(self._core._crypto.sha3Hash(data)) retData = False if not hashed.isalnum(): @@ -56,7 +57,7 @@ class OnionrBlackList: def deleteExpired(self, dataType=0): '''Delete expired entries''' deleteList = [] - curTime = self._core._utils.getEpoch() + curTime = epoch.get_epoch() try: int(dataType) @@ -98,7 +99,7 @@ class OnionrBlackList: 2=pubkey ''' # we hash the data so we can remove data entirely from our node's disk - hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) + hashed = bytesconverter.bytes_to_str(self._core._crypto.sha3Hash(data)) if len(hashed) > 64: raise Exception("Hashed data is too large") @@ -115,7 +116,7 @@ class OnionrBlackList: if self.inBlacklist(hashed): return insert = (hashed,) - blacklistDate = self._core._utils.getEpoch() + blacklistDate = epoch.get_epoch() try: self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) except sqlite3.IntegrityError: diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index e7a4395b..00d377b0 100755 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -21,6 +21,7 @@ import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions import json, os, sys, datetime, base64, onionrstorage from onionrusers import onionrusers +from onionrutils import stringvalidators, epoch class Block: blockCacheOrder = list() # NEVER write your own code that writes to this! @@ -88,9 +89,9 @@ class Block: # Check for replay attacks try: - if self.core._utils.getEpoch() - self.core.getBlockDate(self.hash) < 60: + if epoch.get_epoch() - self.core.getBlockDate(self.hash) > 60: assert self.core._crypto.replayTimestampValidation(self.bmetadata['rply']) - except (AssertionError, KeyError) as e: + except (AssertionError, KeyError, TypeError) as e: if not self.bypassReplayCheck: # Zero out variables to prevent reading of replays self.bmetadata = {} @@ -215,7 +216,10 @@ class Block: ''' if self.exists(): - os.remove(self.getBlockFile()) + try: + os.remove(self.getBlockFile()) + except TypeError: + pass self.getCore().removeBlock(self.getHash()) return True return False @@ -438,7 +442,7 @@ class Block: ''' try: - if (not self.isSigned()) or (not self.getCore()._utils.validatePubKey(signer)): + if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)): return False return bool(self.getCore()._crypto.edVerify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData)) diff --git a/onionr/onionrcommands/README.md b/onionr/onionrcommands/README.md new file mode 100755 index 00000000..fed35bb5 --- /dev/null +++ b/onionr/onionrcommands/README.md @@ -0,0 +1,27 @@ +# onionrcommands + +This module contains handlers/functions for Onionr cli interface commands. + +## Files + +__init__.py: stores the command references (aside from plugins) and help info. + +banblocks.py: command handler for manually removing blocks from one's node + +daemonlaunch.py: command to run Onionr (start the api servers, tor and communicator) + +exportblocks.py: command to export an onionr block to the export folder. Exported blocks can be manually shared outside of the Onionr network + +filecommands.py commands to insert and fetch files from the Onionr network + +keyadders.py: commands to add an onionr user key or transport address + +onionrstatistics.py: commands to print out various information about one's Onionr node + +openwebinterface.py: command to open the web interface (useful because it requires a randomly generated token) + +plugincommands.py: commands to enable/disable/reload plugins + +pubkeymanager.py: commands to generate a new onionr user id, change the active id, or add/remove/list friends + +resettor.py: command to delete the Tor data directory \ No newline at end of file diff --git a/onionr/onionrcommands/__init__.py b/onionr/onionrcommands/__init__.py old mode 100644 new mode 100755 index bf10f4ed..59c9d6bc --- a/onionr/onionrcommands/__init__.py +++ b/onionr/onionrcommands/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module defines commands for CLI usage ''' @@ -21,6 +21,8 @@ import webbrowser, sys import logger from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders +from . import banblocks, exportblocks, openwebinterface, resettor +from onionrutils import importnewblocks def show_help(o_inst, command): @@ -39,16 +41,6 @@ def show_help(o_inst, command): for command, helpmessage in helpmenu.items(): o_inst.showHelp(command) -def open_home(o_inst): - try: - url = o_inst.onionrUtils.getClientAPIServer() - except FileNotFoundError: - logger.error('Onionr seems to not be running (could not get api host)') - else: - url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) - print('If Onionr does not open automatically, use this URL:', url) - webbrowser.open_new_tab(url) - def get_commands(onionr_inst): return {'': onionr_inst.showHelpSuggestion, 'help': onionr_inst.showHelp, @@ -119,8 +111,8 @@ def get_commands(onionr_inst): 'listconn': onionr_inst.listConn, 'list-conn': onionr_inst.listConn, - 'import-blocks': onionr_inst.onionrUtils.importNewBlocks, - 'importblocks': onionr_inst.onionrUtils.importNewBlocks, + 'import-blocks': importnewblocks.import_new_blocks, + 'importblocks': importnewblocks.import_new_blocks, 'introduce': onionr_inst.onionrCore.introduceNode, 'pex': onionr_inst.doPEX, @@ -137,7 +129,9 @@ def get_commands(onionr_inst): 'friend': onionr_inst.friendCmd, 'addid': onionr_inst.addID, 'add-id': onionr_inst.addID, - 'change-id': onionr_inst.changeID + 'change-id': onionr_inst.changeID, + + 'reset-tor': resettor.reset_tor } cmd_help = { @@ -168,5 +162,6 @@ cmd_help = { 'friend': '[add|remove] [public key/id]', 'add-id': 'Generate a new ID (key pair)', 'change-id': 'Change active ID', - 'open-home': 'Open your node\'s home/info screen' + 'open-home': 'Open your node\'s home/info screen', + 'reset-tor': 'Delete the Tor data directory. Only do this if Tor never starts.' } diff --git a/onionr/onionrcommands/banblocks.py b/onionr/onionrcommands/banblocks.py new file mode 100755 index 00000000..426e852e --- /dev/null +++ b/onionr/onionrcommands/banblocks.py @@ -0,0 +1,40 @@ +''' + Onionr - Private P2P Communication + + This file contains the command for banning blocks from the node +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys +import logger +from onionrutils import stringvalidators +def ban_block(o_inst): + try: + ban = sys.argv[2] + except IndexError: + ban = logger.readline('Enter a block hash:') + if stringvalidators.validate_hash(ban): + if not o_inst.onionrCore._blacklist.inBlacklist(ban): + try: + o_inst.onionrCore._blacklist.addToDB(ban) + o_inst.onionrCore.removeBlock(ban) + except Exception as error: + logger.error('Could not blacklist block', error=error, terminal=True) + else: + logger.info('Block blacklisted', terminal=True) + else: + logger.warn('That block is already blacklisted', terminal=True) + else: + logger.error('Invalid block hash', terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/daemonlaunch.py b/onionr/onionrcommands/daemonlaunch.py old mode 100644 new mode 100755 index b5334068..d24b2dff --- a/onionr/onionrcommands/daemonlaunch.py +++ b/onionr/onionrcommands/daemonlaunch.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication launch the api server and communicator ''' @@ -18,29 +18,29 @@ along with this program. If not, see . ''' -import os, time, sys, platform, sqlite3 +import os, time, sys, platform, sqlite3, signal from threading import Thread -import onionr, api, logger, communicator +import onionr, apiservers, logger, communicator import onionrevents as events from netcontroller import NetController +from onionrutils import localcommand + +def _proper_shutdown(o_inst): + localcommand.local_command(o_inst.onionrCore, 'shutdown') + sys.exit(1) + def daemon(o_inst): ''' Starts the Onionr communication daemon ''' # remove runcheck if it exists - if os.path.isfile('data/.runcheck'): + if os.path.isfile('%s/.runcheck' % (o_inst.onionrCore.dataDir,)): logger.debug('Runcheck file found on daemon start, deleting in advance.') - os.remove('data/.runcheck') + os.remove('%s/.runcheck' % (o_inst.onionrCore.dataDir,)) - Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION)).start() - Thread(target=api.PublicAPI, args=[o_inst.getClientApi()]).start() - try: - time.sleep(0) - except KeyboardInterrupt: - logger.debug('Got keyboard interrupt, shutting down...') - time.sleep(1) - o_inst.onionrUtils.localCommand('shutdown') + Thread(target=apiservers.ClientAPI, args=(o_inst, o_inst.debug, onionr.API_VERSION), daemon=True).start() + Thread(target=apiservers.PublicAPI, args=[o_inst.getClientApi()], daemon=True).start() apiHost = '' while apiHost == '': @@ -50,43 +50,49 @@ def daemon(o_inst): except FileNotFoundError: pass time.sleep(0.5) - onionr.Onionr.setupConfig('data/', self = o_inst) + #onionr.Onionr.setupConfig('data/', self = o_inst) + + logger.raw('', terminal=True) + # print nice header thing :) + if o_inst.onionrCore.config.get('general.display_header', True): + o_inst.header() + o_inst.version(verbosity = 5, function = logger.info) + logger.debug('Python version %s' % platform.python_version()) if o_inst._developmentMode: - logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False) + logger.warn('Development mode enabled', timestamp = False, terminal=True) net = NetController(o_inst.onionrCore.config.get('client.public.port', 59497), apiServerIP=apiHost) - logger.debug('Tor is starting...') + logger.info('Tor is starting...', terminal=True) if not net.startTor(): - o_inst.onionrUtils.localCommand('shutdown') + localcommand.local_command(o_inst.onionrCore, 'shutdown') sys.exit(1) - if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level') == 0: + if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level', 1) == 0: logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) else: logger.debug('.onion service disabled') - logger.debug('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey)) - time.sleep(1) + logger.info('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey[:52]), terminal=True) + + try: + time.sleep(1) + except KeyboardInterrupt: + _proper_shutdown(o_inst) o_inst.onionrCore.torPort = net.socksPort - communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort))) + communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort)), daemon=True) communicatorThread.start() while o_inst.communicatorInst is None: time.sleep(0.1) - # print nice header thing :) - if o_inst.onionrCore.config.get('general.display_header', True): - o_inst.header() - - # print out debug info - o_inst.version(verbosity = 5, function = logger.debug) - logger.debug('Python version %s' % platform.python_version()) - logger.debug('Started communicator.') events.event('daemon_start', onionr = o_inst) - try: - while True: + while True: + try: time.sleep(3) + except KeyboardInterrupt: + o_inst.communicatorInst.shutdown = True + finally: # Debug to print out used FDs (regular and net) #proc = psutil.Process() #print('api-files:',proc.open_files(), len(psutil.net_connections())) @@ -95,22 +101,25 @@ def daemon(o_inst): break if o_inst.killed: break # Break out if sigterm for clean exit - except KeyboardInterrupt: - pass - finally: - o_inst.onionrCore.daemonQueueAdd('shutdown') - o_inst.onionrUtils.localCommand('shutdown') + + signal.signal(signal.SIGINT, _ignore_sigint) + o_inst.onionrCore.daemonQueueAdd('shutdown') + localcommand.local_command(o_inst.onionrCore, 'shutdown') + net.killTor() - time.sleep(3) + time.sleep(5) # Time to allow threads to finish, if not any "daemon" threads will be slaughtered http://docs.python.org/library/threading.html#threading.Thread.daemon o_inst.deleteRunFiles() return +def _ignore_sigint(sig, frame): + return + def kill_daemon(o_inst): ''' Shutdown the Onionr daemon ''' - logger.warn('Stopping the running daemon...', timestamp = False) + logger.warn('Stopping the running daemon...', timestamp = False, terminal=True) try: events.event('daemon_stop', onionr = o_inst) net = NetController(o_inst.onionrCore.config.get('client.port', 59496)) @@ -121,12 +130,12 @@ def kill_daemon(o_inst): net.killTor() except Exception as e: - logger.error('Failed to shutdown daemon.', error = e, timestamp = False) + logger.error('Failed to shutdown daemon.', error = e, timestamp = False, terminal=True) return def start(o_inst, input = False, override = False): if os.path.exists('.onionr-lock') and not override: - logger.fatal('Cannot start. Daemon is already running, or it did not exit cleanly.\n(if you are sure that there is not a daemon running, delete .onionr-lock & try again).') + logger.fatal('Cannot start. Daemon is already running, or it did not exit cleanly.\n(if you are sure that there is not a daemon running, delete .onionr-lock & try again).', terminal=True) else: if not o_inst.debug and not o_inst._developmentMode: lockFile = open('.onionr-lock', 'w') diff --git a/onionr/onionrcommands/exportblocks.py b/onionr/onionrcommands/exportblocks.py new file mode 100755 index 00000000..a941e025 --- /dev/null +++ b/onionr/onionrcommands/exportblocks.py @@ -0,0 +1,44 @@ +''' + Onionr - Private P2P Communication + + This file handles the command for exporting blocks to disk +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys, os +import logger, onionrstorage +from onionrutils import stringvalidators +def doExport(o_inst, bHash): + exportDir = o_inst.dataDir + 'block-export/' + if not os.path.exists(exportDir): + if os.path.exists(o_inst.dataDir): + os.mkdir(exportDir) + else: + logger.error('Onionr Not initialized', terminal=True) + data = onionrstorage.getData(o_inst.onionrCore, bHash) + with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: + exportFile.write(data) + logger.info('Block exported as file', terminal=True) + +def export_block(o_inst): + exportDir = o_inst.dataDir + 'block-export/' + try: + assert stringvalidators.validate_hash(sys.argv[2]) + except (IndexError, AssertionError): + logger.error('No valid block hash specified.', terminal=True) + sys.exit(1) + else: + bHash = sys.argv[2] + doExport(o_inst, bHash) \ No newline at end of file diff --git a/onionr/onionrcommands/filecommands.py b/onionr/onionrcommands/filecommands.py old mode 100644 new mode 100755 index f9d05f01..7e2cd086 --- a/onionr/onionrcommands/filecommands.py +++ b/onionr/onionrcommands/filecommands.py @@ -1,6 +1,27 @@ +''' + Onionr - Private P2P Communication + + This file handles the commands for adding and getting files from the Onionr network +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + import base64, sys, os import logger from onionrblockapi import Block +from onionrutils import stringvalidators def add_file(o_inst, singleBlock=False, blockType='bin'): ''' Adds a file to the onionr network @@ -11,18 +32,18 @@ def add_file(o_inst, singleBlock=False, blockType='bin'): contents = None if not os.path.exists(filename): - logger.error('That file does not exist. Improper path (specify full path)?') + logger.error('That file does not exist. Improper path (specify full path)?', terminal=True) return - logger.info('Adding file... this might take a long time.') + logger.info('Adding file... this might take a long time.', terminal=True) try: with open(filename, 'rb') as singleFile: blockhash = o_inst.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) if len(blockhash) > 0: - logger.info('File %s saved in block %s' % (filename, blockhash)) + logger.info('File %s saved in block %s' % (filename, blockhash), terminal=True) except: - logger.error('Failed to save file in block.', timestamp = False) + logger.error('Failed to save file in block.', timestamp = False, terminal=True) else: - logger.error('%s add-file ' % sys.argv[0], timestamp = False) + logger.error('%s add-file ' % sys.argv[0], timestamp = False, terminal=True) def getFile(o_inst): ''' @@ -32,16 +53,16 @@ def getFile(o_inst): fileName = sys.argv[2] bHash = sys.argv[3] except IndexError: - logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename ')) + logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename '), terminal=True) else: - logger.info(fileName) + logger.info(fileName, terminal=True) contents = None if os.path.exists(fileName): - logger.error("File already exists") + logger.error("File already exists", terminal=True) return - if not o_inst.onionrUtils.validateHash(bHash): - logger.error('Block hash is invalid') + if not stringvalidators.validate_hash(bHash): + logger.error('Block hash is invalid', terminal=True) return with open(fileName, 'wb') as myFile: diff --git a/onionr/onionrcommands/keyadders.py b/onionr/onionrcommands/keyadders.py old mode 100644 new mode 100755 index d52b81f9..b9401d60 --- a/onionr/onionrcommands/keyadders.py +++ b/onionr/onionrcommands/keyadders.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication add keys (transport and pubkey) ''' @@ -25,15 +25,15 @@ def add_peer(o_inst): except IndexError: pass else: - if o_inst.onionrUtils.hasKey(newPeer): - logger.info('We already have that key') + if newPeer in o_inst.onionrCore.listPeers(): + logger.info('We already have that key', terminal=True) return - logger.info("Adding peer: " + logger.colors.underline + newPeer) + logger.info("Adding peer: " + logger.colors.underline + newPeer, terminal=True) try: if o_inst.onionrCore.addPeer(newPeer): - logger.info('Successfully added key') + logger.info('Successfully added key', terminal=True) except AssertionError: - logger.error('Failed to add key') + logger.error('Failed to add key', terminal=True) def add_address(o_inst): try: @@ -42,8 +42,8 @@ def add_address(o_inst): except IndexError: pass else: - logger.info("Adding address: " + logger.colors.underline + newAddress) + logger.info("Adding address: " + logger.colors.underline + newAddress, terminal=True) if o_inst.onionrCore.addAddress(newAddress): - logger.info("Successfully added address.") + logger.info("Successfully added address.", terminal=True) else: - logger.warn("Unable to add address.") \ No newline at end of file + logger.warn("Unable to add address.", terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/onionrstatistics.py b/onionr/onionrcommands/onionrstatistics.py old mode 100644 new mode 100755 index 04264655..c01c5e1b --- a/onionr/onionrcommands/onionrstatistics.py +++ b/onionr/onionrcommands/onionrstatistics.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module defines commands to show stats/details about the local node ''' @@ -18,9 +18,11 @@ along with this program. If not, see . ''' import os, uuid, time -import logger, onionrutils +import logger from onionrblockapi import Block import onionr +from onionrutils import checkcommunicator, mnemonickeys +from utils import sizeutils def show_stats(o_inst): try: @@ -29,17 +31,17 @@ def show_stats(o_inst): signedBlocks = len(Block.getBlocks(signed = True)) messages = { # info about local client - 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if o_inst.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'), + 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if checkcommunicator.is_communicator_running(o_inst.onionrCore, timeout = 9) else logger.colors.fg.red + 'Offline'), # file and folder size stats 'div1' : True, # this creates a solid line across the screen, a div - 'Total Block Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'blocks/')), - 'Total Plugin Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'plugins/')), - 'Log File Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'output.log')), + 'Total Block Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'blocks/')), + 'Total Plugin Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'plugins/')), + 'Log File Size' : sizeutils.human_size(sizeutils.size(o_inst.dataDir + 'output.log')), # count stats 'div2' : True, - 'Known Peers' : str(len(o_inst.onionrCore.listPeers()) - 1), + 'Known Peers' : str(max(len(o_inst.onionrCore.listPeers()) - 1, 0)), 'Enabled Plugins' : str(len(o_inst.onionrCore.config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir(o_inst.dataDir + 'plugins/'))), 'Stored Blocks' : str(totalBlocks), 'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%' @@ -65,32 +67,32 @@ def show_stats(o_inst): groupsize = width - prewidth - len('[+] ') # generate stats table - logger.info(colors['title'] + 'Onionr v%s Statistics' % onionr.ONIONR_VERSION + colors['reset']) - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) + logger.info(colors['title'] + 'Onionr v%s Statistics' % onionr.ONIONR_VERSION + colors['reset'], terminal=True) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) for key, val in messages.items(): if not (type(val) is bool and val is True): val = [str(val)[i:i + groupsize] for i in range(0, len(str(val)), groupsize)] - logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset']) + logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset'], terminal=True) for value in val: - logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset']) + logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset'], terminal=True) else: - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) - logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset']) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) + logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'], terminal=True) except Exception as e: - logger.error('Failed to generate statistics table.', error = e, timestamp = False) + logger.error('Failed to generate statistics table. ' + str(e), error = e, timestamp = False, terminal=True) def show_details(o_inst): details = { 'Node Address' : o_inst.get_hostname(), 'Web Password' : o_inst.getWebPassword(), 'Public Key' : o_inst.onionrCore._crypto.pubKey, - 'Human-readable Public Key' : o_inst.onionrCore._utils.getHumanReadableID() + 'Human-readable Public Key' : mnemonickeys.get_human_readable_ID(o_inst.onionrCore) } for detail in details: - logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True) + logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), terminal = True) def show_peers(o_inst): randID = str(uuid.uuid4()) @@ -104,7 +106,10 @@ def show_peers(o_inst): if not type(peers) is None: if peers not in ('', 'failure', None): if peers != False: - print(peers) + if peers == 'none': + print('No current outgoing connections.') + else: + print(peers) else: print('Daemon probably not running. Unable to list connected peers.') break \ No newline at end of file diff --git a/onionr/onionrcommands/openwebinterface.py b/onionr/onionrcommands/openwebinterface.py new file mode 100755 index 00000000..42c80b57 --- /dev/null +++ b/onionr/onionrcommands/openwebinterface.py @@ -0,0 +1,31 @@ +''' + Onionr - Private P2P Communication + + Open the web interface properly into a web browser +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import webbrowser +import logger +from onionrutils import getclientapiserver +def open_home(o_inst): + try: + url = getclientapiserver.get_client_API_server(o_inst.onionrCore) + except FileNotFoundError: + logger.error('Onionr seems to not be running (could not get api host)', terminal=True) + else: + url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword')) + logger.info('If Onionr does not open automatically, use this URL: ' + url, terminal=True) + webbrowser.open_new_tab(url) \ No newline at end of file diff --git a/onionr/onionrcommands/plugincommands.py b/onionr/onionrcommands/plugincommands.py old mode 100644 new mode 100755 index c357956f..154bb6cf --- a/onionr/onionrcommands/plugincommands.py +++ b/onionr/onionrcommands/plugincommands.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication plugin CLI commands ''' @@ -24,19 +24,18 @@ import logger, onionrplugins as plugins def enable_plugin(o_inst): if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Enabling plugin "%s"...' % plugin_name) + logger.info('Enabling plugin "%s"...' % plugin_name, terminal=True) plugins.enable(plugin_name, o_inst) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) def disable_plugin(o_inst): - if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Disabling plugin "%s"...' % plugin_name) + logger.info('Disabling plugin "%s"...' % plugin_name, terminal=True) plugins.disable(plugin_name, o_inst) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) def reload_plugin(o_inst): ''' @@ -45,11 +44,11 @@ def reload_plugin(o_inst): if len(sys.argv) >= 3: plugin_name = sys.argv[2] - logger.info('Reloading plugin "%s"...' % plugin_name) + logger.info('Reloading plugin "%s"...' % plugin_name, terminal=True) plugins.stop(plugin_name, o_inst) plugins.start(plugin_name, o_inst) else: - logger.info('Reloading all plugins...') + logger.info('Reloading all plugins...', terminal=True) plugins.reload(o_inst) @@ -63,7 +62,7 @@ def create_plugin(o_inst): plugin_name = re.sub('[^0-9a-zA-Z_]+', '', str(sys.argv[2]).lower()) if not plugins.exists(plugin_name): - logger.info('Creating plugin "%s"...' % plugin_name) + logger.info('Creating plugin "%s"...' % plugin_name, terminal=True) os.makedirs(plugins.get_plugins_folder(plugin_name)) with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main: @@ -77,12 +76,12 @@ def create_plugin(o_inst): with open(plugins.get_plugins_folder(plugin_name) + '/info.json', 'a') as main: main.write(json.dumps({'author' : 'anonymous', 'description' : 'the default description of the plugin', 'version' : '1.0'})) - logger.info('Enabling plugin "%s"...' % plugin_name) + logger.info('Enabling plugin "%s"...' % plugin_name, terminal=True) plugins.enable(plugin_name, o_inst) else: - logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name) + logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name, terminal=True) except Exception as e: - logger.error('Failed to create plugin directory structure.', e) + logger.error('Failed to create plugin directory structure.', e, terminal=True) else: - logger.info('%s %s ' % (sys.argv[0], sys.argv[1])) \ No newline at end of file + logger.info('%s %s ' % (sys.argv[0], sys.argv[1]), terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py old mode 100644 new mode 100755 index ea55a3fe..72db422a --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -1,7 +1,7 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication - This module defines ID-related CLI commands + This module defines user ID-related CLI commands ''' ''' This program is free software: you can redistribute it and/or modify @@ -19,8 +19,10 @@ ''' import sys, getpass -import logger -from onionrusers import onionrusers +import logger, onionrexceptions +from onionrutils import stringvalidators, bytesconverter +from onionrusers import onionrusers, contactmanager +import unpaddedbase32 def add_ID(o_inst): try: sys.argv[2] @@ -28,41 +30,46 @@ def add_ID(o_inst): except (IndexError, AssertionError) as e: newID = o_inst.onionrCore._crypto.keyManager.addKey()[0] else: - logger.warn('Deterministic keys require random and long passphrases.') - logger.warn('If a good passphrase is not used, your key can be easily stolen.') - logger.warn('You should use a series of hard to guess words, see this for reference: https://www.xkcd.com/936/') + logger.warn('Deterministic keys require random and long passphrases.', terminal=True) + logger.warn('If a good passphrase is not used, your key can be easily stolen.', terminal=True) + logger.warn('You should use a series of hard to guess words, see this for reference: https://www.xkcd.com/936/', terminal=True) pass1 = getpass.getpass(prompt='Enter at least %s characters: ' % (o_inst.onionrCore._crypto.deterministicRequirement,)) pass2 = getpass.getpass(prompt='Confirm entry: ') if o_inst.onionrCore._crypto.safeCompare(pass1, pass2): try: - logger.info('Generating deterministic key. This can take a while.') + logger.info('Generating deterministic key. This can take a while.', terminal=True) newID, privKey = o_inst.onionrCore._crypto.generateDeterministic(pass1) except onionrexceptions.PasswordStrengthError: - logger.error('Must use at least 25 characters.') + logger.error('Passphrase must use at least %s characters.' % (o_inst.onionrCore._crypto.deterministicRequirement,), terminal=True) sys.exit(1) else: - logger.error('Passwords do not match.') + logger.error('Passwords do not match.', terminal=True) sys.exit(1) - o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID, - privKey=privKey) - logger.info('Added ID: %s' % (o_inst.onionrUtils.bytesToStr(newID),)) + try: + o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID, + privKey=privKey) + except ValueError: + logger.error('That ID is already available, you can change to it with the change-id command.', terminal=True) + return + logger.info('Added ID: %s' % (bytesconverter.bytes_to_str(newID),), terminal=True) def change_ID(o_inst): try: key = sys.argv[2] + key = unpaddedbase32.repad(key.encode()).decode() except IndexError: - logger.error('Specify pubkey to use') + logger.warn('Specify pubkey to use', terminal=True) else: - if o_inst.onionrUtils.validatePubKey(key): + if stringvalidators.validate_pub_key(key): if key in o_inst.onionrCore._crypto.keyManager.getPubkeyList(): o_inst.onionrCore.config.set('general.public_key', key) o_inst.onionrCore.config.save() - logger.info('Set active key to: %s' % (key,)) - logger.info('Restart Onionr if it is running.') + logger.info('Set active key to: %s' % (key,), terminal=True) + logger.info('Restart Onionr if it is running.', terminal=True) else: - logger.error('That key does not exist') + logger.warn('That key does not exist', terminal=True) else: - logger.error('Invalid key %s' % (key,)) + logger.warn('Invalid key %s' % (key,), terminal=True) def friend_command(o_inst): friend = '' @@ -70,32 +77,33 @@ def friend_command(o_inst): # Get the friend command action = sys.argv[2] except IndexError: - logger.info('Syntax: friend add/remove/list [address]') + logger.info('Syntax: friend add/remove/list [address]', terminal=True) else: action = action.lower() if action == 'list': # List out peers marked as our friend - for friend in onionrusers.OnionrUser.list_friends(o_inst.onionrCore): - logger.info(friend.publicKey + ' - ' + friend.getName()) + for friend in contactmanager.ContactManager.list_friends(o_inst.onionrCore): + logger.info(friend.publicKey + ' - ' + friend.get_info('name'), terminal=True) elif action in ('add', 'remove'): try: friend = sys.argv[3] - if not o_inst.onionrUtils.validatePubKey(friend): + if not stringvalidators.validate_pub_key(friend): raise onionrexceptions.InvalidPubkey('Public key is invalid') if friend not in o_inst.onionrCore.listPeers(): raise onionrexceptions.KeyNotKnown friend = onionrusers.OnionrUser(o_inst.onionrCore, friend) except IndexError: - logger.error('Friend ID is required.') + logger.warn('Friend ID is required.', terminal=True) + action = 'error' # set to 'error' so that the finally block does not process anything except onionrexceptions.KeyNotKnown: o_inst.onionrCore.addPeer(friend) friend = onionrusers.OnionrUser(o_inst.onionrCore, friend) finally: if action == 'add': friend.setTrust(1) - logger.info('Added %s as friend.' % (friend.publicKey,)) - else: + logger.info('Added %s as friend.' % (friend.publicKey,), terminal=True) + elif action == 'remove': friend.setTrust(0) - logger.info('Removed %s as friend.' % (friend.publicKey,)) + logger.info('Removed %s as friend.' % (friend.publicKey,), terminal=True) else: - logger.info('Syntax: friend add/remove/list [address]') \ No newline at end of file + logger.info('Syntax: friend add/remove/list [address]', terminal=True) \ No newline at end of file diff --git a/onionr/onionrcommands/resettor.py b/onionr/onionrcommands/resettor.py new file mode 100755 index 00000000..e327bccc --- /dev/null +++ b/onionr/onionrcommands/resettor.py @@ -0,0 +1,31 @@ +''' + Onionr - Private P2P Communication + + Command to delete the Tor data directory if its safe to do so +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os, shutil +import logger, core +from onionrutils import localcommand + +def reset_tor(): + c = core.Core() + tor_dir = c.dataDir + 'tordata' + if os.path.exists(tor_dir): + if localcommand.local_command(c, '/ping') == 'pong!': + logger.warn('Cannot delete Tor data while Onionr is running', terminal=True) + else: + shutil.rmtree(tor_dir) \ No newline at end of file diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 4b5a72e1..957733eb 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles Onionr's cryptography. ''' @@ -17,18 +17,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys, hmac -import onionrexceptions, keymanager, core -# secrets module was added into standard lib in 3.6+ -if sys.version_info[0] == 3 and sys.version_info[1] < 6: - from dependencies import secrets -elif sys.version_info[0] == 3 and sys.version_info[1] >= 6: - import secrets +import os, binascii, base64, hashlib, time, sys, hmac, secrets +import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret +import unpaddedbase32 +import logger, onionrproofs +from onionrutils import stringvalidators, epoch, bytesconverter +import onionrexceptions, keymanager, core, onionrutils import config +config.reload() class OnionrCrypto: def __init__(self, coreInstance): - config.reload() self._core = coreInstance self._keyFile = self._core.dataDir + 'keys.txt' self.pubKey = None @@ -40,8 +39,8 @@ class OnionrCrypto: # Load our own pub/priv Ed25519 keys, gen & save them if they don't exist if os.path.exists(self._keyFile): - if len(config.get('general.public_key', '')) > 0: - self.pubKey = config.get('general.public_key') + if len(self._core.config.get('general.public_key', '')) > 0: + self.pubKey = self._core.config.get('general.public_key') else: self.pubKey = self.keyManager.getPubkeyList()[0] self.privKey = self.keyManager.getPrivkey(self.pubKey) @@ -96,9 +95,10 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' + pubkey = unpaddedbase32.repad(bytesconverter.str_to_bytes(pubkey)) retVal = '' box = None - data = self._core._utils.strToBytes(data) + data = bytesconverter.str_to_bytes(data) pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key() @@ -123,7 +123,7 @@ class OnionrCrypto: privkey = self.privKey ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() - if self._core._utils.validatePubKey(privkey): + if stringvalidators.validate_pub_key(privkey): privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() anonBox = nacl.public.SealedBox(privkey) else: @@ -132,7 +132,7 @@ class OnionrCrypto: return decrypted def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): - '''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)''' + '''Encrypt data with a 32-byte key (Salsa20-Poly1305 MAC)''' if encodedKey: encoding = nacl.encoding.Base64Encoder else: @@ -182,7 +182,7 @@ class OnionrCrypto: def generateDeterministic(self, passphrase, bypassCheck=False): '''Generate a Ed25519 public key pair from a password''' passStrength = self.deterministicRequirement - passphrase = self._core._utils.strToBytes(passphrase) # Convert to bytes if not already + passphrase = bytesconverter.str_to_bytes(passphrase) # Convert to bytes if not already # Validate passphrase length if not bypassCheck: if len(passphrase) < passStrength: @@ -202,7 +202,7 @@ class OnionrCrypto: if pubkey == '': pubkey = self.pubKey prev = '' - pubkey = pubkey.encode() + pubkey = bytesconverter.str_to_bytes(pubkey) for i in range(self.HASH_ID_ROUNDS): try: prev = prev.encode() @@ -249,10 +249,10 @@ class OnionrCrypto: except AttributeError: pass - difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False) + difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False, coreInst=self._core) - if difficulty < int(config.get('general.minimum_block_pow')): - difficulty = int(config.get('general.minimum_block_pow')) + if difficulty < int(self._core.config.get('general.minimum_block_pow')): + difficulty = int(self._core.config.get('general.minimum_block_pow')) mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode() puzzle = mainHash[:difficulty] @@ -266,7 +266,7 @@ class OnionrCrypto: @staticmethod def replayTimestampValidation(timestamp): - if core.Core()._utils.getEpoch() - int(timestamp) > 2419200: + if epoch.get_epoch() - int(timestamp) > 2419200: return False else: return True diff --git a/onionr/onionrevents.py b/onionr/onionrevents.py index 3301a3ac..beaeca01 100755 --- a/onionr/onionrevents.py +++ b/onionr/onionrevents.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with configuration management. ''' @@ -67,6 +67,7 @@ def call(plugin, event_name, data = None, pluginapi = None): return True except Exception as e: + logger.error(str(e)) return False else: return True diff --git a/onionr/onionrfragment/__init__.py b/onionr/onionrfragment/__init__.py deleted file mode 100755 index c8386465..00000000 --- a/onionr/onionrfragment/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - This file contains the OnionrFragment class which implements the fragment system -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -# onionr:10ch+10ch+10chgdecryptionkey -import core, sys, binascii, os - -FRAGMENT_SIZE = 0.25 -TRUNCATE_LENGTH = 30 - -class OnionrFragment: - def __init__(self, uri=None): - uri = uri.replace('onionr:', '') - count = 0 - blocks = [] - appendData = '' - key = '' - for x in uri: - if x == 'k': - key = uri[uri.index('k') + 1:] - appendData += x - if count == TRUNCATE_LENGTH: - blocks.append(appendData) - appendData = '' - count = 0 - count += 1 - self.key = key - self.blocks = blocks - return - - @staticmethod - def generateFragments(data=None, coreInst=None): - if coreInst is None: - coreInst = core.Core() - - key = os.urandom(32) - data = coreInst._crypto.symmetricEncrypt(data, key).decode() - blocks = [] - blockData = b"" - uri = "onionr:" - total = sys.getsizeof(data) - for x in data: - blockData += x.encode() - if round(len(blockData) / len(data), 3) > FRAGMENT_SIZE: - blocks.append(core.Core().insertBlock(blockData)) - blockData = b"" - - for bl in blocks: - uri += bl[:TRUNCATE_LENGTH] - uri += "k" - uri += binascii.hexlify(key).decode() - return (uri, key) - -if __name__ == '__main__': - uri = OnionrFragment.generateFragments("test")[0] - print(uri) - OnionrFragment(uri) \ No newline at end of file diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py deleted file mode 100755 index 8a88d649..00000000 --- a/onionr/onionrpeers.py +++ /dev/null @@ -1,121 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - This file contains both the PeerProfiles class for network profiling of Onionr nodes -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import core, config, logger, sqlite3 -class PeerProfiles: - ''' - PeerProfiles - ''' - def __init__(self, address, coreInst): - self.address = address # node address - self.score = None - self.friendSigCount = 0 - self.success = 0 - self.failure = 0 - self.connectTime = None - - if not isinstance(coreInst, core.Core): - raise TypeError("coreInst must be a type of core.Core") - self.coreInst = coreInst - assert isinstance(self.coreInst, core.Core) - - self.loadScore() - self.getConnectTime() - return - - def loadScore(self): - '''Load the node's score from the database''' - try: - self.success = int(self.coreInst.getAddressInfo(self.address, 'success')) - except (TypeError, ValueError) as e: - self.success = 0 - self.score = self.success - - def getConnectTime(self): - try: - self.connectTime = int(self.coreInst.getAddressInfo(self.address, 'lastConnect')) - except (KeyError, ValueError, TypeError) as e: - pass - - def saveScore(self): - '''Save the node's score to the database''' - self.coreInst.setAddressInfo(self.address, 'success', self.score) - return - - def addScore(self, toAdd): - '''Add to the peer's score (can add negative)''' - self.score += toAdd - self.saveScore() - -def getScoreSortedPeerList(coreInst): - if not type(coreInst is core.Core): - raise TypeError('coreInst must be instance of core.Core') - - peerList = coreInst.listAdders() - peerScores = {} - peerTimes = {} - - for address in peerList: - # Load peer's profiles into a list - profile = PeerProfiles(address, coreInst) - peerScores[address] = profile.score - if not isinstance(profile.connectTime, type(None)): - peerTimes[address] = profile.connectTime - else: - peerTimes[address] = 9000 - - # Sort peers by their score, greatest to least, and then last connected time - peerList = sorted(peerScores, key=peerScores.get, reverse=True) - peerList = sorted(peerTimes, key=peerTimes.get, reverse=True) - return peerList - -def peerCleanup(coreInst): - '''Removes peers who have been offline too long or score too low''' - if not type(coreInst is core.Core): - raise TypeError('coreInst must be instance of core.Core') - - logger.info('Cleaning peers...') - config.reload() - - adders = getScoreSortedPeerList(coreInst) - adders.reverse() - - if len(adders) > 1: - - minScore = int(config.get('peers.minimum_score', -100)) - maxPeers = int(config.get('peers.max_stored', 5000)) - - for address in adders: - # Remove peers that go below the negative score - if PeerProfiles(address, coreInst).score < minScore: - coreInst.removeAddress(address) - try: - if (int(coreInst._utils.getEpoch()) - int(coreInst.getPeerInfo(address, 'dateSeen'))) >= 600: - expireTime = 600 - else: - expireTime = 86400 - coreInst._blacklist.addToDB(address, dataType=1, expire=expireTime) - except sqlite3.IntegrityError: #TODO just make sure its not a unique constraint issue - pass - except ValueError: - pass - logger.warn('Removed address ' + address + '.') - - # Unban probably not malicious peers TODO improve - coreInst._blacklist.deleteExpired(dataType=1) diff --git a/onionr/onionrpeers/__init__.py b/onionr/onionrpeers/__init__.py new file mode 100755 index 00000000..cd4aa8b2 --- /dev/null +++ b/onionr/onionrpeers/__init__.py @@ -0,0 +1,4 @@ +from . import scoresortedpeerlist, peercleanup, peerprofiles +get_score_sorted_peer_list = scoresortedpeerlist.get_score_sorted_peer_list +peer_cleanup = peercleanup.peer_cleanup +PeerProfiles = peerprofiles.PeerProfiles \ No newline at end of file diff --git a/onionr/onionrpeers/peercleanup.py b/onionr/onionrpeers/peercleanup.py new file mode 100644 index 00000000..4446aa3c --- /dev/null +++ b/onionr/onionrpeers/peercleanup.py @@ -0,0 +1,54 @@ +''' + Onionr - Private P2P Communication + + Cleanup the peer database +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sqlite3 +import logger +from onionrutils import epoch +from . import scoresortedpeerlist, peerprofiles +def peer_cleanup(core_inst): + '''Removes peers who have been offline too long or score too low''' + config = core_inst.config + logger.info('Cleaning peers...') + + adders = scoresortedpeerlist.get_score_sorted_peer_list(core_inst) + adders.reverse() + + if len(adders) > 1: + + min_score = int(config.get('peers.minimum_score', -100)) + max_peers = int(config.get('peers.max_stored', 5000)) + + for address in adders: + # Remove peers that go below the negative score + if peerprofiles.PeerProfiles(address, core_inst).score < min_score: + core_inst.removeAddress(address) + try: + if (int(epoch.get_epoch()) - int(core_inst.getPeerInfo(address, 'dateSeen'))) >= 600: + expireTime = 600 + else: + expireTime = 86400 + core_inst._blacklist.addToDB(address, dataType=1, expire=expireTime) + except sqlite3.IntegrityError: #TODO just make sure its not a unique constraint issue + pass + except ValueError: + pass + logger.warn('Removed address ' + address + '.') + + # Unban probably not malicious peers TODO improve + core_inst._blacklist.deleteExpired(dataType=1) \ No newline at end of file diff --git a/onionr/onionrpeers/peerprofiles.py b/onionr/onionrpeers/peerprofiles.py new file mode 100644 index 00000000..791e8c45 --- /dev/null +++ b/onionr/onionrpeers/peerprofiles.py @@ -0,0 +1,60 @@ +''' + Onionr - Private P2P Communication + + This file contains both the PeerProfiles class for network profiling of Onionr nodes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +class PeerProfiles: + ''' + PeerProfiles + ''' + def __init__(self, address, coreInst): + self.address = address # node address + self.score = None + self.friendSigCount = 0 + self.success = 0 + self.failure = 0 + self.connectTime = None + + self.coreInst = coreInst + + self.loadScore() + self.getConnectTime() + return + + def loadScore(self): + '''Load the node's score from the database''' + try: + self.success = int(self.coreInst.getAddressInfo(self.address, 'success')) + except (TypeError, ValueError) as e: + self.success = 0 + self.score = self.success + + def getConnectTime(self): + try: + self.connectTime = int(self.coreInst.getAddressInfo(self.address, 'lastConnect')) + except (KeyError, ValueError, TypeError) as e: + pass + + def saveScore(self): + '''Save the node's score to the database''' + self.coreInst.setAddressInfo(self.address, 'success', self.score) + return + + def addScore(self, toAdd): + '''Add to the peer's score (can add negative)''' + self.score += toAdd + self.saveScore() \ No newline at end of file diff --git a/onionr/onionrpeers/scoresortedpeerlist.py b/onionr/onionrpeers/scoresortedpeerlist.py new file mode 100644 index 00000000..9c30e586 --- /dev/null +++ b/onionr/onionrpeers/scoresortedpeerlist.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Return a reliability score sorted list of peers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from . import peerprofiles +def get_score_sorted_peer_list(coreInst): + peer_list = coreInst.listAdders() + peer_scores = {} + peer_times = {} + + for address in peer_list: + # Load peer's profiles into a list + profile = peerprofiles.PeerProfiles(address, coreInst) + peer_scores[address] = profile.score + if not isinstance(profile.connectTime, type(None)): + peer_times[address] = profile.connectTime + else: + peer_times[address] = 9000 + + # Sort peers by their score, greatest to least, and then last connected time + peer_list = sorted(peer_scores, key=peer_scores.get, reverse=True) + peer_list = sorted(peer_times, key=peer_times.get, reverse=True) + return peer_list \ No newline at end of file diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py index 0120dad7..9b9842c3 100755 --- a/onionr/onionrpluginapi.py +++ b/onionr/onionrpluginapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with the object that is passed with each event ''' @@ -19,6 +19,7 @@ ''' import onionrplugins, core as onionrcore, logger +from onionrutils import localcommand class DaemonAPI: def __init__(self, pluginapi): @@ -40,7 +41,7 @@ class DaemonAPI: return def local_command(self, command): - return self.pluginapi.get_utils().localCommand(self, command) + return localcommand.local_command(self.pluginapi.get_core(), command) def queue_pop(self): return self.get_core().daemonQueue() @@ -169,9 +170,6 @@ class pluginapi: def get_core(self): return self.core - def get_utils(self): - return self.get_core()._utils - def get_crypto(self): return self.get_core()._crypto diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index 7595805b..a76fc92b 100755 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file deals with management of modules/plugins. ''' @@ -17,17 +17,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - -import os, re, importlib, config, logger -import onionrevents as events - +import os, re, importlib +import onionrevents as events, config, logger +from utils import identifyhome # set data dir -dataDir = os.environ.get('ONIONR_HOME', os.environ.get('DATA_DIR', 'data/')) -if not dataDir.endswith('/'): - dataDir += '/' +dataDir = identifyhome.identify_home() _pluginsfolder = dataDir + 'plugins/' _instances = dict() +config.reload() def reload(onionr = None, stop_event = True): ''' @@ -71,19 +69,19 @@ def enable(name, onionr = None, start_event = True): events.call(get_plugin(name), 'enable', onionr) except ImportError: # Was getting import error on Gitlab CI test "data" # NOTE: If you are experiencing issues with plugins not being enabled, it might be this resulting from an error in the module - # can happen inconsistenly (especially between versions) + # can happen inconsistently (especially between versions) return False else: enabled_plugins.append(name) - config.set('plugins.enabled', enabled_plugins, True) - + config.set('plugins.enabled', enabled_plugins, savefile=True) + if start_event is True: start(name) return True else: return False else: - logger.error('Failed to enable plugin \"%s\", disabling plugin.' % name) + logger.error('Failed to enable plugin \"%s\", disabling plugin.' % name, terminal=True) disable(name) return False @@ -170,6 +168,7 @@ def import_module_from_file(full_path_to_module): module_dir, module_file = os.path.split(full_path_to_module) module_name, module_ext = os.path.splitext(module_file) + module_name = module_dir # Module name must be unique otherwise it will get written in other imports # Get module "spec" from filename spec = importlib.util.spec_from_file_location(module_name,full_path_to_module) @@ -187,7 +186,7 @@ def get_plugin(name): if str(name).lower() in _instances: return _instances[str(name).lower()] else: - _instances[str(name).lower()] = import_module_from_file(get_plugins_folder(name, False) + 'main.py') + _instances[str(name).lower()] = import_module_from_file(get_plugins_folder(str(name).lower(), False) + 'main.py') return get_plugin(name) def get_plugins(): @@ -211,8 +210,6 @@ def get_enabled_plugins(): check() - config.reload() - return list(config.get('plugins.enabled', list())) def is_enabled(name): @@ -233,6 +230,7 @@ def get_plugins_folder(name = None, absolute = True): path = _pluginsfolder else: # only allow alphanumeric characters + #path = _pluginsfolder + str(name.lower()) path = _pluginsfolder + re.sub('[^0-9a-zA-Z_]+', '', str(name).lower()) if absolute is True: @@ -245,21 +243,21 @@ def get_plugin_data_folder(name, absolute = True): Returns the location of a plugin's data folder ''' - return get_plugins_folder(name, absolute) + dataDir + return get_plugins_folder(name, absolute) def check(): ''' Checks to make sure files exist ''' - config.reload() - if not config.is_set('plugins'): logger.debug('Generating plugin configuration data...') config.set('plugins', {'enabled': []}, True) if not os.path.exists(os.path.dirname(get_plugins_folder())): logger.debug('Generating plugin data folder...') - os.makedirs(os.path.dirname(get_plugins_folder())) - + try: + os.makedirs(os.path.dirname(get_plugins_folder())) + except FileExistsError: + pass return diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 62dc215c..51f35325 100755 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Proof of work module ''' @@ -17,8 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, base64, json -import core, onionrutils, config, logger, onionrblockapi +import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json +import core, config, logger, onionrblockapi +from onionrutils import bytesconverter + +config.reload() def getDifficultyModifier(coreOrUtilsInst=None): '''Accepts a core or utils instance returns @@ -27,12 +30,7 @@ def getDifficultyModifier(coreOrUtilsInst=None): ''' classInst = coreOrUtilsInst retData = 0 - if isinstance(classInst, core.Core): - useFunc = classInst._utils.storageCounter.getPercent - elif isinstance(classInst, onionrutils.OnionrUtils): - useFunc = classInst.storageCounter.getPercent - else: - useFunc = core.Core()._utils.storageCounter.getPercent + useFunc = classInst.storage_counter.getPercent percentUse = useFunc() @@ -45,7 +43,7 @@ def getDifficultyModifier(coreOrUtilsInst=None): return retData -def getDifficultyForNewBlock(data, ourBlock=True): +def getDifficultyForNewBlock(data, ourBlock=True, coreInst=None): ''' Get difficulty for block. Accepts size in integer, Block instance, or str/bytes full block contents ''' @@ -53,20 +51,15 @@ def getDifficultyForNewBlock(data, ourBlock=True): dataSize = 0 if isinstance(data, onionrblockapi.Block): dataSize = len(data.getRaw().encode('utf-8')) - elif isinstance(data, str): - dataSize = len(data.encode('utf-8')) - elif isinstance(data, bytes): - dataSize = len(data) - elif isinstance(data, int): - dataSize = data else: - raise ValueError('not Block, str, or int') + dataSize = len(bytesconverter.str_to_bytes(data)) + if ourBlock: minDifficulty = config.get('general.minimum_send_pow', 4) else: minDifficulty = config.get('general.minimum_block_pow', 4) - retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier() + retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier(coreInst) return retData @@ -87,7 +80,6 @@ def hashMeetsDifficulty(h): ''' Return bool for a hash string to see if it meets pow difficulty defined in config ''' - config.reload() hashDifficulty = getHashDifficulty(h) try: expected = int(config.get('general.minimum_block_pow')) @@ -105,7 +97,6 @@ class DataPOW: self.data = data self.threadCount = threadCount self.rounds = 0 - config.reload() if forceDifficulty == 0: dataLen = sys.getsizeof(data) @@ -131,8 +122,6 @@ class DataPOW: for i in range(max(1, threadCount)): t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore)) t.start() - - return def pow(self, reporting = False, myCore = None): startTime = math.floor(time.time()) @@ -212,18 +201,19 @@ class POW: else: myCore = coreInst - dataLen = len(data) + len(json.dumps(metadata)) + json_metadata = json.dumps(metadata).encode() - if forceDifficulty > 0: - self.difficulty = forceDifficulty - else: - # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = getDifficultyForNewBlock(dataLen) - try: self.data = self.data.encode() except AttributeError: pass + + if forceDifficulty > 0: + self.difficulty = forceDifficulty + else: + # Calculate difficulty. Dumb for now, may use good algorithm in the future. + self.difficulty = getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data), coreInst=myCore) + logger.info('Computing POW (difficulty: %s)...' % self.difficulty) @@ -247,7 +237,7 @@ class POW: startNonce = nonce while self.hashing: #token = nacl.hash.blake2b(rand + self.data).decode() - self.metadata['powRandomToken'] = nonce + self.metadata['pow'] = nonce payload = json.dumps(self.metadata).encode() + b'\n' + self.data token = myCore._crypto.sha3Hash(payload) try: @@ -259,7 +249,6 @@ class POW: self.hashing = False iFound = True self.result = payload - print('count', nonce - startNonce) break nonce += 1 diff --git a/onionr/onionrservices/README.md b/onionr/onionrservices/README.md new file mode 100755 index 00000000..2022231c --- /dev/null +++ b/onionr/onionrservices/README.md @@ -0,0 +1,13 @@ +# onionrservices + +onionservices is a submodule to handle direct connections to Onionr peers, using the Onionr network to broker them. + +## Files + +__init__.py: Contains the OnionrServices class which can create direct connection servers or clients. + +bootstrapservice.py: Creates a bootstrap server for a peer and announces the connection by creating a block encrypted to the peer we want to connect to. + +connectionserver.py: Creates a direct connection server for a peer + +httpheaders.py: Modifies a Flask response object http response headers for security purposes. \ No newline at end of file diff --git a/onionr/onionrservices/__init__.py b/onionr/onionrservices/__init__.py new file mode 100755 index 00000000..1f56dabe --- /dev/null +++ b/onionr/onionrservices/__init__.py @@ -0,0 +1,61 @@ +''' + Onionr - Private P2P Communication + + Onionr services provide the server component to direct connections +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time +import stem +import core +from . import connectionserver, bootstrapservice +from onionrutils import stringvalidators, basicrequests + +class OnionrServices: + ''' + Create a client or server for connecting to peer interfaces + ''' + def __init__(self, onionr_core): + assert isinstance(onionr_core, core.Core) + self._core = onionr_core + self.servers = {} + self.clients = {} + self.shutdown = False + return + + def create_server(self, peer, address): + ''' + When a client wants to connect, contact their bootstrap address and tell them our + ephemeral address for our service by creating a new ConnectionServer instance + ''' + assert stringvalidators.validate_transport(address) + BOOTSTRAP_TRIES = 10 # How many times to attempt contacting the bootstrap server + TRY_WAIT = 3 # Seconds to wait before trying bootstrap again + # HTTP is fine because .onion/i2p is encrypted/authenticated + base_url = 'http://%s/' % (address,) + socks = self._core.config.get('tor.socksport') + for x in range(BOOTSTRAP_TRIES): + if basicrequests.do_get_request(self._core, base_url + 'ping', port=socks, ignoreAPI=True) == 'pong!': + # if bootstrap sever is online, tell them our service address + connectionserver.ConnectionServer(peer, address, core_inst=self._core) + else: + time.sleep(TRY_WAIT) + else: + return False + + def create_client(self, peer): + # Create ephemeral onion service to bootstrap connection + address = bootstrapservice.bootstrap_client_service(peer) + return address \ No newline at end of file diff --git a/onionr/onionrservices/bootstrapservice.py b/onionr/onionrservices/bootstrapservice.py new file mode 100755 index 00000000..2c23983b --- /dev/null +++ b/onionr/onionrservices/bootstrapservice.py @@ -0,0 +1,89 @@ +''' + Onionr - Private P2P Communication + + Bootstrap onion direct connections for the clients +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time, threading, uuid +from gevent.pywsgi import WSGIServer, WSGIHandler +from stem.control import Controller +from flask import Flask, Response +import core +from netcontroller import get_open_port +from . import httpheaders +from onionrutils import stringvalidators, epoch + +def bootstrap_client_service(peer, core_inst=None, bootstrap_timeout=300): + ''' + Bootstrap client services + ''' + if core_inst is None: + core_inst = core.Core() + + if not stringvalidators.validate_pub_key(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + bootstrap_port = get_open_port() + bootstrap_app = Flask(__name__) + http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) + try: + assert core_inst.onionrInst.communicatorInst is not None + except (AttributeError, AssertionError) as e: + pass + else: + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) + + bootstrap_address = '' + shutdown = False + bs_id = str(uuid.uuid4()) + + @bootstrap_app.route('/ping') + def get_ping(): + return "pong!" + + @bootstrap_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + + @bootstrap_app.route('/bs/
', methods=['POST']) + def get_bootstrap(address): + if stringvalidators.validate_transport(address + '.onion'): + # Set the bootstrap address then close the server + bootstrap_address = address + '.onion' + core_inst.keyStore.put(bs_id, bootstrap_address) + http_server.stop() + return Response("success") + else: + return Response("") + + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: + # Connect to the Tor process for Onionr + controller.authenticate(core_inst.config.get('tor.controlpassword')) + # Create the v3 onion service + response = controller.create_ephemeral_hidden_service({80: bootstrap_port}, key_type = 'NEW', key_content = 'ED25519-V3', await_publication = True) + core_inst.insertBlock(response.service_id, header='con', sign=True, encryptType='asym', + asymPeer=peer, disableForward=True, expire=(epoch.get_epoch() + bootstrap_timeout)) + # Run the bootstrap server + try: + http_server.serve_forever() + except TypeError: + pass + # This line reached when server is shutdown by being bootstrapped + + # Now that the bootstrap server has received a server, return the address + return core_inst.keyStore.get(bs_id) diff --git a/onionr/onionrservices/connectionserver.py b/onionr/onionrservices/connectionserver.py new file mode 100755 index 00000000..047fdcf1 --- /dev/null +++ b/onionr/onionrservices/connectionserver.py @@ -0,0 +1,89 @@ +''' + Onionr - Private P2P Communication + + This module does the second part of the bootstrap block handshake and creates the API server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +from gevent.pywsgi import WSGIServer +from stem.control import Controller +from flask import Flask +import core, logger, httpapi +import onionrexceptions +from netcontroller import get_open_port +from httpapi import apiutils +from onionrutils import stringvalidators, basicrequests, bytesconverter +from . import httpheaders + +class ConnectionServer: + def __init__(self, peer, address, core_inst=None): + if core_inst is None: + self.core_inst = core.Core() + else: + self.core_inst = core_inst + + if not stringvalidators.validate_pub_key(peer): + raise ValueError('Peer must be valid base32 ed25519 public key') + + socks = core_inst.config.get('tor.socksport') # Load config for Tor socks port for proxy + service_app = Flask(__name__) # Setup Flask app for server. + service_port = get_open_port() + service_ip = apiutils.setbindip.set_bind_IP(core_inst=self.core_inst) + http_server = WSGIServer(('127.0.0.1', service_port), service_app, log=None) + core_inst.onionrInst.communicatorInst.service_greenlets.append(http_server) + + # TODO define basic endpoints useful for direct connections like stats + + httpapi.load_plugin_blueprints(service_app, blueprint='direct_blueprint') + + @service_app.route('/ping') + def get_ping(): + return "pong!" + + @service_app.route('/close') + def shutdown_server(): + core_inst.onionrInst.communicatorInst.service_greenlets.remove(http_server) + http_server.stop() + return Response('goodbye') + + @service_app.after_request + def afterReq(resp): + # Security headers + resp = httpheaders.set_default_onionr_http_headers(resp) + return resp + + with Controller.from_port(port=core_inst.config.get('tor.controlPort')) as controller: + # Connect to the Tor process for Onionr + controller.authenticate(core_inst.config.get('tor.controlpassword')) + # Create the v3 onion service for the peer to connect to + response = controller.create_ephemeral_hidden_service({80: service_port}, await_publication = True, key_type='NEW', key_content = 'ED25519-V3') + + try: + for x in range(3): + attempt = basicrequests.do_post_request(self.core_inst, 'http://' + address + '/bs/' + response.service_id, port=socks) + if attempt == 'success': + break + else: + raise ConnectionError + except ConnectionError: + # Re-raise + raise ConnectionError('Could not reach %s bootstrap address %s' % (peer, address)) + else: + # If no connection error, create the service and save it to local global key store + self.core_inst.keyStore.put('dc-' + response.service_id, bytesconverter.bytes_to_str(peer)) + logger.info('hosting on %s with %s' % (response.service_id, peer)) + http_server.serve_forever() + http_server.stop() + self.core_inst.keyStore.delete('dc-' + response.service_id) \ No newline at end of file diff --git a/onionr/onionrservices/httpheaders.py b/onionr/onionrservices/httpheaders.py new file mode 100755 index 00000000..63c4a6ad --- /dev/null +++ b/onionr/onionrservices/httpheaders.py @@ -0,0 +1,28 @@ +''' + Onionr - Private P2P Communication + + Set default onionr http headers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def set_default_onionr_http_headers(flask_response): + '''Response headers''' + flask_response.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:" + flask_response.headers['X-Frame-Options'] = 'deny' + flask_response.headers['X-Content-Type-Options'] = "nosniff" + flask_response.headers['Server'] = '' + flask_response.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch. + flask_response.headers['Connection'] = "close" + return flask_response \ No newline at end of file diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py deleted file mode 100755 index aa02c3db..00000000 --- a/onionr/onionrsockets.py +++ /dev/null @@ -1,172 +0,0 @@ -''' - Onionr - P2P Anonymous Storage Network - - Onionr Socket interface -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -import stem.control -import threading -import socks, config, uuid -import onionrexceptions, time, requests, onionrblockapi, logger -from dependencies import secrets -from gevent.pywsgi import WSGIServer -from flask import request, Response, abort -import flask -class OnionrSocketServer: - def __init__(self, coreInst): - self._core = coreInst - app = flask.Flask(__name__) - self._core.socketServerConnData = {} - self.bindPort = 0 - - self.sockets = {} - - while self.bindPort < 1024: - self.bindPort = secrets.randbelow(65535) - - self.responseData = {} - - threading.Thread(target=self.detectShutdown).start() - threading.Thread(target=self.socketStarter).start() - app = flask.Flask(__name__) - self.http_server = WSGIServer(('127.0.0.1', self.bindPort), app) - self.http_server.serve_forever() - - @app.route('/dc/', methods=['POST']) - def acceptConn(self): - data = request.form['data'] - data = self._core._utils.bytesTorStr(data) - data = {'date': self._core._utils.getEpoch(), 'data': data} - myPeer = '' - retData = '' - for peer in self.sockets: - if self.sockets[peer] == request.host: - myPeer = peer - break - else: - return "" - - if request.host in self.sockets: - self._core.socketServerConnData[myPeer].append(data) - else: - self._core.socketServerConnData[myPeer] = [data] - - try: - retData = self._core.socketServerResponseData[myPeer] - except KeyError: - pass - else: - self._core.socketServerResponseData[myPeer] = '' - - return retData - - def socketStarter(self): - while not self._core.killSockets: - try: - self.addSocket(self._core.startSocket['peer'], reason=self._core.startSocket['reason']) - except KeyError: - pass - else: - logger.info('%s socket started with %s' % (self._core.startSocket['reason'], self._core.startSocket['peer'])) - self._core.startSocket = {} - time.sleep(1) - - def detectShutdown(self): - while not self._core.killSockets: - time.sleep(5) - - logger.debug('Killing socket server...') - self.http_server.stop() - - def addSocket(self, peer, reason=''): - bindPort = 1337 - - assert len(reason) <= 12 - - with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: - controller.authenticate(config.get('tor.controlpassword')) - - socket = controller.create_ephemeral_hidden_service({80: bindPort}, await_publication = True) - self.sockets[peer] = socket.service_id + '.onion' - - self.responseData[socket.service_id + '.onion'] = '' - - self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) - self._core.socketReasons[peer] = reason - return - -class OnionrSocketClient: - def __init__(self, coreInst): - self.sockets = {} # pubkey: tor address - self.connPool = {} - self.sendData = {} - self._core = coreInst - self.response = '' - self.request = '' - self.connected = False - self.killSocket = False - - def startSocket(self, peer, reason): - address = '' - logger.info('Trying to find socket server for %s' % (peer,)) - # Find the newest open socket for a given peer - for block in self._core.getBlocksByType('socket'): - block = onionrblockapi.Block(block, core=self._core) - if block.decrypt(): - theSigner = block.signer - try: - theSigner = theSigner.decode() - except AttributeError: - pass - if block.verifySig() and theSigner == peer: - address = block.getMetadata('address') - if self._core._utils.validateID(address): - # If we got their address, it is valid, and verified, we can break out - if block.getMetadata('reason') == reason: - break - else: - logger.error('The socket the peer opened is not for %s' % (reason,)) - else: - logger.error('Peer transport id is invalid for socket: %s' % (address,)) - address = '' - else: - logger.warn('Block has invalid sig or id, was for %s' % (theSigner,)) - if address != '': - logger.info('%s socket client started with %s' % (reason, peer)) - self.sockets[peer] = address - data = 'hey' - while not self.killSocket: - try: - data = self.sendData[peer] - logger.info('Sending %s to %s' % (data, peer)) - except KeyError: - pass - else: - self.sendData[peer] = '' - postData = {'data': data} - self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} - time.sleep(2) - - def getResponse(self, peer): - retData = '' - try: - retData = self.connPool[peer] - except KeyError: - pass - return - - def sendData(self, peer, data): - self.sendData[peer] = data diff --git a/onionr/onionrstorage.py b/onionr/onionrstorage/__init__.py old mode 100644 new mode 100755 similarity index 87% rename from onionr/onionrstorage.py rename to onionr/onionrstorage/__init__.py index 65fe6757..4bfc8bf6 --- a/onionr/onionrstorage.py +++ b/onionr/onionrstorage/__init__.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles block storage, providing an abstraction for storing blocks between file system and database ''' @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import core, sys, sqlite3, os, dbcreator +import core, sys, sqlite3, os, dbcreator, onionrexceptions +from onionrutils import bytesconverter, stringvalidators DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option @@ -65,7 +66,7 @@ def deleteBlock(coreInst, blockHash): def store(coreInst, data, blockHash=''): assert isinstance(coreInst, core.Core) - assert coreInst._utils.validateHash(blockHash) + assert stringvalidators.validate_hash(blockHash) ourHash = coreInst._crypto.sha3Hash(data) if blockHash != '': assert ourHash == blockHash @@ -80,9 +81,9 @@ def store(coreInst, data, blockHash=''): def getData(coreInst, bHash): assert isinstance(coreInst, core.Core) - assert coreInst._utils.validateHash(bHash) + assert stringvalidators.validate_hash(bHash) - bHash = coreInst._utils.bytesToStr(bHash) + bHash = bytesconverter.bytes_to_str(bHash) # First check DB for data entry by hash # if no entry, check disk @@ -94,4 +95,6 @@ def getData(coreInst, bHash): retData = block.read() else: retData = _dbFetch(coreInst, bHash) + if retData is None: + raise onionrexceptions.NoDataAvailable("Block data for %s is not available" % [bHash]) return retData \ No newline at end of file diff --git a/onionr/onionrstorage/removeblock.py b/onionr/onionrstorage/removeblock.py new file mode 100644 index 00000000..76112199 --- /dev/null +++ b/onionr/onionrstorage/removeblock.py @@ -0,0 +1,21 @@ +import sys, sqlite3 +import onionrexceptions, onionrstorage +from onionrutils import stringvalidators +def remove_block(core_inst, block): + ''' + remove a block from this node (does not automatically blacklist) + + **You may want blacklist.addToDB(blockHash) + ''' + + if stringvalidators.validate_hash(block): + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + t = (block,) + c.execute('Delete from hashes where hash=?;', t) + conn.commit() + conn.close() + dataSize = sys.getsizeof(onionrstorage.getData(core_inst, block)) + core_inst.storage_counter.removeBytes(dataSize) + else: + raise onionrexceptions.InvalidHexHash \ No newline at end of file diff --git a/onionr/onionrstorage/setdata.py b/onionr/onionrstorage/setdata.py new file mode 100644 index 00000000..05cd24d1 --- /dev/null +++ b/onionr/onionrstorage/setdata.py @@ -0,0 +1,36 @@ +import sys, sqlite3 +import onionrstorage, onionrexceptions +def set_data(core_inst, data): + ''' + Set the data assciated with a hash + ''' + + data = data + dataSize = sys.getsizeof(data) + + if not type(data) is bytes: + data = data.encode() + + dataHash = core_inst._crypto.sha3Hash(data) + + if type(dataHash) is bytes: + dataHash = dataHash.decode() + blockFileName = core_inst.blockDataLocation + dataHash + '.dat' + try: + onionrstorage.getData(core_inst, dataHash) + except onionrexceptions.NoDataAvailable: + if core_inst.storage_counter.addBytes(dataSize) != False: + onionrstorage.store(core_inst, data, blockHash=dataHash) + conn = sqlite3.connect(core_inst.blockDB, timeout=30) + c = conn.cursor() + c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) + conn.commit() + conn.close() + with open(core_inst.dataNonceFile, 'a') as nonceFile: + nonceFile.write(dataHash + '\n') + else: + raise onionrexceptions.DiskAllocationReached + else: + raise onionrexceptions.DataExists("Data is already set for " + dataHash) + + return dataHash \ No newline at end of file diff --git a/onionr/onionrusers/README.md b/onionr/onionrusers/README.md new file mode 100755 index 00000000..e85c0952 --- /dev/null +++ b/onionr/onionrusers/README.md @@ -0,0 +1,9 @@ +# onionrusers + +onionrusers is a small collection of classes for interacting with onionr public keys, such as encrypting messages to them with forward secrecy, interacting with their settings, or else. + +## Files + +onionrusers.py: OnionrUsers class can be used to encrypt/decrypt messages to a particular Onionr user (incl. forward secrecy), view information about them, and get our friend list. + +contactmanager.py: Inheriting from OnionrUsers, ContactManager allows arbitrary information to be associated with an Onionr user. \ No newline at end of file diff --git a/onionr/onionrusers/__init__.py b/onionr/onionrusers/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index c47bbe45..680de9b0 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Sets more abstract information related to a peer. Can be thought of as traditional 'contact' system ''' @@ -18,10 +18,13 @@ along with this program. If not, see . ''' import os, json, onionrexceptions +import unpaddedbase32 from onionrusers import onionrusers +from onionrutils import bytesconverter, epoch class ContactManager(onionrusers.OnionrUser): def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): + publicKey = unpaddedbase32.repad(bytesconverter.str_to_bytes(publicKey)).decode() super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser) self.dataDir = coreInst.dataDir + '/contacts/' self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey) @@ -39,7 +42,7 @@ class ContactManager(onionrusers.OnionrUser): dataFile.write(data) def _loadData(self): - self.lastRead = self._core._utils.getEpoch() + self.lastRead = epoch.get_epoch() retData = {} if os.path.exists(self.dataFile): with open(self.dataFile, 'r') as dataFile: @@ -59,7 +62,7 @@ class ContactManager(onionrusers.OnionrUser): if self.deleted: raise onionrexceptions.ContactDeleted - if (self._core._utils.getEpoch() - self.lastRead >= self.recordExpire) or forceReload: + if (epoch.get_epoch() - self.lastRead >= self.recordExpire) or forceReload: self.data = self._loadData() try: return self.data[key] diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index e05aac0e..28a5784d 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication Contains abstractions for interacting with users of Onionr ''' @@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import onionrblockapi, logger, onionrexceptions, json, sqlite3, time +import logger, onionrexceptions, json, sqlite3, time +from onionrutils import stringvalidators, bytesconverter, epoch +import unpaddedbase32 import nacl.exceptions def deleteExpiredKeys(coreInst): @@ -25,7 +27,7 @@ def deleteExpiredKeys(coreInst): conn = sqlite3.connect(coreInst.forwardKeysFile, timeout=10) c = conn.cursor() - curTime = coreInst._utils.getEpoch() + curTime = epoch.get_epoch() c.execute("DELETE from myForwardKeys where expire <= ?", (curTime,)) conn.commit() conn.execute("VACUUM") @@ -37,7 +39,7 @@ def deleteTheirExpiredKeys(coreInst, pubkey): c = conn.cursor() # Prepare the insert - command = (pubkey, coreInst._utils.getEpoch()) + command = (pubkey, epoch.get_epoch()) c.execute("DELETE from forwardKeys where peerKey = ? and expire <= ?", command) @@ -55,8 +57,7 @@ class OnionrUser: Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser saveUser determines if we should add a user to our peer database or not. ''' - if ' ' in coreInst._utils.bytesToStr(publicKey).strip(): - publicKey = coreInst._utils.convertHumanReadableID(publicKey) + publicKey = unpaddedbase32.repad(bytesconverter.str_to_bytes(publicKey)).decode() self.trust = 0 self._core = coreInst @@ -103,7 +104,7 @@ class OnionrUser: deleteExpiredKeys(self._core) retData = '' forwardKey = self._getLatestForwardKey() - if self._core._utils.validatePubKey(forwardKey[0]): + if stringvalidators.validate_pub_key(forwardKey[0]): retData = self._core._crypto.pubKeyEncrypt(data, forwardKey[0], encodedData=True) else: raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") @@ -158,10 +159,10 @@ class OnionrUser: conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) c = conn.cursor() # Prepare the insert - time = self._core._utils.getEpoch() + time = epoch.get_epoch() newKeys = self._core._crypto.generatePubKey() - newPub = self._core._utils.bytesToStr(newKeys[0]) - newPriv = self._core._utils.bytesToStr(newKeys[1]) + newPub = bytesconverter.bytes_to_str(newKeys[0]) + newPriv = bytesconverter.bytes_to_str(newKeys[1]) command = (self.publicKey, newPub, newPriv, time, expire + time) @@ -176,7 +177,7 @@ class OnionrUser: conn = sqlite3.connect(self._core.forwardKeysFile, timeout=10) c = conn.cursor() pubkey = self.publicKey - pubkey = self._core._utils.bytesToStr(pubkey) + pubkey = bytesconverter.bytes_to_str(pubkey) command = (pubkey,) keyList = [] # list of tuples containing pub, private for peer @@ -190,7 +191,8 @@ class OnionrUser: return list(keyList) def addForwardKey(self, newKey, expire=DEFAULT_KEY_EXPIRE): - if not self._core._utils.validatePubKey(newKey): + newKey = bytesconverter.bytes_to_str(unpaddedbase32.repad(bytesconverter.str_to_bytes(newKey))) + if not stringvalidators.validate_pub_key(newKey): # Do not add if something went wrong with the key raise onionrexceptions.InvalidPubkey(newKey) @@ -198,7 +200,7 @@ class OnionrUser: c = conn.cursor() # Get the time we're inserting the key at - timeInsert = self._core._utils.getEpoch() + timeInsert = epoch.get_epoch() # Look at our current keys for duplicate key data or time for entry in self._getForwardKeys(): diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py deleted file mode 100755 index f8fd13af..00000000 --- a/onionr/onionrutils.py +++ /dev/null @@ -1,565 +0,0 @@ -''' - Onionr - P2P Microblogging Platform & Social network - - OnionrUtils offers various useful functions to Onionr. Relatively misc. -''' -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' -# Misc functions that do not fit in the main api, but are useful -import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string -import nacl.signing, nacl.encoding -from onionrblockapi import Block -import onionrexceptions -from onionr import API_VERSION -import onionrevents -import storagecounter -from etc import pgpwords -from onionrusers import onionrusers -if sys.version_info < (3, 6): - try: - import sha3 - except ModuleNotFoundError: - logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') - sys.exit(1) - -class OnionrUtils: - ''' - Various useful functions for validating things, etc functions, connectivity - ''' - def __init__(self, coreInstance): - #self.fingerprintFile = 'data/own-fingerprint.txt' #TODO Remove since probably not needed - self._core = coreInstance # onionr core instance - - self.timingToken = '' # for when we make local connections to our http api, to bypass timing attack defense mechanism - self.avoidDupe = [] # list used to prevent duplicate requests per peer for certain actions - self.peerProcessing = {} # dict of current peer actions: peer, actionList - self.storageCounter = storagecounter.StorageCounter(self._core) # used to keep track of how much data onionr is using on disk - config.reload() # onionr config - return - - def getTimeBypassToken(self): - ''' - Load our timingToken from disk for faster local HTTP API - ''' - try: - if os.path.exists(self._core.dataDir + 'time-bypass.txt'): - with open(self._core.dataDir + 'time-bypass.txt', 'r') as bypass: - self.timingToken = bypass.read() - except Exception as error: - logger.error('Failed to fetch time bypass token.', error = error) - - return self.timingToken - - def getRoundedEpoch(self, roundS=60): - ''' - Returns the epoch, rounded down to given seconds (Default 60) - ''' - epoch = self.getEpoch() - return epoch - (epoch % roundS) - - def getClientAPIServer(self): - retData = '' - try: - with open(self._core.privateApiHostFile, 'r') as host: - hostname = host.read() - except FileNotFoundError: - raise FileNotFoundError - else: - retData += '%s:%s' % (hostname, config.get('client.client.port')) - return retData - - def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=20): - ''' - Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. - ''' - config.reload() - self.getTimeBypassToken() - # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. - hostname = '' - waited = 0 - while hostname == '': - try: - hostname = self.getClientAPIServer() - except FileNotFoundError: - time.sleep(1) - waited += 1 - if waited == maxWait: - return False - if data != '': - data = '&data=' + urllib.parse.quote_plus(data) - payload = 'http://%s/%s%s' % (hostname, command, data) - try: - if post: - retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text - else: - retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text - except Exception as error: - if not silent: - logger.error('Failed to make local request (command: %s):%s' % (command, error)) - retData = False - - return retData - - def getHumanReadableID(self, pub=''): - '''gets a human readable ID from a public key''' - if pub == '': - pub = self._core._crypto.pubKey - pub = base64.b16encode(base64.b32decode(pub)).decode() - return ' '.join(pgpwords.wordify(pub)) - - def convertHumanReadableID(self, pub): - '''Convert a human readable pubkey id to base32''' - pub = pub.lower() - return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip())))) - - def getBlockMetadataFromData(self, blockData): - ''' - accepts block contents as string, returns a tuple of - metadata, meta (meta being internal metadata, which will be - returned as an encrypted base64 string if it is encrypted, dict if not). - ''' - meta = {} - metadata = {} - data = blockData - try: - blockData = blockData.encode() - except AttributeError: - pass - - try: - metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) - except json.decoder.JSONDecodeError: - pass - else: - data = blockData[blockData.find(b'\n'):].decode() - - if not metadata['encryptType'] in ('asym', 'sym'): - try: - meta = json.loads(metadata['meta']) - except KeyError: - pass - meta = metadata['meta'] - return (metadata, meta, data) - - def processBlockMetadata(self, blockHash): - ''' - Read metadata from a block and cache it to the block database - ''' - myBlock = Block(blockHash, self._core) - if myBlock.isEncrypted: - myBlock.decrypt() - if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): - blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks - signer = self.bytesToStr(myBlock.signer) - valid = myBlock.verifySig() - if myBlock.getMetadata('newFSKey') is not None: - onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) - - try: - if len(blockType) <= 10: - self._core.updateBlockInfo(blockHash, 'dataType', blockType) - onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = None) - except TypeError: - logger.warn("Missing block information") - pass - # Set block expire time if specified - try: - expireTime = myBlock.getHeader('expire') - assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) - except (AssertionError, ValueError, TypeError) as e: - pass - else: - self._core.updateBlockInfo(blockHash, 'expire', expireTime) - else: - pass - #logger.debug('Not processing metadata on encrypted block we cannot decrypt.') - - def escapeAnsi(self, line): - ''' - Remove ANSI escape codes from a string with regex - - taken or adapted from: https://stackoverflow.com/a/38662876 - ''' - ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') - return ansi_escape.sub('', line) - - def hasBlock(self, hash): - ''' - Check for new block in the list - ''' - conn = sqlite3.connect(self._core.blockDB) - c = conn.cursor() - if not self.validateHash(hash): - raise Exception("Invalid hash") - for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): - if result[0] >= 1: - conn.commit() - conn.close() - return True - else: - conn.commit() - conn.close() - return False - - def hasKey(self, key): - ''' - Check for key in list of public keys - ''' - return key in self._core.listPeers() - - def validateHash(self, data, length=64): - ''' - Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) - ''' - retVal = True - if data == False or data == True: - return False - data = data.strip() - if len(data) != length: - retVal = False - else: - try: - int(data, 16) - except ValueError: - retVal = False - - return retVal - - def validateMetadata(self, metadata, blockData): - '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' - # TODO, make this check sane sizes - retData = False - maxClockDifference = 60 - - # convert to dict if it is json string - if type(metadata) is str: - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - pass - - # Validate metadata dict for invalid keys to sizes that are too large - maxAge = config.get("general.max_block_age", 2678400) - if type(metadata) is dict: - for i in metadata: - try: - self._core.requirements.blockMetadataLengths[i] - except KeyError: - logger.warn('Block has invalid metadata key ' + i) - break - else: - testData = metadata[i] - try: - testData = len(testData) - except (TypeError, AttributeError) as e: - testData = len(str(testData)) - if self._core.requirements.blockMetadataLengths[i] < testData: - logger.warn('Block metadata key ' + i + ' exceeded maximum size') - break - if i == 'time': - if not self.isIntegerString(metadata[i]): - logger.warn('Block metadata time stamp is not integer string or int') - break - isFuture = (metadata[i] - self.getEpoch()) - if isFuture > maxClockDifference: - logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) - break - if (self.getEpoch() - metadata[i]) > maxAge: - logger.warn('Block is outdated: %s' % (metadata[i],)) - break - elif i == 'expire': - try: - assert int(metadata[i]) > self.getEpoch() - except AssertionError: - logger.warn('Block is expired: %s greater than %s' % (metadata[i], self.getEpoch())) - break - elif i == 'encryptType': - try: - assert metadata[i] in ('asym', 'sym', '') - except AssertionError: - logger.warn('Invalid encryption mode') - break - else: - # if metadata loop gets no errors, it does not break, therefore metadata is valid - # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) - nonce = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(blockData)) - try: - with open(self._core.dataNonceFile, 'r') as nonceFile: - if nonce in nonceFile.read(): - retData = False # we've seen that nonce before, so we can't pass metadata - raise onionrexceptions.DataExists - except FileNotFoundError: - retData = True - except onionrexceptions.DataExists: - # do not set retData to True, because nonce has been seen before - pass - else: - retData = True - else: - logger.warn('In call to utils.validateMetadata, metadata must be JSON string or a dictionary object') - - return retData - - def validatePubKey(self, key): - ''' - Validate if a string is a valid base32 encoded Ed25519 key - ''' - retVal = False - if type(key) is type(None): - return False - try: - nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) - except nacl.exceptions.ValueError: - pass - except base64.binascii.Error as err: - pass - else: - retVal = True - return retVal - - def validateID(self, id): - ''' - Validate if an address is a valid tor or i2p hidden service - ''' - try: - idLength = len(id) - retVal = True - idNoDomain = '' - peerType = '' - # i2p b32 addresses are 60 characters long (including .b32.i2p) - if idLength == 60: - peerType = 'i2p' - if not id.endswith('.b32.i2p'): - retVal = False - else: - idNoDomain = id.split('.b32.i2p')[0] - # Onion v2's are 22 (including .onion), v3's are 62 with .onion - elif idLength == 22 or idLength == 62: - peerType = 'onion' - if not id.endswith('.onion'): - retVal = False - else: - idNoDomain = id.split('.onion')[0] - else: - retVal = False - if retVal: - if peerType == 'i2p': - try: - id.split('.b32.i2p')[2] - except: - pass - else: - retVal = False - elif peerType == 'onion': - try: - id.split('.onion')[2] - except: - pass - else: - retVal = False - if not idNoDomain.isalnum(): - retVal = False - - # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 - for x in idNoDomain.upper(): - if x not in string.ascii_uppercase and x not in '234567': - retVal = False - - return retVal - except: - return False - - def isIntegerString(self, data): - '''Check if a string is a valid base10 integer (also returns true if already an int)''' - try: - int(data) - except (ValueError, TypeError) as e: - return False - else: - return True - - def isCommunicatorRunning(self, timeout = 5, interval = 0.1): - try: - runcheck_file = self._core.dataDir + '.runcheck' - - if not os.path.isfile(runcheck_file): - open(runcheck_file, 'w+').close() - - # self._core.daemonQueueAdd('runCheck') # deprecated - starttime = time.time() - - while True: - time.sleep(interval) - - if not os.path.isfile(runcheck_file): - return True - elif time.time() - starttime >= timeout: - return False - except: - return False - - def importNewBlocks(self, scanDir=''): - ''' - This function is intended to scan for new blocks ON THE DISK and import them - ''' - blockList = self._core.getBlockList() - exist = False - if scanDir == '': - scanDir = self._core.blockDataLocation - if not scanDir.endswith('/'): - scanDir += '/' - for block in glob.glob(scanDir + "*.dat"): - if block.replace(scanDir, '').replace('.dat', '') not in blockList: - exist = True - logger.info('Found new block on dist %s' % block) - with open(block, 'rb') as newBlock: - block = block.replace(scanDir, '').replace('.dat', '') - if self._core._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): - self._core.addToBlockDB(block.replace('.dat', ''), dataSaved=True) - logger.info('Imported block %s.' % block) - self._core._utils.processBlockMetadata(block) - else: - logger.warn('Failed to verify hash for %s' % block) - if not exist: - print('No blocks found to import') - - def progressBar(self, value = 0, endvalue = 100, width = None): - ''' - Outputs a progress bar with a percentage. Write \n after use. - ''' - - if width is None or height is None: - width, height = shutil.get_terminal_size((80, 24)) - - bar_length = width - 6 - - percent = float(value) / endvalue - arrow = '─' * int(round(percent * bar_length)-1) + '>' - spaces = ' ' * (bar_length - len(arrow)) - - sys.stdout.write("\r┣{0}┫ {1}%".format(arrow + spaces, int(round(percent * 100)))) - sys.stdout.flush() - - def getEpoch(self): - '''returns epoch''' - return math.floor(time.time()) - - def doPostRequest(self, url, data={}, port=0, proxyType='tor'): - ''' - Do a POST request through a local tor or i2p instance - ''' - if proxyType == 'tor': - if port == 0: - port = self._core.torPort - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - elif proxyType == 'i2p': - proxies = {'http': 'http://127.0.0.1:4444'} - else: - return - headers = {'user-agent': 'PyOnionr', 'Connection':'close'} - try: - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) - retData = r.text - except KeyboardInterrupt: - raise KeyboardInterrupt - except requests.exceptions.RequestException as e: - logger.debug('Error: %s' % str(e)) - retData = False - return retData - - def doGetRequest(self, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False): - ''' - Do a get request through a local tor or i2p instance - ''' - retData = False - if proxyType == 'tor': - if port == 0: - raise onionrexceptions.MissingPort('Socks port required for Tor HTTP get request') - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - elif proxyType == 'i2p': - proxies = {'http': 'http://127.0.0.1:4444'} - else: - return - headers = {'user-agent': 'PyOnionr', 'Connection':'close'} - response_headers = dict() - try: - proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} - r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30), ) - # Check server is using same API version as us - if not ignoreAPI: - try: - response_headers = r.headers - if r.headers['X-API'] != str(API_VERSION): - raise onionrexceptions.InvalidAPIVersion - except KeyError: - raise onionrexceptions.InvalidAPIVersion - retData = r.text - except KeyboardInterrupt: - raise KeyboardInterrupt - except ValueError as e: - logger.debug('Failed to make GET request to %s' % url, error = e, sensitive = True) - except onionrexceptions.InvalidAPIVersion: - if 'X-API' in response_headers: - logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) - else: - logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION) - except requests.exceptions.RequestException as e: - if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): - logger.debug('Error: %s' % str(e)) - retData = False - if returnHeaders: - return (retData, response_headers) - else: - return retData - - def strToBytes(self, data): - try: - data = data.encode() - except AttributeError: - pass - return data - def bytesToStr(self, data): - try: - data = data.decode() - except AttributeError: - pass - return data - -def size(path='.'): - ''' - Returns the size of a folder's contents in bytes - ''' - total = 0 - if os.path.exists(path): - if os.path.isfile(path): - total = os.path.getsize(path) - else: - for entry in os.scandir(path): - if entry.is_file(): - total += entry.stat().st_size - elif entry.is_dir(): - total += size(entry.path) - return total - -def humanSize(num, suffix='B'): - ''' - Converts from bytes to a human readable format. - ''' - for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: - if abs(num) < 1024.0: - return "%.1f %s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.1f %s%s" % (num, 'Yi', suffix) \ No newline at end of file diff --git a/onionr/onionrutils/__init__.py b/onionr/onionrutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/onionr/onionrutils/basicrequests.py b/onionr/onionrutils/basicrequests.py new file mode 100644 index 00000000..3a6ec5c4 --- /dev/null +++ b/onionr/onionrutils/basicrequests.py @@ -0,0 +1,91 @@ +''' + Onionr - Private P2P Communication + + Do HTTP GET or POST requests through a proxy +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import requests, streamedrequests +import logger, onionrexceptions +def do_post_request(core_inst, url, data={}, port=0, proxyType='tor', max_size=10000): + ''' + Do a POST request through a local tor or i2p instance + ''' + if proxyType == 'tor': + if port == 0: + port = core_inst.torPort + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + elif proxyType == 'i2p': + proxies = {'http': 'http://127.0.0.1:4444'} + else: + return + headers = {'user-agent': 'PyOnionr', 'Connection':'close'} + try: + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + #r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) + r = streamedrequests.post(url, post_data=data, request_headers=headers, proxy=proxies, connect_timeout=15, stream_timeout=30, max_size=max_size, allow_redirects=False) + retData = r[1] + except KeyboardInterrupt: + raise KeyboardInterrupt + except requests.exceptions.RequestException as e: + logger.debug('Error: %s' % str(e)) + retData = False + return retData + +def do_get_request(core_inst, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False, max_size=5242880): + ''' + Do a get request through a local tor or i2p instance + ''' + API_VERSION = core_inst.onionrInst.API_VERSION + retData = False + if proxyType == 'tor': + if port == 0: + raise onionrexceptions.MissingPort('Socks port required for Tor HTTP get request') + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + elif proxyType == 'i2p': + proxies = {'http': 'http://127.0.0.1:4444'} + else: + return + headers = {'user-agent': 'PyOnionr', 'Connection':'close'} + response_headers = dict() + try: + proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} + r = streamedrequests.get(url, request_headers=headers, allow_redirects=False, proxy=proxies, connect_timeout=15, stream_timeout=120, max_size=max_size) + # Check server is using same API version as us + if not ignoreAPI: + try: + response_headers = r[0].headers + if r[0].headers['X-API'] != str(API_VERSION): + raise onionrexceptions.InvalidAPIVersion + except KeyError: + raise onionrexceptions.InvalidAPIVersion + retData = r[1] + except KeyboardInterrupt: + raise KeyboardInterrupt + except ValueError as e: + pass + except onionrexceptions.InvalidAPIVersion: + if 'X-API' in response_headers: + logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) + else: + logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION) + except requests.exceptions.RequestException as e: + if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): + logger.debug('Error: %s' % str(e)) + retData = False + if returnHeaders: + return (retData, response_headers) + else: + return retData \ No newline at end of file diff --git a/onionr/onionrutils/blockmetadata.py b/onionr/onionrutils/blockmetadata.py new file mode 100644 index 00000000..7b789edf --- /dev/null +++ b/onionr/onionrutils/blockmetadata.py @@ -0,0 +1,108 @@ +''' + Onionr - Private P2P Communication + + Module to fetch block metadata from raw block data and process it +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import json, sqlite3 +import logger, onionrevents +from onionrusers import onionrusers +from etc import onionrvalues +import onionrblockapi +from . import epoch, stringvalidators, bytesconverter +def get_block_metadata_from_data(blockData): + ''' + accepts block contents as string, returns a tuple of + metadata, meta (meta being internal metadata, which will be + returned as an encrypted base64 string if it is encrypted, dict if not). + ''' + meta = {} + metadata = {} + data = blockData + try: + blockData = blockData.encode() + except AttributeError: + pass + + try: + metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) + except json.decoder.JSONDecodeError: + pass + else: + data = blockData[blockData.find(b'\n'):].decode() + + if not metadata['encryptType'] in ('asym', 'sym'): + try: + meta = json.loads(metadata['meta']) + except KeyError: + pass + meta = metadata['meta'] + return (metadata, meta, data) + +def process_block_metadata(core_inst, blockHash): + ''' + Read metadata from a block and cache it to the block database + ''' + curTime = epoch.get_rounded_epoch(roundS=60) + myBlock = onionrblockapi.Block(blockHash, core_inst) + if myBlock.isEncrypted: + myBlock.decrypt() + if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted): + blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks + + signer = bytesconverter.bytes_to_str(myBlock.signer) + valid = myBlock.verifySig() + if myBlock.getMetadata('newFSKey') is not None: + onionrusers.OnionrUser(core_inst, signer).addForwardKey(myBlock.getMetadata('newFSKey')) + + try: + if len(blockType) <= 10: + core_inst.updateBlockInfo(blockHash, 'dataType', blockType) + except TypeError: + logger.warn("Missing block information") + pass + # Set block expire time if specified + try: + expireTime = myBlock.getHeader('expire') + assert len(str(int(expireTime))) < 20 # test that expire time is an integer of sane length (for epoch) + except (AssertionError, ValueError, TypeError) as e: + expireTime = onionrvalues.OnionrValues().default_expire + curTime + finally: + core_inst.updateBlockInfo(blockHash, 'expire', expireTime) + if not blockType is None: + core_inst.updateBlockInfo(blockHash, 'dataType', blockType) + onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid}, onionr = core_inst.onionrInst) + else: + pass + +def has_block(core_inst, hash): + ''' + Check for new block in the list + ''' + conn = sqlite3.connect(core_inst.blockDB) + c = conn.cursor() + if not stringvalidators.validate_hash(hash): + raise Exception("Invalid hash") + for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): + if result[0] >= 1: + conn.commit() + conn.close() + return True + else: + conn.commit() + conn.close() + return False + return False \ No newline at end of file diff --git a/onionr/onionrutils/bytesconverter.py b/onionr/onionrutils/bytesconverter.py new file mode 100644 index 00000000..5df4d673 --- /dev/null +++ b/onionr/onionrutils/bytesconverter.py @@ -0,0 +1,14 @@ +def str_to_bytes(data): + '''Converts a string to bytes with .encode()''' + try: + data = data.encode('UTF-8') + except AttributeError: + pass + return data + +def bytes_to_str(data): + try: + data = data.decode('UTF-8') + except AttributeError: + pass + return data \ No newline at end of file diff --git a/onionr/onionrutils/checkcommunicator.py b/onionr/onionrutils/checkcommunicator.py new file mode 100644 index 00000000..cfc2c31a --- /dev/null +++ b/onionr/onionrutils/checkcommunicator.py @@ -0,0 +1,38 @@ +''' + Onionr - Private P2P Communication + + Check if the communicator is running +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import time, os +def is_communicator_running(core_inst, timeout = 5, interval = 0.1): + try: + runcheck_file = core_inst.dataDir + '.runcheck' + + if not os.path.isfile(runcheck_file): + open(runcheck_file, 'w+').close() + + starttime = time.time() + + while True: + time.sleep(interval) + + if not os.path.isfile(runcheck_file): + return True + elif time.time() - starttime >= timeout: + return False + except: + return False \ No newline at end of file diff --git a/onionr/onionrutils/epoch.py b/onionr/onionrutils/epoch.py new file mode 100644 index 00000000..01922a4c --- /dev/null +++ b/onionr/onionrutils/epoch.py @@ -0,0 +1,30 @@ +''' + Onionr - Private P2P Communication + + Get floored epoch, or rounded epoch +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import math, time +def get_rounded_epoch(roundS=60): + ''' + Returns the epoch, rounded down to given seconds (Default 60) + ''' + epoch = get_epoch() + return epoch - (epoch % roundS) + +def get_epoch(): + '''returns epoch''' + return math.floor(time.time()) \ No newline at end of file diff --git a/onionr/onionrutils/escapeansi.py b/onionr/onionrutils/escapeansi.py new file mode 100644 index 00000000..1f88e149 --- /dev/null +++ b/onionr/onionrutils/escapeansi.py @@ -0,0 +1,10 @@ +import re +def escape_ANSI(line): + ''' + Remove ANSI escape codes from a string with regex + + adapted from: https://stackoverflow.com/a/38662876 by user https://stackoverflow.com/users/802365/%c3%89douard-lopez + cc-by-sa-3 license https://creativecommons.org/licenses/by-sa/3.0/ + ''' + ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') + return ansi_escape.sub('', line) \ No newline at end of file diff --git a/onionr/onionrutils/getclientapiserver.py b/onionr/onionrutils/getclientapiserver.py new file mode 100644 index 00000000..e0afe61b --- /dev/null +++ b/onionr/onionrutils/getclientapiserver.py @@ -0,0 +1,29 @@ +''' + Onionr - Private P2P Communication + + Return the client api server address and port, which is usually random +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def get_client_API_server(core_inst): + retData = '' + try: + with open(core_inst.privateApiHostFile, 'r') as host: + hostname = host.read() + except FileNotFoundError: + raise FileNotFoundError + else: + retData += '%s:%s' % (hostname, core_inst.config.get('client.client.port')) + return retData \ No newline at end of file diff --git a/onionr/onionrutils/importnewblocks.py b/onionr/onionrutils/importnewblocks.py new file mode 100644 index 00000000..e1793adf --- /dev/null +++ b/onionr/onionrutils/importnewblocks.py @@ -0,0 +1,48 @@ +''' + Onionr - Private P2P Communication + + import new blocks from disk, providing transport agnosticism +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import glob +import logger, core +from onionrutils import blockmetadata +def import_new_blocks(core_inst=None, scanDir=''): + ''' + This function is intended to scan for new blocks ON THE DISK and import them + ''' + if core_inst is None: + core_inst = core.Core() + blockList = core_inst.getBlockList() + exist = False + if scanDir == '': + scanDir = core_inst.blockDataLocation + if not scanDir.endswith('/'): + scanDir += '/' + for block in glob.glob(scanDir + "*.dat"): + if block.replace(scanDir, '').replace('.dat', '') not in blockList: + exist = True + logger.info('Found new block on dist %s' % block, terminal=True) + with open(block, 'rb') as newBlock: + block = block.replace(scanDir, '').replace('.dat', '') + if core_inst._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''): + core_inst.addToBlockDB(block.replace('.dat', ''), dataSaved=True) + logger.info('Imported block %s.' % block, terminal=True) + blockmetadata.process_block_metadata(core_inst, block) + else: + logger.warn('Failed to verify hash for %s' % block, terminal=True) + if not exist: + logger.info('No blocks found to import', terminal=True) \ No newline at end of file diff --git a/onionr/onionrutils/localcommand.py b/onionr/onionrutils/localcommand.py new file mode 100644 index 00000000..13495a3c --- /dev/null +++ b/onionr/onionrutils/localcommand.py @@ -0,0 +1,51 @@ +''' + Onionr - Private P2P Communication + + send a command to the local API server +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import urllib, requests, time +import logger +from onionrutils import getclientapiserver +def local_command(core_inst, command, data='', silent = True, post=False, postData = {}, maxWait=20): + ''' + Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. + ''' + # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. + hostname = '' + waited = 0 + while hostname == '': + try: + hostname = getclientapiserver.get_client_API_server(core_inst) + except FileNotFoundError: + time.sleep(1) + waited += 1 + if waited == maxWait: + return False + if data != '': + data = '&data=' + urllib.parse.quote_plus(data) + payload = 'http://%s/%s%s' % (hostname, command, data) + try: + if post: + retData = requests.post(payload, data=postData, headers={'token': core_inst.config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + else: + retData = requests.get(payload, headers={'token': core_inst.config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text + except Exception as error: + if not silent: + logger.error('Failed to make local request (command: %s):%s' % (command, error), terminal=True) + retData = False + + return retData \ No newline at end of file diff --git a/onionr/onionrutils/mnemonickeys.py b/onionr/onionrutils/mnemonickeys.py new file mode 100644 index 00000000..56085536 --- /dev/null +++ b/onionr/onionrutils/mnemonickeys.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + convert a base32 string (intended for ed25519 user ids) to pgp word list +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64 +from etc import pgpwords +def get_human_readable_ID(core_inst, pub=''): + '''gets a human readable ID from a public key''' + if pub == '': + pub = core_inst._crypto.pubKey + pub = base64.b16encode(base64.b32decode(pub)).decode() + return ' '.join(pgpwords.wordify(pub)) diff --git a/onionr/onionrutils/stringvalidators.py b/onionr/onionrutils/stringvalidators.py new file mode 100644 index 00000000..951c25ed --- /dev/null +++ b/onionr/onionrutils/stringvalidators.py @@ -0,0 +1,119 @@ +''' + Onionr - Private P2P Communication + + validate various string data types +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import base64, string +import unpaddedbase32, nacl.signing, nacl.encoding +from onionrutils import bytesconverter +def validate_hash(data, length=64): + ''' + Validate if a string is a valid hash hex digest (does not compare, just checks length and charset) + + Length is only invalid if its *more* than the specified + ''' + retVal = True + if data == False or data == True: + return False + data = data.strip() + if len(data) > length: + retVal = False + else: + try: + int(data, 16) + except ValueError: + retVal = False + + return retVal + +def validate_pub_key(key): + ''' + Validate if a string is a valid base32 encoded Ed25519 key + ''' + if type(key) is type(None): + return False + # Accept keys that have no = padding + key = unpaddedbase32.repad(bytesconverter.str_to_bytes(key)) + + retVal = False + try: + nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) + except nacl.exceptions.ValueError: + pass + except base64.binascii.Error as err: + pass + else: + retVal = True + return retVal + +def validate_transport(id): + try: + idLength = len(id) + retVal = True + idNoDomain = '' + peerType = '' + # i2p b32 addresses are 60 characters long (including .b32.i2p) + if idLength == 60: + peerType = 'i2p' + if not id.endswith('.b32.i2p'): + retVal = False + else: + idNoDomain = id.split('.b32.i2p')[0] + # Onion v2's are 22 (including .onion), v3's are 62 with .onion + elif idLength == 22 or idLength == 62: + peerType = 'onion' + if not id.endswith('.onion'): + retVal = False + else: + idNoDomain = id.split('.onion')[0] + else: + retVal = False + if retVal: + if peerType == 'i2p': + try: + id.split('.b32.i2p')[2] + except: + pass + else: + retVal = False + elif peerType == 'onion': + try: + id.split('.onion')[2] + except: + pass + else: + retVal = False + if not idNoDomain.isalnum(): + retVal = False + + # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 + for x in idNoDomain.upper(): + if x not in string.ascii_uppercase and x not in '234567': + retVal = False + + return retVal + except Exception as e: + return False + +def is_integer_string(data): + '''Check if a string is a valid base10 integer (also returns true if already an int)''' + try: + int(data) + except (ValueError, TypeError) as e: + return False + else: + return True diff --git a/onionr/onionrutils/validatemetadata.py b/onionr/onionrutils/validatemetadata.py new file mode 100644 index 00000000..2800c24a --- /dev/null +++ b/onionr/onionrutils/validatemetadata.py @@ -0,0 +1,97 @@ +''' + Onionr - Private P2P Communication + + validate new block's metadata +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import json +import logger, onionrexceptions +from etc import onionrvalues +from onionrutils import stringvalidators, epoch, bytesconverter +def validate_metadata(core_inst, metadata, blockData): + '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string''' + # TODO, make this check sane sizes + retData = False + maxClockDifference = 120 + + # convert to dict if it is json string + if type(metadata) is str: + try: + metadata = json.loads(metadata) + except json.JSONDecodeError: + pass + + # Validate metadata dict for invalid keys to sizes that are too large + maxAge = core_inst.config.get("general.max_block_age", onionrvalues.OnionrValues().default_expire) + if type(metadata) is dict: + for i in metadata: + try: + core_inst.requirements.blockMetadataLengths[i] + except KeyError: + logger.warn('Block has invalid metadata key ' + i) + break + else: + testData = metadata[i] + try: + testData = len(testData) + except (TypeError, AttributeError) as e: + testData = len(str(testData)) + if core_inst.requirements.blockMetadataLengths[i] < testData: + logger.warn('Block metadata key ' + i + ' exceeded maximum size') + break + if i == 'time': + if not stringvalidators.is_integer_string(metadata[i]): + logger.warn('Block metadata time stamp is not integer string or int') + break + isFuture = (metadata[i] - epoch.get_epoch()) + if isFuture > maxClockDifference: + logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture)) + break + if (epoch.get_epoch() - metadata[i]) > maxAge: + logger.warn('Block is outdated: %s' % (metadata[i],)) + break + elif i == 'expire': + try: + assert int(metadata[i]) > epoch.get_epoch() + except AssertionError: + logger.warn('Block is expired: %s less than %s' % (metadata[i], epoch.get_epoch())) + break + elif i == 'encryptType': + try: + assert metadata[i] in ('asym', 'sym', '') + except AssertionError: + logger.warn('Invalid encryption mode') + break + else: + # if metadata loop gets no errors, it does not break, therefore metadata is valid + # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) + nonce = bytesconverter.bytes_to_str(core_inst._crypto.sha3Hash(blockData)) + try: + with open(core_inst.dataNonceFile, 'r') as nonceFile: + if nonce in nonceFile.read(): + retData = False # we've seen that nonce before, so we can't pass metadata + raise onionrexceptions.DataExists + except FileNotFoundError: + retData = True + except onionrexceptions.DataExists: + # do not set retData to True, because nonce has been seen before + pass + else: + retData = True + else: + logger.warn('In call to utils.validateMetadata, metadata must be JSON string or a dictionary object') + + return retData \ No newline at end of file diff --git a/onionr/serializeddata.py b/onionr/serializeddata.py old mode 100644 new mode 100755 index 1587e74e..ef58c304 --- a/onionr/serializeddata.py +++ b/onionr/serializeddata.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This module serializes various data pieces for use in other modules, in particular the web api ''' @@ -18,7 +18,7 @@ along with this program. If not, see . ''' -import core, api, uuid, json +import core, json class SerializedData: def __init__(self, coreInst): diff --git a/onionr/setupconfig.py b/onionr/setupconfig.py old mode 100644 new mode 100755 index 1229faeb..3919ecff --- a/onionr/setupconfig.py +++ b/onionr/setupconfig.py @@ -1,68 +1,88 @@ +''' + Onionr - Private P2P Communication + + Initialize Onionr configuration +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' import os, json import config, logger +from logger.settings import * def setup_config(dataDir, o_inst = None): data_exists = os.path.exists(dataDir) - if not data_exists: os.mkdir(dataDir) + config.reload() + + if not os.path.exists(config._configfile): + if os.path.exists('static-data/default_config.json'): + # this is the default config, it will be overwritten if a config file already exists. Else, it saves it + with open('static-data/default_config.json', 'r') as configReadIn: + config.set_config(json.loads(configReadIn.read())) + else: + # the default config file doesn't exist, try hardcoded config + logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!') + config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) - if os.path.exists('static-data/default_config.json'): - # this is the default config, it will be overwritten if a config file already exists. Else, it saves it - with open('static-data/default_config.json', 'r') as configReadIn: - config.set_config(json.loads(configReadIn.read())) - else: - # the default config file doesn't exist, try hardcoded config - logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!') - config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) - if not data_exists: config.save() - config.reload() # this will read the configuration file into memory settings = 0b000 if config.get('log.console.color', True): - settings = settings | logger.USE_ANSI + settings = settings | USE_ANSI if config.get('log.console.output', True): - settings = settings | logger.OUTPUT_TO_CONSOLE + settings = settings | OUTPUT_TO_CONSOLE if config.get('log.file.output', True): - settings = settings | logger.OUTPUT_TO_FILE - logger.set_settings(settings) + settings = settings | OUTPUT_TO_FILE + set_settings(settings) if not o_inst is None: if str(config.get('general.dev_mode', True)).lower() == 'true': o_inst._developmentMode = True - logger.set_level(logger.LEVEL_DEBUG) + set_level(LEVEL_DEBUG) else: o_inst._developmentMode = False - logger.set_level(logger.LEVEL_INFO) + set_level(LEVEL_INFO) verbosity = str(config.get('log.verbosity', 'default')).lower().strip() if not verbosity in ['default', 'null', 'none', 'nil']: map = { - str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG, - 'verbose' : logger.LEVEL_DEBUG, - 'debug' : logger.LEVEL_DEBUG, - str(logger.LEVEL_INFO) : logger.LEVEL_INFO, - 'info' : logger.LEVEL_INFO, - 'information' : logger.LEVEL_INFO, - str(logger.LEVEL_WARN) : logger.LEVEL_WARN, - 'warn' : logger.LEVEL_WARN, - 'warning' : logger.LEVEL_WARN, - 'warnings' : logger.LEVEL_WARN, - str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR, - 'err' : logger.LEVEL_ERROR, - 'error' : logger.LEVEL_ERROR, - 'errors' : logger.LEVEL_ERROR, - str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL, - 'fatal' : logger.LEVEL_FATAL, - str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT, - 'silent' : logger.LEVEL_IMPORTANT, - 'quiet' : logger.LEVEL_IMPORTANT, - 'important' : logger.LEVEL_IMPORTANT + str(LEVEL_DEBUG) : LEVEL_DEBUG, + 'verbose' : LEVEL_DEBUG, + 'debug' : LEVEL_DEBUG, + str(LEVEL_INFO) : LEVEL_INFO, + 'info' : LEVEL_INFO, + 'information' : LEVEL_INFO, + str(LEVEL_WARN) : LEVEL_WARN, + 'warn' : LEVEL_WARN, + 'warning' : LEVEL_WARN, + 'warnings' : LEVEL_WARN, + str(LEVEL_ERROR) : LEVEL_ERROR, + 'err' : LEVEL_ERROR, + 'error' : LEVEL_ERROR, + 'errors' : LEVEL_ERROR, + str(LEVEL_FATAL) : LEVEL_FATAL, + 'fatal' : LEVEL_FATAL, + str(LEVEL_IMPORTANT) : LEVEL_IMPORTANT, + 'silent' : LEVEL_IMPORTANT, + 'quiet' : LEVEL_IMPORTANT, + 'important' : LEVEL_IMPORTANT } if verbosity in map: - logger.set_level(map[verbosity]) + set_level(map[verbosity]) else: logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity) diff --git a/onionr/static-data/README.md b/onionr/static-data/README.md new file mode 100755 index 00000000..90b2156a --- /dev/null +++ b/onionr/static-data/README.md @@ -0,0 +1,19 @@ +# static-data + +This folder contains whatever static files are needed by Onionr and default plugins. + +default-plugins/: default plugin program files to be installed on first Onionr run. + +www/: onionr web ui static files including those of default plugins + +bootstrap-nodes.txt: comma separated list of bootstrap nodes + +connect-check.txt: non-onionr .onion sites to check Tor connectivity with + +default_config.json: default configuration values for Onionr nodes + +default_plugin.py: template for people to make new plugins + +header.txt: Onionr ASCII art logo + +index.html: Notice shown to people who try to visit Onionr nodes public interfaces in their browser \ No newline at end of file diff --git a/onionr/static-data/bootstrap-nodes.txt b/onionr/static-data/bootstrap-nodes.txt index e69de29b..da45b083 100755 --- a/onionr/static-data/bootstrap-nodes.txt +++ b/onionr/static-data/bootstrap-nodes.txt @@ -0,0 +1 @@ +6i2apk7llawfcqy4y4xumqiedo7nltczmdk2cbvdztaqpkjnygwwz6qd.onion diff --git a/onionr/static-data/connect-check.txt b/onionr/static-data/connect-check.txt index 7be85aa0..c776e53e 100755 --- a/onionr/static-data/connect-check.txt +++ b/onionr/static-data/connect-check.txt @@ -1 +1 @@ -https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt +https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt,http://cockmailwwfvrtqj.onion/robots.txt \ No newline at end of file diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index e1398a13..c65917c8 100755 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This is an interactive menu-driven CLI interface for Onionr ''' @@ -23,6 +23,7 @@ import threading, time, uuid, subprocess, sys import config, logger from onionrblockapi import Block import onionrplugins +from onionrutils import localcommand plugin_name = 'cliui' PLUGIN_VERSION = '0.0.1' @@ -39,8 +40,6 @@ class OnionrCLIUI: def subCommand(self, command, args=None): try: - #subprocess.run(["./onionr.py", command]) - #subprocess.Popen(['./onionr.py', command], stdin=subprocess.STD, stdout=subprocess.STDOUT, stderr=subprocess.STDOUT) if args != None: subprocess.call(['./onionr.py', command, args]) else: @@ -50,7 +49,7 @@ class OnionrCLIUI: def isRunning(self): while not self.shutdown: - if self.myCore._utils.localCommand('ping', maxWait=5) == 'pong!': + if localcommand.local_command(self.myCore, 'ping', maxWait=5) == 'pong!': self.running = 'Yes' else: self.running = 'No' @@ -89,15 +88,20 @@ class OnionrCLIUI: else: print('Plugin not enabled') elif choice in ("3", "file sharing", "file"): - filename = input("Enter full path to file: ").strip() - self.subCommand("addfile", filename) + try: + filename = input("Enter full path to file: ").strip() + except (EOFError, KeyboardInterrupt) as e: + pass + else: + if len(filename.strip()) > 0: + self.subCommand("addfile", filename) elif choice in ("4", "quit"): showMenu = False self.shutdown = True elif choice == "": pass else: - logger.error("Invalid choice") + logger.error("Invalid choice", terminal=True) return def on_init(api, data = None): diff --git a/onionr/static-data/default-plugins/contactmanager/main.py b/onionr/static-data/default-plugins/contactmanager/main.py index bdf6ac5b..d6c4499c 100755 --- a/onionr/static-data/default-plugins/contactmanager/main.py +++ b/onionr/static-data/default-plugins/contactmanager/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This is an interactive menu-driven CLI interface for Onionr ''' diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index f917d72a..5e5590ad 100755 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This default plugin allows users to encrypt/decrypt messages without using blocks ''' @@ -21,9 +21,11 @@ # Imports some useful libraries import logger, config, threading, time, datetime, sys, json from onionrblockapi import Block +from onionrutils import stringvalidators import onionrexceptions, onionrusers import locale locale.setlocale(locale.LC_ALL, '') +plugin_name = 'encrypt' class PlainEncryption: def __init__(self, api): @@ -42,16 +44,16 @@ class PlainEncryption: pass try: - if not self.api.get_core()._utils.validatePubKey(sys.argv[2]): + if not stringvalidators.validate_pub_key(sys.argv[2]): raise onionrexceptions.InvalidPubkey except (ValueError, IndexError) as e: - logger.error("Peer public key not specified") + logger.error("Peer public key not specified", terminal=True) except onionrexceptions.InvalidPubkey: - logger.error("Invalid public key") + logger.error("Invalid public key", terminal=True) else: pubkey = sys.argv[2] # Encrypt if public key is valid - logger.info("Please enter your message (ctrl-d or -q to stop):") + logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True) try: for line in sys.stdin: if line == '-q\n': @@ -71,12 +73,12 @@ class PlainEncryption: plaintext = data encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, encodedData=True) encrypted = self.api.get_core()._utils.bytesToStr(encrypted) - logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) + logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,), terminal=True) def decrypt(self): plaintext = "" data = "" - logger.info("Please enter your message (ctrl-d or -q to stop):") + logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True) try: for line in sys.stdin: if line == '-q\n': @@ -90,17 +92,17 @@ class PlainEncryption: myPub = self.api.get_core()._crypto.pubKey decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, encodedData=True) if decrypted == False: - logger.error("Decryption failed") + logger.error("Decryption failed", terminal=True) else: data = json.loads(decrypted) - logger.info('Decrypted Message: \n\n%s' % data['data']) + logger.info('Decrypted Message: \n\n%s' % data['data'], terminal=True) try: - logger.info("Signing public key: %s" % (data['signer'],)) + logger.info("Signing public key: %s" % (data['signer'],), terminal=True) assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False except (AssertionError, KeyError) as e: - logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE") + logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE", terminal=True) else: - logger.info("Message has good signature.") + logger.info("Message has good signature.", terminal=True) return def on_init(api, data = None): diff --git a/onionr/static-data/default-plugins/esoteric/controlapi.py b/onionr/static-data/default-plugins/esoteric/controlapi.py new file mode 100755 index 00000000..7c298063 --- /dev/null +++ b/onionr/static-data/default-plugins/esoteric/controlapi.py @@ -0,0 +1,71 @@ +''' + Onionr - Private P2P Communication + + HTTP endpoints for controlling IMs +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import json +from flask import Response, request, redirect, Blueprint, send_from_directory +import core + +core_inst = core.Core() +flask_blueprint = Blueprint('esoteric_control', __name__) + +@flask_blueprint.route('/esoteric/ping') +def ping(): + return 'pong!' + +@flask_blueprint.route('/esoteric/send/', methods=['POST']) +def send_message(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('s' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('s' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/esoteric/gets/') +def get_sent(peer): + sent = core_inst.keyStore.get('s' + peer) + if sent is None: + sent = [] + return Response(json.dumps(sent)) + +@flask_blueprint.route('/esoteric/addrec/', methods=['POST']) +def add_rec(peer): + data = request.get_json(force=True) + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + existing.append(data) + core_inst.keyStore.put('r' + peer, existing) + core_inst.keyStore.flush() + return Response('success') + +@flask_blueprint.route('/esoteric/getrec/') +def get_messages(peer): + core_inst.keyStore.refresh() + existing = core_inst.keyStore.get('r' + peer) + if existing is None: + existing = [] + else: + existing = list(existing) + core_inst.keyStore.delete('r' + peer) + return Response(json.dumps(existing)) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/esoteric/info.json b/onionr/static-data/default-plugins/esoteric/info.json new file mode 100755 index 00000000..6fcc6e0e --- /dev/null +++ b/onionr/static-data/default-plugins/esoteric/info.json @@ -0,0 +1,5 @@ +{ + "name" : "esoteric", + "version" : "1.0", + "author" : "onionr" +} diff --git a/onionr/static-data/default-plugins/esoteric/main.py b/onionr/static-data/default-plugins/esoteric/main.py new file mode 100755 index 00000000..495129c0 --- /dev/null +++ b/onionr/static-data/default-plugins/esoteric/main.py @@ -0,0 +1,95 @@ +''' + Onionr - Private P2P Communication + + Instant message conversations with Onionr peers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +# Imports some useful libraries +import locale, sys, os, threading, json +locale.setlocale(locale.LC_ALL, '') +import onionrservices, logger +from onionrservices import bootstrapservice +from onionrutils import stringvalidators, epoch, basicrequests + +plugin_name = 'esoteric' +PLUGIN_VERSION = '0.0.0' +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +import controlapi, peerserver +flask_blueprint = controlapi.flask_blueprint +direct_blueprint = peerserver.direct_blueprint + +def exit_with_error(text=''): + if text != '': + logger.error(text) + sys.exit(1) + +class Esoteric: + def __init__(self, pluginapi): + self.myCore = pluginapi.get_core() + self.peer = None + self.transport = None + self.shutdown = False + + def _sender_loop(self): + print('Enter a message to send, with ctrl-d or -s on a new line.') + print('-c on a new line or ctrl-c stops') + message = '' + while not self.shutdown: + try: + message += input() + if message == '-s': + raise EOFError + elif message == '-c': + raise KeyboardInterrupt + else: + message += '\n' + except EOFError: + message = json.dumps({'m': message, 't': epoch.get_epoch()}) + print(basicrequests.do_post_request(self.myCore, 'http://%s/esoteric/sendto' % (self.transport,), port=self.socks, data=message)) + message = '' + except KeyboardInterrupt: + self.shutdown = True + + def create(self): + try: + peer = sys.argv[2] + if not stringvalidators.validate_pub_key(peer): + exit_with_error('Invalid public key specified') + except IndexError: + exit_with_error('You must specify a peer public key') + self.peer = peer + # Ask peer for transport address by creating block for them + peer_transport_address = bootstrapservice.bootstrap_client_service(peer, self.myCore) + self.transport = peer_transport_address + self.socks = self.myCore.config.get('tor.socksport') + + print('connected with', peer, 'on', peer_transport_address) + if basicrequests.do_get_request(self.myCore, 'http://%s/ping' % (peer_transport_address,), ignoreAPI=True, port=self.socks) == 'pong!': + print('connected', peer_transport_address) + threading.Thread(target=self._sender_loop).start() + +def on_init(api, data = None): + ''' + This event is called after Onionr is initialized, but before the command + inputted is executed. Could be called when daemon is starting or when + just the client is running. + ''' + + pluginapi = api + chat = Esoteric(pluginapi) + api.commands.register(['esoteric'], chat.create) + return diff --git a/onionr/static-data/default-plugins/esoteric/peerserver.py b/onionr/static-data/default-plugins/esoteric/peerserver.py new file mode 100755 index 00000000..f48ba0be --- /dev/null +++ b/onionr/static-data/default-plugins/esoteric/peerserver.py @@ -0,0 +1,56 @@ +''' + Onionr - Private P2P Communication + + HTTP endpoints for communicating with peers +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import sys, os, json +import core +from onionrutils import localcommand +from flask import Response, request, redirect, Blueprint, abort, g +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +direct_blueprint = Blueprint('esoteric', __name__) +core_inst = core.Core() + +storage_dir = core_inst.dataDir + +@direct_blueprint.before_request +def request_setup(): + core_inst.keyStore.refresh() + host = request.host + host = host.strip('.b32.i2p') + host = host.strip('.onion') + g.host = host + g.peer = core_inst.keyStore.get('dc-' + g.host) + +@direct_blueprint.route('/esoteric/ping') +def pingdirect(): + return 'pong!' + +@direct_blueprint.route('/esoteric/sendto', methods=['POST', 'GET']) +def sendto(): + try: + msg = request.get_json(force=True) + except: + msg = '' + else: + msg = json.dumps(msg) + localcommand.local_command(core_inst, '/esoteric/addrec/%s' % (g.peer,), post=True, postData=msg) + return Response('success') + +@direct_blueprint.route('/esoteric/poll') +def poll_chat(): + return Response(localcommand.local_command(core_inst, '/esoteric/gets/%s' % (g.peer,))) \ No newline at end of file diff --git a/onionr/static-data/default-plugins/flow/flowapi.py b/onionr/static-data/default-plugins/flow/flowapi.py new file mode 100755 index 00000000..09b54629 --- /dev/null +++ b/onionr/static-data/default-plugins/flow/flowapi.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + This file primarily serves to allow specific fetching of flow board messages +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +from flask import Response, request, redirect, Blueprint, abort + +flask_blueprint = Blueprint('flow', __name__) + +@flask_blueprint.route('/flow/getpostsbyboard/') +def get_post_by_board(board): + return Response('WIP') \ No newline at end of file diff --git a/onionr/static-data/default-plugins/flow/main.py b/onionr/static-data/default-plugins/flow/main.py index 2587df93..de0c60e3 100755 --- a/onionr/static-data/default-plugins/flow/main.py +++ b/onionr/static-data/default-plugins/flow/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This default plugin handles "flow" messages (global chatroom style communication) ''' @@ -19,10 +19,18 @@ ''' # Imports some useful libraries -import logger, config, threading, time +import threading, time, locale, sys, os from onionrblockapi import Block +import logger, config +from onionrutils import escapeansi, epoch +locale.setlocale(locale.LC_ALL, '') + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +import flowapi # import after path insert +flask_blueprint = flowapi.flask_blueprint plugin_name = 'flow' +PLUGIN_VERSION = '0.0.1' class OnionrFlow: def __init__(self): @@ -33,10 +41,10 @@ class OnionrFlow: return def start(self): - logger.warn("Please note: everything said here is public, even if a random channel name is used.") + logger.warn("Please note: everything said here is public, even if a random channel name is used.", terminal=True) message = "" self.flowRunning = True - newThread = threading.Thread(target=self.showOutput) + newThread = threading.Thread(target=self.showOutput, daemon=True) newThread.start() try: self.channel = logger.readline("Enter a channel name or none for default:") @@ -52,11 +60,12 @@ class OnionrFlow: else: if message == "q": self.flowRunning = False - expireTime = self.myCore._utils.getEpoch() + 43200 + expireTime = epoch.get_epoch() + 43200 if len(message) > 0: + logger.info('Inserting message as block...', terminal=True) self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel}) - logger.info("Flow is exiting, goodbye") + logger.info("Flow is exiting, goodbye", terminal=True) return def showOutput(self): @@ -64,23 +73,21 @@ class OnionrFlow: time.sleep(1) try: while self.flowRunning: - for block in self.myCore.getBlocksByType('txt'): - block = Block(block) - if block.getMetadata('ch') != self.channel: - #print('not chan', block.getMetadata('ch')) - continue - if block.getHash() in self.alreadyOutputed: - #print('already') - continue - if not self.flowRunning: - break - logger.info('\n------------------------', prompt = False) - content = block.getContent() - # Escape new lines, remove trailing whitespace, and escape ansi sequences - content = self.myCore._utils.escapeAnsi(content.replace('\n', '\\n').replace('\r', '\\r').strip()) - logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False) - self.alreadyOutputed.append(block.getHash()) - time.sleep(5) + for block in self.myCore.getBlocksByType('txt'): + block = Block(block) + if block.getMetadata('ch') != self.channel: + continue + if block.getHash() in self.alreadyOutputed: + continue + if not self.flowRunning: + break + logger.info('\n------------------------', prompt = False, terminal=True) + content = block.getContent() + # Escape new lines, remove trailing whitespace, and escape ansi sequences + content = escapeansi.escape_ANSI(content.replace('\n', '\\n').replace('\r', '\\r').strip()) + logger.info(block.getDate().strftime("%m/%d %H:%M") + ' - ' + logger.colors.reset + content, prompt = False, terminal=True) + self.alreadyOutputed.append(block.getHash()) + time.sleep(5) except KeyboardInterrupt: self.flowRunning = False diff --git a/onionr/static-data/default-plugins/metadataprocessor/main.py b/onionr/static-data/default-plugins/metadataprocessor/main.py index e7277c7d..4c730783 100755 --- a/onionr/static-data/default-plugins/metadataprocessor/main.py +++ b/onionr/static-data/default-plugins/metadataprocessor/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This processes metadata for Onionr blocks ''' @@ -23,6 +23,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block import onionrusers, onionrexceptions +from onionrutils import stringvalidators plugin_name = 'metadataprocessor' @@ -36,45 +37,22 @@ def _processForwardKey(api, myBlock): key = myBlock.getMetadata('newFSKey') # We don't need to validate here probably, but it helps - if api.get_utils().validatePubKey(key): + if stringvalidators.validate_pub_key(key): peer.addForwardKey(key) else: - raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) + raise onionrexceptions.InvalidPubkey("%s is not a valid pubkey key" % (key,)) def on_processblocks(api, data=None): # Generally fired by utils. myBlock = api.data['block'] blockType = api.data['type'] - logger.info('blockType is ' + blockType) - # Process specific block types # forwardKey blocks, add a new forward secrecy key for a peer if blockType == 'forwardKey': if api.data['validSig'] == True: _processForwardKey(api, myBlock) - # socket blocks - elif blockType == 'socket': - if api.data['validSig'] == True and myBlock.decrypted: # we check if it is decrypted as a way of seeing if it was for us - logger.info('Detected socket advertised to us...') - try: - address = myBlock.getMetadata('address') - except KeyError: - raise onionrexceptions.MissingAddress("Missing address for new socket") - try: - port = myBlock.getMetadata('port') - except KeyError: - raise ValueError("Missing port for new socket") - try: - reason = myBlock.getMetadata('reason') - except KeyError: - raise ValueError("Missing socket reason") - - socketInfo = json.dumps({'peer': api.data['signer'], 'address': address, 'port': port, 'create': False, 'reason': reason}) - api.get_core().daemonQueueAdd('addSocket', socketInfo) - else: - logger.warn("socket is not for us or is invalid") - + def on_init(api, data = None): pluginapi = api diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index aa5c2887..807feec6 100755 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - Private P2P Communication This plugin acts as a plugin manager, and allows the user to install other plugins distributed over Onionr. ''' @@ -22,6 +22,7 @@ import logger, config import os, sys, json, time, random, shutil, base64, getpass, datetime, re from onionrblockapi import Block +from onionrutils import importnewblocks, stringvalidators plugin_name = 'pluginmanager' @@ -180,11 +181,11 @@ def blockToPlugin(block): shutil.unpack_archive(source, destination) pluginapi.plugins.enable(name) - logger.info('Installation of %s complete.' % name) + logger.info('Installation of %s complete.' % name, terminal=True) return True except Exception as e: - logger.error('Failed to install plugin.', error = e, timestamp = False) + logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False @@ -236,13 +237,13 @@ def pluginToBlock(plugin, import_block = True): # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True) if import_block: - pluginapi.get_utils().importNewBlocks() + importnewblocks.import_new_blocks(pluginapi.get_core()) return hash else: - logger.error('Plugin %s does not exist.' % plugin) + logger.error('Plugin %s does not exist.' % plugin, terminal=True) except Exception as e: - logger.error('Failed to convert plugin to block.', error = e, timestamp = False) + logger.error('Failed to convert plugin to block.', error = e, timestamp = False, terminal=True) return False @@ -261,7 +262,7 @@ def installBlock(block): install = False - logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author)) + logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author), terminal=True) # TODO: Convert to single line if statement if os.path.exists(pluginapi.plugins.get_folder(name)): @@ -273,12 +274,12 @@ def installBlock(block): blockToPlugin(block.getHash()) addPlugin(name) else: - logger.info('Installation cancelled.') + logger.info('Installation cancelled.', terminal=True) return False return True except Exception as e: - logger.error('Failed to install plugin.', error = e, timestamp = False) + logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True) return False def uninstallPlugin(plugin): @@ -291,12 +292,12 @@ def uninstallPlugin(plugin): remove = False if not exists: - logger.warn('Plugin %s does not exist.' % plugin, timestamp = False) + logger.warn('Plugin %s does not exist.' % plugin, timestamp = False, terminal=True) return False default = 'y' if not installedByPluginManager: - logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False) + logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False, terminal=True) default = 'n' remove = logger.confirm(message = 'All plugin data will be lost. Are you sure you want to proceed %s?', default = default) @@ -306,20 +307,20 @@ def uninstallPlugin(plugin): pluginapi.plugins.disable(plugin) shutil.rmtree(pluginFolder) - logger.info('Uninstallation of %s complete.' % plugin) + logger.info('Uninstallation of %s complete.' % plugin, terminal=True) return True else: logger.info('Uninstallation cancelled.') except Exception as e: - logger.error('Failed to uninstall plugin.', error = e) + logger.error('Failed to uninstall plugin.', error = e, terminal=True) return False # command handlers def help(): - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]') - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) def commandInstallPlugin(): if len(sys.argv) >= 3: @@ -345,20 +346,20 @@ def commandInstallPlugin(): if pkobh is None: # still nothing found, try searching repositories - logger.info('Searching for public key in repositories...') + logger.info('Searching for public key in repositories...', terminal=True) try: repos = getRepositories() distributors = list() for repo, records in repos.items(): if pluginname in records: - logger.debug('Found %s in repository %s for plugin %s.' % (records[pluginname], repo, pluginname)) + logger.debug('Found %s in repository %s for plugin %s.' % (records[pluginname], repo, pluginname), terminal=True) distributors.append(records[pluginname]) if len(distributors) != 0: distributor = None if len(distributors) == 1: - logger.info('Found distributor: %s' % distributors[0]) + logger.info('Found distributor: %s' % distributors[0], terminal=True) distributor = distributors[0] else: distributors_message = '' @@ -368,11 +369,11 @@ def commandInstallPlugin(): distributors_message += ' ' + logger.colors.bold + str(index) + ') ' + logger.colors.reset + str(dist) + '\n' index += 1 - logger.info((logger.colors.bold + 'Found distributors (%s):' + logger.colors.reset + '\n' + distributors_message) % len(distributors)) + logger.info((logger.colors.bold + 'Found distributors (%s):' + logger.colors.reset + '\n' + distributors_message) % len(distributors), terminal=True) valid = False while not valid: - choice = logger.readline('Select the number of the key to use, from 1 to %s, or press Ctrl+C to cancel:' % (index - 1)) + choice = logger.readline('Select the number of the key to use, from 1 to %s, or press Ctrl+C to cancel:' % (index - 1), terminal=True) try: choice = int(choice) @@ -380,7 +381,7 @@ def commandInstallPlugin(): distributor = distributors[int(choice)] valid = True except KeyboardInterrupt: - logger.info('Installation cancelled.') + logger.info('Installation cancelled.', terminal=True) return True except: pass @@ -388,44 +389,42 @@ def commandInstallPlugin(): if not distributor is None: pkobh = distributor except Exception as e: - logger.warn('Failed to lookup plugin in repositories.', timestamp = False) - logger.error('asdf', error = e, timestamp = False) - + logger.warn('Failed to lookup plugin in repositories.', timestamp = False, terminal=True) return True if pkobh is None: - logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False) + logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False, terminal=True) return True - valid_hash = pluginapi.get_utils().validateHash(pkobh) + valid_hash = stringvalidators.validate_hash(pkobh) real_block = False - valid_key = pluginapi.get_utils().validatePubKey(pkobh) + valid_key = stringvalidators.validate_pub_key(pkobh) real_key = False if valid_hash: real_block = Block.exists(pkobh) elif valid_key: - real_key = pluginapi.get_utils().hasKey(pkobh) + real_key = pkobh in pluginapi.get_core().listPeers() blockhash = None if valid_hash and not real_block: - logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False) - logger.debug('Is valid hash, but does not belong to a known block.') + logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True) + logger.debug('Is valid hash, but does not belong to a known block.', terminal=True) return True elif valid_hash and real_block: blockhash = str(pkobh) - logger.debug('Using block %s...' % blockhash) + logger.debug('Using block %s...' % blockhash, terminal=True) installBlock(blockhash) elif valid_key and not real_key: - logger.error('Public key not found. Try adding the node by address manually, if possible.', timestamp = False) - logger.debug('Is valid key, but the key is not a known one.') + logger.error('Public key not found. Try adding the node by address manually, if possible.', timestamp = False, terminal=True) + logger.debug('Is valid key, but the key is not a known one.', terminal=True) elif valid_key and real_key: publickey = str(pkobh) - logger.debug('Using public key %s...' % publickey) + logger.debug('Using public key %s...' % publickey, terminal=True) saveKey(pluginname, pkobh) @@ -457,14 +456,14 @@ def commandInstallPlugin(): except Exception as e: pass - logger.warn('Only continue the installation if you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False) - logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock) + logger.warn('Only continue the installation if you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False, terminal=True) + logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock, terminal=True) installBlock(mostRecentVersionBlock) else: - logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh), timestamp = False) + logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh), timestamp = False, terminal=True) return else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]', terminal=True) return True @@ -472,12 +471,12 @@ def commandUninstallPlugin(): if len(sys.argv) >= 3: uninstallPlugin(sys.argv[2]) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) return True def commandSearchPlugin(): - logger.info('This feature has not been created yet. Please check back later.') + logger.info('This feature has not been created yet. Please check back later.', terminal=True) return True def commandAddRepository(): @@ -486,7 +485,7 @@ def commandAddRepository(): blockhash = sys.argv[2] - if pluginapi.get_utils().validateHash(blockhash): + if stringvalidators.validate_hash(blockhash): if Block.exists(blockhash): try: blockContent = json.loads(Block(blockhash, core = pluginapi.get_core()).getContent()) @@ -494,25 +493,25 @@ def commandAddRepository(): pluginslist = dict() for pluginname, distributor in blockContent['plugins']: - if pluginapi.get_utils().validatePubKey(distributor): + if stringvalidators.validate_pub_key(distributor): pluginslist[pluginname] = distributor - logger.debug('Found %s records in repository.' % len(pluginslist)) + logger.debug('Found %s records in repository.' % len(pluginslist), terminal=True) if len(pluginslist) != 0: addRepository(blockhash, pluginslist) - logger.info('Successfully added repository.') + logger.info('Successfully added repository.', terminal=True) else: - logger.error('Repository contains no records, not importing.', timestamp = False) + logger.error('Repository contains no records, not importing.', timestamp = False, terminal=True) except Exception as e: - logger.error('Failed to parse block.', error = e) + logger.error('Failed to parse block.', error = e, terminal=True) else: - logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False) + logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True) logger.debug('Is valid hash, but does not belong to a known block.') else: - logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), timestamp = False) + logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), timestamp = False, terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True @@ -522,19 +521,19 @@ def commandRemoveRepository(): blockhash = sys.argv[2] - if pluginapi.get_utils().validateHash(blockhash): + if stringvalidators.validate_hash(blockhash): if blockhash in getRepositories(): try: removeRepository(blockhash) - logger.info('Successfully removed repository.') + logger.info('Successfully removed repository.', terminal=True) except Exception as e: - logger.error('Failed to parse block.', error = e) + logger.error('Failed to parse block.', error = e, terminal=True) else: - logger.error('Repository has not been imported, nothing to remove.', timestamp = False) + logger.error('Repository has not been imported, nothing to remove.', timestamp = False, terminal=True) else: - logger.error('Unknown data "%s"; must be block hash.' % str(pkobh)) + logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True) return True @@ -547,11 +546,11 @@ def commandPublishPlugin(): if os.path.exists(pluginfolder) and not os.path.isfile(pluginfolder): block = pluginToBlock(pluginname) - logger.info('Plugin saved in block %s.' % block) + logger.info('Plugin saved in block %s.' % block, terminal=True) else: - logger.error('Plugin %s does not exist.' % pluginname, timestamp = False) + logger.error('Plugin %s does not exist.' % pluginname, timestamp = False, terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ', terminal=True) def commandCreateRepository(): if len(sys.argv) >= 3: @@ -575,22 +574,22 @@ def commandCreateRepository(): if distributor is None: distributor = getKey(pluginname) if distributor is None: - logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False) + logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False, terminal=True) success = False plugins.append([pluginname, distributor]) if not success: - logger.error('Please correct the above errors, then recreate the repository.') + logger.error('Please correct the above errors, then recreate the repository.', terminal=True) return True blockhash = createRepository(plugins) if not blockhash is None: - logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash)) + logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash), terminal=True) else: - logger.error('Failed to create repository, an unknown error occurred.') + logger.error('Failed to create repository, an unknown error occurred.', terminal=True) else: - logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]') + logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]', terminal=True) return True diff --git a/onionr/static-data/default-plugins/pms/loadinbox.py b/onionr/static-data/default-plugins/pms/loadinbox.py old mode 100644 new mode 100755 index 996d8f06..b4011b00 --- a/onionr/static-data/default-plugins/pms/loadinbox.py +++ b/onionr/static-data/default-plugins/pms/loadinbox.py @@ -1,3 +1,22 @@ +''' + Onionr - Private P2P Communication + + Load the user's inbox and return it as a list +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' import onionrblockapi def load_inbox(myCore): inbox_list = [] diff --git a/onionr/static-data/default-plugins/pms/mailapi.py b/onionr/static-data/default-plugins/pms/mailapi.py old mode 100644 new mode 100755 index 87cdb678..922432a0 --- a/onionr/static-data/default-plugins/pms/mailapi.py +++ b/onionr/static-data/default-plugins/pms/mailapi.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication HTTP endpoints for mail plugin. ''' @@ -21,6 +21,7 @@ import sys, os, json from flask import Response, request, redirect, Blueprint, abort import core from onionrusers import contactmanager +from onionrutils import stringvalidators sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) import loadinbox, sentboxdb @@ -34,7 +35,7 @@ def mail_ping(): @flask_blueprint.route('/mail/deletemsg/', methods=['POST']) def mail_delete(block): - if not c._utils.validateHash(block): + if not stringvalidators.validate_hash(block): abort(504) existing = kv.get('deleted_mail') if existing is None: @@ -42,6 +43,7 @@ def mail_delete(block): if block not in existing: existing.append(block) kv.put('deleted_mail', existing) + kv.flush() return 'success' @flask_blueprint.route('/mail/getinbox') @@ -50,16 +52,15 @@ def list_inbox(): @flask_blueprint.route('/mail/getsentbox') def list_sentbox(): + kv.refresh() sentbox_list = sentboxdb.SentBox(c).listSent() - sentbox_list_copy = list(sentbox_list) + list_copy = list(sentbox_list) deleted = kv.get('deleted_mail') if deleted is None: deleted = [] - for x in range(len(sentbox_list_copy) - 1): - if sentbox_list_copy[x]['hash'] in deleted: - x -= 1 - sentbox_list.pop(x) - else: - sentbox_list[x]['name'] = contactmanager.ContactManager(c, sentbox_list_copy[x]['peer'], saveUser=False).get_info('name') - - return json.dumps(sentbox_list) \ No newline at end of file + for x in list_copy: + if x['hash'] in deleted: + sentbox_list.remove(x) + continue + x['name'] = contactmanager.ContactManager(c, x['peer'], saveUser=False).get_info('name') + return json.dumps(sentbox_list) diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index 363c1259..5c532877 100755 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This default plugin handles private messages in an email like fashion ''' @@ -23,6 +23,7 @@ import logger, config, threading, time, datetime from onionrblockapi import Block import onionrexceptions from onionrusers import onionrusers +from onionrutils import stringvalidators, escapeansi, bytesconverter import locale, sys, os, json locale.setlocale(locale.LC_ALL, '') @@ -35,7 +36,7 @@ import sentboxdb, mailapi, loadinbox # import after path insert flask_blueprint = mailapi.flask_blueprint def draw_border(text): - #https://stackoverflow.com/a/20757491 + # This function taken from https://stackoverflow.com/a/20757491 by https://stackoverflow.com/users/816449/bunyk, under https://creativecommons.org/licenses/by-sa/3.0/ lines = text.splitlines() width = max(len(s) for s in lines) res = ['┌' + '─' * width + '┐'] @@ -73,7 +74,7 @@ class OnionrMail: blockCount = 0 pmBlockMap = {} pmBlocks = {} - logger.info('Decrypting messages...') + logger.info('Decrypting messages...', terminal=True) choice = '' displayList = [] subject = '' @@ -108,7 +109,7 @@ class OnionrMail: displayList.append('%s. %s - %s - <%s>: %s' % (blockCount, blockDate, senderDisplay[:12], subject[:10], blockHash)) while choice not in ('-q', 'q', 'quit'): for i in displayList: - logger.info(i) + logger.info(i, terminal=True) try: choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() except (EOFError, KeyboardInterrupt): @@ -135,27 +136,27 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer) + senderDisplay = bytesconverter.bytes_to_str(readBlock.signer) if len(senderDisplay.strip()) == 0: senderDisplay = 'Anonymous' - logger.info('Message received from %s' % (senderDisplay,)) - logger.info('Valid signature: %s' % readBlock.validSig) + logger.info('Message received from %s' % (senderDisplay,), terminal=True) + logger.info('Valid signature: %s' % readBlock.validSig, terminal=True) if not readBlock.validSig: - logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.') + logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.', terminal=True) cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') print('') if cancel != '-q': try: - print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) + print(draw_border(escapeansi.escape_ANSI(readBlock.bcontent.decode().strip()))) except ValueError: - logger.warn('Error presenting message. This is usually due to a malformed or blank message.') + logger.warn('Error presenting message. This is usually due to a malformed or blank message.', terminal=True) pass if readBlock.validSig: reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) print('') if reply == "-r": - self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,)) + self.draft_message(bytesconverter.bytes_to_str(readBlock.signer,)) else: logger.readline("Press enter to continue") print('') @@ -168,7 +169,7 @@ class OnionrMail: entering = True while entering: self.get_sent_list() - logger.info('Enter a block number or -q to return') + logger.info('Enter a block number or -q to return', terminal=True) try: choice = input('>') except (EOFError, KeyboardInterrupt) as e: @@ -182,11 +183,11 @@ class OnionrMail: try: self.sentboxList[int(choice)] except (IndexError, ValueError) as e: - logger.warn('Invalid block.') + logger.warn('Invalid block.', terminal=True) else: - logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1]) + logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1], terminal=True) # Print ansi escaped sent message - logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice)]][0])) + logger.info(escapeansi.escape_ANSI(self.sentMessages[self.sentboxList[int(choice)]][0]), terminal=True) input('Press enter to continue...') finally: if choice == '-q': @@ -199,9 +200,9 @@ class OnionrMail: self.sentMessages = {} for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) - self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject']) + self.sentMessages[i['hash']] = (bytesconverter.bytes_to_str(i['message']), i['peer'], i['subject']) if display: - logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date'])) + logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']), terminal=True) count += 1 return json.dumps(self.sentMessages) @@ -217,10 +218,10 @@ class OnionrMail: recip = logger.readline('Enter peer address, or -q to stop:').strip() if recip in ('-q', 'q'): raise EOFError - if not self.myCore._utils.validatePubKey(recip): + if not stringvalidators.validate_pub_key(recip): raise onionrexceptions.InvalidPubkey('Must be a valid ed25519 base32 encoded public key') except onionrexceptions.InvalidPubkey: - logger.warn('Invalid public key') + logger.warn('Invalid public key', terminal=True) except (KeyboardInterrupt, EOFError): entering = False else: @@ -234,7 +235,7 @@ class OnionrMail: pass cancelEnter = False - logger.info('Enter your message, stop by entering -q on a new line. -c to cancel') + logger.info('Enter your message, stop by entering -q on a new line. -c to cancel', terminal=True) while newLine != '-q': try: newLine = input() @@ -249,7 +250,7 @@ class OnionrMail: message += newLine if not cancelEnter: - logger.info('Inserting encrypted message as Onionr block....') + logger.info('Inserting encrypted message as Onionr block....', terminal=True) blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject}) @@ -261,16 +262,16 @@ class OnionrMail: while True: sigMsg = 'Message Signing: %s' - logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey) + logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey, terminal=True) if self.doSigs: sigMsg = sigMsg % ('enabled',) else: sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',) if self.doSigs: - logger.info(sigMsg) + logger.info(sigMsg, terminal=True) else: - logger.warn(sigMsg) - logger.info(self.strings.mainMenu.title()) # print out main menu + logger.warn(sigMsg, terminal=True) + logger.info(self.strings.mainMenu.title(), terminal=True) # print out main menu try: choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() except (KeyboardInterrupt, EOFError): @@ -285,12 +286,12 @@ class OnionrMail: elif choice in (self.strings.mainMenuChoices[3], '4'): self.toggle_signing() elif choice in (self.strings.mainMenuChoices[4], '5'): - logger.info('Goodbye.') + logger.info('Goodbye.', terminal=True) break elif choice == '': pass else: - logger.warn('Invalid choice.') + logger.warn('Invalid choice.', terminal=True) return def add_deleted(keyStore, bHash): @@ -303,10 +304,10 @@ def add_deleted(keyStore, bHash): keyStore.put('deleted_mail', existing.append(bHash)) def on_insertblock(api, data={}): - sentboxTools = sentboxdb.SentBox(api.get_core()) meta = json.loads(data['meta']) - sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) - + if meta['type'] == 'pm': + sentboxTools = sentboxdb.SentBox(api.get_core()) + sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) def on_init(api, data = None): ''' diff --git a/onionr/static-data/default-plugins/pms/sentboxdb.py b/onionr/static-data/default-plugins/pms/sentboxdb.py index 28a5de6f..f5a56272 100755 --- a/onionr/static-data/default-plugins/pms/sentboxdb.py +++ b/onionr/static-data/default-plugins/pms/sentboxdb.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network + Onionr - Private P2P Communication This file handles the sentbox for the mail plugin ''' @@ -19,6 +19,7 @@ ''' import sqlite3, os import core +from onionrutils import epoch class SentBox: def __init__(self, mycore): assert isinstance(mycore, core.Core) @@ -60,7 +61,7 @@ class SentBox: def addToSent(self, blockID, peer, message, subject=''): self.connect() - args = (blockID, peer, message, subject, self.core._utils.getEpoch()) + args = (blockID, peer, message, subject, epoch.get_epoch()) self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args) self.conn.commit() self.close() diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 86261502..3fb3b3a8 100755 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -1,28 +1,29 @@ { "general" : { "dev_mode" : true, + "announce_node" : true, "display_header" : false, - "minimum_block_pow": 4, - "minimum_send_pow": 4, - "socket_servers": false, - "security_level": 0, - "max_block_age": 2678400, - "bypass_tor_check": false, - "public_key": "", - "random_bind_ip": true + "minimum_block_pow" : 4, + "minimum_send_pow" : 4, + "use_subprocess_pow_if_possible" : true, + "socket_servers" : false, + "security_level" : 0, + "hide_created_blocks" : true, + "insert_deniable_blocks" : true, + "max_block_age" : 2678400, + "public_key" : "", + "random_bind_ip" : false }, "www" : { "public" : { "run" : true, - "path" : "static-data/www/public/", - "guess_mime" : true + "path" : "static-data/www/public/" }, "private" : { "run" : true, - "path" : "static-data/www/private/", - "guess_mime" : true + "path" : "static-data/www/private/" }, "ui" : { diff --git a/onionr/static-data/index.html b/onionr/static-data/index.html index 916bfe9c..926f1979 100755 --- a/onionr/static-data/index.html +++ b/onionr/static-data/index.html @@ -4,4 +4,4 @@

Onionr is a decentralized peer-to-peer data storage system.

-

To learn more about Onionr, see the website at https://Onionr.VoidNet.tech/

+

To learn more about Onionr, see the website at http://onionr.onionkvc5ibm37bmxwr56bdxcdnb6w3wm4bdghh5qo6f6za7gn7styid.onion or Onionr.net

diff --git a/onionr/static-data/www/board/board.js b/onionr/static-data/www/board/board.js old mode 100644 new mode 100755 index 7f513357..c4ead3c1 --- a/onionr/static-data/www/board/board.js +++ b/onionr/static-data/www/board/board.js @@ -1,29 +1,55 @@ -webpassword = '' requested = [] -document.getElementById('webpassWindow').style.display = 'block'; - var windowHeight = window.innerHeight; -document.getElementById('webpassWindow').style.height = windowHeight + "px"; - -function httpGet(theUrl) { - var xmlHttp = new XMLHttpRequest() - xmlHttp.open( "GET", theUrl, false ) // false for synchronous request - xmlHttp.setRequestHeader('token', webpassword) - xmlHttp.send( null ) - if (xmlHttp.status == 200){ - return xmlHttp.responseText +webpassword = webpass +newPostForm = document.getElementById('addMsg') +firstLoad = true +function appendMessages(msg){ + var humanDate = new Date(0) + if (msg.length == 0){ + return + } + var msg = JSON.parse(msg) + var dateEl = document.createElement('div') + var el = document.createElement('div') + var msgDate = msg['meta']['time'] + if (msgDate === undefined){ + msgDate = 'unknown' } else{ - return ""; + humanDate.setUTCSeconds(msgDate) + msgDate = humanDate.toDateString() } -} -function appendMessages(msg){ - el = document.createElement('div') el.className = 'entry' - el.innerText = msg - document.getElementById('feed').appendChild(el) - document.getElementById('feed').appendChild(document.createElement('br')) + el.innerText = msg['content'] + + /* Template Test */ + // Test to see if the browser supports the HTML template element by checking + // for the presence of the template element's content attribute. + if ('content' in document.createElement('template')) { + + // Instantiate the table with the existing HTML tbody + // and the row with the template + var template = document.getElementById('cMsgTemplate') + + // Clone the new row and insert it into the table + var feed = document.getElementById("feed") + var clone = document.importNode(template.content, true); + var div = clone.querySelectorAll("div") + div[2].textContent = msg['content'] + div[3].textContent = msgDate + + if (firstLoad){ + feed.appendChild(clone) + } + else{ + feed.prepend(clone) + } + + } else { + // Find another way to add the rows to the table because + // the HTML template element is not supported. + } } function getBlocks(){ @@ -32,27 +58,37 @@ function getBlocks(){ } var feedText = httpGet('/getblocksbytype/txt') - var blockList = feedText.split(',') + var blockList = feedText.split(',').reverse() for (i = 0; i < blockList.length; i++){ if (! requested.includes(blockList[i])){ - bl = httpGet('/gethtmlsafeblockdata/' + blockList[i]) + bl = httpGet('/getblockdata/' + blockList[i]) appendMessages(bl) requested.push(blockList[i]) } } -} - -document.getElementById('registerPassword').onclick = function(){ - webpassword = document.getElementById('webpassword').value - if (httpGet('/ping') === 'pong!'){ - document.getElementById('webpassWindow').style.display = 'none' - getBlocks() - } - else{ - alert('Sorry, but that password appears invalid.') - } + firstLoad = false } document.getElementById('refreshFeed').onclick = function(){ getBlocks() +} + +newPostForm.onsubmit = function(){ + var message = document.getElementById('newMsgText').value + var postData = {'message': message, 'type': 'txt', 'encrypt': false} + postData = JSON.stringify(postData) + newPostForm.style.display = 'none' + fetch('/insertblock', { + method: 'POST', + body: postData, + headers: { + "content-type": "application/json", + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into json + .then(function(data) { + newPostForm.style.display = 'block' + alert('Queued for submission!') + }) + return false } \ No newline at end of file diff --git a/onionr/static-data/www/board/index.html b/onionr/static-data/www/board/index.html old mode 100644 new mode 100755 index 7486a910..4d718078 --- a/onionr/static-data/www/board/index.html +++ b/onionr/static-data/www/board/index.html @@ -1,22 +1,160 @@ - - - - - OnionrBoard - - - - - + + + +
+ + +
+
+ +
+
+
+
+

+ Post message +

+
+
+
+ +
+
+ +
+
+
+ +
+
+
+

+ Feed +

+
+
+
+
+
+

+ Board Name +

+

+ +

+

+ Refresh Feed +

+
+
+
+
+
+ None Yet :) + +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/onionr/static-data/www/board/theme.css b/onionr/static-data/www/board/theme.css old mode 100644 new mode 100755 index 766e4407..2a9eeaff --- a/onionr/static-data/www/board/theme.css +++ b/onionr/static-data/www/board/theme.css @@ -1,31 +1,5 @@ -h1, h2, h3{ - font-family: sans-serif; -} -.hidden{ - display: none; -} -p{ - font-family: sans-serif; -} -#webpassWindow{ - background-color: black; - border: 1px solid black; - border-radius: 5px; - width: 100%; - z-index: 2; - color: white; - text-align: center; -} -.entry{ - color: red; -} - -#feed{ - margin-left: 2%; - margin-right: 25%; - margin-top: 1em; - border: 2px solid black; - padding: 5px; - min-height: 50px; +.cMsg{ + word-wrap:break-word; + word-break:break-word; } \ No newline at end of file diff --git a/onionr/static-data/www/clandestine/index.html b/onionr/static-data/www/clandestine/index.html new file mode 100755 index 00000000..cbed280a --- /dev/null +++ b/onionr/static-data/www/clandestine/index.html @@ -0,0 +1,88 @@ + + + + + + + + + Clandestine + + + + + + + + + + + +
+
+
+
+
+

+ Clandestine +

+

+ Instant messaging +

+
+
+
+ +
+

+ Identity +

+

+ +

+

+ Copy +

+
+
+
+
+
+
+ +
+ +
+ +
+
    +
    + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/clandestine/js/main.js b/onionr/static-data/www/clandestine/js/main.js new file mode 100755 index 00000000..a757f713 --- /dev/null +++ b/onionr/static-data/www/clandestine/js/main.js @@ -0,0 +1,28 @@ +friendList = {} +convoListElement = document.getElementsByClassName('conversationList')[0] + +function createConvoList(){ + console.log(friendList) + + for (friend in friendList){ + var convoEntry = document.createElement('div') + convoEntry.classList.add('convoEntry') + convoEntry.setAttribute('data-pubkey', friend) + convoEntry.innerText = friendList[friend] + convoListElement.append(convoEntry) + } +} + +fetch('/friends/list', { + headers: { + "token": webpass + }}) +.then((resp) => resp.json()) // Transform the data into json +.then(function(resp) { + var keys = [] + for(var k in resp) keys.push(k) + for (var i = 0; i < keys.length; i++){ + friendList[keys[i]] = resp[keys[i]]['name'] + } + createConvoList() +}) \ No newline at end of file diff --git a/onionr/static-data/www/friends/friends.js b/onionr/static-data/www/friends/friends.js index cbefae4d..57c35f64 100755 --- a/onionr/static-data/www/friends/friends.js +++ b/onionr/static-data/www/friends/friends.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the UI for managing friends/contacts @@ -27,6 +27,10 @@ function removeFriend(pubkey){ addForm.onsubmit = function(){ var friend = document.getElementsByName('addKey')[0] var alias = document.getElementsByName('data')[0] + if (alias.value.toLowerCase() == 'anonymous'){ + alert('Anonymous is a reserved name') + return false + } fetch('/friends/add/' + friend.value, { method: 'POST', @@ -50,7 +54,10 @@ fetch('/friends/list', { var keys = []; for(var k in resp) keys.push(k); console.log(keys) - friendListDisplay.innerHTML = 'Click name to view info

    ' + + if (keys.length == 0){ + friendListDisplay.innerText = "None yet :(" + } for (var i = 0; i < keys.length; i++){ var peer = keys[i] var name = resp[keys[i]]['name'] @@ -61,7 +68,7 @@ fetch('/friends/list', { var nameText = document.createElement('input') removeButton = document.createElement('button') removeButton.classList.add('friendRemove') - removeButton.classList.add('dangerBtn') + removeButton.classList.add('button', 'is-danger') entry.setAttribute('data-pubkey', peer) removeButton.innerText = 'X' nameText.value = name @@ -71,14 +78,6 @@ fetch('/friends/list', { entry.appendChild(removeButton) entry.appendChild(nameText) friendListDisplay.appendChild(entry) - entry.onclick = (function(entry, nameText, peer) {return function() { - if (nameText.length == 0){ - nameText = 'Anonymous' - } - document.getElementById('friendPubkey').value = peer - document.getElementById('friendName').innerText = nameText - overlay('friendInfo') - };})(entry, nameText.value, peer); } // If friend delete buttons are pressed @@ -90,8 +89,6 @@ fetch('/friends/list', { removeFriend(friendKey) } } - }) - document.getElementById('defriend').onclick = function(){ - removeFriend(document.getElementById('friendPubkey').value) - } \ No newline at end of file + + }) \ No newline at end of file diff --git a/onionr/static-data/www/friends/index.html b/onionr/static-data/www/friends/index.html index 8e38ac39..0df4805a 100755 --- a/onionr/static-data/www/friends/index.html +++ b/onionr/static-data/www/friends/index.html @@ -1,39 +1,158 @@ - - - - Onionr - - - - - - - -
    -
    - -
    Name:
    - - + + + + + + + Friends + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    + Friends +

    +

    + Manage your friend list +

    +
    +
    +
    + +
    +

    + Identity +

    +

    + +

    +

    + Copy +

    +
    +
    +
    +
    +
    - - - +
    + + +
    + + +
    +
    + +
    +
    +
    +

    + Add Friend +

    +
    +
    +
    +
    +
    + +

    + +

    +
    +
    + +

    + +

    +
    +
    +
    + +
    +
    + +
    +
    +
    +

    + Friend List +

    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/friends/style.css b/onionr/static-data/www/friends/style.css index 34bf64db..6c0aa90a 100755 --- a/onionr/static-data/www/friends/style.css +++ b/onionr/static-data/www/friends/style.css @@ -1,41 +1,4 @@ -h2, h3{ - font-family: Arial, Helvetica, sans-serif; -} - -form{ - border: 1px solid black; - border-radius: 5px; - padding: 1em; - margin-right: 10%; -} -form label{ - display: block; - margin-top: 0.5em; - margin-bottom: 0.5em; -} - -#friendList{ - display: inline; -} -#friendList span{ - text-align: center; -} #friendList button{ display: inline; margin-right: 10px; } - -#friendInfo .overlayContent{ - background-color: lightgray; - border: 3px solid black; - border-radius: 3px; - color: black; - font-family: Verdana, Geneva, Tahoma, sans-serif; - min-height: 100%; - padding: 1em; - margin: 1em; -} -#defriend{ - display: block; - margin-top: 1em; -} \ No newline at end of file diff --git a/onionr/static-data/www/mail/index.html b/onionr/static-data/www/mail/index.html old mode 100644 new mode 100755 index 8af6f035..b02b27d1 --- a/onionr/static-data/www/mail/index.html +++ b/onionr/static-data/www/mail/index.html @@ -1,72 +1,161 @@ - - - - Onionr Mail - - - - - - - -
    + + + + + + + Onionr Mail + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    + Mail +

    +

    + Send email style messages +

    +
    +
    +
    + +
    +

    + Identity +

    +

    + +

    +

    + Copy +

    +
    +
    +
    +

    + Refresh Page +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + From: Signature: +
    +
    + Subject: +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + To: +
    +
    +
    +
    +
    +
    + + + + To: + Subject: + + + +
    +
    + +
    +
    - - Onionr Mail ✉️ -

    -
    Home
    API server either shutdown, has disabled mail, or has experienced a bug.
    -
    -
    Current Used Identity:
    -

    -
    - -
    Nothing here yet 😞
    -
    -
    - -
    - From: Signature: -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - - To: -
    -
    -
    -
    -
    -
    - -
    - - To: - Subject: - - -
    -
    -
    - - - - +
    + +
    +
    + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css old mode 100644 new mode 100755 index af80deb1..f2c31086 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -54,10 +54,13 @@ input{ } .mailPing{ - display: none; color: orange; } + #addUnknownContact, .mailPing{ + display: none; + } + .danger{ color: red; } @@ -92,7 +95,7 @@ input{ color: black; } -#replyBtn{ +.break-up{ margin-top: 1em; } @@ -110,4 +113,8 @@ input{ color: black; font-size: 1.5em; width: 10%; +} + +.content{ + min-height: 1000px; } \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js old mode 100644 new mode 100755 index 302c84f6..d4cae20b --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface @@ -23,10 +23,22 @@ threadPart = document.getElementById('threads') threadPlaceholder = document.getElementById('threadPlaceholder') tabBtns = document.getElementById('tabBtns') threadContent = {} -myPub = httpGet('/getActivePubkey') replyBtn = document.getElementById('replyBtn') +addUnknownContact = document.getElementById('addUnknownContact') -function openReply(bHash){ +function addContact(pubkey, friendName){ + fetch('/friends/add/' + pubkey, { + method: 'POST', + headers: { + "token": webpass + }}).then(function(data) { + if (friendName.trim().length > 0){ + post_to_url('/friends/setinfo/' + pubkey + '/name', {'data': friendName, 'token': webpass}) + } + }) +} + +function openReply(bHash, quote, subject){ var inbox = document.getElementsByClassName('threadEntry') var entry = '' var friendName = '' @@ -36,22 +48,39 @@ function openReply(bHash){ entry = inbox[i] } } - if (entry.getAttribute('data-nameSet') == 'true'){ + if (entry.getAttribute('data-nameset') == 'true'){ document.getElementById('friendSelect').value = entry.getElementsByTagName('input')[0].value } key = entry.getAttribute('data-pubkey') document.getElementById('draftID').value = key - setActiveTab('send message') + document.getElementById('draftSubject').value = 'RE: ' + subject + + // Add quoted reply + var splitQuotes = quote.split('\n') + for (var x = 0; x < splitQuotes.length; x++){ + splitQuotes[x] = '> ' + splitQuotes[x] + } + quote = '\n' + key.substring(0, 12) + ' wrote:' + '\n' + splitQuotes.join('\n') + document.getElementById('draftText').value = quote + setActiveTab('compose') } -function openThread(bHash, sender, date, sigBool, pubkey){ +function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ + addUnknownContact.style.display = 'none' var messageDisplay = document.getElementById('threadDisplay') var blockContent = httpGet('/getblockbody/' + bHash) - document.getElementById('fromUser').value = sender + + document.getElementById('fromUser').value = sender || 'Anonymous' + document.getElementById('subjectView').innerText = subjectLine messageDisplay.innerText = blockContent var sigEl = document.getElementById('sigValid') var sigMsg = 'signature' + // show add unknown contact button if peer is unknown but still has pubkey + if (sender === pubkey && sender !== myPub && sigBool){ + addUnknownContact.style.display = 'inline' + } + if (sigBool){ sigMsg = 'Good ' + sigMsg sigEl.classList.remove('danger') @@ -64,7 +93,14 @@ function openThread(bHash, sender, date, sigBool, pubkey){ sigEl.innerText = sigMsg overlay('messageDisplay') replyBtn.onclick = function(){ - openReply(bHash) + openReply(bHash, messageDisplay.innerText, subjectLine) + } + addUnknownContact.onclick = function(){ + var friendName = prompt("Enter an alias for this contact:") + if (friendName === null || friendName.length == 0){ + return + } + addContact(pubkey, friendName) } } @@ -74,11 +110,12 @@ function setActiveTab(tabName){ case 'inbox': refreshPms() break - case 'sentbox': + case 'sent': getSentbox() break - case 'send message': + case 'compose': overlay('sendMessage') + setActiveTab('inbox') break } } @@ -133,6 +170,7 @@ function loadInboxEntries(bHash){ var humanDate = new Date(0) var metadata = resp['metadata'] humanDate.setUTCSeconds(resp['meta']['time']) + humanDate = humanDate.toString() validSig.style.display = 'none' if (resp['meta']['signer'] != ''){ senderInput.value = httpGet('/friends/getinfo/' + resp['meta']['signer'] + '/name') @@ -144,16 +182,15 @@ function loadInboxEntries(bHash){ } entry.setAttribute('data-nameSet', true) if (senderInput.value == ''){ - senderInput.value = resp['meta']['signer'] + senderInput.value = resp['meta']['signer'] || 'Anonymous' entry.setAttribute('data-nameSet', false) } - bHashDisplay.innerText = bHash.substring(0, 10) + //bHashDisplay.innerText = bHash.substring(0, 10) entry.setAttribute('data-hash', bHash) entry.setAttribute('data-pubkey', resp['meta']['signer']) senderInput.readOnly = true - dateStr.innerText = humanDate.toString() - deleteBtn.innerText = 'X' - deleteBtn.classList.add('dangerBtn', 'deleteBtn') + dateStr.innerText = humanDate.substring(0, humanDate.indexOf('(')) + deleteBtn.classList.add('delete', 'deleteBtn') if (metadata['subject'] === undefined || metadata['subject'] === null) { subjectLine.innerText = '()' } @@ -174,7 +211,7 @@ function loadInboxEntries(bHash){ if (event.target.classList.contains('deleteBtn')){ return } - openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey')) + openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey'), subjectLine.innerText) } deleteBtn.onclick = function(){ @@ -190,6 +227,7 @@ function getInbox(){ var requested = '' for(var i = 0; i < pms.length; i++) { if (pms[i].trim().length == 0){ + threadPart.innerText = 'No messages to show ¯\\_(ツ)_/¯' continue } else{ @@ -216,7 +254,7 @@ function getSentbox(){ if (keys.length == 0){ threadPart.innerHTML = "nothing to show here yet." } - for (var i = 0; i < keys.length; i++){ + for (var i = 0; i < keys.length; i++) (function(i, resp){ var entry = document.createElement('div') var obj = resp[i] var toLabel = document.createElement('span') @@ -225,39 +263,39 @@ function getSentbox(){ var sentDate = document.createElement('span') var humanDate = new Date(0) humanDate.setUTCSeconds(resp[i]['date']) + humanDate = humanDate.toString() var preview = document.createElement('span') var deleteBtn = document.createElement('button') var message = resp[i]['message'] - deleteBtn.classList.add('deleteBtn', 'dangerBtn') - deleteBtn.innerText = 'X' + deleteBtn.classList.add('deleteBtn', 'delete') toEl.readOnly = true - sentDate.innerText = humanDate - if (resp[i]['name'] == null){ + sentDate.innerText = humanDate.substring(0, humanDate.indexOf('(')) + if (resp[i]['name'] == null || resp[i]['name'].toLowerCase() == 'anonymous'){ toEl.value = resp[i]['peer'] } else{ toEl.value = resp[i]['name'] } preview.innerText = '(' + resp[i]['subject'] + ')' + entry.classList.add('sentboxList') entry.setAttribute('data-hash', resp[i]['hash']) entry.appendChild(deleteBtn) entry.appendChild(toLabel) entry.appendChild(toEl) entry.appendChild(preview) entry.appendChild(sentDate) - entry.onclick = (function(tree, el, msg) {return function() { - console.log(resp) - if (! entry.classList.contains('deleteBtn')){ - showSentboxWindow(el.value, msg) - } - };})(entry, toEl, message); - - deleteBtn.onclick = function(){ - entry.parentNode.removeChild(entry); - deleteMessage(entry.getAttribute('data-hash')) - } + threadPart.appendChild(entry) - } + + entry.onclick = function(e){ + if (e.target.classList.contains('deleteBtn')){ + deleteMessage(e.target.parentNode.getAttribute('data-hash')) + e.target.parentNode.parentNode.removeChild(e.target.parentNode) + return + } + showSentboxWindow(toEl.value, message) + } + })(i, resp) threadPart.appendChild(entry) }.bind(threadPart)) } @@ -281,30 +319,19 @@ fetch('/mail/getinbox', { } tabBtns.onclick = function(event){ - var children = tabBtns.children + var children = tabBtns.children[0].children for (var i = 0; i < children.length; i++) { var btn = children[i] - btn.classList.remove('activeTab') + btn.classList.remove('is-active') } - event.target.classList.add('activeTab') + event.target.parentElement.parentElement.classList.add('is-active') setActiveTab(event.target.innerText.toLowerCase()) } -var idStrings = document.getElementsByClassName('myPub') -for (var i = 0; i < idStrings.length; i++){ - if (idStrings[i].tagName.toLowerCase() == 'input'){ - idStrings[i].value = myPub - } - else{ - idStrings[i].innerText = myPub - } -} - for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){ document.getElementsByClassName('refresh')[i].style.float = 'right' } - fetch('/friends/list', { headers: { "token": webpass @@ -319,7 +346,7 @@ fetch('/friends/list', { friendSelectParent.appendChild(document.createElement('option')) for (var i = 0; i < keys.length; i++) { var option = document.createElement("option") - var name = resp[keys[i]]['name'] + var name = resp[keys[i]]['name'] || "" option.value = keys[i] if (name.length == 0){ option.text = keys[i] @@ -329,12 +356,6 @@ fetch('/friends/list', { } friendSelectParent.appendChild(option) } - - for (var i = 0; i < keys.length; i++){ - - //friendSelectParent - //alert(resp[keys[i]]['name']) - } }) setActiveTab('inbox') diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js index 704574e6..e2d34c1f 100755 --- a/onionr/static-data/www/mail/sendmail.js +++ b/onionr/static-data/www/mail/sendmail.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface @@ -54,6 +54,11 @@ sendForm.onsubmit = function(){ return false } } - sendMail(to.value, messageContent.value, subject.value) + if (to.value.length !== 56 && to.value.length !== 52){ + alert('Public key is not valid') + } + else{ + sendMail(to.value, messageContent.value, subject.value) + } return false } diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html old mode 100644 new mode 100755 index 8d35f070..6b09f976 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -1,40 +1,192 @@ - - - - Onionr - - - - - - -
    -
    -

    Your node will shutdown. Thank you for using Onionr.

    + + + + + + + Onionr + + + + + + + +
    +
    +

    Your node will shutdown. Thank you for using Onionr.

    +
    +
    + + + + +
    +
    +
    +
    +
    +

    + Onionr +

    +

    + Private Decentralized Communication +

    +
    +
    +
    + +
    +

    + Identity +

    +

    + +

    +

    + Copy +

    +
    +
    + +
    +
    +
    - - - - - +
    + + +
    + + +
    +
    +
    + +
    +
    +

    + Onionr +

    +
    +
    +
    +
    + +
    +

    + +

    +

    + Open Onionr Site +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    + Statistics +

    +
    +
    +
    +
    +
    + 🔒 Security level: +
    +
    + 🕰️ Uptime: +
    +
    +
    Session Connections
    +
    +
    + ️ Last Received: None since start +
    +
    + ⬇️ Total Requests: None since start +
    +
    + 🔗 Outgoing Connections: +
    +
    Unable to get nodes
    +
    +
    +
    Blocks
    +
    +
    + 💾 Stored Blocks: +
    +
    + 📨 Blocks in queue: +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +

    + Edit Configuration +

    +
    +
    +
    +

    Warning: Some values can be dangerous to change. Use caution.

    + + Save Config +
    +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/private/main.css b/onionr/static-data/www/private/main.css new file mode 100755 index 00000000..f7954963 --- /dev/null +++ b/onionr/static-data/www/private/main.css @@ -0,0 +1,18 @@ +.configEditor{ + width: 100%; + height: 300px; +} + +.saveConfig{ + margin-top: 1em; +} + + +.idLink{ + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ +} diff --git a/onionr/static-data/www/profiles/index.html b/onionr/static-data/www/profiles/index.html new file mode 100755 index 00000000..c03c3eb4 --- /dev/null +++ b/onionr/static-data/www/profiles/index.html @@ -0,0 +1,20 @@ + + + + + + Onionr Profiles + + + + + + +
    +

    +
    + + + + + \ No newline at end of file diff --git a/onionr/static-data/www/profiles/profiles.js b/onionr/static-data/www/profiles/profiles.js new file mode 100755 index 00000000..e69de29b diff --git a/onionr/static-data/www/shared/configeditor.js b/onionr/static-data/www/shared/configeditor.js new file mode 100755 index 00000000..172b3d5a --- /dev/null +++ b/onionr/static-data/www/shared/configeditor.js @@ -0,0 +1,54 @@ +/* + Onionr - Private P2P Communication + + This file is for configuration editing in the web interface + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +var saveBtns = document.getElementsByClassName('saveConfig') +var saveBtn = document.getElementsByClassName('saveConfig')[0] +var configEditor = document.getElementsByClassName('configEditor')[0] +var config = {} + +fetch('/config/get', { +headers: { + "token": webpass +}}) +.then((resp) => resp.text()) // Transform the data into text +.then(function(resp) { + configEditor.value = resp + config = JSON.parse(resp) //parse here so we can set the text field to pretty json +}) + +saveBtn.onclick = function(){ + var postData = configEditor.value + try { + JSON.parse(postData) + } catch (e) { + alert('Configuration is not valid JSON') + return false + } + fetch('/config/setall', { + method: 'POST', + body: postData, + headers: { + "content-type": "application/json", + "token": webpass + }}) + .then((resp) => resp.text()) // Transform the data into text + .then(function(data) { + alert('Config saved') + }) +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/images/favicon.ico b/onionr/static-data/www/shared/images/favicon.ico new file mode 100644 index 00000000..3c11d7e2 Binary files /dev/null and b/onionr/static-data/www/shared/images/favicon.ico differ diff --git a/onionr/static-data/www/shared/onionr-icon.png b/onionr/static-data/www/shared/images/onionr-icon.png old mode 100644 new mode 100755 similarity index 100% rename from onionr/static-data/www/shared/onionr-icon.png rename to onionr/static-data/www/shared/images/onionr-icon.png diff --git a/onionr/static-data/www/shared/images/onionr-text.png b/onionr/static-data/www/shared/images/onionr-text.png new file mode 100644 index 00000000..dd85097c Binary files /dev/null and b/onionr/static-data/www/shared/images/onionr-text.png differ diff --git a/onionr/static-data/www/shared/mail.css b/onionr/static-data/www/shared/mail.css new file mode 100644 index 00000000..5e9de2f6 --- /dev/null +++ b/onionr/static-data/www/shared/mail.css @@ -0,0 +1,32 @@ +.threadEntry{ + margin-bottom: 2%; +} +.threadEntry button{ + margin-right: 1%; +} +.threadEntry span, .sentboxList span{ + padding-left: 1%; +} + +.overlayContent{ + background-color: lightgray; + border: 3px solid black; + border-radius: 3px; + color: black; + font-family: Verdana, Geneva, Tahoma, sans-serif; + min-height: 100%; + padding: 1em; + margin: 1em; +} + +#draftText{ + margin-top: 1em; + margin-bottom: 1em; + display: block; + width: 50%; + height: 75%; + min-width: 2%; + min-height: 5%; + background: white; + color: black; +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/bulma.min.css b/onionr/static-data/www/shared/main/bulma.min.css new file mode 100644 index 00000000..434a9789 --- /dev/null +++ b/onionr/static-data/www/shared/main/bulma.min.css @@ -0,0 +1 @@ +/*! bulma.io v0.7.5 | MIT License | github.com/jgthms/bulma */@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.breadcrumb,.button,.delete,.file,.is-unselectable,.modal-close,.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous,.tabs{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link:not(.is-arrowless)::after,.select:not(.is-multiple):not(.is-loading)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:center;transform-origin:center;width:.625em}.block:not(:last-child),.box:not(:last-child),.breadcrumb:not(:last-child),.content:not(:last-child),.highlight:not(:last-child),.level:not(:last-child),.list:not(:last-child),.message:not(:last-child),.notification:not(:last-child),.progress:not(:last-child),.subtitle:not(:last-child),.table-container:not(:last-child),.table:not(:last-child),.tabs:not(:last-child),.title:not(:last-child){margin-bottom:1.5rem}.delete,.modal-close{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(10,10,10,.2);border:none;border-radius:290486px;cursor:pointer;pointer-events:auto;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:20px;max-height:20px;max-width:20px;min-height:20px;min-width:20px;outline:0;position:relative;vertical-align:top;width:20px}.delete::after,.delete::before,.modal-close::after,.modal-close::before{background-color:#fff;content:"";display:block;left:50%;position:absolute;top:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);-webkit-transform-origin:center center;transform-origin:center center}.delete::before,.modal-close::before{height:2px;width:50%}.delete::after,.modal-close::after{height:50%;width:2px}.delete:focus,.delete:hover,.modal-close:focus,.modal-close:hover{background-color:rgba(10,10,10,.3)}.delete:active,.modal-close:active{background-color:rgba(10,10,10,.4)}.is-small.delete,.is-small.modal-close{height:16px;max-height:16px;max-width:16px;min-height:16px;min-width:16px;width:16px}.is-medium.delete,.is-medium.modal-close{height:24px;max-height:24px;max-width:24px;min-height:24px;min-width:24px;width:24px}.is-large.delete,.is-large.modal-close{height:32px;max-height:32px;max-width:32px;min-height:32px;min-width:32px;width:32px}.button.is-loading::after,.control.is-loading::after,.loader,.select.is-loading::after{-webkit-animation:spinAround .5s infinite linear;animation:spinAround .5s infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:1em;position:relative;width:1em}.hero-video,.image.is-16by9 .has-ratio,.image.is-16by9 img,.image.is-1by1 .has-ratio,.image.is-1by1 img,.image.is-1by2 .has-ratio,.image.is-1by2 img,.image.is-1by3 .has-ratio,.image.is-1by3 img,.image.is-2by1 .has-ratio,.image.is-2by1 img,.image.is-2by3 .has-ratio,.image.is-2by3 img,.image.is-3by1 .has-ratio,.image.is-3by1 img,.image.is-3by2 .has-ratio,.image.is-3by2 img,.image.is-3by4 .has-ratio,.image.is-3by4 img,.image.is-3by5 .has-ratio,.image.is-3by5 img,.image.is-4by3 .has-ratio,.image.is-4by3 img,.image.is-4by5 .has-ratio,.image.is-4by5 img,.image.is-5by3 .has-ratio,.image.is-5by3 img,.image.is-5by4 .has-ratio,.image.is-5by4 img,.image.is-9by16 .has-ratio,.image.is-9by16 img,.image.is-square .has-ratio,.image.is-square img,.is-overlay,.modal,.modal-background{bottom:0;left:0;position:absolute;right:0;top:0}.button,.file-cta,.file-name,.input,.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous,.select select,.textarea{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.25em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.375em - 1px);padding-left:calc(.625em - 1px);padding-right:calc(.625em - 1px);padding-top:calc(.375em - 1px);position:relative;vertical-align:top}.button:active,.button:focus,.file-cta:active,.file-cta:focus,.file-name:active,.file-name:focus,.input:active,.input:focus,.is-active.button,.is-active.file-cta,.is-active.file-name,.is-active.input,.is-active.pagination-ellipsis,.is-active.pagination-link,.is-active.pagination-next,.is-active.pagination-previous,.is-active.textarea,.is-focused.button,.is-focused.file-cta,.is-focused.file-name,.is-focused.input,.is-focused.pagination-ellipsis,.is-focused.pagination-link,.is-focused.pagination-next,.is-focused.pagination-previous,.is-focused.textarea,.pagination-ellipsis:active,.pagination-ellipsis:focus,.pagination-link:active,.pagination-link:focus,.pagination-next:active,.pagination-next:focus,.pagination-previous:active,.pagination-previous:focus,.select select.is-active,.select select.is-focused,.select select:active,.select select:focus,.textarea:active,.textarea:focus{outline:0}.button[disabled],.file-cta[disabled],.file-name[disabled],.input[disabled],.pagination-ellipsis[disabled],.pagination-link[disabled],.pagination-next[disabled],.pagination-previous[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .button,fieldset[disabled] .file-cta,fieldset[disabled] .file-name,fieldset[disabled] .input,fieldset[disabled] .pagination-ellipsis,fieldset[disabled] .pagination-link,fieldset[disabled] .pagination-next,fieldset[disabled] .pagination-previous,fieldset[disabled] .select select,fieldset[disabled] .textarea{cursor:not-allowed}/*! minireset.css v0.0.4 | MIT License | github.com/jgthms/minireset.css */blockquote,body,dd,dl,dt,fieldset,figure,h1,h2,h3,h4,h5,h6,hr,html,iframe,legend,li,ol,p,pre,textarea,ul{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:400}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,::after,::before{box-sizing:inherit}embed,iframe,img,object,video{height:auto;max-width:100%}audio{max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}td:not([align]),th:not([align]){text-align:left}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1em;font-weight:400;line-height:1.5}a{color:#3273dc;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#ff3860;font-size:.875em;font-weight:400;padding:.25em .5em .25em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}fieldset{border:none}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#4a4a4a;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{vertical-align:top}table td:not([align]),table th:not([align]){text-align:left}table th{color:#363636}.is-clearfix::after{clear:both;content:" ";display:table}.is-pulled-left{float:left!important}.is-pulled-right{float:right!important}.is-clipped{overflow:hidden!important}.is-size-1{font-size:3rem!important}.is-size-2{font-size:2.5rem!important}.is-size-3{font-size:2rem!important}.is-size-4{font-size:1.5rem!important}.is-size-5{font-size:1.25rem!important}.is-size-6{font-size:1rem!important}.is-size-7{font-size:.75rem!important}@media screen and (max-width:768px){.is-size-1-mobile{font-size:3rem!important}.is-size-2-mobile{font-size:2.5rem!important}.is-size-3-mobile{font-size:2rem!important}.is-size-4-mobile{font-size:1.5rem!important}.is-size-5-mobile{font-size:1.25rem!important}.is-size-6-mobile{font-size:1rem!important}.is-size-7-mobile{font-size:.75rem!important}}@media screen and (min-width:769px),print{.is-size-1-tablet{font-size:3rem!important}.is-size-2-tablet{font-size:2.5rem!important}.is-size-3-tablet{font-size:2rem!important}.is-size-4-tablet{font-size:1.5rem!important}.is-size-5-tablet{font-size:1.25rem!important}.is-size-6-tablet{font-size:1rem!important}.is-size-7-tablet{font-size:.75rem!important}}@media screen and (max-width:1023px){.is-size-1-touch{font-size:3rem!important}.is-size-2-touch{font-size:2.5rem!important}.is-size-3-touch{font-size:2rem!important}.is-size-4-touch{font-size:1.5rem!important}.is-size-5-touch{font-size:1.25rem!important}.is-size-6-touch{font-size:1rem!important}.is-size-7-touch{font-size:.75rem!important}}@media screen and (min-width:1024px){.is-size-1-desktop{font-size:3rem!important}.is-size-2-desktop{font-size:2.5rem!important}.is-size-3-desktop{font-size:2rem!important}.is-size-4-desktop{font-size:1.5rem!important}.is-size-5-desktop{font-size:1.25rem!important}.is-size-6-desktop{font-size:1rem!important}.is-size-7-desktop{font-size:.75rem!important}}@media screen and (min-width:1216px){.is-size-1-widescreen{font-size:3rem!important}.is-size-2-widescreen{font-size:2.5rem!important}.is-size-3-widescreen{font-size:2rem!important}.is-size-4-widescreen{font-size:1.5rem!important}.is-size-5-widescreen{font-size:1.25rem!important}.is-size-6-widescreen{font-size:1rem!important}.is-size-7-widescreen{font-size:.75rem!important}}@media screen and (min-width:1408px){.is-size-1-fullhd{font-size:3rem!important}.is-size-2-fullhd{font-size:2.5rem!important}.is-size-3-fullhd{font-size:2rem!important}.is-size-4-fullhd{font-size:1.5rem!important}.is-size-5-fullhd{font-size:1.25rem!important}.is-size-6-fullhd{font-size:1rem!important}.is-size-7-fullhd{font-size:.75rem!important}}.has-text-centered{text-align:center!important}.has-text-justified{text-align:justify!important}.has-text-left{text-align:left!important}.has-text-right{text-align:right!important}@media screen and (max-width:768px){.has-text-centered-mobile{text-align:center!important}}@media screen and (min-width:769px),print{.has-text-centered-tablet{text-align:center!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-centered-tablet-only{text-align:center!important}}@media screen and (max-width:1023px){.has-text-centered-touch{text-align:center!important}}@media screen and (min-width:1024px){.has-text-centered-desktop{text-align:center!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-centered-desktop-only{text-align:center!important}}@media screen and (min-width:1216px){.has-text-centered-widescreen{text-align:center!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-centered-widescreen-only{text-align:center!important}}@media screen and (min-width:1408px){.has-text-centered-fullhd{text-align:center!important}}@media screen and (max-width:768px){.has-text-justified-mobile{text-align:justify!important}}@media screen and (min-width:769px),print{.has-text-justified-tablet{text-align:justify!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-justified-tablet-only{text-align:justify!important}}@media screen and (max-width:1023px){.has-text-justified-touch{text-align:justify!important}}@media screen and (min-width:1024px){.has-text-justified-desktop{text-align:justify!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-justified-desktop-only{text-align:justify!important}}@media screen and (min-width:1216px){.has-text-justified-widescreen{text-align:justify!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-justified-widescreen-only{text-align:justify!important}}@media screen and (min-width:1408px){.has-text-justified-fullhd{text-align:justify!important}}@media screen and (max-width:768px){.has-text-left-mobile{text-align:left!important}}@media screen and (min-width:769px),print{.has-text-left-tablet{text-align:left!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-left-tablet-only{text-align:left!important}}@media screen and (max-width:1023px){.has-text-left-touch{text-align:left!important}}@media screen and (min-width:1024px){.has-text-left-desktop{text-align:left!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-left-desktop-only{text-align:left!important}}@media screen and (min-width:1216px){.has-text-left-widescreen{text-align:left!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-left-widescreen-only{text-align:left!important}}@media screen and (min-width:1408px){.has-text-left-fullhd{text-align:left!important}}@media screen and (max-width:768px){.has-text-right-mobile{text-align:right!important}}@media screen and (min-width:769px),print{.has-text-right-tablet{text-align:right!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-right-tablet-only{text-align:right!important}}@media screen and (max-width:1023px){.has-text-right-touch{text-align:right!important}}@media screen and (min-width:1024px){.has-text-right-desktop{text-align:right!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-right-desktop-only{text-align:right!important}}@media screen and (min-width:1216px){.has-text-right-widescreen{text-align:right!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-right-widescreen-only{text-align:right!important}}@media screen and (min-width:1408px){.has-text-right-fullhd{text-align:right!important}}.is-capitalized{text-transform:capitalize!important}.is-lowercase{text-transform:lowercase!important}.is-uppercase{text-transform:uppercase!important}.is-italic{font-style:italic!important}.has-text-white{color:#fff!important}a.has-text-white:focus,a.has-text-white:hover{color:#e6e6e6!important}.has-background-white{background-color:#fff!important}.has-text-black{color:#0a0a0a!important}a.has-text-black:focus,a.has-text-black:hover{color:#000!important}.has-background-black{background-color:#0a0a0a!important}.has-text-light{color:#f5f5f5!important}a.has-text-light:focus,a.has-text-light:hover{color:#dbdbdb!important}.has-background-light{background-color:#f5f5f5!important}.has-text-dark{color:#363636!important}a.has-text-dark:focus,a.has-text-dark:hover{color:#1c1c1c!important}.has-background-dark{background-color:#363636!important}.has-text-primary{color:#00d1b2!important}a.has-text-primary:focus,a.has-text-primary:hover{color:#009e86!important}.has-background-primary{background-color:#00d1b2!important}.has-text-link{color:#3273dc!important}a.has-text-link:focus,a.has-text-link:hover{color:#205bbc!important}.has-background-link{background-color:#3273dc!important}.has-text-info{color:#209cee!important}a.has-text-info:focus,a.has-text-info:hover{color:#0f81cc!important}.has-background-info{background-color:#209cee!important}.has-text-success{color:#23d160!important}a.has-text-success:focus,a.has-text-success:hover{color:#1ca64c!important}.has-background-success{background-color:#23d160!important}.has-text-warning{color:#ffdd57!important}a.has-text-warning:focus,a.has-text-warning:hover{color:#ffd324!important}.has-background-warning{background-color:#ffdd57!important}.has-text-danger{color:#ff3860!important}a.has-text-danger:focus,a.has-text-danger:hover{color:#ff0537!important}.has-background-danger{background-color:#ff3860!important}.has-text-black-bis{color:#121212!important}.has-background-black-bis{background-color:#121212!important}.has-text-black-ter{color:#242424!important}.has-background-black-ter{background-color:#242424!important}.has-text-grey-darker{color:#363636!important}.has-background-grey-darker{background-color:#363636!important}.has-text-grey-dark{color:#4a4a4a!important}.has-background-grey-dark{background-color:#4a4a4a!important}.has-text-grey{color:#7a7a7a!important}.has-background-grey{background-color:#7a7a7a!important}.has-text-grey-light{color:#b5b5b5!important}.has-background-grey-light{background-color:#b5b5b5!important}.has-text-grey-lighter{color:#dbdbdb!important}.has-background-grey-lighter{background-color:#dbdbdb!important}.has-text-white-ter{color:#f5f5f5!important}.has-background-white-ter{background-color:#f5f5f5!important}.has-text-white-bis{color:#fafafa!important}.has-background-white-bis{background-color:#fafafa!important}.has-text-weight-light{font-weight:300!important}.has-text-weight-normal{font-weight:400!important}.has-text-weight-medium{font-weight:500!important}.has-text-weight-semibold{font-weight:600!important}.has-text-weight-bold{font-weight:700!important}.is-family-primary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-secondary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-sans-serif{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-monospace{font-family:monospace!important}.is-family-code{font-family:monospace!important}.is-block{display:block!important}@media screen and (max-width:768px){.is-block-mobile{display:block!important}}@media screen and (min-width:769px),print{.is-block-tablet{display:block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-block-tablet-only{display:block!important}}@media screen and (max-width:1023px){.is-block-touch{display:block!important}}@media screen and (min-width:1024px){.is-block-desktop{display:block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-block-desktop-only{display:block!important}}@media screen and (min-width:1216px){.is-block-widescreen{display:block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-block-widescreen-only{display:block!important}}@media screen and (min-width:1408px){.is-block-fullhd{display:block!important}}.is-flex{display:flex!important}@media screen and (max-width:768px){.is-flex-mobile{display:flex!important}}@media screen and (min-width:769px),print{.is-flex-tablet{display:flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-flex-tablet-only{display:flex!important}}@media screen and (max-width:1023px){.is-flex-touch{display:flex!important}}@media screen and (min-width:1024px){.is-flex-desktop{display:flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-flex-desktop-only{display:flex!important}}@media screen and (min-width:1216px){.is-flex-widescreen{display:flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-flex-widescreen-only{display:flex!important}}@media screen and (min-width:1408px){.is-flex-fullhd{display:flex!important}}.is-inline{display:inline!important}@media screen and (max-width:768px){.is-inline-mobile{display:inline!important}}@media screen and (min-width:769px),print{.is-inline-tablet{display:inline!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-tablet-only{display:inline!important}}@media screen and (max-width:1023px){.is-inline-touch{display:inline!important}}@media screen and (min-width:1024px){.is-inline-desktop{display:inline!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-desktop-only{display:inline!important}}@media screen and (min-width:1216px){.is-inline-widescreen{display:inline!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-widescreen-only{display:inline!important}}@media screen and (min-width:1408px){.is-inline-fullhd{display:inline!important}}.is-inline-block{display:inline-block!important}@media screen and (max-width:768px){.is-inline-block-mobile{display:inline-block!important}}@media screen and (min-width:769px),print{.is-inline-block-tablet{display:inline-block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-block-tablet-only{display:inline-block!important}}@media screen and (max-width:1023px){.is-inline-block-touch{display:inline-block!important}}@media screen and (min-width:1024px){.is-inline-block-desktop{display:inline-block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-block-desktop-only{display:inline-block!important}}@media screen and (min-width:1216px){.is-inline-block-widescreen{display:inline-block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-block-widescreen-only{display:inline-block!important}}@media screen and (min-width:1408px){.is-inline-block-fullhd{display:inline-block!important}}.is-inline-flex{display:inline-flex!important}@media screen and (max-width:768px){.is-inline-flex-mobile{display:inline-flex!important}}@media screen and (min-width:769px),print{.is-inline-flex-tablet{display:inline-flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-flex-tablet-only{display:inline-flex!important}}@media screen and (max-width:1023px){.is-inline-flex-touch{display:inline-flex!important}}@media screen and (min-width:1024px){.is-inline-flex-desktop{display:inline-flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-flex-desktop-only{display:inline-flex!important}}@media screen and (min-width:1216px){.is-inline-flex-widescreen{display:inline-flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-flex-widescreen-only{display:inline-flex!important}}@media screen and (min-width:1408px){.is-inline-flex-fullhd{display:inline-flex!important}}.is-hidden{display:none!important}.is-sr-only{border:none!important;clip:rect(0,0,0,0)!important;height:.01em!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:.01em!important}@media screen and (max-width:768px){.is-hidden-mobile{display:none!important}}@media screen and (min-width:769px),print{.is-hidden-tablet{display:none!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-hidden-tablet-only{display:none!important}}@media screen and (max-width:1023px){.is-hidden-touch{display:none!important}}@media screen and (min-width:1024px){.is-hidden-desktop{display:none!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-hidden-desktop-only{display:none!important}}@media screen and (min-width:1216px){.is-hidden-widescreen{display:none!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-hidden-widescreen-only{display:none!important}}@media screen and (min-width:1408px){.is-hidden-fullhd{display:none!important}}.is-invisible{visibility:hidden!important}@media screen and (max-width:768px){.is-invisible-mobile{visibility:hidden!important}}@media screen and (min-width:769px),print{.is-invisible-tablet{visibility:hidden!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-invisible-tablet-only{visibility:hidden!important}}@media screen and (max-width:1023px){.is-invisible-touch{visibility:hidden!important}}@media screen and (min-width:1024px){.is-invisible-desktop{visibility:hidden!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-invisible-desktop-only{visibility:hidden!important}}@media screen and (min-width:1216px){.is-invisible-widescreen{visibility:hidden!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-invisible-widescreen-only{visibility:hidden!important}}@media screen and (min-width:1408px){.is-invisible-fullhd{visibility:hidden!important}}.is-marginless{margin:0!important}.is-paddingless{padding:0!important}.is-radiusless{border-radius:0!important}.is-shadowless{box-shadow:none!important}.is-relative{position:relative!important}.box{background-color:#fff;border-radius:6px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;display:block;padding:1.25rem}a.box:focus,a.box:hover{box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px #3273dc}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #3273dc}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.375em - 1px);padding-left:.75em;padding-right:.75em;padding-top:calc(.375em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-large,.button .icon.is-medium,.button .icon.is-small{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-.375em - 1px);margin-right:.1875em}.button .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:calc(-.375em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-.375em - 1px);margin-right:calc(-.375em - 1px)}.button.is-hovered,.button:hover{border-color:#b5b5b5;color:#363636}.button.is-focused,.button:focus{border-color:#3273dc;color:#363636}.button.is-focused:not(:active),.button:focus:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-active,.button:active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#4a4a4a;text-decoration:underline}.button.is-text.is-focused,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text:hover{background-color:#f5f5f5;color:#363636}.button.is-text.is-active,.button.is-text:active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled],fieldset[disabled] .button.is-text{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white.is-hovered,.button.is-white:hover{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white.is-focused,.button.is-white:focus{border-color:transparent;color:#0a0a0a}.button.is-white.is-focused:not(:active),.button.is-white:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white.is-active,.button.is-white:active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled],fieldset[disabled] .button.is-white{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-hovered,.button.is-white.is-inverted:hover{background-color:#000}.button.is-white.is-inverted[disabled],fieldset[disabled] .button.is-white.is-inverted{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined.is-focused,.button.is-white.is-outlined.is-hovered,.button.is-white.is-outlined:focus,.button.is-white.is-outlined:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-outlined.is-loading.is-focused::after,.button.is-white.is-outlined.is-loading.is-hovered::after,.button.is-white.is-outlined.is-loading:focus::after,.button.is-white.is-outlined.is-loading:hover::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined[disabled],fieldset[disabled] .button.is-white.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined.is-focused,.button.is-white.is-inverted.is-outlined.is-hovered,.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined:hover{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-white.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-white.is-inverted.is-outlined.is-loading:focus::after,.button.is-white.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black.is-hovered,.button.is-black:hover{background-color:#040404;border-color:transparent;color:#fff}.button.is-black.is-focused,.button.is-black:focus{border-color:transparent;color:#fff}.button.is-black.is-focused:not(:active),.button.is-black:focus:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black.is-active,.button.is-black:active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled],fieldset[disabled] .button.is-black{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-hovered,.button.is-black.is-inverted:hover{background-color:#f2f2f2}.button.is-black.is-inverted[disabled],fieldset[disabled] .button.is-black.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined.is-focused,.button.is-black.is-outlined.is-hovered,.button.is-black.is-outlined:focus,.button.is-black.is-outlined:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-outlined.is-loading.is-focused::after,.button.is-black.is-outlined.is-loading.is-hovered::after,.button.is-black.is-outlined.is-loading:focus::after,.button.is-black.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined[disabled],fieldset[disabled] .button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined.is-focused,.button.is-black.is-inverted.is-outlined.is-hovered,.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined:hover{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-black.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-black.is-inverted.is-outlined.is-loading:focus::after,.button.is-black.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:#363636}.button.is-light.is-hovered,.button.is-light:hover{background-color:#eee;border-color:transparent;color:#363636}.button.is-light.is-focused,.button.is-light:focus{border-color:transparent;color:#363636}.button.is-light.is-focused:not(:active),.button.is-light:focus:not(:active){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light.is-active,.button.is-light:active{background-color:#e8e8e8;border-color:transparent;color:#363636}.button.is-light[disabled],fieldset[disabled] .button.is-light{background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted.is-hovered,.button.is-light.is-inverted:hover{background-color:#292929}.button.is-light.is-inverted[disabled],fieldset[disabled] .button.is-light.is-inverted{background-color:#363636;border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined.is-focused,.button.is-light.is-outlined.is-hovered,.button.is-light.is-outlined:focus,.button.is-light.is-outlined:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.button.is-light.is-outlined.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-outlined.is-loading.is-focused::after,.button.is-light.is-outlined.is-loading.is-hovered::after,.button.is-light.is-outlined.is-loading:focus::after,.button.is-light.is-outlined.is-loading:hover::after{border-color:transparent transparent #363636 #363636!important}.button.is-light.is-outlined[disabled],fieldset[disabled] .button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-light.is-inverted.is-outlined.is-focused,.button.is-light.is-inverted.is-outlined.is-hovered,.button.is-light.is-inverted.is-outlined:focus,.button.is-light.is-inverted.is-outlined:hover{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-light.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-light.is-inverted.is-outlined.is-loading:focus::after,.button.is-light.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark{background-color:#363636;border-color:transparent;color:#f5f5f5}.button.is-dark.is-hovered,.button.is-dark:hover{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused,.button.is-dark:focus{border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused:not(:active),.button.is-dark:focus:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark.is-active,.button.is-dark:active{background-color:#292929;border-color:transparent;color:#f5f5f5}.button.is-dark[disabled],fieldset[disabled] .button.is-dark{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted.is-hovered,.button.is-dark.is-inverted:hover{background-color:#e8e8e8}.button.is-dark.is-inverted[disabled],fieldset[disabled] .button.is-dark.is-inverted{background-color:#f5f5f5;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined.is-focused,.button.is-dark.is-outlined.is-hovered,.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-outlined.is-loading.is-focused::after,.button.is-dark.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-outlined.is-loading:focus::after,.button.is-dark.is-outlined.is-loading:hover::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-dark.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-dark.is-inverted.is-outlined.is-focused,.button.is-dark.is-inverted.is-outlined.is-hovered,.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined:hover{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-inverted.is-outlined.is-loading:focus::after,.button.is-dark.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-primary{background-color:#00d1b2;border-color:transparent;color:#fff}.button.is-primary.is-hovered,.button.is-primary:hover{background-color:#00c4a7;border-color:transparent;color:#fff}.button.is-primary.is-focused,.button.is-primary:focus{border-color:transparent;color:#fff}.button.is-primary.is-focused:not(:active),.button.is-primary:focus:not(:active){box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.button.is-primary.is-active,.button.is-primary:active{background-color:#00b89c;border-color:transparent;color:#fff}.button.is-primary[disabled],fieldset[disabled] .button.is-primary{background-color:#00d1b2;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted.is-hovered,.button.is-primary.is-inverted:hover{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled],fieldset[disabled] .button.is-primary.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#00d1b2}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined{background-color:transparent;border-color:#00d1b2;color:#00d1b2}.button.is-primary.is-outlined.is-focused,.button.is-primary.is-outlined.is-hovered,.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined:hover{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #00d1b2 #00d1b2!important}.button.is-primary.is-outlined.is-loading.is-focused::after,.button.is-primary.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-outlined.is-loading:focus::after,.button.is-primary.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-outlined{background-color:transparent;border-color:#00d1b2;box-shadow:none;color:#00d1b2}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined.is-focused,.button.is-primary.is-inverted.is-outlined.is-hovered,.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined:hover{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-inverted.is-outlined.is-loading:focus::after,.button.is-primary.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #00d1b2 #00d1b2!important}.button.is-primary.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link{background-color:#3273dc;border-color:transparent;color:#fff}.button.is-link.is-hovered,.button.is-link:hover{background-color:#276cda;border-color:transparent;color:#fff}.button.is-link.is-focused,.button.is-link:focus{border-color:transparent;color:#fff}.button.is-link.is-focused:not(:active),.button.is-link:focus:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-link.is-active,.button.is-link:active{background-color:#2366d1;border-color:transparent;color:#fff}.button.is-link[disabled],fieldset[disabled] .button.is-link{background-color:#3273dc;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-hovered,.button.is-link.is-inverted:hover{background-color:#f2f2f2}.button.is-link.is-inverted[disabled],fieldset[disabled] .button.is-link.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3273dc}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;color:#3273dc}.button.is-link.is-outlined.is-focused,.button.is-link.is-outlined.is-hovered,.button.is-link.is-outlined:focus,.button.is-link.is-outlined:hover{background-color:#3273dc;border-color:#3273dc;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-outlined.is-loading.is-focused::after,.button.is-link.is-outlined.is-loading.is-hovered::after,.button.is-link.is-outlined.is-loading:focus::after,.button.is-link.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined[disabled],fieldset[disabled] .button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;box-shadow:none;color:#3273dc}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined.is-focused,.button.is-link.is-inverted.is-outlined.is-hovered,.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined:hover{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-link.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-link.is-inverted.is-outlined.is-loading:focus::after,.button.is-link.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info{background-color:#209cee;border-color:transparent;color:#fff}.button.is-info.is-hovered,.button.is-info:hover{background-color:#1496ed;border-color:transparent;color:#fff}.button.is-info.is-focused,.button.is-info:focus{border-color:transparent;color:#fff}.button.is-info.is-focused:not(:active),.button.is-info:focus:not(:active){box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.button.is-info.is-active,.button.is-info:active{background-color:#118fe4;border-color:transparent;color:#fff}.button.is-info[disabled],fieldset[disabled] .button.is-info{background-color:#209cee;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#209cee}.button.is-info.is-inverted.is-hovered,.button.is-info.is-inverted:hover{background-color:#f2f2f2}.button.is-info.is-inverted[disabled],fieldset[disabled] .button.is-info.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#209cee}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined{background-color:transparent;border-color:#209cee;color:#209cee}.button.is-info.is-outlined.is-focused,.button.is-info.is-outlined.is-hovered,.button.is-info.is-outlined:focus,.button.is-info.is-outlined:hover{background-color:#209cee;border-color:#209cee;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #209cee #209cee!important}.button.is-info.is-outlined.is-loading.is-focused::after,.button.is-info.is-outlined.is-loading.is-hovered::after,.button.is-info.is-outlined.is-loading:focus::after,.button.is-info.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined[disabled],fieldset[disabled] .button.is-info.is-outlined{background-color:transparent;border-color:#209cee;box-shadow:none;color:#209cee}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined.is-focused,.button.is-info.is-inverted.is-outlined.is-hovered,.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined:hover{background-color:#fff;color:#209cee}.button.is-info.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-info.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-info.is-inverted.is-outlined.is-loading:focus::after,.button.is-info.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #209cee #209cee!important}.button.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success{background-color:#23d160;border-color:transparent;color:#fff}.button.is-success.is-hovered,.button.is-success:hover{background-color:#22c65b;border-color:transparent;color:#fff}.button.is-success.is-focused,.button.is-success:focus{border-color:transparent;color:#fff}.button.is-success.is-focused:not(:active),.button.is-success:focus:not(:active){box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.button.is-success.is-active,.button.is-success:active{background-color:#20bc56;border-color:transparent;color:#fff}.button.is-success[disabled],fieldset[disabled] .button.is-success{background-color:#23d160;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#23d160}.button.is-success.is-inverted.is-hovered,.button.is-success.is-inverted:hover{background-color:#f2f2f2}.button.is-success.is-inverted[disabled],fieldset[disabled] .button.is-success.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#23d160}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined{background-color:transparent;border-color:#23d160;color:#23d160}.button.is-success.is-outlined.is-focused,.button.is-success.is-outlined.is-hovered,.button.is-success.is-outlined:focus,.button.is-success.is-outlined:hover{background-color:#23d160;border-color:#23d160;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #23d160 #23d160!important}.button.is-success.is-outlined.is-loading.is-focused::after,.button.is-success.is-outlined.is-loading.is-hovered::after,.button.is-success.is-outlined.is-loading:focus::after,.button.is-success.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined[disabled],fieldset[disabled] .button.is-success.is-outlined{background-color:transparent;border-color:#23d160;box-shadow:none;color:#23d160}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined.is-focused,.button.is-success.is-inverted.is-outlined.is-hovered,.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined:hover{background-color:#fff;color:#23d160}.button.is-success.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-success.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-success.is-inverted.is-outlined.is-loading:focus::after,.button.is-success.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #23d160 #23d160!important}.button.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-hovered,.button.is-warning:hover{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused,.button.is-warning:focus{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused:not(:active),.button.is-warning:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning.is-active,.button.is-warning:active{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning[disabled],fieldset[disabled] .button.is-warning{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-hovered,.button.is-warning.is-inverted:hover{background-color:rgba(0,0,0,.7)}.button.is-warning.is-inverted[disabled],fieldset[disabled] .button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined.is-focused,.button.is-warning.is-outlined.is-hovered,.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined:hover{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-outlined.is-loading.is-focused::after,.button.is-warning.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-outlined.is-loading:focus::after,.button.is-warning.is-outlined.is-loading:hover::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important}.button.is-warning.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-warning.is-inverted.is-outlined.is-focused,.button.is-warning.is-inverted.is-outlined.is-hovered,.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined:hover{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-inverted.is-outlined.is-loading:focus::after,.button.is-warning.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-danger{background-color:#ff3860;border-color:transparent;color:#fff}.button.is-danger.is-hovered,.button.is-danger:hover{background-color:#ff2b56;border-color:transparent;color:#fff}.button.is-danger.is-focused,.button.is-danger:focus{border-color:transparent;color:#fff}.button.is-danger.is-focused:not(:active),.button.is-danger:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.button.is-danger.is-active,.button.is-danger:active{background-color:#ff1f4b;border-color:transparent;color:#fff}.button.is-danger[disabled],fieldset[disabled] .button.is-danger{background-color:#ff3860;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted.is-hovered,.button.is-danger.is-inverted:hover{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled],fieldset[disabled] .button.is-danger.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#ff3860}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined{background-color:transparent;border-color:#ff3860;color:#ff3860}.button.is-danger.is-outlined.is-focused,.button.is-danger.is-outlined.is-hovered,.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined:hover{background-color:#ff3860;border-color:#ff3860;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #ff3860 #ff3860!important}.button.is-danger.is-outlined.is-loading.is-focused::after,.button.is-danger.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-outlined.is-loading:focus::after,.button.is-danger.is-outlined.is-loading:hover::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-outlined{background-color:transparent;border-color:#ff3860;box-shadow:none;color:#ff3860}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined.is-focused,.button.is-danger.is-inverted.is-outlined.is-hovered,.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined:hover{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted.is-outlined.is-loading.is-focused::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-inverted.is-outlined.is-loading:focus::after,.button.is-danger.is-inverted.is-outlined.is-loading:hover::after{border-color:transparent transparent #ff3860 #ff3860!important}.button.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-normal{font-size:1rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled],fieldset[disabled] .button{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent!important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em / 2));top:calc(50% - (1em / 2));position:absolute!important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child):not(.is-fullwidth){margin-right:.5rem}.buttons:last-child{margin-bottom:-.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large){border-radius:2px;font-size:.75rem}.buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large){font-size:1.25rem}.buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium){font-size:1.5rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button.is-hovered,.buttons.has-addons .button:hover{z-index:2}.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-focused,.buttons.has-addons .button.is-selected,.buttons.has-addons .button:active,.buttons.has-addons .button:focus{z-index:3}.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button.is-selected:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button:focus:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1;flex-shrink:1}.buttons.is-centered{justify-content:center}.buttons.is-centered:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.buttons.is-right{justify-content:flex-end}.buttons.is-right:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.container{flex-grow:1;margin:0 auto;position:relative;width:auto}@media screen and (min-width:1024px){.container{max-width:960px}.container.is-fluid{margin-left:32px;margin-right:32px;max-width:none}}@media screen and (max-width:1215px){.container.is-widescreen{max-width:1152px}}@media screen and (max-width:1407px){.container.is-fullhd{max-width:1344px}}@media screen and (min-width:1216px){.container{max-width:1152px}}@media screen and (min-width:1408px){.container{max-width:1344px}}.content li+li{margin-top:.25em}.content blockquote:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content p:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child),.content ul:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style-position:outside;margin-left:2em;margin-top:1em}.content ol:not([type]){list-style-type:decimal}.content ol:not([type]).is-lower-alpha{list-style-type:lower-alpha}.content ol:not([type]).is-lower-roman{list-style-type:lower-roman}.content ol:not([type]).is-upper-alpha{list-style-type:upper-alpha}.content ol:not([type]).is-upper-roman{list-style-type:upper-roman}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sub,.content sup{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636}.content table th:not([align]){text-align:left}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content .tabs li+li{margin-top:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.image{display:block;position:relative}.image img{display:block;height:auto;width:100%}.image img.is-rounded{border-radius:290486px}.image.is-16by9 .has-ratio,.image.is-16by9 img,.image.is-1by1 .has-ratio,.image.is-1by1 img,.image.is-1by2 .has-ratio,.image.is-1by2 img,.image.is-1by3 .has-ratio,.image.is-1by3 img,.image.is-2by1 .has-ratio,.image.is-2by1 img,.image.is-2by3 .has-ratio,.image.is-2by3 img,.image.is-3by1 .has-ratio,.image.is-3by1 img,.image.is-3by2 .has-ratio,.image.is-3by2 img,.image.is-3by4 .has-ratio,.image.is-3by4 img,.image.is-3by5 .has-ratio,.image.is-3by5 img,.image.is-4by3 .has-ratio,.image.is-4by3 img,.image.is-4by5 .has-ratio,.image.is-4by5 img,.image.is-5by3 .has-ratio,.image.is-5by3 img,.image.is-5by4 .has-ratio,.image.is-5by4 img,.image.is-9by16 .has-ratio,.image.is-9by16 img,.image.is-square .has-ratio,.image.is-square img{height:100%;width:100%}.image.is-1by1,.image.is-square{padding-top:100%}.image.is-5by4{padding-top:80%}.image.is-4by3{padding-top:75%}.image.is-3by2{padding-top:66.6666%}.image.is-5by3{padding-top:60%}.image.is-16by9{padding-top:56.25%}.image.is-2by1{padding-top:50%}.image.is-3by1{padding-top:33.3333%}.image.is-4by5{padding-top:125%}.image.is-3by4{padding-top:133.3333%}.image.is-2by3{padding-top:150%}.image.is-3by5{padding-top:166.6666%}.image.is-9by16{padding-top:177.7777%}.image.is-1by2{padding-top:200%}.image.is-1by3{padding-top:300%}.image.is-16x16{height:16px;width:16px}.image.is-24x24{height:24px;width:24px}.image.is-32x32{height:32px;width:32px}.image.is-48x48{height:48px;width:48px}.image.is-64x64{height:64px;width:64px}.image.is-96x96{height:96px;width:96px}.image.is-128x128{height:128px;width:128px}.notification{background-color:#f5f5f5;border-radius:4px;padding:1.25rem 2.5rem 1.25rem 1.5rem;position:relative}.notification a:not(.button):not(.dropdown-item){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code,.notification pre{background:#fff}.notification pre code{background:0 0}.notification>.delete{position:absolute;right:.5rem;top:.5rem}.notification .content,.notification .subtitle,.notification .title{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light{background-color:#f5f5f5;color:#363636}.notification.is-dark{background-color:#363636;color:#f5f5f5}.notification.is-primary{background-color:#00d1b2;color:#fff}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-info{background-color:#209cee;color:#fff}.notification.is-success{background-color:#23d160;color:#fff}.notification.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.notification.is-danger{background-color:#ff3860;color:#fff}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#dbdbdb}.progress::-webkit-progress-value{background-color:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-white:indeterminate{background-image:linear-gradient(to right,#fff 30%,#dbdbdb 30%)}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-black:indeterminate{background-image:linear-gradient(to right,#0a0a0a 30%,#dbdbdb 30%)}.progress.is-light::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill{background-color:#f5f5f5}.progress.is-light:indeterminate{background-image:linear-gradient(to right,#f5f5f5 30%,#dbdbdb 30%)}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-dark:indeterminate{background-image:linear-gradient(to right,#363636 30%,#dbdbdb 30%)}.progress.is-primary::-webkit-progress-value{background-color:#00d1b2}.progress.is-primary::-moz-progress-bar{background-color:#00d1b2}.progress.is-primary::-ms-fill{background-color:#00d1b2}.progress.is-primary:indeterminate{background-image:linear-gradient(to right,#00d1b2 30%,#dbdbdb 30%)}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-link:indeterminate{background-image:linear-gradient(to right,#3273dc 30%,#dbdbdb 30%)}.progress.is-info::-webkit-progress-value{background-color:#209cee}.progress.is-info::-moz-progress-bar{background-color:#209cee}.progress.is-info::-ms-fill{background-color:#209cee}.progress.is-info:indeterminate{background-image:linear-gradient(to right,#209cee 30%,#dbdbdb 30%)}.progress.is-success::-webkit-progress-value{background-color:#23d160}.progress.is-success::-moz-progress-bar{background-color:#23d160}.progress.is-success::-ms-fill{background-color:#23d160}.progress.is-success:indeterminate{background-image:linear-gradient(to right,#23d160 30%,#dbdbdb 30%)}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-warning:indeterminate{background-image:linear-gradient(to right,#ffdd57 30%,#dbdbdb 30%)}.progress.is-danger::-webkit-progress-value{background-color:#ff3860}.progress.is-danger::-moz-progress-bar{background-color:#ff3860}.progress.is-danger::-ms-fill{background-color:#ff3860}.progress.is-danger:indeterminate{background-image:linear-gradient(to right,#ff3860 30%,#dbdbdb 30%)}.progress:indeterminate{-webkit-animation-duration:1.5s;animation-duration:1.5s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:moveIndeterminate;animation-name:moveIndeterminate;-webkit-animation-timing-function:linear;animation-timing-function:linear;background-color:#dbdbdb;background-image:linear-gradient(to right,#4a4a4a 30%,#dbdbdb 30%);background-position:top left;background-repeat:no-repeat;background-size:150% 150%}.progress:indeterminate::-webkit-progress-bar{background-color:transparent}.progress:indeterminate::-moz-progress-bar{background-color:transparent}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}@-webkit-keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}@keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#f5f5f5}.table td.is-primary,.table th.is-primary{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#209cee;border-color:#209cee;color:#fff}.table td.is-success,.table th.is-success{background-color:#23d160;border-color:#23d160;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.table td.is-danger,.table th.is-danger{background-color:#ff3860;border-color:#ff3860;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#00d1b2;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table th{color:#363636}.table th:not([align]){text-align:left}.table tr.is-selected{background-color:#00d1b2;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead{background-color:transparent}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot{background-color:transparent}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody{background-color:transparent}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(even){background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.are-medium .tag:not(.is-normal):not(.is-large){font-size:1rem}.tags.are-large .tag:not(.is-normal):not(.is-medium){font-size:1.25rem}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.tags.has-addons .tag:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#4a4a4a;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light{background-color:#f5f5f5;color:#363636}.tag:not(body).is-dark{background-color:#363636;color:#f5f5f5}.tag:not(body).is-primary{background-color:#00d1b2;color:#fff}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-info{background-color:#209cee;color:#fff}.tag:not(body).is-success{background-color:#23d160;color:#fff}.tag:not(body).is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.tag:not(body).is-danger{background-color:#ff3860;color:#fff}.tag:not(body).is-normal{font-size:.75rem}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-.375em;margin-right:-.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::after,.tag:not(body).is-delete::before{background-color:currentColor;content:"";display:block;left:50%;position:absolute;top:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);-webkit-transform-origin:center center;transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:focus,.tag:not(body).is-delete:hover{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.subtitle,.title{word-break:break-word}.subtitle em,.subtitle span,.title em,.title span{font-weight:inherit}.subtitle sub,.title sub{font-size:.75em}.subtitle sup,.title sup{font-size:.75em}.subtitle .tag,.title .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.input,.select select,.textarea{background-color:#fff;border-color:#dbdbdb;border-radius:4px;color:#363636}.input::-moz-placeholder,.select select::-moz-placeholder,.textarea::-moz-placeholder{color:rgba(54,54,54,.3)}.input::-webkit-input-placeholder,.select select::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:rgba(54,54,54,.3)}.input:-moz-placeholder,.select select:-moz-placeholder,.textarea:-moz-placeholder{color:rgba(54,54,54,.3)}.input:-ms-input-placeholder,.select select:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:rgba(54,54,54,.3)}.input:hover,.is-hovered.input,.is-hovered.textarea,.select select.is-hovered,.select select:hover,.textarea:hover{border-color:#b5b5b5}.input:active,.input:focus,.is-active.input,.is-active.textarea,.is-focused.input,.is-focused.textarea,.select select.is-active,.select select.is-focused,.select select:active,.select select:focus,.textarea:active,.textarea:focus{border-color:#3273dc;box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.input[disabled]::-moz-placeholder,.select fieldset[disabled] select::-moz-placeholder,.select select[disabled]::-moz-placeholder,.textarea[disabled]::-moz-placeholder,fieldset[disabled] .input::-moz-placeholder,fieldset[disabled] .select select::-moz-placeholder,fieldset[disabled] .textarea::-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]::-webkit-input-placeholder,.select fieldset[disabled] select::-webkit-input-placeholder,.select select[disabled]::-webkit-input-placeholder,.textarea[disabled]::-webkit-input-placeholder,fieldset[disabled] .input::-webkit-input-placeholder,fieldset[disabled] .select select::-webkit-input-placeholder,fieldset[disabled] .textarea::-webkit-input-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-moz-placeholder,.select fieldset[disabled] select:-moz-placeholder,.select select[disabled]:-moz-placeholder,.textarea[disabled]:-moz-placeholder,fieldset[disabled] .input:-moz-placeholder,fieldset[disabled] .select select:-moz-placeholder,fieldset[disabled] .textarea:-moz-placeholder{color:rgba(122,122,122,.3)}.input[disabled]:-ms-input-placeholder,.select fieldset[disabled] select:-ms-input-placeholder,.select select[disabled]:-ms-input-placeholder,.textarea[disabled]:-ms-input-placeholder,fieldset[disabled] .input:-ms-input-placeholder,fieldset[disabled] .select select:-ms-input-placeholder,fieldset[disabled] .textarea:-ms-input-placeholder{color:rgba(122,122,122,.3)}.input,.textarea{box-shadow:inset 0 1px 2px rgba(10,10,10,.1);max-width:100%;width:100%}.input[readonly],.textarea[readonly]{box-shadow:none}.is-white.input,.is-white.textarea{border-color:#fff}.is-white.input:active,.is-white.input:focus,.is-white.is-active.input,.is-white.is-active.textarea,.is-white.is-focused.input,.is-white.is-focused.textarea,.is-white.textarea:active,.is-white.textarea:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.is-black.input,.is-black.textarea{border-color:#0a0a0a}.is-black.input:active,.is-black.input:focus,.is-black.is-active.input,.is-black.is-active.textarea,.is-black.is-focused.input,.is-black.is-focused.textarea,.is-black.textarea:active,.is-black.textarea:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.is-light.input,.is-light.textarea{border-color:#f5f5f5}.is-light.input:active,.is-light.input:focus,.is-light.is-active.input,.is-light.is-active.textarea,.is-light.is-focused.input,.is-light.is-focused.textarea,.is-light.textarea:active,.is-light.textarea:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.is-dark.input,.is-dark.textarea{border-color:#363636}.is-dark.input:active,.is-dark.input:focus,.is-dark.is-active.input,.is-dark.is-active.textarea,.is-dark.is-focused.input,.is-dark.is-focused.textarea,.is-dark.textarea:active,.is-dark.textarea:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.is-primary.input,.is-primary.textarea{border-color:#00d1b2}.is-primary.input:active,.is-primary.input:focus,.is-primary.is-active.input,.is-primary.is-active.textarea,.is-primary.is-focused.input,.is-primary.is-focused.textarea,.is-primary.textarea:active,.is-primary.textarea:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.is-link.input,.is-link.textarea{border-color:#3273dc}.is-link.input:active,.is-link.input:focus,.is-link.is-active.input,.is-link.is-active.textarea,.is-link.is-focused.input,.is-link.is-focused.textarea,.is-link.textarea:active,.is-link.textarea:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.is-info.input,.is-info.textarea{border-color:#209cee}.is-info.input:active,.is-info.input:focus,.is-info.is-active.input,.is-info.is-active.textarea,.is-info.is-focused.input,.is-info.is-focused.textarea,.is-info.textarea:active,.is-info.textarea:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.is-success.input,.is-success.textarea{border-color:#23d160}.is-success.input:active,.is-success.input:focus,.is-success.is-active.input,.is-success.is-active.textarea,.is-success.is-focused.input,.is-success.is-focused.textarea,.is-success.textarea:active,.is-success.textarea:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.is-warning.input,.is-warning.textarea{border-color:#ffdd57}.is-warning.input:active,.is-warning.input:focus,.is-warning.is-active.input,.is-warning.is-active.textarea,.is-warning.is-focused.input,.is-warning.is-focused.textarea,.is-warning.textarea:active,.is-warning.textarea:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.is-danger.input,.is-danger.textarea{border-color:#ff3860}.is-danger.input:active,.is-danger.input:focus,.is-danger.is-active.input,.is-danger.is-active.textarea,.is-danger.is-focused.input,.is-danger.is-focused.textarea,.is-danger.textarea:active,.is-danger.textarea:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.is-small.input,.is-small.textarea{border-radius:2px;font-size:.75rem}.is-medium.input,.is-medium.textarea{font-size:1.25rem}.is-large.input,.is-large.textarea{font-size:1.5rem}.is-fullwidth.input,.is-fullwidth.textarea{display:block;width:100%}.is-inline.input,.is-inline.textarea{display:inline;width:auto}.input.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.input.is-static{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.textarea{display:block;max-width:100%;min-width:100%;padding:.625em;resize:vertical}.textarea:not([rows]){max-height:600px;min-height:120px}.textarea[rows]{height:initial}.textarea.has-fixed-size{resize:none}.checkbox,.radio{cursor:pointer;display:inline-block;line-height:1.25;position:relative}.checkbox input,.radio input{cursor:pointer}.checkbox:hover,.radio:hover{color:#363636}.checkbox[disabled],.radio[disabled],fieldset[disabled] .checkbox,fieldset[disabled] .radio{color:#7a7a7a;cursor:not-allowed}.radio+.radio{margin-left:.5em}.select{display:inline-block;max-width:100%;position:relative;vertical-align:top}.select:not(.is-multiple){height:2.25em}.select:not(.is-multiple):not(.is-loading)::after{border-color:#3273dc;right:1.125em;z-index:4}.select.is-rounded select{border-radius:290486px;padding-left:1em}.select select{cursor:pointer;display:block;font-size:1em;max-width:100%;outline:0}.select select::-ms-expand{display:none}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#f5f5f5}.select select:not([multiple]){padding-right:2.5em}.select select[multiple]{height:auto;padding:0}.select select[multiple] option{padding:.5em 1em}.select:not(.is-multiple):not(.is-loading):hover::after{border-color:#363636}.select.is-white:not(:hover)::after{border-color:#fff}.select.is-white select{border-color:#fff}.select.is-white select.is-hovered,.select.is-white select:hover{border-color:#f2f2f2}.select.is-white select.is-active,.select.is-white select.is-focused,.select.is-white select:active,.select.is-white select:focus{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.select.is-black:not(:hover)::after{border-color:#0a0a0a}.select.is-black select{border-color:#0a0a0a}.select.is-black select.is-hovered,.select.is-black select:hover{border-color:#000}.select.is-black select.is-active,.select.is-black select.is-focused,.select.is-black select:active,.select.is-black select:focus{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.select.is-light:not(:hover)::after{border-color:#f5f5f5}.select.is-light select{border-color:#f5f5f5}.select.is-light select.is-hovered,.select.is-light select:hover{border-color:#e8e8e8}.select.is-light select.is-active,.select.is-light select.is-focused,.select.is-light select:active,.select.is-light select:focus{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.select.is-dark:not(:hover)::after{border-color:#363636}.select.is-dark select{border-color:#363636}.select.is-dark select.is-hovered,.select.is-dark select:hover{border-color:#292929}.select.is-dark select.is-active,.select.is-dark select.is-focused,.select.is-dark select:active,.select.is-dark select:focus{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.select.is-primary:not(:hover)::after{border-color:#00d1b2}.select.is-primary select{border-color:#00d1b2}.select.is-primary select.is-hovered,.select.is-primary select:hover{border-color:#00b89c}.select.is-primary select.is-active,.select.is-primary select.is-focused,.select.is-primary select:active,.select.is-primary select:focus{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.select.is-link:not(:hover)::after{border-color:#3273dc}.select.is-link select{border-color:#3273dc}.select.is-link select.is-hovered,.select.is-link select:hover{border-color:#2366d1}.select.is-link select.is-active,.select.is-link select.is-focused,.select.is-link select:active,.select.is-link select:focus{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select.is-info:not(:hover)::after{border-color:#209cee}.select.is-info select{border-color:#209cee}.select.is-info select.is-hovered,.select.is-info select:hover{border-color:#118fe4}.select.is-info select.is-active,.select.is-info select.is-focused,.select.is-info select:active,.select.is-info select:focus{box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.select.is-success:not(:hover)::after{border-color:#23d160}.select.is-success select{border-color:#23d160}.select.is-success select.is-hovered,.select.is-success select:hover{border-color:#20bc56}.select.is-success select.is-active,.select.is-success select.is-focused,.select.is-success select:active,.select.is-success select:focus{box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.select.is-warning:not(:hover)::after{border-color:#ffdd57}.select.is-warning select{border-color:#ffdd57}.select.is-warning select.is-hovered,.select.is-warning select:hover{border-color:#ffd83d}.select.is-warning select.is-active,.select.is-warning select.is-focused,.select.is-warning select:active,.select.is-warning select:focus{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.select.is-danger:not(:hover)::after{border-color:#ff3860}.select.is-danger select{border-color:#ff3860}.select.is-danger select.is-hovered,.select.is-danger select:hover{border-color:#ff1f4b}.select.is-danger select.is-active,.select.is-danger select.is-focused,.select.is-danger select:active,.select.is-danger select:focus{box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.select.is-small{border-radius:2px;font-size:.75rem}.select.is-medium{font-size:1.25rem}.select.is-large{font-size:1.5rem}.select.is-disabled::after{border-color:#7a7a7a}.select.is-fullwidth{width:100%}.select.is-fullwidth select{width:100%}.select.is-loading::after{margin-top:0;position:absolute;right:.625em;top:.625em;-webkit-transform:none;transform:none}.select.is-loading.is-small:after{font-size:.75rem}.select.is-loading.is-medium:after{font-size:1.25rem}.select.is-loading.is-large:after{font-size:1.5rem}.file{align-items:stretch;display:flex;justify-content:flex-start;position:relative}.file.is-white .file-cta{background-color:#fff;border-color:transparent;color:#0a0a0a}.file.is-white.is-hovered .file-cta,.file.is-white:hover .file-cta{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.file.is-white.is-focused .file-cta,.file.is-white:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,255,255,.25);color:#0a0a0a}.file.is-white.is-active .file-cta,.file.is-white:active .file-cta{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.file.is-black .file-cta{background-color:#0a0a0a;border-color:transparent;color:#fff}.file.is-black.is-hovered .file-cta,.file.is-black:hover .file-cta{background-color:#040404;border-color:transparent;color:#fff}.file.is-black.is-focused .file-cta,.file.is-black:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(10,10,10,.25);color:#fff}.file.is-black.is-active .file-cta,.file.is-black:active .file-cta{background-color:#000;border-color:transparent;color:#fff}.file.is-light .file-cta{background-color:#f5f5f5;border-color:transparent;color:#363636}.file.is-light.is-hovered .file-cta,.file.is-light:hover .file-cta{background-color:#eee;border-color:transparent;color:#363636}.file.is-light.is-focused .file-cta,.file.is-light:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(245,245,245,.25);color:#363636}.file.is-light.is-active .file-cta,.file.is-light:active .file-cta{background-color:#e8e8e8;border-color:transparent;color:#363636}.file.is-dark .file-cta{background-color:#363636;border-color:transparent;color:#f5f5f5}.file.is-dark.is-hovered .file-cta,.file.is-dark:hover .file-cta{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.file.is-dark.is-focused .file-cta,.file.is-dark:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(54,54,54,.25);color:#f5f5f5}.file.is-dark.is-active .file-cta,.file.is-dark:active .file-cta{background-color:#292929;border-color:transparent;color:#f5f5f5}.file.is-primary .file-cta{background-color:#00d1b2;border-color:transparent;color:#fff}.file.is-primary.is-hovered .file-cta,.file.is-primary:hover .file-cta{background-color:#00c4a7;border-color:transparent;color:#fff}.file.is-primary.is-focused .file-cta,.file.is-primary:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(0,209,178,.25);color:#fff}.file.is-primary.is-active .file-cta,.file.is-primary:active .file-cta{background-color:#00b89c;border-color:transparent;color:#fff}.file.is-link .file-cta{background-color:#3273dc;border-color:transparent;color:#fff}.file.is-link.is-hovered .file-cta,.file.is-link:hover .file-cta{background-color:#276cda;border-color:transparent;color:#fff}.file.is-link.is-focused .file-cta,.file.is-link:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(50,115,220,.25);color:#fff}.file.is-link.is-active .file-cta,.file.is-link:active .file-cta{background-color:#2366d1;border-color:transparent;color:#fff}.file.is-info .file-cta{background-color:#209cee;border-color:transparent;color:#fff}.file.is-info.is-hovered .file-cta,.file.is-info:hover .file-cta{background-color:#1496ed;border-color:transparent;color:#fff}.file.is-info.is-focused .file-cta,.file.is-info:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(32,156,238,.25);color:#fff}.file.is-info.is-active .file-cta,.file.is-info:active .file-cta{background-color:#118fe4;border-color:transparent;color:#fff}.file.is-success .file-cta{background-color:#23d160;border-color:transparent;color:#fff}.file.is-success.is-hovered .file-cta,.file.is-success:hover .file-cta{background-color:#22c65b;border-color:transparent;color:#fff}.file.is-success.is-focused .file-cta,.file.is-success:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(35,209,96,.25);color:#fff}.file.is-success.is-active .file-cta,.file.is-success:active .file-cta{background-color:#20bc56;border-color:transparent;color:#fff}.file.is-warning .file-cta{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-hovered .file-cta,.file.is-warning:hover .file-cta{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning.is-focused .file-cta,.file.is-warning:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,221,87,.25);color:rgba(0,0,0,.7)}.file.is-warning.is-active .file-cta,.file.is-warning:active .file-cta{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-danger .file-cta{background-color:#ff3860;border-color:transparent;color:#fff}.file.is-danger.is-hovered .file-cta,.file.is-danger:hover .file-cta{background-color:#ff2b56;border-color:transparent;color:#fff}.file.is-danger.is-focused .file-cta,.file.is-danger:focus .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,56,96,.25);color:#fff}.file.is-danger.is-active .file-cta,.file.is-danger:active .file-cta{background-color:#ff1f4b;border-color:transparent;color:#fff}.file.is-small{font-size:.75rem}.file.is-medium{font-size:1.25rem}.file.is-medium .file-icon .fa{font-size:21px}.file.is-large{font-size:1.5rem}.file.is-large .file-icon .fa{font-size:28px}.file.has-name .file-cta{border-bottom-right-radius:0;border-top-right-radius:0}.file.has-name .file-name{border-bottom-left-radius:0;border-top-left-radius:0}.file.has-name.is-empty .file-cta{border-radius:4px}.file.has-name.is-empty .file-name{display:none}.file.is-boxed .file-label{flex-direction:column}.file.is-boxed .file-cta{flex-direction:column;height:auto;padding:1em 3em}.file.is-boxed .file-name{border-width:0 1px 1px}.file.is-boxed .file-icon{height:1.5em;width:1.5em}.file.is-boxed .file-icon .fa{font-size:21px}.file.is-boxed.is-small .file-icon .fa{font-size:14px}.file.is-boxed.is-medium .file-icon .fa{font-size:28px}.file.is-boxed.is-large .file-icon .fa{font-size:35px}.file.is-boxed.has-name .file-cta{border-radius:4px 4px 0 0}.file.is-boxed.has-name .file-name{border-radius:0 0 4px 4px;border-width:0 1px 1px}.file.is-centered{justify-content:center}.file.is-fullwidth .file-label{width:100%}.file.is-fullwidth .file-name{flex-grow:1;max-width:none}.file.is-right{justify-content:flex-end}.file.is-right .file-cta{border-radius:0 4px 4px 0}.file.is-right .file-name{border-radius:4px 0 0 4px;border-width:1px 0 1px 1px;order:-1}.file-label{align-items:stretch;display:flex;cursor:pointer;justify-content:flex-start;overflow:hidden;position:relative}.file-label:hover .file-cta{background-color:#eee;color:#363636}.file-label:hover .file-name{border-color:#d5d5d5}.file-label:active .file-cta{background-color:#e8e8e8;color:#363636}.file-label:active .file-name{border-color:#cfcfcf}.file-input{height:100%;left:0;opacity:0;outline:0;position:absolute;top:0;width:100%}.file-cta,.file-name{border-color:#dbdbdb;border-radius:4px;font-size:1em;padding-left:1em;padding-right:1em;white-space:nowrap}.file-cta{background-color:#f5f5f5;color:#4a4a4a}.file-name{border-color:#dbdbdb;border-style:solid;border-width:1px 1px 1px 0;display:block;max-width:16em;overflow:hidden;text-align:left;text-overflow:ellipsis}.file-icon{align-items:center;display:flex;height:1em;justify-content:center;margin-right:.5em;width:1em}.file-icon .fa{font-size:14px}.label{color:#363636;display:block;font-size:1rem;font-weight:700}.label:not(:last-child){margin-bottom:.5em}.label.is-small{font-size:.75rem}.label.is-medium{font-size:1.25rem}.label.is-large{font-size:1.5rem}.help{display:block;font-size:.75rem;margin-top:.25rem}.help.is-white{color:#fff}.help.is-black{color:#0a0a0a}.help.is-light{color:#f5f5f5}.help.is-dark{color:#363636}.help.is-primary{color:#00d1b2}.help.is-link{color:#3273dc}.help.is-info{color:#209cee}.help.is-success{color:#23d160}.help.is-warning{color:#ffdd57}.help.is-danger{color:#ff3860}.field:not(:last-child){margin-bottom:.75rem}.field.has-addons{display:flex;justify-content:flex-start}.field.has-addons .control:not(:last-child){margin-right:-1px}.field.has-addons .control:not(:first-child):not(:last-child) .button,.field.has-addons .control:not(:first-child):not(:last-child) .input,.field.has-addons .control:not(:first-child):not(:last-child) .select select{border-radius:0}.field.has-addons .control:first-child:not(:only-child) .button,.field.has-addons .control:first-child:not(:only-child) .input,.field.has-addons .control:first-child:not(:only-child) .select select{border-bottom-right-radius:0;border-top-right-radius:0}.field.has-addons .control:last-child:not(:only-child) .button,.field.has-addons .control:last-child:not(:only-child) .input,.field.has-addons .control:last-child:not(:only-child) .select select{border-bottom-left-radius:0;border-top-left-radius:0}.field.has-addons .control .button:not([disabled]).is-hovered,.field.has-addons .control .button:not([disabled]):hover,.field.has-addons .control .input:not([disabled]).is-hovered,.field.has-addons .control .input:not([disabled]):hover,.field.has-addons .control .select select:not([disabled]).is-hovered,.field.has-addons .control .select select:not([disabled]):hover{z-index:2}.field.has-addons .control .button:not([disabled]).is-active,.field.has-addons .control .button:not([disabled]).is-focused,.field.has-addons .control .button:not([disabled]):active,.field.has-addons .control .button:not([disabled]):focus,.field.has-addons .control .input:not([disabled]).is-active,.field.has-addons .control .input:not([disabled]).is-focused,.field.has-addons .control .input:not([disabled]):active,.field.has-addons .control .input:not([disabled]):focus,.field.has-addons .control .select select:not([disabled]).is-active,.field.has-addons .control .select select:not([disabled]).is-focused,.field.has-addons .control .select select:not([disabled]):active,.field.has-addons .control .select select:not([disabled]):focus{z-index:3}.field.has-addons .control .button:not([disabled]).is-active:hover,.field.has-addons .control .button:not([disabled]).is-focused:hover,.field.has-addons .control .button:not([disabled]):active:hover,.field.has-addons .control .button:not([disabled]):focus:hover,.field.has-addons .control .input:not([disabled]).is-active:hover,.field.has-addons .control .input:not([disabled]).is-focused:hover,.field.has-addons .control .input:not([disabled]):active:hover,.field.has-addons .control .input:not([disabled]):focus:hover,.field.has-addons .control .select select:not([disabled]).is-active:hover,.field.has-addons .control .select select:not([disabled]).is-focused:hover,.field.has-addons .control .select select:not([disabled]):active:hover,.field.has-addons .control .select select:not([disabled]):focus:hover{z-index:4}.field.has-addons .control.is-expanded{flex-grow:1;flex-shrink:1}.field.has-addons.has-addons-centered{justify-content:center}.field.has-addons.has-addons-right{justify-content:flex-end}.field.has-addons.has-addons-fullwidth .control{flex-grow:1;flex-shrink:0}.field.is-grouped{display:flex;justify-content:flex-start}.field.is-grouped>.control{flex-shrink:0}.field.is-grouped>.control:not(:last-child){margin-bottom:0;margin-right:.75rem}.field.is-grouped>.control.is-expanded{flex-grow:1;flex-shrink:1}.field.is-grouped.is-grouped-centered{justify-content:center}.field.is-grouped.is-grouped-right{justify-content:flex-end}.field.is-grouped.is-grouped-multiline{flex-wrap:wrap}.field.is-grouped.is-grouped-multiline>.control:last-child,.field.is-grouped.is-grouped-multiline>.control:not(:last-child){margin-bottom:.75rem}.field.is-grouped.is-grouped-multiline:last-child{margin-bottom:-.75rem}.field.is-grouped.is-grouped-multiline:not(:last-child){margin-bottom:0}@media screen and (min-width:769px),print{.field.is-horizontal{display:flex}}.field-label .label{font-size:inherit}@media screen and (max-width:768px){.field-label{margin-bottom:.5rem}}@media screen and (min-width:769px),print{.field-label{flex-basis:0;flex-grow:1;flex-shrink:0;margin-right:1.5rem;text-align:right}.field-label.is-small{font-size:.75rem;padding-top:.375em}.field-label.is-normal{padding-top:.375em}.field-label.is-medium{font-size:1.25rem;padding-top:.375em}.field-label.is-large{font-size:1.5rem;padding-top:.375em}}.field-body .field .field{margin-bottom:0}@media screen and (min-width:769px),print{.field-body{display:flex;flex-basis:0;flex-grow:5;flex-shrink:1}.field-body .field{margin-bottom:0}.field-body>.field{flex-shrink:1}.field-body>.field:not(.is-narrow){flex-grow:1}.field-body>.field:not(:last-child){margin-right:.75rem}}.control{box-sizing:border-box;clear:both;font-size:1rem;position:relative;text-align:left}.control.has-icons-left .input:focus~.icon,.control.has-icons-left .select:focus~.icon,.control.has-icons-right .input:focus~.icon,.control.has-icons-right .select:focus~.icon{color:#7a7a7a}.control.has-icons-left .input.is-small~.icon,.control.has-icons-left .select.is-small~.icon,.control.has-icons-right .input.is-small~.icon,.control.has-icons-right .select.is-small~.icon{font-size:.75rem}.control.has-icons-left .input.is-medium~.icon,.control.has-icons-left .select.is-medium~.icon,.control.has-icons-right .input.is-medium~.icon,.control.has-icons-right .select.is-medium~.icon{font-size:1.25rem}.control.has-icons-left .input.is-large~.icon,.control.has-icons-left .select.is-large~.icon,.control.has-icons-right .input.is-large~.icon,.control.has-icons-right .select.is-large~.icon{font-size:1.5rem}.control.has-icons-left .icon,.control.has-icons-right .icon{color:#dbdbdb;height:2.25em;pointer-events:none;position:absolute;top:0;width:2.25em;z-index:4}.control.has-icons-left .input,.control.has-icons-left .select select{padding-left:2.25em}.control.has-icons-left .icon.is-left{left:0}.control.has-icons-right .input,.control.has-icons-right .select select{padding-right:2.25em}.control.has-icons-right .icon.is-right{right:0}.control.is-loading::after{position:absolute!important;right:.625em;top:.625em;z-index:4}.control.is-loading.is-small:after{font-size:.75rem}.control.is-loading.is-medium:after{font-size:1.25rem}.control.is-loading.is-large:after{font-size:1.5rem}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:"\0002f"}.breadcrumb ol,.breadcrumb ul{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:"\02192"}.breadcrumb.has-bullet-separator li+li::before{content:"\02022"}.breadcrumb.has-dot-separator li+li::before{content:"\000b7"}.breadcrumb.has-succeeds-separator li+li::before{content:"\0227B"}.card{background-color:#fff;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;max-width:100%;position:relative}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 1px 2px rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem}.card-image{display:block;position:relative}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid #dbdbdb;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #dbdbdb}.card .media:not(:last-child){margin-bottom:1.5rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:left;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active,button.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#dbdbdb;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item:not(:last-child){margin-bottom:0;margin-right:.75rem}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width:769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .subtitle,.level-item .title{margin-bottom:0}@media screen and (max-width:768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width:769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width:768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width:769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width:769px),print{.level-right{display:flex}}.list{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1)}.list-item{display:block;padding:.5em 1em}.list-item:not(a){color:#4a4a4a}.list-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-item:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.list-item:not(:last-child){border-bottom:1px solid #dbdbdb}.list-item.is-active{background-color:#3273dc;color:#fff}a.list-item{background-color:#f5f5f5;cursor:pointer}.media{align-items:flex-start;display:flex;text-align:left}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:left}@media screen and (max-width:768px){.media-content{overflow-x:auto}}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#4a4a4a;display:block;padding:.5em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:1em}.menu-label:not(:last-child){margin-bottom:1em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff;color:#4d4d4d}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a;color:#090909}.message.is-light{background-color:#fafafa}.message.is-light .message-header{background-color:#f5f5f5;color:#363636}.message.is-light .message-body{border-color:#f5f5f5;color:#505050}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#f5f5f5}.message.is-dark .message-body{border-color:#363636;color:#2a2a2a}.message.is-primary{background-color:#f5fffd}.message.is-primary .message-header{background-color:#00d1b2;color:#fff}.message.is-primary .message-body{border-color:#00d1b2;color:#021310}.message.is-link{background-color:#f6f9fe}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#22509a}.message.is-info{background-color:#f6fbfe}.message.is-info .message-header{background-color:#209cee;color:#fff}.message.is-info .message-body{border-color:#209cee;color:#12537e}.message.is-success{background-color:#f6fef9}.message.is-success .message-header{background-color:#23d160;color:#fff}.message.is-success .message-body{border-color:#23d160;color:#0e301a}.message.is-warning{background-color:#fffdf5}.message.is-warning .message-header{background-color:#ffdd57;color:rgba(0,0,0,.7)}.message.is-warning .message-body{border-color:#ffdd57;color:#3b3108}.message.is-danger{background-color:#fff5f7}.message.is-danger .message-header{background-color:#ff3860;color:#fff}.message.is-danger .message-body{border-color:#ff3860;color:#cd0930}.message-header{align-items:center;background-color:#4a4a4a;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#4a4a4a;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;flex-direction:column;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-card,.modal-content{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width:769px),print{.modal-card,.modal-content{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:0 0;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden;-ms-overflow-y:visible}.modal-card-foot,.modal-card-head{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:.5em}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link,.navbar.is-white .navbar-brand>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link.is-active,.navbar.is-white .navbar-brand .navbar-link:focus,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand>a.navbar-item:focus,.navbar.is-white .navbar-brand>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width:1024px){.navbar.is-white .navbar-end .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-start>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link.is-active,.navbar.is-white .navbar-end .navbar-link:focus,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end>a.navbar-item:focus,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-start .navbar-link:focus,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start>a.navbar-item:focus,.navbar.is-white .navbar-start>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link::after,.navbar.is-white .navbar-start .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-white .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand .navbar-link,.navbar.is-black .navbar-brand>.navbar-item{color:#fff}.navbar.is-black .navbar-brand .navbar-link.is-active,.navbar.is-black .navbar-brand .navbar-link:focus,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand>a.navbar-item:focus,.navbar.is-black .navbar-brand>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-black .navbar-end .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-start>.navbar-item{color:#fff}.navbar.is-black .navbar-end .navbar-link.is-active,.navbar.is-black .navbar-end .navbar-link:focus,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end>a.navbar-item:focus,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-start .navbar-link:focus,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start>a.navbar-item:focus,.navbar.is-black .navbar-start>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-end .navbar-link::after,.navbar.is-black .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-black .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:#363636}.navbar.is-light .navbar-brand .navbar-link,.navbar.is-light .navbar-brand>.navbar-item{color:#363636}.navbar.is-light .navbar-brand .navbar-link.is-active,.navbar.is-light .navbar-brand .navbar-link:focus,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand>a.navbar-item:focus,.navbar.is-light .navbar-brand>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-brand .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-burger{color:#363636}@media screen and (min-width:1024px){.navbar.is-light .navbar-end .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-start>.navbar-item{color:#363636}.navbar.is-light .navbar-end .navbar-link.is-active,.navbar.is-light .navbar-end .navbar-link:focus,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end>a.navbar-item:focus,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-start .navbar-link:focus,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start>a.navbar-item:focus,.navbar.is-light .navbar-start>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-end .navbar-link::after,.navbar.is-light .navbar-start .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-light .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#363636}}.navbar.is-dark{background-color:#363636;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link,.navbar.is-dark .navbar-brand>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link.is-active,.navbar.is-dark .navbar-brand .navbar-link:focus,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand>a.navbar-item:focus,.navbar.is-dark .navbar-brand>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-burger{color:#f5f5f5}@media screen and (min-width:1024px){.navbar.is-dark .navbar-end .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-start>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link.is-active,.navbar.is-dark .navbar-end .navbar-link:focus,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end>a.navbar-item:focus,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-start .navbar-link:focus,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start>a.navbar-item:focus,.navbar.is-dark .navbar-start>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link::after,.navbar.is-dark .navbar-start .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#f5f5f5}}.navbar.is-primary{background-color:#00d1b2;color:#fff}.navbar.is-primary .navbar-brand .navbar-link,.navbar.is-primary .navbar-brand>.navbar-item{color:#fff}.navbar.is-primary .navbar-brand .navbar-link.is-active,.navbar.is-primary .navbar-brand .navbar-link:focus,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand>a.navbar-item:focus,.navbar.is-primary .navbar-brand>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-primary .navbar-end .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-start>.navbar-item{color:#fff}.navbar.is-primary .navbar-end .navbar-link.is-active,.navbar.is-primary .navbar-end .navbar-link:focus,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end>a.navbar-item:focus,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-start .navbar-link:focus,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start>a.navbar-item:focus,.navbar.is-primary .navbar-start>a.navbar-item:hover{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-end .navbar-link::after,.navbar.is-primary .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#00d1b2;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand .navbar-link,.navbar.is-link .navbar-brand>.navbar-item{color:#fff}.navbar.is-link .navbar-brand .navbar-link.is-active,.navbar.is-link .navbar-brand .navbar-link:focus,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand>a.navbar-item:focus,.navbar.is-link .navbar-brand>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-link .navbar-end .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-start>.navbar-item{color:#fff}.navbar.is-link .navbar-end .navbar-link.is-active,.navbar.is-link .navbar-end .navbar-link:focus,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end>a.navbar-item:focus,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-start .navbar-link:focus,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start>a.navbar-item:focus,.navbar.is-link .navbar-start>a.navbar-item:hover{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-end .navbar-link::after,.navbar.is-link .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-link .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#209cee;color:#fff}.navbar.is-info .navbar-brand .navbar-link,.navbar.is-info .navbar-brand>.navbar-item{color:#fff}.navbar.is-info .navbar-brand .navbar-link.is-active,.navbar.is-info .navbar-brand .navbar-link:focus,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand>a.navbar-item:focus,.navbar.is-info .navbar-brand>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-info .navbar-end .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-start>.navbar-item{color:#fff}.navbar.is-info .navbar-end .navbar-link.is-active,.navbar.is-info .navbar-end .navbar-link:focus,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end>a.navbar-item:focus,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-start .navbar-link:focus,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start>a.navbar-item:focus,.navbar.is-info .navbar-start>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-end .navbar-link::after,.navbar.is-info .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-info .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#209cee;color:#fff}}.navbar.is-success{background-color:#23d160;color:#fff}.navbar.is-success .navbar-brand .navbar-link,.navbar.is-success .navbar-brand>.navbar-item{color:#fff}.navbar.is-success .navbar-brand .navbar-link.is-active,.navbar.is-success .navbar-brand .navbar-link:focus,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand>a.navbar-item:focus,.navbar.is-success .navbar-brand>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-success .navbar-end .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-start>.navbar-item{color:#fff}.navbar.is-success .navbar-end .navbar-link.is-active,.navbar.is-success .navbar-end .navbar-link:focus,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end>a.navbar-item:focus,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-start .navbar-link:focus,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start>a.navbar-item:focus,.navbar.is-success .navbar-start>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-end .navbar-link::after,.navbar.is-success .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-success .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#23d160;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link,.navbar.is-warning .navbar-brand>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link.is-active,.navbar.is-warning .navbar-brand .navbar-link:focus,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand>a.navbar-item:focus,.navbar.is-warning .navbar-brand>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:1024px){.navbar.is-warning .navbar-end .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-start>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link.is-active,.navbar.is-warning .navbar-end .navbar-link:focus,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end>a.navbar-item:focus,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-start .navbar-link:focus,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start>a.navbar-item:focus,.navbar.is-warning .navbar-start>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link::after,.navbar.is-warning .navbar-start .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#ff3860;color:#fff}.navbar.is-danger .navbar-brand .navbar-link,.navbar.is-danger .navbar-brand>.navbar-item{color:#fff}.navbar.is-danger .navbar-brand .navbar-link.is-active,.navbar.is-danger .navbar-brand .navbar-link:focus,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand>a.navbar-item:focus,.navbar.is-danger .navbar-brand>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-danger .navbar-end .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-start>.navbar-item{color:#fff}.navbar.is-danger .navbar-end .navbar-link.is-active,.navbar.is-danger .navbar-end .navbar-link:focus,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end>a.navbar-item:focus,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-start .navbar-link:focus,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start>a.navbar-item:focus,.navbar.is-danger .navbar-start>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-end .navbar-link::after,.navbar.is-danger .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#ff3860;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px 0 0 #f5f5f5}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px 0 0 #f5f5f5}.navbar.is-fixed-top{top:0}body.has-navbar-fixed-top,html.has-navbar-fixed-top{padding-top:3.25rem}body.has-navbar-fixed-bottom,html.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:focus,.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#4a4a4a;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;-webkit-transform-origin:center;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,-webkit-transform;transition-property:background-color,opacity,transform;transition-property:background-color,opacity,transform,-webkit-transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,.05)}.navbar-burger.is-active span:nth-child(1){-webkit-transform:translateY(5px) rotate(45deg);transform:translateY(5px) rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){-webkit-transform:translateY(-5px) rotate(-45deg);transform:translateY(-5px) rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}.navbar-link,a.navbar-item{cursor:pointer}.navbar-link.is-active,.navbar-link:focus,.navbar-link:focus-within,.navbar-link:hover,a.navbar-item.is-active,a.navbar-item:focus,a.navbar-item:focus-within,a.navbar-item:hover{background-color:#fafafa;color:#3273dc}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:focus,.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#3273dc;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:1023px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}body.has-navbar-fixed-top-touch,html.has-navbar-fixed-top-touch{padding-top:3.25rem}body.has-navbar-fixed-bottom-touch,html.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:1024px){.navbar,.navbar-end,.navbar-menu,.navbar-start{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-end,.navbar.is-spaced .navbar-start{align-items:center}.navbar.is-spaced .navbar-link,.navbar.is-spaced a.navbar-item{border-radius:4px}.navbar.is-transparent .navbar-link.is-active,.navbar.is-transparent .navbar-link:focus,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent a.navbar-item:focus,.navbar.is-transparent a.navbar-item:hover{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus-within .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:focus,.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{-webkit-transform:rotate(135deg) translate(.25em,-.25em);transform:rotate(135deg) translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:focus .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:focus-within .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:focus .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown{opacity:1;pointer-events:auto;-webkit-transform:translateY(0);transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:focus,.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-dropdown{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));-webkit-transform:translateY(-5px);transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.container>.navbar .navbar-brand,.navbar>.container .navbar-brand{margin-left:-.75rem}.container>.navbar .navbar-menu,.navbar>.container .navbar-menu{margin-right:-.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}body.has-navbar-fixed-top-desktop,html.has-navbar-fixed-top-desktop{padding-top:3.25rem}body.has-navbar-fixed-bottom-desktop,html.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}body.has-spaced-navbar-fixed-top,html.has-spaced-navbar-fixed-top{padding-top:5.25rem}body.has-spaced-navbar-fixed-bottom,html.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}.navbar-link.is-active,a.navbar-item.is-active{color:#0a0a0a}.navbar-link.is-active:not(:focus):not(:hover),a.navbar-item.is-active:not(:focus):not(:hover){background-color:transparent}.navbar-item.has-dropdown.is-active .navbar-link,.navbar-item.has-dropdown:focus .navbar-link,.navbar-item.has-dropdown:hover .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.pagination{font-size:1rem;margin:-.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-next,.pagination.is-rounded .pagination-previous{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-ellipsis,.pagination-link,.pagination-next,.pagination-previous{font-size:1em;justify-content:center;margin:.25rem;padding-left:.5em;padding-right:.5em;text-align:center}.pagination-link,.pagination-next,.pagination-previous{border-color:#dbdbdb;color:#363636;min-width:2.25em}.pagination-link:hover,.pagination-next:hover,.pagination-previous:hover{border-color:#b5b5b5;color:#363636}.pagination-link:focus,.pagination-next:focus,.pagination-previous:focus{border-color:#3273dc}.pagination-link:active,.pagination-next:active,.pagination-previous:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-link[disabled],.pagination-next[disabled],.pagination-previous[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-next,.pagination-previous{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width:768px){.pagination{flex-wrap:wrap}.pagination-next,.pagination-previous{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width:769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel-block,.panel-heading,.panel-tabs{border-bottom:1px solid #dbdbdb;border-left:1px solid #dbdbdb;border-right:1px solid #dbdbdb}.panel-block:first-child,.panel-heading:first-child,.panel-tabs:first-child{border-top:1px solid #dbdbdb}.panel-heading{background-color:#f5f5f5;border-radius:4px 4px 0 0;color:#363636;font-size:1.25em;font-weight:300;line-height:1.25;padding:.5em .75em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#4a4a4a}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#4a4a4a;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent!important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-radius:4px 0 0 4px}.tabs.is-toggle li:last-child a{border-radius:0 4px 4px 0}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-0{flex:none;width:0%}.columns.is-mobile>.column.is-offset-0{margin-left:0}.columns.is-mobile>.column.is-1{flex:none;width:8.33333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-0-mobile{flex:none;width:0%}.column.is-offset-0-mobile{margin-left:0}.column.is-1-mobile{flex:none;width:8.33333%}.column.is-offset-1-mobile{margin-left:8.33333%}.column.is-2-mobile{flex:none;width:16.66667%}.column.is-offset-2-mobile{margin-left:16.66667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333%}.column.is-offset-4-mobile{margin-left:33.33333%}.column.is-5-mobile{flex:none;width:41.66667%}.column.is-offset-5-mobile{margin-left:41.66667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333%}.column.is-offset-7-mobile{margin-left:58.33333%}.column.is-8-mobile{flex:none;width:66.66667%}.column.is-offset-8-mobile{margin-left:66.66667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333%}.column.is-offset-10-mobile{margin-left:83.33333%}.column.is-11-mobile{flex:none;width:91.66667%}.column.is-offset-11-mobile{margin-left:91.66667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-0,.column.is-0-tablet{flex:none;width:0%}.column.is-offset-0,.column.is-offset-0-tablet{margin-left:0}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:1023px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-0-touch{flex:none;width:0%}.column.is-offset-0-touch{margin-left:0}.column.is-1-touch{flex:none;width:8.33333%}.column.is-offset-1-touch{margin-left:8.33333%}.column.is-2-touch{flex:none;width:16.66667%}.column.is-offset-2-touch{margin-left:16.66667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333%}.column.is-offset-4-touch{margin-left:33.33333%}.column.is-5-touch{flex:none;width:41.66667%}.column.is-offset-5-touch{margin-left:41.66667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333%}.column.is-offset-7-touch{margin-left:58.33333%}.column.is-8-touch{flex:none;width:66.66667%}.column.is-offset-8-touch{margin-left:66.66667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333%}.column.is-offset-10-touch{margin-left:83.33333%}.column.is-11-touch{flex:none;width:91.66667%}.column.is-offset-11-touch{margin-left:91.66667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:1024px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-0-desktop{flex:none;width:0%}.column.is-offset-0-desktop{margin-left:0}.column.is-1-desktop{flex:none;width:8.33333%}.column.is-offset-1-desktop{margin-left:8.33333%}.column.is-2-desktop{flex:none;width:16.66667%}.column.is-offset-2-desktop{margin-left:16.66667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333%}.column.is-offset-4-desktop{margin-left:33.33333%}.column.is-5-desktop{flex:none;width:41.66667%}.column.is-offset-5-desktop{margin-left:41.66667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333%}.column.is-offset-7-desktop{margin-left:58.33333%}.column.is-8-desktop{flex:none;width:66.66667%}.column.is-offset-8-desktop{margin-left:66.66667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333%}.column.is-offset-10-desktop{margin-left:83.33333%}.column.is-11-desktop{flex:none;width:91.66667%}.column.is-offset-11-desktop{margin-left:91.66667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1216px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-0-widescreen{flex:none;width:0%}.column.is-offset-0-widescreen{margin-left:0}.column.is-1-widescreen{flex:none;width:8.33333%}.column.is-offset-1-widescreen{margin-left:8.33333%}.column.is-2-widescreen{flex:none;width:16.66667%}.column.is-offset-2-widescreen{margin-left:16.66667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333%}.column.is-offset-4-widescreen{margin-left:33.33333%}.column.is-5-widescreen{flex:none;width:41.66667%}.column.is-offset-5-widescreen{margin-left:41.66667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333%}.column.is-offset-7-widescreen{margin-left:58.33333%}.column.is-8-widescreen{flex:none;width:66.66667%}.column.is-offset-8-widescreen{margin-left:66.66667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333%}.column.is-offset-10-widescreen{margin-left:83.33333%}.column.is-11-widescreen{flex:none;width:91.66667%}.column.is-offset-11-widescreen{margin-left:91.66667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1408px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-0-fullhd{flex:none;width:0%}.column.is-offset-0-fullhd{margin-left:0}.column.is-1-fullhd{flex:none;width:8.33333%}.column.is-offset-1-fullhd{margin-left:8.33333%}.column.is-2-fullhd{flex:none;width:16.66667%}.column.is-offset-2-fullhd{margin-left:16.66667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333%}.column.is-offset-4-fullhd{margin-left:33.33333%}.column.is-5-fullhd{flex:none;width:41.66667%}.column.is-offset-5-fullhd{margin-left:41.66667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333%}.column.is-offset-7-fullhd{margin-left:58.33333%}.column.is-8-fullhd{flex:none;width:66.66667%}.column.is-offset-8-fullhd{margin-left:66.66667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333%}.column.is-offset-10-fullhd{margin-left:83.33333%}.column.is-11-fullhd{flex:none;width:91.66667%}.column.is-offset-11-fullhd{margin-left:91.66667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:1024px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}@media screen and (max-width:768px){.columns.is-variable.is-0-mobile{--columnGap:0rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-0-tablet{--columnGap:0rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-0-tablet-only{--columnGap:0rem}}@media screen and (max-width:1023px){.columns.is-variable.is-0-touch{--columnGap:0rem}}@media screen and (min-width:1024px){.columns.is-variable.is-0-desktop{--columnGap:0rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-0-desktop-only{--columnGap:0rem}}@media screen and (min-width:1216px){.columns.is-variable.is-0-widescreen{--columnGap:0rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-0-widescreen-only{--columnGap:0rem}}@media screen and (min-width:1408px){.columns.is-variable.is-0-fullhd{--columnGap:0rem}}.columns.is-variable.is-1{--columnGap:0.25rem}@media screen and (max-width:768px){.columns.is-variable.is-1-mobile{--columnGap:0.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-1-tablet{--columnGap:0.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-1-tablet-only{--columnGap:0.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-1-touch{--columnGap:0.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-1-desktop{--columnGap:0.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-1-desktop-only{--columnGap:0.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-1-widescreen{--columnGap:0.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-1-widescreen-only{--columnGap:0.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-1-fullhd{--columnGap:0.25rem}}.columns.is-variable.is-2{--columnGap:0.5rem}@media screen and (max-width:768px){.columns.is-variable.is-2-mobile{--columnGap:0.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-2-tablet{--columnGap:0.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-2-tablet-only{--columnGap:0.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-2-touch{--columnGap:0.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-2-desktop{--columnGap:0.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-2-desktop-only{--columnGap:0.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-2-widescreen{--columnGap:0.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-2-widescreen-only{--columnGap:0.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-2-fullhd{--columnGap:0.5rem}}.columns.is-variable.is-3{--columnGap:0.75rem}@media screen and (max-width:768px){.columns.is-variable.is-3-mobile{--columnGap:0.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-3-tablet{--columnGap:0.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-3-tablet-only{--columnGap:0.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-3-touch{--columnGap:0.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-3-desktop{--columnGap:0.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-3-desktop-only{--columnGap:0.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-3-widescreen{--columnGap:0.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-3-widescreen-only{--columnGap:0.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-3-fullhd{--columnGap:0.75rem}}.columns.is-variable.is-4{--columnGap:1rem}@media screen and (max-width:768px){.columns.is-variable.is-4-mobile{--columnGap:1rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-4-tablet{--columnGap:1rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-4-tablet-only{--columnGap:1rem}}@media screen and (max-width:1023px){.columns.is-variable.is-4-touch{--columnGap:1rem}}@media screen and (min-width:1024px){.columns.is-variable.is-4-desktop{--columnGap:1rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-4-desktop-only{--columnGap:1rem}}@media screen and (min-width:1216px){.columns.is-variable.is-4-widescreen{--columnGap:1rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-4-widescreen-only{--columnGap:1rem}}@media screen and (min-width:1408px){.columns.is-variable.is-4-fullhd{--columnGap:1rem}}.columns.is-variable.is-5{--columnGap:1.25rem}@media screen and (max-width:768px){.columns.is-variable.is-5-mobile{--columnGap:1.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-5-tablet{--columnGap:1.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-5-tablet-only{--columnGap:1.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-5-touch{--columnGap:1.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-5-desktop{--columnGap:1.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-5-desktop-only{--columnGap:1.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-5-widescreen{--columnGap:1.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-5-widescreen-only{--columnGap:1.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-5-fullhd{--columnGap:1.25rem}}.columns.is-variable.is-6{--columnGap:1.5rem}@media screen and (max-width:768px){.columns.is-variable.is-6-mobile{--columnGap:1.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-6-tablet{--columnGap:1.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-6-tablet-only{--columnGap:1.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-6-touch{--columnGap:1.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-6-desktop{--columnGap:1.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-6-desktop-only{--columnGap:1.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-6-widescreen{--columnGap:1.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-6-widescreen-only{--columnGap:1.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-6-fullhd{--columnGap:1.5rem}}.columns.is-variable.is-7{--columnGap:1.75rem}@media screen and (max-width:768px){.columns.is-variable.is-7-mobile{--columnGap:1.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-7-tablet{--columnGap:1.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-7-tablet-only{--columnGap:1.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-7-touch{--columnGap:1.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-7-desktop{--columnGap:1.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-7-desktop-only{--columnGap:1.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-7-widescreen{--columnGap:1.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-7-widescreen-only{--columnGap:1.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-7-fullhd{--columnGap:1.75rem}}.columns.is-variable.is-8{--columnGap:2rem}@media screen and (max-width:768px){.columns.is-variable.is-8-mobile{--columnGap:2rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-8-tablet{--columnGap:2rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-8-tablet-only{--columnGap:2rem}}@media screen and (max-width:1023px){.columns.is-variable.is-8-touch{--columnGap:2rem}}@media screen and (min-width:1024px){.columns.is-variable.is-8-desktop{--columnGap:2rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-8-desktop-only{--columnGap:2rem}}@media screen and (min-width:1216px){.columns.is-variable.is-8-widescreen{--columnGap:2rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-8-widescreen-only{--columnGap:2rem}}@media screen and (min-width:1408px){.columns.is-variable.is-8-fullhd{--columnGap:2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:-webkit-min-content;min-height:-moz-min-content;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333%}.tile.is-2{flex:none;width:16.66667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333%}.tile.is-5{flex:none;width:41.66667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333%}.tile.is-8{flex:none;width:66.66667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333%}.tile.is-11{flex:none;width:91.66667%}.tile.is-12{flex:none;width:100%}}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:0 0}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:1023px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white .navbar-link.is-active,.hero.is-white .navbar-link:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black .navbar-link.is-active,.hero.is-black .navbar-link:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black a.navbar-item:hover{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:#363636}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-light strong{color:inherit}.hero.is-light .title{color:#363636}.hero.is-light .subtitle{color:rgba(54,54,54,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:#363636}@media screen and (max-width:1023px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(54,54,54,.7)}.hero.is-light .navbar-link.is-active,.hero.is-light .navbar-link:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.hero.is-light .tabs a{color:#363636;opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:#363636}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}}.hero.is-dark{background-color:#363636;color:#f5f5f5}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#f5f5f5}.hero.is-dark .subtitle{color:rgba(245,245,245,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#f5f5f5}@media screen and (max-width:1023px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(245,245,245,.7)}.hero.is-dark .navbar-link.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.hero.is-dark .tabs a{color:#f5f5f5;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#f5f5f5}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}}.hero.is-primary{background-color:#00d1b2;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-primary .navbar-menu{background-color:#00d1b2}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary .navbar-link.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary a.navbar-item:hover{background-color:#00b89c;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#00d1b2}.hero.is-primary.is-bold{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#009e6c 0,#00d1b2 71%,#00e7eb 100%)}}.hero.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link .navbar-link.is-active,.hero.is-link .navbar-link:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link a.navbar-item:hover{background-color:#2366d1;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1577c6 0,#3273dc 71%,#4366e5 100%)}}.hero.is-info{background-color:#209cee;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-info .navbar-menu{background-color:#209cee}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info .navbar-link.is-active,.hero.is-info .navbar-link:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info a.navbar-item:hover{background-color:#118fe4;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#209cee}.hero.is-info.is-bold{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}}.hero.is-success{background-color:#23d160;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-success .navbar-menu{background-color:#23d160}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success .navbar-link.is-active,.hero.is-success .navbar-link:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success a.navbar-item:hover{background-color:#20bc56;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#23d160}.hero.is-success.is-bold{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}}.hero.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1023px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning .navbar-link.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger{background-color:#ff3860;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-danger .navbar-menu{background-color:#ff3860}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger .navbar-link.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#ff3860}.hero.is-danger.is-bold{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}}.hero.is-small .hero-body{padding-bottom:1.5rem;padding-top:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body{padding-bottom:9rem;padding-top:9rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body{padding-bottom:18rem;padding-top:18rem}}.hero.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body,.hero.is-halfheight .hero-body{align-items:center;display:flex}.hero.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container,.hero.is-halfheight .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-foot,.hero-head{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:1024px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem} \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js old mode 100644 new mode 100755 index 3a445acb..9b50185e --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -1,6 +1,5 @@ /* - - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file loads stats to show on the main node web page @@ -19,9 +18,34 @@ */ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') +connectedDisplay.style.maxHeight = '300px' +connectedDisplay.style.overflowY = 'scroll' storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') lastIncoming = document.getElementById('lastIncoming') +totalRec = document.getElementById('totalRec') +securityLevel = document.getElementById('securityLevel') +sec_description_str = 'unknown' + +function showSecStatNotice(){ + var secWarnEls = document.getElementsByClassName('secRequestNotice') + for (el = 0; el < secWarnEls.length; el++){ + secWarnEls[el].style.display = 'block' + } +} + +switch (httpGet('/config/get/general.security_level')){ + case "0": + sec_description_str = 'normal' + break; + case "1": + sec_description_str = 'high' + break; +} + +if (sec_description_str !== 'normal'){ + showSecStatNotice() +} function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -29,14 +53,17 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] + securityLevel.innerText = sec_description_str + totalRec.innerText = httpGet('/hitcount') var lastConnect = httpGet('/lastconnect') if (lastConnect > 0){ var humanDate = new Date(0) humanDate.setUTCSeconds(httpGet('/lastconnect')) - lastConnect = humanDate.toString() + humanDate = humanDate.toString() + lastConnect = humanDate.substring(0, humanDate.indexOf('(')); } else{ - lastConnect = 'Unknown' + lastConnect = 'None since start' } lastIncoming.innerText = lastConnect } diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css old mode 100644 new mode 100755 index 90d4af13..8181df8d --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -41,11 +41,15 @@ body{ vertical-align: middle; } .logoText{ - font-family: sans-serif; font-size: 2em; margin-top: 1em; margin-left: 1%; } + + .logoText, h1, h2, h3{ + font-family: Verdana, Geneva, Tahoma, sans-serif; + } + .main{ min-height: 500px; } @@ -152,7 +156,7 @@ body{ padding: 5px; } - .btn, .warnBtn, .dangerBtn, .successBtn{ + .btn, .warnBtn, .dangerBtn, .successBtn, .primaryBtn{ padding: 5px; border-radius: 5px; border: 2px solid black; @@ -174,8 +178,16 @@ body{ background-color:#396BAC; } +.btn:hover{ + opacity: 0.6; +} + .openSiteBtn{ padding: 5px; border: 1px solid black; border-radius: 5px; } + +.hidden{ + display: none; +} diff --git a/onionr/static-data/www/shared/main/styles-new.css b/onionr/static-data/www/shared/main/styles-new.css new file mode 100644 index 00000000..b73b3fd6 --- /dev/null +++ b/onionr/static-data/www/shared/main/styles-new.css @@ -0,0 +1,56 @@ +html { + background-color: #f5f5f5; +} + + +/* Config on homepage */ +#configContent{ + display:none; + } +#configContent.show{ + display:block; /* P.S: Use `!important` if missing `#content` (selector specificity). */ + } + +.hiddenOverlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + height:100%; +} + + +/* https://stackoverflow.com/a/16778646/ +* Kept due to shutdown message on homepage +*/ +.overlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + height:100%; + text-align:left; + z-index: 1000; + background-color: #2c2b3f; + color: white; + } + + .closeOverlay{ + background-color: white; + color: black; + border: 1px solid red; + border-radius: 5px; + float: right; + font-family: sans-serif; + } + .closeOverlay:after{ + content: '❌'; + padding: 5px; + } + + .navbarLogo{ + margin-right: 5px; + color: red; +} \ No newline at end of file diff --git a/onionr/static-data/www/shared/misc.js b/onionr/static-data/www/shared/misc.js old mode 100644 new mode 100755 index 3c960c8f..775ef7c8 --- a/onionr/static-data/www/shared/misc.js +++ b/onionr/static-data/www/shared/misc.js @@ -1,5 +1,5 @@ /* - Onionr - P2P Anonymous Storage Network + Onionr - Private P2P Communication This file handles the mail interface @@ -20,6 +20,8 @@ webpass = document.location.hash.replace('#', '') nowebpass = false +myPub = httpGet('/getActivePubkey') + function post_to_url(path, params) { var form = document.createElement("form") @@ -91,4 +93,31 @@ for (var i = 0; i < document.getElementsByClassName('closeOverlay').length; i++) document.getElementsByClassName('closeOverlay')[i].onclick = function(e){ document.getElementById(e.target.getAttribute('overlay')).style.visibility = 'hidden' } +} + +var idStrings = document.getElementsByClassName('myPub') +for (var i = 0; i < idStrings.length; i++){ + if (idStrings[i].tagName.toLowerCase() == 'input'){ + idStrings[i].value = myPub + } + else{ + idStrings[i].innerText = myPub + } +} + +/* Copy public ID on homepage */ +myPubCopy.onclick = function() { + var copyText = document.getElementById("myPub"); + copyText.select(); + document.execCommand("copy") +} + +/* For Config toggle on homepage */ +var toggle = document.getElementById("configToggle"); +var content = document.getElementById("configContent"); + +if(typeof toggle !== 'undefined' && toggle !== null) { + toggle.addEventListener("click", function() { + content.classList.toggle("show"); + }) } \ No newline at end of file diff --git a/onionr/static-data/www/shared/navbar.js b/onionr/static-data/www/shared/navbar.js new file mode 100644 index 00000000..0b281b38 --- /dev/null +++ b/onionr/static-data/www/shared/navbar.js @@ -0,0 +1,49 @@ +document.addEventListener('DOMContentLoaded', () => { + // This function taken from the official bulma docs + /* + The MIT License (MIT) + + Copyright (c) 2019 Jeremy Thomas + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + + // Get all "navbar-burger" elements + const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); + + // Check if there are any navbar burgers + if ($navbarBurgers.length > 0) { + + // Add a click event on each of them + $navbarBurgers.forEach( el => { + el.addEventListener('click', () => { + + // Get the target from the "data-target" attribute + const target = el.dataset.target; + const $target = document.getElementById(target); + + // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" + el.classList.toggle('is-active'); + $target.classList.toggle('is-active'); + + }); + }); + } + + }); \ No newline at end of file diff --git a/onionr/static-data/www/shared/onionrblocks.js b/onionr/static-data/www/shared/onionrblocks.js deleted file mode 100644 index 6be0210d..00000000 --- a/onionr/static-data/www/shared/onionrblocks.js +++ /dev/null @@ -1,7 +0,0 @@ -class Block { - constructor(hash, raw) { - this.hash = hash; - this.raw = raw; - } -} - \ No newline at end of file diff --git a/onionr/static-data/www/shared/panel.js b/onionr/static-data/www/shared/panel.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/shared/sites.js b/onionr/static-data/www/shared/sites.js old mode 100644 new mode 100755 diff --git a/onionr/static-data/www/ui/dist/js/main.js b/onionr/static-data/www/ui/dist/js/main.js deleted file mode 100644 index 0ddf141e..00000000 --- a/onionr/static-data/www/ui/dist/js/main.js +++ /dev/null @@ -1,753 +0,0 @@ - -/* handy localstorage functions for quick usage */ - -function set(key, val) { - return localStorage.setItem(key, val); -} - -function get(key, df) { // df is default - value = localStorage.getItem(key); - if(value == null) - value = df; - - return value; -} - -function remove(key) { - return localStorage.removeItem(key); -} - -function getParameter(name) { - var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); - return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); -} - -/* usermap localStorage stuff */ - -var usermap = JSON.parse(get('usermap', '{}')); -var postmap = JSON.parse(get('postmap', '{}')) - -function getUserMap() { - return usermap; -} - -function getPostMap(hash) { - if(hash !== undefined) { - if(hash in postmap) - return postmap[hash]; - return null; - } - - return postmap; -} - -function deserializeUser(id) { - if(!(id in getUserMap())) - return null; - - var serialized = getUserMap()[id] - var user = new User(); - - user.setName(serialized['name']); - user.setID(serialized['id']); - user.setIcon(serialized['icon']); - user.setDescription(serialized['description']); - - return user; -} - -function getCurrentUser() { - var user = get('currentUser', null); - - if(user === null) - return null; - - return User.getUser(user, function() {}); -} - -function setCurrentUser(user) { - set('currentUser', user.getID()); -} - -/* returns a relative date format, e.g. "5 minutes" */ -function timeSince(date, size) { - // taken from https://stackoverflow.com/a/3177838/3678023 - - var seconds = Math.floor((new Date() - date) / 1000); - var interval = Math.floor(seconds / 31536000); - - if (size === null) - size = 'desktop'; - - var dates = { - 'mobile' : { - 'yr' : 'yrs', - 'mo' : 'mo', - 'd' : 'd', - 'hr' : 'h', - 'min' : 'm', - 'secs' : 's', - 'sec' : 's', - }, - - 'desktop' : { - 'yr' : ' years', - 'mo' : ' months', - 'd' : ' days', - 'hr' : ' hours', - 'min' : ' minutes', - 'secs' : ' seconds', - 'sec' : ' second', - }, - }; - - if (interval > 1) - return interval + dates[size]['yr']; - interval = Math.floor(seconds / 2592000); - - if (interval > 1) - return interval + dates[size]['mo']; - interval = Math.floor(seconds / 86400); - - if (interval > 1) - return interval + dates[size]['d']; - interval = Math.floor(seconds / 3600); - - if (interval > 1) - return interval + dates[size]['hr']; - interval = Math.floor(seconds / 60); - - if (interval > 1) - return interval + dates[size]['min']; - - if(Math.floor(seconds) !== 1) - return Math.floor(seconds) + dates[size]['secs']; - - return '1' + dates[size]['sec']; -} - -/* replace all instances of string */ -String.prototype.replaceAll = function(search, replacement, limit) { - // taken from https://stackoverflow.com/a/17606289/3678023 - var target = this; - return target.split(search, limit).join(replacement); -}; - -/* useful functions to sanitize data */ -class Sanitize { - /* sanitizes HTML in a string */ - static html(html) { - return String(html).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } - - /* URL encodes a string */ - static url(url) { - return encodeURIComponent(url); - } - - /* usernames */ - static username(username) { - return String(username).replace(/[\W_]+/g, " ").substring(0, 25); - } - - /* profile descriptions */ - static description(description) { - return String(description).substring(0, 128); - } -} - -/* config stuff */ -function getWebPassword() { - return get("web-password", null); -} - -function setWebPassword(password) { - return set("web-password", password); -} - -function getTimingToken() { - return get("timing-token", null); -} - -function setTimingToken(token) { - return set("timing-token", token); -} - -/* user class */ -class User { - constructor() { - this.name = 'Unknown'; - this.id = 'unknown'; - this.image = 'img/default.png'; - } - - setName(name) { - this.name = name; - } - - getName() { - return this.name; - } - - setID(id) { - this.id = id; - } - - getID() { - return this.id; - } - - setIcon(image) { - this.image = image; - } - - getIcon() { - return this.image; - } - - setDescription(description) { - this.description = description; - } - - getDescription() { - return this.description; - } - - serialize() { - return { - 'name' : this.getName(), - 'id' : this.getID(), - 'icon' : this.getIcon(), - 'description' : this.getDescription() - }; - } - - /* save in usermap */ - remember() { - usermap[this.getID()] = this.serialize(); - set('usermap', JSON.stringify(usermap)); - } - - /* save as a block */ - save(callback) { - var block = new Block(); - - block.setType('onionr-user'); - block.setContent(JSON.stringify(this.serialize())); - - return block.save(true, callback); - } - - static getUser(id, callback) { - // console.log(callback); - var user = deserializeUser(id); - if(user === null) { - Block.getBlocks({'type' : 'onionr-user-info', 'signed' : true, 'reverse' : true}, function(data) { - if(data.length !== 0) { - try { - user = new User(); - - var userInfo = JSON.parse(data[0].getContent()); - - if(userInfo['id'] === id) { - user.setName(userInfo['name']); - user.setIcon(userInfo['icon']); - user.setDescription(userInfo['description']); - user.setID(id); - - user.remember(); - // console.log(callback); - callback(user); - return user; - } - } catch(e) { - console.log(e); - - callback(null); - return null; - } - } else { - callback(null); - return null; - } - }); - } else { - // console.log(callback); - callback(user); - return user; - } - } -} - -/* post class */ -class Post { - /* returns the html content of a post */ - getHTML(type) { - var replyTemplate = '\ -
    \ -
    \ -
    \ -
    \ - \ -
    \ -
    \ -
    \ -
    \ - $user-name\ -
    \ -\ -
    \ - \ -
    \ -
    \ -\ -
    \ - $content\ -
    \ -\ -
    \ - $liked\ - reply\ -
    \ -
    \ -
    \ -
    \ -
    \ -\ -'; - var postTemplate = '\ -
    \ -
    \ -
    \ -
    \ - \ -
    \ -
    \ -
    \ - \ -\ -
    \ - \ -
    \ -
    \ -\ -
    \ - $content\ -
    \ -\ -
    \ - $liked\ - reply\ -
    \ -
    \ -
    \ -
    \ -
    \ -\ -'; - - var template = ''; - - if(type !== undefined && type !== null && type == 'reply') - template = replyTemplate; - else - template = postTemplate; - - var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop'); - - template = template.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName()))); - template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName())); - template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID()))); - - template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...')); - // template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-'))); - - template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID())); - template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon())); - template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '
    ', 16)); // Maximum of 16 lines - template = template.replaceAll('$post-hash', this.getHash()); - template = template.replaceAll('$date-relative-truncated', timeSince(this.getPostDate(), 'mobile')); - template = template.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : '')); - template = template.replaceAll('$date', this.getPostDate().toLocaleString()); - - if(this.getHash() in getPostMap() && getPostMap()[this.getHash()]['liked']) { - template = template.replaceAll('$liked', 'unlike'); - } else { - template = template.replaceAll('$liked', 'like'); - } - - return template; - } - - setUser(user) { - this.user = user; - } - - getUser() { - return this.user; - } - - setContent(content) { - this.content = content; - } - - getContent() { - return this.content; - } - - setParent(parent) { - this.parent = parent; - } - - getParent() { - return this.parent; - } - - setPostDate(date) { // unix timestamp input - if(date instanceof Date) - this.date = date; - else - this.date = new Date(date * 1000); - } - - getPostDate() { - return this.date; - } - - setHash(hash) { - this.hash = hash; - } - - getHash() { - return this.hash; - } - - save(callback) { - var args = {'type' : 'onionr-post', 'sign' : true, 'content' : JSON.stringify({'content' : this.getContent()})}; - - if(this.getParent() !== undefined && this.getParent() !== null) - args['parent'] = (this.getParent() instanceof Post ? this.getParent().getHash() : (this.getParent() instanceof Block ? this.getParent().getHash() : this.getParent())); - - var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); - - console.log(url); - - var http = new XMLHttpRequest(); - - if(callback !== undefined) { - // async - - var thisObject = this; - - http.addEventListener('load', function() { - thisObject.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); - callback(thisObject.getHash()); - }, false); - - http.open('GET', url, true); - http.timeout = 5000; - http.send(null); - } else { - // sync - - http.open('GET', url, false); - http.send(null); - - this.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); - - return this.getHash(); - } - } -} - -/* block class */ -class Block { - constructor(type, content) { - this.type = type; - this.content = content; - } - - // returns the block hash, if any - getHash() { - return this.hash; - } - - // returns the block type - getType() { - return this.type; - } - - // returns the block header - getHeader(key, df) { // df is default - if(key !== undefined) { - if(this.getHeader().hasOwnProperty(key)) - return this.getHeader()[key]; - else - return (df === undefined ? null : df); - } else - return this.header; - } - - // returns the block metadata - getMetadata(key, df) { // df is default - if(key !== undefined) { - if(this.getMetadata().hasOwnProperty(key)) - return this.getMetadata()[key]; - else - return (df === undefined ? null : df); - } else - return this.metadata; - } - - // returns the block content - getContent() { - return this.content; - } - - // returns the parent block's hash (not Block object, for performance) - getParent() { - // console.log(this.parent); - - // TODO: Create a function to fetch the block contents and parse it from the server; right now it is only possible to search for types of blocks (see Block.getBlocks), so it is impossible to return a Block object here - - // if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null) - // this.parent = Block.openBlock(this.parent); // convert hash to Block object - return this.parent; - } - - // returns the date that the block was received - getDate() { - return this.date; - } - - // returns a boolean that indicates whether or not the block is valid - isValid() { - return this.valid; - } - - // returns a boolean thati ndicates whether or not the block is signed - isSigned() { - return this.signed; - } - - // returns the block signature - getSignature() { - return this.signature; - } - - // returns the block type - setType(type) { - this.type = type; - return this; - } - - // sets block metadata by key - setMetadata(key, val) { - this.metadata[key] = val; - return this; - } - - // sets block content - setContent(content) { - this.content = content; - return this; - } - - // sets the block parent by hash or Block object - setParent(parent) { - this.parent = parent; - return this; - } - - // indicates if the Block exists or not - exists() { - return !(this.hash === null || this.hash === undefined); - } - - // saves the block, returns the hash - save(sign, callback) { - var type = this.getType(); - var content = this.getContent(); - var parent = this.getParent(); - - if(content !== undefined && content !== null && type !== '') { - var args = {'content' : content}; - - if(type !== undefined && type !== null && type !== '') - args['type'] = type; - if(parent !== undefined && parent !== null && parent.getHash() !== undefined && parent.getHash() !== null && parent.getHash() !== '') - args['parent'] = parent.getHash(); - if(sign !== undefined && sign !== null) - args['sign'] = String(sign) !== 'false' - - - var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); - - console.log(url); - - var http = new XMLHttpRequest(); - - if(callback !== undefined) { - // async - - http.addEventListener('load', function() { - callback(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); - }, false); - - http.open('GET', url, true); - http.timeout = 5000; - http.send(null); - } else { - // sync - - http.open('GET', url, false); - http.send(null); - - return Block.parseBlockArray(JSON.parse(http.responseText)['hash']); - } - } - - return false; - } - - /* static functions */ - - // recreates a block by hash - static openBlock(hash) { - return Block.parseBlock(hash); - } - - // converts an associative array to a Block - static parseBlock(val) { - var block = new Block(); - - block.type = val['type']; - block.content = val['content']; - block.header = val['header']; - block.metadata = val['metadata']; - block.date = new Date(val['date'] * 1000); - block.hash = val['hash']; - block.signature = val['signature']; - block.signed = val['signed']; - block.valid = val['valid']; - block.parent = val['parent']; - - if(block.getParent() !== null) { - // if the block data is already in the associative array - - /* - if (blocks.hasOwnProperty(block.getParent())) - block.setParent(Block.parseAssociativeArray({blocks[block.getParent()]})[0]); - */ - } - - return block; - } - - // converts an array of associative arrays to an array of Blocks - static parseBlockArray(blocks) { - var outputBlocks = []; - - for(var key in blocks) { - if(blocks.hasOwnProperty(key)) { - var val = blocks[key]; - - var block = Block.parseBlock(val); - - outputBlocks.push(block); - } - } - - return outputBlocks; - } - - static getBlocks(args, callback) { // callback is optional - args = args || {} - - var url = '/client/?action=searchBlocks&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); - - console.log(url); - - var http = new XMLHttpRequest(); - - if(callback !== undefined) { - // async - - http.addEventListener('load', function() { - callback(Block.parseBlockArray(JSON.parse(http.responseText)['blocks'])); - }, false); - - http.open('GET', url, true); - http.timeout = 5000; - http.send(null); - } else { - // sync - - http.open('GET', url, false); - http.send(null); - - return Block.parseBlockArray(JSON.parse(http.responseText)['blocks']); - } - } -} - -/* temporary code */ - -var tt = getParameter("timingToken"); -if(tt !== null && tt !== undefined) { - setTimingToken(tt); -} - -if(getWebPassword() === null) { - var password = ""; - while(password.length != 64) { - password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)"); - } - - setWebPassword(password); -} - -if(getCurrentUser() === null) { - jQuery('#modal').modal('show'); - - var url = '/client/?action=info&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); - - console.log(url); - - var http = new XMLHttpRequest(); - - // sync - - http.addEventListener('load', function() { - var id = JSON.parse(http.responseText)['pubkey']; - - User.getUser(id, function(data) { - if(data === null || data === undefined) { - var user = new User(); - - user.setName('New User'); - user.setID(id); - user.setIcon('/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAcFBQYFBAcGBQYIBwcIChELCgkJChUPEAwRGBUaGRgVGBcbHichGx0lHRcYIi4iJSgpKywrGiAvMy8qMicqKyr/2wBDAQcICAoJChQLCxQqHBgcKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKir/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDrtTvrlL51jlkyGPANUZNSuvJZ2uJFYHjB6UmpTE6jcZUH5iCR0FQQLHvww3An8K8jmuz0lHQvwXV1gNLcSBmGcZqcXtwo/wBe/X1rzqw1e/stWmaTdKpcl1Le9dqmoJc2qupxnoCOauUWkOzRpnULhsATMPXmoptSuFGPPfjvms8Xew4OaY7NOSEyAT3rK9w5bFn+0rlmCrPIvqc9KRL+9UGVrr5ew39aoN5qkRhjt9Vp0Vv5bFmHJ6Z7Ucz2KsjXi1K4kUYmk6Z61Ot1Owz5z9OOayYcquGZgw59sVaikZ1OSQB0FUmQ0XftVwP+WznjoDS/bZx83msBjpmqobb1IBPv1prOpGD+lVzE2LP9ozEHEznPvTDe3JBImbaO4NZ0jlfliGM52jHWlW2nEO6eRuBnCU7jsXft068+dIR9amtLycupaduvOTWH/aIPyqjxkHBDd/pV2BiZEYdAacZJ7Eyi0QXC7dVn3Nw0hzxxTRPCgAXAZucY+9RewzDUpjuYp5h7VGLZW+VAVJ6Fj0rn5pX2Nkkc/qFuV1KbdGHiLb1ZcZTPYj61JazNbNtfJib+HofqD6ioPEQ+y6lAQziTZ9/djvwM0z7XfSRhJj8hxnzAMj8a9CDUqepErp6G0uriOdYNQOQRmKZRw49x2PrWnHd2/lZDqufeuIulcWpjlYb433IR0B6EUnmyMu55AFiHrzz0rzpO0rI6uRNXO08yNySGVv8AgXWpTKEXaRg+9cLZvIzM7s+M/L61Oby5+0eXG7ZXknqFHqTSE6Z10ksUMZknJVR7Vg3viCV/3dngAHl/Wsh759QuPKDmSJT8x3Ec1pRQReSViKMf7prtp0rq7MZWi9SvpmsTvrEKTuWDNt4OcZrs1kaBVcweYpPU1w2n2Dt4mtsqFAffgH0rugSr4Y7j168fhWdRcrKmlpYJJy2H2IHHpwB/9eoxO5G0ZxjpnrSGNpW5ZVGePb1p3ynKMPn6ZHGKzWpGiIVt/mwycjJPrVi2ZvMA3dcAEelOAYEHBdTwfWnwxATgldqE9B1FaqyehndvcsXSk6hNzxuNRpFuyCQO/Spr35b6Tp944xVaeby4GkH8Kkn8BUDOU8QvG2p+Qy7wqjk96rtes0KJsGMYBI6j0qCwf+0J2u7hgCx+X3H9K1xpp+0RkkFO/wDhVXk1ZGlktzAu1kdyMLleFyeuapSWbrsjYnO4Bs9/f+laNxKsk7vkeX9q8pCO2AS1XNMRbtby5lTekOGII5J7AD8BWPLd2OhSsiitnLDeFGUkeSD+JNWEQ7Xixt3dcHPNS7ZVvnWQ7p3jDOPTvj9f0pwTeBwQwPPHSp21HqzIltDY3BZdylz8oUEnP4VBHqzyXot7uHysdJGOOfwroy7iP5iQBxkHFYl/YWzXsZZXJZhliMd+wrtp1FYx5XzanQ+F7b/iZXHmIS6fL5jd/YVu3cLxyBdzZP3eM8VBpMUYdjHn52GPwAH9K6aS0ElqCy/Mo4qV+8bMqsuV3MJLVduJJMfhxVxYovL/ANpeMFeKx7vXLSzmZJHbKHoqGs6TxZBI22KOV29+AKy5lHcPZylsdMu9EG3I5zjFQ/a1imXzWyVG3k5rlf7bvLudU8zyYs8hD1/Gty3jWSNORjjrVKd9gdNrc0bqVRfT7sg7yR71A7edGYzIoDqRyarXjeXfzebwd7Z+b+lQM7KodcMvrjFLqI4nSbC0ivpoNQmdGZiI8OVxg+orJ1TWfEfhnWnS2uWuLYPgRSLv3Iff1966LUlP26RGVnw+QpH3gecg+orS06yTVLHyNRtvtEUYIVnOGQezDqK0pvldmrlzXNG9zmtK1F7qGxIiPlM7srP1Vxncp/xr0bw7p6WukzvMhKzPuxj0rz2ztxb3I06yiZktbh5mbOQC+Bt/nXsNor23h2NLeESXZjPlRFgNx9ee3rWlOMXN2MqspKKPOb3WtN0fxRevqd2tv5qKkKYLMeOTgdPTmtC31PQ7qEraXsbSYztbgn35FUNS+FGq3zTSzzQzSXMnmyT7yrof6/hWtpGk6f4dR4riJr27nULLM6YUAdFGf51M6UILUuNRyegxHhnUhWXHoCDzSWwAkwyrwepHSobnQ3l1BrvRIjbso+ZcYVqYL1kcCdfKlxhlYYFcTTTOlNNaHWaU5MyIETIPUADFdVJgx9O1cl4fuFuSNrAleu2uivL1Le3LyHAArtwzsmzhxGskjzPxNCiazOqdM5xXOBGWZiMDNdLqRW7ee+bA3EhQeuPWsA8MecZAwDXFLWbZ6MNIpMnhV2ZWD9+wrr7fKRxqik9Msa4pYmEyMsyo2eATj8q6XT7i8QoG2FOxV60j3M6hraope/n3cfOcVnOpPVsj0ra1CaJLybC7iXOfasm6dWUBAMk5JxitNDlVzF1SEZEykgrwR6irtjqiW9jLFIhTzY9qHHU9qrXQzCQ+CD2z0rHMrO3llyjKeCDgNWsJWE1cTw8IvtVw8r+XN5xUknJ4PP416DHq9/N4hguLOAyW1nH5LZHDEj9DivOprSCTWreUymJLg7bkL1YAdRjuRxXrGk6jZWemx29lHEkCjIG4j8+DzWkKbfWxVapFJaXZuvdo8AK4BK52nqPwrnbyO3aYyttYHtkirrXkNxC7K0cbKM8S5H6isKQSSyHy1+U9HByK2l7y1OOF4vQs7UuWCGFfL6Ehzx9BTH0C2m/ds8j+m4D5adZRT+Z8rAj124rSMqW6Evkc4Yk1HJF7ov2klsS2Gn22nW4SHC+9YXiW+MrpZqQQxwxq7qWpR2tqXLowYcDPWuBe9ka/M4PsFNYV5KEeWJvQg5y5mXtYmiW1WJChGduB1Fc+qqyyZDGMdDnIzVnU7mUzfOHiOPmJHWpI4zHpOIwu5upyOfwriWrO/ZGZmeGeNjHuGeAB1H41vWOpxzypKgGeCV2jqD6VzpNzGwLOjKrZGByv4VVe6aG+Zo+CjBgQB0zyPpWiFJXPStSnAv5wso3Bzxj3rOkkWUAnBZOQ2/vUWpysdTuBk7jKw+ZfeqsjfZ1KzEH3XmtDjK9/MkYGZD83UA9KxXuEfnd0PBPU1ZvZYip2tgnqCKwHlJuRGjBueMVSd9CraHS209tKuJEUnP0zWxDIkIAhuJl7gbyRXHrbzBgcEt2UdquwSTRnbI/19q2i2ZyR2UF7JwJJGYdAM5ratImMW/hRn5lHQ++K5Ow1BWVGdduBxkdTWtDqbvKY4+MdDWqZhJHUxyxqgCcMOfrVHVb9LG1eWTDs3QepAqhHelbd5ZjsYfpXHarq8mpzkI5WIEhlz0/zioqVOVF0qTm9SeXUXv7kmRwEY/Lt4zUkNsC4D4Ii+Y4PSqVqMN5eBmQcAdh/StC4aKzsGRGUsfbOa86TcnqeitNEOkmWexkbbjnA2nkfUVlqkluoizhX5GcYp8DkgPIrbT97aMg1JcwRuRK67oiOuc4pLUrYytSiSJlAJGeSFPzL/jVJ2TIlz5xAABC4P196u3EUN8PsxfKKcod2CtVLqBrKQwsS2xcHPXkitVawtUdfqrSrq9y4XOJG4P1rLuJywbcu3nBGK6HUS51OcKgZfMJJU/55rB1CN47dmdl3ZzgNyKlSVznsc/qW5d25+f7tcxevKkwaMmNvXPSuqvNQiVSmGP8As7OWFcve/vWLRmTrjb6VvTbuElodf4Zu7K5gSLzmaVR8+/qa61dPhdQFA/DvXkmibk1EiaM8rwFOP1r0zQL47VXb06sZQ1dCkk7HPOLtdGoukKu2RsEpyoPAzVqCwWNshwWI9OTVuEedbl5BgnocVCJJJJTHEOFOGOcYrTQx1ZmeIbxljW1TgyfKNo6+9cwbRYju3bvJBL55AP8A9aut1C1Es8sqSbzCm3IHAJ6gfQVyt/GttGyI24bcEeue3+NcdS97s7aVrWQtpKyTGaTkdFGT+dTXd5PecYQRn1BzWPNMYLZVQkZASPPrV7S5fMuxFNs3Rgbmc8A/Tua52n0OlW3Ztmymi0pXhypx36H61n263NwxiWIKD1y/BrohLatbiOWcOcemB+QrHvI5EkAt5EKj+HdjH4UnsTGWupYTwzEyF5QEkHO5Gzj8KwdVsmtroywskoAGec47YI96s3M1+8Yj3TADoyAisW6hvba4WWVXKS8MfU9Rk+tVFodn1Z3Gp3jf2ldCRWwJWGBxnmqYjLJlFRycnkcj610F/pmL6Yht+ZCeVqmbGRCHji3EDjCmqtbY5eY5q90gSqBMCfRvSufutJ8uQkKMDuetd5LDPtIuEIwOMLjNY1xGskb79yH+4y0RZdzj7C2WfWI43Xf2KkYr1LTdOe1t1Nv5MSD0QH/CuDhtY49YjZgwU8Y3EE16JptneXMai2sGSMfxyMR+ldtOKauc9WTNq3wIgWcE46CnSBHGSvBGOKsJaSR24MsRYrztVMVMLSQrkLhupXHGD6VvZnNc5XVLdrUSiHJSQ5Cgd65i+tp4dKedQiTsdoLjhfU4716LqGnuVw6MD1VgOlchqFgyXkT3GXVHyA+dufeuedNPU6adS2hxtxFOIS3lsZZASiMvfoGqlNb31g0dtnZu+ZnH3vr9a7V7iKW6WK0ge7nkON5Xauf8BVTW7CSDT5jdkRSS5LSY5I/oPaudw5TrjUuZOnX9lt2G4leUDBO7j8RWxaX1urj/AEWE+jp6+4NcCYDcaiyWaKijptX5vwPua0H0y/gVZcXicfeLZFZSj5mySZ6OmpwiEyRLl1+9C67SP8+tYuo61a6nFJAEktpPQ9DWXpFprGqbbd/MaMcFmToPr1rpD4OijVTN50zDH3RyfxqbtbE8sYvU/9k=\ -'); - user.setDescription('A new OnionrUI user'); - - user.remember(); - user.save(); - - setCurrentUser(user); - } else { - setCurrentUser(data); - } - - window.location.reload(); - }); - }, false); - - http.open('GET', url, true); - http.send(null); -} - -currentUser = getCurrentUser(); diff --git a/onionr/storagecounter.py b/onionr/storagecounter.py index 76ebe16b..9dbb8827 100755 --- a/onionr/storagecounter.py +++ b/onionr/storagecounter.py @@ -1,5 +1,5 @@ ''' - Onionr - P2P Microblogging Platform & Social network. + Onionr - Private P2P Communication Keeps track of how much disk space we're using ''' diff --git a/onionr/subprocesspow.py b/onionr/subprocesspow.py index a2fee7c6..1ca2f9b1 100755 --- a/onionr/subprocesspow.py +++ b/onionr/subprocesspow.py @@ -1,34 +1,60 @@ #!/usr/bin/env python3 -import subprocess, sys, os -import multiprocessing, threading, time, json, math, binascii +''' + Onionr - Private P2P Communication + + Multiprocess proof of work +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +import subprocess, os +import multiprocessing, threading, time, json from multiprocessing import Pipe, Process import core, onionrblockapi, config, onionrutils, logger, onionrproofs +from onionrutils import bytesconverter class SubprocessPOW: - def __init__(self, data, metadata, core_inst=None, subprocCount=None): + def __init__(self, data, metadata, core_inst=None, subproc_count=None): + ''' + Onionr proof of work using multiple processes + Accepts block data, block metadata + and optionally an onionr core library instance. + if subproc_count is not set, os.cpu_count() is used to determine the number of processes + + Do to Python GIL multiprocessing or use of external libraries is necessary to accelerate CPU bound tasks + ''' + # Option to accept existing core instance to save memory if core_inst is None: core_inst = core.Core() - if subprocCount is None: - subprocCount = os.cpu_count() - self.subprocCount = subprocCount + # No known benefit to using more processes than there are cores. + # Note: os.cpu_count perhaps not always accurate + if subproc_count is None: + subproc_count = os.cpu_count() + self.subproc_count = subproc_count self.result = '' self.shutdown = False self.core_inst = core_inst self.data = data self.metadata = metadata - dataLen = len(data) + len(json.dumps(metadata)) + # dump dict to measure bytes of json metadata. Cannot reuse later because the pow token must be added + json_metadata = json.dumps(metadata).encode() - #if forceDifficulty > 0: - # self.difficulty = forceDifficulty - #else: - # Calculate difficulty. Dumb for now, may use good algorithm in the future. - self.difficulty = onionrproofs.getDifficultyForNewBlock(dataLen) - - try: - self.data = self.data.encode() - except AttributeError: - pass + self.data = bytesconverter.str_to_bytes(data) + # Calculate difficulty. Dumb for now, may use good algorithm in the future. + self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data), coreInst=self.core_inst) logger.info('Computing POW (difficulty: %s)...' % self.difficulty) @@ -38,9 +64,10 @@ class SubprocessPOW: self.payload = None def start(self): - startTime = self.core_inst._utils.getEpoch() - for x in range(self.subprocCount): + # Create a new thread for each subprocess + for x in range(self.subproc_count): threading.Thread(target=self._spawn_proc).start() + # Monitor the processes for a payload, shut them down when its found while True: if self.payload is None: time.sleep(0.1) @@ -49,6 +76,7 @@ class SubprocessPOW: return self.payload def _spawn_proc(self): + # Create a child proof of work process, wait for data and send shutdown signal when its found parent_conn, child_conn = Pipe() p = Process(target=self.do_pow, args=(child_conn,)) p.start() @@ -67,24 +95,24 @@ class SubprocessPOW: self.payload = payload def do_pow(self, pipe): - nonce = int(binascii.hexlify(os.urandom(2)), 16) + nonce = -10000000 # Start nonce at negative 10 million so that the chosen nonce is likely to be small in length nonceStart = nonce data = self.data metadata = self.metadata puzzle = self.puzzle difficulty = self.difficulty - mcore = core.Core() + mcore = core.Core() # I think we make a new core here because of multiprocess bugs while True: - metadata['powRandomToken'] = nonce - payload = json.dumps(metadata).encode() + b'\n' + data - token = mcore._crypto.sha3Hash(payload) - try: - # on some versions, token is bytes - token = token.decode() - except AttributeError: - pass + # Break if shutdown received if pipe.poll() and pipe.recv() == 'shutdown': break + # Load nonce into block metadata + metadata['pow'] = nonce + # Serialize metadata, combine with block data + payload = json.dumps(metadata).encode() + b'\n' + data + # Check sha3_256 hash of block, compare to puzzle. Send payload if puzzle finished + token = mcore._crypto.sha3Hash(payload) + token = bytesconverter.bytes_to_str(token) # ensure token is string if puzzle == token[0:difficulty]: pipe.send(payload) break diff --git a/onionr/tests/test_highlevelcrypto.py b/onionr/tests/test_highlevelcrypto.py index 0a675ed7..aab2e198 100755 --- a/onionr/tests/test_highlevelcrypto.py +++ b/onionr/tests/test_highlevelcrypto.py @@ -4,6 +4,7 @@ sys.path.append(".") import unittest, uuid, hashlib, base64 import nacl.exceptions import nacl.signing, nacl.hash, nacl.encoding +from onionrutils import stringvalidators, mnemonickeys TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' print("Test directory:", TEST_DIR) os.environ["ONIONR_HOME"] = TEST_DIR @@ -45,19 +46,12 @@ class OnionrCryptoTests(unittest.TestCase): self.assertEqual(crypto.sha3Hash(b'test'), normal) def valid_default_id(self): - self.assertTrue(c._utils.validatePubKey(crypto.pubKey)) + self.assertTrue(stringvalidators.validate_pub_key(crypto.pubKey)) def test_human_readable_length(self): - human = c._utils.getHumanReadableID() + human = mnemonickeys.get_human_readable_ID(c) self.assertTrue(len(human.split(' ')) == 32) - def test_human_readable_rebuild(self): - return # Broken right now - # Test if we can get the human readable id, and convert it back to valid base32 key - human = c._utils.getHumanReadableID() - unHuman = c._utils.convertHumanReadableID(human) - nacl.signing.VerifyKey(c._utils.convertHumanReadableID(human), encoder=nacl.encoding.Base32Encoder) - def test_safe_compare(self): self.assertTrue(crypto.safeCompare('test', 'test')) self.assertTrue(crypto.safeCompare('test', b'test')) @@ -130,7 +124,7 @@ class OnionrCryptoTests(unittest.TestCase): def test_deterministic(self): password = os.urandom(32) gen = crypto.generateDeterministic(password) - self.assertTrue(c._utils.validatePubKey(gen[0])) + self.assertTrue(stringvalidators.validate_pub_key(gen[0])) try: crypto.generateDeterministic('weakpassword') except onionrexceptions.PasswordStrengthError: @@ -151,6 +145,6 @@ class OnionrCryptoTests(unittest.TestCase): gen2 = crypto.generateDeterministic(password) self.assertFalse(gen == gen1) self.assertTrue(gen1 == gen2) - self.assertTrue(c._utils.validatePubKey(gen1[0])) + self.assertTrue(stringvalidators.validate_pub_key(gen1[0])) unittest.main() \ No newline at end of file diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 0cc45eae..eef418ba 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -6,6 +6,7 @@ TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' print("Test directory:", TEST_DIR) os.environ["ONIONR_HOME"] = TEST_DIR import core, onionr +from onionrutils import stringvalidators core.Core() @@ -13,7 +14,6 @@ class OnionrValidations(unittest.TestCase): def test_peer_validator(self): # Test hidden service domain validities - c = core.Core() valid = ['facebookcorewwwi.onion', 'vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion', '5bvb5ncnfr4dlsfriwczpzcvo65kn7fnnlnt2ln7qvhzna2xaldq.b32.i2p'] @@ -21,35 +21,55 @@ class OnionrValidations(unittest.TestCase): for x in valid: print('testing', x) - self.assertTrue(c._utils.validateID(x)) + self.assertTrue(stringvalidators.validate_transport(x)) for x in invalid: print('testing', x) - self.assertFalse(c._utils.validateID(x)) + self.assertFalse(stringvalidators.validate_transport(x)) + + def test_hash_validator(self): + valid = ['00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96\n', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96', b'00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c96', + '00003b3813a166e706e490238e9515633cc36', b'00003b3813a166e706e490238e9515633cc3d083'] + invalid = [None, 0, 1, True, False, '%#W483242#', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c9666', '', b'', + b'00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00c9666666', b' ', '\n', '00003b3813a166e706e490238e9515633cc3d083efe982a67753d50d87a00ccccc\n'] + + for x in valid: + self.assertTrue(stringvalidators.validate_hash(x)) + for x in invalid: + try: + result = stringvalidators.validate_hash(x) + print('testing', x, result) + except AttributeError: + result = False + try: + self.assertFalse(result) + except AssertionError: + raise AssertionError("%s returned true" % (x,)) def test_pubkey_validator(self): # Test ed25519 public key validity - valid = 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====' + valids = ['JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ'] invalid = [None, '', ' ', 'dfsg', '\n', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIR$Q===='] c = core.Core() - print('testing', valid) - self.assertTrue(c._utils.validatePubKey(valid)) + + for valid in valids: + print('testing', valid) + self.assertTrue(stringvalidators.validate_pub_key(valid)) for x in invalid: #print('testing', x) - self.assertFalse(c._utils.validatePubKey(x)) + self.assertFalse(stringvalidators.validate_pub_key(x)) def test_integer_string(self): valid = ["1", "100", 100, "-5", -5] invalid = ['test', "1d3434", "1e100", None] - c = core.Core() for x in valid: #print('testing', x) - self.assertTrue(c._utils.isIntegerString(x)) + self.assertTrue(stringvalidators.is_integer_string(x)) for x in invalid: #print('testing', x) - self.assertFalse(c._utils.isIntegerString(x)) + self.assertFalse(stringvalidators.is_integer_string(x)) unittest.main() \ No newline at end of file diff --git a/onionr/tests/test_zfill.py b/onionr/tests/test_zfill.py new file mode 100644 index 00000000..e13ce1bb --- /dev/null +++ b/onionr/tests/test_zfill.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import unittest, sys +sys.path.append(".") + +from utils import reconstructhash + +class ZFill_Hash(unittest.TestCase): + def test_reconstruct(self): + h = b"4d20d791cbc293999b97cc627aa011692d317dede3d0fbd390c763210b0d" + self.assertEqual(reconstructhash.reconstruct_hash(h), b"0000" + h) + h = b"4d20d791cbc293999b97cc627aa011692d317dede3d0fbd390c763210b0d" + self.assertEqual(reconstructhash.reconstruct_hash(h, 62), b"00" + h) + + def test_deconstruct(self): + h = b"0000e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + h_no = b"e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + self.assertEqual(reconstructhash.deconstruct_hash(h), h_no) + h = "0000e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + h_no = "e918d24999ad9b0ff00c1d414f36b74afc93871a0ece4bd452f82b56af87" + self.assertEqual(reconstructhash.deconstruct_hash(h), h_no) +unittest.main() \ No newline at end of file diff --git a/onionr/utils/detectoptimization.py b/onionr/utils/detectoptimization.py new file mode 100755 index 00000000..20570242 --- /dev/null +++ b/onionr/utils/detectoptimization.py @@ -0,0 +1,27 @@ +''' + Onionr - Private P2P Communication + + Detect if Python is being run in optimized mode or not, which has security considerations for assert statements +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' + +def detect_optimization(): + '''Returns true if Python is run in optimized mode (-o), based on optimization ignoring assert statements''' + try: + assert True is False + except AssertionError: + return False + return True \ No newline at end of file diff --git a/onionr/utils/identifyhome.py b/onionr/utils/identifyhome.py new file mode 100644 index 00000000..b62fdf22 --- /dev/null +++ b/onionr/utils/identifyhome.py @@ -0,0 +1,39 @@ +''' + Onionr - Private P2P Communication + + Identify a data directory for Onionr +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +import os, platform + +def identify_home(): + + path = os.environ.get('ONIONR_HOME', None) + if path is None: + system = platform.system() + if system == 'Linux': + path = os.path.expanduser('~') + '/.local/share/onionr/' + elif system == 'Windows': + path = os.path.expanduser('~') + '\\AppData\\Local\\onionr\\' + elif system == 'Darwin': + path = os.path.expanduser('~' + '/Library/Application Support/onionr/') + else: + path = 'data/' + else: + path = os.path.abspath(path) + if not path.endswith('/'): + path += '/' + return path \ No newline at end of file diff --git a/onionr/utils/netutils.py b/onionr/utils/netutils.py index b6f16922..1c7bea1c 100755 --- a/onionr/utils/netutils.py +++ b/onionr/utils/netutils.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -def checkNetwork(utilsInst, torPort=0): +from onionrutils import basicrequests +def checkNetwork(core_inst, torPort=0): '''Check if we are connected to the internet (through Tor)''' retData = False connectURLs = [] @@ -26,7 +27,7 @@ def checkNetwork(utilsInst, torPort=0): connectURLs = connectTest.read().split(',') for url in connectURLs: - if utilsInst.doGetRequest(url, port=torPort, ignoreAPI=True) != False: + if basicrequests.do_get_request(core_inst, url, port=torPort, ignoreAPI=True) != False: retData = True break except FileNotFoundError: diff --git a/onionr/utils/reconstructhash.py b/onionr/utils/reconstructhash.py new file mode 100644 index 00000000..ae50c0b2 --- /dev/null +++ b/onionr/utils/reconstructhash.py @@ -0,0 +1,43 @@ +''' + Onionr - Private P2P Communication + + z-fill (zero fill) a string to a specific length, intended for reconstructing block hashes +''' +''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +''' +def reconstruct_hash(hex_hash, length=64): + return hex_hash.zfill(length) + +def deconstruct_hash(hex_hash): + new_hash = '' + ret_bytes = False + try: + hex_hash = hex_hash.decode() + ret_bytes = True + except AttributeError: + pass + + c = 0 + for x in hex_hash: + if x == '0': + c += 1 + else: + break + new_hash = hex_hash[c:] + + if ret_bytes: + + new_hash = new_hash.encode() + return new_hash \ No newline at end of file diff --git a/onionr/utils/sizeutils.py b/onionr/utils/sizeutils.py new file mode 100644 index 00000000..da121910 --- /dev/null +++ b/onionr/utils/sizeutils.py @@ -0,0 +1,27 @@ +import sqlite3, os +from onionrutils import stringvalidators +def human_size(num, suffix='B'): + ''' + Converts from bytes to a human readable format. + ''' + for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: + if abs(num) < 1024.0: + return "%.1f %s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f %s%s" % (num, 'Yi', suffix) + +def size(path='.'): + ''' + Returns the size of a folder's contents in bytes + ''' + total = 0 + if os.path.exists(path): + if os.path.isfile(path): + total = os.path.getsize(path) + else: + for entry in os.scandir(path): + if entry.is_file(): + total += entry.stat().st_size + elif entry.is_dir(): + total += size(entry.path) + return total \ No newline at end of file diff --git a/requirements.in b/requirements.in old mode 100755 new mode 100644 index 6b1cb663..3b90933e --- a/requirements.in +++ b/requirements.in @@ -1,9 +1,11 @@ -urllib3==1.23 -requests==2.20.0 -PyNaCl==1.2.1 +urllib3==1.24.2 +requests==2.21.0 +PyNaCl==1.3.0 gevent==1.3.6 -defusedxml==0.5.0 Flask==1.0.2 PySocks==1.6.8 -stem==1.6.0 -deadsimplekv==0.0.1 \ No newline at end of file +stem==1.7.1 +deadsimplekv==0.1.1 +unpaddedbase32==0.1.0 +streamedrequests==1.0.0 +jinja2==2.10.1 diff --git a/requirements.txt b/requirements.txt index b2c93d7c..b5ca0af4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --generate-hashes --output-file requirements.txt requirements.in +# pip-compile --generate-hashes --output-file=requirements.txt requirements.in # certifi==2018.11.29 \ --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ @@ -46,11 +46,8 @@ click==7.0 \ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 \ # via flask -deadsimplekv==0.0.1 \ - --hash=sha256:1bb78e4feb01d975e89e81cac7b0141666a14ebefa06fffc1c2d86c3308e3930 -defusedxml==0.5.0 \ - --hash=sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4 \ - --hash=sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20 +deadsimplekv==0.1.1 \ + --hash=sha256:4bf951e188c302006e37f95bde6117b1b938fb454153d583c6346090d9bead1a flask==1.0.2 \ --hash=sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48 \ --hash=sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05 @@ -107,10 +104,9 @@ itsdangerous==1.1.0 \ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \ --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \ # via flask -jinja2==2.10 \ - --hash=sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd \ - --hash=sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4 \ - # via flask +jinja2==2.10.1 \ + --hash=sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013 \ + --hash=sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b markupsafe==1.1.1 \ --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ @@ -144,52 +140,44 @@ markupsafe==1.1.1 \ pycparser==2.19 \ --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \ # via cffi -pynacl==1.2.1 \ - --hash=sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca \ - --hash=sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512 \ - --hash=sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6 \ - --hash=sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776 \ - --hash=sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac \ - --hash=sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b \ - --hash=sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb \ - --hash=sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98 \ - --hash=sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd \ - --hash=sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2 \ - --hash=sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef \ - --hash=sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f \ - --hash=sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988 \ - --hash=sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b \ - --hash=sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415 \ - --hash=sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2 \ - --hash=sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101 \ - --hash=sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0 \ - --hash=sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582 \ - --hash=sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a \ - --hash=sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975 \ - --hash=sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1 \ - --hash=sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb \ - --hash=sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45 \ - --hash=sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031 \ - --hash=sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9 \ - --hash=sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752 \ - --hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \ - --hash=sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c \ - --hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \ - --hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4 +pynacl==1.3.0 \ + --hash=sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255 \ + --hash=sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c \ + --hash=sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e \ + --hash=sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae \ + --hash=sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621 \ + --hash=sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56 \ + --hash=sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39 \ + --hash=sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310 \ + --hash=sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1 \ + --hash=sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a \ + --hash=sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786 \ + --hash=sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b \ + --hash=sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b \ + --hash=sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f \ + --hash=sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20 \ + --hash=sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415 \ + --hash=sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715 \ + --hash=sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1 \ + --hash=sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0 pysocks==1.6.8 \ --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 -requests==2.20.0 \ - --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ - --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 +requests==2.21.0 \ + --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ + --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b six==1.12.0 \ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \ # via pynacl -stem==1.6.0 \ - --hash=sha256:d7fe1fb13ed5a94d610b5ad77e9f1b3404db0ca0586ded7a34afd323e3b849ed -urllib3==1.23 \ - --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ - --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 +stem==1.7.1 \ + --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 +streamedrequests==1.0.0 \ + --hash=sha256:1d9d07394804a6e1fd66bde74a804e71cab98e6920053865574a459f1cf7d3b7 +unpaddedbase32==0.1.0 \ + --hash=sha256:5e4143fcaf77c9c6b4f60d18301c7570f0dac561dcf9b9aed8b5ba6ead7f218c +urllib3==1.24.2 \ + --hash=sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0 \ + --hash=sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3 werkzeug==0.14.1 \ --hash=sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c \ --hash=sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b \ diff --git a/run-windows-dev.bat b/run-windows-dev.bat new file mode 100755 index 00000000..50de1152 --- /dev/null +++ b/run-windows-dev.bat @@ -0,0 +1,7 @@ +@echo off +echo This script is only intended for use in Onionr development, as it uses a random profile. +set ONIONR_HOME=data%random% +echo Using profile: %ONIONR_HOME% +setlocal +chdir onionr +python onionr.py %* diff --git a/setprofile.sh b/setprofile.sh old mode 100755 new mode 100644 diff --git a/start-daemon.sh b/start-daemon.sh index 1b713100..d427d8e8 100755 --- a/start-daemon.sh +++ b/start-daemon.sh @@ -1,5 +1,5 @@ -#!/usr/bin/bash +#!/bin/bash cd "$(dirname "$0")" echo "starting Onionr daemon..." -echo "run onionr.sh stop to stop the daemon, or onionr.sh start to get output" -nohup ./onionr.sh start & disown > /dev/null +echo "run onionr.sh stop to stop the daemon" +exec nohup ./onionr.sh start > /dev/null 2>&1 & disown