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>…</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}">« Previous</a></li>' 387 else: 388 pagination += '<li class="page-item disabled"><span class="page-link">« 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 »</a></li>' 399 else: 400 pagination += '<li class="page-item disabled"><span class="page-link">Next »</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)