haraka/haraka-tld

View on GitHub
lib/update.js

Summary

Maintainability
A
1 hr
Test Coverage
const fs = require('fs')
const fsp = require('fs/promises')
const http = require('https')
const path = require('path')

const CommentStripper = require('./comment-stripper')

const pslFile = path.resolve(__dirname, '..', 'etc', 'public-suffix-list')
// console.log(`pslFile: ${pslFile}`)

const httpOpts = {
  hostname: 'publicsuffix.org',
  path: '/list/effective_tld_names.dat',
  port: 443,
  headers: {
    'User-Agent': 'Haraka-TLD',
  },
  agent: false,
}

exports.updatePSLfile = async function () {
  const isNewer = await exports.isRemoteNewer(pslFile)
  if (!isNewer) return false // no update

  await exports.download(pslFile)
  // console.log('update completed')
  return true
}

exports.atomicWrite = async function (tmp, dest) {
  await fsp.rename(tmp, dest)
  return true
}

exports.getWritableStream = async function (filePath) {
  const ws = fs.createWriteStream(filePath)
  return ws
}

exports.download = function (dest, opts) {
  if (!dest) dest = pslFile // for tests
  const tmpFile = `${dest}.tmp`

  return new Promise((resolve, reject) => {
    // open file before attempting HTTP GET
    this.getWritableStream(tmpFile)
      .then((ws) => {
        ws.on('close', () => {
          this.atomicWrite(tmpFile, dest).then(resolve).catch(reject)
        })

        const request = http
          .get(Object.assign({}, httpOpts, opts), (res) => {
            if (res.statusCode !== 200) {
              // console.error(`HEADERS: ${JSON.stringify(res.headers)}`);
              reject(new Error(`response code ${res.statusCode} not handled!`))
              return
            }

            res.pipe(new CommentStripper()).pipe(ws)
          })
          .on('error', (e) => {
            // console.error(e)
            fs.unlink(tmpFile, () => {
              // unlikely the file exists. This callback catches the error and ignores it.
              reject(e)
            })
          })

        request.end()
      })
      .catch(reject)
  })
}

exports.getFileStats = async function (filePath) {
  if (!filePath) filePath = pslFile

  try {
    await fsp.access(filePath)

    const stats = await fsp.stat(filePath)
    if (stats.isFile()) return stats

    // console.error(`${filePath} is not a file`);
    await fsp.unlink(filePath)
    // console.log(`${filePath} deleted`);
    return null
  } catch (e) {
    // console.log(`${filePath} does not exist`);
    return null
  }
}

exports.isRemoteNewer = function (dest, opts) {
  if (!dest) dest = pslFile

  return new Promise((resolve, reject) => {
    this.getFileStats(dest)
      .then((stats) => {
        opts = Object.assign({}, httpOpts, opts, { method: 'HEAD' })
        if (stats) {
          opts.headers['If-Modified-Since'] = stats.mtime.toUTCString()
        }

        const request = http
          .request(opts, (res) => {
            switch (res.statusCode) {
              case 200:
                return resolve(true)
              case 304:
                // console.log(`${path.basename(dest)} is up-to-date`);
                return resolve(false)
              case 403:
                // console.log(`Access Denied for ${dest}`);
                return resolve(false)
              case 404:
                // console.log(`Not Found received for ${dest}`);
                return resolve(false)
              default:
                // console.log(`Unhandled status code: ${res.statusCode}`);
                // console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
                resolve(false)
            }
          })
          .on('error', reject)

        request.end()
      })
      .catch(reject)
  })
}