1
+ import type { IncomingMessage , Server , ServerResponse } from 'http' ;
2
+ import type { Socket } from 'net' ;
3
+
1
4
import assert from 'assert' ;
2
- import fs from 'fs' ;
3
- import http from 'http' ;
4
- import https from 'https' ;
5
- import net from 'net' ;
6
- import path from 'path' ;
7
- import url from 'url' ;
5
+ import { existsSync , readFileSync } from 'fs' ;
6
+ import { createServer } from 'http' ;
7
+ import { createServer as createSecureServer } from 'https' ;
8
+ import { posix } from 'path' ;
9
+ import { parse } from 'url' ;
8
10
9
11
import bind from 'bind-decorator' ;
10
12
import gzipStatic , { RequestHandler } from 'connect-gzip-static' ;
@@ -29,10 +31,10 @@ export default class Frontend extends Extension {
29
31
private sslCert : string | undefined ;
30
32
private sslKey : string | undefined ;
31
33
private authToken : string | undefined ;
32
- private server : http . Server | undefined ;
33
- private fileServer : RequestHandler | undefined ;
34
- private wss : WebSocket . Server | undefined ;
35
- private frontendBaseUrl : string ;
34
+ private server ! : Server ;
35
+ private fileServer ! : RequestHandler ;
36
+ private wss ! : WebSocket . Server ;
37
+ private baseUrl : string ;
36
38
37
39
constructor (
38
40
zigbee : Zigbee ,
@@ -53,16 +55,13 @@ export default class Frontend extends Extension {
53
55
this . sslCert = frontendSettings . ssl_cert ;
54
56
this . sslKey = frontendSettings . ssl_key ;
55
57
this . authToken = frontendSettings . auth_token ;
58
+ this . baseUrl = frontendSettings . base_url ;
56
59
this . mqttBaseTopic = settings . get ( ) . mqtt . base_topic ;
57
- this . frontendBaseUrl = settings . get ( ) . frontend ?. base_url ?? '/' ;
58
- if ( ! this . frontendBaseUrl . startsWith ( '/' ) ) {
59
- this . frontendBaseUrl = '/' + this . frontendBaseUrl ;
60
- }
61
60
}
62
61
63
62
private isHttpsConfigured ( ) : boolean {
64
63
if ( this . sslCert && this . sslKey ) {
65
- if ( ! fs . existsSync ( this . sslCert ) || ! fs . existsSync ( this . sslKey ) ) {
64
+ if ( ! existsSync ( this . sslCert ) || ! existsSync ( this . sslKey ) ) {
66
65
logger . error ( `defined ssl_cert '${ this . sslCert } ' or ssl_key '${ this . sslKey } ' file path does not exists, server won't be secured.` ) ;
67
66
return false ;
68
67
}
@@ -72,30 +71,30 @@ export default class Frontend extends Extension {
72
71
}
73
72
74
73
override async start ( ) : Promise < void > {
75
- if ( this . isHttpsConfigured ( ) ) {
76
- const serverOptions = {
77
- key : fs . readFileSync ( this . sslKey ! ) , // valid from `isHttpsConfigured`
78
- cert : fs . readFileSync ( this . sslCert ! ) , // valid from `isHttpsConfigured`
79
- } ;
80
- this . server = https . createServer ( serverOptions , this . onRequest ) ;
81
- } else {
82
- this . server = http . createServer ( this . onRequest ) ;
83
- }
84
-
85
- this . server . on ( 'upgrade' , this . onUpgrade ) ;
86
-
87
74
/* istanbul ignore next */
88
75
const options = {
89
- setHeaders : ( res : http . ServerResponse , path : string ) : void => {
76
+ setHeaders : ( res : ServerResponse , path : string ) : void => {
90
77
if ( path . endsWith ( 'index.html' ) ) {
91
78
res . setHeader ( 'Cache-Control' , 'no-store' ) ;
92
79
}
93
80
} ,
94
81
} ;
95
82
this . fileServer = gzipStatic ( frontend . getPath ( ) , options ) ;
96
- this . wss = new WebSocket . Server ( { noServer : true , path : path . posix . join ( this . frontendBaseUrl , 'api' ) } ) ;
83
+ this . wss = new WebSocket . Server ( { noServer : true , path : posix . join ( this . baseUrl , 'api' ) } ) ;
84
+
97
85
this . wss . on ( 'connection' , this . onWebSocketConnection ) ;
98
86
87
+ if ( this . isHttpsConfigured ( ) ) {
88
+ const serverOptions = {
89
+ key : readFileSync ( this . sslKey ! ) , // valid from `isHttpsConfigured`
90
+ cert : readFileSync ( this . sslCert ! ) , // valid from `isHttpsConfigured`
91
+ } ;
92
+ this . server = createSecureServer ( serverOptions , this . onRequest ) ;
93
+ } else {
94
+ this . server = createServer ( this . onRequest ) ;
95
+ }
96
+
97
+ this . server . on ( 'upgrade' , this . onUpgrade ) ;
99
98
this . eventBus . onMQTTMessagePublished ( this , this . onMQTTPublishMessage ) ;
100
99
101
100
if ( ! this . host ) {
@@ -112,43 +111,42 @@ export default class Frontend extends Extension {
112
111
113
112
override async stop ( ) : Promise < void > {
114
113
await super . stop ( ) ;
115
- this . wss ? .clients . forEach ( ( client ) => {
114
+ this . wss . clients . forEach ( ( client ) => {
116
115
client . send ( stringify ( { topic : 'bridge/state' , payload : 'offline' } ) ) ;
117
116
client . terminate ( ) ;
118
117
} ) ;
119
- this . wss ?. close ( ) ;
120
- /* istanbul ignore else */
121
- if ( this . server ) {
122
- return await new Promise ( ( cb : ( ) => void ) => this . server ! . close ( cb ) ) ;
123
- }
118
+ this . wss . close ( ) ;
119
+
120
+ await new Promise ( ( resolve ) => this . server . close ( resolve ) ) ;
124
121
}
125
122
126
- @bind private onRequest ( request : http . IncomingMessage , response : http . ServerResponse ) : void {
123
+ @bind private onRequest ( request : IncomingMessage , response : ServerResponse ) : void {
127
124
const fin = finalhandler ( request , response ) ;
125
+ const newUrl = posix . relative ( this . baseUrl , request . url ! ) ;
128
126
129
- const newUrl = path . posix . relative ( this . frontendBaseUrl , request . url ! ) ;
130
127
// The request url is not within the frontend base url, so the relative path starts with '..'
131
128
if ( newUrl . startsWith ( '.' ) ) {
132
129
return fin ( ) ;
133
130
}
134
131
135
- // Attach originalUrl so that static-server can perform a redirect to '/' when serving the
136
- // root directory. This is necessary for the browser to resolve relative assets paths correctly.
132
+ // Attach originalUrl so that static-server can perform a redirect to '/' when serving the root directory.
133
+ // This is necessary for the browser to resolve relative assets paths correctly.
137
134
request . originalUrl = request . url ;
138
135
request . url = '/' + newUrl ;
139
- this . fileServer ?.( request , response , fin ) ;
136
+
137
+ this . fileServer ( request , response , fin ) ;
140
138
}
141
139
142
- private authenticate ( request : http . IncomingMessage , cb : ( authenticate : boolean ) => void ) : void {
143
- const { query} = url . parse ( request . url ! , true ) ;
140
+ private authenticate ( request : IncomingMessage , cb : ( authenticate : boolean ) => void ) : void {
141
+ const { query} = parse ( request . url ! , true ) ;
144
142
cb ( ! this . authToken || this . authToken === query . token ) ;
145
143
}
146
144
147
- @bind private onUpgrade ( request : http . IncomingMessage , socket : net . Socket , head : Buffer ) : void {
148
- this . wss ! . handleUpgrade ( request , socket , head , ( ws ) => {
145
+ @bind private onUpgrade ( request : IncomingMessage , socket : Socket , head : Buffer ) : void {
146
+ this . wss . handleUpgrade ( request , socket , head , ( ws ) => {
149
147
this . authenticate ( request , ( isAuthenticated ) => {
150
148
if ( isAuthenticated ) {
151
- this . wss ! . emit ( 'connection' , ws , request ) ;
149
+ this . wss . emit ( 'connection' , ws , request ) ;
152
150
} else {
153
151
ws . close ( 4401 , 'Unauthorized' ) ;
154
152
}
@@ -182,6 +180,7 @@ export default class Frontend extends Extension {
182
180
for ( const device of this . zigbee . devicesIterator ( utils . deviceNotCoordinator ) ) {
183
181
const payload = this . state . get ( device ) ;
184
182
const lastSeen = settings . get ( ) . advanced . last_seen ;
183
+
185
184
/* istanbul ignore if */
186
185
if ( lastSeen !== 'disable' ) {
187
186
payload . last_seen = utils . formatDate ( device . zh . lastSeen ?? 0 , lastSeen ) ;
@@ -202,7 +201,7 @@ export default class Frontend extends Extension {
202
201
const topic = data . topic . substring ( this . mqttBaseTopic . length + 1 ) ;
203
202
const payload = utils . parseJSON ( data . payload , data . payload ) ;
204
203
205
- for ( const client of this . wss ! . clients ) {
204
+ for ( const client of this . wss . clients ) {
206
205
/* istanbul ignore else */
207
206
if ( client . readyState === WebSocket . OPEN ) {
208
207
client . send ( stringify ( { topic, payload} ) ) ;
0 commit comments