lib_test_fileserver.js

// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict'

const path = require('node:path')
const url = require('node:url')

const express = require('express')
const multer = require('multer')
const serveIndex = require('serve-index')

const { isDevMode } = require('./build')
const resources = require('./resources')
const { Server } = require('./httpserver')

const WEB_ROOT = '/common'
const DATA_ROOT = '/data'
const JS_ROOT = '/javascript'

const baseDirectory = resources.locate('common/src/web')
const dataDirectory = path.join(__dirname, 'data')
const jsDirectory = resources.locate('javascript')

const Pages = (function () {
  let pages = {}

  function addPage(page, path) {
    pages.__defineGetter__(page, function () {
      return exports.whereIs(path)
    })
  }

  addPage('ajaxyPage', 'ajaxy_page.html')
  addPage('alertsPage', 'alerts.html')
  addPage('basicAuth', 'basicAuth')
  addPage('blankPage', 'blank.html')
  addPage('bodyTypingPage', 'bodyTypingTest.html')
  addPage('booleanAttributes', 'booleanAttributes.html')
  addPage('childPage', 'child/childPage.html')
  addPage('chinesePage', 'cn-test.html')
  addPage('clickJacker', 'click_jacker.html')
  addPage('clickEventPage', 'clickEventPage.html')
  addPage('clicksPage', 'clicks.html')
  addPage('colorPage', 'colorPage.html')
  addPage('deletingFrame', 'deletingFrame.htm')
  addPage('draggableLists', 'draggableLists.html')
  addPage('dragAndDropPage', 'dragAndDropTest.html')
  addPage('droppableItems', 'droppableItems.html')
  addPage('documentWrite', 'document_write_in_onload.html')
  addPage('dynamicallyModifiedPage', 'dynamicallyModifiedPage.html')
  addPage('dynamicPage', 'dynamic.html')
  addPage('echoPage', 'echo')
  addPage('errorsPage', 'errors.html')
  addPage('xhtmlFormPage', 'xhtmlFormPage.xhtml')
  addPage('formPage', 'formPage.html')
  addPage('formSelectionPage', 'formSelectionPage.html')
  addPage('framesetPage', 'frameset.html')
  addPage('grandchildPage', 'child/grandchild/grandchildPage.html')
  addPage('html5Page', 'html5Page.html')
  addPage('html5OfflinePage', 'html5/offline.html')
  addPage('iframePage', 'iframes.html')
  addPage('javascriptEnhancedForm', 'javascriptEnhancedForm.html')
  addPage('javascriptPage', 'javascriptPage.html')
  addPage('linkedImage', 'linked_image.html')
  addPage('longContentPage', 'longContentPage.html')
  addPage('macbethPage', 'macbeth.html')
  addPage('mapVisibilityPage', 'map_visibility.html')
  addPage('metaRedirectPage', 'meta-redirect.html')
  addPage('missedJsReferencePage', 'missedJsReference.html')
  addPage('mouseTrackerPage', 'mousePositionTracker.html')
  addPage('nestedPage', 'nestedElements.html')
  addPage('readOnlyPage', 'readOnlyPage.html')
  addPage('rectanglesPage', 'rectangles.html')
  addPage('relativeLocators', 'relative_locators.html')
  addPage('redirectPage', 'redirect')
  addPage('resultPage', 'resultPage.html')
  addPage('richTextPage', 'rich_text.html')
  addPage('printPage', 'printPage.html')
  addPage('scrollingPage', 'scrollingPage.html')
  addPage('selectableItemsPage', 'selectableItems.html')
  addPage('selectPage', 'selectPage.html')
  addPage('selectSpacePage', 'select_space.html')
  addPage('simpleTestPage', 'simpleTest.html')
  addPage('simpleXmlDocument', 'simple.xml')
  addPage('sleepingPage', 'sleep')
  addPage('slowIframes', 'slow_loading_iframes.html')
  addPage('slowLoadingAlertPage', 'slowLoadingAlert.html')
  addPage('svgPage', 'svgPiechart.xhtml')
  addPage('tables', 'tables.html')
  addPage('underscorePage', 'underscore.html')
  addPage('unicodeLtrPage', 'utf8/unicode_ltr.html')
  addPage('uploadPage', 'upload.html')
  addPage('veryLargeCanvas', 'veryLargeCanvas.html')
  addPage('webComponents', 'webComponents.html')
  addPage('xhtmlTestPage', 'xhtmlTest.html')
  addPage('uploadInvisibleTestPage', 'upload_invisible.html')
  addPage('userpromptPage', 'userprompt.html')
  addPage('virtualAuthenticator', 'virtual-authenticator.html')
  addPage('logEntryAdded', 'bidi/logEntryAdded.html')
  addPage('scriptTestAccessProperty', 'bidi/scriptTestAccessProperty.html')
  addPage('scriptTestRemoveProperty', 'bidi/scriptTestRemoveProperty.html')
  addPage('emptyPage', 'bidi/emptyPage.html')
  addPage('emptyText', 'bidi/emptyText.txt')
  addPage('redirectedHttpEquiv', 'bidi/redirected_http_equiv.html')
  addPage('releaseAction', 'bidi/release_action.html')

  return pages
})()

