Skip to content
Snippets Groups Projects
index.js 4.95 KiB
Newer Older
  • Learn to ignore specific revisions
  • Remi Rampin's avatar
    Remi Rampin committed
    const express = require('express');
    
    const cookie = require('cookie');
    
    Remi Rampin's avatar
    Remi Rampin committed
    const { auth, requiresAuth } = require('express-openid-connect');
    
    const fs = require('fs');
    
    Remi Rampin's avatar
    Remi Rampin committed
    const http = require('http');
    
    Remi Rampin's avatar
    Remi Rampin committed
    const https = require('https');
    
    const httpProxy = require('http-proxy');
    
    Remi Rampin's avatar
    Remi Rampin committed
    
    const PORT = 3000;
    
    const app = express();
    
    const config = {
    
      authRequired: false,
    
    Remi Rampin's avatar
    Remi Rampin committed
      auth0Logout: true,
      baseURL: process.env.BASE_URL,
      secret: process.env.SECRET,
      clientID: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
      issuerBaseURL: process.env.ISSUER_BASE_URL,
      routes: {
        callback: '/oauth2/callback',
        login: '/oauth2/login',
        logout: '/oauth2/logout',
        postLogoutRedirect: '/',
      },
      authorizationParams: {
        scope: 'openid profile email',
        response_type: 'code',
      },
    };
    
    
    const ACCESS_FILE = process.env.ACCESS_FILE;
    
    function listsDiffer(a, b) {
      if(a.length != b.length) {
        return true;
      }
      for(let i = 0; i < a.length; ++i) {
        if(a[i] != b[i]) {
          return true;
        }
      }
      return false;
    }
    
    let allowedUsers = [];
    
    let bypassTokens = [];
    
    function loadAllowedUsers() {
      fs.readFile(ACCESS_FILE, 'utf8', (err, data) => {
        if(err) {
          console.error(err);
          process.exit(1);
        }
    
        // Load the list
        const loadedUsers = [];
    
        const loadedTokens = [];
    
        for(let line of data.split('\n')) {
          line = line.trim();
          if(line.length > 0 && line[0] != '#') {
    
            if(line[0] == '%') {
              loadedTokens.push(line.substring(1));
            } else {
              loadedUsers.push(line);
            }
    
          }
        }
    
        // Log if the list has changed
    
        if(listsDiffer(allowedUsers, loadedUsers)
        || listsDiffer(bypassTokens, loadedTokens)) {
    
          console.log('Loaded new users list');
        }
    
        // Update global and reset timer
        allowedUsers = loadedUsers;
    
        bypassTokens = loadedTokens;
    
        setTimeout(loadAllowedUsers, 30000);
      });
    }
    loadAllowedUsers();
    
    
    PUBLIC_REGEX = process.env.PUBLIC_REGEX;
    if(PUBLIC_REGEX) {
    
    Remi Rampin's avatar
    Remi Rampin committed
      PUBLIC_REGEX = new RegExp('^(?:' + PUBLIC_REGEX + ')');
    
    Remi Rampin's avatar
    Remi Rampin committed
    const upstream_url = new URL(process.env.UPSTREAM);
    
    Remi Rampin's avatar
    Remi Rampin committed
    let UPSTREAM_PROTO, UPSTREAM_PORT;
    if(upstream_url.protocol === 'http:') {
      UPSTREAM_PROTO = http;
      UPSTREAM_PORT = 80;
    } else if(upstream_url.protocol === 'https:') {
      UPSTREAM_PROTO = https;
      UPSTREAM_PORT = 443;
    } else {
      console.error('Invalid UPSTREAM: protocol should be http or https');
    
    Remi Rampin's avatar
    Remi Rampin committed
      process.exit(1);
    }
    if(
      (upstream_url.pathname !== '/' && upstream_url.pathname !== '')
      || upstream_url.search
      || upstream_url.hash
      || upstream_url.username
      || upstream_url.password
    ) {
      console.error('Invalid UPSTREAM: path is set');
      process.exit(1);
    }
    
    const UPSTREAM_HOST = upstream_url.hostname;
    
    Remi Rampin's avatar
    Remi Rampin committed
    if(upstream_url.port) {
      UPSTREAM_PORT = parseInt(upstream_url.port);
    }
    console.log(`Using upstream ${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
    
    Remi Rampin's avatar
    Remi Rampin committed
    
    
    // Create proxy
    const proxy = httpProxy.createProxyServer({target: upstream_url});
    
    proxy.on('error', function(err, req, res) {
      res.writeHead(503, {'Content-type': 'text/plain'});
      res.end('Upstream server is unavailable');
    });
    
    
    Remi Rampin's avatar
    Remi Rampin committed
    app.use(auth(config));
    
    
    Remi Rampin's avatar
    Remi Rampin committed
    function accessCheck(req, res) {
      // Check token header
      if(req.headers['x-oidc-proxy-bypass']) {
    
        // Token is set, check it
        if(bypassTokens.indexOf(req.headers['x-oidc-proxy-bypass']) === -1) {
          // Bad token
          res.sendStatus(403);
    
    Remi Rampin's avatar
    Remi Rampin committed
          return false;
    
    Remi Rampin's avatar
    Remi Rampin committed
        return true;
      }
    
    
      // Check cookie
      let cookies = req.headers['cookie'];
      if(cookies) {
        cookies = cookie.parse(cookies);
      } else {
        cookies = {};
      }
      if(cookies['oidc-proxy-bypass']) {
        // Cookie is set, check it against tokens
        if(bypassTokens.indexOf(cookies['oidc-proxy-bypass']) === -1) {
          // Bad token, remove cookie
          res.clearCookie('oidc-proxy-bypass');
        } else {
          return true;
        }
      }
    
    
    Remi Rampin's avatar
    Remi Rampin committed
      if(!req.oidc.isAuthenticated()) {
    
        // Not authenticated, send login form or error
        if(req.accepts('html')) {
    
    Remi Rampin's avatar
    Remi Rampin committed
          res.oidc.login();
          return false;
    
        } else {
          res.sendStatus(403);
    
    Remi Rampin's avatar
    Remi Rampin committed
          return false;
    
    Remi Rampin's avatar
    Remi Rampin committed
      }
    
      // Logged in, validate user
      if(allowedUsers.indexOf(req.oidc.user.sub) == -1) {
    
        // Unauthorized user, reject
    
        res.status(403);
        res.setHeader('content-type', 'text/html');
        let whereTo = 'the documentation';
        if(process.env.DOCUMENTATION_URL) {
          whereTo = `<a href="${process.env.DOCUMENTATION_URL}">${whereTo}</a>`;
        }
        res.send(`\
    <!DOCTYPE html>
    <html>
    <head><title>Forbidden</title></head>
    <body>You don't have access to this server. Please see ${whereTo}.</body>
    </html>`);
    
    Remi Rampin's avatar
    Remi Rampin committed
        return false;
      } else {
        return true;
      }
    }
    
    app.all('/*', (req, res) => {
      if(PUBLIC_REGEX && PUBLIC_REGEX.test(req.path)) {
        // Public paths require no authentication
      } else if(!accessCheck(req, res)) {
        // Failed access check
    
      proxy.web(req, res);
    
    });
    
    const server = http.createServer(app);
    server.on('upgrade', (req, socket, head) => {
    
      proxy.ws(req, socket, head);
    
    Remi Rampin's avatar
    Remi Rampin committed
    });
    
    
    server.listen(PORT, () => {
    
    Remi Rampin's avatar
    Remi Rampin committed
      console.log(`app listening on port ${PORT}`);
    });