about summary refs log tree commit diff
path: root/bskylink
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2025-02-24 13:59:57 -0800
committerGitHub <noreply@github.com>2025-02-24 13:59:57 -0800
commit61a14043e51475b64c5c505dd10d81a0165bb3f2 (patch)
tree5b6deda9cface0b2d4a04a61f59c2bc7e9b588eb /bskylink
parent3fc4c32ac62403269340c40ad529bfa67cd0a35a (diff)
downloadvoidsky-61a14043e51475b64c5c505dd10d81a0165bb3f2.tar.zst
add to blink (#7788)
Diffstat (limited to 'bskylink')
-rw-r--r--bskylink/src/routes/createShortLink.ts (renamed from bskylink/src/routes/create.ts)0
-rw-r--r--bskylink/src/routes/index.ts8
-rw-r--r--bskylink/src/routes/redirect.ts55
-rw-r--r--bskylink/src/routes/shortLink.ts54
4 files changed, 83 insertions, 34 deletions
diff --git a/bskylink/src/routes/create.ts b/bskylink/src/routes/createShortLink.ts
index db7c3f809..db7c3f809 100644
--- a/bskylink/src/routes/create.ts
+++ b/bskylink/src/routes/createShortLink.ts
diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts
index f60b99bcb..cfdaf3eaa 100644
--- a/bskylink/src/routes/index.ts
+++ b/bskylink/src/routes/index.ts
@@ -1,9 +1,10 @@
 import {Express} from 'express'
 
 import {AppContext} from '../context.js'
-import {default as create} from './create.js'
+import {default as createShortLink} from './createShortLink.js'
 import {default as health} from './health.js'
 import {default as redirect} from './redirect.js'
+import {default as shortLink} from './shortLink.js'
 import {default as siteAssociation} from './siteAssociation.js'
 
 export * from './util.js'
@@ -11,7 +12,8 @@ export * from './util.js'
 export default function (ctx: AppContext, app: Express) {
   app = health(ctx, app) // GET /_health
   app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association
-  app = create(ctx, app) // POST /link
-  app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching)
+  app = redirect(ctx, app) // GET /redirect
+  app = createShortLink(ctx, app) // POST /link
+  app = shortLink(ctx, app) // GET /:linkId (should go last due to permissive matching)
   return app
 }
diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts
index 276aae1ca..4e7052af7 100644
--- a/bskylink/src/routes/redirect.ts
+++ b/bskylink/src/routes/redirect.ts
@@ -6,47 +6,40 @@ import {Express} from 'express'
 import {AppContext} from '../context.js'
 import {handler} from './util.js'
 
+const INTERNAL_IP_REGEX = new RegExp(
+  '(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost',
+  'i',
+)
+
 export default function (ctx: AppContext, app: Express) {
   return app.get(
-    '/:linkId',
+    '/redirect',
     handler(async (req, res) => {
-      const linkId = req.params.linkId
-      const contentType = req.accepts(['html', 'json'])
+      let link = req.query.u
       assert(
-        typeof linkId === 'string',
-        'express guarantees id parameter is a string',
+        typeof link === 'string',
+        'express guarantees link query parameter is a string',
       )
-      const found = await ctx.db.db
-        .selectFrom('link')
-        .selectAll()
-        .where('id', '=', linkId)
-        .executeTakeFirst()
-      if (!found) {
-        // potentially broken or mistyped link
+      link = decodeURIComponent(link)
+
+      let url: URL | undefined
+      try {
+        url = new URL(link)
+      } catch {}
+
+      if (
+        !url ||
+        (url.protocol !== 'http:' && url.protocol !== 'https:') || // is a http(s) url
+        (ctx.cfg.service.hostnames.includes(url.hostname.toLowerCase()) &&
+          url.pathname === '/redirect') || // is a redirect loop
+        INTERNAL_IP_REGEX.test(url.hostname) // isn't directing to an internal location
+      ) {
         res.setHeader('Cache-Control', 'no-store')
-        if (contentType === 'json') {
-          return res
-            .status(404)
-            .json({
-              error: 'NotFound',
-              message: 'Link not found',
-            })
-            .end()
-        }
-        // send the user to the app
         res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
         return res.status(302).end()
       }
-      // build url from original url in order to preserve query params
-      const url = new URL(
-        req.originalUrl,
-        `https://${ctx.cfg.service.appHostname}`,
-      )
-      url.pathname = found.path
+
       res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
-      if (contentType === 'json') {
-        return res.json({url: url.href}).end()
-      }
       res.setHeader('Location', url.href)
       return res.status(301).end()
     }),
diff --git a/bskylink/src/routes/shortLink.ts b/bskylink/src/routes/shortLink.ts
new file mode 100644
index 000000000..276aae1ca
--- /dev/null
+++ b/bskylink/src/routes/shortLink.ts
@@ -0,0 +1,54 @@
+import assert from 'node:assert'
+
+import {DAY, SECOND} from '@atproto/common'
+import {Express} from 'express'
+
+import {AppContext} from '../context.js'
+import {handler} from './util.js'
+
+export default function (ctx: AppContext, app: Express) {
+  return app.get(
+    '/:linkId',
+    handler(async (req, res) => {
+      const linkId = req.params.linkId
+      const contentType = req.accepts(['html', 'json'])
+      assert(
+        typeof linkId === 'string',
+        'express guarantees id parameter is a string',
+      )
+      const found = await ctx.db.db
+        .selectFrom('link')
+        .selectAll()
+        .where('id', '=', linkId)
+        .executeTakeFirst()
+      if (!found) {
+        // potentially broken or mistyped link
+        res.setHeader('Cache-Control', 'no-store')
+        if (contentType === 'json') {
+          return res
+            .status(404)
+            .json({
+              error: 'NotFound',
+              message: 'Link not found',
+            })
+            .end()
+        }
+        // send the user to the app
+        res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
+        return res.status(302).end()
+      }
+      // build url from original url in order to preserve query params
+      const url = new URL(
+        req.originalUrl,
+        `https://${ctx.cfg.service.appHostname}`,
+      )
+      url.pathname = found.path
+      res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
+      if (contentType === 'json') {
+        return res.json({url: url.href}).end()
+      }
+      res.setHeader('Location', url.href)
+      return res.status(301).end()
+    }),
+  )
+}