const Path = {
  BASIC_AUTH: WEB_ROOT + '/basicAuth',
  ECHO: WEB_ROOT + '/echo',
  GENERATED: WEB_ROOT + '/generated',
  MANIFEST: WEB_ROOT + '/manifest',
  REDIRECT: WEB_ROOT + '/redirect',
  PAGE: WEB_ROOT + '/page',
  SLEEP: WEB_ROOT + '/sleep',
  UPLOAD: WEB_ROOT + '/upload',
}

var app = express()

app
  .get('/', sendIndex)
  .get('/favicon.ico', function (_req, res) {
    res.writeHead(204)
    res.end()
  })
  .use(JS_ROOT, serveIndex(jsDirectory), express.static(jsDirectory))
  .post(Path.UPLOAD, handleUpload)
  .use(WEB_ROOT, serveIndex(baseDirectory), express.static(baseDirectory))
  .use(DATA_ROOT, serveIndex(dataDirectory), express.static(dataDirectory))
  .get(Path.ECHO, sendEcho)
  .get(Path.PAGE, sendInifinitePage)
  .get(Path.PAGE + '/*', sendInifinitePage)
  .get(Path.REDIRECT, redirectToResultPage)
  .get(Path.SLEEP, sendDelayedResponse)
  .get(Path.BASIC_AUTH, sendBasicAuth)

if (isDevMode()) {
  var closureDir = resources.locate('third_party/closure/goog')
  app.use('/third_party/closure/goog', serveIndex(closureDir), express.static(closureDir))
}
var server = new Server(app)

function redirectToResultPage(_, response) {
  response.writeHead(303, {
    Location: Pages.resultPage,
  })
  return response.end()
}

function sendInifinitePage(request, response) {
  // eslint-disable-next-line n/no-deprecated-api
  var pathname = url.parse(request.url).pathname
  var lastIndex = pathname.lastIndexOf('/')
  var pageNumber = lastIndex == -1 ? 'Unknown' : pathname.substring(lastIndex + 1)
  var body = [
    '<!DOCTYPE html>',
    '<title>Page',
    pageNumber,
    '</title>',
    'Page number <span id="pageNumber">',
    pageNumber,
    '</span>',
    '<p><a href="../xhtmlTest.html" target="_top">top</a>',
  ].join('')
  response.writeHead(200, {
    'Content-Length': Buffer.byteLength(body, 'utf8'),
    'Content-Type': 'text/html; charset=utf-8',
  })
  response.end(body)
}

function sendBasicAuth(request, response) {
  const denyAccess = function () {
    response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="test"' })
    response.end('Access denied')
  }

  const basicAuthRegExp = /^\s*basic\s+([a-z0-9\-._~+/]+)=*\s*$/i
  const auth = request.headers.authorization
  const match = basicAuthRegExp.exec(auth || '')
  if (!match) {
    denyAccess()
    return
  }

  const userNameAndPass = Buffer.from(match[1], 'base64').toString()
  const parts = userNameAndPass.split(':', 2)
  if (parts[0] !== 'genie' || parts[1] !== 'bottle') {
    denyAccess()
    return
  }

  response.writeHead(200, { 'content-type': 'text/plain' })
  response.end('Access granted!')
}

