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
8 from allmydata.web import introweb, root
9 from allmydata.web.common import IOpHandleTable, MyExceptionHandler
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
18 parse_qs = http.parse_qs
19 class MyRequest(appserver.NevowRequest):
21 _tahoe_request_had_error = None
23 def requestReceived(self, command, path, version):
24 """Called by channel when all data has been received.
26 This method is not intended for users.
28 self.content.seek(0,0)
32 self.method, self.uri = command, path
33 self.clientproto = version
34 x = self.uri.split('?', 1)
39 self.path, argstring = x
40 self.args = parse_qs(argstring, 1)
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()
47 # Argument processing.
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.
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))
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()
80 self.processing_started_timestamp = time.time()
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)
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].."
110 uri = path + queryargs
113 if self._tahoe_request_had_error:
116 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s%(error)s",
117 clientip=self.getClientIP(),
121 length=(self.sentLength or "-"),
123 facility="tahoe.webish",
124 level=log.OPERATIONAL,
128 class WebishServer(service.MultiService):
131 def __init__(self, client, webport, nodeurl_path=None, staticdir=None,
132 clock=None, now=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, now)
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)
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)
151 self.root.putChild("static", static.File(staticdir))
152 if re.search(r'^\d', webport):
153 webport = "tcp:"+webport # twisted warns about bare "0" or "3456"
154 s = strports.service(webport, site)
155 s.setServiceParent(self)
160 self._listener = s # stash it so we can query for the portnum
162 self._started = defer.Deferred()
164 def _write_nodeurl_file(ign):
165 # this file will be created with default permissions
166 line = self.getURL() + "\n"
167 fileutil.write_atomically(nodeurl_path, line, mode="")
168 self._started.addCallback(_write_nodeurl_file)
174 def getPortnum(self):
178 def startService(self):
180 self._portnum = lp.getHost().port
181 # what is our webport?
183 self._url = "%s://127.0.0.1:%d/" % (self._scheme, self._portnum)
184 self._started.callback(None)
187 self._started.errback(f)
190 service.MultiService.startService(self)
192 if hasattr(s, 'endpoint') and hasattr(s, '_waitingForPort'):
193 # Twisted 10.2 gives us a StreamServerEndpointService. This is
194 # ugly but should do for now.
195 classname = s.endpoint.__class__.__name__
196 if classname.startswith('SSL'):
197 self._scheme = 'https'
199 self._scheme = 'http'
200 s._waitingForPort.addCallbacks(_got_port, _fail)
201 elif isinstance(s, internet.TCPServer):
203 self._scheme = 'http'
205 elif isinstance(s, internet.SSLServer):
207 self._scheme = 'https'
210 # who knows, probably some weirdo future version of Twisted
211 self._started.errback(AssertionError("couldn't find out the scheme or port for the web-API server"))
214 class IntroducerWebishServer(WebishServer):
215 def __init__(self, introducer, webport, nodeurl_path=None, staticdir=None):
216 service.MultiService.__init__(self)
217 self.root = introweb.IntroducerRoot(introducer)
218 self.buildServer(webport, nodeurl_path, staticdir)