Most efficient way to bypass Meteor for simple HTML pages

Howdy all,

TL;DR - What’s the best way to serve an HTML template without impacting my website’s Meteor functionality?


Background:

I’ve created a simple HTML landing page product on my platform: SubmitHub

Unfortunately, a few nefarious actors have taken advantage of these, and in the latest incident, over 45 million requests were sent within a few minutes.

Unsurprisingly, this brought all my containers down.

@philippeoliveira did an excellent job helping bring everything back online, but I’m wondering if there’s a more efficient way of handling these types of requests that don’t actually need any of the Meteor functionality.

Current approach:

Here’s how I handle the requests:

const cookieParser = require('cookie-parser');
const Bottleneck = require('bottleneck')
const limiter = new Bottleneck({
    minTime: 200, // minimum time between requests
    maxConcurrent: 1, // only process one request at a time
    highWater: 50, // maximum queue size
    strategy: Bottleneck.strategy.BLOCK // blocks excess requests beyond highWater
})
WebApp.connectHandlers.use(cookieParser())
WebApp.connectHandlers.use('/', function(req, res, next) {
    const host = req.headers.host
    const redirect = (href) => {
        res.writeHead(301,{
            'Location':href,
            'Cache-Control':'private, no-cache, no-store, must-revalidate',
            'Expires':'-1',
            'Pragma':'no-cache',
        })
    }
    if (req.url.startsWith('/link/')) {
        limiter.schedule(
            Meteor.bindEnvironment(() => {
                const html = linksRoute(req)
                res.statusCode = 200
                res.end(html)
            })
        ).catch(err => {
            // Send 429 if queue limit is exceeded
            res.writeHead(429, { 'Content-Type': 'text/plain' })
            res.end('Too Many Requests')
        })
    } else {
    ... // code continues

Then inside the linksRoute function, I’m getting my data like this (simplified version):

linksRoute = function(req,id) {
   const cache = getLinksCache(id)
   let link = false
   if (cache) {
        link = cache
   } else {
        link = Links.findOne({id})
        if (link) setLinksCache(id,link)
   }
   let html = Assets.get('link.html')
   html = html.replace('title_placeholder',link.title)
   return html
}

To reduce hits on Mongodb, I cache the result of link:

const linksCache = {}                                                                                                                                        
// 600,000 ms = 10 minutes                                                                                                                                   
const LINKS_CACHE_TTL = 600000                                                                                                                               
                                                                                                                                                             
getLinksCache = function(id) {                                                                                                                               
    return linksCache[id] ? linksCache[id].data : null                                                                                                       
}                                                                                                                                                            
                                                                                                                                                             
setLinksCache = function(id, data) {                                                                                                                         
    linksCache[id] = { data, timestamp: Date.now() }                                                                                                         
}                                                                                                                                                            
                                                                                                                                                             
// cache gets invalidated based on TTL

Question:

Is there a more efficient way to serve these kinds of requests to avoid my main Meteor app from getting wrecked when someone tries to spam me?

Many thanks for your time!

Option 1:
Reverse proxy using nginx. Nginx serves the html files directly and pass the rest of connections to meteor.

Option 2:
CDN cache to serve the html

1 Like

Thanks for the suggestions. I’m not actually sure that’s possible if I’m using Galaxy?

You can contact Meteor support. Option 2 should be possible with any host.

Also ask about any firewall settings. Hundreds of request should trigger some blocking strategy and should never reach millions

@jasongrishkoff what I would do but I don’t know if this works for your flow.
Many platforms keep the landing page separated from the app page such as example.com and app.example.com.

You could have your example.com as a pure HTML page hosted with Netlify and have all your actions (buttons, links) directed to app.example.com hosted with Galaxy. In order to prevent direct access to app.example.com, you could only accept directs from example.com. Any direct entry on app.example.com should direct to example.com unless some conditions are met such as the url has a token that is being validated by your app.

There is a way to determine a probable real human click on a button by checking the event.isTrusted on the click event so that only humans can be directed from example.com to app.example.com

1 Like

Hey, @jasongrishkoff!

You can use a CDN without any problems. If you need something more advanced, our custom plans offer many options. We’ll be in touch soon!

Thanks for the response @fredmaiaarantes. Really impressed with the attention your team has given this!

I’m already using a CDN for the static assets being used here (Cloudfront), but the HTML that I pull from Assets needs to be manipulated server-side to replace certain variables that change every time the page loads (unique identifiers that get passed to Meta for ads).

I’ve come up with a solution in the interim: as this product was intended for people to promote music links (eg, Spotify, Apple Music, YouTube Music), I am now restricting the content that can be linked such that only certain platforms are allowed. This has so far stopped at least 3 malicious actors from attempting to use the product to promote spam websites (such as the Vietnamese gambling site that caused all these initial issues).

I hope this approach means we can run safely within the existing infrastructure!

Jason

1 Like