]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
Censor the introducer and helper furls' swissnums from the web welcome page. refs...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / root.py
1 import time, os
2
3 from twisted.internet import address
4 from twisted.web import http
5 from nevow import rend, url, tags as T
6 from nevow.inevow import IRequest
7 from nevow.static import File as nevow_File # TODO: merge with static.File?
8 from nevow.util import resource_filename
9
10 import allmydata # to display import path
11 from allmydata import get_package_versions_string
12 from allmydata.util import idlib, log
13 from allmydata.interfaces import IFileNode
14 from allmydata.web import filenode, directory, unlinked, status, operations
15 from allmydata.web import storage
16 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
17      get_arg, RenderMixin, get_format, get_mutable_type
18
19
20 class URIHandler(RenderMixin, rend.Page):
21     # I live at /uri . There are several operations defined on /uri itself,
22     # mostly involved with creation of unlinked files and directories.
23
24     def __init__(self, client):
25         rend.Page.__init__(self, client)
26         self.client = client
27
28     def render_GET(self, ctx):
29         req = IRequest(ctx)
30         uri = get_arg(req, "uri", None)
31         if uri is None:
32             raise WebError("GET /uri requires uri=")
33         there = url.URL.fromContext(ctx)
34         there = there.clear("uri")
35         # I thought about escaping the childcap that we attach to the URL
36         # here, but it seems that nevow does that for us.
37         there = there.child(uri)
38         return there
39
40     def render_PUT(self, ctx):
41         req = IRequest(ctx)
42         # either "PUT /uri" to create an unlinked file, or
43         # "PUT /uri?t=mkdir" to create an unlinked directory
44         t = get_arg(req, "t", "").strip()
45         if t == "":
46             file_format = get_format(req, "CHK")
47             mutable_type = get_mutable_type(file_format)
48             if mutable_type is not None:
49                 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
50             else:
51                 return unlinked.PUTUnlinkedCHK(req, self.client)
52         if t == "mkdir":
53             return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
54         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
55                   "and POST?t=mkdir")
56         raise WebError(errmsg, http.BAD_REQUEST)
57
58     def render_POST(self, ctx):
59         # "POST /uri?t=upload&file=newfile" to upload an
60         # unlinked file or "POST /uri?t=mkdir" to create a
61         # new directory
62         req = IRequest(ctx)
63         t = get_arg(req, "t", "").strip()
64         if t in ("", "upload"):
65             file_format = get_format(req)
66             mutable_type = get_mutable_type(file_format)
67             if mutable_type is not None:
68                 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
69             else:
70                 return unlinked.POSTUnlinkedCHK(req, self.client)
71         if t == "mkdir":
72             return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
73         elif t == "mkdir-with-children":
74             return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
75                                                                     self.client)
76         elif t == "mkdir-immutable":
77             return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
78                                                                  self.client)
79         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
80                   "and POST?t=mkdir")
81         raise WebError(errmsg, http.BAD_REQUEST)
82
83     def childFactory(self, ctx, name):
84         # 'name' is expected to be a URI
85         try:
86             node = self.client.create_node_from_uri(name)
87             return directory.make_handler_for(node, self.client)
88         except (TypeError, AssertionError):
89             raise WebError("'%s' is not a valid file- or directory- cap"
90                            % name)
91
92 class FileHandler(rend.Page):
93     # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
94     # file can be downloaded correctly by tools like "wget".
95
96     def __init__(self, client):
97         rend.Page.__init__(self, client)
98         self.client = client
99
100     def childFactory(self, ctx, name):
101         req = IRequest(ctx)
102         if req.method not in ("GET", "HEAD"):
103             raise WebError("/file can only be used with GET or HEAD")
104         # 'name' must be a file URI
105         try:
106             node = self.client.create_node_from_uri(name)
107         except (TypeError, AssertionError):
108             # I think this can no longer be reached
109             raise WebError("'%s' is not a valid file- or directory- cap"
110                            % name)
111         if not IFileNode.providedBy(node):
112             raise WebError("'%s' is not a file-cap" % name)
113         return filenode.FileNodeDownloadHandler(self.client, node)
114
115     def renderHTTP(self, ctx):
116         raise WebError("/file must be followed by a file-cap and a name",
117                        http.NOT_FOUND)
118
119 class IncidentReporter(RenderMixin, rend.Page):
120     def render_POST(self, ctx):
121         req = IRequest(ctx)
122         log.msg(format="User reports incident through web page: %(details)s",
123                 details=get_arg(req, "details", ""),
124                 level=log.WEIRD, umid="LkD9Pw")
125         req.setHeader("content-type", "text/plain")
126         return "Thank you for your report!"
127
128 SPACE = u"\u00A0"*2
129
130 class Root(rend.Page):
131
132     addSlash = True
133     docFactory = getxmlfile("welcome.xhtml")
134
135     def __init__(self, client, clock=None):
136         rend.Page.__init__(self, client)
137         self.client = client
138         # If set, clock is a twisted.internet.task.Clock that the tests
139         # use to test ophandle expiration.
140         self.child_operations = operations.OphandleTable(clock)
141         try:
142             s = client.getServiceNamed("storage")
143         except KeyError:
144             s = None
145         self.child_storage = storage.StorageStatus(s, self.client.nickname)
146
147         self.child_uri = URIHandler(client)
148         self.child_cap = URIHandler(client)
149
150         self.child_file = FileHandler(client)
151         self.child_named = FileHandler(client)
152         self.child_status = status.Status(client.get_history())
153         self.child_statistics = status.Statistics(client.stats_provider)
154         static_dir = resource_filename("allmydata.web", "static")
155         for filen in os.listdir(static_dir):
156             self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
157
158     def child_helper_status(self, ctx):
159         # the Helper isn't attached until after the Tub starts, so this child
160         # needs to created on each request
161         return status.HelperStatus(self.client.helper)
162
163     child_report_incident = IncidentReporter()
164     #child_server # let's reserve this for storage-server-over-HTTP
165
166     # FIXME: This code is duplicated in root.py and introweb.py.
167     def data_version(self, ctx, data):
168         return get_package_versions_string()
169     def data_import_path(self, ctx, data):
170         return str(allmydata)
171     def data_my_nodeid(self, ctx, data):
172         return idlib.nodeid_b2a(self.client.nodeid)
173     def data_my_nickname(self, ctx, data):
174         return self.client.nickname
175
176     def render_services(self, ctx, data):
177         ul = T.ul()
178         try:
179             ss = self.client.getServiceNamed("storage")
180             stats = ss.get_stats()
181             if stats["storage_server.accepting_immutable_shares"]:
182                 msg = "accepting new shares"
183             else:
184                 msg = "not accepting new shares (read-only)"
185             available = stats.get("storage_server.disk_avail")
186             if available is not None:
187                 msg += ", %s available" % abbreviate_size(available)
188             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
189         except KeyError:
190             ul[T.li["Not running storage server"]]
191
192         if self.client.helper:
193             stats = self.client.helper.get_stats()
194             active_uploads = stats["chk_upload_helper.active_uploads"]
195             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
196         else:
197             ul[T.li["Not running helper"]]
198
199         return ctx.tag[ul]
200
201     def data_introducer_furl_prefix(self, ctx, data):
202         ifurl = self.client.introducer_furl
203         # trim off the secret swissnum
204         (prefix, _, swissnum) = ifurl.rpartition("/")
205         if not ifurl:
206             return None
207         if swissnum == "introducer":
208             return ifurl
209         else:
210             return "%s/[censored]" % (prefix,)
211
212     def data_introducer_description(self, ctx, data):
213         if self.data_connected_to_introducer(ctx, data) == "no":
214             return "Introducer not connected"
215         return "Introducer"
216
217     def data_connected_to_introducer(self, ctx, data):
218         if self.client.connected_to_introducer():
219             return "yes"
220         return "no"
221
222     def data_helper_furl_prefix(self, ctx, data):
223         try:
224             uploader = self.client.getServiceNamed("uploader")
225         except KeyError:
226             return None
227         furl, connected = uploader.get_helper_info()
228         if not furl:
229             return None
230         # trim off the secret swissnum
231         (prefix, _, swissnum) = furl.rpartition("/")
232         return "%s/[censored]" % (prefix,)
233
234     def data_helper_description(self, ctx, data):
235         if self.data_connected_to_helper(ctx, data) == "no":
236             return "Helper not connected"
237         return "Helper"
238
239     def data_connected_to_helper(self, ctx, data):
240         try:
241             uploader = self.client.getServiceNamed("uploader")
242         except KeyError:
243             return "no" # we don't even have an Uploader
244         furl, connected = uploader.get_helper_info()
245
246         if furl is None:
247             return "not-configured"
248         if connected:
249             return "yes"
250         return "no"
251
252     def data_known_storage_servers(self, ctx, data):
253         sb = self.client.get_storage_broker()
254         return len(sb.get_all_serverids())
255
256     def data_connected_storage_servers(self, ctx, data):
257         sb = self.client.get_storage_broker()
258         return len(sb.get_connected_servers())
259
260     def data_services(self, ctx, data):
261         sb = self.client.get_storage_broker()
262         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
263
264     def render_service_row(self, ctx, server):
265         nodeid = server.get_serverid()
266
267         ctx.fillSlots("peerid", server.get_longname())
268         ctx.fillSlots("nickname", server.get_nickname())
269         rhost = server.get_remote_host()
270         if rhost:
271             if nodeid == self.client.nodeid:
272                 rhost_s = "(loopback)"
273             elif isinstance(rhost, address.IPv4Address):
274                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
275             else:
276                 rhost_s = str(rhost)
277             addr = rhost_s
278             connected = "yes"
279             since = server.get_last_connect_time()
280         else:
281             addr = "N/A"
282             connected = "no"
283             since = server.get_last_loss_time()
284         announced = server.get_announcement_time()
285         announcement = server.get_announcement()
286         version = announcement["my-version"]
287         service_name = announcement["service-name"]
288
289         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
290         ctx.fillSlots("address", addr)
291         ctx.fillSlots("connected", connected)
292         ctx.fillSlots("connected-bool", bool(rhost))
293         ctx.fillSlots("since", time.strftime(TIME_FORMAT,
294                                              time.localtime(since)))
295         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
296                                                  time.localtime(announced)))
297         ctx.fillSlots("version", version)
298         ctx.fillSlots("service_name", service_name)
299
300         return ctx.tag
301
302     def render_download_form(self, ctx, data):
303         # this is a form where users can download files by URI
304         form = T.form(action="uri", method="get",
305                       enctype="multipart/form-data")[
306             T.fieldset[
307             T.legend(class_="freeform-form-label")["Download a file"],
308             T.div["Tahoe-URI to download:"+SPACE,
309                   T.input(type="text", name="uri")],
310             T.div["Filename to download as:"+SPACE,
311                   T.input(type="text", name="filename")],
312             T.input(type="submit", value="Download!"),
313             ]]
314         return T.div[form]
315
316     def render_view_form(self, ctx, data):
317         # this is a form where users can download files by URI, or jump to a
318         # named directory
319         form = T.form(action="uri", method="get",
320                       enctype="multipart/form-data")[
321             T.fieldset[
322             T.legend(class_="freeform-form-label")["View a file or directory"],
323             "Tahoe-URI to view:"+SPACE,
324             T.input(type="text", name="uri"), SPACE*2,
325             T.input(type="submit", value="View!"),
326             ]]
327         return T.div[form]
328
329     def render_upload_form(self, ctx, data):
330         # This is a form where users can upload unlinked files.
331         # Users can choose immutable, SDMF, or MDMF from a radio button.
332
333         upload_chk  = T.input(type='radio', name='format',
334                               value='chk', id='upload-chk',
335                               checked='checked')
336         upload_sdmf = T.input(type='radio', name='format',
337                               value='sdmf', id='upload-sdmf')
338         upload_mdmf = T.input(type='radio', name='format',
339                               value='mdmf', id='upload-mdmf')
340
341         form = T.form(action="uri", method="post",
342                       enctype="multipart/form-data")[
343             T.fieldset[
344             T.legend(class_="freeform-form-label")["Upload a file"],
345             T.div["Choose a file:"+SPACE,
346                   T.input(type="file", name="file", class_="freeform-input-file")],
347             T.input(type="hidden", name="t", value="upload"),
348             T.div[upload_chk,  T.label(for_="upload-chk") [" Immutable"],           SPACE,
349                   upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],                SPACE,
350                   upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
351                   T.input(type="submit", value="Upload!")],
352             ]]
353         return T.div[form]
354
355     def render_mkdir_form(self, ctx, data):
356         # This is a form where users can create new directories.
357         # Users can choose SDMF or MDMF from a radio button.
358
359         mkdir_sdmf = T.input(type='radio', name='format',
360                              value='sdmf', id='mkdir-sdmf',
361                              checked='checked')
362         mkdir_mdmf = T.input(type='radio', name='format',
363                              value='mdmf', id='mkdir-mdmf')
364
365         form = T.form(action="uri", method="post",
366                       enctype="multipart/form-data")[
367             T.fieldset[
368             T.legend(class_="freeform-form-label")["Create a directory"],
369             mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
370             mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
371             T.input(type="hidden", name="t", value="mkdir"),
372             T.input(type="hidden", name="redirect_to_result", value="true"),
373             T.input(type="submit", value="Create a directory"),
374             ]]
375         return T.div[form]
376
377     def render_incident_button(self, ctx, data):
378         # this button triggers a foolscap-logging "incident"
379         form = T.form(action="report_incident", method="post",
380                       enctype="multipart/form-data")[
381             T.fieldset[
382             T.input(type="hidden", name="t", value="report-incident"),
383             "What went wrong?"+SPACE,
384             T.input(type="text", name="details"), SPACE,
385             T.input(type="submit", value=u"Report \u00BB"),
386             ]]
387         return T.div[form]