gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-wallet-webex] branch master updated: always decode '


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated: always decode 'Taler-' headers
Date: Sat, 24 Aug 2019 19:31:32 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-webex.

The following commit(s) were added to refs/heads/master by this push:
     new 3247a45d always decode 'Taler-' headers
3247a45d is described below

commit 3247a45d9709d787ac56e3148a0c8edc9a5e30a3
Author: Florian Dold <address@hidden>
AuthorDate: Sat Aug 24 19:31:24 2019 +0200

    always decode 'Taler-' headers
---
 src/webex/wxBackend.ts | 300 ++++++++++++++++++++++++++++++++-----------------
 1 file changed, 195 insertions(+), 105 deletions(-)

diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index b9c0db87..69ba8bad 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -20,7 +20,6 @@
  * logic as possible.
  */
 
-
 /**
  * Imports.
  */
@@ -36,20 +35,14 @@ import {
   ReturnCoinsRequest,
 } from "../walletTypes";
 
-import {
-  Wallet,
-} from "../wallet";
+import { Wallet } from "../wallet";
 
 import { isFirefox } from "./compat";
 
-import {
-  PurchaseRecord,
-  WALLET_DB_VERSION,
-} from "../dbTypes";
+import { PurchaseRecord, WALLET_DB_VERSION } from "../dbTypes";
 
 import { openTalerDb, exportDb, importDb, deleteDb } from "../db";
 
-
 import { ChromeBadge } from "./chromeBadge";
 import { MessageType } from "./messages";
 import * as wxApi from "./wxApi";
@@ -62,8 +55,11 @@ import { BrowserCryptoWorkerFactory } from 
"../crypto/cryptoApi";
 
 const NeedsWallet = Symbol("NeedsWallet");
 
