Skip to content

Commit

Permalink
Adds support for Astro.clientAddress (#3973)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
matthewp authored Jul 19, 2022
1 parent d73c04a commit 5a23483
Show file tree
Hide file tree
Showing 25 changed files with 311 additions and 49 deletions.
18 changes: 18 additions & 0 deletions .changeset/olive-dryers-sell.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export interface AstroGlobal extends AstroGlobalPartial {
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astrocanonicalurl)
*/
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)
*
* Example usage:
Expand Down
65 changes: 38 additions & 27 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';

import mime from 'mime';
import { call as callEndpoint } from '../endpoint/index.js';
import { error } from '../logger/core.js';
import { consoleLogDestination } from '../logger/console.js';
import { joinPaths, prependForwardSlash } from '../path.js';
import { render } from '../render/core.js';
Expand Down Expand Up @@ -96,33 +97,43 @@ export class App {
}
}

const response = await render({
links,
logging: this.#logging,
markdown: manifest.markdown,
mod,
origin: url.origin,
pathname: url.pathname,
scripts,
renderers,
async resolve(specifier: string) {
if (!(specifier in manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
return bundlePath.startsWith('data:')
? bundlePath
: prependForwardSlash(joinPaths(manifest.base, bundlePath));
},
route: routeData,
routeCache: this.#routeCache,
site: this.#manifest.site,
ssr: true,
request,
streaming: this.#streaming,
});

return response;
try {
const response = await render({
adapterName: manifest.adapterName,
links,
logging: this.#logging,
markdown: manifest.markdown,
mod,
mode: 'production',
origin: url.origin,
pathname: url.pathname,
scripts,
renderers,
async resolve(specifier: string) {
if (!(specifier in manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
return bundlePath.startsWith('data:')
? bundlePath
: prependForwardSlash(joinPaths(manifest.base, bundlePath));
},
route: routeData,
routeCache: this.#routeCache,
site: this.#manifest.site,
ssr: true,
request,
streaming: this.#streaming,
});

return response;
} catch(err) {
error(this.#logging, 'ssr', err);
return new Response(null, {
status: 500,
statusText: 'Internal server error'
});
}
}

async #callEndpoint(
Expand Down
8 changes: 7 additions & 1 deletion packages/astro/src/core/app/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import { IncomingMessage } from 'http';
import { deserializeManifest } from './common.js';
import { App } from './index.js';

const clientAddressSymbol = Symbol.for('astro.clientAddress');

function createRequestFromNodeRequest(req: IncomingMessage): Request {
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, {
method: req.method || 'GET',
headers: new Headers(entries),
});
if(req.socket.remoteAddress) {
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
}
return request;
}

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
};

export interface SSRManifest {
adapterName: string;
routes: RouteInfo[];
site?: string;
base?: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,12 @@ async function generatePath(
const ssr = isBuildingToSSR(opts.astroConfig);
const url = new URL(opts.astroConfig.base + removeLeadingForwardSlash(pathname), origin);
const options: RenderOptions = {
adapterName: undefined,
links,
logging,
markdown: astroConfig.markdown,
mod,
mode: opts.mode,
origin,
pathname,
scripts,
Expand Down
36 changes: 22 additions & 14 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 fs from 'fs';
Expand All @@ -24,7 +24,7 @@ import { staticBuild } from './static-build.js';
import { getTimeStat } from './util.js';

export interface BuildOptions {
mode?: string;
mode?: RuntimeMode;
logging: LogOptions;
telemetry: AstroTelemetry;
}
Expand All @@ -39,7 +39,7 @@ export default async function build(config: AstroConfig, options: BuildOptions):
class AstroBuilder {
private config: AstroConfig;
private logging: LogOptions;
private mode = 'production';
private mode: RuntimeMode = 'production';
private origin: string;
private routeCache: RouteCache;
private manifest: ManifestData;
Expand Down Expand Up @@ -129,17 +129,25 @@ class AstroBuilder {
colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
);

await staticBuild({
allPages,
astroConfig: this.config,
logging: this.logging,
manifest: this.manifest,
origin: this.origin,
pageNames,
routeCache: this.routeCache,
viteConfig,
buildConfig,
});
try {
await staticBuild({
allPages,
astroConfig: this.config,
logging: this.logging,
manifest: this.manifest,
mode: this.mode,
origin: this.origin,
pageNames,
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.
this.timer.assetsStart = performance.now();
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
...(viteConfig.plugins || []),
// SSR needs to be last
isBuildingToSSR(opts.astroConfig) &&
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
vitePluginSSR(internals, opts.astroConfig._ctx.adapter!),
vitePluginAnalyzer(opts.astroConfig, internals),
],
publicDir: ssr ? false : viteConfig.publicDir,
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ComponentInstance,
ManifestData,
RouteData,
RuntimeMode,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { ViteConfigWithSSR } from '../create-vite';
Expand All @@ -30,6 +31,7 @@ export interface StaticBuildOptions {
buildConfig: BuildConfig;
logging: LogOptions;
manifest: ManifestData;
mode: RuntimeMode;
origin: string;
pageNames: string[];
routeCache: RouteCache;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/vite-plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');

export function vitePluginSSR(
buildOpts: StaticBuildOptions,
internals: BuildInternals,
adapter: AstroAdapter
): VitePlugin {
Expand Down Expand Up @@ -153,6 +152,7 @@ function buildManifest(
'data:text/javascript;charset=utf-8,//[no before-hydration script]';

const ssrManifest: SerializedSSRManifest = {
adapterName: opts.astroConfig._ctx.adapter!.name,
routes,
site: astroConfig.site,
base: astroConfig.base,
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Params,
Props,
RouteData,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
} from '../../@types/astro';
Expand Down Expand Up @@ -66,11 +67,13 @@ export async function getParamsAndProps(
}

export interface RenderOptions {
adapterName: string | undefined;
logging: LogOptions;
links: Set<SSRElement>;
styles?: Set<SSRElement>;
markdown: MarkdownRenderingOptions;
mod: ComponentInstance;
mode: RuntimeMode;
origin: string;
pathname: string;
scripts: Set<SSRElement>;
Expand All @@ -86,12 +89,14 @@ export interface RenderOptions {

export async function render(opts: RenderOptions): Promise<Response> {
const {
adapterName,
links,
styles,
logging,
origin,
markdown,
mod,
mode,
pathname,
scripts,
renderers,
Expand Down Expand Up @@ -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}`);

const result = createResult({
adapterName,
links,
styles,
logging,
markdown,
mode,
origin,
params,
props: pageProps,
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,13 @@ export async function render(
});

let response = await coreRender({
adapterName: astroConfig.adapter?.name,
links,
styles,
logging,
markdown: astroConfig.markdown,
mod,
mode,
origin,
pathname,
scripts,
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
Page,
Params,
Props,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
SSRResult,
Expand All @@ -15,6 +16,8 @@ import { LogOptions, warn } from '../logger/core.js';
import { isScriptRequest } from './script.js';
import { createCanonicalURL, isCSSRequest } from './util.js';

const clientAddressSymbol = Symbol.for('astro.clientAddress');

function onlyAvailableInSSR(name: string) {
return function _onlyAvailableInSSR() {
// TODO add more guidance when we have docs and adapters.
Expand All @@ -23,11 +26,13 @@ function onlyAvailableInSSR(name: string) {
}

export interface CreateResultArgs {
adapterName: string | undefined;
ssr: boolean;
streaming: boolean;
logging: LogOptions;
origin: string;
markdown: MarkdownRenderingOptions;
mode: RuntimeMode;
params: Params;
pathname: string;
props: Props;
Expand Down Expand Up @@ -151,6 +156,17 @@ export function createResult(args: CreateResultArgs): SSRResult {
const Astro = {
__proto__: astroGlobal,
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,
props,
request,
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/src/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormD

export interface CreateRequestOptions {
url: URL | string;
clientAddress?: string | undefined;
headers: HeaderType;
method?: string;
body?: RequestBody | undefined;
logging: LogOptions;
ssr: boolean;
}

const clientAddressSymbol = Symbol.for('astro.clientAddress');

export function createRequest({
url,
headers,
clientAddress,
method = 'GET',
body = undefined,
logging,
Expand Down Expand Up @@ -67,6 +71,8 @@ export function createRequest({
return _headers;
},
});
} else if(clientAddress) {
Reflect.set(request, clientAddressSymbol, clientAddress);
}

return request;
Expand Down
Loading

0 comments on commit 5a23483

Please sign in to comment.