#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c) Remi Ferrand , 2014 # # This software is a computer program whose purpose is to [describe # functionalities and technical features of your software]. # # This software is governed by the CeCILL license under French law and # abiding by the rules of distribution of free software. You can use, # modify and/ or redistribute the software under the terms of the CeCILL # license as circulated by CEA, CNRS and INRIA at the following URL # "http://www.cecill.info". # # As a counterpart to the access to the source code and rights to copy, # modify and redistribute granted by the license, users are provided only # with a limited warranty and the software's author, the holder of the # economic rights, and the successive licensors have only limited # liability. # # In this respect, the user's attention is drawn to the risks associated # with loading, using, modifying and/or developing or reproducing the # software by the user in light of its specific status of free software, # that may mean that it is complicated to manipulate, and that also # therefore means that it is reserved for developers and experienced # professionals having in-depth computer knowledge. Users are therefore # encouraged to load and test the software's suitability as regards their # requirements in conditions enabling the security of their systems and/or # data to be ensured and, more generally, to use and operate it in the # same conditions as regards security. # # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. # import re import os import sys import json import logging import requests import HTMLParser from optparse import OptionParser from prettytable import PrettyTable # try: # import http.client as http_client # except ImportError: # import httplib as http_client # # http_client.HTTPConnection.debuglevel = 1 # # logging.basicConfig() # logging.getLogger().setLevel(logging.DEBUG) # requests_log = logging.getLogger("requests.packages.urllib3") # requests_log.setLevel(logging.DEBUG) # requests_log.propagate = True class HTTPClient(object): USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36' def __init__(this): pass def _default_headers(this): return { 'User-Agent': this.USER_AGENT } def get(this, url, headers={}, *args, **kwargs): h = this._default_headers().copy() headers.update(h) return requests.get(url, headers=headers, *args, **kwargs) def post(this, url, headers={}, *args, **kwargs): h = this._default_headers().copy() headers.update(h) return requests.post(url, headers=headers, *args, **kwargs) @staticmethod def unescapeHTML(text): return HTMLParser.HTMLParser().unescape(text) class FreeAdslBillFetcher(HTTPClient): LOGIN_URL = 'https://subscribe.free.fr/login/login.pl' LOGOUT_URL = 'https://adsl.free.fr/logout.pl' BILL_ROOT_URL = 'https://adsl.free.fr' LIST_BILLS_URL = BILL_ROOT_URL + '/liste-factures.pl' BILLFINDER_RE = re.compile(r'(?P\w+?\s*\d{4}).*?(?P\d+(?:\.\d+)?) Euros.*?" % this.title.encode('utf-8') def __init__(this, user, password): this.user = user this.password = password this._transaction_creds = None def login(this): payload = { 'login': this.user, 'pass' : this.password, 'ok' : 'Valider', 'link' : '' } headers = { 'Content-Type': 'application/x-www-form-urlencoded' } r = this.post(this.LOGIN_URL, headers=headers, data=payload, allow_redirects=False) location = r.headers['location'] this._buildSessionCreds(location) def _buildSessionCreds(this, url): # https://adsl.free.fr/home.pl?id=16779576&idt=1015c62833046b0f ids_s = url[url.index('?')+1:] h = dict(x.split('=') for x in ids_s.split('&')) this._transaction_creds = FreeAdslBillFetcher.FreeSessionCredentials(id=h['id'], idt=h['idt']) def _appendUrlCreds(this, url): creds = this._transaction_creds return url + '?' + str(creds) def listBills(this): if this._transaction_creds is None: this.login() # We need to build the URL/params by hand. # python-requests params doesn't work here # it seems that url parameters are order dependant # on this webservice url = this._appendUrlCreds(this.LIST_BILLS_URL) r = this.get(url, allow_redirects=False) bills = this._parseBillsList(this.unescapeHTML(r.text)) return bills def fetchBill(this, bill): # assume that whole PDF fits in memory return this.get(bill.url).content def writeBillToFile(this, bill, file): # assume that whole PDF fits in memory with open(file, 'wb') as f: f.write(this.fetchBill(bill)) def _parseBillsList(this, body): match = this.BILLFINDER_RE.findall(body) bills = [] for month, price, url in match: bills.append(FreeAdslBillFetcher.FreeAdslBill(month, float(price), this.BILL_ROOT_URL + '/' + url)) return bills def logout(this): if this._transaction_creds is None: return url = this._appendUrlCreds(this.LOGOUT_URL) this.get(url, allow_redirects=False) def __enter__(this): this.login() return this def __exit__(this, type, value, traceback): this.logout() class FreeAdslBillFetcherCli(object): PROG_NAME = 'free_adsl_bill_fetcher' @staticmethod def _buildOptParser(): parser = OptionParser() parser.add_option("-p", "--show-price", dest="show_price", default=False, action="store_true", help="show price when listing bills") parser.add_option("--latest", dest="fetch_latest", default=False, action="store_true", help="only fetch latest bill") parser.add_option("-c", "--config", dest="config_file", default=os.path.expanduser('~') + '/.' + FreeAdslBillFetcherCli.PROG_NAME + '.conf', metavar='FILE', help="configuration file") parser.add_option("--get", dest="wanted_bills", action="append", default=[], metavar='BILL_TITLE', help="Download bill BILL_TITLE and write it as BILL_TITLE.pdf") parser.add_option("-d", "--write-dir", dest="write_directory", default='.', metavar='DIR', help="write bills to directory DIR") parser.add_option("--name-prefix", dest="name_prefix", default='', metavar='STR', help="prefix each bill filename with STR") parser.add_option("--name-suffix", dest="name_suffix", default='', metavar='STR', help="suffix each bill filename with STR (before PDF extension)") return parser def parseArgs(this, arg): (options, args) = this.opt_parser.parse_args(args=arg) this.options = options this.args = args def __init__(this, args=sys.argv[1:]): this.opt_parser = this._buildOptParser() this.options = None this.args = None this.parseArgs(args) this.config = this._parseConfigFile(this.options.config_file) this.fetcher = None this.table = this._prepareTable() def __enter__(this, *args, **kwargs): this.fetcher = FreeAdslBillFetcher(this.config['login'], this.config['password']) this.fetcher.login() return this def __exit__(this, type, value, traceback): this.fetcher.logout() @staticmethod def _parseConfigFile(file): with open(file) as f: return json.load(f) def _prepareTable(this): headers = ['Month'] if this.options.show_price: headers.insert(1, 'Price') pt = PrettyTable(headers) pt.align['Month'] = 'l' return pt def run(this): wanted_bills = len(this.options.wanted_bills) if wanted_bills != 0 or this.options.fetch_latest: table_output = False else: table_output = True for bill in this.fetcher.listBills(): if this.options.fetch_latest is True: pdf_path = this._fetchBill(bill) print "Your latest bill was written to '%s'" % pdf_path break if wanted_bills > 0: if bill.title in [x.decode('utf-8') for x in this.options.wanted_bills]: pdf_path = this._fetchBill(bill) print "[*] %s bill was written to '%s'" % (bill.title, pdf_path) else: this._appendBillToTable(bill) if table_output: print this.table sys.exit(0) def _appendBillToTable(this, bill): row = [bill.title] if this.options.show_price: row.insert(1, '%.2f €' % bill.amount) this.table.add_row(row) def _composeBillFilename(this, bill): bill_filename = this.options.write_directory.rstrip('/') + '/' bill_filename += this.options.name_prefix bill_filename += re.sub(r'\s+', '_', bill.title) bill_filename += this.options.name_suffix + '.pdf' return bill_filename def _fetchBill(this, bill): bill_filename = this._composeBillFilename(bill) this.fetcher.writeBillToFile(bill, bill_filename) return bill_filename if __name__ == "__main__": with FreeAdslBillFetcherCli() as cli: cli.run()