/ cgi-bin / main
main
  1  #!/usr/bin/python3
  2  
  3  import html
  4  from urllib.parse import quote_plus
  5  import datetime
  6  import time
  7  import re
  8  import string
  9  import base64
 10  import json
 11  import random
 12  from util import *
 13  from config import *
 14  
 15  sys.excepthook = lambda t,v,tb: print('<pre>'+html.escape(''.join(traceback.format_exception(t,v,tb)))+'</pre>')
 16  
 17  def print_error (msg):
 18      print('<p class="error-message">'+html.escape(msg)+'</p>')
 19      
 20  def print_info (msg):
 21      print('<p>'+html.escape(msg)+'</p>')
 22  
 23  HTML_HEAD='''
 24  <!doctype html>
 25  <html lang="en">
 26    <head>
 27      <meta charset="utf-8">
 28      <meta name="viewport" content="width=device-width, initial-scale=1">
 29      <title>Cradicle</title>
 30      <link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet">
 31      <style>
 32        .bd-placeholder-img {
 33  	  font-size: 1.125rem;
 34  	  text-anchor: middle;
 35  	  -webkit-user-select: none;
 36  	  -moz-user-select: none;
 37  	  user-select: none;
 38        }
 39  
 40        @media (min-width: 768px) {
 41  	  .bd-placeholder-img-lg {
 42  	      font-size: 3.5rem;
 43  	  }
 44        }
 45  
 46        .b-example-divider {
 47  	  height: 3rem;
 48  	  background-color: rgba(0, 0, 0, .1);
 49  	  border: solid rgba(0, 0, 0, .15);
 50  	  border-width: 1px 0;
 51  	  box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
 52        }
 53  
 54        .b-example-vr {
 55  	  flex-shrink: 0;
 56  	  width: 1.5rem;
 57  	  height: 100vh;
 58        }
 59  
 60        .bi {
 61  	  vertical-align: -.125em;
 62  	  fill: currentColor;
 63        }
 64  
 65        .nav-scroller {
 66  	  position: relative;
 67  	  z-index: 2;
 68  	  height: 2.75rem;
 69  	  overflow-y: hidden;
 70        }
 71  
 72        .nav-scroller .nav {
 73  	  display: flex;
 74  	  flex-wrap: nowrap;
 75  	  padding-bottom: 1rem;
 76  	  margin-top: -1px;
 77  	  overflow-x: auto;
 78  	  text-align: center;
 79  	  white-space: nowrap;
 80  	  -webkit-overflow-scrolling: touch;
 81        }
 82        .form-control-dark::placeholder {
 83            color: #aaa;
 84            opacity: 1;
 85        }
 86      </style>
 87      <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
 88      <link rel="icon" type="image/png" href="/favicon.png">
 89  '''
 90  
 91  print(HTML_HEAD)
 92  
 93  form = FormData()
 94  alias = form.getvalue('alias')
 95  password = form.getvalue('password')
 96  password2 = form.getvalue('password2')
 97  random_create = form.getvalue('random')
 98  vis = form.getvalue('vis')
 99  sync = form.getvalue('sync')
