mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) when redirecting, use protocol in APP_HOME_URL if available
Summary: Currently, Grist behind a reverse proxy will generate many needless redirects via `http`, and can't be used with only port 443. This diff centralizes generation of these redirects and uses the protocol in APP_HOME_URL if it is set. Test Plan: manually tested by rebuilding grist-core and doing a reverse proxy deployment that had no support for port 80. Prior to this change, there are lots of problems; after, the site works as expected. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3400
This commit is contained in:
		
							parent
							
								
									6f00106d7c
								
							
						
					
					
						commit
						4de5928396
					
				@ -15,7 +15,7 @@ import {COOKIE_MAX_AGE, getAllowedOrgForSessionID, getCookieDomain,
 | 
				
			|||||||
import {makeId} from 'app/server/lib/idUtils';
 | 
					import {makeId} from 'app/server/lib/idUtils';
 | 
				
			||||||
import * as log from 'app/server/lib/log';
 | 
					import * as log from 'app/server/lib/log';
 | 
				
			||||||
import {IPermitStore, Permit} from 'app/server/lib/Permit';
 | 
					import {IPermitStore, Permit} from 'app/server/lib/Permit';
 | 
				
			||||||
import {allowHost, optStringParam} from 'app/server/lib/requestUtils';
 | 
					import {allowHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
 | 
				
			||||||
import * as cookie from 'cookie';
 | 
					import * as cookie from 'cookie';
 | 
				
			||||||
import {NextFunction, Request, RequestHandler, Response} from 'express';
 | 
					import {NextFunction, Request, RequestHandler, Response} from 'express';
 | 
				
			||||||
import {IncomingMessage} from 'http';
 | 
					import {IncomingMessage} from 'http';
 | 
				
			||||||
@ -344,7 +344,7 @@ export function redirectToLoginUnconditionally(
 | 
				
			|||||||
    // logging out again, `users` will still be set.
 | 
					    // logging out again, `users` will still be set.
 | 
				
			||||||
    const signUp: boolean = (mreq.session.users === undefined);
 | 
					    const signUp: boolean = (mreq.session.users === undefined);
 | 
				
			||||||
    log.debug(`Authorizer: redirecting to ${signUp ? 'sign up' : 'log in'}`);
 | 
					    log.debug(`Authorizer: redirecting to ${signUp ? 'sign up' : 'log in'}`);
 | 
				
			||||||
    const redirectUrl = new URL(req.protocol + '://' + req.get('host') + req.originalUrl);
 | 
					    const redirectUrl = new URL(getOriginUrl(req) + req.originalUrl);
 | 
				
			||||||
    if (signUp) {
 | 
					    if (signUp) {
 | 
				
			||||||
      return resp.redirect(await getSignUpRedirectUrl(req, redirectUrl));
 | 
					      return resp.redirect(await getSignUpRedirectUrl(req, redirectUrl));
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,7 @@
 | 
				
			|||||||
import type {Express, NextFunction, Request, RequestHandler, Response} from 'express';
 | 
					import type {Express, NextFunction, Request, RequestHandler, Response} from 'express';
 | 
				
			||||||
import type {RequestWithLogin} from 'app/server/lib/Authorizer';
 | 
					import type {RequestWithLogin} from 'app/server/lib/Authorizer';
 | 
				
			||||||
import {expressWrap} from 'app/server/lib/expressWrap';
 | 
					import {expressWrap} from 'app/server/lib/expressWrap';
 | 
				
			||||||
 | 
					import {getOriginUrl} from 'app/server/lib/requestUtils';
 | 
				
			||||||
import * as crypto from 'crypto';
 | 
					import * as crypto from 'crypto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DISCOURSE_CONNECT_SECRET = process.env.DISCOURSE_CONNECT_SECRET;
 | 
					const DISCOURSE_CONNECT_SECRET = process.env.DISCOURSE_CONNECT_SECRET;
 | 
				
			||||||
@ -65,8 +66,8 @@ function discourseConnect(req: Request, resp: Response) {
 | 
				
			|||||||
    throw new Error('User is not authenticated');
 | 
					    throw new Error('User is not authenticated');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!req.query.user && mreq.users && mreq.users.length > 1) {
 | 
					  if (!req.query.user && mreq.users && mreq.users.length > 1) {
 | 
				
			||||||
    const origUrl = new URL(req.originalUrl, `${req.protocol}://${req.get('host')}`);
 | 
					    const origUrl = new URL(req.originalUrl, getOriginUrl(req));
 | 
				
			||||||
    const redirectUrl = new URL('/welcome/select-account', `${req.protocol}://${req.get('host')}`);
 | 
					    const redirectUrl = new URL('/welcome/select-account', getOriginUrl(req));
 | 
				
			||||||
    redirectUrl.searchParams.set('next', origUrl.toString());
 | 
					    redirectUrl.searchParams.set('next', origUrl.toString());
 | 
				
			||||||
    return resp.redirect(redirectUrl.toString());
 | 
					    return resp.redirect(redirectUrl.toString());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,8 @@ import {IPermitStore} from 'app/server/lib/Permit';
 | 
				
			|||||||
import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
 | 
					import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
 | 
				
			||||||
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
 | 
					import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
 | 
				
			||||||
import {PluginManager} from 'app/server/lib/PluginManager';
 | 
					import {PluginManager} from 'app/server/lib/PluginManager';
 | 
				
			||||||
import {adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getScope, optStringParam,
 | 
					import {
 | 
				
			||||||
 | 
					  adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getOriginUrl, getScope, optStringParam,
 | 
				
			||||||
        RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
 | 
					        RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
 | 
				
			||||||
import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
 | 
					import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
 | 
				
			||||||
import {getDatabaseUrl} from 'app/server/lib/serverUtils';
 | 
					import {getDatabaseUrl} from 'app/server/lib/serverUtils';
 | 
				
			||||||
@ -1097,7 +1098,7 @@ export class FlexServer implements GristServer {
 | 
				
			|||||||
      const planRequired = task === 'signup' || task === 'updatePlan';
 | 
					      const planRequired = task === 'signup' || task === 'updatePlan';
 | 
				
			||||||
      if (!BillingTask.guard(task) || (planRequired && !req.query.billingPlan)) {
 | 
					      if (!BillingTask.guard(task) || (planRequired && !req.query.billingPlan)) {
 | 
				
			||||||
        // If the payment task/plan are invalid, redirect to the summary page.
 | 
					        // If the payment task/plan are invalid, redirect to the summary page.
 | 
				
			||||||
        return resp.redirect(req.protocol + '://' + req.get('host') + `/billing`);
 | 
					        return resp.redirect(getOriginUrl(req) + `/billing`);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return this._sendAppPage(req, resp, {path: 'billing.html', status: 200, config: {}});
 | 
					        return this._sendAppPage(req, resp, {path: 'billing.html', status: 200, config: {}});
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -1519,7 +1520,7 @@ export class FlexServer implements GristServer {
 | 
				
			|||||||
  private _getOrgRedirectUrl(req: RequestWithLogin, subdomain: string, pathname: string = req.originalUrl): string {
 | 
					  private _getOrgRedirectUrl(req: RequestWithLogin, subdomain: string, pathname: string = req.originalUrl): string {
 | 
				
			||||||
    const config = this.getGristConfig();
 | 
					    const config = this.getGristConfig();
 | 
				
			||||||
    const {hostname, orgInPath} = getOrgUrlInfo(subdomain, req.get('host')!, config);
 | 
					    const {hostname, orgInPath} = getOrgUrlInfo(subdomain, req.get('host')!, config);
 | 
				
			||||||
    const redirectUrl = new URL(pathname, `${req.protocol}://${req.get('host')}`);
 | 
					    const redirectUrl = new URL(pathname, getOriginUrl(req));
 | 
				
			||||||
    if (hostname) {
 | 
					    if (hostname) {
 | 
				
			||||||
      redirectUrl.hostname = hostname;
 | 
					      redirectUrl.hostname = hostname;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -61,6 +61,7 @@ import {expressWrap} from 'app/server/lib/expressWrap';
 | 
				
			|||||||
import {GristLoginSystem, GristServer} from 'app/server/lib/GristServer';
 | 
					import {GristLoginSystem, GristServer} from 'app/server/lib/GristServer';
 | 
				
			||||||
import * as log from 'app/server/lib/log';
 | 
					import * as log from 'app/server/lib/log';
 | 
				
			||||||
import {Permit} from 'app/server/lib/Permit';
 | 
					import {Permit} from 'app/server/lib/Permit';
 | 
				
			||||||
 | 
					import {getOriginUrl} from 'app/server/lib/requestUtils';
 | 
				
			||||||
import {fromCallback} from 'app/server/lib/serverUtils';
 | 
					import {fromCallback} from 'app/server/lib/serverUtils';
 | 
				
			||||||
import {Sessions} from 'app/server/lib/Sessions';
 | 
					import {Sessions} from 'app/server/lib/Sessions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -156,7 +157,7 @@ export class SamlConfig {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Starting point for login. It redirects to the IdP, and then to /saml/assert.
 | 
					    // Starting point for login. It redirects to the IdP, and then to /saml/assert.
 | 
				
			||||||
    app.get("/saml/login", expressWrap(async (req, res, next) => {
 | 
					    app.get("/saml/login", expressWrap(async (req, res, next) => {
 | 
				
			||||||
      res.redirect(await this.getLoginRedirectUrl(req, new URL(req.protocol + "://" + req.get('host'))));
 | 
					      res.redirect(await this.getLoginRedirectUrl(req, new URL(getOriginUrl(req))));
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Assert endpoint for when the login completes as POST.
 | 
					    // Assert endpoint for when the login completes as POST.
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import { mapGetOrSet, MapWithTTL } from 'app/common/AsyncCreate';
 | 
				
			|||||||
import { extractOrgParts, getHostType, getKnownOrg } from 'app/common/gristUrls';
 | 
					import { extractOrgParts, getHostType, getKnownOrg } from 'app/common/gristUrls';
 | 
				
			||||||
import { Organization } from 'app/gen-server/entity/Organization';
 | 
					import { Organization } from 'app/gen-server/entity/Organization';
 | 
				
			||||||
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
 | 
					import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
 | 
				
			||||||
 | 
					import { getOriginUrl } from 'app/server/lib/requestUtils';
 | 
				
			||||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
 | 
					import { NextFunction, Request, RequestHandler, Response } from 'express';
 | 
				
			||||||
import { IncomingMessage } from 'http';
 | 
					import { IncomingMessage } from 'http';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -155,7 +156,7 @@ export class Hosts {
 | 
				
			|||||||
        return o && o.host || undefined;
 | 
					        return o && o.host || undefined;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      if (orgHost && orgHost !== req.hostname) {
 | 
					      if (orgHost && orgHost !== req.hostname) {
 | 
				
			||||||
        const url = new URL(`${req.protocol}://${req.headers.host}${req.path}`);
 | 
					        const url = new URL(getOriginUrl(req) + req.path);
 | 
				
			||||||
        url.hostname = orgHost;  // assigning hostname rather than host preserves port.
 | 
					        url.hostname = orgHost;  // assigning hostname rather than host preserves port.
 | 
				
			||||||
        return resp.redirect(url.href);
 | 
					        return resp.redirect(url.href);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -70,7 +70,7 @@ export function addOrgToPath(req: RequestWithOrg, path: string): string {
 | 
				
			|||||||
 * Get url to the org associated with the request.
 | 
					 * Get url to the org associated with the request.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function getOrgUrl(req: Request, path: string = '/') {
 | 
					export function getOrgUrl(req: Request, path: string = '/') {
 | 
				
			||||||
  return req.protocol + '://' + req.get('host') + addOrgToPathIfNeeded(req, path);
 | 
					  return getOriginUrl(req) + addOrgToPathIfNeeded(req, path);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -97,8 +97,8 @@ export function trustOrigin(req: Request, resp: Response): boolean {
 | 
				
			|||||||
// enough if only the base domains match. Differing ports are allowed, which helps in dev/testing.
 | 
					// enough if only the base domains match. Differing ports are allowed, which helps in dev/testing.
 | 
				
			||||||
export function allowHost(req: Request, allowedHost: string|URL) {
 | 
					export function allowHost(req: Request, allowedHost: string|URL) {
 | 
				
			||||||
  const mreq = req as RequestWithOrg;
 | 
					  const mreq = req as RequestWithOrg;
 | 
				
			||||||
  const proto = req.protocol;
 | 
					  const proto = getEndUserProtocol(req);
 | 
				
			||||||
  const actualUrl = new URL(`${proto}://${req.get('host')}`);
 | 
					  const actualUrl = new URL(getOriginUrl(req));
 | 
				
			||||||
  const allowedUrl = (typeof allowedHost === 'string') ? new URL(`${proto}://${allowedHost}`) : allowedHost;
 | 
					  const allowedUrl = (typeof allowedHost === 'string') ? new URL(`${proto}://${allowedHost}`) : allowedHost;
 | 
				
			||||||
  if (mreq.isCustomHost) {
 | 
					  if (mreq.isCustomHost) {
 | 
				
			||||||
    // For a request to a custom domain, the full hostname must match.
 | 
					    // For a request to a custom domain, the full hostname must match.
 | 
				
			||||||
@ -282,11 +282,24 @@ export interface RequestWithGristInfo extends Request {
 | 
				
			|||||||
 * https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html
 | 
					 * https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function getOriginUrl(req: Request) {
 | 
					export function getOriginUrl(req: Request) {
 | 
				
			||||||
  const host = req.headers.host!;
 | 
					  const host = req.get('host')!;
 | 
				
			||||||
  const protocol = req.get("X-Forwarded-Proto") || req.protocol;
 | 
					  const protocol = getEndUserProtocol(req);
 | 
				
			||||||
  return `${protocol}://${host}`;
 | 
					  return `${protocol}://${host}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get the protocol to use in Grist URLs that are intended to be reachable
 | 
				
			||||||
 | 
					 * from a user's browser. Use the protocol in APP_HOME_URL if available,
 | 
				
			||||||
 | 
					 * otherwise X-Forwarded-Proto is set on the provided request, otherwise
 | 
				
			||||||
 | 
					 * the protocol of the request itself.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function getEndUserProtocol(req: Request) {
 | 
				
			||||||
 | 
					  if (process.env.APP_HOME_URL) {
 | 
				
			||||||
 | 
					    return new URL(process.env.APP_HOME_URL).protocol.replace(':', '');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return req.get("X-Forwarded-Proto") || req.protocol;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * In some configurations, session information may be cached by the server.
 | 
					 * In some configurations, session information may be cached by the server.
 | 
				
			||||||
 * When session information changes, give the server a chance to clear its
 | 
					 * When session information changes, give the server a chance to clear its
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user