1
- import { GitBookAPIError } from '@gitbook/api' ;
1
+ import { CustomizationThemeMode , GitBookAPIError } from '@gitbook/api' ;
2
2
import type { NextRequest } from 'next/server' ;
3
3
import { NextResponse } from 'next/server' ;
4
4
5
5
import { getContentSecurityPolicy } from '@/lib/csp' ;
6
+ import { validateSerializedCustomization } from '@/lib/customization' ;
6
7
import { removeLeadingSlash , removeTrailingSlash } from '@/lib/paths' ;
8
+ import { getResponseCookiesForVisitorAuth , getVisitorToken } from '@/lib/visitor-token' ;
7
9
import { serveResizedImage } from '@/routes/image' ;
8
10
import { getPublishedContentByURL } from '@v2/lib/data' ;
11
+ import { GITBOOK_URL } from '@v2/lib/env' ;
9
12
import { MiddlewareHeaders } from '@v2/lib/middleware' ;
10
13
11
14
export const config = {
12
- matcher : [ '/((?!_next/|_static/|_vercel|[\\w-]+\\.\\w+ ).*)' ] ,
15
+ matcher : [ '/((?!_next/static|_next/image ).*)' ] ,
13
16
} ;
14
17
15
18
type URLWithMode = { url : URL ; mode : 'url' | 'url-host' } ;
@@ -48,12 +51,18 @@ export async function middleware(request: NextRequest) {
48
51
*/
49
52
async function serveSiteByURL ( request : NextRequest , urlWithMode : URLWithMode ) {
50
53
const { url, mode } = urlWithMode ;
51
- const dynamicHeaders = getDynamicHeaders ( url , request ) ;
54
+
55
+ // Visitor authentication
56
+ // @ts -ignore - request typing
57
+ const visitorToken = getVisitorToken ( request , url ) ;
52
58
53
59
const result = await getPublishedContentByURL ( {
54
60
url : url . toString ( ) ,
55
- visitorAuthToken : null ,
56
- redirectOnError : false ,
61
+ visitorAuthToken : visitorToken ?. token ?? null ,
62
+ // When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
63
+ // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
64
+ // For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
65
+ redirectOnError : visitorToken ?. source === 'visitor-auth-cookie' ,
57
66
} ) ;
58
67
59
68
if ( result . error ) {
@@ -66,17 +75,31 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
66
75
return NextResponse . redirect ( data . redirect ) ;
67
76
}
68
77
69
- const routeType = dynamicHeaders ? 'dynamic' : 'static' ;
78
+ // When visitor has authentication (adaptive content or VA), we serve dynamic routes.
79
+ let routeType = visitorToken ? 'dynamic' : 'static' ;
70
80
71
81
const requestHeaders = new Headers ( request . headers ) ;
72
82
requestHeaders . set ( MiddlewareHeaders . RouteType , routeType ) ;
73
83
requestHeaders . set ( MiddlewareHeaders . URLMode , mode ) ;
74
84
requestHeaders . set ( MiddlewareHeaders . SiteURL , `${ url . origin } ${ data . basePath } ` ) ;
75
85
requestHeaders . set ( MiddlewareHeaders . SiteURLData , JSON . stringify ( data ) ) ;
76
- if ( dynamicHeaders ) {
77
- for ( const [ key , value ] of Object . entries ( dynamicHeaders ) ) {
78
- requestHeaders . set ( key , value ) ;
79
- }
86
+
87
+ // Preview of customization/theme
88
+ const customization = url . searchParams . get ( 'customization' ) ;
89
+ if ( customization && validateSerializedCustomization ( customization ) ) {
90
+ routeType = 'dynamic' ;
91
+ requestHeaders . set ( MiddlewareHeaders . Customization , customization ) ;
92
+ }
93
+ const theme = url . searchParams . get ( 'theme' ) ;
94
+ if ( theme === CustomizationThemeMode . Dark || theme === CustomizationThemeMode . Light ) {
95
+ routeType = 'dynamic' ;
96
+ requestHeaders . set ( MiddlewareHeaders . Theme , theme ) ;
97
+ }
98
+
99
+ // We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
100
+ // This is useful for testing dynamic routes.
101
+ if ( request . cookies . has ( 'gitbook-dynamic-route' ) ) {
102
+ routeType = 'dynamic' ;
80
103
}
81
104
82
105
// Pass a x-forwarded-host and origin that are equal to ensure Next doesn't block server actions when proxied
@@ -102,6 +125,17 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
102
125
103
126
// Add Content Security Policy header
104
127
response . headers . set ( 'content-security-policy' , getContentSecurityPolicy ( ) ) ;
128
+ // Basic security headers
129
+ response . headers . set ( 'strict-transport-security' , 'max-age=31536000' ) ;
130
+ response . headers . set ( 'referrer-policy' , 'no-referrer-when-downgrade' ) ;
131
+ response . headers . set ( 'x-content-type-options' , 'nosniff' ) ;
132
+
133
+ if ( visitorToken ) {
134
+ const cookies = getResponseCookiesForVisitorAuth ( data . basePath , visitorToken ) ;
135
+ for ( const [ key , value ] of Object . entries ( cookies ) ) {
136
+ response . cookies . set ( key , value . value , value . options ) ;
137
+ }
138
+ }
105
139
106
140
return response ;
107
141
}
@@ -121,9 +155,13 @@ function serveErrorResponse(error: Error) {
121
155
}
122
156
123
157
/**
124
- * The URL of the GitBook content can be passed in 3 different ways:
125
- * - The request URL is in the `X-GitBook-URL` header.
126
- * - The request URL is matching `/url/:url`
158
+ * The URL of the GitBook content can be passed in 3 different ways (in order of priority):
159
+ * - The request has a `X-GitBook-URL` header:
160
+ * URL is taken from the header.
161
+ * - The request has a `X-Forwarded-Host` header:
162
+ * Host is taken from the header, pathname is taken from the request URL.
163
+ * - The request URL is matching `/url/:url`:
164
+ * URL is taken from the pathname.
127
165
*/
128
166
function extractURL ( request : NextRequest ) : URLWithMode | null {
129
167
const xGitbookUrl = request . headers . get ( 'x-gitbook-url' ) ;
@@ -134,6 +172,19 @@ function extractURL(request: NextRequest): URLWithMode | null {
134
172
} ;
135
173
}
136
174
175
+ const xForwardedHost = request . headers . get ( 'x-forwarded-host' ) ;
176
+ // The x-forwarded-host is set by Vercel for all requests
177
+ // so we ignore it if the hostname is the same as the instance one.
178
+ if ( xForwardedHost && GITBOOK_URL && new URL ( GITBOOK_URL ) . host !== xForwardedHost ) {
179
+ return {
180
+ url : appendQueryParams (
181
+ new URL ( `https://${ xForwardedHost } ${ request . nextUrl . pathname } ` ) ,
182
+ request . nextUrl . searchParams
183
+ ) ,
184
+ mode : 'url-host' ,
185
+ } ;
186
+ }
187
+
137
188
const prefix = '/url/' ;
138
189
if ( request . nextUrl . pathname . startsWith ( prefix ) ) {
139
190
return {
@@ -148,17 +199,6 @@ function extractURL(request: NextRequest): URLWithMode | null {
148
199
return null ;
149
200
}
150
201
151
- /**
152
- * Evaluate if a request is dynamic or static.
153
- */
154
- function getDynamicHeaders ( _url : URL , _request : NextRequest ) : null | Record < string , string > {
155
- // TODO:
156
- // - check token in query string
157
- // - check token in cookies
158
- // - check special headers or query string
159
- return null ;
160
- }
161
-
162
202
/**
163
203
* Encode path in a site content.
164
204
* Special paths are not encoded and passed to be handled by the route handlers.
0 commit comments