function sendDelayedResponse(request, response) {
  var duration = 0
  // eslint-disable-next-line n/no-deprecated-api
  var query = url.parse(request.url).search.substr(1) || ''
  var match = query.match(/\btime=(\d+)/)
  if (match) {
    duration = parseInt(match[1], 10)
  }

  setTimeout(function () {
    var body = ['<!DOCTYPE html>', '<title>Done</title>', '<body>Slept for ', duration, 's</body>'].join('')
    response.writeHead(200, {
      'Content-Length': Buffer.byteLength(body, 'utf8'),
      'Content-Type': 'text/html; charset=utf-8',
      'Cache-Control': 'no-cache',
      Pragma: 'no-cache',
      Expires: 0,
    })
    response.end(body)
  }, duration * 1000)
}

function handleUpload(request, response) {
  let upload = multer({ storage: multer.memoryStorage() }).any()
  upload(request, response, function (err) {
    if (err) {
      response.writeHead(500)
      response.end(err + '')
    } else {
      if (!request.files) {
        return response.status(400).send('No files were uploaded')
      }

      let files = []
      let keys = Object.keys(request.files)

      keys.forEach((file) => {
        files.push(request.files[file].originalname)
      })

      response
        .status(200)
        .contentType('html')
        .send(files.join('\n') + '\n<script>window.top.window.onUploadDone();</script>')
    }
  })
}

function sendEcho(request, response) {
  if (request.query['html']) {
    const html = request.query['html']
    if (html) {
      response.writeHead(200, {
        'Content-Length': Buffer.byteLength(html, 'utf8'),
        'Content-Type': 'text/html; charset=utf-8',
      })
      response.end(html)
      return
    }
  }

  var body = [
    '<!DOCTYPE html>',
    '<title>Echo</title>',
    '<div class="request">',
    request.method,
    ' ',
    request.url,
    ' ',
    'HTTP/',
    request.httpVersion,
    '</div>',
  ]
  for (var name in request.headers) {
    body.push('<div class="header ', name, '">', name, ': ', request.headers[name], '</div>')
  }
  body = body.join('')
  response.writeHead(200, {
    'Content-Length': Buffer.byteLength(body, 'utf8'),
    'Content-Type': 'text/html; charset=utf-8',
  })
  response.end(body)
}

/**
 * Responds to a request for the file server's main index.
 * @param {!http.ServerRequest} request The request object.
 * @param {!http.ServerResponse} response The response object.
 */
function sendIndex(request, response) {
  // eslint-disable-next-line n/no-deprecated-api
  var pathname = url.parse(request.url).pathname

  var host = request.headers.host
  if (!host) {
    host = server.host()
  }

  var requestUrl = ['http://' + host + pathname].join('')

  function createListEntry(path) {
    var url = requestUrl + path
    return ['<li><a href="', url, '">', path, '</a>'].join('')
  }

  var data = ['<!DOCTYPE html><h1>/</h1><hr/><ul>', createListEntry('common'), createListEntry('data')]
  if (isDevMode()) {
    data.push(createListEntry('javascript'))
  }
  data.push('</ul>')
  data = data.join('')

  response.writeHead(200, {
    'Content-Type': 'text/html; charset=UTF-8',
    'Content-Length': Buffer.byteLength(data, 'utf8'),
  })
  response.end(data)
}

// PUBLIC application

/**
 * Starts the server on the specified port.
 * @param {number=} opt_port The port to use, or 0 for any free port.
 * @return {!Promise<Host>} A promise that will resolve
 *     with the server host when it has fully started.
 */
exports.start = server.start.bind(server)

/**
 * Stops the server.
 * @return {!Promise} A promise that will resolve when the
 *     server has closed all connections.
 */
exports.stop = server.stop.bind(server)

/**
 * Formats a URL for this server.
 * @param {string=} opt_pathname The desired pathname on the server.
 * @return {string} The formatted URL.
 * @throws {Error} If the server is not running.
 */
exports.url = server.url.bind(server)

/**
 * Builds the URL for a file in the //common/src/web directory of the
 * Selenium client.
 * @param {string} filePath A path relative to //common/src/web to compute a
 *     URL for.
 * @return {string} The formatted URL.
 * @throws {Error} If the server is not running.
 */
exports.whereIs = function (filePath) {
  filePath = filePath.replace(/\\/g, '/')
  if (!filePath.startsWith('/')) {
    filePath = `${WEB_ROOT}/${filePath}`
  }
  return server.url(filePath)
}

exports.Pages = Pages

if (require.main === module) {
  server.start(2310).then(function () {
    console.log('Server running at ' + server.url())
  })
}