paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

paywall.en.must (5320B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3   <head>
      4     <title>Payment Required</title>
      5     <meta http-equiv="content-type" content="text/html;CHARSET=utf-8">
      6     <!-- FIXME: probably should serve this ourselves... -->
      7     <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"
      8             integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA=="
      9             crossorigin="anonymous" referrerpolicy="no-referrer"></script>
     10   </head>
     11   <body>
     12     <h1>Payment Required</h1>
     13     <p>Scan the QR code or follow the link below with your
     14        <a href="https://wallet.taler.net">GNU Taler</a> wallet.</p>
     15 
     16     <div id="qrcode"></div>
     17     <p><a id="talerlink" href="#">Generating payment link...</a></p>
     18 
     19     <script>
     20     (async () => {
     21 
     22       function waitMs(ms) {
     23         return new Promise(resolve => setTimeout(resolve, ms));
     24       }
     25 
     26       // Note: good like this ONLY for short inputs...
     27       function toBase64Url(bytes) {
     28         let binary = String.fromCharCode(...bytes);
     29         return btoa(binary)
     30          .replace(/\+/g, '-')
     31          .replace(/\//g, '_')
     32          .replace(/=+$/, ''); // remove padding
     33       }
     34 
     35       async function sha256b64(str) {
     36         const buf = await crypto.subtle.digest(
     37           'SHA-256', new TextEncoder().encode(str));
     38         return toBase64Url(new Uint8Array(buf));
     39       }
     40 
     41       async function makePaivanaId(curTime, nonce, website) {
     42         const hash = await sha256b64(nonce + website + String(curTime));
     43         return `${curTime}-${hash}`;
     44       }
     45 
     46       const href = window.location.href;
     47       // This is a policy decision: we include the query parameters
     48       // in the website to support sites that need them, say if
     49       // "?page=42" is being used instead of "/pages/42".
     50       const website = href.split('#')[0];
     51       const metaMerchant = '{{ merchant_backend }}';
     52       const metaTemplate = '{{ template_id }}';
     53       const maxPickupDelay = {{ max_pickup_delay }}; // in seconds
     54       // cap at 100 years, we don't deal well with 'forever' otherwise
     55       const usePickupDelay = Math.min(maxPickupDelay, 60*60*24*356*100);
     56 
     57       // Strip trailing slash so we can append /templateId cleanly.
     58       const merchantBase  = metaMerchant.replace(/\/$/, '');
     59       const merchantUrl   = new URL(merchantBase);
     60       const merchantProto = merchantUrl.protocol;
     61       const suffix        = merchantProto === 'http:' ? '+http' : '';
     62       const merchantHost  = merchantUrl.host;
     63       const merchantPath  = merchantUrl.path;
     64 
     65       const expTime  = Math.floor(Date.now() / 1000) + maxPickupDelay;
     66       const nonceArr = new Uint8Array(32);
     67       crypto.getRandomValues(nonceArr);
     68       const nonce = Array.from(nonceArr)
     69                       .map(b => b.toString(16).padStart(2, '0')).join('');
     70 
     71       const paivanaId = await makePaivanaId(expTime, nonce, website);
     72 
     73       const talerUri = [
     74         `taler${suffix}://pay-template/`,
     75         merchantHost,
     76         merchantPath,
     77         `/`,
     78         metaTemplate,
     79         `?session_id=${encodeURIComponent(paivanaId)}`,
     80         `&fulfillment_url=${encodeURIComponent(href)}`
     81       ].join('');
     82 
     83       const talerLink = document.getElementById('talerlink');
     84       talerLink.href        = talerUri;
     85       talerLink.textContent = talerUri;
     86 
     87       new QRCode(document.getElementById('qrcode'), {
     88         text:         talerUri,
     89         width:        200,
     90         height:       200,
     91         correctLevel: QRCode.CorrectLevel.M
     92       });
     93 
     94       async function confirmPayment(orderId) {
     95         talerLink.textContent = 'Payment confirmed! Loading page...';
     96         talerLink.href = '#';
     97         try {
     98           const res = await fetch(`${website}/.well-known/paivana`, {
     99             method:  'POST',
    100             headers: { 'Content-Type': 'application/json' },
    101             body:    JSON.stringify({ order_id: orderId, nonce, cur_time: expTime, website }),
    102           });
    103           const dest = res.redirected ? res.url : website;
    104           // FIXME: 400 just for testing...
    105           await waitMs(400);
    106           location.href = dest;
    107         } catch (e) {
    108           console.warn('[paivana] confirm error:', e);
    109           talerLink.href = '#';
    110           talerLink.textContent = 'Could not reach the server!';
    111         }
    112       }
    113 
    114       const pollBase = `${website}/.well-known/paivana/sessions/`
    115                      + encodeURIComponent(paivanaId);
    116       while (true) {
    117         const start = performance.now();
    118         try {
    119           const res = await fetch(`${pollBase}?timeout_ms=30000`, { cache: 'no-store' });
    120           if (res.status === 200) {
    121             let orderId = null;
    122             try   { orderId = (await res.json()).order_id ?? null; } catch (_) {}
    123             if (orderId) {
    124               await confirmPayment(orderId);
    125             } else {
    126               // FIXME: for testing...
    127               await waitMs(400);
    128               location.href = website;
    129             }
    130             return;
    131           }
    132         } catch (e) {
    133           console.warn('[paivana] poll error:', e);
    134           talerLink.href = '#';
    135           talerLink.textContent = 'Network error! Retrying...';
    136         }
    137         const remMs = Math.round(30_000 - (performance.now() - start));
    138         if (remMs > 0) await waitMs(remMs);
    139       }
    140 
    141     })();
    142     </script>
    143   </body>
    144 </html>