From 2ea64185cf148c2dd0b4c5d9bf19d30c10d1770b Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Fri, 12 Nov 2021 18:47:27 +0100
Subject: [PATCH] re introduce saneUrl

---
 deploy/auth/hbp-oidc-v2.js                    |   4 -
 deploy/auth/index.spec.js                     |   2 +-
 deploy/package-lock.json                      |  27 ++
 deploy/package.json                           |   2 +
 deploy/saneUrl/depcObjStore.js                |   4 +
 deploy/saneUrl/index.js                       |  94 ++--
 deploy/saneUrl/store.js                       | 446 +++---------------
 deploy/saneUrl/util.js                        |  57 +++
 deploy/saneUrl/util.spec.js                   | 208 ++++++++
 docs/releases/v2.5.7.md                       |   1 +
 src/state/stateAggregator.directive.ts        |  40 +-
 .../nehuba/statusCard/statusCard.component.ts |   2 -
 .../statusCard/statusCard.template.html       |   7 +-
 13 files changed, 443 insertions(+), 451 deletions(-)
 create mode 100644 deploy/saneUrl/util.js
 create mode 100644 deploy/saneUrl/util.spec.js

diff --git a/deploy/auth/hbp-oidc-v2.js b/deploy/auth/hbp-oidc-v2.js
index 102aa25c2..d54274808 100644
--- a/deploy/auth/hbp-oidc-v2.js
+++ b/deploy/auth/hbp-oidc-v2.js
@@ -78,8 +78,4 @@ module.exports = {
       console.error('oidcv2 auth error', e)
     }
   },
-  getClient: async () => {
-    await memoizedInit()
-    return client
-  }
 }
diff --git a/deploy/auth/index.spec.js b/deploy/auth/index.spec.js
index 84691ddd6..8e33c30bd 100644
--- a/deploy/auth/index.spec.js
+++ b/deploy/auth/index.spec.js
@@ -8,7 +8,7 @@ const appGetStub = sinon.stub()
 describe('auth/index.js', () => {
   before(() => {
     require.cache[require.resolve('./util')] = {
-      exports: { initPassportJs: initPassportJsStub }
+      exports: { initPassportJs: initPassportJsStub, objStoreDb: new Map() }
     }
     require.cache[require.resolve('./hbp-oidc-v2')] = {
       exports: {
diff --git a/deploy/package-lock.json b/deploy/package-lock.json
index 3a8058b86..b8399147d 100644
--- a/deploy/package-lock.json
+++ b/deploy/package-lock.json
@@ -503,6 +503,11 @@
         "wrap-ansi": "^2.0.0"
       }
     },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+    },
     "clone-response": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
@@ -695,6 +700,14 @@
         "type-detect": "^4.0.0"
       }
     },
+    "defaults": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+      "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+      "requires": {
+        "clone": "^1.0.2"
+      }
+    },
     "defer-to-connect": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz",
@@ -936,6 +949,11 @@
         }
       }
     },
+    "express-rate-limit": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
+      "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
+    },
     "express-session": {
       "version": "1.15.6",
       "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz",
@@ -2118,6 +2136,15 @@
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
       "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
     },
+    "rate-limit-redis": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-2.1.0.tgz",
+      "integrity": "sha512-6SAsTCzY0v6UCIKLOLLYqR2XzFmgdtF7jWXlSPq2FrNIZk8tZ7xwBvyGW7GFMCe5I4S9lYNdrSJ9E84rz3/CpA==",
+      "requires": {
+        "defaults": "^1.0.3",
+        "redis": "^3.0.2"
+      }
+    },
     "raw-body": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
diff --git a/deploy/package.json b/deploy/package.json
index a8b5d1488..04cdc908c 100644
--- a/deploy/package.json
+++ b/deploy/package.json
@@ -17,6 +17,7 @@
     "connect-redis": "^5.0.0",
     "cookie-parser": "^1.4.5",
     "express": "^4.16.4",
+    "express-rate-limit": "^5.5.1",
     "express-session": "^1.15.6",
     "got": "^10.5.5",
     "hbp-seafile": "^0.2.0",
@@ -26,6 +27,7 @@
     "nomiseco": "0.0.2",
     "openid-client": "^4.4.0",
     "passport": "^0.4.0",
+    "rate-limit-redis": "^2.1.0",
     "redis": "^3.1.2",
     "request": "^2.88.0",
     "showdown": "^1.9.1",
diff --git a/deploy/saneUrl/depcObjStore.js b/deploy/saneUrl/depcObjStore.js
index a46d6c845..c67876686 100644
--- a/deploy/saneUrl/depcObjStore.js
+++ b/deploy/saneUrl/depcObjStore.js
@@ -21,6 +21,10 @@ class Store {
     })
   }
 
+  async del(id){
+    // noop
+  }
+
   async set(id, val){
     throw new Error(`Object store is deprecated. Please use seafile storage instead`)
   }
diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js
index 09fe10b25..4bfcf3740 100644
--- a/deploy/saneUrl/index.js
+++ b/deploy/saneUrl/index.js
@@ -1,46 +1,31 @@
 const express = require('express')
 const router = express.Router()
-const { FallbackStore: Store, NotFoundError } = require('./store')
+const { GitlabSnippetStore: Store, NotFoundError } = require('./store')
 const { Store: DepcStore } = require('./depcObjStore')
+const RateLimit = require('express-rate-limit')
+const RedisStore = require('rate-limit-redis')
+const { redisURL } = require('../lruStore')
+const { ProxyStore, NotExactlyPromiseAny } = require('./util')
 
 const store = new Store()
 const depStore = new DepcStore()
 
-const { HOSTNAME, HOST_PATHNAME } = process.env
+const proxyStore = new ProxyStore(store)
 
-const acceptHtmlProg = /text\/html/i
-
-const getFileFromStore = async (name, store) => {
-  try {
-    const value = await store.get(name)
-    const json = JSON.parse(value)
-    const { expiry } = json
-    if ( expiry && ((Date.now() - expiry) > 0) ) {
-      return null
-    }
-  
-    return value
-  } catch (e) {
-    if (e instanceof NotFoundError) {
-      return null
-    }
-    throw e
-  }
-}
-
-const getFile = async name => {
-  const value = await getFileFromStore(name, depStore)
-    || await getFileFromStore(name, store)
+const {
+  HOSTNAME,
+  HOST_PATHNAME,
+  DISABLE_LIMITER,
+} = process.env
 
-  return value
-}
+const limiter = new RateLimit({
+  windowMs: 1e3 * 5,
+  max: 5,
+  ...( redisURL ? { store: new RedisStore({ redisURL }) } : {} )
+})
+const passthrough = (_, __, next) => next()
 
