Fake-Formular per POST an Bank verschicken

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Fake-Formular per POST an Bank verschicken

Beitragvon droptix » Freitag 29. Dezember 2006, 14:44

Meine Bank bietet mir den Download der Kontoumsätze als CSV-Datei an. Mich nervt allerdings immer das Einloggen und die ganze Prozedur, bis ich endlich bei den Umsätzen bin. Ich möchte daher den Download weitestgehend automatisieren. Leider bietet die Bank auch keine API dafür an.

Die Bank bietet zwar eine Software dafür an, allerdings läuft diese nur unter Windows und auch sonst ist die nicht so grandios. Also dachte ich dran, das ganze mit PHP oder besser noch mit Python zu machen.

Der Login erfolgt mit Kontonummer und PIN. Beides geht über eine SSL-Verbindung zum Webserver. Die anschließenden Formulare sind immer gleich. Diese lassen sich also faken. Ich erhalte aber bei jedem Login eine andere Session-ID, die immer als hidden-Field in den HTML-Formularen steckt.

Es gilt also zunächst, den Login automatisiert per HTTP-POST-Request durchzuführen. Anschließend kann ich aus dem Seitenquelltext via RegEx die Session-ID extrahieren.

Wie gestaltet man in Python einen HTTP-POST-Request?
Benutzeravatar
Leonidas
Administrator
Beiträge: 16023
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Re: Fake-Formular per POST an Bank verschicken

Beitragvon Leonidas » Freitag 29. Dezember 2006, 14:52

droptix hat geschrieben:Wie gestaltet man in Python einen HTTP-POST-Request?

Das geht über den Data-Parameter von urllib2.urlopen(). Hier mal ein Beispiel, wie ich so etwas ähnliches gemacht habe (ohne Cookielib). Dabei habe ich aber BeautifulSoup statt regulären Ausdrücken verwendet um mit mein Ticket, in deinem Fall eine Session-ID, rauszuholen.
My god, it's full of CARs! | Leonidasvoice vs Modvoice
Benutzeravatar
Luzandro
User
Beiträge: 87
Registriert: Freitag 21. April 2006, 17:03

Beitragvon Luzandro » Samstag 30. Dezember 2006, 11:30

Ich habe mir für solche und ähnliche Sachen einen "FakeBrowser" geschrieben:

Code: Alles auswählen

 #! /usr/bin/env python
# -*- coding: UTF-8 -*-

import cookielib, urllib2, socket, urlparse, libxml2dom
from urllib import urlencode

class FakeBrowser:
    HEADERS = {'User-agent' : 'Opera/9.10 (X11; Linux i686; U; en)'}
   
    def __init__(self):
        socket.setdefaulttimeout(30) # seconds
        self.cj = cookielib.LWPCookieJar()
        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
        urllib2.install_opener(opener)
        self.standardParser = None
        self.url = None
       
    def _getAbsoluteURL(self, url):
        if url.startswith('http://') or url.startswith('https://'):
            return url
        else:
            if self.url is None:
                raise Exception, "History is empty - cannot open relative url"
            else:
                return urlparse.urljoin(self.url, url)
       
    def GET(self, url, getDomNode=False, parse=True):
        return self.POST(url, None, getDomNode, parse)
           
    def POST(self, url, params={}, getDomNode=False, parse=True):
        if params != None:
            params = urlencode(params)
        url = self._getAbsoluteURL(url)
        req = urllib2.Request(url, params, self.HEADERS)
        retries = 0
        while retries < 5:
            try:
                handle = urllib2.urlopen(req)
                data = handle.read()
                parse and self.standardParser != None and self.standardParser(data)
                self.url = handle.geturl()
                if getDomNode:
                    return libxml2dom.parseString(data, html=True)
                else:
                    return data
            except (socket.timeout, urllib2.URLError):
                retries += 1
        raise Exception, "5 retries - aborting..."
       
    def getHiddenFields(self, formNode, params=None):
        if params is None:
            params = {}
        for h in formNode.xpath("//input[@type='hidden']"):
            params[h.getAttribute('name')] = h.getAttribute('value')
        return params
   
    def printCookies(self):
        for index, cookie in enumerate(self.cj):
            print index, '  :  ', cookie


