Browse Source

add loading indicators, caching

Samir Menon 2 years ago
parent
commit
9205deeff9

+ 135 - 0
client/static/css/loading.css

@@ -0,0 +1,135 @@
+/*!
+ * Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
+ * Copyright 2015 Daniel Cardoso <@DanielCardoso>
+ * Licensed under MIT
+ */
+ .la-ball-clip-rotate,
+ .la-ball-clip-rotate > div {
+     position: relative;
+     -webkit-box-sizing: border-box;
+        -moz-box-sizing: border-box;
+             box-sizing: border-box;
+ }
+ .la-ball-clip-rotate {
+     display: block;
+     font-size: 0;
+     color: #fff;
+ }
+ .la-ball-clip-rotate.la-dark {
+     color: #333;
+ }
+ .la-ball-clip-rotate > div {
+     display: inline-block;
+     float: none;
+     background-color: currentColor;
+     border: 0 solid currentColor;
+ }
+ .la-ball-clip-rotate {
+     width: 32px;
+     height: 32px;
+ }
+ .la-ball-clip-rotate > div {
+     width: 32px;
+     height: 32px;
+     background: transparent;
+     border-width: 2px;
+     border-bottom-color: transparent;
+     border-radius: 100%;
+     -webkit-animation: ball-clip-rotate 1.0s linear infinite;
+        -moz-animation: ball-clip-rotate 1.0s linear infinite;
+          -o-animation: ball-clip-rotate 1.0s linear infinite;
+             animation: ball-clip-rotate 1.0s linear infinite;
+ }
+ .la-ball-clip-rotate.la-sm {
+     width: 16px;
+     height: 16px;
+ }
+ .la-ball-clip-rotate.la-sm > div {
+     width: 16px;
+     height: 16px;
+     border-width: 1px;
+ }
+ .la-ball-clip-rotate.la-2x {
+     width: 64px;
+     height: 64px;
+ }
+ .la-ball-clip-rotate.la-2x > div {
+     width: 64px;
+     height: 64px;
+     border-width: 4px;
+ }
+ .la-ball-clip-rotate.la-3x {
+     width: 96px;
+     height: 96px;
+ }
+ .la-ball-clip-rotate.la-3x > div {
+     width: 96px;
+     height: 96px;
+     border-width: 6px;
+ }
+ /*
+  * Animation
+  */
+ @-webkit-keyframes ball-clip-rotate {
+     0% {
+         -webkit-transform: rotate(0deg);
+                 transform: rotate(0deg);
+     }
+     50% {
+         -webkit-transform: rotate(180deg);
+                 transform: rotate(180deg);
+     }
+     100% {
+         -webkit-transform: rotate(360deg);
+                 transform: rotate(360deg);
+     }
+ }
+ @-moz-keyframes ball-clip-rotate {
+     0% {
+         -moz-transform: rotate(0deg);
+              transform: rotate(0deg);
+     }
+     50% {
+         -moz-transform: rotate(180deg);
+              transform: rotate(180deg);
+     }
+     100% {
+         -moz-transform: rotate(360deg);
+              transform: rotate(360deg);
+     }
+ }
+ @-o-keyframes ball-clip-rotate {
+     0% {
+         -o-transform: rotate(0deg);
+            transform: rotate(0deg);
+     }
+     50% {
+         -o-transform: rotate(180deg);
+            transform: rotate(180deg);
+     }
+     100% {
+         -o-transform: rotate(360deg);
+            transform: rotate(360deg);
+     }
+ }
+ @keyframes ball-clip-rotate {
+     0% {
+         -webkit-transform: rotate(0deg);
+            -moz-transform: rotate(0deg);
+              -o-transform: rotate(0deg);
+                 transform: rotate(0deg);
+     }
+     50% {
+         -webkit-transform: rotate(180deg);
+            -moz-transform: rotate(180deg);
+              -o-transform: rotate(180deg);
+                 transform: rotate(180deg);
+     }
+     100% {
+         -webkit-transform: rotate(360deg);
+            -moz-transform: rotate(360deg);
+              -o-transform: rotate(360deg);
+                 transform: rotate(360deg);
+     }
+ }
+ 

