3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.internet import defer, address
6 from twisted.internet.interfaces import IConsumer
7 from nevow import inevow, rend, appserver, url, tags as T
8 from nevow.static import File as nevow_File # TODO: merge with static.File?
9 from allmydata.util import fileutil, idlib, log
11 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
13 import allmydata # to display import path
14 from allmydata import download
15 from allmydata.upload import FileHandle, FileName
16 from allmydata import provisioning
17 from allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
20 from formless import webform
21 from foolscap.eventual import fireEventually
23 from nevow.util import resource_filename
25 from allmydata.web import status, unlinked, introweb
26 from allmydata.web.common import IClient, getxmlfile, get_arg, \
27 boolean_of_arg, abbreviate_size
29 class ILocalAccess(Interface):
30 def local_access_is_allowed():
31 """Return True if t=upload&localdir= is allowed, giving anyone who
32 can talk to the webserver control over the local (disk) filesystem."""
34 # we must override twisted.web.http.Request.requestReceived with a version
35 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
36 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
37 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
38 # that modifies the way form arguments are parsed. Note that this sort of
39 # surgery may induce a dependency upon a particular version of twisted.web
41 parse_qs = http.parse_qs
42 class MyRequest(appserver.NevowRequest):
44 def requestReceived(self, command, path, version):
45 """Called by channel when all data has been received.
47 This method is not intended for users.
49 self.content.seek(0,0)
53 self.method, self.uri = command, path
54 self.clientproto = version
55 x = self.uri.split('?', 1)
60 self.path, argstring = x
61 self.args = parse_qs(argstring, 1)
63 # cache the client and server information, we'll need this later to be
64 # serialized and sent with the request so CGIs will work remotely
65 self.client = self.channel.transport.getPeer()
66 self.host = self.channel.transport.getHost()
68 # Argument processing.
70 ## The original twisted.web.http.Request.requestReceived code parsed the
71 ## content and added the form fields it found there to self.args . It
72 ## did this with cgi.parse_multipart, which holds the arguments in RAM
73 ## and is thus unsuitable for large file uploads. The Nevow subclass
74 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
75 ## the results in self.fields), which is much more memory-efficient.
76 ## Since we know we're using Nevow, we can anticipate these arguments
77 ## appearing in self.fields instead of self.args, and thus skip the
78 ## parse-content-into-self.args step.
81 ## ctype = self.getHeader('content-type')
82 ## if self.method == "POST" and ctype:
83 ## mfd = 'multipart/form-data'
84 ## key, pdict = cgi.parse_header(ctype)
85 ## if key == 'application/x-www-form-urlencoded':
86 ## args.update(parse_qs(self.content.read(), 1))
89 ## args.update(cgi.parse_multipart(self.content, pdict))
90 ## except KeyError, e:
91 ## if e.args[0] == 'content-disposition':
92 ## # Parse_multipart can't cope with missing
93 ## # content-dispostion headers in multipart/form-data
94 ## # parts, so we catch the exception and tell the client
95 ## # it was a bad request.
96 ## self.channel.transport.write(
97 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
98 ## self.channel.transport.loseConnection()
105 # we build up a log string that hides most of the cap, to preserve
106 # user privacy. We retain the query args so we can identify things
107 # like t=json. Then we send it to the flog. We make no attempt to
108 # match apache formatting. TODO: when we move to DSA dirnodes and
109 # shorter caps, consider exposing a few characters of the cap, or
110 # maybe a few characters of its hash.
111 x = self.uri.split("?", 1)
118 # there is a form handler which redirects POST /uri?uri=FOO into
119 # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
120 # sure we censor these too.
121 if queryargs.startswith("uri="):
122 queryargs = "[uri=CENSORED]"
123 queryargs = "?" + queryargs
124 if path.startswith("/uri"):
125 path = "/uri/[CENSORED].."
126 uri = path + queryargs
128 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
129 clientip=self.getClientIP(),
133 length=(self.sentLength or "-"),
134 facility="tahoe.webish",
135 level=log.OPERATIONAL,
138 class Directory(rend.Page):
140 docFactory = getxmlfile("directory.xhtml")
142 def __init__(self, rootname, dirnode, dirpath):
143 self._rootname = rootname
144 self._dirnode = dirnode
145 self._dirpath = dirpath
147 def dirpath_as_string(self):
148 return "/" + "/".join(self._dirpath)
150 def render_title(self, ctx, data):
151 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
153 def render_header(self, ctx, data):
154 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
155 num_dirs = len(parent_directories)
157 header = ["Directory '"]
158 for i,d in enumerate(parent_directories):
159 upness = num_dirs - i - 1
161 link = "/".join( ("..",) * upness )
164 header.append(T.a(href=link)[d])
169 if self._dirnode.is_readonly():
170 header.append(" (readonly)")
172 return ctx.tag[header]
174 def render_welcome(self, ctx, data):
175 depth = len(self._dirpath) + 2
176 link = "/".join([".."] * depth)
177 return T.div[T.a(href=link)["Return to Welcome page"]]
179 def data_children(self, ctx, data):
180 d = self._dirnode.list()
181 d.addCallback(lambda dict: sorted(dict.items()))
182 def _stall_some(items):
183 # Deferreds don't optimize out tail recursion, and the way
184 # Nevow's flattener handles Deferreds doesn't take this into
185 # account. As a result, large lists of Deferreds that fire in the
186 # same turn (i.e. the output of defer.succeed) will cause a stack
187 # overflow. To work around this, we insert a turn break after
188 # every 100 items, using foolscap's fireEventually(). This gives
189 # the stack a chance to be popped. It would also work to put
190 # every item in its own turn, but that'd be a lot more
191 # inefficient. This addresses ticket #237, for which I was never
192 # able to create a failing unit test.
194 for i,item in enumerate(items):
196 output.append(fireEventually(item))
200 d.addCallback(_stall_some)
203 def render_row(self, ctx, data):
204 name, (target, metadata) = data
205 name = name.encode("utf-8")
206 assert not isinstance(name, unicode)
208 if self._dirnode.is_readonly():
212 # this creates a button which will cause our child__delete method
213 # to be invoked, which deletes the file and then redirects the
214 # browser back to this directory
215 delete = T.form(action=url.here, method="post")[
216 T.input(type='hidden', name='t', value='delete'),
217 T.input(type='hidden', name='name', value=name),
218 T.input(type='hidden', name='when_done', value=url.here),
219 T.input(type='submit', value='del', name="del"),
222 rename = T.form(action=url.here, method="get")[
223 T.input(type='hidden', name='t', value='rename-form'),
224 T.input(type='hidden', name='name', value=name),
225 T.input(type='hidden', name='when_done', value=url.here),
226 T.input(type='submit', value='rename', name="rename"),
229 ctx.fillSlots("delete", delete)
230 ctx.fillSlots("rename", rename)
231 check = T.form(action=url.here, method="post")[
232 T.input(type='hidden', name='t', value='check'),
233 T.input(type='hidden', name='name', value=name),
234 T.input(type='hidden', name='when_done', value=url.here),
235 T.input(type='submit', value='check', name="check"),
237 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
238 ctx.fillSlots("check", check)
241 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
242 if "ctime" in metadata:
243 ctime = time.strftime(TIME_FORMAT,
244 time.localtime(metadata["ctime"]))
245 times.append("c: " + ctime)
246 if "mtime" in metadata:
247 mtime = time.strftime(TIME_FORMAT,
248 time.localtime(metadata["mtime"]))
251 times.append("m: " + mtime)
252 ctx.fillSlots("times", times)
255 # build the base of the uri_link link url
256 uri_link = "/uri/" + urllib.quote(target.get_uri())
258 assert (IFileNode.providedBy(target)
259 or IDirectoryNode.providedBy(target)
260 or IMutableFileNode.providedBy(target)), target
262 if IMutableFileNode.providedBy(target):
265 # add the filename to the uri_link url
266 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
268 # to prevent javascript in displayed .html files from stealing a
269 # secret directory URI from the URL, send the browser to a URI-based
270 # page that doesn't know about the directory at all
271 #dlurl = urllib.quote(name)
274 ctx.fillSlots("filename",
275 T.a(href=dlurl)[html.escape(name)])
276 ctx.fillSlots("type", "SSK")
278 ctx.fillSlots("size", "?")
280 text_plain_link = uri_link + "?filename=foo.txt"
281 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
283 elif IFileNode.providedBy(target):
286 # add the filename to the uri_link url
287 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
289 # to prevent javascript in displayed .html files from stealing a
290 # secret directory URI from the URL, send the browser to a URI-based
291 # page that doesn't know about the directory at all
292 #dlurl = urllib.quote(name)
295 ctx.fillSlots("filename",
296 T.a(href=dlurl)[html.escape(name)])
297 ctx.fillSlots("type", "FILE")
299 ctx.fillSlots("size", target.get_size())
301 text_plain_link = uri_link + "?filename=foo.txt"
302 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
304 elif IDirectoryNode.providedBy(target):
306 ctx.fillSlots("filename",
307 T.a(href=uri_link)[html.escape(name)])
308 if target.is_readonly():
312 ctx.fillSlots("type", dirtype)
313 ctx.fillSlots("size", "-")
314 text_plain_tag = None
316 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
317 T.a(href="%s?t=uri" % name)["URI"], ", ",
318 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
321 childdata.extend([", ", text_plain_tag])
323 ctx.fillSlots("data", childdata)
326 checker = IClient(ctx).getServiceNamed("checker")
330 d = defer.maybeDeferred(checker.checker_results_for,
331 target.get_verifier())
332 def _got(checker_results):
333 recent_results = reversed(checker_results[-5:])
334 if IFileNode.providedBy(target):
336 ", ".join(["%d/%d" % (found, needed)
338 (needed, total, found, sharemap))
339 in recent_results]) +
341 elif IDirectoryNode.providedBy(target):
343 "".join([{True:"+",False:"-"}[res]
344 for (when, res) in recent_results]) +
347 results = "%d results" % len(checker_results)
353 # TODO: include a link to see more results, including timestamps
354 # TODO: use a sparkline
355 ctx.fillSlots("checker_results", results)
359 def render_forms(self, ctx, data):
360 if self._dirnode.is_readonly():
361 return T.div["No upload forms: directory is read-only"]
362 mkdir = T.form(action=".", method="post",
363 enctype="multipart/form-data")[
365 T.input(type="hidden", name="t", value="mkdir"),
366 T.input(type="hidden", name="when_done", value=url.here),
367 T.legend(class_="freeform-form-label")["Create a new directory"],
368 "New directory name: ",
369 T.input(type="text", name="name"), " ",
370 T.input(type="submit", value="Create"),
373 upload = T.form(action=".", method="post",
374 enctype="multipart/form-data")[
376 T.input(type="hidden", name="t", value="upload"),
377 T.input(type="hidden", name="when_done", value=url.here),
378 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
379 "Choose a file to upload: ",
380 T.input(type="file", name="file", class_="freeform-input-file"),
382 T.input(type="submit", value="Upload"),
384 T.input(type="checkbox", name="mutable"),
387 mount = T.form(action=".", method="post",
388 enctype="multipart/form-data")[
390 T.input(type="hidden", name="t", value="uri"),
391 T.input(type="hidden", name="when_done", value=url.here),
392 T.legend(class_="freeform-form-label")["Attach a file or directory"
396 T.input(type="text", name="name"), " ",
397 "URI of new child: ",
398 T.input(type="text", name="uri"), " ",
399 T.input(type="submit", value="Attach"),
401 return [T.div(class_="freeform-form")[mkdir],
402 T.div(class_="freeform-form")[upload],
403 T.div(class_="freeform-form")[mount],
406 def build_overwrite(self, ctx, data):
408 if IMutableFileNode.providedBy(target) and not target.is_readonly():
409 action="/uri/" + urllib.quote(target.get_uri())
410 overwrite = T.form(action=action, method="post",
411 enctype="multipart/form-data")[
413 T.input(type="hidden", name="t", value="overwrite"),
414 T.input(type='hidden', name='name', value=name),
415 T.input(type='hidden', name='when_done', value=url.here),
416 T.legend(class_="freeform-form-label")["Overwrite"],
418 T.input(type="file", name="file", class_="freeform-input-file"),
420 T.input(type="submit", value="Overwrite")
422 return [T.div(class_="freeform-form")[overwrite],]
426 def render_results(self, ctx, data):
427 req = inevow.IRequest(ctx)
428 return get_arg(req, "results", "")
430 class WebDownloadTarget:
431 implements(IDownloadTarget, IConsumer)
432 def __init__(self, req, content_type, content_encoding, save_to_file):
434 self._content_type = content_type
435 self._content_encoding = content_encoding
437 self._producer = None
438 self._save_to_file = save_to_file
440 def registerProducer(self, producer, streaming):
441 self._req.registerProducer(producer, streaming)
442 def unregisterProducer(self):
443 self._req.unregisterProducer()
445 def open(self, size):
447 self._req.setHeader("content-type", self._content_type)
448 if self._content_encoding:
449 self._req.setHeader("content-encoding", self._content_encoding)
450 self._req.setHeader("content-length", str(size))
451 if self._save_to_file is not None:
452 # tell the browser to save the file rather display it
453 # TODO: quote save_to_file properly
454 filename = self._save_to_file.encode("utf-8")
455 self._req.setHeader("content-disposition",
456 'attachment; filename="%s"'
459 def write(self, data):
460 self._req.write(data)
466 # The content-type is already set, and the response code
467 # has already been sent, so we can't provide a clean error
468 # indication. We can emit text (which a browser might interpret
469 # as something else), and if we sent a Size header, they might
470 # notice that we've truncated the data. Keep the error message
471 # small to improve the chances of having our error response be
472 # shorter than the intended results.
474 # We don't have a lot of options, unfortunately.
475 self._req.write("problem during download\n")
477 # We haven't written anything yet, so we can provide a sensible
480 msg.replace("\n", "|")
481 self._req.setResponseCode(http.GONE, msg)
482 self._req.setHeader("content-type", "text/plain")
483 # TODO: HTML-formatted exception?
484 self._req.write(str(why))
487 def register_canceller(self, cb):
492 class FileDownloader(resource.Resource):
493 def __init__(self, filenode, name):
494 assert (IFileNode.providedBy(filenode)
495 or IMutableFileNode.providedBy(filenode))
496 self._filenode = filenode
499 def render(self, req):
500 gte = static.getTypeAndEncoding
501 ctype, encoding = gte(self._name,
502 static.File.contentTypes,
503 static.File.contentEncodings,
504 defaultType="text/plain")
506 if get_arg(req, "save", False):
507 # TODO: make the API specification clear: should "save=" or
508 # "save=false" count?
509 save_to_file = self._name
510 wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
511 d = self._filenode.download(wdt)
512 # exceptions during download are handled by the WebDownloadTarget
513 d.addErrback(lambda why: None)
514 return server.NOT_DONE_YET
516 class BlockingFileError(Exception):
517 """We cannot auto-create a parent directory, because there is a file in
519 class NoReplacementError(Exception):
520 """There was already a child by that name, and you asked me to not replace it"""
521 class NoLocalDirectoryError(Exception):
522 """The localdir= directory didn't exist"""
524 LOCALHOST = "127.0.0.1"
526 class NeedLocalhostError:
527 implements(inevow.IResource)
529 def renderHTTP(self, ctx):
530 req = inevow.IRequest(ctx)
531 req.setResponseCode(http.FORBIDDEN)
532 req.setHeader("content-type", "text/plain")
533 return "localfile= or localdir= requires a local connection"
535 class NeedAbsolutePathError:
536 implements(inevow.IResource)
538 def renderHTTP(self, ctx):
539 req = inevow.IRequest(ctx)
540 req.setResponseCode(http.FORBIDDEN)
541 req.setHeader("content-type", "text/plain")
542 return "localfile= or localdir= requires an absolute path"
544 class LocalAccessDisabledError:
545 implements(inevow.IResource)
547 def renderHTTP(self, ctx):
548 req = inevow.IRequest(ctx)
549 req.setResponseCode(http.FORBIDDEN)
550 req.setHeader("content-type", "text/plain")
551 return "local file access is disabled"
554 implements(inevow.IResource)
555 def __init__(self, response_code, errmsg):
556 self._response_code = response_code
557 self._errmsg = errmsg
559 def renderHTTP(self, ctx):
560 req = inevow.IRequest(ctx)
561 req.setResponseCode(self._response_code)
562 req.setHeader("content-type", "text/plain")
566 class LocalFileDownloader(resource.Resource):
567 def __init__(self, filenode, local_filename):
568 self._local_filename = local_filename
570 self._filenode = filenode
572 def render(self, req):
573 target = download.FileName(self._local_filename)
574 d = self._filenode.download(target)
576 req.write(self._filenode.get_uri())
579 return server.NOT_DONE_YET
582 class FileJSONMetadata(rend.Page):
583 def __init__(self, filenode):
584 self._filenode = filenode
586 def renderHTTP(self, ctx):
587 req = inevow.IRequest(ctx)
588 req.setHeader("content-type", "text/plain")
589 return self.renderNode(self._filenode)
591 def renderNode(self, filenode):
592 file_uri = filenode.get_uri()
595 'size': filenode.get_size(),
597 return simplejson.dumps(data, indent=1)
599 class FileURI(FileJSONMetadata):
600 def renderNode(self, filenode):
601 file_uri = filenode.get_uri()
604 class FileReadOnlyURI(FileJSONMetadata):
605 def renderNode(self, filenode):
606 if filenode.is_readonly():
607 return filenode.get_uri()
609 return filenode.get_readonly().get_uri()
611 class DirnodeWalkerMixin:
612 """Visit all nodes underneath (and including) the rootnode, one at a
613 time. For each one, call the visitor. The visitor will see the
614 IDirectoryNode before it sees any of the IFileNodes inside. If the
615 visitor returns a Deferred, I do not call the visitor again until it has
619 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
620 ## # this is what we'd be doing if we didn't have the Deferreds and
621 ## # thus could use generators
622 ## yield rootpath, rootnode
623 ## for childname, childnode in rootnode.list().items():
624 ## childpath = rootpath + (childname,)
625 ## if IFileNode.providedBy(childnode):
626 ## yield childpath, childnode
627 ## elif IDirectoryNode.providedBy(childnode):
628 ## for res in self._walk_if_we_could_use_generators(childnode,
632 def walk(self, rootnode, visitor, rootpath=()):
634 def _listed(listing):
635 return listing.items()
636 d.addCallback(_listed)
637 d.addCallback(self._handle_items, visitor, rootpath)
640 def _handle_items(self, items, visitor, rootpath):
643 childname, (childnode, metadata) = items[0]
644 childpath = rootpath + (childname,)
645 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
646 if IDirectoryNode.providedBy(childnode):
647 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
648 d.addCallback(lambda res:
649 self._handle_items(items[1:], visitor, rootpath))
652 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
653 def __init__(self, dirnode, localdir):
654 self._dirnode = dirnode
655 self._localdir = localdir
657 def _handle(self, path, node, metadata):
658 path = tuple([p.encode("utf-8") for p in path])
659 localfile = os.path.join(self._localdir, os.sep.join(path))
660 if IDirectoryNode.providedBy(node):
661 fileutil.make_dirs(localfile)
662 elif IFileNode.providedBy(node):
663 target = download.FileName(localfile)
664 return node.download(target)
666 def render(self, req):
667 d = self.walk(self._dirnode, self._handle)
669 req.setHeader("content-type", "text/plain")
670 return "operation complete"
674 class DirectoryJSONMetadata(rend.Page):
675 def __init__(self, dirnode):
676 self._dirnode = dirnode
678 def renderHTTP(self, ctx):
679 req = inevow.IRequest(ctx)
680 req.setHeader("content-type", "text/plain")
681 return self.renderNode(self._dirnode)
683 def renderNode(self, node):
687 for name, (childnode, metadata) in children.iteritems():
688 if IFileNode.providedBy(childnode):
689 kiduri = childnode.get_uri()
690 kiddata = ("filenode",
692 'size': childnode.get_size(),
693 'metadata': metadata,
696 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
697 kiddata = ("dirnode",
698 {'ro_uri': childnode.get_readonly_uri(),
699 'metadata': metadata,
701 if not childnode.is_readonly():
702 kiddata[1]['rw_uri'] = childnode.get_uri()
704 contents = { 'children': kids,
705 'ro_uri': node.get_readonly_uri(),
707 if not node.is_readonly():
708 contents['rw_uri'] = node.get_uri()
709 data = ("dirnode", contents)
710 return simplejson.dumps(data, indent=1)
714 class DirectoryURI(DirectoryJSONMetadata):
715 def renderNode(self, node):
716 return node.get_uri()
718 class DirectoryReadonlyURI(DirectoryJSONMetadata):
719 def renderNode(self, node):
720 return node.get_readonly_uri()
722 class RenameForm(rend.Page):
724 docFactory = getxmlfile("rename-form.xhtml")
726 def __init__(self, rootname, dirnode, dirpath):
727 self._rootname = rootname
728 self._dirnode = dirnode
729 self._dirpath = dirpath
731 def dirpath_as_string(self):
732 return "/" + "/".join(self._dirpath)
734 def render_title(self, ctx, data):
735 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
737 def render_header(self, ctx, data):
738 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
739 num_dirs = len(parent_directories)
741 header = [ "Rename in directory '",
742 "<%s>/" % self._rootname,
743 "/".join(self._dirpath),
746 if self._dirnode.is_readonly():
747 header.append(" (readonly)")
748 return ctx.tag[header]
750 def render_when_done(self, ctx, data):
751 return T.input(type="hidden", name="when_done", value=url.here)
753 def render_get_name(self, ctx, data):
754 req = inevow.IRequest(ctx)
755 name = get_arg(req, "name", "")
756 ctx.tag.attributes['value'] = name
759 class POSTHandler(rend.Page):
760 def __init__(self, node, replace):
762 self._replace = replace
764 def _check_replacement(self, name):
766 return defer.succeed(None)
767 d = self._node.has_child(name)
770 raise NoReplacementError("There was already a child by that "
771 "name, and you asked me to not "
777 def _POST_mkdir(self, name):
778 d = self._check_replacement(name)
779 d.addCallback(lambda res: self._node.create_empty_directory(name))
780 d.addCallback(lambda res: "directory created")
783 def _POST_uri(self, name, newuri):
784 d = self._check_replacement(name)
785 d.addCallback(lambda res: self._node.set_uri(name, newuri))
786 d.addCallback(lambda res: newuri)
789 def _POST_delete(self, name):
791 # apparently an <input type="hidden" name="name" value="">
792 # won't show up in the resulting encoded form.. the 'name'
793 # field is completely missing. So to allow deletion of an
794 # empty file, we have to pretend that None means ''. The only
795 # downide of this is a slightly confusing error message if
796 # someone does a POST without a name= field. For our own HTML
797 # thisn't a big deal, because we create the 'delete' POST
800 d = self._node.delete(name)
801 d.addCallback(lambda res: "thing deleted")
804 def _POST_rename(self, name, from_name, to_name):
805 d = self._check_replacement(to_name)
806 d.addCallback(lambda res: self._node.get(from_name))
808 uri = child.get_uri()
809 # now actually do the rename
810 return self._node.set_uri(to_name, uri)
811 d.addCallback(add_dest)
813 return self._node.delete(from_name)
814 d.addCallback(rm_src)
815 d.addCallback(lambda res: "thing renamed")
818 def _POST_upload(self, contents, name, mutable, client):
820 # SDMF: files are small, and we can only upload data.
821 contents.file.seek(0)
822 data = contents.file.read()
823 #uploadable = FileHandle(contents.file)
824 d = self._check_replacement(name)
825 d.addCallback(lambda res: self._node.has_child(name))
826 def _checked(present):
828 # modify the existing one instead of creating a new
830 d2 = self._node.get(name)
831 def _got_newnode(newnode):
832 d3 = newnode.replace(data)
833 d3.addCallback(lambda res: newnode.get_uri())
835 d2.addCallback(_got_newnode)
837 d2 = client.create_mutable_file(data)
838 def _uploaded(newnode):
839 d1 = self._node.set_node(name, newnode)
840 d1.addCallback(lambda res: newnode.get_uri())
842 d2.addCallback(_uploaded)
844 d.addCallback(_checked)
846 uploadable = FileHandle(contents.file)
847 d = self._check_replacement(name)
848 d.addCallback(lambda res: self._node.add_file(name, uploadable))
850 return newnode.get_uri()
854 def _POST_overwrite(self, contents):
855 # SDMF: files are small, and we can only upload data.
856 contents.file.seek(0)
857 data = contents.file.read()
858 # TODO: 'name' handling needs review
859 d = defer.succeed(self._node)
860 def _got_child_overwrite(child_node):
861 child_node.replace(data)
862 return child_node.get_uri()
863 d.addCallback(_got_child_overwrite)
866 def _POST_check(self, name):
867 d = self._node.get(name)
868 def _got_child_check(child_node):
869 d2 = child_node.check()
871 log.msg("checked %s, results %s" % (child_node, res),
872 facility="tahoe.webish", level=log.NOISY)
874 d2.addCallback(_done)
876 d.addCallback(_got_child_check)
879 def _POST_set_children(self, children):
881 for name, (file_or_dir, mddict) in children.iteritems():
882 cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
883 cs.append((name, cap, mddict.get('metadata')))
885 d = self._node.set_children(cs)
886 d.addCallback(lambda res: "Okay so I did it.")
889 def renderHTTP(self, ctx):
890 req = inevow.IRequest(ctx)
892 t = get_arg(req, "t")
895 charset = get_arg(req, "_charset", "utf-8")
897 name = get_arg(req, "name", None)
898 if name and "/" in name:
899 req.setResponseCode(http.BAD_REQUEST)
900 req.setHeader("content-type", "text/plain")
901 return "name= may not contain a slash"
904 name = name.decode(charset)
905 assert isinstance(name, unicode)
906 # we allow the user to delete an empty-named file, but not to create
907 # them, since that's an easy and confusing mistake to make
909 when_done = get_arg(req, "when_done", None)
910 if not boolean_of_arg(get_arg(req, "replace", "true")):
911 self._replace = False
915 raise RuntimeError("mkdir requires a name")
916 d = self._POST_mkdir(name)
919 raise RuntimeError("set-uri requires a name")
920 newuri = get_arg(req, "uri")
921 assert newuri is not None
922 d = self._POST_uri(name, newuri)
924 d = self._POST_delete(name)
926 from_name = get_arg(req, "from_name")
927 if from_name is not None:
928 from_name = from_name.strip()
929 from_name = from_name.decode(charset)
930 assert isinstance(from_name, unicode)
931 to_name = get_arg(req, "to_name")
932 if to_name is not None:
933 to_name = to_name.strip()
934 to_name = to_name.decode(charset)
935 assert isinstance(to_name, unicode)
936 if not from_name or not to_name:
937 raise RuntimeError("rename requires from_name and to_name")
938 if not IDirectoryNode.providedBy(self._node):
939 raise RuntimeError("rename must only be called on directories")
940 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
942 req.setResponseCode(http.BAD_REQUEST)
943 req.setHeader("content-type", "text/plain")
944 return "%s= may not contain a slash" % (k,)
945 d = self._POST_rename(name, from_name, to_name)
947 contents = req.fields["file"]
948 name = name or contents.filename
952 # this prohibts empty, missing, and all-whitespace filenames
953 raise RuntimeError("upload requires a name")
954 name = name.decode(charset)
955 assert isinstance(name, unicode)
956 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
957 d = self._POST_upload(contents, name, mutable, IClient(ctx))
958 elif t == "overwrite":
959 contents = req.fields["file"]
960 d = self._POST_overwrite(contents)
962 d = self._POST_check(name)
963 elif t == "set_children":
965 body = req.content.read()
967 children = simplejson.loads(body)
968 except ValueError, le:
969 le.args = tuple(le.args + (body,))
970 # TODO test handling of bad JSON
972 d = self._POST_set_children(children)
975 return "BAD t=%s" % t
977 d.addCallback(lambda res: url.URL.fromString(when_done))
978 def _check_replacement(f):
979 # TODO: make this more human-friendly: maybe send them to the
980 # when_done page but with an extra query-arg that will display
981 # the error message in a big box at the top of the page. The
982 # directory page that when_done= usually points to accepts a
983 # result= argument.. use that.
984 f.trap(NoReplacementError)
985 req.setResponseCode(http.CONFLICT)
986 req.setHeader("content-type", "text/plain")
988 d.addErrback(_check_replacement)
991 class DELETEHandler(rend.Page):
992 def __init__(self, node, name):
996 def renderHTTP(self, ctx):
997 req = inevow.IRequest(ctx)
998 d = self._node.delete(self._name)
1000 # what should this return??
1001 return "%s deleted" % self._name.encode("utf-8")
1002 d.addCallback(_done)
1003 def _trap_missing(f):
1005 req.setResponseCode(http.NOT_FOUND)
1006 req.setHeader("content-type", "text/plain")
1007 return "no such child %s" % self._name.encode("utf-8")
1008 d.addErrback(_trap_missing)
1011 class PUTHandler(rend.Page):
1012 def __init__(self, node, path, t, localfile, localdir, replace):
1016 self._localfile = localfile
1017 self._localdir = localdir
1018 self._replace = replace
1020 def renderHTTP(self, ctx):
1021 req = inevow.IRequest(ctx)
1023 localfile = self._localfile
1024 localdir = self._localdir
1026 if t == "upload" and not (localfile or localdir):
1027 req.setResponseCode(http.BAD_REQUEST)
1028 req.setHeader("content-type", "text/plain")
1029 return "t=upload requires localfile= or localdir="
1031 # we must traverse the path, creating new directories as necessary
1032 d = self._get_or_create_directories(self._node, self._path[:-1])
1033 name = self._path[-1]
1034 d.addCallback(self._check_replacement, name, self._replace)
1037 d.addCallback(self._upload_localfile, localfile, name)
1040 # take the last step
1041 d.addCallback(self._get_or_create_directories, self._path[-1:])
1042 d.addCallback(self._upload_localdir, localdir)
1044 d.addCallback(self._attach_uri, req.content, name)
1046 d.addCallback(self._mkdir, name)
1048 d.addCallback(self._upload_file, req.content, name)
1050 def _transform_error(f):
1051 errors = {BlockingFileError: http.BAD_REQUEST,
1052 NoReplacementError: http.CONFLICT,
1053 NoLocalDirectoryError: http.BAD_REQUEST,
1055 for k,v in errors.items():
1057 req.setResponseCode(v)
1058 req.setHeader("content-type", "text/plain")
1061 d.addErrback(_transform_error)
1064 def _get_or_create_directories(self, node, path):
1065 if not IDirectoryNode.providedBy(node):
1066 # unfortunately it is too late to provide the name of the
1067 # blocking directory in the error message.
1068 raise BlockingFileError("cannot create directory because there "
1069 "is a file in the way")
1071 return defer.succeed(node)
1072 d = node.get(path[0])
1073 def _maybe_create(f):
1075 return node.create_empty_directory(path[0])
1076 d.addErrback(_maybe_create)
1077 d.addCallback(self._get_or_create_directories, path[1:])
1080 def _check_replacement(self, node, name, replace):
1083 d = node.has_child(name)
1086 raise NoReplacementError("There was already a child by that "
1087 "name, and you asked me to not "
1093 def _mkdir(self, node, name):
1094 d = node.create_empty_directory(name)
1096 return newnode.get_uri()
1097 d.addCallback(_done)
1100 def _upload_file(self, node, contents, name):
1101 uploadable = FileHandle(contents)
1102 d = node.add_file(name, uploadable)
1103 def _done(filenode):
1104 log.msg("webish upload complete",
1105 facility="tahoe.webish", level=log.NOISY)
1106 return filenode.get_uri()
1107 d.addCallback(_done)
1110 def _upload_localfile(self, node, localfile, name):
1111 uploadable = FileName(localfile)
1112 d = node.add_file(name, uploadable)
1113 d.addCallback(lambda filenode: filenode.get_uri())
1116 def _attach_uri(self, parentnode, contents, name):
1117 newuri = contents.read().strip()
1118 d = parentnode.set_uri(name, newuri)
1121 d.addCallback(_done)
1124 def _upload_localdir(self, node, localdir):
1125 # build up a list of files to upload. TODO: for now, these files and
1126 # directories must have UTF-8 encoded filenames: anything else will
1127 # cause the upload to break.
1130 msg = "No files to upload! %s is empty" % localdir
1131 if not os.path.exists(localdir):
1132 msg = "%s doesn't exist!" % localdir
1133 raise NoLocalDirectoryError(msg)
1134 for root, dirs, files in os.walk(localdir):
1135 if root == localdir:
1138 relative_root = root[len(localdir)+1:]
1139 path = tuple(relative_root.split(os.sep))
1141 this_dir = path + (d,)
1142 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1143 all_dirs.append(this_dir)
1145 this_file = path + (f,)
1146 this_file = tuple([p.decode("utf-8") for p in this_file])
1147 all_files.append(this_file)
1148 d = defer.succeed(msg)
1149 for dir in all_dirs:
1151 d.addCallback(self._makedir, node, dir)
1153 d.addCallback(self._upload_one_file, node, localdir, f)
1156 def _makedir(self, res, node, dir):
1157 d = defer.succeed(None)
1158 # get the parent. As long as os.walk gives us parents before
1159 # children, this ought to work
1160 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1161 # then create the child directory
1162 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1165 def _upload_one_file(self, res, node, localdir, f):
1166 # get the parent. We can be sure this exists because we already
1167 # went through and created all the directories we require.
1168 localfile = os.path.join(localdir, *f)
1169 d = node.get_child_at_path(f[:-1])
1170 d.addCallback(self._upload_localfile, localfile, f[-1])
1174 class Manifest(rend.Page):
1175 docFactory = getxmlfile("manifest.xhtml")
1176 def __init__(self, dirnode, dirpath):
1177 self._dirnode = dirnode
1178 self._dirpath = dirpath
1180 def dirpath_as_string(self):
1181 return "/" + "/".join(self._dirpath)
1183 def render_title(self, ctx):
1184 return T.title["Manifest of %s" % self.dirpath_as_string()]
1186 def render_header(self, ctx):
1187 return T.p["Manifest of %s" % self.dirpath_as_string()]
1189 def data_items(self, ctx, data):
1190 return self._dirnode.build_manifest()
1192 def render_row(self, ctx, refresh_cap):
1193 ctx.fillSlots("refresh_capability", refresh_cap)
1197 implements(inevow.IResource)
1198 def renderHTTP(self, ctx):
1199 req = inevow.IRequest(ctx)
1200 req.setResponseCode(http.BAD_REQUEST)
1201 req.setHeader("content-type", "text/plain")
1204 def child_error(text):
1209 class VDrive(rend.Page):
1211 def __init__(self, node, name):
1215 def get_child_at_path(self, path):
1217 return self.node.get_child_at_path(path)
1218 return defer.succeed(self.node)
1220 def locateChild(self, ctx, segments):
1221 req = inevow.IRequest(ctx)
1223 path = tuple([seg.decode("utf-8") for seg in segments])
1225 t = get_arg(req, "t", "")
1226 localfile = get_arg(req, "localfile", None)
1227 if localfile is not None:
1228 if localfile != os.path.abspath(localfile):
1229 return NeedAbsolutePathError(), ()
1230 localdir = get_arg(req, "localdir", None)
1231 if localdir is not None:
1232 if localdir != os.path.abspath(localdir):
1233 return NeedAbsolutePathError(), ()
1234 if localfile or localdir:
1235 if not ILocalAccess(ctx).local_access_is_allowed():
1236 return LocalAccessDisabledError(), ()
1237 if req.getHost().host != LOCALHOST:
1238 return NeedLocalhostError(), ()
1239 # TODO: think about clobbering/revealing config files and node secrets
1241 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1244 # the node must exist, and our operation will be performed on the
1246 d = self.get_child_at_path(path)
1247 def file_or_dir(node):
1248 if (IFileNode.providedBy(node)
1249 or IMutableFileNode.providedBy(node)):
1250 filename = "unknown"
1253 filename = get_arg(req, "filename", filename)
1256 # write contents to a local file
1257 return LocalFileDownloader(node, localfile), ()
1258 # send contents as the result
1259 return FileDownloader(node, filename), ()
1261 # send contents as the result
1262 return FileDownloader(node, filename), ()
1264 return FileJSONMetadata(node), ()
1266 return FileURI(node), ()
1267 elif t == "readonly-uri":
1268 return FileReadOnlyURI(node), ()
1270 return child_error("bad t=%s" % t)
1271 elif IDirectoryNode.providedBy(node):
1274 # recursive download to a local directory
1275 return LocalDirectoryDownloader(node, localdir), ()
1276 return child_error("t=download requires localdir=")
1278 # send an HTML representation of the directory
1279 return Directory(self.name, node, path), ()
1281 return DirectoryJSONMetadata(node), ()
1283 return DirectoryURI(node), ()
1284 elif t == "readonly-uri":
1285 return DirectoryReadonlyURI(node), ()
1286 elif t == "manifest":
1287 return Manifest(node, path), ()
1288 elif t == 'rename-form':
1289 return RenameForm(self.name, node, path), ()
1291 return child_error("bad t=%s" % t)
1293 return child_error("unknown node type")
1294 d.addCallback(file_or_dir)
1295 elif method == "POST":
1296 # the node must exist, and our operation will be performed on the
1298 d = self.get_child_at_path(path)
1299 def _got_POST(node):
1300 return POSTHandler(node, replace), ()
1301 d.addCallback(_got_POST)
1302 elif method == "DELETE":
1303 # the node must exist, and our operation will be performed on its
1305 assert path # you can't delete the root
1306 d = self.get_child_at_path(path[:-1])
1307 def _got_DELETE(node):
1308 return DELETEHandler(node, path[-1]), ()
1309 d.addCallback(_got_DELETE)
1310 elif method in ("PUT",):
1311 # the node may or may not exist, and our operation may involve
1312 # all the ancestors of the node.
1313 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1315 return rend.NotFound
1318 class Root(rend.Page):
1321 docFactory = getxmlfile("welcome.xhtml")
1323 def locateChild(self, ctx, segments):
1324 client = IClient(ctx)
1325 req = inevow.IRequest(ctx)
1327 segments = list(segments) # XXX HELP I AM YUCKY!
1328 while segments and not segments[-1]:
1332 segments = tuple(segments)
1334 if segments[0] == "uri":
1335 if len(segments) == 1 or segments[1] == '':
1336 uri = get_arg(req, "uri", None)
1338 there = url.URL.fromContext(ctx)
1339 there = there.clear("uri")
1340 there = there.child("uri").child(uri)
1342 if len(segments) == 1:
1344 if req.method == "PUT":
1345 # either "PUT /uri" to create an unlinked file, or
1346 # "PUT /uri?t=mkdir" to create an unlinked directory
1347 t = get_arg(req, "t", "").strip()
1349 mutable = bool(get_arg(req, "mutable", "").strip())
1351 return unlinked.UnlinkedPUTSSKUploader(), ()
1353 return unlinked.UnlinkedPUTCHKUploader(), ()
1355 return unlinked.UnlinkedPUTCreateDirectory(), ()
1356 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1357 return WebError(http.BAD_REQUEST, errmsg), ()
1359 elif req.method == "POST":
1360 # "POST /uri?t=upload&file=newfile" to upload an
1361 # unlinked file or "POST /uri?t=mkdir" to create a
1363 t = get_arg(req, "t", "").strip()
1364 if t in ("", "upload"):
1365 mutable = bool(get_arg(req, "mutable", "").strip())
1367 return unlinked.UnlinkedPOSTSSKUploader(), ()
1369 return unlinked.UnlinkedPOSTCHKUploader(client, req), ()
1371 return unlinked.UnlinkedPOSTCreateDirectory(), ()
1372 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1373 return WebError(http.BAD_REQUEST, errmsg), ()
1374 if len(segments) < 2:
1375 return rend.NotFound
1377 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1378 d.addCallback(lambda node: VDrive(node, uri))
1379 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1380 def _trap_KeyError(f):
1382 return rend.FourOhFour(), ()
1383 d.addErrback(_trap_KeyError)
1385 elif segments[0] == "xmlrpc":
1386 raise NotImplementedError()
1387 return rend.Page.locateChild(self, ctx, segments)
1389 child_webform_css = webform.defaultCSS
1390 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1392 child_provisioning = provisioning.ProvisioningTool()
1393 child_status = status.Status()
1395 def data_version(self, ctx, data):
1396 return get_package_versions_string()
1397 def data_import_path(self, ctx, data):
1398 return str(allmydata)
1399 def data_my_nodeid(self, ctx, data):
1400 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1401 def data_storage(self, ctx, data):
1402 client = IClient(ctx)
1404 ss = client.getServiceNamed("storage")
1406 return "Not running"
1407 allocated = "about %s allocated" % abbreviate_size(ss.allocated_size())
1408 sizelimit = "no size limit"
1409 if ss.sizelimit is not None:
1410 sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit)
1411 return "%s, %s" % (allocated, sizelimit)
1413 def data_introducer_furl(self, ctx, data):
1414 return IClient(ctx).introducer_furl
1415 def data_connected_to_introducer(self, ctx, data):
1416 if IClient(ctx).connected_to_introducer():
1420 def data_helper_furl(self, ctx, data):
1422 uploader = IClient(ctx).getServiceNamed("uploader")
1425 furl, connected = uploader.get_helper_info()
1427 def data_connected_to_helper(self, ctx, data):
1429 uploader = IClient(ctx).getServiceNamed("uploader")
1431 return "no" # we don't even have an Uploader
1432 furl, connected = uploader.get_helper_info()
1437 def data_known_storage_servers(self, ctx, data):
1438 ic = IClient(ctx).introducer_client
1440 for c in ic.get_all_connectors().values()
1441 if c.service_name == "storage"]
1444 def data_connected_storage_servers(self, ctx, data):
1445 ic = IClient(ctx).introducer_client
1446 return len(ic.get_all_connections_for("storage"))
1448 def data_services(self, ctx, data):
1449 ic = IClient(ctx).introducer_client
1450 c = [ (service_name, nodeid, rsc)
1451 for (nodeid, service_name), rsc
1452 in ic.get_all_connectors().items() ]
1456 def render_service_row(self, ctx, data):
1457 (service_name, nodeid, rsc) = data
1458 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1461 rhost = rsc.remote_host
1462 if nodeid == IClient(ctx).nodeid:
1463 rhost_s = "(loopback)"
1464 elif isinstance(rhost, address.IPv4Address):
1465 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1467 rhost_s = str(rhost)
1468 connected = "Yes: to " + rhost_s
1469 since = rsc.last_connect_time
1472 since = rsc.last_loss_time
1474 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1475 ctx.fillSlots("connected", connected)
1476 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1477 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1478 time.localtime(rsc.announcement_time)))
1479 ctx.fillSlots("version", rsc.version)
1480 ctx.fillSlots("service_name", rsc.service_name)
1484 def render_download_form(self, ctx, data):
1485 # this is a form where users can download files by URI
1486 form = T.form(action="uri", method="get",
1487 enctype="multipart/form-data")[
1489 T.legend(class_="freeform-form-label")["Download a file"],
1490 "URI to download: ",
1491 T.input(type="text", name="uri"), " ",
1492 "Filename to download as: ",
1493 T.input(type="text", name="filename"), " ",
1494 T.input(type="submit", value="Download!"),
1498 def render_view_form(self, ctx, data):
1499 # this is a form where users can download files by URI, or jump to a
1501 form = T.form(action="uri", method="get",
1502 enctype="multipart/form-data")[
1504 T.legend(class_="freeform-form-label")["View a file or directory"],
1506 T.input(type="text", name="uri"), " ",
1507 T.input(type="submit", value="View!"),
1511 def render_upload_form(self, ctx, data):
1512 # this is a form where users can upload unlinked files
1513 form = T.form(action="uri", method="post",
1514 enctype="multipart/form-data")[
1516 T.legend(class_="freeform-form-label")["Upload a file"],
1518 T.input(type="file", name="file", class_="freeform-input-file"),
1519 T.input(type="hidden", name="t", value="upload"),
1520 " Mutable?:", T.input(type="checkbox", name="mutable"),
1521 T.input(type="submit", value="Upload!"),
1525 def render_mkdir_form(self, ctx, data):
1526 # this is a form where users can create new directories
1527 form = T.form(action="uri", method="post",
1528 enctype="multipart/form-data")[
1530 T.legend(class_="freeform-form-label")["Create a directory"],
1531 T.input(type="hidden", name="t", value="mkdir"),
1532 T.input(type="hidden", name="redirect_to_result", value="true"),
1533 T.input(type="submit", value="Create Directory!"),
1539 implements(ILocalAccess)
1541 self.local_access = False
1542 def local_access_is_allowed(self):
1543 return self.local_access
1545 class WebishServer(service.MultiService):
1549 def __init__(self, webport, nodeurl_path=None):
1550 service.MultiService.__init__(self)
1551 self.webport = webport
1552 self.root = self.root_class()
1553 self.site = site = appserver.NevowSite(self.root)
1554 self.site.requestFactory = MyRequest
1555 self.allow_local = LocalAccess()
1556 self.site.remember(self.allow_local, ILocalAccess)
1557 s = strports.service(webport, site)
1558 s.setServiceParent(self)
1559 self.listener = s # stash it so the tests can query for the portnum
1560 self._started = defer.Deferred()
1562 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1564 def allow_local_access(self, enable=True):
1565 self.allow_local.local_access = enable
1567 def startService(self):
1568 service.MultiService.startService(self)
1569 # to make various services available to render_* methods, we stash a
1570 # reference to the client on the NevowSite. This will be available by
1571 # adapting the 'context' argument to a special marker interface named
1573 self.site.remember(self.parent, IClient)
1574 # I thought you could do the same with an existing interface, but
1575 # apparently 'ISite' does not exist
1576 #self.site._client = self.parent
1577 self._started.callback(None)
1579 def _write_nodeurl_file(self, junk, nodeurl_path):
1580 # what is our webport?
1582 if isinstance(s, internet.TCPServer):
1583 base_url = "http://127.0.0.1:%d/" % s._port.getHost().port
1584 elif isinstance(s, internet.SSLServer):
1585 base_url = "https://127.0.0.1:%d/" % s._port.getHost().port
1589 f = open(nodeurl_path, 'wb')
1590 # this file is world-readable
1591 f.write(base_url + "\n")
1594 class IntroducerWebishServer(WebishServer):
1595 root_class = introweb.IntroducerRoot