#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
"""
Dieses Skript implementiert so etwas wie einen kleinen Proxy-Server
ohne Proxy. Er übersetzt Seiten mit einen externen Filter und
biegt zusätzlich URLs so um, dass sie -- bei Bildern -- auf den
Originalserver verweisen oder -- bei Links -- wieder zurück auf
dieses Skript.
"""
import re
import timeoutsocket
import common
import urllib
import urllib2
import urlparse
import operator
import os
# Die Filter sind externe Programme. Als Schlüssel dienen hier die
# Schlüsselwörter, die das Skript aus externen Quellen nimmt
langFilters = {
'schwob_url': 'webschwob',
'sax_url': 'websax',
'frank_url': 'frankifier',
}
# Dieses Dictionary wird von checkPermission verwendet. Ist
# der Host der Zielurl oder ein Teil davon in diesem Dicitionary
# aufgeführt, wird die Übersetzung verweigert
spinner = {
"www.test.bla": 1,
}
noTranslateExtensions = {
".gif": 1,
".jpg": 1,
".pdf": 1,
".ps": 1,
".pdb": 1,
".jpeg": 1,
".mpg": 1,
}
# Ich brauche dringend einen timeout für die http-Verbindungen,
# und signal.alarm bringt den apache durcheinander, jedenfalls,
# wenn das hier unter mod_python lebt. Unter moderneren
# Python-Versionen gibts sowas eingebaut, aber darauf möchte ich
# mich nicht verlassen.
timeoutsocket.setDefaultSocketTimeout(30)
def checkPermission(targURL):
"""Prüft, ob für targURL eine Übersetzung vorgenommen werden soll/darf.
Wir prüfen im Augenblick nur den Host Part. Vgl. auch das spinner-dict
oben.
"""
try:
hostname = re.sub(":.*", "", urlparse.urlparse(targURL)[1])
canonHost = hostname.lower()
if spinner.has_key(canonHost) or reduce(operator.__or__,
map(lambda a, d=spinner: d.has_key(a), canonHost.split("."))):
return 1
except IndexError:
pass
def workAroundUrllibBug(url):
"""Umgeht einen Fehler in der urllib, der eine korrekte Interpretation
von URLs wie http://host?bla verhindert. Es gibt einen Bug Report
dazu, aber offenbar hat niemand Lust, das an der Quelle in Ordnung
zu bringen.
"""
if not '?' in url:
return url
pre, post = url.split("?", 1)
if "/" in pre[7:]:
return url
return pre+"/?"+post
import urllib2
class SchwobErrHandler(urllib2.BaseHandler):
"""Diese Klasse liefert etwas präzisere Fehler als der Orinialhandler
der urllib2.
"""
def http_error_401(self, req, fp, code, msg, hdrs):
raise common.Error("Die Seite %s ist passwortgeschützt. Der"
" USP übersetzt so etwas nicht.")%req.get_full_url()
def http_error_403(self, req, fp, code, msg, hdrs):
raise common.Error("Die Seite %s ist für mich verboten. Könnte sein,"
" dass die Seitenbetreiber kein Schwäbisch mögen.")%(
req.get_full_url())
def http_error_404(self, req, fp, code, msg, hdrs):
raise common.Error("Für die Seite %s habe ich ein 404 bekommen."
" Das bedeutet meistens, dass es die Seite nicht mehr gibt,"
" es kann aber auch sein, dass ich was falsch verstanden habe."
" So oder so, die Seite gibt es so nicht.")%(
req.get_full_url())
class SchwobOpenerDirector(urllib2.OpenerDirector):
"""Hauptziel hier ist ein korrekter User-agent-String. Darüber
hinaus unterdrücke ich hier auch Handler für file, ftp und
Freunde, mit denen irgendwer Unfug machen könnte.
"""
def __init__(self):
urllib2.OpenerDirector.__init__(self)
self.addheaders = [('User-agent', "Schwobifying Proxy, %s"%
common.proxyHomeURL)]
for handler in [urllib2.HTTPHandler(), SchwobErrHandler(),
urllib2.HTTPDefaultErrorHandler(), urllib2.HTTPRedirectHandler(),
urllib2.UnknownHandler()]:
self.add_handler(handler)
urllib2.install_opener(SchwobOpenerDirector())
def urljoin(url1, url2):
"""Noch ein Versuch, url1 und url2 sinnvoll zu verbinden -- was urljoin
tut, hat mir nicht immer gepasst. Aber zugegeben, das hier ist
noch größerer Unfug. Dafür funktioniert es meistens, auch in der
Gegenwart von index.htmls.
"""
u1 = urlparse.urlparse(url1)
u2 = urlparse.urlparse(url2)
if u2[0]:
# url2 hat scheme und ist also offentichtlich komplett
return url2
if u2[1]:
# urlparse deklariert alles ohne einen / davor als host, was
# wenigstens hier Quatsch ist
u2 = ('','',os.path.join(u2[1],u2[2]),u2[3],u2[4],u2[5])
# die zweite URL könnte ein Pfad sein -- oder nur ein Tag
if not u2[2] and u2[5]:
return urlparse.urlunparse((u1[0],u1[1],u1[2],u1[3],u1[4],u2[5]))
if not u2[2]:
u2 = (u2[0],u2[1],"/",u2[3],u2[4],u2[5])
newpath = os.path.join(os.path.dirname(u1[2]),u2[2])
return urlparse.urlunparse((u1[0],u1[1],re.sub("^/","",newpath),
u2[3],u2[4],u2[5]))
def raiseTimeout(signum, stackframe):
raise common.TimeoutError, ("Die Maschine am anderen Ende hat sich"
" nicht rechtzeitig gemeldet")
class Document:
"""Diese Klasse verkapselt ein Dokument samt seines Übersetzungsprozesses.
Sie wurde etwas roh aus ihrer Heimat -- dem UNiMUT -- gerissen und ist
deshalb vielleicht etwas weniger klar als man wünschen könnte...
Darüber hinaus operieren wir vorne und hinten mit regulären Ausdrücken
auf HTML. Es wäre weit besser, etwas wie BeautifulSoup zu verwenden
-- aber das überlassen wir anderen Leuten.
"""
def __init__(self, request):
"""request ist ein Request-Objekt (vgl. unten).
"""
self.request = request
self.proxyURL = common.proxyHomeURL
for processor in langFilters.keys():
if request.form.has_key(processor):
self.processor = processor
break
else:
raise common.Error("Interner Fehler. Yikes.")
self.showFooter = 1
if request.form.has_key("nofooter"):
self.showFooter = 0
if type(request.form[self.processor])==type([]):
raise common.Error("""Bitte den schwob_url-Parameter
nur einmal angeben. Der wahrscheinlichste Grund dieses
Problems ist die Verwendung des übersetzten schwob-URL-Formulars.
Bitte das
unübersetzte Formular
verwenden."""%self.proxyURL)
self.sourceURL = request.form[self.processor].value
if self.sourceURL.find("//")==-1:
self.sourceURL = "http://"+self.sourceURL
self._getDoc()
self._doMeta()
mat = re.search(r'''(?i)]+)''', self.data)
if mat:
self.sourceURL = mat.group(1)
self.data = re.sub("(?is)", "", self.data)
if not self.isData:
self._fixurls()
self._fixBody()
self._filterThroughWebschwob()
def _filterThroughWebschwob(self):
"""besorgt das eigentliche Filtern durch den externen Prozess.
Wir machen uns das Leben hier leicht und filtern in eine
temporäre Datei, die wir dann am Stück lesen können. Das
erspart uns eine bidirektionale Pipe, die immer unerfreulich
ist.
Etwas trickreich ist hier, dass wir komplette Teile nicht
filtern wollen. Dazu gehören script- und style-Bereiche. Es
wäre denkbar, das den externen Programmen zu überlassen, aber
hier tun wir das selbst. Dabei wird einfach jeweils etwas wie
eine entity in den zu übersetzenden Text geschrieben und diese
Entities nachher wieder ersetzt.
"""
import tempfile
fn = tempfile.mktemp()
noProcessChunks = {}
processChunks = []
noProcessOpeners = re.compile(
"""<(script|style|nomangle)""", re.I)
noProcessClosers = re.compile("""(script|style|nomangle)>""", re.I)
pos = 0
while 1:
matStart = noProcessOpeners.search(self.data, pos)
if not matStart:
processChunks.append(self.data[pos:])
break
matEnd = noProcessClosers.search(self.data, matStart.end())
if not matEnd:
processChunks.append(self.data[pos:])
break
processChunks.append(self.data[pos:matStart.start()])
label = "&schwobachunk%d;"%pos
processChunks.append(label)
noProcessChunks[label] = self.data[matStart.start():matEnd.end()]
pos = matEnd.end()
try:
open(fn, "w").write("\n".join(processChunks))
self.data = os.popen(os.path.join(common.physPath,
langFilters[self.processor])+" < %s"%fn).read()
for label in noProcessChunks.keys():
self.data = self.data.replace(label, noProcessChunks[label])
finally:
try:
os.unlink(fn)
except os.error:
pass
def _doMeta(self):
"""interpretiert die Metas, die NutzerInnen in Seiten einbauen
können, und setzt ggf. Attribute dementsprechend.
"""
mat = re.search("""(?is)]*?)>""",
self.data)
if not mat:
return
if mat.group(1).find("nofooter")!=-1:
self.showFooter = 0
if mat.group(1).find("notranslate")!=-1:
self._raiseForbidError()
def _mangleURL(self, matchobj):
"""wird als callback von re.sub aus aufgerufen. matchobj braucht
mindestens drei Gruppen: den Teiltag vor der URL, die URL selbst und
den Teiltag nach der URL. Zurückgegeben wird der komplette Tag mit
der umgebogenen URL; mangleURL sorgt dafür, dass auch das Ziel des
Links durch den Schobifier gejagt wird.
"""
if matchobj.group(2).find("schwobnomangle")!=-1:
return matchobj.group(0)
if matchobj.group(2).startswith("mailto:"):
return matchobj.group(0)
url = matchobj.group(2)
if noTranslateExtensions.has_key(os.path.splitext(url)[1]):
return matchobj.group(0)
else:
newTag = matchobj.group(1)+self.proxyURL+("/schwob?%s="%self.processor
)+urllib.quote_plus(urljoin(
self.sourceURL, url)).replace("%23", "#")+matchobj.group(3)
return newTag
def _mangleActionURL(self, matchobj):
"""biegt eine Action-URL aus einem Form um. Idee ist
hier, einerseits die Zielurl des Forms auf uns weisen
zu lassen, andererseits die wirkliche Zielurl in einem
zusätzlichen (versteckten) Parameter des Formulars
unterzubringen. Mailto geht natürlich aus technischen
Gründen nicht, POST wollen wir nicht, weil (a) die
gegenwärtige Implementation mit sowas nicht umgehen
kann und (b) wir nicht wirklich ein offener Proxy für
jeden denkbaren Unfug sein wollen.
"""
if matchobj.group(2).startswith("mailto:"):
return matchobj.group(0)
elif re.search("(?i)POST", matchobj.group(0)):
return matchobj.group(0)
else:
return matchobj.group(1)+self.proxyURL+"/schwob"+matchobj.group(3)+\
(''%urljoin(self.sourceURL, matchobj.group(2))
def _absolutifyURL(self, matchobj):
"""biegt eine URL so um, dass sie absolut auf den Ursprungsserver
zeigt (für Bilder, Javascript und ähnliches).
"""
return matchobj.group(1)+urljoin(self.sourceURL,
matchobj.group(2))+matchobj.group(3)
def _hackJavascript(self, matchobj):
"""Ein verzweifelter Versuch, mit Javascript-Krampf klarzukommen --
soll mit einem Matchobj auf eine ()(bla)()
RE gerufen werden und versucht, URLs innerhalb von Javascript
umzubiegen. Wirklich gut funktioniert das natürlich nicht.
"""
return re.sub(r"""(["'])([^"']+?\.(?:gif|jpe?g))(["'])""",
self._absolutifyURL,
re.sub(r"""(?si)(href\s*=\s*["'])([^"'(]+)(["'])""",
self._mangleURL,
re.sub(r"""(?si)(src\s*=\s*["'])([^"']+)(["'])""",
self._absolutifyURL, matchobj.group())))
def _fixurls(self):
"""biegt die URLs in data auf den Proxy um bzw. wandelt sie in
absolute um, weil die Dokumentenbasis anders wird.
"""
self.data = \
re.sub("(?i)",
""%self.sourceURL,
re.sub("\x254", "",
re.sub(r"(?si)(", "\x254",
re.sub(
r"""(background-image:\s*url\()([^)]+)(\))""",
self._absolutifyURL,
re.sub(
r"""(?si)(onmouse(?:over|out|up|down)\s*=\s*["'][^>]*?\.src="""
r"""["'])([^"']+)(["'])""",
self._absolutifyURL,
re.sub(
r"""(?si)(]*?href\s*=\s*["']?)([^"' >]+?)"""
"""(["' >][^>]*>)""",
self._absolutifyURL,
re.sub(
r"""(?si)(<(?:body|td|table)\s[^>]*(?:backg?round|td)\s*=\s*["']?)"""
r"""([^"' >]+?)(["' >][^>]*>)""",
self._absolutifyURL,
re.sub(
r"""(?si)(