]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
revert previous commit to fix attribution (vanity)
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1 import re, time
2 from twisted.application import service, strports, internet
3 from twisted.web import http
4 from twisted.internet import defer
5 from nevow import appserver, inevow, static
6 from allmydata.util import log, fileutil
7
8 from allmydata.web import introweb, root
9 from allmydata.web.common import IOpHandleTable, MyExceptionHandler
10
11 # we must override twisted.web.http.Request.requestReceived with a version
12 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
13 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
14 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
15 # that modifies the way form arguments are parsed. Note that this sort of
16 # surgery may induce a dependency upon a particular version of twisted.web
17
18 parse_qs = http.parse_qs
19 class MyRequest(appserver.NevowRequest):
20     fields = None
21     _tahoe_request_had_error = None
22
23     def requestReceived(self, command, path, version):
24         """Called by channel when all data has been received.
25
26         This method is not intended for users.
27         """
28         self.content.seek(0,0)
29         self.args = {}
30         self.stack = []
31
32         self.method, self.uri = command, path
33         self.clientproto = version
34         x = self.uri.split('?', 1)
35
36         if len(x) == 1:
37             self.path = self.uri
38         else:
39             self.path, argstring = x
40             self.args = parse_qs(argstring, 1)
41
42         # cache the client and server information, we'll need this later to be
43         # serialized and sent with the request so CGIs will work remotely
44         self.client = self.channel.transport.getPeer()
45         self.host = self.channel.transport.getHost()
46
47         # Argument processing.
48
49 ##      The original twisted.web.http.Request.requestReceived code parsed the
50 ##      content and added the form fields it found there to self.args . It
51 ##      did this with cgi.parse_multipart, which holds the arguments in RAM
52 ##      and is thus unsuitable for large file uploads. The Nevow subclass
53 ##      (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
54 ##      the results in self.fields), which is much more memory-efficient.
55 ##      Since we know we're using Nevow, we can anticipate these arguments
56 ##      appearing in self.fields instead of self.args, and thus skip the
57 ##      parse-content-into-self.args step.
58
59 ##      args = self.args
60 ##      ctype = self.getHeader('content-type')
61 ##      if self.method == "POST" and ctype:
62 ##          mfd = 'multipart/form-data'
63 ##          key, pdict = cgi.parse_header(ctype)
64 ##          if key == 'application/x-www-form-urlencoded':
65 ##              args.update(parse_qs(self.content.read(), 1))
66 ##          elif key == mfd:
67 ##              try:
68 ##                  args.update(cgi.parse_multipart(self.content, pdict))
69 ##              except KeyError, e:
70 ##                  if e.args[0] == 'content-disposition':
71 ##                      # Parse_multipart can't cope with missing
72 ##                      # content-dispostion headers in multipart/form-data
73 ##                      # parts, so we catch the exception and tell the client
74 ##                      # it was a bad request.
75 ##                      self.channel.transport.write(
76 ##                              "HTTP/1.1 400 Bad Request\r\n\r\n")
77 ##                      self.channel.transport.loseConnection()
78 ##                      return
79 ##                  raise
80         self.processing_started_timestamp = time.time()
81         self.process()
82
83     def _logger(self):
84         # we build up a log string that hides most of the cap, to preserve
85         # user privacy. We retain the query args so we can identify things
86         # like t=json. Then we send it to the flog. We make no attempt to
87         # match apache formatting. TODO: when we move to DSA dirnodes and
88         # shorter caps, consider exposing a few characters of the cap, or
89         # maybe a few characters of its hash.
90         x = self.uri.split("?", 1)
91         if len(x) == 1:
92             # no query args
93             path = self.uri
94             queryargs = ""
95         else:
96             path, queryargs = x
97             # there is a form handler which redirects POST /uri?uri=FOO into
98             # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
99             # sure we censor these too.
100             if queryargs.startswith("uri="):
101                 queryargs = "[uri=CENSORED]"
102             queryargs = "?" + queryargs
103         if path.startswith("/uri"):
104             path = "/uri/[CENSORED].."
105         elif path.startswith("/file"):
106             path = "/file/[CENSORED].."
107         elif path.startswith("/named"):
108             path = "/named/[CENSORED].."
109
110         uri = path + queryargs
111
112         error = ""
113         if self._tahoe_request_had_error:
114             error = " [ERROR]"
115
116         log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s%(error)s",
117                 clientip=self.getClientIP(),
118                 method=self.method,
119                 uri=uri,
120                 code=self.code,
121                 length=(self.sentLength or "-"),
122                 error=error,
123                 facility="tahoe.webish",
124                 level=log.OPERATIONAL,
125                 )
126
127
128 class WebishServer(service.MultiService):
129     name = "webish"
130
131     def __init__(self, client, webport, nodeurl_path=None, staticdir=None,
132                  clock=None):
133         service.MultiService.__init__(self)
134         # the 'data' argument to all render() methods default to the Client
135         # the 'clock' argument to root.Root is, if set, a
136         # twisted.internet.task.Clock that is provided by the unit tests
137         # so that they can test features that involve the passage of
138         # time in a deterministic manner.
139         self.root = root.Root(client, clock)
140         self.buildServer(webport, nodeurl_path, staticdir)
141         if self.root.child_operations:
142             self.site.remember(self.root.child_operations, IOpHandleTable)
143             self.root.child_operations.setServiceParent(self)
144
145     def buildServer(self, webport, nodeurl_path, staticdir):
146         self.webport = webport
147         self.site = site = appserver.NevowSite(self.root)
148         self.site.requestFactory = MyRequest
149         self.site.remember(MyExceptionHandler(), inevow.ICanHandleException)
150         self.staticdir = staticdir # so tests can check
151         if staticdir:
152             self.root.putChild("static", static.File(staticdir))
153         if re.search(r'^\d', webport):
154             webport = "tcp:"+webport # twisted warns about bare "0" or "3456"
155         s = strports.service(webport, site)
156         s.setServiceParent(self)
157
158         self._scheme = None
159         self._portnum = None
160         self._url = None
161         self._listener = s # stash it so we can query for the portnum
162
163         self._started = defer.Deferred()
164         if nodeurl_path:
165             def _write_nodeurl_file(ign):
166                 # this file will be created with default permissions
167                 line = self.getURL() + "\n"
168                 fileutil.write_atomically(nodeurl_path, line, mode="")
169             self._started.addCallback(_write_nodeurl_file)
170
171     def getURL(self):
172         assert self._url
173         return self._url
174
175     def getPortnum(self):
176         assert self._portnum
177         return self._portnum
178
179     def startService(self):
180         def _got_port(lp):
181             self._portnum = lp.getHost().port
182             # what is our webport?
183             assert self._scheme
184             self._url = "%s://127.0.0.1:%d/" % (self._scheme, self._portnum)
185             self._started.callback(None)
186             return lp
187         def _fail(f):
188             self._started.errback(f)
189             return f
190
191         service.MultiService.startService(self)
192         s = self._listener
193         if hasattr(s, 'endpoint') and hasattr(s, '_waitingForPort'):
194             # Twisted 10.2 gives us a StreamServerEndpointService. This is
195             # ugly but should do for now.
196             classname = s.endpoint.__class__.__name__
197             if classname.startswith('SSL'):
198                 self._scheme = 'https'
199             else:
200                 self._scheme = 'http'
201             s._waitingForPort.addCallbacks(_got_port, _fail)
202         elif isinstance(s, internet.TCPServer):
203             # Twisted <= 10.1
204             self._scheme = 'http'
205             _got_port(s._port)
206         elif isinstance(s, internet.SSLServer):
207             # Twisted <= 10.1
208             self._scheme = 'https'
209             _got_port(s._port)
210         else:
211             # who knows, probably some weirdo future version of Twisted
212             self._started.errback(AssertionError("couldn't find out the scheme or port for the web-API server"))
213
214
215 class IntroducerWebishServer(WebishServer):
216     def __init__(self, introducer, webport, nodeurl_path=None, staticdir=None):
217         service.MultiService.__init__(self)
218         self.root = introweb.IntroducerRoot(introducer)
219         self.buildServer(webport, nodeurl_path, staticdir)
220