libxml2dom musst du vermutlich erst installieren, aber es vereinfacht die Verarbeitung doch ziemlich (wie z.B. das Übernehmen der hidden Fields, was du ja auch brauchst)
Noch ein konkretes Bsp:

Code: Alles auswählen

#! /usr/bin/python
# -*- coding: UTF-8 -*-

import os.path
from fakeBrowser import FakeBrowser

LOGIN_URL = 'https://rechnung.bob.at/login.do'
DL_DIR = '/mnt/guardian/shared/bob.rechnung/'

def login(browser, user, pwd):
    doc = browser.GET(LOGIN_URL, getDomNode=True)
    form = doc.xpath("//form")[0]
    params = browser.getHiddenFields(form)
    params['username'] = user
    params['password'] = pwd
    response = browser.POST(form.getAttribute('action'), params, getDomNode=True)
    if browser.url == LOGIN_URL:
        raise Exception, "Login failed..."
    return response
       
def downloadBills(browser, docNode):
    links = docNode.xpath("//a[@title='Rechnung als PDF anzeigen']")
    for link in links:
        href = link.getAttribute('href')
        filename = href[href.rfind('/')+1 : href.find('?')]
        if not os.path.exists(os.path.join(DL_DIR, filename)):
            bill = browser.GET(href)
            f = open(os.path.join(DL_DIR, filename), 'w')
            f.write(bill)
            f.close()
   
if __name__ == '__main__':
    fb = FakeBrowser()
    response = login(fb, 'XXX', 'YYY')
    downloadBills(fb, response)
   
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ganz gut

Beitragvon droptix » Samstag 30. Dezember 2006, 17:19

OK, das klappt schonmal ganz gut. Ich besuche zuerst die Startseite, um eine Pre-Session-ID zu erhalten. Außerdem ist das Feld für das Passwort jedesmal generiert, damit der Login nur im Zusammenspiel aus Pre-Session-ID und Passwortfeld funktioniert.

Ich habe also beides. Nun erstelle ich ein Dictionary, dessen Keys den Feldnamen des Login-Formulars entsprechen:

Code: Alles auswählen

fields = {}
fields['KtoNr'] = "123456789"
fields['d682ca'] = "passwort" # feldname wird immer neu generiert
fields['pre_sess'] = "3dbf2c29f472daad4e232fd387943c20p000_00005078"
fields['Login'] = "Login" # login button


Nun schicke ich das alles ab:

Code: Alles auswählen

url = "http://www.bank.de/login.php"
request = urllib2.Request(url)
data = urllib.urlencode(fields)
try:
   handle = urllib2.urlopen(request, data)
   content = handle.read()
   handle.close()
   return content
except urllib2.HTTPError:
   print "error 404: %s" % url


Es geht! Aber: Wo stelle ich ein, ob ich die Daten in data per GET oder POST abschicken möchte? Müsste man die bei GET gleich direkt an die URL hängen?
Benutzeravatar
Luzandro
User
Beiträge: 87
Registriert: Freitag 21. April 2006, 17:03

Re: Ganz gut

Beitragvon Luzandro » Samstag 30. Dezember 2006, 17:29

droptix hat geschrieben:Es geht! Aber: Wo stelle ich ein, ob ich die Daten in data per GET oder POST abschicken möchte? Müsste man die bei GET gleich direkt an die URL hängen?

Wenn data angegeben ist, wird es laut Doku per POST abgeschickt, d.h. zu der zweiten Frage würde ich mal ja sagen.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Alles funzt!

Beitragvon droptix » Samstag 30. Dezember 2006, 18:00

Jippieh, alles klappt! Nun kriege ich meine Kontoauszüge automatisiert per Python-Skript :D

Jetzt muss ich die Transaktionen aus der CSV-Datei nur noch in eine Datenbank überführen...