+ 0 - 3
client/static/css/reset.css

@@ -26,9 +26,6 @@ footer, header, hgroup, menu, nav, section {
 body {
 	line-height: 1;
 }
-ol, ul {
-	list-style: none;
-}
 blockquote, q {
 	quotes: none;
 }

+ 62 - 5
client/static/css/style.css

@@ -39,6 +39,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   z-index: 1;
 }
 
+.topbar {
+  display: flex;
+}
+
 .searchbox {
   width: 250px;
   font-size: 20px;
@@ -66,26 +70,30 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   display: flex;
   flex-wrap: wrap;
   padding: 16px;
+  align-items: flex-start;
 }
 
 .articles {
   width: 800px;
+  max-width: 1200px;
   margin-left: 20px;
   flex-grow: 1;
+  margin-right: 16px;
 }
 
-.articles:empty {
+/* .articles:empty {
   display: none;
-}
+} */
 
 .sidebar {
-  max-width: 600px;
-  padding: 8px;
+  max-width: 500px;
+  padding: 16px;
   font-family: Arial;
   background-color: #EEE;
   position: relative;
   min-height: 48px;
   flex-grow: 1;
+  line-height: 1.25em;
 }
 
 .sidebar.collapsed {
@@ -239,6 +247,17 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   color: #444;
 }
 
+
+.sidebar p {
+  margin-top: 0;
+  margin-bottom: 14px;
+}
+
+.sidebar p:last-of-type {
+  margin-bottom: 0;
+}
+
+
 .sidebar figure {
   margin: 0;
 }
@@ -420,7 +439,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
 .hidden {
-  display: none;
+  visibility: hidden !important;
 }
 
 .loadingbar {
@@ -470,4 +489,42 @@ b, strong {
 
 i, em {
   font-style: italic;
+}
+
+.title {
+  margin-right: 32px; 
+}
+
+.title h1 {
+  font-family: sans-serif;
+  font-size: 24px;
+  font-weight: bold;
+  margin: 4px;
+}
+
+.searchbutton {
+  display: flex;
+}
+
+#make_query {
+  appearance: none;
+  border: none;
+  height: 30px;
+  background-color: #ddd;
+  cursor: pointer;
+}
+
+.loading {
+  margin-left: 32px;
+  display: flex;
+  align-items: center;
+}
+
+.loading .message {
+  margin-left: 8px;
+  font-family: sans-serif;
+}
+
+.loading .inprogress {
+  font-style: italic;
 }

+ 46 - 8
client/static/index.html

@@ -1,20 +1,58 @@
 <html>
   <head>
     <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
-    <link href="css/reset.css" rel="stylesheet" />
     <link href="css/style.css" rel="stylesheet" />
+    <link href="css/loading.css" rel="stylesheet" />
+
+    <meta http-equiv="Content-Security-Policy"
+      content="default-src 'self' 'unsafe-eval' 'wasm-eval';">
   </head>
   <body>
-    <h1>Spiral Demo</h1>
-
-    <div class="searchsuperbox">
-      <div class="searchandsuggestions">
-        <input type="text" class="searchbox" placeholder="Article title" />
+    <div class="fixedpanel">
+      <div class="topbar">
+        <div class="title">
+          <h1>Spiral Demo</h1>
+        </div>
+        <div class="searchsuperbox">
+          <div class="searchandsuggestions">
+            <div class="searchbutton">
+              <input type="text" class="searchbox" placeholder="Article title" />
+              <input id="make_query" type="button" value="&gt;" />
+              <div class="loading">
+                <div class="loading-icon hidden la-ball-clip-rotate la-dark la-sm"><div></div></div>
+                <div class="message inprogress"></div>
+              </div>
+            </div>
+          </div>
+        </div>
       </div>
-      <input id="make_query" type="button" value="&gt;" />
     </div>
 
-    <div id="output" class="articles"></div>
+    <div class="content">
+      <div id="output" class="articles"></div>
+
+      <div class="sidebar">
+        <p>
+          This is a page is a demo of the work featured in our paper, <a href="https://eprint.iacr.org/2022/368.pdf">"Spiral: Fast, High-Rate Single-Server PIR via FHE Composition"</a>. 
+          The code for our system is available <a href="https://github.com/menonsamir/spiral-rs">here</a>.
+        </p>
+
+        <p>
+          This demo allows private access to 6 GB (~30%) of English Wikipedia.
+          In theory, even if the server is malicious, it will be unable to learn which articles you request.
+          All article title searches are performed locally, and no images are available.
+        </p>
+
+        <p>
+          When making your first query, this demo will upload 18 MB of data; each later query requires only 14 KB of upload. The server response to each query is 250 KB.
+        </p>
+
+        <p>
+          This is research-quality software built for demonstration purposes; it is not intended to be 
+          side-channel resistant and has not undergone any kind fo security review. Don't use this code in production.
+        </p>
+      </div>
+    </div>
 
     <!-- <script type="module" src="js/bz2.js"></script>
     <script type="module" src="js/wtf_wikipedia-client.min.js"></script> -->

+ 34 - 5
client/static/js/main.js

@@ -129,7 +129,7 @@ function showSuggestionsBox(suggestions, query) {
     var htmlSuggestions = '<div class="suggestions">' 
         + suggestions.map(m => "<div>"+addBold(m, query)+"</div>").join('') 
         + "</div>";
-    document.querySelector('.searchbox').insertAdjacentHTML('afterend', htmlSuggestions);
+    document.querySelector('.searchbutton').insertAdjacentHTML('afterend', htmlSuggestions);
     document.querySelectorAll('.suggestions > div').forEach((el) => {
         el.onclick = (e) => {
             document.querySelector(".searchbox").value = el.innerHTML
@@ -162,9 +162,29 @@ function followRedirects(title) {
     }
 }
 
-function queryTitleOnClick(title) {
+function startLoading(message) {
+    window.loading = true;
+    window.started_loading = Date.now();
+    document.querySelector(".loading-icon").classList.remove("hidden");
+    document.querySelector(".loading .message").innerHTML = message+"...";
+    document.querySelector(".loading .message").classList.add("inprogress");
+}
+
+function stopLoading(message) {
+    window.loading = false;
+    document.querySelector(".loading-icon").classList.add("hidden");
+    let seconds = (Date.now() - window.started_loading) / 1000
+    let secondsRounded = Math.round(seconds * 100) / 100;
+    let timingMessage = secondsRounded > 0.01 ? (" Took "+secondsRounded+"s.") : "";
+    document.querySelector(".loading .message").innerHTML = "Done " + message.toLowerCase() + "." + timingMessage;
+    document.querySelector(".loading .message").classList.remove("inprogress");
+}
+
+function queryTitleOnClick(title, displayTitle) {
     return async (e) => {
         e.preventDefault();
+        document.querySelector(".searchbox").value = displayTitle;
+        window.scrollTo(0, 0);
         queryTitle(title);
         return false;
     }
@@ -172,13 +192,14 @@ function queryTitleOnClick(title) {
 
 function enableLinks(element) {
     element.querySelectorAll('a').forEach((el) => {
-        var linkTitle = el.getAttribute("href").slice(2).replace(/_/g, " ").toLowerCase();
+        let displayTitle = el.getAttribute("href").slice(2).replace(/_/g, " ");
+        let linkTitle = displayTitle.toLowerCase();
         if (hasTitle(linkTitle)) {
-            el.onclick = queryTitleOnClick(linkTitle);
+            el.onclick = queryTitleOnClick(linkTitle, displayTitle);
         } else {
             var redirected = followRedirects(linkTitle);
             if (redirected !== null && hasTitle(redirected)) {
-                el.onclick = queryTitleOnClick(redirected);
+                el.onclick = queryTitleOnClick(redirected, displayTitle);
             } else {
                 el.classList.add("broken")
             }
@@ -188,6 +209,7 @@ function enableLinks(element) {
 
 async function query(targetIdx, title) {    
     if (!window.hasSetUp) {
+        startLoading("Uploading setup data");
         console.log("Initializing...");
         window.client = initialize();
         console.log("done");
@@ -200,8 +222,10 @@ async function query(targetIdx, title) {
         console.log(setup_resp);
         window.id = setup_resp["id"];
         window.hasSetUp = true;
+        stopLoading("Uploading setup data");
     }
 
+    startLoading("Loading article");
     console.log("Generating query... ("+targetIdx+")");
     let query = generate_query(window.client, window.id, targetIdx);
     console.log(`done (${query.length} bytes)`);
@@ -225,6 +249,7 @@ async function query(targetIdx, title) {
     outputArea.innerHTML = resultHtml;
 
     enableLinks(outputArea);
+    stopLoading("Loading article");
 }
 
 async function queryTitle(targetTitle) {
@@ -234,7 +259,9 @@ async function queryTitle(targetTitle) {
 }
 
 async function run() {
+    startLoading("Initializing");
     await init();
+    stopLoading("Initializing");
 
     window.numArticles = 65536;
     window.articleSize = 100000;
@@ -242,6 +269,7 @@ async function run() {
     let makeQueryBtn = document.querySelector('#make_query');
     let searchBox = document.querySelector(".searchbox");
 
+    startLoading("Loading article titles");
     let title_index_p = getData("data/enwiki-20220320-index.json", true);
     let redirect_backlinks_p = getData("data/redirects-old.json", true);
 
@@ -262,6 +290,7 @@ async function run() {
             window.redirects[redirect_srcs[j].toLowerCase()] = redirect_dest;
         }
     }
+    stopLoading("Loading article titles");
 
     searchBox.addEventListener('input', (e) => {
         clearExistingSuggestionsBox();

+ 16 - 1
spiral-rs/src/bin/server.rs

@@ -14,6 +14,7 @@ use actix_files as fs;
 use actix_http::HttpServiceBuilder;
 use actix_server::{Server, ServerBuilder};
 use actix_service::map_config;
+use actix_service::Service;
 use actix_web::error::PayloadError;
 use actix_web::{get, http, middleware, post, web, App, HttpServer};
 use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
@@ -21,6 +22,8 @@ use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
 const CERT_FNAME: &str = "/etc/letsencrypt/live/spiralwiki.com/fullchain.pem";
 const KEY_FNAME: &str = "/etc/letsencrypt/live/spiralwiki.com/privkey.pem";
 
+const NO_CACHE_PATHS: [&str; 2] = ["/setup", "/query"];
+
 struct ServerState<'a> {
     params: &'a Params,
     db: AlignedMemory64,
@@ -161,10 +164,22 @@ async fn main() -> std::io::Result<()> {
             .service(setup)
             .service(query)
             .service(fs::Files::new("/", "../client/static").index_file("index.html"))
+            .wrap_fn(|req, srv | {
+                let should_cache = !NO_CACHE_PATHS.contains(&req.path());
+                let fut = srv.call(req);
+                async move {
+                    let mut res = fut.await?;
+                    if should_cache {
+                        res.headers_mut()
+                            .insert(http::header::CACHE_CONTROL, http::header::HeaderValue::from_static("private, max-age=31536000"));
+                    }
+                    Ok(res)
+                }
+            })
     };
 
     Server::build()
-        .bind("http/1", "0.0.0.0:8088", move || {
+        .bind("http/1", "0.0.0.0:443", move || {
             let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
             builder
                 .set_private_key_file(KEY_FNAME, SslFiletype::PEM)