-const hardCodedMap = new Map([
-  ['bigbrainGreyWhite', '#/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:a1655b99-82f1-420f-a3c2-fe80fd4c8588/p:juelich:iav:atlas:v1.0.0:4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..gIW~.10AwC.B1KK~..1LSm'],
-  ['whs4', '#/a:minds:core:parcellationatlas:v1.0.0:522b368e-49a3-49fa-88d3-0870a307974a/t:minds:core:referencespace:v1.0.0:d5717c4a-0fa1-46e6-918c-b8003069ade8/p:minds:core:parcellationatlas:v1.0.0:ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..kxV..0.0.0..8Yu'],
-  ['allen2017', '#/a:juelich:iav:atlas:v1.0.0:2/t:minds:core:referencespace:v1.0.0:265d32a0-3d84-40a5-926f-bf89f68212b9/p:minds:core:parcellationatlas:v1.0.0:05655b58-3b6f-49db-b285-64b5a0276f83/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..kxV..0.0.0..8Yu'],
-  ['mebrains', '#/a:juelich:iav:atlas:v1.0.0:monkey/t:minds:core:referencespace:v1.0.0:MEBRAINS_T1.masked/p:minds:core:parcellationatlas:v1.0.0:mebrains-tmp-id/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..0.0.0..1LSm']
-])
+const acceptHtmlProg = /text\/html/i
 
 router.get('/:name', async (req, res) => {
   const { name } = req.params
@@ -50,30 +35,29 @@ router.get('/:name', async (req, res) => {
     
   try {
     const REAL_HOSTNAME = `${HOSTNAME}${HOST_PATHNAME || ''}/`
-    const hardcodedRedir = hardCodedMap.get(name)
-    if (hardcodedRedir) {
-      if (redirectFlag) res.redirect(`${REAL_HOSTNAME}${hardcodedRedir}`)
-      else res.status(200).send(hardcodedRedir)
-      return
-    }
-
-    const value = await getFile(name)
-    if (!value) throw new NotFoundError()
-    const json = JSON.parse(value)
-    const { queryString } = json
 
+    const json = await NotExactlyPromiseAny([
+      ProxyStore.StaticGet(depStore, req, name),
+      proxyStore.get(req, name)
+    ])
 
-    if (redirectFlag) res.redirect(`${REAL_HOSTNAME}?${queryString}`)
-    else res.status(200).send(value)
+    const { queryString, hashPath } = json
 
+    if (redirectFlag) {
+      if (queryString) return res.redirect(`${REAL_HOSTNAME}?${queryString}`)
+      if (hashPath) return res.redirect(`${REAL_HOSTNAME}#${hashPath}`)
+    } else {
+      return res.status(200).send(json)
+    }
   } catch (e) {
+    const notFoundFlag = e instanceof NotFoundError
     if (redirectFlag) {
 
       const REAL_HOSTNAME = `${HOSTNAME}${HOST_PATHNAME || ''}/`
 
       res.cookie(
         'iav-error', 
-        e instanceof NotFoundError ? `${name} 
+        notFoundFlag ? `${name} 
         
         not found` : `error while fetching ${name}.`,
         {
@@ -84,13 +68,25 @@ router.get('/:name', async (req, res) => {
       )
       return res.redirect(REAL_HOSTNAME)
     }
-    if (e instanceof NotFoundError) return res.status(404).end()
+    if (notFoundFlag) return res.status(404).end()
     else return res.status(500).send(e.toString())
   }
 })
 
 router.post('/:name',
-  (_req, res) => res.status(410).end()
+  DISABLE_LIMITER ? passthrough : limiter,
+  express.json(),
+  async (req, res) => {
+    if (req.headers['x-noop']) return res.status(200).end()
+    const { name } = req.params
+    try {
+      await proxyStore.set(req, name, req.body)
+      res.status(201).end()
+    } catch (e) {
+      console.log(e.body)
+      res.status(500).send(e.toString())
+    }
+  }
 )
 
 router.use((_, res) => {
diff --git a/deploy/saneUrl/store.js b/deploy/saneUrl/store.js
index 656a070f4..9ef35e840 100644
--- a/deploy/saneUrl/store.js
+++ b/deploy/saneUrl/store.js
@@ -1,420 +1,112 @@
-const redis = require('redis')
-const { promisify } = require('util')
-const { getClient } = require('../auth/hbp-oidc-v2')
-const { jwtDecode } = require('../auth/oidc')
 const request = require('request')
 
-const HBP_OIDC_V2_REFRESH_TOKEN_KEY = `HBP_OIDC_V2_REFRESH_TOKEN_KEY` // only insert valid refresh token. needs to be monitored to ensure always get new refresh token(s)
-const HBP_OIDC_V2_ACCESS_TOKEN_KEY = `HBP_OIDC_V2_ACCESS_TOKEN_KEY` // only insert valid access token. if expired, get new one via refresh token, then pop & push
-const HBP_OIDC_V2_UPDATE_CHAN = `HBP_OIDC_V2_UPDATE_CHAN` // stringified JSON key val of above three, with time stamp
-const HBP_OIDC_V2_UPDATE_KEY = `HBP_OIDC_V2_UPDATE_KEY`
-const HBP_DATAPROXY_READURL = `HBP_DATAPROXY_READURL`
-const HBP_DATAPROXY_WRITEURL = `HBP_DATAPROXY_WRITEURL`
+const apiPath = '/api/v4'
+const saneUrlVer = `0.0.1`
+const titlePrefix = `[saneUrl]`
 
 const {
   __DEBUG__,
-
-  HBP_V2_REFRESH_TOKEN,
-  HBP_V2_ACCESS_TOKEN,
-
-  DATA_PROXY_URL,
-  DATA_PROXY_BUCKETNAME,
-
+  GITLAB_ENDPOINT,
+  GITLAB_PROJECT_ID,
+  GITLAB_TOKEN
 } = process.env
 
 class NotFoundError extends Error{}
 
-// not part of class, since class is exported, and prototype of class can be easily changed
-// using stderr instead of console.error, as by default, logs are collected via fluentd
-// debug messages should not be kept as logs
-function log(message){
-  if (__DEBUG__) {
-    process.stderr.write(`__DEBUG__`)
-    process.stdout.write(`\n`)
-    if (typeof message === 'object') {
-      process.stderr.write(JSON.stringify(message, null, 2))
-    }
-    if (typeof message === 'number' || typeof message === 'string') {
-      process.stdout.write(message)
-    }
-    process.stdout.write(`\n`)
-  }
-}
-
-
-function _checkValid(urlString){
-  log({
-    breakpoint: '_checkValid',
-    payload: urlString
-  })
-  if (!urlString) return false
-  const url = new URL(urlString)
-  const expiry = url.searchParams.get('temp_url_expires')
-  return (new Date() - new Date(expiry)) < 1e3 * 10
-}
-
-class FallbackStore {
-  async get(){
-    throw new Error(`saneurl is currently offline for maintainence.`)
-  }
-  async set(){
-    throw new Error(`saneurl is currently offline for maintainence.`)
-  }
-  async healthCheck() {
-    return false
-  }
-}
+class NotImplemented extends Error{}
 
-class Store {
+class GitlabSnippetStore {
   constructor(){
-    throw new Error(`Not implemented`)
-
-    this.healthFlag = false
-    this.seafileHandle = null
-    this.seafileRepoId = null
-
-    /**
-     * setup redis(or mock) client
-     */
-    this.redisClient = redisURL
-      ? redis.createClient({
-          url: redisURL
-        })
-      : ({
-          onCbs: [],
-          keys: {},
-          get: async (key, cb) => {
-            await Promise.resolve()
-            cb(null, this.keys[key])
-          },
-          set: async (key, value, cb) => {
-            await Promise.resolve()
-            this.keys[key] = value
-            cb(null)
-          },
-          on(eventName, cb) {
-            if (eventName === 'message') {
-              this.onCbs.push(cb)
-            }
-          },
-          publish(channel, message){
-            for (const cb of this.onCbs){
-              cb(channel, message)
-            }
-          },
-          quit(){}
-        })
-
-    this.redisUtil = {
-      asyncGet: promisify(this.redisClient.get).bind(this.redisClient),
-      asyncSet: promisify(this.redisClient.set).bind(this.redisClient),
-    }
-
-    this.pending = {}
-    this.keys = {}
-
-    this.redisClient.on('message', async (chan, mess) => {
-      /**
-       * only liten to HBP_OIDC_V2_UPDATE_CHAN update
-       */
-      if (chan === HBP_OIDC_V2_UPDATE_CHAN) {
-        try {
-          const { pending, update } = JSON.parse(mess)
-          this.pending = pending
-          for (const key in update) {
-            try {
-              this.keys[key] = await this.redisUtil.asyncGet(key)
-              console.log('on message get key', key, this.keys[key])
-            } catch (e) {
-              console.error(`[saneUrl][store.js] get key ${key} error`)
-            }
-          }
-        } catch (e) {
-          console.error(`[saneUrl][store.js] parse message HBP_OIDC_V2_UPDATE_CHAN error`)
-        }
-      }
-    })
     
-    this.init()
-
-    /**
-     * check expiry
-     */
-    this.intervalRef = setInterval(() => {
-      this.checkExpiry()
-    }, 1000 * 60)
-  }
-
-  async init() {
-    this.keys = {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: (await this.redisUtil.asyncGet(HBP_OIDC_V2_REFRESH_TOKEN_KEY)) || HBP_V2_REFRESH_TOKEN,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: (await this.redisUtil.asyncGet(HBP_OIDC_V2_ACCESS_TOKEN_KEY)) || HBP_V2_ACCESS_TOKEN,
-      [HBP_DATAPROXY_READURL]: await this.redisUtil.asyncGet(HBP_DATAPROXY_READURL),
-      [HBP_DATAPROXY_WRITEURL]: await this.redisUtil.asyncGet(HBP_DATAPROXY_WRITEURL),
-    }
-    
-    this.healthFlag = true
-  }
-
-  async checkExpiry(){
-
-    const {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken
-    } = this.keys
-
-    log({
-      breakpoint: 'async checkExpiry',
-    })
-
-    /**
-     * if access token is absent
-     * try to refresh token, without needing to check exp
-     */
-    if (!accessToken) {
-      await this.doRefreshTokens()
-      return true
-    }
-    const { exp: refreshExp } = jwtDecode(refreshToken)
-    const { exp: accessExp } = jwtDecode(accessToken)
-
-    const now = new Date().getTime() / 1000
-
-    if (now > refreshExp) {
-      console.warn(`[saneUrl] refresh token expired... Need a new refresh token`)
-      return false
-    }
-    if (now > accessExp) {
-      console.log(`[saneUrl] access token expired. Refreshing access token...`)
-      await this.doRefreshTokens()
-      console.log(`[saneUrl] access token successfully refreshed...`)
-      return true
+    if (
+      GITLAB_ENDPOINT
+      && GITLAB_PROJECT_ID
+      && GITLAB_TOKEN
+    ) {
+      this.url = `${GITLAB_ENDPOINT}${apiPath}/projects/${GITLAB_PROJECT_ID}/snippets`
+      this.token = GITLAB_TOKEN
+      return
     }
-
-    return true
+    throw new NotImplemented('Gitlab snippet key value store cannot be configured')
   }
 
-  async doRefreshTokens(){
-
-    log({
-      breakpoint: 'doing refresh tokens'
+  _promiseRequest(...arg) {
+    return new Promise((rs, rj) => {
+      request(...arg, (err, resp, body) => {
+        if (err) return rj(err)
+        if (resp.statusCode >= 400) return rj(resp)
+        rs(body)
+      })
     })
-
-    /**
-     * first, check if another process/pod is currently updating
-     * if they are, give them 1 minute to complete the process
-     * (usually takes takes less than 5 seconds)
-     */
-    const pendingStart = this.pending[HBP_OIDC_V2_REFRESH_TOKEN_KEY] &&
-      this.pending[HBP_OIDC_V2_REFRESH_TOKEN_KEY].start + 1000 * 60
-    const now = new Date() 
-    if (pendingStart && pendingStart > now) {
-      return
-    }
-
-    /**
-     * When start refreshing the tokens, set pending attribute, and start timestamp
-     */
-    const payload = {
-      pending: {
-        ...this.pending,
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: {
-          start: Date.now()
-        }
-      }
-    }
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_UPDATE_KEY, JSON.stringify(payload))
-    this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-
-    const client = await getClient()
-    const tokenset = await client.refresh(this.keys[HBP_OIDC_V2_REFRESH_TOKEN_KEY])
-    const { access_token: accessToken, refresh_token: refreshToken } = tokenset
-
-    const { exp: accessTokenExp } = jwtDecode(accessToken)
-    const { exp: refreshTokenExp } = jwtDecode(refreshToken)
-
-    if (refreshTokenExp - accessTokenExp < 60 * 60 ) {
-      console.warn(`[saneUrl] refreshToken expires within 1 hour of access token! ${accessTokenExp} ${refreshTokenExp}`)
-    }
-
-    /**
-     * once tokens have been refreshed, set them in the redis store first
-     * Then publish the update message
-     */
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_REFRESH_TOKEN_KEY, refreshToken)
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_ACCESS_TOKEN_KEY, accessToken)
-    this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify({
-      keys: {
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-        [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken,
-      },
-      pending: {
-        ...this.pending,
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: null
-      }
-    }))
-    this.keys = {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken,
-    }
   }
 
-  async _getTmpUrl(permission){
-    if (permission !== 'read' && permission !== 'write') throw new Error(`permission need to be either read or write!`)
-    const method = permission === 'read'
-      ? 'GET'
-      : 'PUT'
-    log({
-      breakpoint: 'access key',
-      access: this.keys[HBP_OIDC_V2_ACCESS_TOKEN_KEY]
-    })
-    const { url } = await new Promise((rs, rj) => {
-      const payload = {
+  _request({ addPath = '', method = 'GET', headers = {}, opt = {} } = {}) {
+    return new Promise((rs, rj) => {
+      request(`${this.url}${addPath}`, {
         method,
-        uri: `${DATA_PROXY_URL}/tempurl/${DATA_PROXY_BUCKETNAME}?lifetime=very_long`,
         headers: {
-          'Authorization': `Bearer ${this.keys[HBP_OIDC_V2_ACCESS_TOKEN_KEY]}`
-        }
-      }
-      log({
-        breakpoint: '_getTmpUrl',
-        payload
-      })
-      request(payload, (err, resp, body) => {
+          'PRIVATE-TOKEN': this.token,
+          ...headers
+        },
+        ...opt
+      }, (err, resp, body) => {
         if (err) return rj(err)
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode === 200) {
-          rs(JSON.parse(body))
-          return
-        }
-        return rj(new Error(`[saneurl] [get] unknown error`))
+        if (resp.statusCode >= 400) return rj(resp)
+        return rs(body)
       })
     })
-
-    return url
-  }
-
-  _getTmplTag(key, _strings, _proxyUrl, bucketName, id){
-    const defaultReturn = `${_strings[0]}${_proxyUrl}${_strings[1]}${bucketName}${_strings[2]}${id}`
-    if (!key) return defaultReturn
-    const {
-      [key]: urlOfInterest
-    } = this.keys
-    if (!urlOfInterest) return defaultReturn
-    const url = new URL(urlOfInterest)
-    url.pathname += id
-    return url
-  }
-
-  _getWriteTmplTag(...args) {
-    return this._getTmplTag(HBP_DATAPROXY_WRITEURL, ...args)
-  }
-
-  _getReadTmplTag(...args){
-    return this._getTmplTag(HBP_DATAPROXY_READURL, ...args)
   }
 
   async get(id) {
-    log({
-      breakpoint: 'async get',
-      id
-    })
-    const {
-      [HBP_DATAPROXY_READURL]: readUrl
-    } = this.keys
-
-    if (!_checkValid(readUrl)) {
-      log({
-        breakpoint: 'read url not valid, getting new url'
-      })
-      const url = await this._getTmpUrl('read')
-      const payload = {
-        keys: {
-          [HBP_DATAPROXY_READURL]: url
-        }
-      }
-      this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-      this.redisUtil.asyncSet(HBP_DATAPROXY_READURL, url)
-      this.keys[HBP_DATAPROXY_READURL] = url
+    const list = JSON.parse(await this._request())
+    const found = list.find(item => item.title === `${titlePrefix}${id}`)
+    if (!found) throw new NotFoundError()
+    const payloadObj = found.files.find(f => f.path === 'payload')
+    if (!payloadObj) {
+      console.error(`id found, but payload not found... this is strange. Check id: ${id}`)
+      throw new NotFoundError()
     }
-
-    return await new Promise((rs, rj) => {
-      request({
-        method: 'GET',
-        uri: this._getReadTmplTag`${DATA_PROXY_URL}/buckets/${DATA_PROXY_BUCKETNAME}/${encodeURIComponent(id)}`,
-      }, (err, resp, body) => {
-        if (err) return rj(err)
-        if (resp.statusCode === 404) return rj(new NotFoundError())
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode === 200) return rs(body)
-        return rj(new Error(`[saneurl] [get] unknown error`))
-      })
-    })
+    if (!payloadObj.raw_url) {
+      throw new Error(`payloadObj.raw_url not found!`)
+    }
+    return await this._promiseRequest(payloadObj.raw_url)
   }
 
-  async _set(id, value) {
-    const {
-      [HBP_DATAPROXY_WRITEURL]: writeUrl
-    } = this.keys
-
-    if (!_checkValid(writeUrl)) {
-      log({
-        breakpoint: '_set',
-        message: 'write url not valid, getting new one'
-      })
-      const url = await this._getTmpUrl('write')
-      const payload = {
-        keys: {
-          [HBP_DATAPROXY_WRITEURL]: url
+  async set(id, value) {
+    return await this._request({
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      opt: {
+        json: {
+          title: `${titlePrefix}${id}`,
+          description: `Created programmatically. v${saneUrlVer}`,
+          visibility: 'public',
+          files: [{
+            file_path: 'payload',
+            content: value
+          }]
         }
       }
-      this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-      this.redisUtil.asyncSet(HBP_DATAPROXY_WRITEURL, url)
-      this.keys[HBP_DATAPROXY_WRITEURL] = url
-      log({
-        breakpoint: '_set',
-        message: 'got new write url'
-      })
-    }
-
-    await new Promise((rs, rj) => {
-      const payload = {
-        method: 'PUT',
-        uri: this._getWriteTmplTag`${DATA_PROXY_URL}/buckets/${DATA_PROXY_BUCKETNAME}/${encodeURIComponent(id)}`,
-        headers: {
-          'content-type': 'text/plain; charset=utf-8'
-        },
-        body: value
-      }
-      log({
-        breakpoint: 'pre set',
-        payload
-      })
-      request(payload, (err, resp, body) => {
-        if (err) return rj(err)
-        if (resp.statusCode === 404) return rj(new NotFoundError())
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode >= 200 && resp.statusCode < 300) return rs(body)
-        return rj(new Error(`[saneurl] [get] unknown error`))
-      })
     })
   }
 
-  async set(id, value) {
-    const result = await this._set(id, value)
-    return result
+  async del(id) {
+    return await this._request({
+      addPath: `/${id}`,
+      method: 'DELETE',
+    })
   }
 
-  dispose(){
-    clearInterval(this.intervalRef)
-    this.redisClient && this.redisClient.quit()
+  async dispose(){
+    
   }
 
   async healthCheck(){
-    return this.healthFlag
+    return true
   }
 }
 
-exports.FallbackStore = FallbackStore
-exports.Store = Store
+exports.GitlabSnippetStore = GitlabSnippetStore
 exports.NotFoundError = NotFoundError
diff --git a/deploy/saneUrl/util.js b/deploy/saneUrl/util.js
new file mode 100644
index 000000000..249ec333d
--- /dev/null
+++ b/deploy/saneUrl/util.js
@@ -0,0 +1,57 @@
+const { NotFoundError } = require('./store')
+
+class ProxyStore {
+  static async StaticGet(store, req, name) {
+    const payload = JSON.parse(await store.get(name))
+    const { expiry, value, ...rest } = payload
+    if (expiry && (Date.now() > expiry)) {
+      await store.del(name)
+      throw new NotFoundError('Expired')
+    }
+    // backwards compatibility for depcObjStore .
+    // when depcObjStore is fully removed, || rest can also be removed
+    return value || rest
+  }
+
+  constructor(store){
+    this.store = store
+  }
+  async get(req, name) {
+    return await ProxyStore.StaticGet(this.store, req, name)
+  }
+
+  async set(req, name, value) {
+    const supplementary = req.user
+    ? {
+        userId: req.user.id,
+        expiry: null
+      }
+    : {
+        userId: null,
+        expiry: Date.now() + 1000 * 60 * 60 * 72
+      }
+    
+    const fullPayload = {
+      value,
+      ...supplementary
+    }
+    return await this.store.set(name, JSON.stringify(fullPayload))
+  }
+}
+
+const NotExactlyPromiseAny = async arg => {
+  const errs = []
+  for (const pr of arg) {
+    try{
+      return await pr
+    } catch (e) {
+      errs.push(e)
+    }
+  }
+  throw new NotFoundError(errs)
+}
+
+module.exports = {
+  ProxyStore,
+  NotExactlyPromiseAny
+}
diff --git a/deploy/saneUrl/util.spec.js b/deploy/saneUrl/util.spec.js
new file mode 100644
index 000000000..0645c3291
--- /dev/null
+++ b/deploy/saneUrl/util.spec.js
@@ -0,0 +1,208 @@
+const { ProxyStore, NotExactlyPromiseAny } = require('./util')
+const { expect, assert } = require('chai')
+const sinon = require('sinon')
+const { NotFoundError } = require('./store')
+
+const _name = 'foo-bar'
+const _stringValue = 'hello world'
+const _objValue = {
+  'foo': 'bar'
+}
+const _req = {}
+
+describe('> saneUrl/util.js', () => {
+  describe('> ProxyStore', () => {
+    let store = {
+      set: sinon.stub(),
+      get: sinon.stub(),
+      del: sinon.stub(),
+    }
+    beforeEach(() => {
+      store.set.returns(Promise.resolve())
+      store.get.returns(Promise.resolve('{}'))
+      store.del.returns(Promise.resolve())
+    })
+    afterEach(() => {
+      store.set.resetHistory()
+      store.get.resetHistory()
+      store.del.resetHistory()
+    })
+    describe('> StaticGet', () => {
+      it('> should call store.get', () => {
+        ProxyStore.StaticGet(store, _req, _name)
+        assert(store.get.called, 'called')
+        assert(store.get.calledWith(_name), 'called with right param')
+      })
+      describe('> if not hit', () => {
+        const err = new NotFoundError('not found')
+        beforeEach(() => {
+          store.get.returns(
+            Promise.reject(err)
+          )
+        })
+        it('should throw same error', async () => {
+          try{
+            await ProxyStore.StaticGet(store, _req, _name)
+            assert(false, 'Should throw')
+          } catch (e) {
+            assert(e instanceof NotFoundError)
+          }
+        })
+      })
+      describe('> if hit', () => {
+        describe('> if expired', () => {
+          beforeEach(() => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  expiry: Date.now() - 1000
+                })
+              )
+            )
+          })
+          it('> should call store.del', async () => {
+            try {
+              await ProxyStore.StaticGet(store, _req, _name)
+            } catch (e) {
+
+            }
+            assert(store.del.called, 'store.del should be called')
+          })
+          it('> should throw NotFoundError', async () => {
+            try {
+              await ProxyStore.StaticGet(store, _req, _name)
+              assert(false, 'expect throw')
+            } catch (e) {
+              assert(e instanceof NotFoundError, 'throws NotFoundError')
+            }
+          })
+        })
+        describe('> if not expired', () => {
+          it('> should return .value, if exists', async () => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  value: _objValue,
+                  others: _stringValue
+                })
+              )
+            )
+            const returnObj = await ProxyStore.StaticGet(store, _req, _name)
+            expect(returnObj).to.deep.equal(_objValue)
+          })
+          it('> should return ...rest if .value does not exist', async () => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  others: _stringValue
+                })
+              )
+            )
+            const returnObj = await ProxyStore.StaticGet(store, _req, _name)
+            expect(returnObj).to.deep.equal({
+              others: _stringValue
+            })
+          })
+        })
+      })
+
+    })
+
+    describe('> get', () => {
+      let staticGetStub
+      beforeEach(() => {
+        staticGetStub = sinon.stub(ProxyStore, 'StaticGet')
+        staticGetStub.returns(Promise.resolve())
+      })
+      afterEach(() => {
+        staticGetStub.restore()
+      })
+      it('> proxies calls to StaticGet', async () => {
+        const store = {}
+        const proxyStore = new ProxyStore(store)
+        await proxyStore.get(_req, _name)
+        assert(staticGetStub.called)
+        assert(staticGetStub.calledWith(store, _req, _name))
+      })
+    })
+
+    describe('> set', () => {
+      let proxyStore
+      beforeEach(() => {
+        proxyStore = new ProxyStore(store)
+        store.set.returns(Promise.resolve())
+      })
+      
+      describe('> no user', () => {
+        it('> sets expiry some time in the future', async () => {
+          await proxyStore.set(_req, _name, _objValue)
+          assert(store.set.called)
+
+          const [ name, stringifiedJson ] = store.set.args[0]
+          assert(name === _name, 'name is correct')
+          const payload = JSON.parse(stringifiedJson)
+          expect(payload.value).to.deep.equal(_objValue, 'payload is correct')
+          assert(!!payload.expiry, 'expiry exists')
+          assert((payload.expiry - Date.now()) > 1000 * 60 * 60 * 24, 'expiry is at least 24 hrs in the future')
+
+          assert(!payload.userId, 'userId does not exist')
+        })
+      })
+      describe('> yes user', () => {
+        let __req = {
+          ..._req,
+          user: {
+            id: 'foo-bar-2'
+          }
+        }
+        it('> does not set expiry, but sets userId', async () => {
+          await proxyStore.set(__req, _name, _objValue)
+          assert(store.set.called)
+
+          const [ name, stringifiedJson ] = store.set.args[0]
+          assert(name === _name, 'name is correct')
+          const payload = JSON.parse(stringifiedJson)
+          expect(payload.value).to.deep.equal(_objValue, 'payload is correct')
+          assert(!payload.expiry, 'expiry does not exist')
+
+          assert(payload.userId, 'userId exists')
+          assert(payload.userId === 'foo-bar-2', 'user-id matches')
+        })
+      })
+    })
+  })
+
+  describe('> NotExactlyPromiseAny', () => {
+    describe('> nothing resolves', () => {
+      it('> throws not found error', async () => {
+        try {
+          await NotExactlyPromiseAny([
+            (async () => {
+              throw new Error(`not here`)
+            })(),
+            new Promise((rs, rj) => setTimeout(rj, 100)),
+            Promise.reject('uhoh')
+          ])
+          assert(false, 'expected to throw')
+        } catch (e) {
+          assert(e instanceof NotFoundError, 'expect to throw not found error')
+        }
+      })
+    })
+    describe('> something resolves', () => {
+      it('> returns the first in list order', async () => {
+        try {
+
+          const result = await NotExactlyPromiseAny([
+            Promise.reject('uhoh'),
+            new Promise(rs => setTimeout(() => rs('hello world'), 100)),
+            Promise.resolve('foo-bar')
+          ])
+          assert(result == 'hello world', 'expecting first in list to resolve successfully')
+        } catch (e) {
+          assert(false, 'not expecting to throw')
+        }
+      })
+    })
+  })
+})
diff --git a/docs/releases/v2.5.7.md b/docs/releases/v2.5.7.md
index 4a8698429..f043d7801 100644
--- a/docs/releases/v2.5.7.md
+++ b/docs/releases/v2.5.7.md
@@ -10,6 +10,7 @@
 ## Feature
 
 - Add menu to change perspective orientation by coronal/sagittal/axial views.
+- re-introduced saneUrl
 
 ## Under the hood
 
diff --git a/src/state/stateAggregator.directive.ts b/src/state/stateAggregator.directive.ts
index 62d666662..28df9bf19 100644
--- a/src/state/stateAggregator.directive.ts
+++ b/src/state/stateAggregator.directive.ts
@@ -1,14 +1,14 @@
-import { Directive } from "@angular/core";
+import { Directive, OnDestroy } from "@angular/core";
 import { NavigationEnd, Router } from "@angular/router";
-import { Observable } from "rxjs";
-import { filter, map } from "rxjs/operators";
+import { Observable, Subscription } from "rxjs";
+import { filter, map, startWith } from "rxjs/operators";
 
 const jsonVersion = '1.0.0'
 // ver 0.0.1 === query param
 
 interface IJsonifiedState {
   ver: string
-  queryString: any
+  hashPath: string
 }
 
 @Directive({
@@ -16,20 +16,30 @@ interface IJsonifiedState {
   exportAs: 'iavStateAggregator'
 })
 
-export class StateAggregator{
+export class StateAggregator implements OnDestroy{
 
-  public jsonifiedSstate$: Observable<IJsonifiedState>
+  public jsonifiedState: IJsonifiedState
+  public jsonifiedState$: Observable<IJsonifiedState> = this.router.events.pipe(
+    filter(ev => ev instanceof NavigationEnd),
+    map((ev: NavigationEnd) => ev.urlAfterRedirects),
+    startWith(this.router.url),
+    map((path: string) => {
+      return {
+        ver: jsonVersion,
+        hashPath: path
+      }
+    }),
+  )
   constructor(
-    router: Router
+    private router: Router
   ){
-    this.jsonifiedSstate$ = router.events.pipe(
-      filter(ev => ev instanceof NavigationEnd),
-      map((ev: NavigationEnd) => {
-        return {
-          ver: jsonVersion,
-          queryString: ev.urlAfterRedirects
-        }
-      })
+    this.subscriptions.push(
+      this.jsonifiedState$.subscribe(val => this.jsonifiedState = val)
     )
   }
+
+  private subscriptions: Subscription[] = []
+  ngOnDestroy(){
+    while (this.subscriptions.length) this.subscriptions.pop().unsubscribe()
+  }
 }
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
index 76dfa6bbb..a6daa233d 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts
+++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
@@ -57,8 +57,6 @@ export class StatusCardComponent implements OnInit, OnChanges{
     order: 6,
   }
 
-  public saneUrlDeprecated = `Custom URL is going away. New custom URLs can no longer be created. Custom URLs you generated in the past will continue to work.`
-
   public SHARE_BTN_ARIA_LABEL = ARIA_LABELS.SHARE_BTN
   public COPY_URL_TO_CLIPBOARD_ARIA_LABEL = ARIA_LABELS.SHARE_COPY_URL_CLIPBOARD
   public SHARE_CUSTOM_URL_ARIA_LABEL = ARIA_LABELS.SHARE_CUSTOM_URL
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html
index 5f57d9719..098ea21df 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.template.html
+++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html
@@ -157,8 +157,9 @@
     <mat-list-item
       [attr.aria-label]="SHARE_CUSTOM_URL_ARIA_LABEL"
       [attr.tab-index]="10"
-      [matTooltip]="saneUrlDeprecated"
-      class="text-muted">
+      (click)="openDialog(shareSaneUrl, { ariaLabel: SHARE_CUSTOM_URL_ARIA_LABEL })"
+      [matTooltip]="SHARE_CUSTOM_URL_ARIA_LABEL"
+      >
       <mat-icon
         class="mr-4"
         fontSet="fas"
@@ -212,7 +213,7 @@
     </div>
 
     <iav-sane-url iav-state-aggregator
-      [stateTobeSaved]="stateAggregator.jsonifiedSstate$ | async"
+      [stateTobeSaved]="stateAggregator.jsonifiedState$ | async"
       #stateAggregator="iavStateAggregator">
     </iav-sane-url>
   </div>
-- 
GitLab