EDIT: Ich stelle fest, dass die CSV-Datei nach dem Runterladen immer eine Leerzeile nach jeder Zeile hat. Wenn ich sie manuell über die Website runterlade, dann ist das nicht der Fall. Muss ich da noch irgend einen Header mitsenden, um den Zeilenumbruch wegzukriegen oder passiert das während des Speicherns? Ich suche mir nach dem Login die URL der Datei raus und fordere die URL an. Als Ergebnis erhalte ich den Inhalt der Datei als String, den ich anschließend speichere. Muss ich vorher vielleicht noch Zeilenumbrüche umwandeln?
Benutzeravatar
Leonidas
Administrator
Beiträge: 16023
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Beitragvon Leonidas » Samstag 30. Dezember 2006, 18:57

Dazu musst du uns wohl den Code zeigen, der die CSV-Datei speichert. Ich tippe mal drauf, dass writeline() aufgerufen wird, die Datei aber an den einzlenen Zeilen schon ein \n hat. Irgendwie so wird es sein, dass die Zeilenumbrüche dann doppelt sind. Lässt sich sicher leicht beheben.
My god, it's full of CARs! | Leonidasvoice vs Modvoice
BlackJack

Beitragvon BlackJack » Samstag 30. Dezember 2006, 18:58

Wie hast Du das mit der Leerzeile festgestellt? Ist da irgendwo ``print`` involviert? Das gibt ja selbst nochmal einen Zeilenumbruch aus, auch wenn die Zeichenkette schon einen am Ende hatte.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Hier der Code...

Beitragvon droptix » Sonntag 31. Dezember 2006, 15:30

BlackJack hat geschrieben:Wie hast Du das mit der Leerzeile festgestellt?


Na indem ich die runtergeladene Datei in einem Editor oder in OpenOffice.org Calc öffne.

Hier der Code...

Prinzipiell bastel ich mir die Download-URL analog zu Leonidas' Code-Snippet mittels urllib2 und BeautifulSoup zusammen. Die URL wird dann in getPage(url) angefordert. Als Ergebnis erhalte ich den Inhalt der Datei als String, den ich mit write() in eine Datei schreibe.

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import urllib
import urllib2
import urlparse
import os.path
from BeautifulSoup import BeautifulSoup

class Object:
   pass

class AccountTurnover:
   def __init__(self):
      # configuration
      self.config = Object()
      # front page
      self.config.frontpage = Object()
      self.config.frontpage.url = None
      # login page
      self.config.login = Object()
      self.config.login.url = None
      self.config.login.user = None
      self.config.login.passwd = None
      # download page
      self.config.download = Object()
      self.config.download.url = None
      # logout action
      self.config.logout = Object()
      self.config.logout.url = None
      # session
      self.session = None
      # file content and filename
      self.fileContent = ""
   
   def getPage(self, url, data=None, headers=None):
      request = urllib2.Request(url)
      if data is not None:
         data = urllib.urlencode(data)
      try:
         handle = urllib2.urlopen(request, data)
         content = handle.read()
         handle.close()
         return content
      except urllib2.HTTPError:
         print "error 404: %s" % url
   
   def getElement(self, html, name, attrs):
      soup = BeautifulSoup(html)
      tag = soup.find(name, attrs)
      return tag
   
   def login(self):
      # get pre-session
      html = self.getPage(self.config.frontpage.url)
      frameSrc = self.getElement(html, "frame", {'scrolling' : "yes"})['src']
      # get password field
      html = self.getPage(urlparse.urljoin(self.config.login.url, frameSrc))
      fields = {}
      fields['KtoNr'] = self.config.login.user
      fields['pre_sess'] = self.getElement(html, "input", {'name' : "pre_sess"})['value']
      fields['Direkt'] = "120401"
      fields['screenres'] = ""
      fields['Login'] = "Login"
      passwdField = self.getElement(html, "input", {'type' : "password"})['name']
      fields[passwdField] = self.config.login.passwd
      # get session
      html = self.getPage(self.config.login.url, fields)
      self.session = self.getElement(html, "input", {'name' : "sess"})['value']
   
   def logout(self):
      fields = {}
      fields['sess'] = self.session
      fields['Logout'] = "Logout"
      self.getPage(self.config.logout.url, fields)
      print "logged out"
   
   def download(self):
      fields = {}
      fields['Konto'] = self.config.login.user
      fields['Art'] = "99"
      fields['von'] = "01.12.2006"
      fields['bis'] = "31.12.2006"
      fields['ungebucht'] = "1"
      fields['sess'] = self.session
      fields['Weiter'] = "Weiter"
      html = self.getPage(self.config.download.url, fields)
      # get downloads
      downloads = self.getElement(html, "ul", {'class' : "downloads"})
      csv = self.getElement(html, "a", {'target' : "_blank"})['href']
      url = urlparse.urljoin(self.config.download.url, csv)
      url = url.split("&")
      url = "&".join(url)
      self.fileContent = self.getPage(url)
   
   def saveToDisk(self, filename):
      print "saving to %s..." % filename
      try:
         handle = file(filename, "w")
         handle.write(self.fileContent)
         handle.close()
         print "saved successfully"
      except:
         print "error while saving"

