Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hono",
"version": "4.11.10",
"version": "4.12.0",
"description": "Web framework built on Web Standards",
"main": "dist/cjs/index.js",
"type": "module",
Expand Down
112 changes: 112 additions & 0 deletions src/adapter/aws-lambda/conninfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Context } from '../../context'
import { getConnInfo } from './conninfo'

describe('getConnInfo', () => {
describe('API Gateway v1', () => {
it('Should return the client IP from identity.sourceIp', () => {
const ip = '203.0.113.42'
const c = new Context(new Request('http://localhost/'), {
env: {
requestContext: {
identity: {
sourceIp: ip,
userAgent: 'test',
},
accountId: '123',
apiId: 'abc',
authorizer: {},
domainName: 'example.com',
domainPrefix: 'api',
extendedRequestId: 'xxx',
httpMethod: 'GET',
path: '/',
protocol: 'HTTP/1.1',
requestId: 'req-1',
requestTime: '',
requestTimeEpoch: 0,
resourcePath: '/',
stage: 'prod',
},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBe(ip)
})
})

describe('API Gateway v2', () => {
it('Should return the client IP from http.sourceIp', () => {
const ip = '198.51.100.23'
const c = new Context(new Request('http://localhost/'), {
env: {
requestContext: {
http: {
method: 'GET',
path: '/',
protocol: 'HTTP/1.1',
sourceIp: ip,
userAgent: 'test',
},
accountId: '123',
apiId: 'abc',
authentication: null,
authorizer: {},
domainName: 'example.com',
domainPrefix: 'api',
requestId: 'req-1',
routeKey: 'GET /',
stage: 'prod',
time: '',
timeEpoch: 0,
},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBe(ip)
})
})

describe('ALB', () => {
it('Should return the client IP from x-forwarded-for header', () => {
const ip = '192.0.2.50'
const req = new Request('http://localhost/', {
headers: {
'x-forwarded-for': `${ip}, 10.0.0.1`,
},
})
const c = new Context(req, {
env: {
requestContext: {
elb: {
targetGroupArn: 'arn:aws:elasticloadbalancing:...',
},
},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBe(ip)
})

it('Should return undefined when no x-forwarded-for header', () => {
const c = new Context(new Request('http://localhost/'), {
env: {
requestContext: {
elb: {
targetGroupArn: 'arn:aws:elasticloadbalancing:...',
},
},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBeUndefined()
})
})
})
72 changes: 72 additions & 0 deletions src/adapter/aws-lambda/conninfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Context } from '../../context'
import type { GetConnInfo } from '../../helper/conninfo'
import type {
ApiGatewayRequestContext,
ApiGatewayRequestContextV2,
ALBRequestContext,
} from './types'

type LambdaRequestContext =
| ApiGatewayRequestContext
| ApiGatewayRequestContextV2
| ALBRequestContext

type Env = {
Bindings: {
requestContext: LambdaRequestContext
}
}

/**
* Get connection information from AWS Lambda
*
* Extracts client IP from various Lambda event sources:
* - API Gateway v1 (REST API): requestContext.identity.sourceIp
* - API Gateway v2 (HTTP API/Function URLs): requestContext.http.sourceIp
* - ALB: Falls back to x-forwarded-for header
*
* @param c - Context
* @returns Connection information including remote address
* @example
* ```ts
* import { Hono } from 'hono'
* import { handle, getConnInfo } from 'hono/aws-lambda'
*
* const app = new Hono()
*
* app.get('/', (c) => {
* const info = getConnInfo(c)
* return c.text(`Your IP: ${info.remote.address}`)
* })
*
* export const handler = handle(app)
* ```
*/
export const getConnInfo: GetConnInfo = (c: Context<Env>) => {
const requestContext = c.env.requestContext

let address: string | undefined

// API Gateway v1 - has identity object
if ('identity' in requestContext && requestContext.identity?.sourceIp) {
address = requestContext.identity.sourceIp
}
// API Gateway v2 - has http object
else if ('http' in requestContext && requestContext.http?.sourceIp) {
address = requestContext.http.sourceIp
}
// ALB - use X-Forwarded-For header
else {
const xff = c.req.header('x-forwarded-for')
if (xff) {
// First IP is the client
address = xff.split(',')[0].trim()
}
}

return {
remote: {
address,
},
}
}
1 change: 1 addition & 0 deletions src/adapter/aws-lambda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

export { handle, streamHandle, defaultIsContentTypeBinary } from './handler'
export { getConnInfo } from './conninfo'
export type { APIGatewayProxyResult, LambdaEvent } from './handler'
export type {
ApiGatewayRequestContext,
Expand Down
27 changes: 27 additions & 0 deletions src/adapter/cloudflare-pages/conninfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Context } from '../../context'
import { getConnInfo } from './conninfo'

describe('getConnInfo', () => {
it('Should return the client IP from cf-connecting-ip header', () => {
const address = Math.random().toString()
const req = new Request('http://localhost/', {
headers: {
'cf-connecting-ip': address,
},
})
const c = new Context(req)

const info = getConnInfo(c)

expect(info.remote.address).toBe(address)
expect(info.remote.addressType).toBeUndefined()
})

it('Should return undefined when cf-connecting-ip header is not present', () => {
const c = new Context(new Request('http://localhost/'))

const info = getConnInfo(c)

expect(info.remote.address).toBeUndefined()
})
})
26 changes: 26 additions & 0 deletions src/adapter/cloudflare-pages/conninfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { GetConnInfo } from '../../helper/conninfo'

/**
* Get connection information from Cloudflare Pages
* @param c - Context
* @returns Connection information including remote address
* @example
* ```ts
* import { Hono } from 'hono'
* import { handle, getConnInfo } from 'hono/cloudflare-pages'
*
* const app = new Hono()
*
* app.get('/', (c) => {
* const info = getConnInfo(c)
* return c.text(`Your IP: ${info.remote.address}`)
* })
*
* export const onRequest = handle(app)
* ```
*/
export const getConnInfo: GetConnInfo = (c) => ({
remote: {
address: c.req.header('cf-connecting-ip'),
},
})
1 change: 1 addition & 0 deletions src/adapter/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*/

export { handle, handleMiddleware, serveStatic } from './handler'
export { getConnInfo } from './conninfo'
export type { EventContext } from './handler'
41 changes: 41 additions & 0 deletions src/adapter/netlify/conninfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Context } from '../../context'
import { getConnInfo } from './conninfo'

describe('getConnInfo', () => {
it('Should return the client IP from context.ip', () => {
const ip = '203.0.113.50'
const c = new Context(new Request('http://localhost/'), {
env: {
context: {
ip,
},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBe(ip)
})

it('Should return undefined when context.ip is not present', () => {
const c = new Context(new Request('http://localhost/'), {
env: {
context: {},
},
})

const info = getConnInfo(c)

expect(info.remote.address).toBeUndefined()
})

it('Should return undefined when context is not present', () => {
const c = new Context(new Request('http://localhost/'), {
env: {},
})

const info = getConnInfo(c)

expect(info.remote.address).toBeUndefined()
})
})
57 changes: 57 additions & 0 deletions src/adapter/netlify/conninfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Context } from '../../context'
import type { GetConnInfo } from '../../helper/conninfo'

/**
* Netlify context type
* @see https://docs.netlify.com/functions/api/
*/
type NetlifyContext = {
ip?: string
geo?: {
city?: string
country?: {
code?: string
name?: string
}
subdivision?: {
code?: string
name?: string
}
latitude?: number
longitude?: number
timezone?: string
postalCode?: string
}
requestId?: string
}

type Env = {
Bindings: {
context: NetlifyContext
}
}

/**
* Get connection information from Netlify
* @param c - Context
* @returns Connection information including remote address
* @example
* ```ts
* import { Hono } from 'hono'
* import { handle, getConnInfo } from 'hono/netlify'
*
* const app = new Hono()
*
* app.get('/', (c) => {
* const info = getConnInfo(c)
* return c.text(`Your IP: ${info.remote.address}`)
* })
*
* export default handle(app)
* ```
*/
export const getConnInfo: GetConnInfo = (c: Context<Env>) => ({
remote: {
address: c.env.context?.ip,
},
})
1 change: 1 addition & 0 deletions src/adapter/netlify/mod.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { handle } from './handler'
export { getConnInfo } from './conninfo'
Loading
Loading