100  node = form.getvalue('node')
101  tor = form.getvalue('tor')
102  query = form.getvalue('q')
103  page_str = form.getvalue('page')
104  page = int(page_str) if page_str and page_str.isdigit() else 1
105  if page < 1:
106      page = 1
107  PAGE_SIZE = 256
108  
109  random_password = None
110  if random_create:
111      alias = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1,32)))
112      random_password = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=24))
113      password = random_password
114      password2 = random_password
115  
116  if alias and password and password2 and password == password2:
117      ret_create,out_create,err_create = exec_command(crad_bin,["auth","--alias",alias,"--password",password])
118  
119  ret,out,err = exec_command(crad_bin,["self","--alias"])
120  self_alias = 'unknown user'
121  if not ret:
122      self_alias = html.escape(out.strip())
123  ret,out,err = exec_command(crad_bin,["self","--did"])
124  self_did = ''
125  if not ret:
126      self_did = html.escape(out.strip())
127  
128  node_on = False
129  ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid'])
130  if not ret:
131      node_on = True
132  tor_on = False
133  ret,out,err = exec_command(crad_bin,['self','--network-mode'])
134  if not ret and out.strip() == 'tor':
135      tor_on = True
136  
137  if tor == 'on' and not tor_on:
138      set_config_tor(True)
139      if node_on and node != 'off':
140          node = 'restart'
141  elif tor == 'off' and tor_on:
142      set_config_tor(False)
143      if node_on and node != 'off':
144          node = 'restart'
145      
146  if node == 'on' and not node_on:
147      exec_command_bg(radicle_node_wrapped_bin,[])
148      time.sleep(8)
149  elif node == 'off' and node_on:
150      ret,out,err = exec_command(rad_node_stop_wrapped_bin,[])
151  elif node == 'restart':
152      if node_on:
153          ret,out,err = exec_command(rad_node_stop_wrapped_bin,[])
154      exec_command_bg(radicle_node_wrapped_bin,[])
155      time.sleep(8)
156  
157  ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid'])
158  if not ret:
159      node_on = True
160      node_switch_url='&node=off'
161      node_title='click to disconnect from network'
162  else:
163      node_on = False
164      node_switch_url ='&node=on'
165      node_title ='click to connect to network'
166  
167  tor_on = False
168  tor_switch_url = '&tor=on'
169  tor_title = 'click to route through Tor network'
170  ret,out,err = exec_command(crad_bin,["self","--network-mode"])
171  if not ret and out.strip() == 'tor':
172      tor_on = True
173      tor_switch_url = '&tor=off'
174      tor_title = 'click to disconnect from Tor network'
175  
176  class_repos = ''
177  class_private = ''
178  class_public = ''
179  aria_repos = ''
180  aria_private = ''
181  aria_public = ''
182  class_node_icon = ' icon-off'
183  class_tor_icon = ' icon-off'
184  if vis == 'private':
185      class_private = 'active'
186      aria_private = 'aria_current="page"'
187  elif vis == 'public':
188      class_public = 'active'
189      aria_public = 'aria_current="page"'
190  else:
191      class_repos = 'active'
192      aria_repos = 'aria_current="page"'
193  if node_on:
194      class_node_icon = ' icon-on'
195  if tor_on:
196      class_tor_icon = ' icon-on'
197      
198  HTML_DASHBOARD_START=f'''
199                  <link href="/css/dashboard.css" rel="stylesheet">
200                  </head>
201                  <body>
202                  <header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
203                    <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="/">Cradicle</a>
204                    <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
205                      <span class="navbar-toggler-icon"></span>
206                    </button>
207                    <form method="get" action="/cgi-bin/main" style="width:100%;"><input class="form-control form-control-dark w-100 rounded-0 border-0" type="text" name="q" placeholder="Search repos" aria-label="Search" value="{html.escape(query) if query else ''}"></form>
208                    <div class="navbar-nav flex-row">
209                      <div class="nav-item text-nowrap">
210                        <a class="nav-link px-3 active" title="{node_title}" href="/cgi-bin/main?{node_switch_url}"><i class="fa-solid fa-circle-nodes{class_node_icon}"></i></a>
211                      </div>
212                      <div class="nav-item text-nowrap">
213                        <a class="nav-link px-3 active" title="{tor_title}" href="/cgi-bin/main?{tor_switch_url}"><i class="ft-onion{class_tor_icon}"></i></a>
214                      </div>
215                      <div class="nav-item text-nowrap">
216                        <a class="nav-link px-3 active" aria_current="page" href="/cgi-bin/profile">{self_alias[:12]}-{self_did[8:20]}</a>
217                      </div>
218                      <div class="nav-item text-nowrap">
219                        <a class="nav-link px-3" href="/cgi-bin/settings"><i class="fa-solid fa-gear"></i> Settings</a>
220                      </div>
221                      <div class="nav-item text-nowrap">
222                        <a class="nav-link px-3" href="/cgi-bin/signout"><i class="fa-solid fa-sign-out"></i> Sign out</a>
223                      </div>
224                    </div>
225                  </header>
226                  <div class="container-fluid">
227                    <div class="row">
228                      <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-dark sidebar collapse">
229                        <div class="position-sticky pt-3 sidebar-sticky">
230                          <ul class="nav flex-column">
231                            <li class="nav-item">
232                              <a class="nav-link {class_repos}" {aria_repos} href="/cgi-bin/main">
233                                <i class="align-text-bottom fa-solid fa-layer-group"></i>
234                                Repositories
235                              </a>
236                            </li>
237                            <li class="nav-item">
238                              <a class="nav-link {class_private}" {aria_private} href="/cgi-bin/main?vis=private">
239                                <i class="align-text-bottom fa-solid fa-lock"></i>
240                                Private
241                              </a>
242                            </li>
243                            <li class="nav-item">
244                              <a class="nav-link {class_public}" {aria_public} href="/cgi-bin/main?vis=public">
245                                <i class="align-text-bottom fa-solid fa-eye"></i> Public
246                              </a>
247                            </li>
248                          <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
249                            <span></span>
250                          </h6>
251                          <ul class="nav flex-column mb-2">
252                            <li class="nav-item">
253                              <a class="nav-link" href="/cgi-bin/main?sync=yes">
254                                <i class="align-text-bottom fa-solid fa-sync"></i> Sync Repos
255                              </a>
256                            </li>
257                            <li class="nav-item">
258                              <a class="nav-link" href="/cgi-bin/seed">
259                                <i class="align-text-bottom fa-solid fa-seedling"></i> Seed a Repo
260                              </a>
261                            </li>
262                          </ul>
263                        </div>
264                      </nav>
265                  <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
266                    <div class="container px-1 py-1">
267                      <div class="row row-cols-1 row-cols-lg-3 align-items-stretch g-4 py-4">
268          '''
269  HTML_DASHBOARD_END = '''
270  </div>
271  </div>
272  </main>
273  </div>
274  </div>
275  '''
276  
277  HTML_AUTH_START = '''
278  <link href="/css/signin.css" rel="stylesheet">
279  </head>
280  <body class="text-center">
281  <main class="form-signin w-100 m-auto">
282  <form action="/" method="post">
283  <div class="mb-4" width="100%" style="font-size:2.5rem; font-weight:bold; color:#2470dc;">Cradicle</div>
284  '''
285  
286  HTML_AUTH_END_CREATE='''
287  <div class="form-floating">
288  <input type="text" class="form-control" id="floatingInput" name="alias" placeholder="user">
289  <label for="floatingInput">Alias</label>
290  </div>
291  <div class="form-floating" style="margin-top:10px;">
292  <input type="password" class="form-control" id="floatingPassword" name="password" placeholder="">
293  <label for="floatingPassword">Password</label>
294  </div>
295  <div class="form-floating">
296  <input type="password" class="form-control" id="floatingPassword2" name="password2" placeholder="">
297  <label for="floatingPassword">Repeat Password</label>
298  </div>
299  <button class="w-100 btn btn-lg btn-primary" type="submit">Create</button>
300  </form>
301  <form action="/" method="post" style="margin-top:10px;">
302  <input type="hidden" name="random" value="1">
303  <button class="w-100 btn btn-lg btn-secondary" type="submit">Create Random</button>
304  </form>
305  </main>
306  '''
307  
308  HTML_AUTH_END_LOGIN='''
309  <div class="form-floating">
310  <input type="password" class="form-control" id="floatingPassword" name="password" placeholder="">
311  <label for="floatingPassword">Password</label>
312  </div>
313  <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
314  </form>
315  </main>
316  '''
317  
318  dashboard_content = ''
319  #dashboard_content += '<div><p>ret: '+html.escape(str(ret_ns))+'</p><p>out: '+html.escape(out_ns)+'</p><p>err: '+html.escape(err_ns)+'</p></div>'
320  
321  sync_ret = None
322  
323  if sync == 'yes':
324      if not node_on:
325          exec_command_bg(radicle_node_wrapped_bin,[])
326          time.sleep(8)
327      ret,out,err = exec_command(crad_bin,['ls','--public'])
328      if not ret:
329          for line in out.splitlines():
330              entries = line.split()
331              if len(entries)<2:
332                  continue
333              sync_ret,sync_out,sync_err = exec_command(crad_bin,['sync','-R',entries[1].strip()])
334              if sync_ret:
335                  break
336              
337  if sync == 'yes' and sync_ret:
338      dashboard_content += f'<div>{html.escape(sync_out)}</div><div class="error-message">{html.escape(sync_err)}</div>'
339  else:
340      skip = (page - 1) * PAGE_SIZE
341      ls_args = ["ls","--json","--limit",str(PAGE_SIZE + 1),"--skip",str(skip)]
342      if vis == 'private':
343          ls_args.append("--private")
344      elif vis == 'public':
345          ls_args.append("--public")
346      if query:
347          ls_args += ["-q",query]
348      ret,out,err = exec_command(crad_bin,ls_args)
349      if ret or not out:
350          dashboard_content += '<p>No repositories</p>'
351      else:
352          repos = json.loads(out)
353          has_next = len(repos) > PAGE_SIZE
354          repos = repos[:PAGE_SIZE]
355          if not repos:
356              dashboard_content += '<p>No matching repositories</p>'
357          else:
358              for repo in repos:
359                  visibility = repo['visibility']
360                  vis_icon = 'lock' if visibility == 'private' else 'eye'
361                  rid_short = repo['rid'][:12]
362                  dashboard_content += f'''
363                        <div class="col">
364                          <a href="/cgi-bin/repo?id={quote_plus(repo['rid'])}"><div class="card card-cover h-100 overflow-hidden text-bg-dark rounded-4 shadow-lg">
365                            <div class="d-flex flex-column h-100 p-3 pb-2 text-white text-shadow-1">
366                              <span class="display-6 lh-1 fw-bold" style="font-size:1.3rem;">{html.escape(repo['name'][:32])}</span>
367                              <span class="display-6 lh-1 repo-item" style="font-size:1.0rem;">{html.escape(repo['description'][:64])}</span>
368                              <span class="display-6 lh-1" style="font-size:1.0rem;"><i>rad:{html.escape(rid_short)}</i>&hellip;</span>
369                              <ul class="d-flex list-unstyled mt-1">
370                                <li class="d-flex align-items-center me-3">
371                                  <i class="repo-item fa-solid fa-{vis_icon}"></i>
372                                </li>
373                              </ul>
374                            </div>
375                          </div></a>
376                        </div>
377                  '''
378              # Pagination links
379              page_params = ''
380              if vis:
381                  page_params += f'&vis={quote_plus(vis)}'
382              if query:
383                  page_params += f'&q={quote_plus(query)}'
384              pagination = '<nav class="mt-3"><ul class="pagination justify-content-center">'
385              if page > 1:
386                  pagination += f'<li class="page-item"><a class="page-link" href="/cgi-bin/main?page={page-1}{page_params}">&laquo; Previous</a></li>'
387              else:
388                  pagination += '<li class="page-item disabled"><span class="page-link">&laquo; Previous</span></li>'
389              # Show page numbers around current page
390              start_page = max(1, page - 3)
391              end_page = page + 3 if has_next else page
392              for p in range(start_page, end_page + 1):
393                  if p == page:
394                      pagination += f'<li class="page-item active"><span class="page-link">{p}</span></li>'
395                  else:
396                      pagination += f'<li class="page-item"><a class="page-link" href="/cgi-bin/main?page={p}{page_params}">{p}</a></li>'
397              if has_next:
398                  pagination += f'<li class="page-item"><a class="page-link" href="/cgi-bin/main?page={page+1}{page_params}">Next &raquo;</a></li>'
399              else:
400                  pagination += '<li class="page-item disabled"><span class="page-link">Next &raquo;</span></li>'
401              pagination += '</ul></nav>'
402              dashboard_content += pagination
403  
404  if alias and password and password2 and password == password2:
405      if ret_create:
406          print(HTML_AUTH_START)
407          print_error(err_create)
408          print(HTML_AUTH_END_CREATE)
409      elif random_password:
410          print(HTML_AUTH_START)
411          print(f'<h1 class="h3 mb-3 fw-normal">Identity created</h1>')
412          print(f'<div class="alert alert-warning text-start"><strong>Save your password:</strong><br><code style="font-size:1.2rem;">{html.escape(random_password)}</code></div>')
413          print(f'<a class="w-100 btn btn-lg btn-primary" href="/">Continue</a>')
414          print('</main>')
415      else:
416          print(HTML_DASHBOARD_START)
417          print(dashboard_content)
418          print(HTML_DASHBOARD_END)
419  elif password and not password2:
420      ret,out,err = exec_command(crad_bin,['auth','--password',password])
421      if ret:
422          print(HTML_AUTH_START)
423          print_error(err)
424          print(HTML_AUTH_END_LOGIN)
425      else:
426          print(HTML_DASHBOARD_START)
427          print(dashboard_content)
428          print(HTML_DASHBOARD_END)
429  else:
430      ret,out,err = exec_command(crad_bin,["self","--alias"])
431      ret2,out2,err2 = exec_command(crad_bin,["self","--authed"])
432      if ret:
433          print(HTML_AUTH_START)
434          if password and password != password2:
435              print_error("passwords don't match")
436          else:
437              print('<h1 class="h3 mb-3 fw-normal">Create a new identity</h1>')
438          print(HTML_AUTH_END_CREATE)
439      elif ret2:
440          print(HTML_AUTH_START)
441          if password:
442              print_error('invalid password')
443          else:
444              print('<h1 class="h3 mb-3 fw-normal">Hello <i>'+html.escape(out)+'</i></h1>')
445          print(HTML_AUTH_END_LOGIN)        
446      else:
447          print(HTML_DASHBOARD_START)
448          print(dashboard_content)
449          print(HTML_DASHBOARD_END)
450  
451  HTML_FOOTER='''
452  </body>
453  </html>
454  '''
455  print(HTML_FOOTER)