-function handleMessage(sender: MessageSender,
-                       type: MessageType, detail: any): any {
+function handleMessage(
+  sender: MessageSender,
+  type: MessageType,
+  detail: any,
+): any {
   function assertNotFound(t: never): never {
     console.error(`Request type ${t as string} unknown`);
     console.error(`Request detail was ${detail}`);
@@ -133,7 +129,10 @@ function handleMessage(sender: MessageSender,
       if (typeof detail.contractTermsHash !== "string") {
         throw Error("contractTermsHash must be a string");
       }
-      return needsWallet().submitPay(detail.contractTermsHash, 
detail.sessionId);
+      return needsWallet().submitPay(
+        detail.contractTermsHash,
+        detail.sessionId,
+      );
     }
     case "check-pay": {
       if (typeof detail.proposalId !== "number") {
@@ -172,9 +171,11 @@ function handleMessage(sender: MessageSender,
       if (!detail.contract) {
         return Promise.resolve({ error: "contract missing" });
       }
-      return needsWallet().hashContract(detail.contract).then((hash) => {
-        return hash;
-      });
+      return needsWallet()
+        .hashContract(detail.contract)
+        .then(hash => {
+          return hash;
+        });
     }
     case "reserve-creation-info": {
       if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
@@ -269,8 +270,10 @@ function handleMessage(sender: MessageSender,
       return resp;
     }
     case "log-and-display-error":
-      logging.storeReport(detail).then((reportUid) => {
-        const url = 
chrome.extension.getURL(`/src/webex/pages/error.html?reportUid=${reportUid}`);
+      logging.storeReport(detail).then(reportUid => {
+        const url = chrome.extension.getURL(
+          `/src/webex/pages/error.html?reportUid=${reportUid}`,
+        );
         if (detail.sameTab && sender && sender.tab && sender.tab.id) {
           chrome.tabs.update(detail.tabId, { url });
         } else {
@@ -343,7 +346,11 @@ function handleMessage(sender: MessageSender,
   }
 }
 
-async function dispatch(req: any, sender: any, sendResponse: any): 
Promise<void> {
+async function dispatch(
+  req: any,
+  sender: any,
+  sendResponse: any,
+): Promise<void> {
   try {
     const p = handleMessage(sender, req.type, req.detail);
     const r = await p;
@@ -375,12 +382,11 @@ async function dispatch(req: any, sender: any, 
sendResponse: any): Promise<void>
   }
 }
 
-
 class ChromeNotifier implements Notifier {
   private ports: Port[] = [];
 
   constructor() {
-    chrome.runtime.onConnect.addListener((port) => {
+    chrome.runtime.onConnect.addListener(port => {
       console.log("got connect!");
       this.ports.push(port);
       port.onDisconnect.addListener(() => {
@@ -401,8 +407,11 @@ class ChromeNotifier implements Notifier {
   }
 }
 
-
-async function talerPay(fields: any, url: string, tabId: number): 
Promise<string | undefined> {
+async function talerPay(
+  fields: any,
+  url: string,
+  tabId: number,
+): Promise<string | undefined> {
   if (!currentWallet) {
     console.log("can't handle payment, no wallet");
     return undefined;
@@ -422,13 +431,18 @@ async function talerPay(fields: any, url: string, tabId: 
number): Promise<string
   if (fields.resource_url) {
     const p = await w.queryPaymentByFulfillmentUrl(fields.resource_url);
     console.log("query for resource url", fields.resource_url, "result", p);
-    if (p && (fields.session_id === undefined || fields.session_id === 
p.lastSessionId)) {
+    if (
+      p &&
+      (fields.session_id === undefined || fields.session_id === 
p.lastSessionId)
+    ) {
       return goToPayment(p);
     }
   }
   if (fields.contract_url) {
     const proposalId = await w.downloadProposal(fields.contract_url);
-    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
+    const uri = new URI(
+      chrome.extension.getURL("/src/webex/pages/confirm-contract.html"),
+    );
     if (fields.session_id) {
       uri.addSearch("sessionId", fields.session_id);
     }
@@ -441,7 +455,9 @@ async function talerPay(fields: any, url: string, tabId: 
number): Promise<string
   }
   if (fields.refund_url) {
     console.log("processing refund");
-    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/refund.html"));
+    const uri = new URI(
+      chrome.extension.getURL("/src/webex/pages/refund.html"),
+    );
     return uri.query({ refundUrl: fields.refund_url }).href();
   }
   if (fields.tip) {
@@ -451,14 +467,12 @@ async function talerPay(fields: any, url: string, tabId: 
number): Promise<string
   return undefined;
 }
 
-
 function getTab(tabId: number): Promise<chrome.tabs.Tab> {
   return new Promise((resolve, reject) => {
     chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
   });
 }
 
-
 function setBadgeText(options: chrome.browserAction.BadgeTextDetails) {
   // not supported by all browsers ...
   if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) {
@@ -468,18 +482,20 @@ function setBadgeText(options: 
chrome.browserAction.BadgeTextDetails) {
   }
 }
 
-
 function waitMs(timeoutMs: number): Promise<void> {
   return new Promise((resolve, reject) => {
-      chrome.extension.getBackgroundPage()!.setTimeout(() => resolve(), 
timeoutMs);
+    chrome.extension
+      .getBackgroundPage()!
+      .setTimeout(() => resolve(), timeoutMs);
   });
 }
 
-
-function makeSyncWalletRedirect(url: string,
-                                tabId: number,
-                                oldUrl: string,
-                                params?: {[name: string]: string | 
undefined}): object {
+function makeSyncWalletRedirect(
+  url: string,
+  tabId: number,
+  oldUrl: string,
+  params?: { [name: string]: string | undefined },
+): object {
   const innerUrl = new URI(chrome.extension.getURL("/src/webex/pages/" + url));
   if (params) {
     for (const key in params) {
@@ -488,12 +504,14 @@ function makeSyncWalletRedirect(url: string,
       }
     }
   }
-  const outerUrl = new 
URI(chrome.extension.getURL("/src/webex/pages/redirect.html"));
+  const outerUrl = new URI(
+    chrome.extension.getURL("/src/webex/pages/redirect.html"),
+  );
   outerUrl.addSearch("url", innerUrl);
   if (isFirefox()) {
     // Some platforms don't support the sync redirect (yet), so fall back to
     // async redirect after a timeout.
-    const doit = async() => {
+    const doit = async () => {
       await waitMs(150);
       const tab = await getTab(tabId);
       if (tab.url === oldUrl) {
@@ -505,14 +523,17 @@ function makeSyncWalletRedirect(url: string,
   return { redirectUrl: outerUrl.href() };
 }
 
-
 /**
  * Handle a HTTP response that has the "402 Payment Required" status.
  * In this callback we don't have access to the body, and must communicate via
  * shared state with the content script that will later be run later
  * in this tab.
  */
-function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: 
string, tabId: number): any {
+function handleHttpPayment(
+  headerList: chrome.webRequest.HttpHeader[],
+  url: string,
+  tabId: number,
+): any {
   if (!currentWallet) {
     console.log("can't handle payment, no wallet");
     return;
@@ -525,16 +546,30 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
     }
   }
 
+  const decodeIfDefined = (url?: string) =>
+    url ? decodeURIComponent(url) : undefined;
+
   const fields = {
-    contract_url: headers["x-taler-contract-url"] || 
headers["taler-contract-url"],
-    offer_url: headers["x-taler-offer-url"] || headers["taler-offer-url"],
-    refund_url: headers["x-taler-refund-url"] || headers["taler-refund-url"],
-    resource_url: headers["x-taler-resource-url"] || 
headers["taler-resource-url"],
-    session_id: headers["x-taler-session-id"] || headers["taler-session-id"],
-    tip: headers["x-taler-tip"] || headers["taler-tip"],
+    contract_url: decodeIfDefined(
+      headers["x-taler-contract-url"] || headers["taler-contract-url"],
+    ),
+    offer_url: decodeIfDefined(
+      headers["x-taler-offer-url"] || headers["taler-offer-url"],
+    ),
+    refund_url: decodeIfDefined(
+      headers["x-taler-refund-url"] || headers["taler-refund-url"],
+    ),
+    resource_url: decodeIfDefined(
+      headers["x-taler-resource-url"] || headers["taler-resource-url"],
+    ),
+    session_id: decodeIfDefined(
+      headers["x-taler-session-id"] || headers["taler-session-id"],
+    ),
+    tip: decodeIfDefined(headers["x-taler-tip"] || headers["taler-tip"]),
   };
 
-  const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as 
any)[x]).length !== 0;
+  const talerHeaderFound =
+    Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0;
 
   if (!talerHeaderFound) {
     // looks like it's not a taler request, it might be
@@ -548,7 +583,11 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
   // Synchronous fast path for existing payment
   if (fields.resource_url) {
     const result = 
currentWallet.getNextUrlFromResourceUrl(fields.resource_url);
-    if (result && (fields.session_id === undefined || fields.session_id === 
result.lastSessionId)) {
+    if (
+      result &&
+      (fields.session_id === undefined ||
+        fields.session_id === result.lastSessionId)
+    ) {
       return { redirectUrl: result.nextUrl };
     }
   }
@@ -563,23 +602,29 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
 
   // Synchronous fast path for tip
   if (fields.tip) {
-    return makeSyncWalletRedirect("tip.html", tabId, url, { tip_token: 
fields.tip });
+    return makeSyncWalletRedirect("tip.html", tabId, url, {
+      tip_token: fields.tip,
+    });
   }
 
   // Synchronous fast path for refund
   if (fields.refund_url) {
     console.log("processing refund");
-    return makeSyncWalletRedirect("refund.html", tabId, url, { refundUrl: 
fields.refund_url });
+    return makeSyncWalletRedirect("refund.html", tabId, url, {
+      refundUrl: fields.refund_url,
+    });
   }
 
   // We need to do some asynchronous operation, we can't directly redirect
-  talerPay(fields, url, tabId).then((nextUrl) => {
+  talerPay(fields, url, tabId).then(nextUrl => {
     if (nextUrl) {
       // We use chrome.tabs.executeScript instead of chrome.tabs.update
       // because the latter is buggy when it does not execute in the same
       // (micro-?)task as the header callback.
       chrome.tabs.executeScript({
-        code: `document.location.href = 
decodeURIComponent("${encodeURI(nextUrl)}");`,
+        code: `document.location.href = decodeURIComponent("${encodeURI(
+          nextUrl,
+        )}");`,
         runAt: "document_start",
       });
     }
@@ -588,9 +633,12 @@ function handleHttpPayment(headerList: 
chrome.webRequest.HttpHeader[], url: stri
   return;
 }
 
-
-function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHeader[],
-                           url: string, tabId: number): any {
+function handleBankRequest(
+  wallet: Wallet,
+  headerList: chrome.webRequest.HttpHeader[],
+  url: string,
+  tabId: number,
+): any {
   const headers: { [s: string]: string } = {};
   for (const kv of headerList) {
     if (kv.value) {
@@ -609,9 +657,11 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
     const reservePub = headers["x-taler-reserve-pub"];
     if (reservePub !== undefined) {
       console.log(`confirming reserve ${reservePub} via 201`);
-      wallet.confirmReserve({reservePub});
+      wallet.confirmReserve({ reservePub });
     } else {
-      console.warn("got 'X-Taler-Operation: confirm-reserve' without 
'X-Taler-Reserve-Pub'");
+      console.warn(
+        "got 'X-Taler-Operation: confirm-reserve' without 
'X-Taler-Reserve-Pub'",
+      );
     }
     return;
   }
@@ -622,7 +672,8 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
       console.log("202 not understood (X-Taler-Amount missing)");
       return;
     }
-    const callbackUrl = headers["x-taler-callback-url"] || 
headers["taler-callback-url"];
+    const callbackUrl =
+      headers["x-taler-callback-url"] || headers["taler-callback-url"];
     if (!callbackUrl) {
       console.log("202 not understood (X-Taler-Callback-Url missing)");
       return;
@@ -630,13 +681,15 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
     try {
       JSON.parse(amount);
     } catch (e) {
-      const errUri = new 
URI(chrome.extension.getURL("/src/webex/pages/error.html"));
+      const errUri = new URI(
+        chrome.extension.getURL("/src/webex/pages/error.html"),
+      );
       const p = {
         message: `Can't parse amount ("${amount}"): ${e.message}`,
       };
       const errRedirectUrl = errUri.query(p).href();
       // FIXME: use direct redirect when 
https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
-      chrome.tabs.update(tabId, {url: errRedirectUrl});
+      chrome.tabs.update(tabId, { url: errRedirectUrl });
       return;
     }
     const wtTypes = headers["x-taler-wt-types"] || headers["taler-wt-types"];
@@ -647,12 +700,17 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
     const params = {
       amount,
       bank_url: url,
-      callback_url: new URI(callbackUrl) .absoluteTo(url),
-      sender_wire: headers["x-taler-sender-wire"] || 
headers["taler-sender-wire"],
-      suggested_exchange_url: headers["x-taler-suggested-exchange"] || 
headers["taler-suggested-exchange"],
+      callback_url: new URI(callbackUrl).absoluteTo(url),
+      sender_wire:
+        headers["x-taler-sender-wire"] || headers["taler-sender-wire"],
+      suggested_exchange_url:
+        headers["x-taler-suggested-exchange"] ||
+        headers["taler-suggested-exchange"],
       wt_types: wtTypes,
     };
-    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"));
+    const uri = new URI(
+      chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"),
+    );
     const redirectUrl = uri.query(params).href();
     console.log("redirecting to", redirectUrl);
     // FIXME: use direct redirect when 
https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
@@ -663,7 +721,6 @@ function handleBankRequest(wallet: Wallet, headerList: 
chrome.webRequest.HttpHea
   console.log("Ignoring unknown (X-)Taler-Operation:", operation);
 }
 
-
 // Rate limit cache for executePayment operations, to break redirect loops
 let rateLimitCache: { [n: number]: number } = {};
 
@@ -671,30 +728,26 @@ function clearRateLimitCache() {
   rateLimitCache = {};
 }
 
-
 /**
  * Currently active wallet instance.  Might be unloaded and
  * re-instantiated when the database is reset.
  */
-let currentWallet: Wallet|undefined;
+let currentWallet: Wallet | undefined;
 
 /**
  * Last version if an outdated DB, if applicable.
  */
-let oldDbVersion: number|undefined;
+let oldDbVersion: number | undefined;
 
 function handleUpgradeUnsupported(oldDbVersion: number, newDbVersion: number) {
   console.log("DB migration not supported");
   chrome.tabs.create({
-    url: chrome.extension.getURL(
-      "/src/webex/pages/reset-required.html",
-    ),
+    url: chrome.extension.getURL("/src/webex/pages/reset-required.html"),
   });
   setBadgeText({ text: "err" });
   chrome.browserAction.setBadgeBackgroundColor({ color: "#F00" });
 }
 
-
 async function reinitWallet() {
   if (currentWallet) {
     currentWallet.stop();
@@ -712,31 +765,43 @@ async function reinitWallet() {
   const http = new BrowserHttpLib();
   const notifier = new ChromeNotifier();
   console.log("setting wallet");
-  const wallet = new Wallet(db, http, badge, notifier, new 
BrowserCryptoWorkerFactory());
+  const wallet = new Wallet(
+    db,
+    http,
+    badge,
+    notifier,
+    new BrowserCryptoWorkerFactory(),
+  );
   // Useful for debugging in the background page.
   (window as any).talerWallet = wallet;
   currentWallet = wallet;
 }
 
-
 /**
  * Inject a script into a tab.  Gracefully logs errors
  * and works around a bug where the tab's URL does not match the internal URL,
  * making the injection fail in a confusing way.
  */
-function injectScript(tabId: number, details: chrome.tabs.InjectDetails, 
actualUrl: string): void {
-  chrome.tabs.executeScript(tabId, details,  () => {
+function injectScript(
+  tabId: number,
+  details: chrome.tabs.InjectDetails,
+  actualUrl: string,
+): void {
+  chrome.tabs.executeScript(tabId, details, () => {
     // Required to squelch chrome's "unchecked lastError" warning.
     // Sometimes chrome reports the URL of a tab as http/https but
     // injection fails.  This can happen when a page is unloaded or
     // shows a "no internet" page etc.
     if (chrome.runtime.lastError) {
-      console.warn("injection failed on page", actualUrl, 
chrome.runtime.lastError.message);
+      console.warn(
+        "injection failed on page",
+        actualUrl,
+        chrome.runtime.lastError.message,
+      );
     }
   });
 }
 
-
 /**
  * Main function to run for the WebExtension backend.
  *
@@ -745,16 +810,23 @@ function injectScript(tabId: number, details: 
chrome.tabs.InjectDetails, actualU
 export async function wxMain() {
   // Explicitly unload the extension page as soon as an update is available,
   // so the update gets installed as soon as possible.
-  chrome.runtime.onUpdateAvailable.addListener((details) => {
+  chrome.runtime.onUpdateAvailable.addListener(details => {
     console.log("update available:", details);
     chrome.runtime.reload();
   });
 
   window.onerror = (m, source, lineno, colno, error) => {
-    logging.record("error", "".concat(m as any, error as any), undefined, 
source || "(unknown)", lineno || 0, colno || 0);
+    logging.record(
+      "error",
+      "".concat(m as any, error as any),
+      undefined,
+      source || "(unknown)",
+      lineno || 0,
+      colno || 0,
+    );
   };
 
-  chrome.tabs.query({}, (tabs) => {
+  chrome.tabs.query({}, tabs => {
     console.log("got tabs", tabs);
     for (const tab of tabs) {
       if (!tab.url || !tab.id) {
@@ -764,8 +836,19 @@ export async function wxMain() {
       if (uri.protocol() !== "http" && uri.protocol() !== "https") {
         continue;
       }
-      console.log("injecting into existing tab", tab.id, "with url", 
uri.href(), "protocol", uri.protocol());
-      injectScript(tab.id, { file: "/dist/contentScript-bundle.js", runAt: 
"document_start" }, uri.href());
+      console.log(
+        "injecting into existing tab",
+        tab.id,
+        "with url",
+        uri.href(),
+        "protocol",
+        uri.protocol(),
+      );
+      injectScript(
+        tab.id,
+        { file: "/dist/contentScript-bundle.js", runAt: "document_start" },
+        uri.href(),
+      );
       const code = `
         if (("taler" in window) || 
document.documentElement.getAttribute("data-taler-nojs")) {
           document.dispatchEvent(new Event("taler-probe-result"));
@@ -775,7 +858,7 @@ export async function wxMain() {
     }
   });
 
-  const tabTimers: {[n: number]: number[]} = {};
+  const tabTimers: { [n: number]: number[] } = {};
 
   chrome.tabs.onRemoved.addListener((tabId, changeInfo) => {
     const tt = tabTimers[tabId] || [];
@@ -796,7 +879,7 @@ export async function wxMain() {
 
     const run = () => {
       timers.shift();
-      chrome.tabs.get(tabId, (tab) => {
+      chrome.tabs.get(tabId, tab => {
         if (chrome.runtime.lastError) {
           return;
         }
@@ -838,38 +921,45 @@ export async function wxMain() {
     return true;
   });
 
-
   // Clear notifications both when the popop opens,
   // as well when it closes.
-  chrome.runtime.onConnect.addListener((port) => {
+  chrome.runtime.onConnect.addListener(port => {
     if (port.name === "popup") {
       if (currentWallet) {
         currentWallet.clearNotification();
       }
       port.onDisconnect.addListener(() => {
-          if (currentWallet) {
-            currentWallet.clearNotification();
-          }
+        if (currentWallet) {
+          currentWallet.clearNotification();
+        }
       });
     }
   });
 
-
   // Handlers for catching HTTP requests
-  chrome.webRequest.onHeadersReceived.addListener((details) => {
-    const wallet = currentWallet;
-    if (!wallet) {
-      console.warn("wallet not available while handling header");
-    }
-    if (details.statusCode === 402) {
-      console.log(`got 402 from ${details.url}`);
-      return handleHttpPayment(details.responseHeaders || [],
-        details.url,
-        details.tabId);
-    } else if (details.statusCode === 202) {
-      return handleBankRequest(wallet!, details.responseHeaders || [],
-        details.url,
-        details.tabId);
-    }
-  }, { urls: ["<all_urls>"] }, ["responseHeaders", "blocking"]);
+  chrome.webRequest.onHeadersReceived.addListener(
+    details => {
+      const wallet = currentWallet;
+      if (!wallet) {
+        console.warn("wallet not available while handling header");
+      }
+      if (details.statusCode === 402) {
+        console.log(`got 402 from ${details.url}`);
+        return handleHttpPayment(
+          details.responseHeaders || [],
+          details.url,
+          details.tabId,
+        );
+      } else if (details.statusCode === 202) {
+        return handleBankRequest(
+          wallet!,
+          details.responseHeaders || [],
+          details.url,
+          details.tabId,
+        );
+      }
+    },
+    { urls: ["<all_urls>"] },
+    ["responseHeaders", "blocking"],
+  );
 }

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]