Adds support for Astro.clientAddress (#3973)
* Adds support for Astro.clientAddress * Pass through mode and adapterName in SSG * Pass through the mode provided * Provide an adapter specific error message when possible
This commit is contained in:
parent
d73c04a9e5
commit
5a23483efb
25 changed files with 311 additions and 49 deletions
18
.changeset/olive-dryers-sell.md
Normal file
18
.changeset/olive-dryers-sell.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
'@astrojs/cloudflare': minor
|
||||||
|
'@astrojs/deno': minor
|
||||||
|
'@astrojs/netlify': minor
|
||||||
|
'@astrojs/vercel': minor
|
||||||
|
'@astrojs/node': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds support for Astro.clientAddress
|
||||||
|
|
||||||
|
The new `Astro.clientAddress` property allows you to get the IP address of the requested user.
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<div>Your address { Astro.clientAddress }</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
This property is only available when building for SSR, and only if the adapter you are using supports providing the IP address. If you attempt to access the property in a SSG app it will throw an error.
|
|
@ -92,6 +92,10 @@ export interface AstroGlobal extends AstroGlobalPartial {
|
||||||
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astrocanonicalurl)
|
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astrocanonicalurl)
|
||||||
*/
|
*/
|
||||||
canonicalURL: URL;
|
canonicalURL: URL;
|
||||||
|
/** The address (usually IP address) of the user. Used with SSR only.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
clientAddress: string;
|
||||||
/** Parameters passed to a dynamic page generated using [getStaticPaths](https://docs.astro.build/en/reference/api-reference/#getstaticpaths)
|
/** Parameters passed to a dynamic page generated using [getStaticPaths](https://docs.astro.build/en/reference/api-reference/#getstaticpaths)
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
|
||||||
|
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
|
import { error } from '../logger/core.js';
|
||||||
import { consoleLogDestination } from '../logger/console.js';
|
import { consoleLogDestination } from '../logger/console.js';
|
||||||
import { joinPaths, prependForwardSlash } from '../path.js';
|
import { joinPaths, prependForwardSlash } from '../path.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
|
@ -96,33 +97,43 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await render({
|
try {
|
||||||
links,
|
const response = await render({
|
||||||
logging: this.#logging,
|
adapterName: manifest.adapterName,
|
||||||
markdown: manifest.markdown,
|
links,
|
||||||
mod,
|
logging: this.#logging,
|
||||||
origin: url.origin,
|
markdown: manifest.markdown,
|
||||||
pathname: url.pathname,
|
mod,
|
||||||
scripts,
|
mode: 'production',
|
||||||
renderers,
|
origin: url.origin,
|
||||||
async resolve(specifier: string) {
|
pathname: url.pathname,
|
||||||
if (!(specifier in manifest.entryModules)) {
|
scripts,
|
||||||
throw new Error(`Unable to resolve [${specifier}]`);
|
renderers,
|
||||||
}
|
async resolve(specifier: string) {
|
||||||
const bundlePath = manifest.entryModules[specifier];
|
if (!(specifier in manifest.entryModules)) {
|
||||||
return bundlePath.startsWith('data:')
|
throw new Error(`Unable to resolve [${specifier}]`);
|
||||||
? bundlePath
|
}
|
||||||
: prependForwardSlash(joinPaths(manifest.base, bundlePath));
|
const bundlePath = manifest.entryModules[specifier];
|
||||||
},
|
return bundlePath.startsWith('data:')
|
||||||
route: routeData,
|
? bundlePath
|
||||||
routeCache: this.#routeCache,
|
: prependForwardSlash(joinPaths(manifest.base, bundlePath));
|
||||||
site: this.#manifest.site,
|
},
|
||||||
ssr: true,
|
route: routeData,
|
||||||
request,
|
routeCache: this.#routeCache,
|
||||||
streaming: this.#streaming,
|
site: this.#manifest.site,
|
||||||
});
|
ssr: true,
|
||||||
|
request,
|
||||||
return response;
|
streaming: this.#streaming,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch(err) {
|
||||||
|
error(this.#logging, 'ssr', err);
|
||||||
|
return new Response(null, {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #callEndpoint(
|
async #callEndpoint(
|
||||||
|
|
|
@ -5,13 +5,19 @@ import { IncomingMessage } from 'http';
|
||||||
import { deserializeManifest } from './common.js';
|
import { deserializeManifest } from './common.js';
|
||||||
import { App } from './index.js';
|
import { App } from './index.js';
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
function createRequestFromNodeRequest(req: IncomingMessage): Request {
|
function createRequestFromNodeRequest(req: IncomingMessage): Request {
|
||||||
let url = `http://${req.headers.host}${req.url}`;
|
let url = `http://${req.headers.host}${req.url}`;
|
||||||
const entries = Object.entries(req.headers as Record<string, any>);
|
let rawHeaders = req.headers as Record<string, any>;
|
||||||
|
const entries = Object.entries(rawHeaders);
|
||||||
let request = new Request(url, {
|
let request = new Request(url, {
|
||||||
method: req.method || 'GET',
|
method: req.method || 'GET',
|
||||||
headers: new Headers(entries),
|
headers: new Headers(entries),
|
||||||
});
|
});
|
||||||
|
if(req.socket.remoteAddress) {
|
||||||
|
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
|
||||||
|
}
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SSRManifest {
|
export interface SSRManifest {
|
||||||
|
adapterName: string;
|
||||||
routes: RouteInfo[];
|
routes: RouteInfo[];
|
||||||
site?: string;
|
site?: string;
|
||||||
base?: string;
|
base?: string;
|
||||||
|
|
|
@ -210,10 +210,12 @@ async function generatePath(
|
||||||
const ssr = isBuildingToSSR(opts.astroConfig);
|
const ssr = isBuildingToSSR(opts.astroConfig);
|
||||||
const url = new URL(opts.astroConfig.base + removeLeadingForwardSlash(pathname), origin);
|
const url = new URL(opts.astroConfig.base + removeLeadingForwardSlash(pathname), origin);
|
||||||
const options: RenderOptions = {
|
const options: RenderOptions = {
|
||||||
|
adapterName: undefined,
|
||||||
links,
|
links,
|
||||||
logging,
|
logging,
|
||||||
markdown: astroConfig.markdown,
|
markdown: astroConfig.markdown,
|
||||||
mod,
|
mod,
|
||||||
|
mode: opts.mode,
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
scripts,
|
scripts,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AstroTelemetry } from '@astrojs/telemetry';
|
import type { AstroTelemetry } from '@astrojs/telemetry';
|
||||||
import type { AstroConfig, BuildConfig, ManifestData } from '../../@types/astro';
|
import type { AstroConfig, BuildConfig, ManifestData, RuntimeMode } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger/core';
|
import type { LogOptions } from '../logger/core';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -24,7 +24,7 @@ import { staticBuild } from './static-build.js';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
|
||||||
export interface BuildOptions {
|
export interface BuildOptions {
|
||||||
mode?: string;
|
mode?: RuntimeMode;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
telemetry: AstroTelemetry;
|
telemetry: AstroTelemetry;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ export default async function build(config: AstroConfig, options: BuildOptions):
|
||||||
class AstroBuilder {
|
class AstroBuilder {
|
||||||
private config: AstroConfig;
|
private config: AstroConfig;
|
||||||
private logging: LogOptions;
|
private logging: LogOptions;
|
||||||
private mode = 'production';
|
private mode: RuntimeMode = 'production';
|
||||||
private origin: string;
|
private origin: string;
|
||||||
private routeCache: RouteCache;
|
private routeCache: RouteCache;
|
||||||
private manifest: ManifestData;
|
private manifest: ManifestData;
|
||||||
|
@ -129,17 +129,25 @@ class AstroBuilder {
|
||||||
colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
|
colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
|
||||||
);
|
);
|
||||||
|
|
||||||
await staticBuild({
|
try {
|
||||||
allPages,
|
await staticBuild({
|
||||||
astroConfig: this.config,
|
allPages,
|
||||||
logging: this.logging,
|
astroConfig: this.config,
|
||||||
manifest: this.manifest,
|
logging: this.logging,
|
||||||
origin: this.origin,
|
manifest: this.manifest,
|
||||||
pageNames,
|
mode: this.mode,
|
||||||
routeCache: this.routeCache,
|
origin: this.origin,
|
||||||
viteConfig,
|
pageNames,
|
||||||
buildConfig,
|
routeCache: this.routeCache,
|
||||||
});
|
viteConfig,
|
||||||
|
buildConfig,
|
||||||
|
});
|
||||||
|
} catch(err: unknown) {
|
||||||
|
// If the build doesn't complete, still shutdown the Vite server so the process doesn't hang.
|
||||||
|
await viteServer.close();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Write any additionally generated assets to disk.
|
// Write any additionally generated assets to disk.
|
||||||
this.timer.assetsStart = performance.now();
|
this.timer.assetsStart = performance.now();
|
||||||
|
|
|
@ -145,7 +145,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
...(viteConfig.plugins || []),
|
...(viteConfig.plugins || []),
|
||||||
// SSR needs to be last
|
// SSR needs to be last
|
||||||
isBuildingToSSR(opts.astroConfig) &&
|
isBuildingToSSR(opts.astroConfig) &&
|
||||||
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
|
vitePluginSSR(internals, opts.astroConfig._ctx.adapter!),
|
||||||
vitePluginAnalyzer(opts.astroConfig, internals),
|
vitePluginAnalyzer(opts.astroConfig, internals),
|
||||||
],
|
],
|
||||||
publicDir: ssr ? false : viteConfig.publicDir,
|
publicDir: ssr ? false : viteConfig.publicDir,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
ManifestData,
|
ManifestData,
|
||||||
RouteData,
|
RouteData,
|
||||||
|
RuntimeMode,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import type { ViteConfigWithSSR } from '../create-vite';
|
import type { ViteConfigWithSSR } from '../create-vite';
|
||||||
|
@ -30,6 +31,7 @@ export interface StaticBuildOptions {
|
||||||
buildConfig: BuildConfig;
|
buildConfig: BuildConfig;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
manifest: ManifestData;
|
manifest: ManifestData;
|
||||||
|
mode: RuntimeMode;
|
||||||
origin: string;
|
origin: string;
|
||||||
pageNames: string[];
|
pageNames: string[];
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
|
|
|
@ -20,7 +20,6 @@ const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||||
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
|
||||||
|
|
||||||
export function vitePluginSSR(
|
export function vitePluginSSR(
|
||||||
buildOpts: StaticBuildOptions,
|
|
||||||
internals: BuildInternals,
|
internals: BuildInternals,
|
||||||
adapter: AstroAdapter
|
adapter: AstroAdapter
|
||||||
): VitePlugin {
|
): VitePlugin {
|
||||||
|
@ -153,6 +152,7 @@ function buildManifest(
|
||||||
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||||
|
|
||||||
const ssrManifest: SerializedSSRManifest = {
|
const ssrManifest: SerializedSSRManifest = {
|
||||||
|
adapterName: opts.astroConfig._ctx.adapter!.name,
|
||||||
routes,
|
routes,
|
||||||
site: astroConfig.site,
|
site: astroConfig.site,
|
||||||
base: astroConfig.base,
|
base: astroConfig.base,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
Params,
|
Params,
|
||||||
Props,
|
Props,
|
||||||
RouteData,
|
RouteData,
|
||||||
|
RuntimeMode,
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
@ -66,11 +67,13 @@ export async function getParamsAndProps(
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderOptions {
|
export interface RenderOptions {
|
||||||
|
adapterName: string | undefined;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
links: Set<SSRElement>;
|
links: Set<SSRElement>;
|
||||||
styles?: Set<SSRElement>;
|
styles?: Set<SSRElement>;
|
||||||
markdown: MarkdownRenderingOptions;
|
markdown: MarkdownRenderingOptions;
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
mode: RuntimeMode;
|
||||||
origin: string;
|
origin: string;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
scripts: Set<SSRElement>;
|
scripts: Set<SSRElement>;
|
||||||
|
@ -86,12 +89,14 @@ export interface RenderOptions {
|
||||||
|
|
||||||
export async function render(opts: RenderOptions): Promise<Response> {
|
export async function render(opts: RenderOptions): Promise<Response> {
|
||||||
const {
|
const {
|
||||||
|
adapterName,
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
logging,
|
logging,
|
||||||
origin,
|
origin,
|
||||||
markdown,
|
markdown,
|
||||||
mod,
|
mod,
|
||||||
|
mode,
|
||||||
pathname,
|
pathname,
|
||||||
scripts,
|
scripts,
|
||||||
renderers,
|
renderers,
|
||||||
|
@ -126,10 +131,12 @@ export async function render(opts: RenderOptions): Promise<Response> {
|
||||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||||
|
|
||||||
const result = createResult({
|
const result = createResult({
|
||||||
|
adapterName,
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
logging,
|
logging,
|
||||||
markdown,
|
markdown,
|
||||||
|
mode,
|
||||||
origin,
|
origin,
|
||||||
params,
|
params,
|
||||||
props: pageProps,
|
props: pageProps,
|
||||||
|
|
|
@ -161,11 +161,13 @@ export async function render(
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = await coreRender({
|
let response = await coreRender({
|
||||||
|
adapterName: astroConfig.adapter?.name,
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
logging,
|
logging,
|
||||||
markdown: astroConfig.markdown,
|
markdown: astroConfig.markdown,
|
||||||
mod,
|
mod,
|
||||||
|
mode,
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
scripts,
|
scripts,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
Page,
|
Page,
|
||||||
Params,
|
Params,
|
||||||
Props,
|
Props,
|
||||||
|
RuntimeMode,
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
|
@ -15,6 +16,8 @@ import { LogOptions, warn } from '../logger/core.js';
|
||||||
import { isScriptRequest } from './script.js';
|
import { isScriptRequest } from './script.js';
|
||||||
import { createCanonicalURL, isCSSRequest } from './util.js';
|
import { createCanonicalURL, isCSSRequest } from './util.js';
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
function onlyAvailableInSSR(name: string) {
|
function onlyAvailableInSSR(name: string) {
|
||||||
return function _onlyAvailableInSSR() {
|
return function _onlyAvailableInSSR() {
|
||||||
// TODO add more guidance when we have docs and adapters.
|
// TODO add more guidance when we have docs and adapters.
|
||||||
|
@ -23,11 +26,13 @@ function onlyAvailableInSSR(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateResultArgs {
|
export interface CreateResultArgs {
|
||||||
|
adapterName: string | undefined;
|
||||||
ssr: boolean;
|
ssr: boolean;
|
||||||
streaming: boolean;
|
streaming: boolean;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
origin: string;
|
origin: string;
|
||||||
markdown: MarkdownRenderingOptions;
|
markdown: MarkdownRenderingOptions;
|
||||||
|
mode: RuntimeMode;
|
||||||
params: Params;
|
params: Params;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
props: Props;
|
props: Props;
|
||||||
|
@ -151,6 +156,17 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
const Astro = {
|
const Astro = {
|
||||||
__proto__: astroGlobal,
|
__proto__: astroGlobal,
|
||||||
canonicalURL,
|
canonicalURL,
|
||||||
|
get clientAddress() {
|
||||||
|
if(!(clientAddressSymbol in request)) {
|
||||||
|
if(args.adapterName) {
|
||||||
|
throw new Error(`Astro.clientAddress is not available in the ${args.adapterName} adapter. File an issue with the adapter to add support.`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Astro.clientAddress is not available in your environment. Ensure that you are using an SSR adapter that supports this feature.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(request, clientAddressSymbol);
|
||||||
|
},
|
||||||
params,
|
params,
|
||||||
props,
|
props,
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -7,6 +7,7 @@ type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormD
|
||||||
|
|
||||||
export interface CreateRequestOptions {
|
export interface CreateRequestOptions {
|
||||||
url: URL | string;
|
url: URL | string;
|
||||||
|
clientAddress?: string | undefined;
|
||||||
headers: HeaderType;
|
headers: HeaderType;
|
||||||
method?: string;
|
method?: string;
|
||||||
body?: RequestBody | undefined;
|
body?: RequestBody | undefined;
|
||||||
|
@ -14,9 +15,12 @@ export interface CreateRequestOptions {
|
||||||
ssr: boolean;
|
ssr: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
export function createRequest({
|
export function createRequest({
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
|
clientAddress,
|
||||||
method = 'GET',
|
method = 'GET',
|
||||||
body = undefined,
|
body = undefined,
|
||||||
logging,
|
logging,
|
||||||
|
@ -67,6 +71,8 @@ export function createRequest({
|
||||||
return _headers;
|
return _headers;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if(clientAddress) {
|
||||||
|
Reflect.set(request, clientAddressSymbol, clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
|
|
|
@ -229,6 +229,7 @@ async function handleRequest(
|
||||||
body,
|
body,
|
||||||
logging,
|
logging,
|
||||||
ssr: buildingToSSR,
|
ssr: buildingToSSR,
|
||||||
|
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
130
packages/astro/test/client-address.test.js
Normal file
130
packages/astro/test/client-address.test.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import testAdapter from './test-adapter.js';
|
||||||
|
import { nodeLogDestination } from '../dist/core/logger/node.js';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
describe('Astro.clientAddress', () => {
|
||||||
|
describe('SSR', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/client-address/',
|
||||||
|
experimental: {
|
||||||
|
ssr: true,
|
||||||
|
},
|
||||||
|
adapter: testAdapter(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Production', () => {
|
||||||
|
before(async () => {
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can get the address', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('#address').text()).to.equal('0.0.0.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Development', () => {
|
||||||
|
/** @type {import('./test-utils').DevServer} */
|
||||||
|
let devServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
devServer = await fixture.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Gets the address', async () => {
|
||||||
|
let res = await fixture.fetch('/');
|
||||||
|
expect(res.status).to.equal(200);
|
||||||
|
let html = await res.text();
|
||||||
|
let $ = cheerio.load(html);
|
||||||
|
let address = $('#address');
|
||||||
|
|
||||||
|
// Just checking that something is here. Not specifying address as it
|
||||||
|
// might differ per machine.
|
||||||
|
expect(address.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SSR adapter not implemented', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/client-address/',
|
||||||
|
experimental: {
|
||||||
|
ssr: true,
|
||||||
|
},
|
||||||
|
adapter: testAdapter({ provideAddress: false }),
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Gets an error message', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(500);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SSG', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/client-address/',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Build', () => {
|
||||||
|
it('throws during generation', async () => {
|
||||||
|
try {
|
||||||
|
await fixture.build();
|
||||||
|
expect(false).to.equal(true, 'Build should not have completed');
|
||||||
|
} catch(err) {
|
||||||
|
expect(err.message).to.match(/Astro\.clientAddress/, 'Error message mentions Astro.clientAddress');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Development', () => {
|
||||||
|
/** @type {import('./test-utils').DevServer} */
|
||||||
|
let devServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
// We expect an error, so silence the output
|
||||||
|
const logging = {
|
||||||
|
dest: nodeLogDestination,
|
||||||
|
level: 'silent',
|
||||||
|
};
|
||||||
|
devServer = await fixture.startDevServer({ logging });
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not accessible', async () => {
|
||||||
|
let res = await fixture.fetch('/');
|
||||||
|
expect(res.status).to.equal(500);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
12
packages/astro/test/fixtures/client-address/src/pages/index.astro
vendored
Normal file
12
packages/astro/test/fixtures/client-address/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
const address = Astro.clientAddress;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Astro.clientAddress</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Astro.clientAddress</h1>
|
||||||
|
<div id="address">{ address }</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -4,7 +4,7 @@ import { viteID } from '../dist/core/util.js';
|
||||||
*
|
*
|
||||||
* @returns {import('../src/@types/astro').AstroIntegration}
|
* @returns {import('../src/@types/astro').AstroIntegration}
|
||||||
*/
|
*/
|
||||||
export default function () {
|
export default function ({ provideAddress } = { provideAddress: true }) {
|
||||||
return {
|
return {
|
||||||
name: 'my-ssr-adapter',
|
name: 'my-ssr-adapter',
|
||||||
hooks: {
|
hooks: {
|
||||||
|
@ -23,7 +23,23 @@ export default function () {
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === '@my-ssr') {
|
if (id === '@my-ssr') {
|
||||||
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (streaming) => new App(manifest, streaming) }; }`;
|
return `
|
||||||
|
import { App } from 'astro/app';
|
||||||
|
|
||||||
|
class MyApp extends App {
|
||||||
|
render(request) {
|
||||||
|
${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''}
|
||||||
|
return super.render(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExports(manifest) {
|
||||||
|
return {
|
||||||
|
manifest,
|
||||||
|
createApp: (streaming) => new MyApp(manifest, streaming)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -127,6 +127,7 @@ export async function loadFixture(inlineConfig) {
|
||||||
// Also do it on process exit, just in case.
|
// Also do it on process exit, just in case.
|
||||||
process.on('exit', resetAllFiles);
|
process.on('exit', resetAllFiles);
|
||||||
|
|
||||||
|
let fixtureId = new Date().valueOf();
|
||||||
let devServer;
|
let devServer;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -150,7 +151,7 @@ export async function loadFixture(inlineConfig) {
|
||||||
await fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true });
|
await fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true });
|
||||||
},
|
},
|
||||||
loadTestAdapterApp: async (streaming) => {
|
loadTestAdapterApp: async (streaming) => {
|
||||||
const url = new URL('./server/entry.mjs', config.outDir);
|
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
|
||||||
const { createApp, manifest } = await import(url);
|
const { createApp, manifest } = await import(url);
|
||||||
const app = createApp(streaming);
|
const app = createApp(streaming);
|
||||||
app.manifest = manifest;
|
app.manifest = manifest;
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function createExports(manifest: SSRManifest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.match(request)) {
|
if (app.match(request)) {
|
||||||
|
Reflect.set(request, Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip'));
|
||||||
return app.render(request);
|
return app.render(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,10 @@ export function start(manifest: SSRManifest, options: Options) {
|
||||||
|
|
||||||
const clientRoot = new URL('../client/', import.meta.url);
|
const clientRoot = new URL('../client/', import.meta.url);
|
||||||
const app = new App(manifest);
|
const app = new App(manifest);
|
||||||
const handler = async (request: Request) => {
|
const handler = async (request: Request, connInfo: any) => {
|
||||||
if (app.match(request)) {
|
if (app.match(request)) {
|
||||||
|
let ip = connInfo?.remoteAddr?.hostname;
|
||||||
|
Reflect.set(request, Symbol.for('astro.clientAddress'), ip);
|
||||||
return await app.render(request);
|
return await app.render(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { SSRManifest } from 'astro';
|
import type { SSRManifest } from 'astro';
|
||||||
import { App } from 'astro/app';
|
import { App } from 'astro/app';
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
export function createExports(manifest: SSRManifest) {
|
export function createExports(manifest: SSRManifest) {
|
||||||
const app = new App(manifest);
|
const app = new App(manifest);
|
||||||
|
|
||||||
|
@ -13,6 +15,8 @@ export function createExports(manifest: SSRManifest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (app.match(request)) {
|
if (app.match(request)) {
|
||||||
|
const ip = request.headers.get('x-nf-client-connection-ip');
|
||||||
|
Reflect.set(request, clientAddressSymbol, ip);
|
||||||
return app.render(request);
|
return app.render(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ function parseContentType(header?: string) {
|
||||||
return header?.split(';')[0] ?? '';
|
return header?.split(';')[0] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
export const createExports = (manifest: SSRManifest, args: Args) => {
|
export const createExports = (manifest: SSRManifest, args: Args) => {
|
||||||
const app = new App(manifest);
|
const app = new App(manifest);
|
||||||
|
|
||||||
|
@ -71,6 +73,9 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ip = headers['x-nf-client-connection-ip'];
|
||||||
|
Reflect.set(request, clientAddressSymbol, ip);
|
||||||
|
|
||||||
const response: Response = await app.render(request);
|
const response: Response = await app.render(request);
|
||||||
const responseHeaders = Object.fromEntries(response.headers.entries());
|
const responseHeaders = Object.fromEntries(response.headers.entries());
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,14 @@ import './shim.js';
|
||||||
import type { SSRManifest } from 'astro';
|
import type { SSRManifest } from 'astro';
|
||||||
import { App } from 'astro/app';
|
import { App } from 'astro/app';
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
export function createExports(manifest: SSRManifest) {
|
export function createExports(manifest: SSRManifest) {
|
||||||
const app = new App(manifest);
|
const app = new App(manifest);
|
||||||
|
|
||||||
const handler = async (request: Request): Promise<Response> => {
|
const handler = async (request: Request): Promise<Response> => {
|
||||||
if (app.match(request)) {
|
if (app.match(request)) {
|
||||||
|
Reflect.set(request, clientAddressSymbol, request.headers.get('x-forwarded-for'));
|
||||||
return await app.render(request);
|
return await app.render(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Credits to the SvelteKit team
|
Credits to the SvelteKit team
|
||||||
https://github.com/sveltejs/kit/blob/69913e9fda054fa6a62a80e2bb4ee7dca1005796/packages/kit/src/node.js
|
https://github.com/sveltejs/kit/blob/69913e9fda054fa6a62a80e2bb4ee7dca1005796/packages/kit/src/node.js
|
||||||
|
@ -66,11 +68,13 @@ export async function getRequest(base: string, req: IncomingMessage): Promise<Re
|
||||||
delete headers[':authority'];
|
delete headers[':authority'];
|
||||||
delete headers[':scheme'];
|
delete headers[':scheme'];
|
||||||
}
|
}
|
||||||
return new Request(base + req.url, {
|
const request = new Request(base + req.url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers,
|
headers,
|
||||||
body: await get_raw_body(req), // TODO stream rather than buffer
|
body: await get_raw_body(req), // TODO stream rather than buffer
|
||||||
});
|
});
|
||||||
|
Reflect.set(request, clientAddressSymbol, headers['x-forwarded-for']);
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setResponse(res: ServerResponse, response: Response): Promise<void> {
|
export async function setResponse(res: ServerResponse, response: Response): Promise<void> {
|
||||||
|
|
Loading…
Reference in a new issue