if __name__ == "__main__":
   at = AccountTurnover()
   at.config.frontpage.url = "https://meine.bank.de/index.php"
   at.config.login.url = "https://meine.bank.de/login.php"
   at.config.logout.url = "https://meine.bank.de/logout.php"
   at.config.download.url = "https://meine.bank.de/umsaetze.php"
   at.config.login.user = "1234567890"
   at.config.login.passwd = raw_input("%s: " % at.config.login.user)
   at.login()
   at.download()
   filename = None
   while not filename:
      filename = raw_input("save to:")
      if os.path.exists(filename):
         print "%s already exists... choose again" % filename
         filename = None
   at.saveToDisk(filename)
   at.logout()


EDIT: Ach ja nochwas: Ich suche sowas wie str_replace() aus PHP. Aus irgend einem Grund krieg ich als Download-URL sowas wie

http://meine.bank.de/download.php?file= ... 8ff8daaa...

Also anstatt dem normalen & als Variablen-Trenner bekomme ich die HTML-Entity & Wenn ich die nicht umwandle (s. download()), ist die URL ungültig und die Datei kann nicht runtergeladen werden. Ich behelfe mir momentan mit einem split("&") und einem anschließenden "&".join().
BlackJack

Re: Hier der Code...

Beitragvon BlackJack » Sonntag 31. Dezember 2006, 16:26

droptix hat geschrieben:
BlackJack hat geschrieben:Wie hast Du das mit der Leerzeile festgestellt?


Na indem ich die runtergeladene Datei in einem Editor oder in OpenOffice.org Calc öffne.


Damit siehst Du normalerweise nicht wirklich was drinsteht. Also Byte für Byte. Wenn man genau wissen will, was da drin steht, braucht man entweder ``print repr(data)`` oder einen Hexeditor.

Du öffnest die Datei als Textdatei. Wenn die Daten die Du runterlädtst schon Zeilenenden im Windowsformat haben ('\r\n'), wird beim schreiben in eine Textdatei unter Windows aus dem '\n' nochmal ein '\r\n', also steht in der Datei letztendlich '\r\r\n' am Ende jeder Zeile. Oder '0D 0D 0A' für den Hexeditor.

Verschwindet das Problem wenn Du die Datei im Binärmodus ('wb') öffnest?

Ach ja nochwas: Ich suche sowas wie str_replace() aus PHP. Aus irgend einem Grund krieg ich als Download-URL sowas wie

http://meine.bank.de/download.php?file= ... 8ff8daaa...


Der Grund wird wohl sein, dass das gültiges HTML ist. Du kannst natürlich die `replace()`-Methode von Zeichenketten benutzen, aber sauberer wäre es wohl, `BeautifulSoup` zu sagen das HTML-Entities ersetzt werden sollen:

Code: Alles auswählen

    soup = BeautifulSoup(source,
                         convertEntities=BeautifulSoup.HTML_ENTITIES)
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Re: Hier der Code...

Beitragvon droptix » Dienstag 2. Januar 2007, 13:57

BlackJack hat geschrieben:Verschwindet das Problem wenn Du die Datei im Binärmodus ('wb') öffnest?


Jepp, danke!

BlackJack hat geschrieben:aber sauberer wäre es wohl, `BeautifulSoup` zu sagen das HTML-Entities ersetzt werden sollen:

Code: Alles auswählen

    soup = BeautifulSoup(source,
                         convertEntities=BeautifulSoup.HTML_ENTITIES)


Klappt nicht, also url.replace("&", "&"). Funzt!

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder