├── .flake8 ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── cloudomate.cfg.example ├── cloudomate ├── __init__.py ├── cmdline.py ├── exceptions │ ├── __init__.py │ └── vps_out_of_stock.py ├── gateway │ ├── __init__.py │ ├── bitpay.py │ ├── blockchain.py │ ├── coinbase.py │ ├── coingate.py │ ├── coinify.py │ ├── coinpayments.py │ ├── custom_mullvad.py │ ├── gateway.py │ └── undergroundprivate.py ├── globals.py ├── hoster │ ├── __init__.py │ ├── hoster.py │ ├── vpn │ │ ├── __init__.py │ │ ├── azirevpn.py │ │ ├── mullvad.py │ │ └── vpn_hoster.py │ └── vps │ │ ├── __init__.py │ │ ├── blueangelhost.py │ │ ├── ccihosting.py │ │ ├── clientarea.py │ │ ├── crowncloud.py │ │ ├── hostsailor.py │ │ ├── libertyvps.py │ │ ├── linevast.py │ │ ├── orangewebsite.py │ │ ├── proxhost.py │ │ ├── pulseservers.py │ │ ├── qhoster.py │ │ ├── routerhosting.py │ │ ├── solusvm_hoster.py │ │ ├── twosync.py │ │ ├── undergroundprivate.py │ │ └── vps_hoster.py ├── test │ ├── __init__.py │ ├── resources │ │ ├── bitpay_invoice_data.json │ │ ├── captcha.png │ │ ├── clientarea_emails.html │ │ ├── clientarea_service.html │ │ ├── clientarea_services.html │ │ ├── coinbase.html │ │ ├── crowncloud_email.html │ │ └── test_settings.cfg │ ├── test_captchasolver.py │ ├── test_clientarea.py │ ├── test_cmdline.py │ ├── test_gateway.py │ ├── test_hoster.py │ ├── test_mullvad.py │ ├── test_settings.py │ ├── test_vpn_hosters.py │ └── test_vps_hosters.py ├── util │ ├── __init__.py │ ├── bitcoinaddress.py │ ├── captchasolver.py │ ├── fakeuserscraper.py │ └── settings.py └── wallet.py ├── setup.cfg └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | exclude = tests/* 4 | max-complexity = 10 5 | ignore = W601, E402 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | env/ 4 | dist/ 5 | *.egg-info/ 6 | *.egg 7 | *.swp 8 | *.py[cod] 9 | __pycache__/ 10 | *.so 11 | *~ 12 | .idea/* 13 | .pypirc 14 | *.sublime-project 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /cloudomate.cfg.example: -------------------------------------------------------------------------------- 1 | [user] 2 | email = 3 | firstname = 4 | lastname = 5 | companyname = 6 | phonenumber = 7 | password = 8 | username = 9 | 10 | [address] 11 | address = 12 | city = 13 | state = 14 | countrycode = 15 | zipcode = 16 | 17 | [payment] 18 | walletpath = 19 | 20 | [server] 21 | ns1 = 22 | ns2 = 23 | hostname = 24 | root_password = 25 | 26 | [anticaptcha] 27 | accountkey = -------------------------------------------------------------------------------- /cloudomate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/__init__.py -------------------------------------------------------------------------------- /cloudomate/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/exceptions/__init__.py -------------------------------------------------------------------------------- /cloudomate/exceptions/vps_out_of_stock.py: -------------------------------------------------------------------------------- 1 | class VPSOutOfStockException(Exception): 2 | """Exception raised when trying to purchase a VPS that is out of stock.""" 3 | 4 | def __init__(self, vps_option, msg=None): 5 | if msg is None: 6 | msg = "VPS Option '{}' is out of stock".format(vps_option.name) 7 | super(Exception, self).__init__(msg) 8 | self.vps_option = vps_option 9 | -------------------------------------------------------------------------------- /cloudomate/gateway/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/gateway/__init__.py -------------------------------------------------------------------------------- /cloudomate/gateway/bitpay.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | from math import pow 8 | 9 | import electrum.bitcoin as bitcoin 10 | from electrum import paymentrequest as pr 11 | from future import standard_library 12 | from future.moves.urllib import request 13 | from future.moves.urllib.parse import urlsplit, parse_qs 14 | 15 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 16 | 17 | standard_library.install_aliases() 18 | 19 | 20 | class BitPay(Gateway): 21 | @staticmethod 22 | def get_name(): 23 | return "BitPay" 24 | 25 | @staticmethod 26 | def extract_info(url): 27 | """ 28 | Extracts amount and BitCoin address from a BitPay URL. 29 | :param url: the BitPay URL like "https://bitpay.com/invoice?id=J3qU6XapEqevfSCW35zXXX" 30 | :return: a tuple of the amount in BitCoin along with the address 31 | """ 32 | # https://bitpay.com/ or https://test.bitpay.com 33 | uspl = urlsplit(url) 34 | base_url = "{0.scheme}://{0.netloc}".format(uspl) 35 | print(base_url) 36 | invoice_id = uspl.query.split("=")[1] 37 | 38 | # On the browser, users have to select between Bitcoin and Bitcoin cash 39 | # trigger bitcoin selection for successful transaction 40 | trigger_url = "{}/invoice-noscript?id={}&buyerSelectedTransactionCurrency=BTC".format(base_url, invoice_id) 41 | print(trigger_url) 42 | request.urlopen(trigger_url) 43 | 44 | # Make the payment 45 | payment_url = "bitcoin:?r={}/i/{}".format(base_url, invoice_id) 46 | print(payment_url) 47 | 48 | # Check for testnet mode 49 | if os.getenv('TESTNET', '0') == '1' and uspl.netloc == 'test.bitpay.com': 50 | bitcoin.set_testnet() 51 | 52 | # get payment request using Electrum's lib 53 | pq = parse_qs(urlsplit(payment_url).query) 54 | out = {k: v[0] for k, v in pq.items()} 55 | payreq = pr.get_payment_request(out.get('r')).get_dict() 56 | 57 | # amount is in satoshis (1/10e8 Bitcoin) 58 | amount = float(payreq.get('amount')) / pow(10, 8) 59 | address = payreq.get('requestor') 60 | 61 | return PaymentInfo(amount, address) 62 | 63 | @staticmethod 64 | def get_gateway_fee(): 65 | """Get the BitPay gateway fee. 66 | 67 | See: https://bitpay.com/pricing 68 | 69 | :return: The BitPay gateway fee 70 | """ 71 | return 0.01 72 | -------------------------------------------------------------------------------- /cloudomate/gateway/blockchain.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from future import standard_library 7 | 8 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 9 | 10 | standard_library.install_aliases() 11 | 12 | 13 | class Blockchain(Gateway): 14 | 15 | @staticmethod 16 | def get_name(): 17 | return "blockchain" 18 | 19 | @staticmethod 20 | def extract_info(url): 21 | amount, address = str(url).split('&') 22 | am = float(amount) 23 | return PaymentInfo(am, address) 24 | -------------------------------------------------------------------------------- /cloudomate/gateway/coinbase.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | 8 | from bs4 import BeautifulSoup 9 | from future import standard_library 10 | 11 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 12 | 13 | from selenium import webdriver 14 | from selenium.webdriver.firefox.options import Options 15 | import geckodriver_autoinstaller 16 | 17 | standard_library.install_aliases() 18 | 19 | 20 | class Coinbase(Gateway): 21 | @staticmethod 22 | def get_name(): 23 | return "coinbase" 24 | 25 | @classmethod 26 | def extract_info(cls, url): 27 | """ 28 | Extracts amount and BitCoin address from a Coinbase URL. 29 | :param url: the Coinbase URL like "https://commerce.coinbase.com/charges/K62GMV5Y" 30 | :return: a tuple of the amount in BitCoin along with the address 31 | """ 32 | geckodriver_autoinstaller.install() # Install the geckodriver of firefox if needed for selenium to work 33 | options = Options() 34 | options.headless = True # don't show firefox window 35 | driver = webdriver.Firefox(options=options) 36 | 37 | driver.get(url) 38 | driver.implicitly_wait(20) # wait for the payment page to completely load 39 | driver.find_element_by_xpath('//img[@alt="Bitcoin"]').click() # click on the bitcoin option 40 | address = cls._extract_address(driver) 41 | amount = cls._extract_amount(driver) 42 | driver.quit() 43 | 44 | return PaymentInfo(amount, address) 45 | 46 | @staticmethod 47 | def get_gateway_fee(): 48 | """Get the coinbase gateway fee. 49 | 50 | See: https://support.coinbase.com/customer/portal/articles/1277919-what-fees-does-coinbase-charge-for-merchant-processing 51 | 52 | :return: The coinbase gateway fee 53 | """ 54 | # I don't think there is any fee anymore 55 | return 0.01 56 | 57 | @staticmethod 58 | def _extract_amount(driver): 59 | """ 60 | Extract amount from driver 61 | :param driver: webpage where the amount and address are stored 62 | :return: Amount to be transferred 63 | """ 64 | return float(driver.find_elements_by_xpath('//div[contains(text(), "BTC")]')[1].text.split(' ')[0]) 65 | 66 | @staticmethod 67 | def _extract_address(driver): 68 | """ 69 | Extract address from driver 70 | :param driver: webpage where the amount and address are stored 71 | :return: Bitcoin address 72 | """ 73 | return driver.find_element_by_id('payment-address').get_attribute('title') 74 | -------------------------------------------------------------------------------- /cloudomate/gateway/coingate.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import json 7 | 8 | from fake_useragent import UserAgent 9 | from future import standard_library 10 | from mechanicalsoup import StatefulBrowser 11 | from websocket import create_connection 12 | 13 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class CoinGate(Gateway): 19 | 20 | @staticmethod 21 | def get_gateway_fee(): 22 | return 0.0 23 | 24 | @staticmethod 25 | def get_name(): 26 | return "coingate" 27 | 28 | @staticmethod 29 | def has_pay_amount(data): 30 | if 'message' not in data: 31 | return False 32 | if 'type' in data: 33 | return False 34 | return isinstance(data['message'], dict) and 'pay_amount' in data['message'] and data['message']['pay_amount'] 35 | 36 | @staticmethod 37 | def extract_info(url): 38 | user_agent = UserAgent(fallback="Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0") 39 | browser = StatefulBrowser(user_agent=user_agent.random) 40 | headers = { 41 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 42 | 'accept-encoding': 'gzip, deflate, br', 43 | 'accept-language': 'en-US,en;q=0.9', 44 | 'upgrade-insecure-requests': '1', 45 | } 46 | response = browser.open(url, headers=headers) 47 | cookies = [] 48 | for cookie in response.cookies: 49 | cookie = cookie 50 | cookies.append('{}: {}'.format(cookie.name, cookie.value)) 51 | ws = create_connection("wss://coingate.com/cable", cookie='; '.join(cookies), origin='https://coingate.com') 52 | 53 | invoice_id = url.split('/')[-1] 54 | 55 | debug_log = [] 56 | 57 | identifier = json.dumps({ 58 | "channel": "InvoiceChannel", 59 | "invoice_uuid": invoice_id 60 | }) 61 | data1 = json.dumps({ 62 | "command": "subscribe", 63 | "identifier": identifier 64 | }) 65 | debug_log.append('> ' + data1) 66 | ws.send(data1) 67 | 68 | data2 = json.dumps({ 69 | "command": "message", 70 | "identifier": identifier, 71 | "data": json.dumps({"invoice_uuid": invoice_id, "action": "check"}) 72 | }) 73 | debug_log.append('> ' + data2) 74 | ws.send(data2) 75 | data3 = json.dumps({ 76 | "command": "message", 77 | "identifier": identifier, 78 | "data": json.dumps({ 79 | "pay_currency_id": 8, 80 | "email": "", 81 | "ln": False, 82 | "action": "select_pay_currency" 83 | }) 84 | }) 85 | debug_log.append('> ' + data3) 86 | ws.send(data3) 87 | 88 | for i in range(0, 20): 89 | result = ws.recv() 90 | debug_log.append('< ' + result) 91 | result_json = json.loads(result) 92 | if CoinGate.has_pay_amount(result_json): 93 | return PaymentInfo(float(result_json['message']['pay_amount']), result_json['message']['address']) 94 | print("CoinGate: No payment amount found \nDebug information: \n{}".format('\n'.join(debug_log))) 95 | return None 96 | 97 | 98 | if __name__ == '__main__': 99 | CoinGate.extract_info('https://coingate.com/invoice/5112d1ec-a160-4f84-958d-d0c73543b034') 100 | -------------------------------------------------------------------------------- /cloudomate/gateway/coinify.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import re 7 | 8 | from fake_useragent import UserAgent 9 | from future import standard_library 10 | from mechanicalsoup import StatefulBrowser 11 | 12 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 13 | 14 | standard_library.install_aliases() 15 | 16 | 17 | class Coinify(Gateway): 18 | 19 | @staticmethod 20 | def get_name(): 21 | return "coinify" 22 | 23 | @staticmethod 24 | def extract_info(url): 25 | user_agent = UserAgent(fallback="Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0") 26 | browser = StatefulBrowser(user_agent=user_agent.random) 27 | browser.open(url) 28 | page = browser.get_current_page() 29 | result = re.search(r'"bitcoin:([\w\d]+)\?amount=(\d+\.\d+)&', str(page)) 30 | return PaymentInfo(float(result.group(2)), result.group(1)) 31 | -------------------------------------------------------------------------------- /cloudomate/gateway/coinpayments.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from future import standard_library 7 | 8 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 9 | 10 | standard_library.install_aliases() 11 | 12 | 13 | class CoinPayments(Gateway): 14 | 15 | @staticmethod 16 | def reuse_session(): 17 | return True 18 | 19 | @staticmethod 20 | def get_name(): 21 | return "CoinPayments" 22 | 23 | @staticmethod 24 | def extract_info(browser, settings): 25 | """ 26 | Uses the browser to walk through the payment process of coinpayments 27 | :browser: a browser currently at https://www.coinpayments.net/index.php 28 | :settings: the usersettings to be given to coinpayments 29 | :return: a tuple of the amount in BitCoin along with the address 30 | """ 31 | 32 | # select coin to pay 33 | browser.select_form(nr=0) 34 | form = browser.get_current_form() 35 | form['selcoin'] = 'BTC' 36 | form['checkout'] = 1 37 | form['first_name'] = settings.get('user', "firstname") 38 | form['last_name'] = settings.get('user', "lastname") 39 | form['email'] = settings.get('user', "email") 40 | form.set('screen_res', '1920x1080', force=True) 41 | browser.submit_selected() 42 | 43 | # go to actual payment page 44 | browser.open("https://www.coinpayments.net/index.php?cmd=checkout") 45 | page = browser.get_current_page() 46 | 47 | try: 48 | bitcoin_url = page.find('div', {'class': 'pay-block'}).find('a')['href'] 49 | except AttributeError: 50 | print("Too many open transactions, try connecting from a different IP") 51 | raise 52 | 53 | dirty_address, dirty_amount = bitcoin_url.split("?") 54 | 55 | address = dirty_address.split(":")[1] 56 | amount = float(dirty_amount.split("=")[1]) 57 | 58 | return PaymentInfo(amount, address) 59 | 60 | @staticmethod 61 | def get_gateway_fee(): 62 | """Get the CoinPayments gateway fee. 63 | 64 | """ 65 | return 0.00 66 | -------------------------------------------------------------------------------- /cloudomate/gateway/custom_mullvad.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from future import standard_library 7 | 8 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 9 | 10 | standard_library.install_aliases() 11 | 12 | 13 | class CustomMullvad(Gateway): 14 | @staticmethod 15 | def get_name(): 16 | return "CustomMullvad" 17 | 18 | @staticmethod 19 | def extract_info(page): 20 | """ 21 | Extracts amount and BitCoin address from MullVad's payment page. 22 | :param page: the HTML page returned after sumbitting the order 23 | :return: a tuple of the amount in BitCoin along with the address 24 | """ 25 | month_price = "" 26 | bitcoin_address = "" 27 | 28 | # Parse page to get bitcoin ammount and address 29 | for line in page.split("\n"): 30 | if "1 month = " in line: 31 | month_price = float(line.strip().split(" ")[3]) 32 | if "input readonly" in line: 33 | bitcoin_address_line = line.strip().split(" ")[3].split("=")[1] 34 | bitcoin_address = bitcoin_address_line.partition("\"")[-1] 35 | bitcoin_address = bitcoin_address.rpartition("\"")[0] 36 | 37 | return PaymentInfo(month_price, bitcoin_address) 38 | 39 | @staticmethod 40 | def get_gateway_fee(): 41 | """Get the BitPay gateway fee. 42 | 43 | See: https://bitpay.com/pricing 44 | 45 | :return: The BitPay gateway fee 46 | """ 47 | return 0.00 48 | -------------------------------------------------------------------------------- /cloudomate/gateway/gateway.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from abc import abstractmethod, ABCMeta 7 | from collections import namedtuple 8 | 9 | from future import standard_library 10 | from future.utils import with_metaclass 11 | 12 | standard_library.install_aliases() 13 | 14 | PaymentInfo = namedtuple('PaymentInfo', ['amount', 'address']) 15 | 16 | 17 | class Gateway(with_metaclass(ABCMeta)): 18 | 19 | @staticmethod 20 | @abstractmethod 21 | def use_session(): 22 | return False 23 | 24 | @staticmethod 25 | @abstractmethod 26 | def reuse_session(): 27 | return False 28 | 29 | @staticmethod 30 | @abstractmethod 31 | def get_name(): 32 | return "" 33 | 34 | @staticmethod 35 | @abstractmethod 36 | def extract_info(url): 37 | return PaymentInfo(None, None) 38 | 39 | @staticmethod 40 | @abstractmethod 41 | def get_gateway_fee(): 42 | return 0.0 43 | 44 | @classmethod 45 | def estimate_price(cls, cost): 46 | return cost * (1.0 + cls.get_gateway_fee()) 47 | -------------------------------------------------------------------------------- /cloudomate/gateway/undergroundprivate.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from fake_useragent import UserAgent 7 | from future import standard_library 8 | from mechanicalsoup import StatefulBrowser 9 | 10 | from cloudomate.gateway.gateway import Gateway, PaymentInfo 11 | 12 | standard_library.install_aliases() 13 | 14 | 15 | class UndergroundPrivate(Gateway): 16 | @staticmethod 17 | def get_name(): 18 | return "SpectroCoin" 19 | 20 | @classmethod 21 | def extract_info(cls, url): 22 | """ 23 | Extracts amount and BitCoin address from a UndergroundPrivate payment URL. 24 | :param url: the URL like https://spectrocoin.com/en/order/view/1045356-0X6XzpZi.html 25 | :return: a tuple of the amount in BitCoin along with the address 26 | """ 27 | user_agent = UserAgent(fallback="Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0") 28 | browser = StatefulBrowser(user_agent=user_agent.random) 29 | browser.open(url) 30 | soup = browser.get_current_page() 31 | 32 | amount = soup.select_one('div.payAmount').text.split(" ")[0] 33 | address = soup.select_one('div.address').text 34 | return PaymentInfo(float(amount), address) 35 | 36 | @staticmethod 37 | def get_gateway_fee(): 38 | return 0.0 39 | -------------------------------------------------------------------------------- /cloudomate/globals.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.4" 2 | global_testnet = False 3 | __BASE_URL__ = 'https://codesalad.nl:5000/cloudomate' 4 | -------------------------------------------------------------------------------- /cloudomate/hoster/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/hoster/__init__.py -------------------------------------------------------------------------------- /cloudomate/hoster/hoster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from abc import abstractmethod, ABCMeta 8 | 9 | from fake_useragent import UserAgent 10 | from future import standard_library 11 | from future.utils import with_metaclass 12 | from mechanicalsoup import StatefulBrowser 13 | 14 | from cloudomate import wallet as wallet_util 15 | 16 | standard_library.install_aliases() 17 | 18 | 19 | class Hoster(with_metaclass(ABCMeta)): 20 | def __init__(self, settings): 21 | self._browser = self._create_browser() 22 | self._settings = settings 23 | 24 | @abstractmethod 25 | def get_configuration(self): 26 | """Get Hoster configuration. 27 | 28 | :return: Returns configuration for the Hoster instance 29 | """ 30 | pass 31 | 32 | @staticmethod 33 | @abstractmethod 34 | def get_gateway(): 35 | """Get payment gateway used by the Hoster. 36 | 37 | :return: Returns the payment gateway module 38 | """ 39 | pass 40 | 41 | @staticmethod 42 | @abstractmethod 43 | def get_metadata(): 44 | """Get metadata about the Hoster. 45 | 46 | :return: Returns tuple of name and website url 47 | """ 48 | pass 49 | 50 | @classmethod 51 | @abstractmethod 52 | def get_options(cls): 53 | """Get Hoster options. 54 | 55 | :return: Returns list of Hoster options 56 | """ 57 | pass 58 | 59 | @staticmethod 60 | @abstractmethod 61 | def get_required_settings(): 62 | """Get settings required by the Hoster. 63 | 64 | :return: Returns dictionary with sections as keys and the required settings in those sections as values 65 | """ 66 | pass 67 | 68 | @abstractmethod 69 | def get_status(self): 70 | """Get Hoster configuration. 71 | 72 | :return: Returns status of the Hoster instance 73 | """ 74 | pass 75 | 76 | def get_browser(self): 77 | return self._browser 78 | 79 | @classmethod 80 | def pay(cls, wallet, gateway, url, browser=None, settings=None): 81 | """Do a payment (should be moved to the payment gateways?) 82 | 83 | :param wallet: the wallet to pay with 84 | :param gateway: gateway through which to make the payment 85 | :param url: url from which the amount and address can be extracted 86 | """ 87 | 88 | name, _ = cls.get_metadata() 89 | 90 | # Make the payment 91 | print("Purchasing {} instance".format(name)) 92 | if gateway.reuse_session(): 93 | info = gateway.extract_info(browser, settings) 94 | else: 95 | info = gateway.extract_info(url) 96 | 97 | print(('Paying %s BTC to %s' % (info.amount, info.address))) 98 | fee = wallet_util.get_network_fee() 99 | print(('Calculated fee: %s' % fee)) 100 | transaction_hash = wallet.pay(info.address, info.amount, fee) 101 | print('Done purchasing') 102 | return transaction_hash 103 | 104 | @abstractmethod 105 | def purchase(self, wallet, option): 106 | """Purchase Hoster. 107 | 108 | :param wallet: The Electrum wallet to use for payments 109 | :param option: Hoster option to purchase 110 | """ 111 | pass 112 | 113 | @staticmethod 114 | def _create_browser(): 115 | user_agent = UserAgent(fallback="Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0") 116 | return StatefulBrowser(user_agent=user_agent.random) 117 | -------------------------------------------------------------------------------- /cloudomate/hoster/vpn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/hoster/vpn/__init__.py -------------------------------------------------------------------------------- /cloudomate/hoster/vpn/azirevpn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import datetime 8 | import sys 9 | from builtins import round 10 | 11 | import requests 12 | from currency_converter import CurrencyConverter 13 | from future import standard_library 14 | 15 | from cloudomate.gateway.bitpay import BitPay 16 | from cloudomate import wallet as wallet_util 17 | from cloudomate.hoster.vpn.vpn_hoster import VpnHoster, VpnOption, VpnStatus, VpnConfiguration 18 | 19 | standard_library.install_aliases() 20 | 21 | 22 | class AzireVpn(VpnHoster): 23 | REGISTER_URL = "https://www.azirevpn.com/en/manager/auth/register" 24 | CONFIGURATION_URL = "https://www.azirevpn.com/cfg/openvpn/generate?os=others&country=se1&nat=0&keys=0&protocol=udp&tls=gcm&port=random" 25 | LOGIN_URL = "https://www.azirevpn.com/manager/auth/login" 26 | ORDER_URL = "https://www.azirevpn.com/manager/order" 27 | OPTIONS_URL = "https://www.azirevpn.com" 28 | DASHBOARD_URL = "https://www.azirevpn.com/manager" 29 | 30 | ''' 31 | Information about the Hoster 32 | ''' 33 | 34 | @staticmethod 35 | def get_gateway(): 36 | return BitPay 37 | 38 | @staticmethod 39 | def get_metadata(): 40 | return "AzireVPN", "https://www.azirevpn.com/" 41 | 42 | @staticmethod 43 | def get_required_settings(): 44 | return {"user": ["username", "password"]} 45 | 46 | ''' 47 | Action methods of the Hoster that can be called 48 | ''' 49 | 50 | def get_configuration(self): 51 | response = requests.get(self.CONFIGURATION_URL) 52 | ovpn = response.text 53 | return VpnConfiguration(self._settings.get("user", "username"), 54 | self._settings.get("user", "password"), ovpn) 55 | 56 | @classmethod 57 | def get_options(cls): 58 | # Get string with price from the website 59 | browser = cls._create_browser() 60 | browser.open(cls.OPTIONS_URL) 61 | soup = browser.get_current_page().find_all('strong') 62 | string = str(soup) 63 | words = string.split() 64 | 65 | # Calculate the price in USD 66 | eur = float(words[2]) 67 | c = CurrencyConverter() 68 | price = round(c.convert(eur, "EUR", "USD"), 2) 69 | 70 | name, _ = cls.get_metadata() 71 | option = VpnOption(name, "OpenVPN", price, sys.maxsize, sys.maxsize) 72 | return [option] 73 | 74 | def get_status(self): 75 | self._login() 76 | 77 | # Retrieve the expiration date 78 | self._browser.open(self.DASHBOARD_URL) 79 | soup = self._browser.get_current_page() 80 | time = soup.select_one("div.dashboard time") 81 | date = time["datetime"] 82 | 83 | # Parse the expiration date 84 | date = date[0:-3] + date[-2:] # Remove colon (:) in timezone 85 | expiration = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z") 86 | 87 | # Determine the status 88 | now = datetime.datetime.now(datetime.timezone.utc) 89 | online = False 90 | if now <= expiration: 91 | online = True 92 | return VpnStatus(online, expiration) 93 | 94 | def purchase(self, wallet, option): 95 | # Prepare for the purchase on the AzireVPN website 96 | self._register() 97 | self._login() 98 | page = self._order() 99 | 100 | # Make the payment 101 | return self.pay(wallet, page.url) 102 | 103 | ''' 104 | Hoster-specific methods that are needed to perform the actions 105 | ''' 106 | 107 | def pay(self, wallet, url): 108 | 109 | self._browser.open(url) 110 | soup = self._browser.get_current_page() 111 | address = soup.select_one("div.transaction > input").get("value") 112 | amount = float(soup.select_one("div.transaction > input:nth-of-type(2)").get("value")) 113 | fee = wallet_util.get_network_fee() 114 | print(('Calculated fee: %s' % fee)) 115 | transaction_hash = wallet.pay(address, amount, fee) 116 | print('Done purchasing') 117 | return transaction_hash 118 | 119 | def _register(self): 120 | self._browser.open(self.REGISTER_URL) 121 | form = self._browser.select_form() 122 | form["username"] = self._settings.get("user", "username") 123 | form["password"] = self._settings.get("user", "password") 124 | form["password_confirmation"] = self._settings.get("user", "password") 125 | page = self._browser.submit_selected() 126 | 127 | if page.url == self.REGISTER_URL: 128 | # An error occurred 129 | soup = self._browser.get_current_page() 130 | ul = soup.select_one("ul.alert-danger") 131 | print(ul.get_text()) 132 | sys.exit(2) 133 | 134 | return page 135 | 136 | def _login(self): 137 | self._browser.open(self.LOGIN_URL) 138 | form = self._browser.select_form() 139 | form["username"] = self._settings.get("user", "username") 140 | form["password"] = self._settings.get("user", "password") 141 | page = self._browser.submit_selected() 142 | 143 | if page.url == self.LOGIN_URL: 144 | # An error occurred 145 | soup = self._browser.get_current_page() 146 | ul = soup.select_one("ul.alert-danger") 147 | print(ul.get_text()) 148 | sys.exit(2) 149 | 150 | return page 151 | 152 | def _order(self): 153 | self._browser.open(self.ORDER_URL) 154 | form = self._browser.select_form("form#orderForm") 155 | form["package"] = "7" 156 | form["payment_gateway"] = "coinpayment" 157 | form["coinpayment_crypto"] = "BTC" 158 | form["tos"] = True 159 | page = self._browser.submit_selected() 160 | 161 | return page 162 | -------------------------------------------------------------------------------- /cloudomate/hoster/vpn/mullvad.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import datetime 7 | import os 8 | import shutil 9 | import sys 10 | import zipfile 11 | from builtins import float 12 | from builtins import round 13 | from builtins import str 14 | 15 | from forex_python.converter import CurrencyRates 16 | from future import standard_library 17 | 18 | from cloudomate.gateway.custom_mullvad import CustomMullvad 19 | from cloudomate.hoster.vpn.vpn_hoster import VpnHoster, VpnOption, VpnStatus, VpnConfiguration 20 | from cloudomate.util.captchasolver import CaptchaSolver 21 | 22 | standard_library.install_aliases() 23 | 24 | if sys.version_info > (3, 0): 25 | from urllib.request import urlretrieve 26 | else: 27 | from urllib import urlretrieve 28 | 29 | 30 | class MullVad(VpnHoster): 31 | REGISTER_URL = "https://www.mullvad.net/en/account/create/" 32 | LOGIN_URL = "https://www.mullvad.net/en/account/login/" 33 | ORDER_URL = "https://www.mullvad.net/en/account" 34 | OPTIONS_URL = "https://www.mullvad.net/en" 35 | INFO_URL = "https://mullvad.net/en/guides/linux-openvpn-installation/" 36 | CONFIGURATION_URL = "https://mullvad.net/en/download/config/" 37 | 38 | ''' 39 | Information about the Hoster 40 | ''' 41 | 42 | @staticmethod 43 | def get_gateway(): 44 | return CustomMullvad 45 | 46 | @staticmethod 47 | def get_metadata(): 48 | return "MullVad", "https://www.mullvad.net/" 49 | 50 | @staticmethod 51 | def get_required_settings(): 52 | return {"anticaptcha": ["accountkey"]} 53 | 54 | ''' 55 | Action methods of the Hoster that can be called 56 | ''' 57 | 58 | def get_configuration(self): 59 | self._download_files() 60 | 61 | userpass_file = open("./config-files/mullvad_userpass.txt", "r") 62 | username = userpass_file.readline().strip() 63 | password = userpass_file.readline().strip() 64 | 65 | conf_file = open("./config-files/mullvad_se-sto.conf", "r") 66 | conf = conf_file.read() 67 | 68 | ca_file = open("./config-files/mullvad_ca.crt", "r") 69 | ca = ca_file.read() 70 | 71 | # include the certificate 72 | conf = conf.replace("ca mullvad_ca.crt", "\n" + ca + "") 73 | 74 | # remove userpass as it is added by cmdline.py 75 | conf = conf.replace("auth-user-pass mullvad_userpass.txt", "") 76 | 77 | return VpnConfiguration(username, password, conf) 78 | 79 | @classmethod 80 | def get_options(cls): 81 | browser = cls._create_browser() 82 | browser.open(cls.OPTIONS_URL) 83 | soup = browser.get_current_page() 84 | p = soup.select("p.hero-description") 85 | string = p[1].get_text() 86 | 87 | # Calculate the price in USD 88 | eur = float(string[string.index("\u20ac") + 1:string.index("/")]) 89 | price = round(CurrencyRates().convert("EUR", "USD", eur), 2) 90 | 91 | name, _ = cls.get_metadata() 92 | option = VpnOption(name, "OpenVPN", price, sys.maxsize, sys.maxsize) 93 | return [option] 94 | 95 | def get_status(self): 96 | self._login() 97 | 98 | # Retrieve days left until expiration 99 | expire_date = self._get_expiration_date() 100 | expire_date = datetime.datetime.strptime(expire_date, "%d %B %Y") 101 | 102 | online = (expire_date > self._get_current_date()) 103 | 104 | return VpnStatus(online, expire_date) 105 | 106 | def purchase(self, wallet, option): 107 | # Prepare for the purchase on the MullVad website 108 | if self._settings.has_key("user", "accountnumber"): 109 | self._login() 110 | else: 111 | self._register() 112 | page = self._order() 113 | 114 | self.pay(wallet, self.get_gateway(), str(page)) 115 | 116 | ''' 117 | Hoster-specific methods that are needed to perform the actions 118 | ''' 119 | 120 | def _register(self): 121 | self._browser.open(self.REGISTER_URL) 122 | form = self._browser.select_form() 123 | soup = self._browser.get_current_page() 124 | 125 | # Get captcha needed for registration 126 | img = soup.select("img.captcha")[0]["src"] 127 | urlretrieve("https://www.mullvad.net" + img, 128 | "captcha.png") 129 | 130 | # Solve captcha 131 | captcha_solver = CaptchaSolver(self._settings.get("anticaptcha", 132 | "accountkey")) 133 | solution = captcha_solver.solve_captcha_text_case_sensitive( 134 | "./captcha.png") 135 | form["captcha_1"] = solution 136 | 137 | self._browser.session.headers["Referer"] = self._browser.get_url() 138 | 139 | page = self._browser.submit_selected() 140 | 141 | # Check if registration was successful 142 | if page.url == self.REGISTER_URL: 143 | # An error occurred 144 | print("The captcha was wrong") 145 | sys.exit(2) 146 | 147 | new_account_number = 0 148 | 149 | # Parse page to get new account number 150 | new_page = str(self._browser.get_current_page()) 151 | for line in new_page.split("\n"): 152 | if "Your account number:" in line: 153 | new_account_number = line.split(":")[1] 154 | new_account_number = new_account_number.split("<")[0].strip(" ") 155 | break 156 | self._settings.put("user", "accountnumber", new_account_number) 157 | self._settings.save_settings() 158 | 159 | return page 160 | 161 | def _login(self): 162 | self._browser.open(self.LOGIN_URL) 163 | form = self._browser.select_form() 164 | 165 | # Use account number to login 166 | form["account_number"] = self._settings.get("user", "accountnumber") 167 | self._browser.session.headers["Referer"] = self._browser.get_url() 168 | page = self._browser.submit_selected() 169 | 170 | # Check if login was successful 171 | if page.url == self.LOGIN_URL: 172 | print("The account number is wrong") 173 | sys.exit(2) 174 | 175 | return page 176 | 177 | def _order(self): 178 | self._browser.open(self.ORDER_URL) 179 | form = self._browser.select_form('form[action="/en/account/bitcoin/"]') 180 | 181 | # Order one month 182 | form["months"] = "1" 183 | self._browser.session.headers["Referer"] = self._browser.get_url() 184 | self._browser.submit_selected() 185 | page = self._browser.get_current_page() 186 | 187 | return page 188 | 189 | def _get_expiration_date(self): 190 | # Checks if VPN expired 191 | soup = self._browser.get_current_page() 192 | expire_date = soup.select(".balance-header")[0].get_text() 193 | expire_date = expire_date.split("\n")[2].strip() 194 | return expire_date 195 | 196 | @staticmethod 197 | def _get_current_date(): 198 | return datetime.datetime.now() 199 | 200 | # Download configuration files for setting up VPN and extract them 201 | def _download_files(self): 202 | # Fill information on website to get right files for openVPN 203 | self._browser.open(self.CONFIGURATION_URL) 204 | form = self._browser.select_form() 205 | form["account_token"] = self._settings.get("user", "accountnumber") 206 | form["platform"] = "linux" 207 | form["region"] = "se-sto" 208 | form["port"] = "0" 209 | self._browser.session.headers["Referer"] = self._browser.get_url() 210 | response = self._browser.submit_selected() 211 | content = response.content 212 | 213 | # Create the folder that will store the configuration files 214 | result = os.popen("mkdir -p config-files").read() 215 | print(result) 216 | 217 | # Download the zip file to the right location 218 | files_path = "./config-files/config.zip" 219 | with open(files_path, "wb") as output: 220 | output.write(content) 221 | 222 | # Unzip files 223 | zip_file = zipfile.ZipFile(files_path, "r") 224 | for member in zip_file.namelist(): 225 | filename = os.path.basename(member) 226 | # Skip directories 227 | if not filename: 228 | continue 229 | 230 | # Copy file (taken from zipfile's extract) 231 | source = zip_file.open(member) 232 | target = open(os.path.join("./config-files/", filename), "wb") 233 | with source, target: 234 | shutil.copyfileobj(source, target) 235 | 236 | # Delete zip file 237 | os.remove(files_path) 238 | -------------------------------------------------------------------------------- /cloudomate/hoster/vpn/vpn_hoster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from abc import abstractmethod 7 | from collections import namedtuple 8 | 9 | from future import standard_library 10 | 11 | from cloudomate.hoster.hoster import Hoster 12 | 13 | standard_library.install_aliases() 14 | 15 | VpnConfiguration = namedtuple('VpnConfiguration', ['username', 'password', 'ovpn']) 16 | VpnOption = namedtuple('VpnOption', ['name', 'protocol', 'price', 'bandwidth', 'speed']) # Price in USD 17 | VpnStatus = namedtuple('VpnStatus', ['online', 'expiration']) # Online is a boolean, expiration an ISO datetime 18 | 19 | 20 | class VpnHoster(Hoster): 21 | """ 22 | Abstract class for VPN Hosters. 23 | This defines all required subclass methods and implements some common methods. 24 | """ 25 | 26 | @abstractmethod 27 | def get_configuration(self): 28 | """Get Hoster configuration. 29 | 30 | :return: Returns VpnConfiguration for the VPN Hoster instance 31 | """ 32 | pass 33 | 34 | @classmethod 35 | @abstractmethod 36 | def get_options(cls): 37 | """Get Hoster options. 38 | 39 | :return: Returns list of VpnOption objects 40 | """ 41 | pass 42 | 43 | @abstractmethod 44 | def get_status(self): 45 | """Get Hoster configuration. 46 | 47 | :return: Returns VpnStatus of the VPN Hoster instance 48 | """ 49 | pass 50 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/hoster/vps/__init__.py -------------------------------------------------------------------------------- /cloudomate/hoster/vps/blueangelhost.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import itertools 7 | from builtins import int 8 | from builtins import super 9 | 10 | from bs4 import Tag 11 | from future import standard_library 12 | from past.builtins import unicode 13 | 14 | from cloudomate.gateway.bitpay import BitPay 15 | from cloudomate.hoster.vps.clientarea import ClientArea 16 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 17 | from cloudomate.hoster.vps.vps_hoster import VpsConfiguration 18 | from cloudomate.hoster.vps.vps_hoster import VpsOption 19 | from cloudomate.hoster.vps.vps_hoster import VpsStatus 20 | from cloudomate.hoster.vps.vps_hoster import VpsStatusResource 21 | 22 | standard_library.install_aliases() 23 | 24 | 25 | class BlueAngelHost(SolusvmHoster): 26 | CART_URL = 'https://www.billing.blueangelhost.com/cart.php?a=view' 27 | 28 | # true if you can enable tuntap in the control panel 29 | TUN_TAP_SETTINGS = False 30 | 31 | def __init__(self, settings): 32 | super(BlueAngelHost, self).__init__(settings) 33 | 34 | ''' 35 | Information about the Hoster 36 | ''' 37 | 38 | @staticmethod 39 | def get_clientarea_url(): 40 | return 'https://www.billing.blueangelhost.com/clientarea.php' 41 | 42 | @staticmethod 43 | def get_email_url(): 44 | return 'https://www.billing.blueangelhost.com/viewemail.php' # + ?id=123456 45 | 46 | @staticmethod 47 | def get_gateway(): 48 | return BitPay 49 | 50 | @staticmethod 51 | def get_metadata(): 52 | return 'BlueAngelHost', 'https://www.blueangelhost.com/' 53 | 54 | @staticmethod 55 | def get_required_settings(): 56 | return { 57 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 58 | 'address': ['address', 'city', 'state', 'zipcode'], 59 | 'server': ['hostname', 'root_password', 'ns1', 'ns2'] 60 | } 61 | 62 | def _create_clientarea(self): 63 | if self._clientarea is None: 64 | self._clientarea = BAHClientArea(self.get_browser(), self.get_clientarea_url(), 65 | self.get_email_url(), self._settings) 66 | return self._clientarea 67 | 68 | ''' 69 | Action methods of the Hoster that can be called 70 | ''' 71 | 72 | @classmethod 73 | def get_options(cls): 74 | browser = cls._create_browser() 75 | browser.open("https://www.blueangelhost.com/openvz-vps/") 76 | options = cls._parse_options(browser.get_current_page()) 77 | 78 | browser.open("https://www.blueangelhost.com/kvm-vps/") 79 | options = itertools.chain(options, cls._parse_options(browser.get_current_page(), is_kvm=True)) 80 | return list(options) 81 | 82 | def get_configuration(self): 83 | """ 84 | Overrides the default configuration method as BlueAngelHost doesn't use the server password during 85 | registration 86 | :return: IP and Password 87 | """ 88 | server_info = self.get_clientarea().get_server_information_from_email() 89 | ip = server_info.get('ip_address') 90 | password = server_info.get('server_password') 91 | 92 | return VpsConfiguration(ip, password) 93 | 94 | def get_status(self): 95 | status = super().get_status() 96 | 97 | # Get server stats 98 | page = self._browser.open('{}&api=json&act=vpsmanage&stats=1'.format(status.clientarea.url)) 99 | data = page.json() 100 | 101 | memory = VpsStatusResource(self._convert_mb_to_gb(data['info']['ram']['used']), 102 | self._convert_mb_to_gb(data['info']['ram']['limit'])) 103 | storage = VpsStatusResource(data['info']['disk']['used_gb'], 104 | data['info']['disk']['limit_gb']) 105 | bandwidth = VpsStatusResource(data['info']['bandwidth']['used_gb'], 106 | data['info']['bandwidth']['limit_gb']) 107 | 108 | return VpsStatus(memory, storage, bandwidth, status.online, status.expiration, status.clientarea) 109 | 110 | def purchase(self, wallet, option): 111 | self._browser.open(option.purchase_url) 112 | self._submit_server_form() 113 | self._browser.open(self.CART_URL) 114 | summary = self._browser.get_current_page().find('div', class_='summary-container') 115 | self._browser.follow_link(summary.find('a', class_='btn-checkout')) 116 | 117 | self._browser.select_form(selector='form[name=orderfrm]') 118 | self._browser.get_current_form()['customfield[4]'] = 'Google' 119 | self._fill_user_form(self.get_gateway().get_name()) 120 | 121 | self._browser.select_form(nr=0) 122 | self._browser.submit_selected() 123 | return self.pay(wallet, self.get_gateway(), self._browser.get_url()) 124 | 125 | ''' 126 | Hoster-specific methods that are needed to perform the actions 127 | ''' 128 | 129 | @staticmethod 130 | def _convert_mb_to_gb(mb): 131 | return mb / 1024.0 132 | 133 | @classmethod 134 | def _parse_options(cls, page, is_kvm=False): 135 | month = page.find('div', {'id': 'monthly_price'}) 136 | details = month.findAll('div', {'class': 'plan_table'}) 137 | for column in details: 138 | yield cls._parse_blue_options(column, is_kvm=is_kvm) 139 | 140 | @staticmethod 141 | def _parse_blue_options(column, is_kvm=False): 142 | if is_kvm: 143 | split_char = ' ' 144 | else: 145 | split_char = ':' 146 | 147 | price = column.find('div', {'class': 'plan_price_m'}).text.strip() 148 | price = price.split('$')[1].split('/')[0] 149 | planinfo = column.find('ul', {'class': 'plan_info_list'}) 150 | info = planinfo.findAll('li') 151 | cpu = info[0].text.split(split_char)[1].strip() 152 | ram = info[1].text.split(split_char)[1].strip() 153 | storage = info[2].text.split(split_char)[1].strip() 154 | connection = info[3].text.split(split_char)[1].strip() 155 | bandwidth = info[4].text.split("h")[1].strip() 156 | 157 | return VpsOption( 158 | name=column.find('div', {'class': 'plan_title'}).find('h4').text, 159 | price=float(price), 160 | cores=int(cpu.split('C')[0].strip()), 161 | memory=float(ram.split('G')[0].strip()), 162 | storage=float(storage.split('G')[0].strip()), 163 | connection=int(connection.split('G')[0].strip()), 164 | bandwidth=float(bandwidth.split('T')[0].strip()), 165 | purchase_url=column.find('a')['href'] 166 | ) 167 | 168 | def _submit_server_form(self): 169 | """ 170 | Fills in the form containing server configuration 171 | :return: 172 | """ 173 | form = self._browser.select_form('form#frmConfigureProduct') 174 | self._fill_server_form() 175 | form['customfield[135]'] = 'ubuntu-16.04-x86_64' # Ubuntu 64 bit 176 | self._browser.submit_selected() 177 | 178 | 179 | class BAHClientArea(ClientArea): 180 | """ 181 | Modified ClientAria for BlueAngelHost, 182 | Extended for looking up server information and control panel credentials 183 | """ 184 | email_url = None 185 | 186 | def __init__(self, browser, clientarea_url, email_url, user_settings): 187 | self.email_url = email_url 188 | ClientArea.__init__(self, browser, clientarea_url, user_settings) 189 | 190 | def get_emails(self): 191 | """ 192 | Returns a list of dicts containing email metadata: {id, title} 193 | This can be used to further select certains emails to parse 194 | """ 195 | self._browser.open(self._url + "?action=emails") 196 | soup = self._browser.get_current_page() 197 | extracted = self._extract_emails(soup) 198 | return extracted 199 | 200 | def get_server_information_from_email(self): 201 | """ 202 | Returns the parsed server information from email 203 | """ 204 | email_id = None 205 | for email in self.get_emails(): 206 | e_id = email['id'] 207 | title = email['title'] 208 | if 'ready' in title.lower(): 209 | email_id = e_id 210 | break 211 | self._browser.open(self.email_url + '?id=' + email_id) 212 | soup = self._browser.get_current_page() 213 | 214 | server_info = { 215 | 'ip_address': None, 216 | 'server_user': None, 217 | 'server_password': None, 218 | 'vmuser': None, 219 | 'vmuser_password': None, 220 | 'control_panel_url': None 221 | } 222 | 223 | ps = soup.findAll('p') 224 | 225 | # map of server_info fields to the labels in the e-mail 226 | server_keyword = 'Hostname' 227 | server_fields = { 228 | 'ip_address': 'Main IP', 229 | 'server_user': 'Username', 230 | 'server_password': 'Root Password' 231 | } 232 | 233 | vm_keyword = 'Manager Details' 234 | vm_fields = { 235 | 'vmuser': 'Username', 236 | 'vmuser_password': 'Password' 237 | } 238 | 239 | for p in ps: 240 | for line in p: 241 | self._parse_email_section(p, line, server_keyword, server_fields, server_info) 242 | self._parse_email_section(p, line, vm_keyword, vm_fields, server_info) 243 | if isinstance(line, Tag) and line.name == 'a': 244 | server_info['control_panel_url'] = unicode(line.next) 245 | 246 | return server_info 247 | 248 | @staticmethod 249 | def _parse_email_section(p, line, keyword, fields, server_info): 250 | if keyword in p.text: 251 | for key, label in fields.items(): 252 | line_str = unicode(line) 253 | if label in line_str: 254 | server_info[key] = line_str.split(':')[1].strip() 255 | 256 | @staticmethod 257 | def _extract_emails(soup): 258 | table = soup.find('table', {'id': 'tableEmailsList'}).tbody 259 | emails = [] 260 | for row in table.findAll('tr'): 261 | emails.append({ 262 | 'id': row['onclick'].split('\'')[1].split('id=')[1], 263 | 'title': row.findAll('td')[1].text 264 | }) 265 | return emails 266 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/ccihosting.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | from builtins import int 8 | 9 | from future import standard_library 10 | from mechanicalsoup import LinkNotFoundError 11 | 12 | from cloudomate.gateway.coinpayments import CoinPayments 13 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 14 | from cloudomate.hoster.vps.vps_hoster import VpsOption 15 | 16 | standard_library.install_aliases() 17 | 18 | 19 | class CCIHosting(SolusvmHoster): 20 | CART_URL = 'https://www.ccihosting.com/accounts/cart.php?a=confdomains' 21 | OPTIONS_URL = 'https://www.ccihosting.com/offshore-vps.html' 22 | 23 | # true if you can enable tuntap in the control panel 24 | TUN_TAP_SETTINGS = False 25 | 26 | ''' 27 | Information about the Hoster 28 | ''' 29 | 30 | @staticmethod 31 | def get_clientarea_url(): 32 | return 'https://www.ccihosting.com/accounts/clientarea.php' 33 | 34 | @staticmethod 35 | def get_gateway(): 36 | return CoinPayments 37 | 38 | @staticmethod 39 | def get_metadata(): 40 | return 'CCIHosting', 'https://www.ccihosting.com/' 41 | 42 | @staticmethod 43 | def get_required_settings(): 44 | return { 45 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 46 | 'address': ['address', 'city', 'state', 'zipcode', 'countrycode'], 47 | 'server': ['hostname', 'root_password', 'ns1', 'ns2'] 48 | } 49 | 50 | ''' 51 | Action methods of the Hoster that can be called 52 | ''' 53 | 54 | @classmethod 55 | def get_options(cls): 56 | browser = cls._create_browser() 57 | browser.open(cls.OPTIONS_URL) 58 | return list(cls._parse_options(browser.get_current_page())) 59 | 60 | def purchase(self, wallet, option): 61 | self._browser.open(option.purchase_url) 62 | 63 | form = self._browser.select_form('form#frmConfigureProduct') 64 | self._fill_server_form() 65 | form['configoption[214]'] = '1193' # Ubuntu 16.04 66 | self._browser.submit_selected() 67 | self._browser.open(self.CART_URL) 68 | 69 | summary = self._browser.get_current_page().find('div', class_='summary-container') 70 | self._browser.follow_link(summary.find('a', class_='btn-checkout')) 71 | 72 | try: 73 | self._browser.select_form(selector='form#frmCheckout') 74 | except LinkNotFoundError: 75 | print("Too many open transactions, try connecting from a different IP") 76 | raise 77 | 78 | self._fill_user_form(self.get_gateway().get_name()) 79 | 80 | self._browser.select_form(nr=0) # Go to payment form 81 | self._browser.submit_selected() 82 | 83 | return self.pay(wallet, self.get_gateway(), self._browser.get_url(), self._browser, self._settings) 84 | 85 | ''' 86 | Hoster-specific methods that are needed to perform the actions 87 | ''' 88 | 89 | @staticmethod 90 | def _convert_gigabyte(number, unit): 91 | u = unit.lower() 92 | n = float(number) 93 | if u == 'kb': 94 | n /= 1024.0 * 1024.0 95 | elif u == 'mb': 96 | n /= 1024.0 97 | elif u == 'gb': 98 | pass 99 | elif u == 'tb': 100 | n *= 1024.0 101 | else: 102 | raise ValueError('Unknown unit {}'.format(u)) 103 | 104 | return n 105 | 106 | @classmethod 107 | def _parse_options(cls, page): 108 | tables = page.findAll('div', class_='pricing') 109 | for column in tables: 110 | yield cls._parse_cci_options(column) 111 | 112 | @staticmethod 113 | def _parse_cci_options(column): 114 | header = column.find('div', class_='phead') 115 | price = column.find('span', class_='starting-price') 116 | info = column.find('ul').findAll('li') 117 | try: 118 | url = column.find('a')['onclick'].split("'")[3] 119 | except KeyError: 120 | url = column.find('a')['href'] 121 | return VpsOption( 122 | name=header.find('h2').contents[0], 123 | price=float(price.contents[0]), 124 | cores=int(info[1].find('strong').contents[0]), 125 | memory=float(info[2].find('strong').contents[0]), 126 | storage=float(info[3].find('strong').contents[0]), 127 | bandwidth=sys.maxsize, 128 | connection=0.01, # See FAQ at https://www.ccihosting.com/offshore-vps.html 129 | purchase_url=url 130 | ) 131 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/clientarea.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import datetime 8 | import re 9 | import sys 10 | from collections import namedtuple 11 | 12 | from future import standard_library 13 | 14 | standard_library.install_aliases() 15 | 16 | ClientAreaService = namedtuple('ClientAreaService', ['name', 'price', 'next_due', 'status', 'url']) 17 | 18 | 19 | class ClientArea(object): 20 | """ 21 | Clientarea is the name of the control panel used in many VPS providers. The purpose of this class is to use 22 | this control panel in an automated manner. 23 | """ 24 | ACTION_POSTFIX = '?action=services&language=english' 25 | 26 | def __init__(self, browser, clientarea_url, user_settings): 27 | self._browser = browser 28 | self._services = None 29 | self._url = clientarea_url 30 | self._login(user_settings.get('user', 'email'), user_settings.get('user', 'password')) 31 | 32 | def get_ip(self, service=None): 33 | if service is None: 34 | service = self.get_services_first() 35 | self._browser.open(service.url) 36 | soup = self._browser.get_current_page() 37 | rows = soup.select('div#domain > div.row') 38 | if len(rows) > 0: 39 | for row in rows: 40 | divs = row.findAll('div') 41 | if 'IP' in divs[0].strong.text: 42 | ip = divs[1].text.strip() 43 | if len(ip) < 7: 44 | ip = re.search(r'\b((?:\d{1,3}\.){3}\d{1,3})\b', soup.text).group(0) 45 | return ip 46 | 47 | def get_services(self): 48 | if self._services is None: 49 | self._browser.open(self._url + self.ACTION_POSTFIX) 50 | soup = self._browser.get_current_page() 51 | rows = soup.select('table#tableServicesList tbody tr') 52 | self._services = [self._parse_service_row(row) for row in rows] 53 | 54 | return self._services 55 | 56 | def get_services_first(self): 57 | return self.get_services()[0] 58 | 59 | def _parse_service_row(self, row): 60 | columns = row.findAll('td') 61 | 62 | name = columns[0].strong.text 63 | 64 | price_string = columns[1].text 65 | dot_index = price_string.index('.') 66 | price = float(price_string[1:dot_index + 3]) 67 | 68 | next_due = columns[2].span.text 69 | next_due = datetime.datetime.strptime(next_due, '%Y-%m-%d') 70 | 71 | status = columns[3].span.text.lower() 72 | 73 | if len(columns) > 4: 74 | url = columns[4].a['href'] 75 | url = url.split('.php') 76 | url = self._url + url[1] 77 | else: # Fixes twosync 78 | url = self._url + '/' + row['data-url'] 79 | 80 | return ClientAreaService(name, price, next_due, status, url) 81 | 82 | def _login(self, email, password): 83 | """ 84 | Login into the clientarea. Exits program if unsuccesful. 85 | :return: The clientarea homepage on succesful login. 86 | """ 87 | self._browser.open(self._url) 88 | self._browser.select_form('.logincontainer form') 89 | self._browser['username'] = email 90 | self._browser['password'] = password 91 | page = self._browser.submit_selected() 92 | if "incorrect=true" in page.url: 93 | print("Login failure") 94 | sys.exit(2) 95 | self.home_page = page 96 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/crowncloud.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from builtins import int 7 | 8 | from future import standard_library 9 | from mechanicalsoup import LinkNotFoundError 10 | 11 | from cloudomate.gateway.bitpay import BitPay 12 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 13 | from cloudomate.hoster.vps.vps_hoster import VpsOption 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class CrownCloud(SolusvmHoster): 19 | CART_URL = 'https://crowncloud.net/clients/cart.php?a=view' 20 | OPTIONS_URL = 'http://crowncloud.net/openvz.php' 21 | 22 | # true if you can enable tuntap in the control panel 23 | TUN_TAP_SETTINGS = False 24 | 25 | ''' 26 | Information about the Hoster 27 | ''' 28 | 29 | @staticmethod 30 | def get_clientarea_url(): 31 | return 'https://crowncloud.net/clients/clientarea.php' 32 | 33 | @staticmethod 34 | def get_gateway(): 35 | return BitPay 36 | 37 | @staticmethod 38 | def get_metadata(): 39 | return "CrownCloud", "https://crowncloud.net/" 40 | 41 | @staticmethod 42 | def get_required_settings(): 43 | return { 44 | 'user': [ 45 | 'firstname', 46 | 'lastname', 47 | 'email', 48 | 'password', 49 | 'phonenumber', 50 | ], 51 | 'address': [ 52 | 'address', 53 | 'city', 54 | 'state', 55 | 'zipcode', 56 | ], 57 | 'server': [ 58 | 'root_password' 59 | ] 60 | } 61 | 62 | ''' 63 | Action methods of the Hoster that can be called 64 | ''' 65 | 66 | @classmethod 67 | def get_options(cls): 68 | browser = cls._create_browser() 69 | browser.open(cls.OPTIONS_URL) 70 | 71 | # Get all pricing boxes 72 | page = browser.get_current_page() 73 | return list(cls._parse_options(page)) 74 | 75 | def purchase(self, wallet, option): 76 | self._browser.open(option.purchase_url) 77 | self._submit_server_form() 78 | self._browser.open(self.CART_URL) 79 | page = self._submit_user_form() 80 | return self.pay(wallet, self.get_gateway(), page.url) 81 | 82 | ''' 83 | Hoster-specific methods that are needed to perform the actions 84 | ''' 85 | 86 | @classmethod 87 | def _parse_options(cls, page): 88 | # Get the connection speed. 89 | connection = page.findAll('p') 90 | connection = connection[3].text.split('Shared ') 91 | connection = connection[1].split(' Gbit/s') 92 | connection = int(connection[0]) 93 | tables = page.findAll('table') 94 | for table in tables: # There are multiple tables with server options on the page 95 | for row in table.findAll('tr'): 96 | if len(row.findAll('td')) > 0: # Ignore headers 97 | option = cls._parse_row(row, connection) 98 | if option is not None: 99 | yield option 100 | 101 | @staticmethod 102 | def _parse_row(row, connection): 103 | details = row.findAll('td') 104 | name = details[0].text 105 | price = details[5].text 106 | if 'yearly only' in price: 107 | return None # Only yearly price possible 108 | try: 109 | i = price.index('/') 110 | except ValueError: 111 | return None # Invalid price string 112 | price = int(price[1:i]) 113 | 114 | cores = int(details[3].text[0]) 115 | 116 | memory = float(details[1].text[0:4]) / 1000 117 | 118 | storage = details[2].text.split(' GB') 119 | storage = int(storage[0]) 120 | 121 | bandwidth = details[4].text.split(' GB') 122 | bandwidth = bandwidth[0] 123 | 124 | purchase_url = details[6].find('a')['href'] 125 | 126 | return VpsOption(name, cores, memory, storage, bandwidth, connection, price, purchase_url) 127 | 128 | def _submit_server_form(self): 129 | try: 130 | form = self._browser.select_form('form#orderfrm') 131 | self._fill_server_form() 132 | 133 | form.form['action'] = 'https://crowncloud.net/clients/cart.php' 134 | print("Frm1") 135 | # form.form['method'] = 'post' 136 | except LinkNotFoundError: 137 | print("Frm2") 138 | form = self._browser.select_form('form#frmConfigureProduct') 139 | self._fill_server_form() 140 | 141 | form['billingcycle'] = 'monthly' 142 | form['configoption[1]'] = '56' 143 | form['configoption[8]'] = '52' 144 | 145 | try: # The extra bandwidth option is not always available 146 | form['configoption[9]'] = '0' 147 | except LinkNotFoundError: 148 | pass 149 | 150 | return self._browser.submit_selected() 151 | 152 | def _submit_user_form(self): 153 | # Select the correct submit button 154 | form = self._browser.select_form('form#frmCheckout') 155 | soup = self._browser.get_current_page() 156 | submit = soup.select('button#btnCompleteOrder')[0] 157 | form.choose_submit(submit) 158 | 159 | # Let SolusVM handle the rest 160 | gateway = self.get_gateway() 161 | self._fill_user_form(gateway.get_name(), errorbox_class='errorbox') 162 | 163 | # Redirect to BitPay 164 | self._browser.select_form(nr=0) 165 | return self._browser.submit_selected() 166 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/hostsailor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | from builtins import super 8 | 9 | from future import standard_library 10 | 11 | from cloudomate.gateway.coingate import CoinGate 12 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 13 | from cloudomate.hoster.vps.vps_hoster import VpsOption 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class HostSailor(SolusvmHoster): 19 | # true if you can enable tuntap in the control panel 20 | TUN_TAP_SETTINGS = False 21 | 22 | def __init__(self, settings): 23 | super(HostSailor, self).__init__(settings) 24 | 25 | ''' 26 | Information about the Hoster 27 | ''' 28 | 29 | @staticmethod 30 | def get_clientarea_url(): 31 | return 'https://clients.hostsailor.com/clientarea.php' 32 | 33 | @staticmethod 34 | def get_gateway(): 35 | return CoinGate 36 | 37 | @staticmethod 38 | def get_metadata(): 39 | return 'hostsailor', 'https://hostsailor.com' 40 | 41 | @staticmethod 42 | def get_required_settings(): 43 | return { 44 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 45 | 'address': ['address', 'city', 'state', 'zipcode'], 46 | } 47 | 48 | ''' 49 | Action methods of the Hoster that can be called 50 | ''' 51 | 52 | @classmethod 53 | def get_options(cls): 54 | browser = cls._create_browser() 55 | browser.open("https://hostsailor.com/vps-hosting/openvz-ssd-vps/") 56 | options = cls._parse_openvz_ssd_hosting(browser.get_current_page()) 57 | return list(options) 58 | 59 | @classmethod 60 | def _parse_openvz_ssd_hosting(cls, page): 61 | options = page.find_all('div', {'class': 'crumina-pricing-tables-item'}) 62 | for option in options: 63 | price_usd = float(option.find('h2', {'class': 'rate'}).text[1:]) 64 | list_elements = option.find_all('li', {'class': 'position-item'}) 65 | yield VpsOption( 66 | name=option.find('h5', {'class': 'pricing-title'}).text.strip(), 67 | storage=list_elements[2].text.strip().split(' ')[2], 68 | cores=list_elements[5].text.strip().split(' ')[1], 69 | memory=cls.parse_memory(list_elements[0]), 70 | bandwidth=cls.parse_bandwidth(list_elements[8]), 71 | connection=list_elements[9].text.strip().split(' ')[2].replace('Gbit', ''), 72 | price=price_usd, 73 | purchase_url=option.find('a', {'class', 'btn'})['href'] 74 | ) 75 | 76 | @staticmethod 77 | def parse_memory(memory): 78 | amount = memory.text.strip().split(' ')[1] 79 | if amount.endswith('MB'): 80 | return int(amount.replace('MB', '')) / 1000 81 | else: 82 | return int(amount.replace('GB', '')) 83 | 84 | @staticmethod 85 | def parse_bandwidth(bandwidth): 86 | amount = bandwidth.text.strip().split(' ')[1] 87 | if amount.endswith('GB'): 88 | return int(amount.replace('GB', '')) / 1000 89 | else: 90 | return int(amount.replace('TB', '')) 91 | 92 | def purchase(self, wallet, option): 93 | self._browser.open(option.purchase_url) 94 | self._server_form() 95 | self._browser.open('https://clients.hostsailor.com/cart.php?a=view') 96 | link = self._browser.get_current_page().find('a', {'id': 'checkout'}) 97 | self.get_browser().follow_link(link) 98 | 99 | form = self._browser.select_form(selector='form#frmCheckout') 100 | form['customfield[96]'] = 'Google' 101 | self._fill_user_form(self.get_gateway().get_name()) 102 | payment_link = self._browser.get_current_page().find('form')['action'] 103 | print("Payment link: " + payment_link) 104 | return self.pay(wallet, self.get_gateway(), payment_link) 105 | 106 | ''' 107 | Hoster-specific methods that are needed to perform the actions 108 | ''' 109 | 110 | def _server_form(self): 111 | """ 112 | Fills in the form containing server configuration. 113 | :return: 114 | """ 115 | form = self._browser.select_form('form#frmConfigureProduct') 116 | self._fill_server_form() 117 | form['configoption[560]'] = '8168' # Ubuntu 16.04 118 | self._browser.submit_selected() 119 | page = self._browser.get_current_page() 120 | if page.find('li'): 121 | print(page.text) 122 | sys.exit(2) 123 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/libertyvps.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from builtins import super 7 | 8 | from future import standard_library 9 | from mechanicalsoup.utils import LinkNotFoundError 10 | 11 | from cloudomate.hoster.vps.clientarea import ClientArea 12 | from cloudomate.gateway.coinpayments import CoinPayments 13 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 14 | from cloudomate.hoster.vps.vps_hoster import VpsConfiguration 15 | from cloudomate.hoster.vps.vps_hoster import VpsOption 16 | 17 | standard_library.install_aliases() 18 | 19 | 20 | class LibertyVPS(SolusvmHoster): 21 | CART_URL = 'https://libertyvps.net/clients/cart.php?a=view' 22 | 23 | # true if you can enable tuntap in the control panel 24 | TUN_TAP_SETTINGS = True 25 | 26 | _settings = None 27 | _controlpanel = None 28 | 29 | def __init__(self, settings): 30 | super(LibertyVPS, self).__init__(settings) 31 | 32 | ''' 33 | Information about the Hoster 34 | ''' 35 | 36 | @staticmethod 37 | def get_clientarea_url(): 38 | return 'https://libertyvps.net/clients/clientarea.php' 39 | 40 | @staticmethod 41 | def get_email_url(): 42 | return 'https://libertyvps.net/clients/viewemail.php' # + ?id=123456 43 | 44 | @staticmethod 45 | def get_gateway(): 46 | return CoinPayments 47 | 48 | @staticmethod 49 | def get_metadata(): 50 | return 'libertyvps', 'https://libertyvps.net/' 51 | 52 | @staticmethod 53 | def get_required_settings(): 54 | return { 55 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 56 | 'address': ['address', 'city', 'state', 'zipcode'], 57 | } 58 | 59 | ''' 60 | Action methods of the Hoster that can be called 61 | ''' 62 | 63 | @classmethod 64 | def get_options(cls): 65 | """ 66 | Linux (OpenVZ) and Windows (KVM) pages are slightly different, therefore their pages are parsed by different 67 | methods. Windows configurations allow a selection of Linux distributions, but not vice-versa. 68 | :return: possible configurations. 69 | """ 70 | browser = cls._create_browser() 71 | browser.open("https://libertyvps.net/offshore-vps") 72 | options = cls._parse_openvz_hosting(browser.get_current_page()) 73 | lst = list(options) 74 | 75 | return lst 76 | 77 | def purchase(self, wallet, option): 78 | self._browser.open(option.purchase_url) 79 | self._server_form() 80 | 81 | self._browser.open(self.CART_URL) 82 | forms = self._browser.get_current_page().find_all('form') 83 | self._browser.select_form(forms[2]) 84 | self._browser.submit_selected() 85 | 86 | try: 87 | self._browser.select_form(selector='form#frmCheckout') 88 | except LinkNotFoundError: 89 | print("Too many open transactions, try connecting from a different IP") 90 | raise 91 | 92 | self._fill_user_form(self.get_gateway().get_name()) 93 | 94 | self._browser.select_form(nr=0) # Go to payment form 95 | self._browser.submit_selected() 96 | 97 | return self.pay(wallet, self.get_gateway(), self._browser.get_url(), self._browser, self._settings) 98 | 99 | ''' 100 | Hoster-specific methods that are needed to perform the actions 101 | ''' 102 | 103 | def _server_form(self): 104 | """ 105 | Fills in the form containing server configuration. 106 | :return: 107 | """ 108 | form = self._browser.select_form('form') 109 | 110 | self._fill_server_form() 111 | form['configoption[1]'] = '4' # Ubuntu 16.04 112 | self._browser.submit_selected() 113 | 114 | @classmethod 115 | def _parse_openvz_hosting(cls, page): 116 | table = page.find('div', {'class': 'uds-pricing-table'}) 117 | thead = page.find('thead') 118 | tfoot = page.find('tfoot') 119 | number_of_entries = len(table.find('tr', {'class': 'even'}).find_all('td')) 120 | 121 | for idx in range(1, number_of_entries + 1): 122 | header_elements = thead.find_all('tr')[1].find('th', {'class': 'column-' + str(idx)}).find_all('p') 123 | list_elements = table.find_all('td', {'class': 'column-' + str(idx)}) 124 | link_element = tfoot.find('th', {'class': 'column-' + str(idx)}) 125 | 126 | yield VpsOption( 127 | name=header_elements[0].text, 128 | storage=list_elements[2].text.strip().split('GB')[0], 129 | cores=list_elements[0].text.strip().split(' ')[0].split('\xa0')[0], 130 | memory=list_elements[1].text.strip().split(' ')[0], 131 | bandwidth=list_elements[3].text.replace('TB', '').strip(), 132 | connection=list_elements[4].text.strip().split('Gbps')[0], 133 | price=float(header_elements[1].text[1:]), 134 | purchase_url=link_element.find('a')['href'], 135 | ) 136 | 137 | def get_configuration(self): 138 | clientarea = self._create_clientarea() 139 | 140 | ip = clientarea.get_server_information_from_email()['ip_address'] 141 | password = clientarea.get_server_information_from_email()['server_password'] 142 | 143 | return VpsConfiguration(ip, password) 144 | 145 | def _create_clientarea(self): 146 | if self._clientarea is None: 147 | self._clientarea = LibertyVPSClientArea(self.get_browser(), self.get_clientarea_url(), 148 | self.get_email_url(), self._settings) 149 | return self._clientarea 150 | 151 | 152 | class LibertyVPSClientArea(ClientArea): 153 | email_url = None 154 | 155 | def __init__(self, browser, clientarea_url, email_url, user_settings): 156 | self.email_url = email_url 157 | ClientArea.__init__(self, browser, clientarea_url, user_settings) 158 | 159 | def get_emails(self): 160 | """ 161 | Returns a list of dicts containing email metadata: {id, title} 162 | This can be used to further select certains emails to parse 163 | """ 164 | self._browser.open(self._url + "?action=emails") 165 | soup = self._browser.get_current_page() 166 | extracted = self._extract_emails(soup) 167 | return extracted 168 | 169 | def get_server_information_from_email(self): 170 | """ 171 | Returns the parsed server information from email 172 | """ 173 | email_id = self._get_email_id() 174 | self._browser.open(self.email_url + '?id=' + email_id) 175 | soup = self._browser.get_current_page() 176 | 177 | server_info = { 178 | 'ip_address': None, 179 | 'server_user': None, 180 | 'server_password': None, 181 | } 182 | 183 | body = soup.find('div', {'class': 'panel-body main-content'}).text 184 | server_info['server_user'] = 'root' 185 | server_info['server_password'] = body.split('root password: ')[1].split('\n')[0] 186 | server_info['ip_address'] = body.split('Main IP: ')[1].split('\n')[0] 187 | 188 | return server_info 189 | 190 | def _get_email_id(self): 191 | for email in self.get_emails(): 192 | e_id = email['id'] 193 | title = email['title'] 194 | if title == 'Your new server login details': 195 | return e_id 196 | 197 | @staticmethod 198 | def _extract_emails(soup): 199 | table = soup.find('table', {'id': 'tableEmailsList'}).tbody 200 | emails = [] 201 | for row in table.findAll('tr'): 202 | emails.append({ 203 | 'id': row['onclick'].split('\'')[1].split('id=')[1], 204 | 'title': row.findAll('td')[1].text 205 | }) 206 | return emails 207 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/linevast.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import json 7 | import re 8 | import sys 9 | from builtins import round 10 | from builtins import super 11 | 12 | from currency_converter import CurrencyConverter 13 | from future import standard_library 14 | from mechanicalsoup.utils import LinkNotFoundError 15 | 16 | from cloudomate.gateway.coinbase import Coinbase 17 | from cloudomate.hoster.vps.clientarea import ClientArea 18 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 19 | from cloudomate.hoster.vps.vps_hoster import VpsOption 20 | 21 | standard_library.install_aliases() 22 | 23 | 24 | class LineVast(SolusvmHoster): 25 | CART_URL = 'https://panel.linevast.de/cart.php?a=view' 26 | 27 | # true if you can enable tuntap in the control panel 28 | TUN_TAP_SETTINGS = True 29 | 30 | _settings = None 31 | _controlpanel = None 32 | 33 | def __init__(self, settings): 34 | super(LineVast, self).__init__(settings) 35 | 36 | ''' 37 | Information about the Hoster 38 | ''' 39 | 40 | @staticmethod 41 | def get_clientarea_url(): 42 | return 'https://panel.linevast.de/clientarea.php' 43 | 44 | @staticmethod 45 | def get_email_url(): 46 | return 'https://panel.linevast.de/viewemail.php' # + ?id=123456 47 | 48 | @staticmethod 49 | def get_gateway(): 50 | return Coinbase 51 | 52 | @staticmethod 53 | def get_metadata(): 54 | return 'linevast', 'https://linevast.de/' 55 | 56 | @staticmethod 57 | def get_required_settings(): 58 | return { 59 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 60 | 'address': ['address', 'city', 'state', 'zipcode'], 61 | } 62 | 63 | def _create_clientarea(self): 64 | if self._clientarea is None: 65 | self._clientarea = LineVastClientArea(self.get_browser(), self.get_clientarea_url(), 66 | self.get_email_url(), self._settings) 67 | return self._clientarea 68 | 69 | def _create_controlpanel(self): 70 | if self._controlpanel is None: 71 | cl = self._create_clientarea() 72 | sinfo = cl.get_server_information_from_email() 73 | self._controlpanel = ControlPanel(self.get_browser(), sinfo['control_panel_url'], 74 | sinfo['vmuser'], sinfo['vmuser_password']) 75 | return self._controlpanel 76 | 77 | ''' 78 | Action methods of the Hoster that can be called 79 | ''' 80 | 81 | @classmethod 82 | def get_options(cls): 83 | """ 84 | Linux (OpenVZ) and Windows (KVM) pages are slightly different, therefore their pages are parsed by different 85 | methods. Windows configurations allow a selection of Linux distributions, but not vice-versa. 86 | :return: possible configurations. 87 | """ 88 | browser = cls._create_browser() 89 | browser.open("https://panel.linevast.de/cart.php?gid=1&language=english") 90 | options = cls._parse_openvz_hosting(browser.get_current_page()) 91 | lst = list(options) 92 | 93 | return lst 94 | 95 | def purchase(self, wallet, option): 96 | self._browser.open(option.purchase_url) 97 | self._server_form() 98 | self._browser.open(self.CART_URL) 99 | 100 | summary = self._browser.get_current_page().find('div', class_='summary-container') 101 | self._browser.follow_link(summary.find('a', class_='btn-checkout')) 102 | 103 | form = self._browser.select_form(selector='form#frmCheckout') 104 | form['acceptdomainwiderruf1'] = True 105 | form['acceptdomainwiderruf2'] = True 106 | self._fill_user_form(self.get_gateway().get_name()) 107 | 108 | self._browser.select_form(nr=0) # Go to payment form 109 | self._browser.submit_selected() 110 | return self.pay(wallet, self.get_gateway(), self._browser.get_url()) 111 | 112 | ''' 113 | Hoster-specific methods that are needed to perform the actions 114 | ''' 115 | 116 | def _server_form(self): 117 | """ 118 | Fills in the form containing server configuration. 119 | :return: 120 | """ 121 | form = self._browser.select_form('form#frmConfigureProduct') 122 | self._fill_server_form() 123 | try: 124 | form['configoption[61]'] = '657' # Ubuntu 16.04 125 | except LinkNotFoundError: 126 | form['configoption[125]'] = '549' # Ubuntu 16.04 127 | self._browser.submit_selected() 128 | 129 | @classmethod 130 | def _parse_openvz_hosting(cls, page): 131 | options = page.find_all('div', {'class': 'price-table'}) 132 | for idx, option in enumerate(options, start=1): 133 | list_elements = option.find_all('li') 134 | price_eur = float(option.find('div', {'class', 'price'}).span.text[:].replace(',', '.')) 135 | c = CurrencyConverter() 136 | price_usd = round(c.convert(price_eur, 'EUR', 'USD'), 2) 137 | yield VpsOption( 138 | name=option.find('div', {'class': 'top-area'}).text.strip(), 139 | storage=list_elements[2].text.strip().split(' ')[0].split('GB')[0], 140 | cores=list_elements[0].text.strip().split(' ')[0], 141 | memory=list_elements[1].text.strip().split(' ')[0], 142 | bandwidth=sys.maxsize, 143 | connection=1, 144 | price=price_usd, 145 | purchase_url='https://panel.linevast.de' + option.find('a', {'class': 'order-button'})['href'], 146 | ) 147 | 148 | @staticmethod 149 | def _extract_vi_from_links(links): 150 | for link in links: 151 | if "_v=" in link.url: 152 | return link.url.split("_v=")[1] 153 | return False 154 | 155 | @staticmethod 156 | def _check_login(text): 157 | data = json.loads(text) 158 | if data['success'] and data['success'] == '1': 159 | return True 160 | return False 161 | 162 | ''' 163 | Control panel actions 164 | ''' 165 | 166 | def enable_tun_tap(self): 167 | self._create_controlpanel() 168 | return self._controlpanel.enable_tun_tap() 169 | 170 | def change_root_password(self, new_password): 171 | self._create_controlpanel() 172 | return self._controlpanel.change_root_password(new_password) 173 | 174 | def get_status_control_panel(self): 175 | self._create_controlpanel() 176 | return self._controlpanel.get_status() 177 | 178 | 179 | class LineVastClientArea(ClientArea): 180 | email_url = None 181 | 182 | def __init__(self, browser, clientarea_url, email_url, user_settings): 183 | self.email_url = email_url 184 | ClientArea.__init__(self, browser, clientarea_url, user_settings) 185 | 186 | def get_emails(self): 187 | """ 188 | Returns a list of dicts containing email metadata: {id, title} 189 | This can be used to further select certains emails to parse 190 | """ 191 | self._browser.open(self._url + "?action=emails") 192 | soup = self._browser.get_current_page() 193 | extracted = self._extract_emails(soup) 194 | return extracted 195 | 196 | def get_server_information_from_email(self): 197 | """ 198 | Returns the parsed server information from email 199 | """ 200 | email_id = self._get_email_id() 201 | self._browser.open(self.email_url + '?id=' + email_id) 202 | soup = self._browser.get_current_page() 203 | 204 | server_info = { 205 | 'ip_address': None, 206 | 'server_user': None, 207 | 'server_password': None, 208 | 'vmuser': None, 209 | 'vmuser_password': None, 210 | 'control_panel_url': None 211 | } 212 | 213 | tds = iter(soup.findAll('td')) 214 | while True: 215 | try: 216 | tdcontent = tds.next().renderContents().strip().decode('utf-8') 217 | if tdcontent == 'IP-Address:' and not server_info['ip_address']: 218 | server_info['ip_address'] = tds.next().renderContents().strip().decode('utf-8') 219 | elif tdcontent == 'User:' and not server_info['server_user']: 220 | server_info['server_user'] = tds.next().renderContents().strip().decode('utf-8') 221 | elif tdcontent == 'Password:' and not server_info['server_password']: 222 | server_info['server_password'] = tds.next().renderContents().strip().decode('utf-8') 223 | elif tdcontent == 'Link:' and not server_info['control_panel_url']: 224 | server_info['control_panel_url'] = tds.next().renderContents().strip().decode('utf-8') 225 | elif tdcontent == 'User:' and not server_info['vmuser']: 226 | server_info['vmuser'] = tds.next().renderContents().strip().decode('utf-8') 227 | elif tdcontent == 'Password:' and not server_info['vmuser_password']: 228 | server_info['vmuser_password'] = tds.next().renderContents().strip().decode('utf-8') 229 | except StopIteration: 230 | break 231 | 232 | return server_info 233 | 234 | def _get_email_id(self): 235 | for email in self.get_emails(): 236 | e_id = email['id'] 237 | title = email['title'] 238 | if title == 'New Server Information': 239 | return e_id 240 | 241 | @staticmethod 242 | def _extract_emails(soup): 243 | table = soup.find('table', {'id': 'tableEmailsList'}).tbody 244 | emails = [] 245 | for row in table.findAll('tr'): 246 | emails.append({ 247 | 'id': row['onclick'].split('\'')[1].split('id=')[1], 248 | 'title': row.findAll('td')[1].text 249 | }) 250 | return emails 251 | 252 | 253 | class ControlPanel(object): 254 | """ 255 | Control panel is allows the user to configure more advanced settings, such as TUN/TAP, rootpassword, hostname, etc. 256 | """ 257 | 258 | _vi = None 259 | 260 | _valid_acts = {'istun', 'rootpassword'} 261 | 262 | def __init__(self, browser, control_panel_url, vmuser, vmuser_password): 263 | self._browser = browser 264 | self._url = control_panel_url 265 | self._login(vmuser, vmuser_password) 266 | # self._enter_management() 267 | self._get_vi() 268 | 269 | def _login(self, user, password): 270 | """ 271 | Login into the clientarea. Exits program if unsuccesful. 272 | :return: The clientarea homepage on succesful login. 273 | """ 274 | self._browser.open(self._url) 275 | self._browser.select_form('form') 276 | self._browser['username'] = user 277 | self._browser['password'] = password 278 | page = self._browser.submit_selected() 279 | if "incorrect=true" in page.url: 280 | print("Login failure") 281 | sys.exit(2) 282 | 283 | def _get_vi(self): 284 | """ 285 | The verification id needed for making POSTS to the control panel server. 286 | The purchased vm is first selected for management, from there the 'vi' can be parsed from source. 287 | :return: vi - verification id. 288 | """ 289 | if not self._vi: 290 | self._browser.open(self._url) 291 | soup = self._browser.get_current_page() 292 | pattern = re.compile(r'control\.php\?_v=(.+)') 293 | ahref = soup.findAll('a', href=pattern)[0]['href'] 294 | self._browser.open(self._url + '/' + ahref) 295 | msoup = self._browser.get_current_page() 296 | mpattern = re.compile(r'vi:\s*"(.+?)"') 297 | self._vi = mpattern.search(str(msoup)).group(1) 298 | print(self._vi) 299 | return self._vi 300 | 301 | def get_status(self): 302 | """ 303 | Requests the status of the server 304 | :return: server status in json 305 | """ 306 | res = self._browser.session.post(self._url + '/_vm_remote.php', data={'act': 'getstats', 'vi': self._get_vi()}) 307 | return res.json() 308 | 309 | def _change_setting(self, act=None, opt=None): 310 | """ 311 | Changes the a server setting, see _valid_acts for possible settings. 312 | :param act: setting/action 313 | :param opt: the new value. For binary (true/false, on/off) settings, use integers 1/0. Otherwise, string. 314 | :return: True if the setting was successfully changed. Else false. 315 | """ 316 | if act not in self._valid_acts: 317 | raise ValueError("Setting action not found, available: [%s]" % ', '.join(self._valid_acts)) 318 | 319 | data = { 320 | 'act': act, 321 | 'opt': opt, 322 | 'vi': self._get_vi() 323 | } 324 | print("posting to: %s" % self._url + '/_vm_remote.php') 325 | res = self._browser.session.post(self._url + '/_vm_remote.php', data=data, verify=True) 326 | return res.status_code == 200 327 | 328 | def enable_tun_tap(self): 329 | print("Enabling TUN/TAP") 330 | return self._change_setting('istun', 1) 331 | 332 | def change_root_password(self, new_password): 333 | print("Changing password to: " + new_password) 334 | return self._change_setting('rootpassword', new_password) 335 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/orangewebsite.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from builtins import round 7 | from builtins import super 8 | 9 | from currency_converter import CurrencyConverter 10 | from future import standard_library 11 | 12 | from cloudomate.hoster.vps.clientarea import ClientArea 13 | from cloudomate.gateway.coinpayments import CoinPayments 14 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 15 | from cloudomate.hoster.vps.vps_hoster import VpsConfiguration 16 | from cloudomate.hoster.vps.vps_hoster import VpsOption 17 | 18 | standard_library.install_aliases() 19 | 20 | 21 | class OrangeWebsite(SolusvmHoster): 22 | CART_URL = 'https://secure.orangewebsite.com/cart.php?a=view' 23 | 24 | # true if you can enable tuntap in the control panel 25 | TUN_TAP_SETTINGS = True 26 | 27 | _settings = None 28 | _controlpanel = None 29 | 30 | def __init__(self, settings): 31 | super(OrangeWebsite, self).__init__(settings) 32 | 33 | ''' 34 | Information about the Hoster 35 | ''' 36 | 37 | @staticmethod 38 | def get_clientarea_url(): 39 | return 'https://secure.orangewebsite.com/clientarea.php' 40 | 41 | @staticmethod 42 | def get_email_url(): 43 | return 'https://secure.orangewebsite.com/viewemail.php' # + ?id=123456 44 | 45 | @staticmethod 46 | def get_gateway(): 47 | return CoinPayments 48 | 49 | @staticmethod 50 | def get_metadata(): 51 | return 'orangewebsite', 'https://www.orangewebsite.com/' 52 | 53 | @staticmethod 54 | def get_required_settings(): 55 | return { 56 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 57 | 'address': ['address', 'city', 'state', 'zipcode'], 58 | } 59 | 60 | ''' 61 | Action methods of the Hoster that can be called 62 | ''' 63 | 64 | @classmethod 65 | def get_options(cls): 66 | """ 67 | Linux (OpenVZ) and Windows (KVM) pages are slightly different, therefore their pages are parsed by different 68 | methods. Windows configurations allow a selection of Linux distributions, but not vice-versa. 69 | :return: possible configurations. 70 | """ 71 | browser = cls._create_browser() 72 | browser.open("https://www.orangewebsite.com/vps.php") 73 | options = cls._parse_openvz_hosting(browser.get_current_page()) 74 | lst = list(options) 75 | 76 | return lst 77 | 78 | def purchase(self, wallet, option): 79 | self._browser.open(option.purchase_url) 80 | self._server_form() 81 | 82 | self._browser.open(self.CART_URL) 83 | self._cart_form() 84 | 85 | self._browser.select_form(nr=0) # Go to payment form 86 | self._browser.submit_selected() 87 | 88 | return self.pay(wallet, self.get_gateway(), self._browser.get_url(), self._browser, self._settings) 89 | 90 | ''' 91 | Hoster-specific methods that are needed to perform the actions 92 | ''' 93 | 94 | def _server_form(self): 95 | """ 96 | Fills in the form containing server configuration. 97 | :return: 98 | """ 99 | form = self._browser.select_form('form#orderfrm') 100 | 101 | form['configoption[6]'] = '1127' # Ubuntu 16.04 102 | form.set("ajax", 1, force=True) 103 | 104 | # checkout = self._browser.get_current_page().find("input", {"value": "Checkout"}) 105 | 106 | self._browser.submit_selected() 107 | 108 | def _cart_form(self): 109 | form = self._browser.select_form('form#mainfrm') 110 | 111 | form['email'] = self._change_email_provider(self._settings.get('user', "email"), '@gmail.com') 112 | form['password'] = self._settings.get('user', "password") 113 | form['password2'] = self._settings.get('user', "password") 114 | form['paymentmethod'] = 'coinpayments' 115 | 116 | form['accepttos'] = True 117 | 118 | # Default submit button is "Validate code", use "Complete order" instead 119 | soup = self._browser.get_current_page() 120 | submit = soup.select_one('input.ordernow') 121 | form.choose_submit(submit) 122 | 123 | self._browser.submit_selected() 124 | 125 | def get_configuration(self): 126 | clientarea = self._create_clientarea() 127 | 128 | ip = clientarea.get_ip() 129 | password = clientarea.get_server_information_from_email()['server_password'] 130 | 131 | return VpsConfiguration(ip, password) 132 | 133 | @classmethod 134 | def _parse_openvz_hosting(cls, page): 135 | options = page.find_all('li', {'class': 'virtual'}) 136 | for idx, option in enumerate(options, start=1): 137 | list_elements = option.find_all('span', {"class": "right"}) 138 | price_eur = float(option.find('span', {'class', 'price_figure'}).text[1:]) 139 | c = CurrencyConverter() 140 | price_usd = round(c.convert(price_eur, 'EUR', 'USD'), 2) 141 | yield VpsOption( 142 | name=option.find('h3').text.strip().replace("Virtual Server - ", ""), 143 | storage=list_elements[1].text.strip().split(' ')[0][:-2], 144 | cores=list_elements[2].text.strip().split(' ')[0], 145 | memory=float(list_elements[0].text.strip().split(' ')[0][:-2]) / 1024, 146 | bandwidth=cls.parse_bandwidth(list_elements[3]), 147 | connection=1, 148 | price=price_usd, 149 | purchase_url=option.find_all('a', {'class': 'action_button'})[1]['href'], 150 | ) 151 | 152 | @staticmethod 153 | def parse_bandwidth(bandwidth): 154 | amount = bandwidth.text 155 | if amount.endswith('GB'): 156 | return int(amount.replace('GB', '')) / 1000 157 | else: 158 | return int(amount.replace('TB', '')) 159 | 160 | def change_root_password(self, new_password): 161 | self._create_controlpanel() 162 | return self._controlpanel.change_root_password(new_password) 163 | 164 | def get_status_control_panel(self): 165 | self._create_controlpanel() 166 | return self._controlpanel.get_status() 167 | 168 | def _create_clientarea(self): 169 | if self._clientarea is None: 170 | self._clientarea = OrangeWebsiteClientArea(self.get_browser(), self.get_clientarea_url(), 171 | self.get_email_url(), self._settings) 172 | return self._clientarea 173 | 174 | 175 | class OrangeWebsiteClientArea(ClientArea): 176 | email_url = None 177 | 178 | def __init__(self, browser, clientarea_url, email_url, user_settings): 179 | self.email_url = email_url 180 | ClientArea.__init__(self, browser, clientarea_url, user_settings) 181 | 182 | def get_emails(self): 183 | """ 184 | Returns a list of dicts containing email metadata: {id, title} 185 | This can be used to further select certains emails to parse 186 | """ 187 | self._browser.open(self._url + "?action=emails") 188 | soup = self._browser.get_current_page() 189 | extracted = self._extract_emails(soup) 190 | return extracted 191 | 192 | def get_server_information_from_email(self): 193 | """ 194 | Returns the parsed server information from email 195 | """ 196 | email_id = self._get_email_id() 197 | self._browser.open(self.email_url + '?id=' + email_id) 198 | soup = self._browser.get_current_page() 199 | 200 | server_info = { 201 | 'ip_address': None, 202 | 'server_user': None, 203 | 'server_password': None, 204 | } 205 | 206 | spans = soup.findAll('span', {'style': 'color: #0000ff;'}) 207 | server_info['server_user'] = spans[0].find('strong').text 208 | server_info['server_password'] = spans[1].find('strong').text 209 | server_info['ip_address'] = spans[2].text.strip() 210 | 211 | return server_info 212 | 213 | def _get_email_id(self): 214 | for email in self.get_emails(): 215 | e_id = email['id'] 216 | title = email['title'] 217 | if "WELCOME EMAIL" in title: 218 | return e_id 219 | 220 | @staticmethod 221 | def _extract_emails(soup): 222 | table = soup.find('table', {'id': 'tableEmailsList'}).tbody 223 | emails = [] 224 | for row in table.findAll('tr'): 225 | emails.append({ 226 | 'id': row['onclick'].split('\'')[1].split('id=')[1], 227 | 'title': row.findAll('td')[1].text 228 | }) 229 | return emails 230 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/proxhost.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import json 7 | import ssl 8 | from builtins import super 9 | 10 | import dateutil.parser 11 | import requests 12 | from future import standard_library 13 | from future.moves.urllib import request 14 | 15 | from cloudomate.gateway.bitpay import BitPay 16 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 17 | from cloudomate.hoster.vps.vps_hoster import VpsOption 18 | 19 | standard_library.install_aliases() 20 | from collections import namedtuple 21 | from cloudomate.globals import __BASE_URL__ 22 | 23 | VpsConfiguration = namedtuple('VpsConfiguration', ['ip', 'root_password']) 24 | VpsStatusResource = namedtuple('VpsStatusResource', ['used', 'total']) 25 | VpsStatusResourceNone = VpsStatusResource(-1, -1) 26 | VpsStatus = namedtuple('VpsStatus', ['memory', # Memory VpsStatusResource in GB 27 | 'storage', # Storage VpsStatusResource in GB 28 | 'bandwidth', # Bandwidth VpsStatusResource in GB 29 | 'online', # Boolean 30 | 'expiration', # Python Datetime object 31 | 'clientarea']) # Service info retrieved from the ClientArea (for overriding) 32 | 33 | 34 | class ProxHost(SolusvmHoster): 35 | # true if you can enable tuntap in the control panel 36 | TUN_TAP_SETTINGS = True 37 | 38 | BASE_URL = __BASE_URL__ 39 | 40 | def __init__(self, settings): 41 | super(ProxHost, self).__init__(settings) 42 | 43 | ''' 44 | Information about the Hoster 45 | ''' 46 | 47 | @staticmethod 48 | def get_clientarea_url(): 49 | return '' 50 | 51 | @staticmethod 52 | def get_gateway(): 53 | return BitPay 54 | 55 | @staticmethod 56 | def get_metadata(): 57 | return 'proxhost', 'https://codesalad.nl:5000/' 58 | 59 | @staticmethod 60 | def get_required_settings(): 61 | return { 62 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 63 | 'address': ['address', 'city', 'state', 'zipcode'], 64 | } 65 | 66 | def json_user_config(self): 67 | data = { 68 | 'firstname': self._settings.get('user', "firstname"), 69 | 'lastname': self._settings.get('user', "lastname"), 70 | 'username': self._settings.get('user', "username"), 71 | 'email': self._change_email_provider(self._settings.get('user', "email"), '@gmail.com'), 72 | 'phonenumber': self._settings.get('user', "phonenumber"), 73 | 'companyname': self._settings.get('user', "companyname"), 74 | 'address1': self._settings.get('address', "address"), 75 | 'city': self._settings.get('address', "city"), 76 | 'state': self._settings.get('address', "state"), 77 | 'postcode': self._settings.get('address', "zipcode"), 78 | 'country': self._settings.get('address', 'countrycode'), 79 | 'password': self._settings.get('user', "password"), 80 | 'password2': self._settings.get('user', "password") 81 | } 82 | return data 83 | 84 | ''' 85 | Action methods of the Hoster that can be called 86 | ''' 87 | 88 | @classmethod 89 | def get_options(cls): 90 | """ 91 | Linux (OpenVZ) and Windows (KVM) pages are slightly different, therefore their pages are parsed by different 92 | methods. Windows configurations allow a selection of Linux distributions, but not vice-versa. 93 | :return: possible configurations. 94 | """ 95 | context = ssl._create_unverified_context() 96 | url = ProxHost.BASE_URL + '/options' 97 | response = request.urlopen(url, context=context) 98 | response_json = json.loads(response.read().decode('utf-8')) 99 | 100 | options = [] 101 | for joption in response_json: 102 | options.append(VpsOption( 103 | name=joption['name'], 104 | storage=joption['storage'], 105 | cores=joption['cores'], 106 | memory=joption['memory'], 107 | bandwidth='unmetered', 108 | connection=joption['connection'], 109 | price=joption['price'], 110 | purchase_url=str(joption['vmid']) 111 | )) 112 | 113 | return list(options) 114 | 115 | def get_configuration(self): 116 | res = requests.post(self.BASE_URL + '/getconfiguration', json=self.json_user_config(), verify=False) 117 | print(res.content) 118 | config = json.loads(res.content) 119 | return VpsConfiguration(config['ip'], config['root_password']) 120 | 121 | def get_status(self): 122 | res = requests.post(self.BASE_URL + '/getstatus', json=self.json_user_config(), verify=False) 123 | print(res.content) 124 | status = json.loads(res.content.decode('utf8')) 125 | return VpsStatus( 126 | VpsStatusResourceNone, 127 | VpsStatusResourceNone, 128 | VpsStatusResourceNone, 129 | status['online'], # online 130 | dateutil.parser.parse(status['expiration']), 131 | VpsStatusResourceNone 132 | ) 133 | 134 | def purchase(self, wallet, option): 135 | res = requests.post(self.BASE_URL + '/purchase/' + option.purchase_url, json=self.json_user_config(), 136 | verify=False) 137 | print(res) 138 | pay_url = res.content.decode('utf8') 139 | print(pay_url) 140 | return self.pay(wallet, self.get_gateway(), pay_url) 141 | 142 | @staticmethod 143 | def get_ip(user_settings): 144 | res = requests.post(ProxHost.BASE_URL + '/getconfiguration', json=ProxHost(user_settings).json_user_config(), 145 | verify=False) 146 | print(res.content) 147 | config = json.loads(res.content) 148 | return config['ip'] 149 | 150 | @staticmethod 151 | def _check_login(text): 152 | data = json.loads(text) 153 | if data['success'] and data['success'] == '1': 154 | return True 155 | return False 156 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/pulseservers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | from builtins import int 8 | 9 | from future import standard_library 10 | 11 | from cloudomate.gateway.coinbase import Coinbase 12 | from cloudomate.hoster.vps import vps_hoster 13 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class Pulseservers(SolusvmHoster): 19 | CART_URL = 'https://www.pulseservers.com/billing/cart.php?a=confdomains' 20 | OPTIONS_URL = 'https://pulseservers.com/vps-linux.html' 21 | 22 | # true if you can enable tuntap in the control panel 23 | TUN_TAP_SETTINGS = False 24 | 25 | ''' 26 | Information about the Hoster 27 | ''' 28 | 29 | @staticmethod 30 | def get_clientarea_url(): 31 | return 'https://www.pulseservers.com/billing/clientarea.php' 32 | 33 | @staticmethod 34 | def get_gateway(): 35 | return Coinbase 36 | 37 | @staticmethod 38 | def get_metadata(): 39 | return 'PulseServers', 'https://pulseservers.com/' 40 | 41 | @staticmethod 42 | def get_required_settings(): 43 | return { 44 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 45 | 'address': ['address', 'city', 'state', 'zipcode'], 46 | 'server': ['hostname', 'root_password'] 47 | } 48 | 49 | ''' 50 | Action methods of the Hoster that can be called 51 | ''' 52 | 53 | @classmethod 54 | def get_options(cls): 55 | browser = cls._create_browser() 56 | browser.open(cls.OPTIONS_URL) 57 | 58 | # Get all pricing boxes 59 | soup = browser.get_current_page() 60 | boxes = soup.select('div.pricing-box') 61 | return [cls._parse_box(box) for box in boxes] 62 | 63 | def purchase(self, wallet, option): 64 | self._browser.open(option.purchase_url) 65 | self._submit_server_form() 66 | self._browser.open(self.CART_URL) 67 | page = self._submit_user_form() 68 | return self.pay(wallet, self.get_gateway(), page.url) 69 | 70 | ''' 71 | Hoster-specific methods that are needed to perform the actions 72 | ''' 73 | 74 | def _submit_server_form(self): 75 | form = self._browser.select_form('form#orderfrm') 76 | 77 | self._fill_server_form() 78 | form.set('billingcycle', 'monthly') 79 | form.form['action'] = 'https://www.pulseservers.com/billing/cart.php' 80 | 81 | return self._browser.submit_selected() 82 | 83 | def _submit_user_form(self): 84 | # Select the correct submit button 85 | form = self._browser.select_form('form#mainfrm') 86 | soup = self._browser.get_current_page() 87 | submit = soup.select_one('input.ordernow') 88 | form.choose_submit(submit) 89 | 90 | # Let SolusVM class handle the rest 91 | gateway = self.get_gateway() 92 | self._fill_user_form(gateway.get_name(), errorbox_class='errorbox') 93 | 94 | # Redirect to Coinbase 95 | self._browser.select_form(nr=0) 96 | return self._browser.submit_selected() 97 | 98 | @staticmethod 99 | def _parse_box(box): 100 | details = box.findAll('li') 101 | 102 | name = details[0].h4.text 103 | 104 | price = details[1].h1.text 105 | price = float(price[1:]) 106 | 107 | cores = details[2].strong.text 108 | cores = int(cores.split(' ')[0]) 109 | 110 | memory = details[3].strong.text 111 | memory = float(memory[0:-2]) 112 | 113 | storage = details[4].strong.text 114 | if storage == '1TB': 115 | storage = 1000.0 116 | else: 117 | storage = float(storage[0:-2]) 118 | 119 | connection = details[5].strong.text 120 | connection = int(connection[0:-7]) 121 | 122 | purchase_url = details[9].a['href'] 123 | 124 | return vps_hoster.VpsOption(name, cores, memory, storage, sys.maxsize, connection, price, purchase_url) 125 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/qhoster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from builtins import super 7 | 8 | from future import standard_library 9 | 10 | from cloudomate.gateway.coinify import Coinify 11 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 12 | from cloudomate.hoster.vps.vps_hoster import VpsOption 13 | 14 | standard_library.install_aliases() 15 | 16 | 17 | class QHoster(SolusvmHoster): 18 | CART_URL = 'https://www.qhoster.com/clients/cart.php?a=view' 19 | 20 | TUN_TAP_SETTINGS = False 21 | 22 | def __init__(self, settings): 23 | super(QHoster, self).__init__(settings) 24 | 25 | ''' 26 | Information about the Hoster 27 | ''' 28 | 29 | @staticmethod 30 | def get_clientarea_url(): 31 | return 'https://www.qhoster.com/clients/clientarea.php' 32 | 33 | @staticmethod 34 | def get_gateway(): 35 | return Coinify 36 | 37 | @staticmethod 38 | def get_metadata(): 39 | return 'qhoster', 'https://www.qhoster.com/' 40 | 41 | @staticmethod 42 | def get_required_settings(): 43 | return { 44 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 45 | 'address': ['address', 'city', 'state', 'zipcode'], 46 | } 47 | 48 | ''' 49 | Action methods of the Hoster that can be called 50 | ''' 51 | 52 | @classmethod 53 | def get_options(cls): 54 | browser = cls._create_browser() 55 | browser.open("https://www.qhoster.com/linux-vps.html") 56 | options = cls._parse_openvz_hosting(browser.get_current_page()) 57 | return list(options) 58 | 59 | def purchase(self, wallet, option): 60 | self._browser.open(option.purchase_url) 61 | self._browser.select_form('form#orderfrm') 62 | self._fill_server_form() 63 | form = self._browser.get_current_form() 64 | form['configoption[3]'] = '1036' # Ubuntu 16.04 65 | self._browser.submit_selected() 66 | response_text = self._browser.get_current_page().text.strip() 67 | if response_text: 68 | print(response_text) 69 | return 70 | 71 | self._browser.open(self.CART_URL) 72 | self._browser.select_form(selector='form#frmCheckout') 73 | self._fill_user_form(self.get_gateway().get_name()) 74 | self._browser.select_form('form#myForm') 75 | self._browser.submit_selected() 76 | return self.pay(wallet, self.get_gateway(), self._browser.get_url()) 77 | 78 | ''' 79 | Hoster-specific methods that are needed to perform the actions 80 | ''' 81 | 82 | @classmethod 83 | def _parse_openvz_hosting(cls, page): 84 | openvz = page.find('div', {'id': 'tab1'}) 85 | options = openvz.find_all('aside', {'class': 'plan1'}) 86 | for option in options: 87 | first_price_option = option.find('select', {'class': 'field2'}).find('option') 88 | yield VpsOption( 89 | name=option.find('h3').text.strip(), 90 | storage=int(option.find('li', {'class': 'icon3'}).text.split(' ')[0]), 91 | cores=int(option.find('li', {'class': 'icon2'}).text.split(' ')[0]), 92 | memory=int(option.find('li', {'class': 'icon6'}).text.split(' ')[0]), 93 | bandwidth=int(option.find('li', {'class': 'icon4'}).text.split(' ')[0]), 94 | connection=int(option.find('li', {'class': 'icon5'}).text.split(' ')[0]), 95 | price=float(first_price_option.text.split('$')[1].split('/')[0]), 96 | purchase_url=first_price_option['value'] 97 | ) 98 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/routerhosting.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from builtins import super 7 | 8 | from future import standard_library 9 | from mechanicalsoup.utils import LinkNotFoundError 10 | 11 | from cloudomate.gateway.coinpayments import CoinPayments 12 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 13 | from cloudomate.hoster.vps.vps_hoster import VpsOption 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class RouterHosting(SolusvmHoster): 19 | CART_URL = 'https://support.routerhosting.com/cart.php?a=view' 20 | 21 | # true if you can enable tuntap in the control panel 22 | TUN_TAP_SETTINGS = True 23 | 24 | _settings = None 25 | _controlpanel = None 26 | 27 | def __init__(self, settings): 28 | super(RouterHosting, self).__init__(settings) 29 | 30 | ''' 31 | Information about the Hoster 32 | ''' 33 | 34 | @staticmethod 35 | def get_clientarea_url(): 36 | return 'https://support.routerhosting.com/clientarea.php' 37 | 38 | @staticmethod 39 | def get_gateway(): 40 | return CoinPayments 41 | 42 | @staticmethod 43 | def get_metadata(): 44 | return 'routerhosting', 'https://routerhosting.com/' 45 | 46 | @staticmethod 47 | def get_required_settings(): 48 | return { 49 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 50 | 'address': ['address', 'city', 'state', 'zipcode'], 51 | } 52 | 53 | ''' 54 | Action methods of the Hoster that can be called 55 | ''' 56 | 57 | @classmethod 58 | def get_options(cls): 59 | """ 60 | Linux (OpenVZ) and Windows (KVM) pages are slightly different, therefore their pages are parsed by different 61 | methods. Windows configurations allow a selection of Linux distributions, but not vice-versa. 62 | :return: possible configurations. 63 | """ 64 | browser = cls._create_browser() 65 | browser.open("https://www.routerhosting.com/buy-cheap-kvm-linux-vps-ssd-server-hosting/") 66 | options = cls._parse_openvz_hosting(browser.get_current_page()) 67 | lst = list(options) 68 | 69 | return lst 70 | 71 | def purchase(self, wallet, option): 72 | self._browser.open(option.purchase_url) 73 | self._server_form() 74 | self._browser.open(self.CART_URL) 75 | 76 | summary = self._browser.get_current_page().find('div', class_='summary-container') 77 | self._browser.follow_link(summary.find('a', class_='btn-checkout')) 78 | 79 | try: 80 | self._browser.select_form(selector='form#frmCheckout') 81 | except LinkNotFoundError: 82 | print("Too many open transactions, try connecting from a different IP") 83 | raise 84 | 85 | self._fill_user_form(self.get_gateway().get_name()) 86 | 87 | self._browser.select_form(nr=0) # Go to payment form 88 | self._browser.submit_selected() 89 | 90 | return self.pay(wallet, self.get_gateway(), self._browser.get_url(), self._browser, self._settings) 91 | 92 | ''' 93 | Hoster-specific methods that are needed to perform the actions 94 | ''' 95 | 96 | def _server_form(self): 97 | """ 98 | Fills in the form containing server configuration. 99 | :return: 100 | """ 101 | form = self._browser.select_form('form#frmConfigureProduct') 102 | 103 | self._fill_server_form() 104 | form['configoption[46]'] = '365' # Ubuntu 16.04 105 | self._browser.submit_selected() 106 | 107 | @classmethod 108 | def _parse_openvz_hosting(cls, page): 109 | # table = page.find_all('div', {'class': 'wpb_wrapper'})[2] 110 | table = page.find('table') 111 | options = table.find_all('tr') 112 | options.pop(0) 113 | for idx, option in enumerate(options, start=1): 114 | list_elements = option.find_all('td') 115 | # price_eur = float(option.find('div', {'class', 'price'}).span.text[1:]) 116 | # c = CurrencyConverter() 117 | # price_usd = float(option.find('', {'class', 'price'}).span.text[1:]) 118 | # list_elements[2].text.strip().split(' ')[0], 119 | yield VpsOption( 120 | name=list_elements[0].text.strip().split('\xa0')[0], 121 | storage=list_elements[2].text.strip().split('GB')[0], 122 | cores=list_elements[1].text.strip().split(' ')[0].split('\xa0')[0], 123 | memory=list_elements[0].text.strip().split('\xa0')[0], 124 | bandwidth=list_elements[4].text.strip().split(' ')[0].replace('TB', ''), 125 | connection=list_elements[3].text.strip().split('Gbps')[0], 126 | price=float(list_elements[6].text.split("\"")[0].split("/")[0][1:]), 127 | purchase_url=list_elements[7].find('a', {'class': 'w-btn'})['href'], 128 | ) 129 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/solusvm_hoster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import subprocess 7 | import sys 8 | from abc import abstractmethod 9 | from builtins import super 10 | 11 | from bs4 import BeautifulSoup 12 | from future import standard_library 13 | from mechanicalsoup import LinkNotFoundError 14 | 15 | from cloudomate.hoster.vps.clientarea import ClientArea 16 | from cloudomate.hoster.vps.vps_hoster import VpsConfiguration 17 | from cloudomate.hoster.vps.vps_hoster import VpsHoster 18 | from cloudomate.hoster.vps.vps_hoster import VpsStatus 19 | from cloudomate.hoster.vps.vps_hoster import VpsStatusResourceNone 20 | 21 | standard_library.install_aliases() 22 | 23 | 24 | class SolusvmHoster(VpsHoster): 25 | _clientarea = None 26 | 27 | # true if you can enable tuntap in the control panel 28 | TUN_TAP_SETTINGS = False 29 | 30 | """ 31 | SolusvmHoster is the common superclass of all VPS hosters that make use of the Solusvm management package. 32 | This makes it possible to fill in the registration form in a similar manner for all Solusvm subclasses. 33 | """ 34 | 35 | def __init__(self, settings): 36 | super().__init__(settings) 37 | self._clientarea = None 38 | 39 | def _create_clientarea(self): 40 | if self._clientarea is None: 41 | self._clientarea = ClientArea(self._browser, self.get_clientarea_url(), self._settings) 42 | return self._clientarea 43 | 44 | ''' 45 | Methods that are the same for all subclasses 46 | ''' 47 | 48 | def get_configuration(self): 49 | clientarea = self._create_clientarea() 50 | 51 | ip = clientarea.get_ip() 52 | password = self._settings.get('server', 'root_password') 53 | 54 | return VpsConfiguration(ip, password) 55 | 56 | def get_status(self): 57 | clientarea = self._create_clientarea() 58 | 59 | service = clientarea.get_services_first() 60 | online = True if service.status == 'active' else False 61 | expiration = service.next_due 62 | 63 | return VpsStatus( 64 | VpsStatusResourceNone, 65 | VpsStatusResourceNone, 66 | VpsStatusResourceNone, 67 | online, 68 | expiration, 69 | service 70 | ) 71 | 72 | def get_clientarea(self): 73 | if not self._clientarea: 74 | self._clientarea = self._create_clientarea() 75 | return self._clientarea 76 | 77 | def change_root_password(self, newpassword): 78 | """ 79 | Changes the rootpassword of the server 80 | This can be overridden in subclasses if there is control panel access (LineVast) 81 | Changing the root password here will not persist after a (manual) RESET in the control panel. 82 | :return: True if password changing succeeded, else False 83 | """ 84 | config = self.get_configuration() 85 | commandline = list( 86 | ['sshpass', '-p', config.root_password, 'ssh', '-o', 'StrictHostKeyChecking=no', 'root@' + config.ip]) 87 | commandline.append('echo "root:' + newpassword + '" | chpasswd') 88 | 89 | try: 90 | subprocess.call(commandline) 91 | return True 92 | except OSError as e: 93 | print(e) 94 | print('Install sshpass to use this command') 95 | return False 96 | 97 | def enable_tun_tap(self): 98 | """ 99 | For servers that are able to have their TUN/TAP settings enabled 100 | This ties along with TUN_TAP_SETTINGS, which must be set to True if provider supports TUN/TAP 101 | :return: Defaults to False, unless implemented on the server 102 | """ 103 | return False 104 | 105 | ''' 106 | Static methods that must be overwritten by subclasses 107 | ''' 108 | 109 | @staticmethod 110 | @abstractmethod 111 | def get_clientarea_url(): 112 | """Get the url of the clientarea for this hoster 113 | 114 | :return: Returns the clientarea url 115 | """ 116 | pass 117 | 118 | ''' 119 | Methods that are used by subclasses to fill parts of the forms that are shared between hosters 120 | ''' 121 | 122 | def _fill_server_form(self): 123 | """Fills the server configuration form (should be currently selected) as much as possible 124 | 125 | """ 126 | form = self._browser.get_current_form() 127 | 128 | try: 129 | form['hostname'] = self._settings.get('server', 'hostname') 130 | except LinkNotFoundError: 131 | print('Couldn\'t set hostname') 132 | 133 | try: 134 | form['rootpw'] = self._settings.get('server', 'root_password') 135 | except LinkNotFoundError: 136 | # TODO: Properly handle this warning 137 | print('Couldn\'t set root password') 138 | 139 | try: 140 | form['ns1prefix'] = self._settings.get('server', 'ns1') 141 | form['ns2prefix'] = self._settings.get('server', 'ns2') 142 | except LinkNotFoundError: 143 | print('Couldn\'t set ns1, ns2') 144 | 145 | # As an alternative to the default Ajax request 146 | form.new_control('hidden', 'a', 'confproduct') 147 | form.new_control('hidden', 'ajax', '1') 148 | form.form['method'] = 'get' 149 | 150 | def _fill_user_form(self, payment_method, errorbox_class='checkout-error-feedback'): 151 | """Fills the user information form (should be currently selected) as much as possible 152 | 153 | :param payment_method: the name of the payment method 154 | :param errorbox_class: the class of the div element containing error messages 155 | :return: the page received after submitted the form 156 | """ 157 | form = self._browser.get_current_form() 158 | 159 | form['firstname'] = self._settings.get('user', "firstname") 160 | form['lastname'] = self._settings.get('user', "lastname") 161 | form['email'] = self._change_email_provider(self._settings.get('user', "email"), '@gmail.com') 162 | form['phonenumber'] = self._settings.get('user', "phonenumber") 163 | form['companyname'] = self._settings.get('user', "companyname") 164 | form['address1'] = self._settings.get('address', "address") 165 | form['city'] = self._settings.get('address', "city") 166 | form['state'] = self._settings.get('address', "state") 167 | form['postcode'] = self._settings.get('address', "zipcode") 168 | form['country'] = self._settings.get('address', 'countrycode') 169 | form['password'] = self._settings.get('user', "password") 170 | form['password2'] = self._settings.get('user', "password") 171 | form['paymentmethod'] = payment_method.lower() 172 | 173 | try: 174 | form['accepttos'] = True # Attempt to accept the terms and conditions 175 | except LinkNotFoundError: 176 | pass 177 | 178 | page = self._browser.submit_selected() 179 | # Error handling 180 | if 'checkout' in page.url: 181 | soup = BeautifulSoup(page.text, 'lxml') 182 | errors = soup.find('div', {'class': errorbox_class}).text 183 | print((errors.strip())) 184 | sys.exit(2) 185 | 186 | return page 187 | 188 | @staticmethod 189 | def _change_email_provider(old_email, new_provider): 190 | new_email, old_provider = old_email.split('@') 191 | if old_provider != 'email.com': 192 | return old_email 193 | new_email = new_email + new_provider 194 | return new_email 195 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/twosync.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import re 7 | import time 8 | from builtins import int 9 | from builtins import super 10 | 11 | from sys import maxsize 12 | 13 | from future import standard_library 14 | from mechanicalsoup.utils import LinkNotFoundError 15 | 16 | from cloudomate.gateway.blockchain import Blockchain 17 | from cloudomate.hoster.vps.clientarea import ClientArea 18 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 19 | from cloudomate.hoster.vps.vps_hoster import VpsConfiguration 20 | from cloudomate.hoster.vps.vps_hoster import VpsOption 21 | from cloudomate.hoster.vps.vps_hoster import VpsStatus 22 | from cloudomate.hoster.vps.vps_hoster import VpsStatusResource 23 | 24 | standard_library.install_aliases() 25 | 26 | 27 | class TwoSync(SolusvmHoster): 28 | CART_URL = 'https://ua.2sync.org/cart.php?a=view' 29 | 30 | def __init__(self, settings): 31 | super(TwoSync, self).__init__(settings) 32 | 33 | ''' 34 | Information about the Hoster 35 | ''' 36 | 37 | @staticmethod 38 | def get_clientarea_url(): 39 | return 'https://ua.2sync.org/clientarea.php' 40 | 41 | @staticmethod 42 | def get_email_url(): 43 | return 'https://ua.2sync.org/viewemail.php' 44 | 45 | @staticmethod 46 | def get_gateway(): 47 | return Blockchain 48 | 49 | @staticmethod 50 | def get_metadata(): 51 | return 'twosync', 'https://www.2sync.co/vps/ukraine/' 52 | 53 | @staticmethod 54 | def get_required_settings(): 55 | return { 56 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 57 | 'address': ['address', 'city', 'state', 'zipcode'], 58 | } 59 | 60 | def _create_clientarea(self): 61 | if self._clientarea is None: 62 | self._clientarea = TSClientArea(self.get_browser(), self.get_clientarea_url(), 63 | self.get_email_url(), self._settings) 64 | return self._clientarea 65 | 66 | ''' 67 | Action methods of the Hoster that can be called 68 | ''' 69 | 70 | @classmethod 71 | def get_options(cls): 72 | """ 73 | Fetches the possible configuration for Ukraine VPS with Linux (OpenVZ) 74 | :return: possible configurations. 75 | """ 76 | 77 | options = cls._parse_openvz_hosting() 78 | lst = list(options) 79 | 80 | return lst 81 | 82 | def get_configuration(self): 83 | """ 84 | Overrides the default configuration method as TwoSync doesn't use the server password during 85 | registration 86 | :return: IP and Password 87 | """ 88 | server_info = self.get_clientarea().get_server_information_from_email() 89 | ip = server_info.get('ip_address') 90 | password = server_info.get('server_password') 91 | 92 | return VpsConfiguration(ip, password) 93 | 94 | def enable_tun_tap(self): 95 | """ 96 | TwoSync already has tuntap enabled 97 | :return: True 98 | """ 99 | return True 100 | 101 | def purchase(self, wallet, option): 102 | self._browser.open(option.purchase_url) 103 | self._server_form() 104 | self._browser.open(self.CART_URL) 105 | 106 | self._browser.select_form(selector='form#frmCheckout') 107 | self._fill_user_form(self.get_gateway().get_name(), 'alert alert-danger') 108 | 109 | self._browser.open('https://ua.2sync.org/cart.php?a=complete') 110 | invoice = self._browser.get_current_page().find('a', {'class': 'alert-link'}) 111 | self._browser.follow_link(invoice) 112 | 113 | url = self._browser.get_url() 114 | urlselected = self.extract_info(url) 115 | 116 | self.pay(wallet, self.get_gateway(), urlselected) 117 | 118 | # open invoice page after paying 119 | invoice = str(url).split('=')[1] 120 | self._browser.open('https://ua.2sync.org/modules/gateways/blockchain.php?invoice=' + invoice) 121 | 122 | msoup = self._browser.get_current_page() 123 | mpattern = re.compile(r'secret:\s*\'(.+?)\'') 124 | secret = mpattern.search(str(msoup)).group(1) 125 | 126 | okdata = { 127 | 'invId': invoice, 128 | 'am': urlselected.split('&')[0], 129 | 'secret': secret 130 | } 131 | 132 | # wait 10s to allow for payment to go through 133 | print("Waiting 10s before 'clicking' on OK...") 134 | time.sleep(10) 135 | 136 | # this emulates a mouse click on the "OK" button 137 | self._browser.session.post(url='https://ua.2sync.org/blockchain_openTicket.php', data=okdata) 138 | 139 | ''' 140 | Hoster-specific methods that are needed to perform the actions 141 | ''' 142 | 143 | def _server_form(self): 144 | """ 145 | Fills in the form containing server configuration. 146 | :return: 147 | """ 148 | form = self._browser.select_form('form#frmConfigureProduct') 149 | self._fill_server_form() 150 | try: 151 | form['configoption[5]'] = '14' # Ubuntu 16.04 152 | except LinkNotFoundError: 153 | print('error') 154 | self._browser.submit_selected() 155 | 156 | @classmethod 157 | def _parse_openvz_hosting(cls): 158 | browser = cls._create_browser() 159 | browser.open('https://ua.2sync.org/cart.php') 160 | page = browser.get_current_page() 161 | 162 | packages = page.find_all('div', {'class': 'package'}) 163 | 164 | for i in range(0, len(packages)): 165 | option = cls._parse_linux_option(packages[i]) 166 | yield option 167 | 168 | @staticmethod 169 | def _parse_linux_option(package): 170 | option = { 171 | 'name': package.find('h3').text, 172 | 'price': package.find('div', {'class': 'price'}).text.split('$')[1].split('USD/mo')[0], 173 | 'purchase_url': 'https://ua.2sync.org/' + package.find('a').get('href') 174 | } 175 | for entry in package.find_all('li'): 176 | key, value = entry.text.replace('\n', '').split(' ')[:2] 177 | option[key] = value 178 | 179 | return VpsOption( 180 | name=option['name'], 181 | storage=option['Space'].split('GB')[0], 182 | cores=option['CPU'], 183 | memory=option['RAM'].split('GB')[0], 184 | bandwidth=maxsize, 185 | connection=int(option['Port'].split('Gbit')[0]), 186 | price=float(option['price']), 187 | purchase_url=option['purchase_url'], 188 | ) 189 | 190 | @classmethod 191 | def extract_info(cls, url): 192 | invoice = str(url).split('=')[1] 193 | browser = cls._create_browser() 194 | browser.open('https://ua.2sync.org//modules/gateways/blockchain.php?invoice=' + invoice) 195 | pages = browser.get_current_page().find_all('b') 196 | amount = float(str(pages[0]).split('>')[1].split(' BTC')[0]) 197 | address = str(pages[1]).split('>')[1].split('<')[0] 198 | return str(amount) + '&' + address 199 | 200 | @classmethod 201 | def _convert_bytes_to_gbytes(cls, num): 202 | return num / 1024 / 1024 / 1024 203 | 204 | def get_status(self): 205 | status = super().get_status() 206 | 207 | service_id = status.clientarea.url.split('=')[-1] 208 | data = self._browser.post(url='https://ua.2sync.org/modules/servers/tProxmox/monitorProxmox.php', 209 | data={'serviceid': service_id, 'typeVm': 'qemu'}).json() 210 | 211 | memory = VpsStatusResource(self._convert_bytes_to_gbytes(data['mem']), 212 | self._convert_bytes_to_gbytes(data['maxmem'])) 213 | storage = VpsStatusResource(self._convert_bytes_to_gbytes(data['freemem']), 214 | self._convert_bytes_to_gbytes(data['maxdisk'])) 215 | bandwidth = VpsStatusResource(float('inf'), float('inf')) 216 | 217 | return VpsStatus(memory, storage, bandwidth, status.online, status.expiration, status.clientarea) 218 | 219 | 220 | class TSClientArea(ClientArea): 221 | """ 222 | Modified ClientArea for twosync, 223 | Extended for looking up server information such as IP, root password 224 | """ 225 | email_url = None 226 | 227 | def __init__(self, browser, clientarea_url, email_url, user_settings): 228 | self.email_url = email_url 229 | ClientArea.__init__(self, browser, clientarea_url, user_settings) 230 | 231 | def get_emails(self): 232 | """ 233 | Returns a list of dicts containing email metadata: {id, title} 234 | This can be used to further select certains emails to parse 235 | """ 236 | self._browser.open(self._url + "?action=emails") 237 | soup = self._browser.get_current_page() 238 | extracted = self._extract_emails(soup) 239 | return extracted 240 | 241 | def get_server_information_from_email(self): 242 | """ 243 | Returns the parsed server information from email 244 | """ 245 | email_id = None 246 | for email in self.get_emails(): 247 | e_id = email['id'] 248 | title = email['title'] 249 | if 'ready' in title: 250 | email_id = e_id 251 | break 252 | self._browser.open(self.email_url + '?id=' + email_id) 253 | soup = self._browser.get_current_page() 254 | 255 | server_info = { 256 | 'ip_address': None, 257 | 'server_user': None, 258 | 'server_password': None, 259 | 'vmuser': None, 260 | 'vmuser_password': None, 261 | 'control_panel_url': None 262 | } 263 | 264 | ps = soup.findAll('p') 265 | pattern1 = re.compile(r'(?:<.>)*((?:Username:)|(?:Root Password:)|(?:VPS IP:))\s*((?:\w{1,3}\.*){1,4})(?:<.>)*', 266 | re.MULTILINE) 267 | 268 | for p in ps: 269 | p = re.sub(r'[^\x00-\x7F]+', '', str(p)).strip() 270 | for (k, v) in re.findall(pattern1, p): 271 | if 'VPS IP' in k and not server_info['ip_address']: 272 | server_info['ip_address'] = v 273 | elif 'Root Password' in k and not server_info['server_password']: 274 | server_info['server_password'] = v 275 | elif 'Username' in k and not server_info['server_user']: 276 | server_info['server_user'] = v 277 | 278 | return server_info 279 | 280 | @staticmethod 281 | def _extract_emails(soup): 282 | table = soup.find('table', {'id': 'tableEmailsList'}).tbody 283 | emails = [] 284 | for row in table.findAll('tr'): 285 | emails.append({ 286 | 'id': row['onclick'].split('\'')[1].split('id=')[1], 287 | 'title': row.findAll('td')[1].text 288 | }) 289 | return emails 290 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/undergroundprivate.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | from builtins import int 8 | 9 | from future import standard_library 10 | 11 | from cloudomate.gateway.undergroundprivate import UndergroundPrivate as UndergroundPrivateGateway 12 | from cloudomate.hoster.vps import vps_hoster 13 | from cloudomate.hoster.vps.solusvm_hoster import SolusvmHoster 14 | 15 | standard_library.install_aliases() 16 | 17 | 18 | class UndergroundPrivate(SolusvmHoster): 19 | CART_URL = 'https://www.clientlogin.sx/cart.php?a=view' 20 | OPTIONS_URL = 'https://undergroundprivate.com/russiaoffshorevps.html' 21 | # true if you can enable tuntap in the control panel 22 | TUN_TAP_SETTINGS = False 23 | 24 | ''' 25 | Information about the Hoster 26 | ''' 27 | 28 | @staticmethod 29 | def get_clientarea_url(): 30 | return 'https://www.clientlogin.sx/clientarea.php' 31 | 32 | @staticmethod 33 | def get_gateway(): 34 | return UndergroundPrivateGateway 35 | 36 | @staticmethod 37 | def get_metadata(): 38 | return 'UndergroundPrivate', 'https://undergroundprivate.com' 39 | 40 | @staticmethod 41 | def get_required_settings(): 42 | return { 43 | 'user': ['firstname', 'lastname', 'email', 'phonenumber', 'password'], 44 | 'address': ['address', 'city', 'state', 'zipcode', 'countrycode'], 45 | 'server': ['hostname', 'root_password'] 46 | } 47 | 48 | ''' 49 | Action methods of the Hoster that can be called 50 | ''' 51 | 52 | @classmethod 53 | def get_options(cls): 54 | browser = cls._create_browser() 55 | browser.open(cls.OPTIONS_URL) 56 | 57 | # Get all pricing boxes 58 | soup = browser.get_current_page() 59 | boxes = soup.select('div.pricingboxes > div.row > div > ul') 60 | boxes = boxes[:-1] # Remove last item, which is a custom server and can't be bought automatically 61 | options = [cls._parse_box(box) for box in boxes] 62 | 63 | # Remove options that are out of stock 64 | # Cookies are no problem, since this method used its own browser 65 | filtered_options = [] 66 | for option in options: 67 | page = browser.open(option.purchase_url) 68 | if 'add' not in page.url: 69 | # Not out of stock! 70 | filtered_options.append(option) 71 | return filtered_options 72 | 73 | def purchase(self, wallet, option): 74 | self._browser.open(option.purchase_url) 75 | self._submit_server_form() 76 | response_text = self._browser.get_current_page().text.strip() 77 | if response_text: 78 | print(response_text) 79 | return 80 | self._browser.open(self.CART_URL) 81 | self._submit_user_form() 82 | 83 | # Retrieve the payment URL from an iFrame 84 | self._browser.select_form('form') 85 | page = self._browser.submit_selected().url 86 | return self.pay(wallet, self.get_gateway(), page) 87 | 88 | ''' 89 | Hoster-specific methods that are needed to perform the actions 90 | ''' 91 | 92 | @staticmethod 93 | def _parse_box(box): 94 | details = box.findAll('li') 95 | 96 | name = details[0].text.rstrip() 97 | 98 | price = details[1].span.text 99 | price = float(price[1:]) 100 | 101 | cores = details[2].text 102 | cores = cores.split('\n') 103 | cores = int(cores[1][0]) 104 | 105 | memory = details[4].text 106 | memory = float(memory[0]) 107 | 108 | storage = details[3].text 109 | gb_index = storage.index('GB') 110 | storage = storage[0:gb_index] 111 | 112 | connection = details[6].text 113 | connection = int(connection[0]) 114 | 115 | purchase_url = details[13].p.span.a['href'] 116 | 117 | return vps_hoster.VpsOption(name, cores, memory, storage, sys.maxsize, connection, price, purchase_url) 118 | 119 | def _submit_server_form(self): 120 | form = self._browser.select_form('form#orderfrm') 121 | 122 | self._fill_server_form() 123 | form['billingcycle'] = 'monthly' 124 | form['configoption[7]'] = '866' # Operating System: Ubuntu 16.04 125 | form['configoption[8]'] = '54' # Control Panel: None 126 | form['configoption[9]'] = '56' # Extra IP Address: None 127 | form['configoption[94]'] = '869' # Manual setup 128 | form.form['action'] = 'https://www.clientlogin.sx/cart.php' 129 | form.form['method'] = 'get' 130 | 131 | return self._browser.submit_selected() 132 | 133 | def _submit_user_form(self): 134 | # Select the correct submit button 135 | form = self._browser.select_form('form#frmCheckout') 136 | soup = self._browser.get_current_page() 137 | submit = soup.select_one('button#btnCompleteOrder') 138 | form.choose_submit(submit) 139 | 140 | # Let SolusVM class handle the rest 141 | gateway = self.get_gateway() 142 | return self._fill_user_form(gateway.get_name(), errorbox_class='errorbox') 143 | -------------------------------------------------------------------------------- /cloudomate/hoster/vps/vps_hoster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from abc import abstractmethod 7 | from collections import namedtuple 8 | 9 | from future import standard_library 10 | 11 | from cloudomate.hoster.hoster import Hoster 12 | 13 | standard_library.install_aliases() 14 | 15 | VpsConfiguration = namedtuple('VpsConfiguration', ['ip', 'root_password']) 16 | VpsOption = namedtuple('VpsOption', ['name', 17 | 'cores', 18 | 'memory', # Memory in GB 19 | 'storage', # Storage in GB 20 | 'bandwidth', # Bandwidth in GB 21 | 'connection', # Connection speed in Gbps 22 | 'price', # Price in USD 23 | 'purchase_url']) 24 | VpsStatusResource = namedtuple('VpsStatusResource', ['used', 'total']) 25 | VpsStatusResourceNone = VpsStatusResource(-1, -1) 26 | VpsStatus = namedtuple('VpsStatus', ['memory', # Memory VpsStatusResource in GB 27 | 'storage', # Storage VpsStatusResource in GB 28 | 'bandwidth', # Bandwidth VpsStatusResource in GB 29 | 'online', # Boolean 30 | 'expiration', # Python Datetime object 31 | 'clientarea']) # Service info retrieved from the ClientArea (for overriding) 32 | 33 | 34 | class VpsHoster(Hoster): 35 | """ 36 | Abstract class for VPS Hosters. 37 | This class already implements some common methods. 38 | """ 39 | 40 | @abstractmethod 41 | def get_configuration(self): 42 | """Get Hoster configuration. 43 | 44 | :return: Returns VpsConfiguration for the VPS Hoster instance 45 | """ 46 | pass 47 | 48 | @classmethod 49 | @abstractmethod 50 | def get_options(cls): 51 | """Get Hoster options. 52 | 53 | :return: Returns list of VpsOption objects 54 | """ 55 | pass 56 | 57 | @abstractmethod 58 | def get_status(self): 59 | """Get Hoster configuration. 60 | 61 | :return: Returns VpsStatus of the VPS Hoster instance 62 | """ 63 | pass 64 | -------------------------------------------------------------------------------- /cloudomate/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/test/__init__.py -------------------------------------------------------------------------------- /cloudomate/test/resources/bitpay_invoice_data.json: -------------------------------------------------------------------------------- 1 | {"facade":"public/invoice","data":{"url":"https://bitpay.com/invoice?id=KXnWTnNsNUrHK2PEp8TpDC","posData":"{\"posData\": \"17086\", \"hash\": \"bA9lYPp9AJ9HQ\"}","status":"new","btcPrice":"0.000652","btcDue":"0.001402","price":7.92,"currency":"USD","orderId":"17086","invoiceTime":1516102773379,"expirationTime":1516103673379,"currentTime":1516103113378,"id":"KXnWTnNsNUrHK2PEp8TpDC","lowFeeDetected":false,"amountPaid":0,"btcPaid":"0.000000","rate":12150,"exceptionStatus":false,"redirectURL":"https://www.billing.blueangelhost.com/","refundAddressRequestPending":false,"buyerProvidedInfo":{"selectedTransactionCurrency":"BTC"},"addresses":{"BTC":"12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL"},"paymentSubtotals":{"BTC":65200},"paymentTotals":{"BTC":140200},"bitcoinAddress":"12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL","minerFees":{"BTC":{"totalFee":75000,"satoshisPerByte":510}},"buyerPaidBtcMinerFee":"0.000750","supportedTransactionCurrencies":{"BTC":{"enabled":true}},"exRates":{"EUR":9932.51565,"USD":12150},"paymentUrls":{"BIP21":"bitcoin:12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL?amount=0.001402","BIP72":"bitcoin:12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL?amount=0.001402&r=https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC","BIP72b":"bitcoin:?r=https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC","BIP73":"https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC"},"exchangeRates":{"BTC":{"EUR":9932.51565,"USD":12150}},"paymentCodes":{"BTC":{"BIP21":"bitcoin:12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL?amount=0.001402","BIP72":"bitcoin:12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL?amount=0.001402&r=https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC","BIP72b":"bitcoin:?r=https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC","BIP73":"https://bitpay.com/i/KXnWTnNsNUrHK2PEp8TpDC"}},"token":"5uqeA84nXkFyYDAk2yW3RSYsHgTcbMuqHDkMu15SqoWJAdSKPqshPLraGBghahYxw"}} 2 | -------------------------------------------------------------------------------- /cloudomate/test/resources/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/test/resources/captcha.png -------------------------------------------------------------------------------- /cloudomate/test/resources/clientarea_emails.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 18 | 19 | 20 | 22 | 23 | 26 | 27 | 28 | 30 | 31 | 34 | 35 | 36 | 38 | 39 | 42 | 43 | 44 | 46 | 47 | 50 | 51 | 52 | 53 |
54 |

Loading...

55 |
56 |
57 | -------------------------------------------------------------------------------- /cloudomate/test/resources/clientarea_service.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Hostname 6 |
7 |
8 | hostname 9 |
10 |
11 |
12 |
13 | Primary IP 14 |
15 |
16 | 178.32.53.129 17 |
18 |
19 |
20 |
21 | Nameservers 22 |
23 |
24 | ns1.pulseservers.com
ns2.pulseservers.com 25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /cloudomate/test/resources/clientarea_services.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 |
38 |

Loading...

39 |
40 |
41 | -------------------------------------------------------------------------------- /cloudomate/test/resources/coinbase.html: -------------------------------------------------------------------------------- 1 |
2 |

Send exactly 0.00041 BTC to this address:

3 |
4 |

5 | 1B7dwaVZrEfwKXoLf1VNq7nXZvnKk7xzHZ

7 |

8 |
9 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /cloudomate/test/resources/crowncloud_email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 |
4 |

Dear Pleb Net (PlebNet),

5 |

We are pleased to tell you that the virtual server you ordered has now been set up and is operational.

6 |

VPS Details
=============================
Main IP: 000.000.000.000nbsp;
Root Password: xxxx

You can access your server using a free simple SSH client program called Putty located at: http://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html

7 |

To reinstall an operating system on your VPS and for various other functions like enabling TUN/TAP, PPP etc you can access the following control panel :
=============================
URL: https://crownpanel.com or http://crownpanel.com
Username: paneluserxxxx 
Password: xxxx (If you already have access to the control panel please use your existing password) 

8 |

Thanks & have a nice day!
CrownCloud - Staff.

9 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /cloudomate/test/resources/test_settings.cfg: -------------------------------------------------------------------------------- 1 | [user] 2 | email = bot@pleb.net 3 | firstname = Pleb 4 | lastname = Net 5 | companyname = PlebNet 6 | phonenumber = 1234567890 7 | password = hunter2 8 | username = Pleb 9 | 10 | [address] 11 | address = Plebweg 4 12 | city = Plebst 13 | state = PlebState 14 | countrycode = PB 15 | zipcode = 123456 16 | 17 | [payment] 18 | walletpath = /path/to/electrum/wallet 19 | 20 | [server] 21 | ns1 = ns1 22 | ns2 = ns2 23 | hostname = hostname 24 | root_password = hunter2 25 | 26 | [testhoster] 27 | email = test@test.net 28 | -------------------------------------------------------------------------------- /cloudomate/test/test_captchasolver.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | import unittest 8 | 9 | from future import standard_library 10 | from mock.mock import patch, MagicMock 11 | 12 | standard_library.install_aliases() 13 | 14 | from cloudomate.util.captchasolver import CaptchaSolver, ReCaptchaSolver 15 | 16 | 17 | class TestCaptchaSolver(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.captcha_solver = CaptchaSolver("213389asd8u912823") 21 | self.recaptcha_solver = ReCaptchaSolver("213389asd8u912824") 22 | 23 | def test_get_current_key(self): 24 | self.assertEqual(self.captcha_solver.get_current_key(), "213389asd8u912823") 25 | self.assertEqual(self.recaptcha_solver.get_current_key(), "213389asd8u912824") 26 | 27 | @patch("time.sleep") 28 | def test_solve_captcha_text_case_sensitive(self, mock_time): 29 | self.captcha_solver._create_task_captcha_text_case_sensitive = MagicMock() 30 | self.captcha_solver._get_task_status = MagicMock() 31 | self.captcha_solver._get_task_result = MagicMock() 32 | 33 | self.captcha_solver.solve_captcha_text_case_sensitive( 34 | os.path.join(os.path.dirname(__file__), "resources/captcha.png")) 35 | 36 | self.assertTrue(self.captcha_solver._create_task_captcha_text_case_sensitive.called) 37 | self.assertTrue(self.captcha_solver._get_task_status.called) 38 | self.assertTrue(self.captcha_solver._get_task_result.called) 39 | 40 | @patch("time.sleep") 41 | def test_solve_google_recaptcha(self, mock_time): 42 | self.recaptcha_solver._create_task_google_recaptcha = MagicMock() 43 | self.recaptcha_solver._get_task_status = MagicMock() 44 | self.recaptcha_solver._get_task_result = MagicMock() 45 | 46 | self.recaptcha_solver.solve_google_recaptcha("test1", "test2") 47 | 48 | self.assertTrue(self.recaptcha_solver._create_task_google_recaptcha.called) 49 | self.assertTrue(self.recaptcha_solver._get_task_status.called) 50 | self.assertTrue(self.recaptcha_solver._get_task_result.called) 51 | 52 | 53 | if __name__ == "__main__": 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /cloudomate/test/test_clientarea.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | import unittest 8 | from builtins import open 9 | from unittest import skip 10 | 11 | from future import standard_library 12 | from mock import MagicMock 13 | 14 | from cloudomate.hoster.vps.clientarea import ClientArea 15 | 16 | standard_library.install_aliases() 17 | 18 | 19 | class TestClientArea(unittest.TestCase): 20 | @skip("Update needed for new clientarea") 21 | def test_extract_services(self): 22 | html_file = open(os.path.join(os.path.dirname(__file__), 'resources/clientarea_services.html'), 'r') 23 | data = html_file.read() 24 | html_file.close() 25 | mock = MagicMock(ClientArea) 26 | mock.clientarea_url = '' 27 | services = ClientArea._extract_services(mock, data) 28 | self.assertEqual(services, [ 29 | {'status': 'active', 'product': 'Basic', 'url': '?action=productdetails&id=8961', 'price': '$4.99 USD', 30 | 'term': 'Monthly', 'next_due_date': '2017-06-19', 'id': '8961'}, 31 | {'status': 'cancelled', 'product': 'Basic', 'url': '?action=productdetails&id=9019', 32 | 'price': '$4.99 USD', 'term': 'Monthly', 'next_due_date': '2017-05-24', 'id': '9019'} 33 | ]) 34 | 35 | @skip("Update needed for new clientarea") 36 | def test_extract_service(self): 37 | html_file = open(os.path.join(os.path.dirname(__file__), 'resources/clientarea_service.html'), 'r') 38 | data = html_file.read() 39 | html_file.close() 40 | info = ClientArea._extract_service_info(data) 41 | self.assertEqual(info, ['hostname', '178.32.53.129', 'ns1.pulseservers.comns2.pulseservers.com']) 42 | 43 | 44 | if __name__ == '__main__': 45 | unittest.main(exit=False) 46 | -------------------------------------------------------------------------------- /cloudomate/test/test_cmdline.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | import unittest 8 | from argparse import Namespace 9 | 10 | from future import standard_library 11 | from mock.mock import MagicMock 12 | 13 | import cloudomate.cmdline as cmdline 14 | from cloudomate.hoster.vpn.azirevpn import AzireVpn 15 | from cloudomate.hoster.vps.linevast import LineVast 16 | from cloudomate.hoster.vps.vps_hoster import VpsOption 17 | 18 | standard_library.install_aliases() 19 | 20 | 21 | class TestCmdLine(unittest.TestCase): 22 | def setUp(self): 23 | self.settings_file = os.path.join(os.path.dirname(__file__), 'resources/test_settings.cfg') 24 | self.vps_options_real = LineVast.get_options 25 | self.vps_purchase_real = LineVast.purchase 26 | 27 | def tearDown(self): 28 | LineVast.get_options = self.vps_options_real 29 | LineVast.purchase = self.vps_purchase_real 30 | 31 | def test_execute_vps_list(self): 32 | command = ["vps", "list"] 33 | cmdline.execute(command) 34 | 35 | def test_execute_vpn_list(self): 36 | command = ["vpn", "list"] 37 | cmdline.execute(command) 38 | 39 | def test_execute_vps_options(self): 40 | mock_method = self._mock_vps_options() 41 | command = ["vps", "options", "linevast"] 42 | cmdline.providers["vps"]["linevast"].configurations = [] 43 | cmdline.execute(command) 44 | mock_method.assert_called_once() 45 | self._restore_vps_options() 46 | 47 | def test_execute_vpn_options(self): 48 | mock_method = self._mock_vpn_options() 49 | command = ["vpn", "options", "azirevpn"] 50 | cmdline.providers["vpn"]["azirevpn"].configurations = [] 51 | cmdline.execute(command) 52 | mock_method.assert_called_once() 53 | self._restore_vpn_options() 54 | 55 | def test_execute_vps_purchase(self): 56 | self._mock_vps_options([self._create_option()]) 57 | purchase = LineVast.purchase 58 | LineVast.purchase = MagicMock() 59 | command = ["vps", "purchase", "linevast", "-f", "-c", self.settings_file, "-rp", "asdf", "0"] 60 | cmdline.execute(command) 61 | LineVast.purchase.assert_called_once() 62 | LineVast.purchase = purchase 63 | self._restore_vps_options() 64 | 65 | @staticmethod 66 | def _create_option(): 67 | return VpsOption( 68 | name="Option name", 69 | memory="Option ram", 70 | cores="Option cpu", 71 | storage="Option storage", 72 | bandwidth="Option bandwidth", 73 | price=12, 74 | connection="Option connection", 75 | purchase_url="Option url" 76 | ) 77 | 78 | def test_execute_vps_purchase_verify_options_failure(self): 79 | self._mock_vps_options() 80 | command = ["vps", "purchase", "linevast", "-f", "-c", self.settings_file, "1"] 81 | self._check_exit_code(1, cmdline.execute, command) 82 | self._restore_vps_options() 83 | 84 | def test_execute_vps_purchase_unknown_provider(self): 85 | command = ["vps", "purchase", "nonode", "-f", "-rp", "asdf", "1"] 86 | self._check_exit_code(2, cmdline.execute, command) 87 | 88 | def test_execute_vps_options_unknown_provider(self): 89 | command = ["vps", "options", "nonode"] 90 | self._check_exit_code(2, cmdline.execute, command) 91 | 92 | def _check_exit_code(self, exit_code, method, args): 93 | try: 94 | method(args) 95 | except SystemExit as e: 96 | self.assertEqual(exit_code, e.code) 97 | 98 | def test_execute_vps_options_no_provider(self): 99 | command = ["vps", "options"] 100 | self._check_exit_code(2, cmdline.execute, command) 101 | 102 | def test_purchase_vps_unknown_provider(self): 103 | args = Namespace() 104 | args.provider = "sd" 105 | args.type = "vps" 106 | self._check_exit_code(2, cmdline.purchase, args) 107 | 108 | def test_purchase_no_provider(self): 109 | args = Namespace() 110 | self._check_exit_code(2, cmdline.purchase, args) 111 | 112 | def test_purchase_vps_bad_provider(self): 113 | args = Namespace() 114 | args.provider = False 115 | args.type = "vps" 116 | self._check_exit_code(2, cmdline.purchase, args) 117 | 118 | def test_purchase_bad_type(self): 119 | args = Namespace() 120 | args.provider = "azirevpn" 121 | args.type = False 122 | self._check_exit_code(2, cmdline.purchase, args) 123 | 124 | def test_execute_vps_purchase_high_id(self): 125 | self._mock_vps_options() 126 | command = ["vps", "purchase", "linevast", "-c", self.settings_file, "-rp", "asdf", "1000"] 127 | self._check_exit_code(1, cmdline.execute, command) 128 | self._restore_vps_options() 129 | 130 | def test_execute_vps_purchase_low_id(self): 131 | mock = self._mock_vps_options() 132 | command = ["vps", "purchase", "linevast", "-c", self.settings_file, "-rp", "asdf", "-1"] 133 | self._check_exit_code(1, cmdline.execute, command) 134 | mock.assert_called_once() 135 | self._restore_vps_options() 136 | 137 | def _mock_vps_options(self, items=None): 138 | if items is None: 139 | items = [] 140 | self.vps_options = LineVast.get_options 141 | LineVast.get_options = MagicMock(return_value=items) 142 | return LineVast.get_options 143 | 144 | def _restore_vps_options(self): 145 | LineVast.get_options = self.vps_options 146 | 147 | def _mock_vpn_options(self, items=None): 148 | if items is None: 149 | items = [] 150 | self.vpn_options = AzireVpn.get_options 151 | AzireVpn.get_options = MagicMock(return_value=items) 152 | return AzireVpn.get_options 153 | 154 | def _restore_vpn_options(self): 155 | AzireVpn.get_options = self.vpn_options 156 | 157 | 158 | if __name__ == '__main__': 159 | unittest.main(exit=False) 160 | -------------------------------------------------------------------------------- /cloudomate/test/test_gateway.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | from unittest import TestCase 8 | from unittest import skip 9 | from builtins import open 10 | 11 | from mock import patch, Mock 12 | 13 | import requests 14 | from future import standard_library 15 | from future.moves.urllib import request 16 | 17 | from cloudomate.gateway.bitpay import BitPay 18 | from cloudomate.gateway.coinbase import Coinbase 19 | from cloudomate.util.bitcoinaddress import validate 20 | 21 | standard_library.install_aliases() 22 | 23 | 24 | class TestCoinbase(TestCase): 25 | # TODO find a new test coinbase url the old one isn't used anymore 26 | # test url from https://developers.coinbase.com/docs/merchants/payment-pages 27 | TEST_URL = 'https://www.coinbase.com/checkouts/2b30a03995ec62f15bdc54e8428caa87' 28 | amount = None 29 | address = None 30 | 31 | @classmethod 32 | @skip('the TEST_URL isn\t used anymore needs a replacement url') 33 | def setUpClass(cls): 34 | cls.amount, cls.address = Coinbase.extract_info(cls.TEST_URL) 35 | 36 | def test_address(self): 37 | self.assertTrue(validate(self.address)) 38 | 39 | def test_amount(self): 40 | self.assertGreater(self.amount, 0) 41 | 42 | 43 | class TestBitPay(TestCase): 44 | amount = None 45 | address = None 46 | rate = None 47 | 48 | @classmethod 49 | @skip('the TEST_URL isn\t used anymore needs a replacement url') 50 | def setUpClass(cls): 51 | html_file = open(os.path.join(os.path.dirname(__file__), 'resources/bitpay_invoice_data.json'), 'r') 52 | data = html_file.read().encode('utf-8') 53 | html_file.close() 54 | response = requests.Response() 55 | response.read = Mock(return_value=data) 56 | with patch.object(request, 'urlopen', return_value=response): 57 | cls.amount, cls.address = BitPay.extract_info('https://bitpay.com/invoice?id=KXnWTnNsNUrHK2PEp8TpDC') 58 | 59 | def test_address(self): 60 | self.assertEqual(self.address, '12cWmVndhmD56dzYcRuYka3Vpgjb3qdRoL') 61 | pass 62 | 63 | def test_amount(self): 64 | self.assertEqual(self.amount, 0.001402) 65 | 66 | def test_address_valid(self): 67 | self.assertTrue(validate(self.address)) 68 | -------------------------------------------------------------------------------- /cloudomate/test/test_hoster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest 7 | 8 | import requests 9 | 10 | from cloudomate.hoster.hoster import Hoster 11 | 12 | 13 | class TestHosterAbstract(unittest.TestCase): 14 | 15 | def test_create_browser(self): 16 | browser = Hoster._create_browser() 17 | if browser.session.headers['user-agent'] == requests.utils.default_user_agent(): 18 | self.fail('No Custom User-agent set in browser') 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /cloudomate/test/test_mullvad.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import datetime 7 | import unittest 8 | 9 | from future import standard_library 10 | from mock.mock import MagicMock 11 | 12 | from cloudomate.hoster.vpn.mullvad import MullVad 13 | from cloudomate.hoster.vpn.vpn_hoster import VpnOption 14 | from cloudomate.util.settings import Settings 15 | from cloudomate.wallet import Wallet 16 | 17 | standard_library.install_aliases() 18 | 19 | 20 | class TestMullvad(unittest.TestCase): 21 | 22 | def setUp(self): 23 | self.settings = Settings() 24 | self.settings.put("user", "accountnumber", "2132sadfqf") 25 | self.wallet = MagicMock(Wallet) 26 | self.mullvad = MullVad(self.settings) 27 | self.option = MagicMock(VpnOption) 28 | 29 | def test_purchase(self): 30 | self.mullvad._login = MagicMock() 31 | self.mullvad._order = MagicMock() 32 | self.mullvad.pay = MagicMock() 33 | 34 | self.mullvad.purchase(self.wallet, self.option) 35 | 36 | self.assertTrue(self.mullvad._login.called) 37 | self.assertTrue(self.mullvad._order.called) 38 | self.assertTrue(self.mullvad.pay.called) 39 | 40 | def test_get_status(self): 41 | self.mullvad._get_expiration_date = MagicMock(return_value="2 January 2019") 42 | self.mullvad._login = MagicMock() 43 | now = datetime.datetime.strptime("9 December 2018", "%d %B %Y") 44 | self.mullvad._get_current_date = MagicMock(return_value=now) 45 | 46 | (online, expire_date) = self.mullvad.get_status() 47 | 48 | self.assertEqual(True, online) 49 | self.assertEqual(2019, expire_date.year) 50 | self.assertEqual(1, expire_date.month) 51 | self.assertEqual(2, expire_date.day) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /cloudomate/test/test_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest 7 | 8 | import os 9 | from future import standard_library 10 | 11 | from cloudomate.util.settings import Settings 12 | 13 | standard_library.install_aliases() 14 | 15 | 16 | class TestSettings(unittest.TestCase): 17 | def setUp(self): 18 | self.settings = Settings() 19 | self.settings.read_settings(os.path.join(os.path.dirname(__file__), 'resources/test_settings.cfg')) 20 | 21 | def test_read_config(self): 22 | self.assertIsNotNone(self.settings) 23 | 24 | def test_has_first_name(self): 25 | self.assertIsNotNone(self.settings.get('user', 'firstname')) 26 | 27 | def test_has_email(self): 28 | self.assertTrue("@" in self.settings.get('user', 'email')) 29 | 30 | def test_verify_config(self): 31 | verification = { 32 | "user": [ 33 | "email", 34 | 'firstname', 35 | 'lastname' 36 | ] 37 | } 38 | self.assertTrue(self.settings.verify_options(verification)) 39 | 40 | def test_verify_bad_config(self): 41 | verification = { 42 | "user": [ 43 | "email", 44 | 'firstname', 45 | 'lastname' 46 | "randomattribute" 47 | ] 48 | } 49 | self.assertFalse(self.settings.verify_options(verification)) 50 | 51 | def test_put(self): 52 | key = "putkey" 53 | section = "putsection" 54 | value = "putvalue" 55 | self.settings.put(section, key, value) 56 | self.assertEqual(self.settings.get(section, key), value) 57 | 58 | def test_get_merge(self): 59 | key = 'email' 60 | sections = ['testhoster', 'user'] 61 | value = 'test@test.net' 62 | self.assertEqual(self.settings.get_merge(sections, key), value) 63 | 64 | def test_get_merge_ordering(self): 65 | key = 'email' 66 | sections = ['user', 'testhoster'] 67 | value = 'bot@pleb.net' 68 | self.assertEqual(self.settings.get_merge(sections, key), value) 69 | 70 | def test_custom_provider(self): 71 | self.assertEqual(self.settings.get("testhoster", "email"), "test@test.net") 72 | 73 | 74 | if __name__ == '__main__': 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /cloudomate/test/test_vpn_hosters.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest 7 | 8 | import os 9 | from future import standard_library 10 | from parameterized import parameterized 11 | 12 | from cloudomate.hoster.vpn.azirevpn import AzireVpn 13 | from cloudomate.util.settings import Settings 14 | 15 | standard_library.install_aliases() 16 | 17 | providers = [ 18 | (AzireVpn,), 19 | ] 20 | 21 | 22 | class TestHosters(unittest.TestCase): 23 | def setUp(self): 24 | self.settings = Settings() 25 | self.settings.read_settings(os.path.join(os.path.dirname(__file__), 'resources/test_settings.cfg')) 26 | 27 | @parameterized.expand(providers) 28 | def test_vpn_hoster_options(self, hoster): 29 | options = hoster.get_options() 30 | self.assertTrue(len(options) > 0) 31 | 32 | @parameterized.expand(providers) 33 | def test_vpn_hoster_configuration(self, hoster): 34 | config = hoster(self.settings).get_configuration() 35 | self.assertTrue(len(config) > 0) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /cloudomate/test/test_vps_hosters.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | import unittest 8 | from unittest import skip 9 | 10 | from future import standard_library 11 | from mock.mock import MagicMock 12 | from parameterized import parameterized 13 | 14 | from cloudomate.exceptions.vps_out_of_stock import VPSOutOfStockException 15 | from cloudomate.hoster.vps.blueangelhost import BlueAngelHost 16 | from cloudomate.hoster.vps.ccihosting import CCIHosting 17 | from cloudomate.hoster.vps.hostsailor import HostSailor 18 | # from cloudomate.hoster.vps.libertyvps import LibertyVPS 19 | from cloudomate.hoster.vps.libertyvps import LibertyVPS 20 | from cloudomate.hoster.vps.linevast import LineVast 21 | from cloudomate.hoster.vps.orangewebsite import OrangeWebsite 22 | # from cloudomate.hoster.vps.pulseservers import Pulseservers 23 | from cloudomate.hoster.vps.qhoster import QHoster 24 | from cloudomate.hoster.vps.routerhosting import RouterHosting 25 | from cloudomate.hoster.vps.twosync import TwoSync 26 | from cloudomate.hoster.vps.undergroundprivate import UndergroundPrivate 27 | from cloudomate.util.fakeuserscraper import UserScraper 28 | from cloudomate.util.settings import Settings 29 | 30 | standard_library.install_aliases() 31 | 32 | # Only the ones that currently work are uncommented 33 | providers = [ 34 | #(BlueAngelHost,), 35 | #(CCIHosting,), 36 | #(HostSailor,), 37 | #(LibertyVPS,), 38 | (LineVast,), 39 | #(OrangeWebsite,), 40 | # (Pulseservers,), 41 | (QHoster,) 42 | #(RouterHosting,), 43 | #(TwoSync,), 44 | #(UndergroundPrivate,), 45 | # (CrownCloud,), Manually checking orders results in being banned after running tests 46 | # (UndergroundPrivate,),# find a way to combine the url and the invoice to be able to go to the payment page 47 | ] 48 | 49 | 50 | class TestHosters(unittest.TestCase): 51 | @parameterized.expand(providers) 52 | def test_hoster_options(self, hoster): 53 | options = hoster.get_options() 54 | self.assertTrue(len(options) > 0) 55 | 56 | @parameterized.expand(providers) 57 | @unittest.skipIf(len(sys.argv) >= 2 and sys.argv[1] == 'discover', 'Integration tests have to be run manually') 58 | @skip('These tests relies on webscraping and form filling of vps pages. these pages change and therefore these ' 59 | 'tests are currently to unreliable') 60 | def test_hoster_purchase(self, hoster): 61 | user_settings = Settings() 62 | self._merge_random_user_data(user_settings) 63 | 64 | host = hoster(user_settings) 65 | options = list(host.get_options())[0] 66 | wallet = MagicMock() 67 | wallet.pay = MagicMock() 68 | 69 | try: 70 | host.purchase(wallet, options) 71 | wallet.pay.assert_called_once() 72 | except VPSOutOfStockException as exception: 73 | self.skipTest(exception) 74 | 75 | @staticmethod 76 | def _merge_random_user_data(user_settings): 77 | usergenerator = UserScraper() 78 | randomuser = usergenerator.get_user() 79 | for section in randomuser.keys(): 80 | for key in randomuser[section].keys(): 81 | user_settings.put(section, key, randomuser[section][key]) 82 | 83 | 84 | if __name__ == '__main__': 85 | unittest.main() 86 | -------------------------------------------------------------------------------- /cloudomate/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/cloudomate/f41af871bdc6bd148d53f94a4d0062eef5d608dd/cloudomate/util/__init__.py -------------------------------------------------------------------------------- /cloudomate/util/bitcoinaddress.py: -------------------------------------------------------------------------------- 1 | """Validate bitcoin/altcoin addresses 2 | 3 | Copied from: 4 | http://rosettacode.org/wiki/Bitcoin/address_validation#Python 5 | """ 6 | from __future__ import absolute_import 7 | from __future__ import division 8 | from __future__ import print_function 9 | from __future__ import unicode_literals 10 | 11 | from builtins import bytes 12 | from builtins import int 13 | from builtins import range 14 | from hashlib import sha256 15 | 16 | from future import standard_library 17 | 18 | standard_library.install_aliases() 19 | 20 | digits58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 21 | 22 | 23 | def _bytes_to_long(bytestring, byteorder): 24 | """Convert a bytestring to a long 25 | 26 | For use in python version prior to 3.2 27 | """ 28 | if byteorder == 'little': 29 | result = (v << i * 8 for (i, v) in enumerate(bytestring)) 30 | else: 31 | result = (v << i * 8 for (i, v) in enumerate(reversed(bytestring))) 32 | return sum(result) 33 | 34 | 35 | def _long_to_bytes(n, length, byteorder): 36 | """Convert a long to a bytestring 37 | 38 | For use in python version prior to 3.2 39 | Source: 40 | http://bugs.python.org/issue16580#msg177208 41 | """ 42 | if byteorder == 'little': 43 | indexes = list(range(length)) 44 | else: 45 | indexes = reversed(list(range(length))) 46 | return bytearray((n >> i * 8) & 0xff for i in indexes) 47 | 48 | 49 | def decode_base58(bitcoin_address, length): 50 | """Decode a base58 encoded address 51 | 52 | This form of base58 decoding is bitcoind specific. Be careful outside of 53 | bitcoind context. 54 | """ 55 | n = 0 56 | for char in bitcoin_address: 57 | try: 58 | n = n * 58 + digits58.index(char) 59 | except ValueError: 60 | msg = "Character not part of Bitcoin's base58: '%s'" 61 | raise ValueError(msg % (char,)) 62 | try: 63 | return n.to_bytes(length, 'big') 64 | except AttributeError: 65 | # Python version < 3.2 66 | return _long_to_bytes(n, length, 'big') 67 | 68 | 69 | def encode_base58(bytestring): 70 | """Encode a bytestring to a base58 encoded string 71 | """ 72 | # Count zero's 73 | zeros = 0 74 | for i in range(len(bytestring)): 75 | if bytestring[i] == 0: 76 | zeros += 1 77 | else: 78 | break 79 | try: 80 | n = int.from_bytes(bytestring, 'big') 81 | except AttributeError: 82 | # Python version < 3.2 83 | n = _bytes_to_long(bytestring, 'big') 84 | result = '' 85 | (n, rest) = divmod(n, 58) 86 | while n or rest: 87 | result += digits58[rest] 88 | (n, rest) = divmod(n, 58) 89 | return zeros * '1' + result[::-1] # reverse string 90 | 91 | 92 | def validate(bitcoin_address, magicbyte=0): 93 | """Check the integrity of a bitcoin address 94 | 95 | Returns False if the address is invalid. 96 | >>> validate('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i') 97 | True 98 | >>> validate('') 99 | False 100 | """ 101 | if isinstance(magicbyte, int): 102 | magicbyte = (magicbyte,) 103 | clen = len(bitcoin_address) 104 | if clen < 27 or clen > 35: # XXX or 34? 105 | return False 106 | try: 107 | bcbytes = decode_base58(bitcoin_address, 25) 108 | except ValueError: 109 | return False 110 | # Check magic byte (for other altcoins, fix by Frederico Reiven) 111 | for mb in magicbyte: 112 | if bcbytes.startswith(bytes(int(mb))): 113 | break 114 | else: 115 | return False 116 | # Compare checksum 117 | checksum = sha256(sha256(bcbytes[:-4]).digest()).digest()[:4] 118 | if bcbytes[-4:] != checksum: 119 | return False 120 | # Encoded bytestring should be equal to the original address, 121 | # for example '14oLvT2' has a valid checksum, but is not a valid btc 122 | # address 123 | return bitcoin_address == encode_base58(bcbytes) 124 | -------------------------------------------------------------------------------- /cloudomate/util/captchasolver.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import base64 7 | import json 8 | import os 9 | import time 10 | from builtins import open 11 | from builtins import str 12 | 13 | import requests 14 | from future import standard_library 15 | 16 | standard_library.install_aliases() 17 | 18 | """ 19 | Usage: 20 | 21 | Feed Anti Captcha account API key to solver: 22 | c_solver = CaptchaSolver("fd58e13e22604e820052b44611d61d6c") 23 | 24 | Find out how much money is left: 25 | temp = c_solver.get_balance() 26 | 27 | Provide captcha image full path to solver method to get solution: 28 | solution = c_solver.solve_captcha_text_case_sensitive( 29 | "/home/testm2/Desktop/testcscr/php/pyhton/cap2.jpg") 30 | print(str(solution)) 31 | 32 | Feed Anti-captcha account API key to solver: 33 | rc_solver = ReCaptchaSolver("fd58e13e22604e820052b44611d61d6c") 34 | 35 | Find out how much money is left: 36 | temp = rc_solver.get_balance() 37 | 38 | Provide solver method with Google ReCapthca URL and ReCaptcha website 39 | key usually found as data-sitekey attribute in an HTML element in the 40 | website containing the ReCaptcha: 41 | wUrl = "http://http.myjino.ru/recaptcha/test-get.php" 42 | wKey = "6Lc_aCMTAAAAABx7u2W0WPXnVbI_v6ZdbM6rYf16" 43 | rc_solution = rc_solver.solve_google_recaptcha(wUrl,wKey) 44 | print(rc_solution) 45 | """ 46 | 47 | 48 | class CaptchaSolver(object): 49 | _client_key = "not set" 50 | 51 | def __init__(self, c_key): 52 | self._client_key = c_key 53 | 54 | def get_balance(self): 55 | # Query API for account balance 56 | response = requests.post("https://api.anti-captcha.com/getBalance", 57 | json={"clientKey": self._client_key}) 58 | 59 | # Check response of HTTP request 60 | if response.status_code == requests.codes.ok: 61 | response_json = json.loads(response.text) 62 | if response_json["errorId"] == 0: 63 | print("Successful, account balance returned") 64 | return response_json["balance"] 65 | else: 66 | # Print API error 67 | print(response.text) 68 | else: 69 | # Print request error 70 | print(response.status_code) 71 | 72 | def solve_captcha_text_case_sensitive(self, full_image_file_path): 73 | # Encode captcha image before sending it 74 | if os.path.isfile(full_image_file_path): 75 | with open(full_image_file_path, "rb") as image_file: 76 | encoded_image_string = base64.b64encode(image_file.read()) 77 | print("Captcha image sucessfully encoded") 78 | else: 79 | print("Error: file path not found") 80 | return 81 | 82 | # Create new Captcha solving task using the API 83 | task_id = self._create_task_captcha_text_case_sensitive( 84 | encoded_image_string) 85 | 86 | # Query API until task is finished 87 | current_status = self._get_task_status(task_id) 88 | while current_status == "processing": 89 | print("Sleeping 5 sec") 90 | time.sleep(5) 91 | current_status = self._get_task_status(task_id) 92 | print("Current status: " + str(current_status)) 93 | 94 | # Get solution 95 | solution = self._get_task_result(task_id) 96 | return solution["text"] 97 | 98 | def _get_task_result(self, task_id): 99 | # Query API for the solution of the task 100 | response = requests.post("https://api.anti-captcha.com/getTaskResult", 101 | json={"clientKey": self._client_key, 102 | "taskId": task_id}) 103 | 104 | # Check response of HTTP request 105 | if response.status_code == requests.codes.ok: 106 | response_json = json.loads(response.text) 107 | if response_json["errorId"] == 0: 108 | print("Successful, captcha solved:") 109 | return response_json["solution"] 110 | else: 111 | # Print API error 112 | print(response.text) 113 | else: 114 | # Print request error 115 | print(response.status_code) 116 | 117 | def _get_task_status(self, task_id): 118 | # Query API for the status of the task 119 | response = requests.post("https://api.anti-captcha.com/getTaskResult", 120 | json={"clientKey": self._client_key, 121 | "taskId": task_id}) 122 | 123 | # Check response of HTTP request 124 | if response.status_code == requests.codes.ok: 125 | response_json = json.loads(response.text) 126 | if response_json["errorId"] == 0: 127 | print("Successful, task status returned") 128 | return response_json["status"] 129 | else: 130 | # Print API error 131 | print(response.text) 132 | else: 133 | # Print request error 134 | print(response.status_code) 135 | 136 | def _create_task_captcha_text_case_sensitive(self, base64_image_string): 137 | # Send task creation command to API 138 | response = requests.post("https://api.anti-captcha.com/createTask", 139 | json={"clientKey": self._client_key, 140 | "task": 141 | { 142 | "type": "ImageToTextTask", 143 | "body": base64_image_string, 144 | "phrase": False, 145 | "case": True, 146 | "numeric": False, 147 | "math": 0, 148 | "minLength": 0, 149 | "maxLength": 0 150 | } 151 | }) 152 | 153 | # Check response of HTTP request 154 | if response.status_code == requests.codes.ok: 155 | response_json = json.loads(response.text) 156 | if response_json["errorId"] == 0: 157 | print("Successful, captcha task was created:" + response.text) 158 | return response_json["taskId"] 159 | elif response_json["errorCode"] == "ERROR_NO_SLOT_AVAILABLE": 160 | time.sleep(15) 161 | return self._create_task_captcha_text_case_sensitive( 162 | base64_image_string) 163 | else: 164 | # Print API error 165 | print(response.text) 166 | else: 167 | # Print request error 168 | print(response.status_code) 169 | 170 | def get_current_key(self): 171 | return self._client_key 172 | 173 | 174 | class ReCaptchaSolver(CaptchaSolver): 175 | 176 | def _create_task_google_recaptcha(self, website_url, website_key): 177 | # Send task creation command to API 178 | response = requests.post("https://api.anti-captcha.com/createTask", 179 | json={ 180 | "clientKey": self._client_key, 181 | "task": 182 | { 183 | "type": "NoCaptchaTaskProxyless", 184 | "websiteURL": website_url, 185 | "websiteKey": website_key 186 | }, 187 | "softId": 0, 188 | "languagePool": "en" 189 | }) 190 | 191 | # Check response of HTTP request 192 | if response.status_code == requests.codes.ok: 193 | response_json = json.loads(response.text) 194 | if response_json["errorId"] == 0: 195 | print("Successful, task was created: " + response.text) 196 | return response_json["taskId"] 197 | elif response_json["errorCode"] == "ERROR_NO_SLOT_AVAILABLE": 198 | # In case task could not be created, create it again 199 | time.sleep(15) 200 | return self._create_task_google_recaptcha(website_url, 201 | website_key) 202 | else: 203 | # Print API errordd 204 | print(response.text) 205 | else: 206 | # Print request error 207 | print(response.status_code) 208 | 209 | def solve_google_recaptcha(self, website_url, website_key): 210 | # Create new Google ReCaptcha solving task using the API 211 | task_id = self._create_task_google_recaptcha(website_url, website_key) 212 | print("Sleeping 15 sec") 213 | time.sleep(15) 214 | 215 | # Query API until task is finished 216 | current_status = self._get_task_status(task_id) 217 | while current_status == "processing": 218 | print("Current status: " + str(current_status)) 219 | print("Sleeping 5 sec") 220 | time.sleep(5) 221 | current_status = self._get_task_status(task_id) 222 | print("Current status: " + str(current_status)) 223 | 224 | # Get solution of task 225 | solution = self._get_task_result(task_id) 226 | return solution["gRecaptchaResponse"] 227 | -------------------------------------------------------------------------------- /cloudomate/util/fakeuserscraper.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import random 7 | import string 8 | from builtins import object 9 | 10 | from future import standard_library 11 | 12 | from mechanicalsoup import StatefulBrowser 13 | 14 | standard_library.install_aliases() 15 | 16 | 17 | class UserScraper(object): 18 | """ 19 | Scrapes fakeaddressgenerator.com for fake user data. 20 | It also adds some basic additional information for server configuration. 21 | """ 22 | 23 | attributes = [ 24 | 'Full Name', 25 | 'Street', 26 | 'City', 27 | 'State Full', 28 | 'Zip Code', 29 | 'Phone Number', 30 | 'Company', 31 | 'Username' 32 | ] 33 | 34 | pages = { 35 | 'NL': 'http://www.fakeaddressgenerator.com/World/Netherlands_address_generator', 36 | 'US': 'http://www.fakeaddressgenerator.com/World/us_address_generator', 37 | 'UK': 'http://www.fakeaddressgenerator.com/World/uk_address_generator', 38 | 'CA': 'http://www.fakeaddressgenerator.com/World/ca_address_generator', 39 | } 40 | 41 | def __init__(self, country='NL'): 42 | self.country_code = country 43 | self.browser = StatefulBrowser() 44 | self.page = UserScraper.pages.get(country) 45 | 46 | def get_user(self): 47 | self.browser.open(self.page) 48 | attrs = {} 49 | 50 | for attr in self.attributes: 51 | attrs[attr] = self._get_attribute(attr) 52 | 53 | attrs['country_code'] = self.country_code 54 | attrs['password'] = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(12)) 55 | attrs['email'] = 'authentic8989+' + attrs['Username'] + '@gmail.com' 56 | attrs['rootpw'] = attrs['password'] 57 | attrs['ns1'] = 'ns1' 58 | attrs['ns2'] = 'ns2' 59 | attrs['hostname'] = attrs['Username'] + '.hostname.com' 60 | attrs['testnet'] = 'off' 61 | 62 | return self._map_to_config(attrs) 63 | 64 | @staticmethod 65 | def _map_to_config(attrs): 66 | config = {} 67 | # Treat full name separately because it needs to be split 68 | if 'Full Name' in attrs: 69 | config['user'] = {} 70 | config['user']['firstname'] = attrs['Full Name'].split('\xa0')[0] 71 | config['user']['lastname'] = attrs['Full Name'].split('\xa0')[-1] 72 | 73 | # Map the possible user attributes to their config names and sections 74 | mapping = { 75 | 'Street': ('address', 'address'), 76 | 'City': ('address', 'city'), 77 | 'State Full': ('address', 'state'), 78 | 'Zip Code': ('address', 'zipcode'), 79 | 'Phone Number': ('user', 'phonenumber'), 80 | 'Company': ('user', 'companyname'), 81 | 'Username': ('user', 'username'), 82 | 'country_code': ('address', 'countrycode'), 83 | 'password': ('user', 'password'), 84 | 'email': ('user', 'email'), 85 | 'rootpw': ('server', 'root_password'), 86 | 'ns1': ('server', 'ns1'), 87 | 'ns2': ('server', 'ns2'), 88 | 'hostname': ('server', 'hostname'), 89 | 'testnet': ('user', 'testnet') 90 | } 91 | 92 | for attr in attrs.keys(): 93 | if attr in mapping.keys(): 94 | section, key = mapping[attr] 95 | if section not in config: 96 | config[section] = {} 97 | config[section][key] = attrs[attr] 98 | return config 99 | 100 | def _get_attribute(self, attribute): 101 | return self.browser.get_current_page() \ 102 | .find(string=attribute) \ 103 | .parent.parent.parent \ 104 | .find('input') \ 105 | .get('value') 106 | -------------------------------------------------------------------------------- /cloudomate/util/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | import sys 8 | from builtins import open 9 | from builtins import str 10 | from configparser import ConfigParser 11 | from configparser import NoOptionError 12 | 13 | from appdirs import user_config_dir 14 | from future import standard_library 15 | 16 | standard_library.install_aliases() 17 | 18 | 19 | class Settings(object): 20 | def __init__(self): 21 | self.settings = ConfigParser() 22 | config_dir = user_config_dir() 23 | self._default_filename = os.path.join(config_dir, 'cloudomate.cfg') 24 | 25 | def get_default_config_location(self): 26 | return self._default_filename 27 | 28 | def read_settings(self, filename=None): 29 | """Read the settings object from a file. 30 | 31 | If the filename is omitted it is read from the default /cloudomate.conf location. 32 | 33 | :param filename: The file to read it from 34 | :return: Whether or not the config was successfully read 35 | """ 36 | if not filename: 37 | filename = self._default_filename 38 | 39 | if not os.path.exists(filename): 40 | print("Config file: '%s' not found" % filename) 41 | return False 42 | files = self.settings.read(filename, encoding='utf-8') 43 | return len(files) > 0 44 | 45 | def save_settings(self, filename=None): 46 | """Save this settings object to a file. 47 | 48 | If the filename is omitted it is saved to the default /cloudomate.conf location. 49 | 50 | :param filename: The file to save it to 51 | :return: Whether or not the config was successfully written 52 | """ 53 | if not filename: 54 | filename = self._default_filename 55 | 56 | try: 57 | self.settings.write(open(filename, 'w', encoding='utf-8')) 58 | except IOError: 59 | print("Failed to write configuration to '{}', printing it to stdout:".format(filename), file=sys.stderr) 60 | self.settings.write(sys.stdout) 61 | 62 | def verify_options(self, options): 63 | valid = True 64 | for section, keys in options.items(): 65 | if not self.settings.has_section(section): 66 | print("Section {} does not exist".format(section)) 67 | valid = False 68 | else: 69 | for key in keys: 70 | if not self.settings.has_option(section, key): 71 | print("Setting {}.{} does not exist".format(section, key)) 72 | valid = False 73 | return valid 74 | 75 | def get(self, section, key): 76 | return self.settings.get(section, key) 77 | 78 | def get_merge(self, sections, key): 79 | """Get a value from a merge of specified sections. 80 | The order of the passed sections denote their priority with the first having the highest priority. 81 | :param sections: The sections to look in for the value 82 | :param key: The key of the setting 83 | :return: The desired settings value 84 | """ 85 | for section in sections: 86 | if self.settings.has_option(section, key): 87 | return self.settings.get(section, key) 88 | print("Setting {} does not exist in any of the given sections".format(key)) 89 | raise NoOptionError(sections[-1], key) 90 | 91 | def put(self, section, key, value): 92 | if not self.settings.has_section(section): 93 | self.settings.add_section(section) 94 | 95 | self.settings.set(section, key, str(value)) 96 | 97 | def has_key(self, section, key): 98 | return self.settings.has_option(section, key) 99 | 100 | def has_key_merge(self, sections, key): 101 | for section in sections: 102 | if self.settings.has_option(section, key): 103 | return True 104 | raise False 105 | -------------------------------------------------------------------------------- /cloudomate/wallet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import 4 | from __future__ import division 5 | from __future__ import print_function 6 | from __future__ import unicode_literals 7 | 8 | import json 9 | import os 10 | import subprocess 11 | import sys 12 | from builtins import object 13 | from builtins import str 14 | 15 | from forex_python.bitcoin import BtcConverter 16 | from future import standard_library 17 | from mechanicalsoup import StatefulBrowser 18 | 19 | standard_library.install_aliases() 20 | 21 | if sys.version_info > (3, 0): 22 | from urllib.request import urlopen 23 | else: 24 | from urllib2 import urlopen 25 | 26 | AVG_TX_SIZE = 226 27 | SATOSHI_TO_BTC = 0.00000001 28 | 29 | 30 | def determine_currency(text): 31 | """ 32 | Determine currency of text 33 | :param text: text cointaining a currency symbol 34 | :return: currency name of symbol 35 | """ 36 | # Naive approach, for example NZ$ also contains $ 37 | if '$' in text or 'usd' in text.lower(): 38 | return 'USD' 39 | elif '€' in text or 'eur' in text.lower(): 40 | return 'EUR' 41 | else: 42 | return None 43 | 44 | 45 | def get_rate(currency='USD'): 46 | """ 47 | Return price of 1 currency in BTC 48 | Supported currencies available at 49 | http://forex-python.readthedocs.io/en/latest/currencysource.html#list-of-supported-currency-codes 50 | :param currency: currency to convert to 51 | :return: conversion rate from currency to BTC 52 | """ 53 | if currency is None: 54 | return None 55 | b = BtcConverter() 56 | factor = b.get_latest_price(currency) 57 | if factor is None: 58 | factor = 1.0 / fallback_get_rate(currency) 59 | return 1.0 / factor 60 | 61 | 62 | def fallback_get_rate(currency): 63 | # Sometimes the method above gets rate limited, in this case use 64 | # https: // blockchain.info / tobtc?currency = USD & value = 500 65 | return float(urlopen('https://blockchain.info/tobtc?currency={0}&value=1'.format(currency)).read()) 66 | 67 | 68 | def get_rates(currencies): 69 | """ 70 | Return rates for all currencies to BTC. 71 | :return: conversion rates from currencies to BTC 72 | """ 73 | rates = {cur: get_rate(cur) for cur in currencies} 74 | return rates 75 | 76 | 77 | def get_price(amount, currency='USD'): 78 | """ 79 | Convert price from one currency to bitcoins 80 | :param amount: number of currencies to convert 81 | :param currency: currency to convert from 82 | :return: price in bitcoins 83 | """ 84 | price = amount * get_rate(currency) 85 | return price 86 | 87 | 88 | def _get_network_cost(speed): 89 | br = StatefulBrowser(user_agent='Firefox') 90 | page = br.open('https://bitcoinfees.earn.com/api/v1/fees/recommended') 91 | response = page.json() 92 | satoshirate = float(response[speed]) 93 | return satoshirate 94 | 95 | 96 | def get_network_fee(speed='halfHourFee'): 97 | """ 98 | Give an estimate of network fee for the average bitcoin transaction for given speed. 99 | Supported speeds are available at https://bitcoinfees.earn.com/api/v1/fees/recommended 100 | :return: network cost 101 | """ 102 | network_fee = _get_network_cost(speed) * SATOSHI_TO_BTC 103 | return network_fee * AVG_TX_SIZE 104 | 105 | 106 | class Wallet(object): 107 | """ 108 | Wallet implements an adapter to the wallet handler. 109 | Currently Wallet only supports electrum wallets without passwords for automated operation. 110 | Wallets with passwords may still be used, but passwords will have to be entered manually. 111 | """ 112 | 113 | def __init__(self, wallet_command=None, wallet_path=None, testnet=None): 114 | if wallet_command is None: 115 | if os.path.exists('/usr/local/bin/electrum'): 116 | wallet_command = ['/usr/local/bin/electrum'] 117 | else: 118 | wallet_command = ['/usr/bin/env', 'electrum'] 119 | if testnet: 120 | wallet_command.append('--testnet') 121 | self.command = wallet_command 122 | self.wallet_handler = ElectrumWalletHandler(wallet_command, wallet_path) 123 | 124 | def get_balance(self, confirmed=True, unconfirmed=True): 125 | """ 126 | Return the balance of the default electrum wallet 127 | Confirmed and unconfirmed can be set to indicate which balance to retrieve. 128 | :param confirmed: default: True 129 | :param unconfirmed: default: True 130 | :return: balance of default wallet 131 | """ 132 | balance_output = self.wallet_handler.get_balance() 133 | balance = 0.0 134 | if confirmed: 135 | balance = balance + float(balance_output.get('confirmed', 0.0)) 136 | if unconfirmed: 137 | balance = balance + float(balance_output.get('unconfirmed', 0.0)) 138 | return balance 139 | 140 | def get_balance_confirmed(self): 141 | """ 142 | Return confirmed balance of default electrum wallet 143 | :return: 144 | """ 145 | return self.get_balance(confirmed=True, unconfirmed=False) 146 | 147 | def get_balance_unconfirmed(self): 148 | """ 149 | Return unconfirmed balance of default electrum wallet 150 | :return: 151 | """ 152 | return self.get_balance(confirmed=False, unconfirmed=True) 153 | 154 | def get_addresses(self): 155 | """ 156 | Return the list of addresses of the default electrum wallet 157 | :return: 158 | """ 159 | address_output = self.wallet_handler.get_addresses() 160 | return address_output 161 | 162 | def pay(self, address, amount, fee=None): 163 | tx_fee = 0 if fee is None else fee 164 | if self.get_balance() < amount + tx_fee: 165 | print('Not enough funds') 166 | return 167 | 168 | transaction_hex = self.wallet_handler.create_transaction(amount, address) 169 | transaction_hash = self.wallet_handler.broadcast(transaction_hex) 170 | # no/empty transaction hash means the broadcast was not successful 171 | if not transaction_hash: 172 | print(('Transaction not successfully broadcast, do error handling: {0}'.format(transaction_hash))) 173 | else: 174 | print('Transaction successful') 175 | print(transaction_hex) 176 | print(transaction_hash) 177 | return transaction_hash 178 | 179 | 180 | class ElectrumWalletHandler(object): 181 | """ 182 | ElectrumWalletHandler ensures the correct opening and closing of the electrum wallet daemon 183 | """ 184 | 185 | def __init__(self, wallet_command=None, wallet_path=None): 186 | """ 187 | Allows wallet_command to be changed to for example electrum --testnet 188 | :param wallet_command: command to call wallet 189 | """ 190 | self._wallet_path = wallet_path 191 | 192 | if wallet_command is None: 193 | if os.path.exists('/usr/local/bin/electrum'): 194 | wallet_command = ['/usr/local/bin/electrum'] 195 | else: 196 | wallet_command = ['/usr/bin/env', 'electrum'] 197 | self.command = wallet_command 198 | p, e = subprocess.Popen(self.command + ['daemon', 'status'], stdout=subprocess.PIPE).communicate() 199 | self.not_running_before = b'not running' in p 200 | if self.not_running_before: 201 | subprocess.call(self.command + ['daemon', 'start']) 202 | 203 | if wallet_path is not None: 204 | print('Using wallet: ', wallet_path) 205 | self._command(['daemon', 'load_wallet'], output=False) 206 | 207 | def __del__(self): 208 | if self.not_running_before: 209 | subprocess.call(self.command + ['daemon', 'stop']) 210 | 211 | def create_transaction(self, amount, address, fee=None): 212 | """ 213 | Create a transaction 214 | :param amount: amount of bitcoins to be transferred 215 | :param address: address to transfer to 216 | :param fee: None for autofee, or specify own fee 217 | :return: transaction details 218 | """ 219 | if fee is None: 220 | transaction = self._command(['payto', str(address), str(amount)]) 221 | else: 222 | transaction = self._command(['payto', str(address), str(amount), '-f', str(fee)]) 223 | jtrs = json.loads(transaction) 224 | return jtrs['hex'] 225 | 226 | def broadcast(self, transaction): 227 | """ 228 | Broadcast a transaction. 229 | If successful it returns a transaction_id, otherwise it doesn't 230 | :param transaction: hex of transaction 231 | :return: transaction_id (or also called transaction hash) 232 | """ 233 | transaction_id = self._command(['broadcast', transaction]) 234 | return transaction_id 235 | 236 | def get_balance(self): 237 | """ 238 | Return the balance of the default electrum wallet 239 | :return: balance of default wallet 240 | """ 241 | output = self._command(['getbalance']) 242 | print('\n\n', output, '\n\n') 243 | balance_dict = json.loads(output) 244 | return balance_dict 245 | 246 | def get_addresses(self): 247 | """ 248 | Return the list of addresses of default wallet 249 | :return: 250 | """ 251 | address = self._command(['listaddresses']) 252 | addr = json.loads(address) 253 | return addr 254 | 255 | def _command(self, c, output=True): 256 | command = self.command + c 257 | if self._wallet_path is not None: 258 | command += ['-w', self._wallet_path] 259 | 260 | if output: 261 | return subprocess.check_output(command).decode() 262 | else: 263 | subprocess.call(command) 264 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | python-tag = py3 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from cloudomate.globals import __version__ 2 | from codecs import open 3 | from os import path 4 | import sys 5 | 6 | 7 | from setuptools import setup, find_packages 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | if sys.version_info.major == 2: 15 | package_data = { 16 | b'cloudomate': [], 17 | } 18 | else: 19 | package_data = { 20 | 'cloudomate': [], 21 | } 22 | 23 | setup( 24 | name='cloudomate', 25 | 26 | version=__version__, 27 | 28 | description='Automate buying VPS instances with Bitcoin', 29 | long_description=long_description, 30 | 31 | url='https://github.com/tribler/cloudomate', 32 | 33 | author='PlebNet', 34 | author_email='authentic8989@gmail.com', 35 | 36 | license='LGPLv3', 37 | 38 | classifiers=[ 39 | 'Development Status :: 3 - Alpha', 40 | 41 | 'Intended Audience :: Developers', 42 | 'Topic :: System :: Installation/Setup', 43 | 'Topic :: Software Development :: Libraries :: Python Modules', 44 | 45 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 46 | 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.6', 49 | 50 | 'Operating System :: POSIX :: Linux', 51 | 'Operating System :: MacOS', 52 | ], 53 | 54 | keywords='vps bitcoin', 55 | 56 | packages=find_packages(exclude=['docs', 'test']), 57 | 58 | install_requires=[ 59 | 'appdirs', 60 | 'lxml', 61 | 'MechanicalSoup', 62 | 'CurrencyConverter', 63 | 'bs4', 64 | 'forex-python', 65 | 'parameterized', 66 | 'fake-useragent', 67 | 'CaseInsensitiveDict', 68 | 'ConfigParser', 69 | 'future', 70 | 'requests[security]', 71 | 'python-dateutil', 72 | 'websocket-client', 73 | 'selenium', 74 | 'geckodriver-autoinstaller' 75 | ], 76 | 77 | extras_require={ 78 | 'dev': [], 79 | 'test': ['mock', 'parameterized'], 80 | }, 81 | 82 | package_data=package_data, 83 | 84 | entry_points={ 85 | 'console_scripts': [ 86 | 'cloudomate=cloudomate.cmdline:execute', 87 | ], 88 | }, 89 | ) 90 | --------------------------------------------------------------------------------