@@ -17,14 +17,15 @@ use bevy_ecs::system::{Res, Resource};
17
17
use bevy_tasks:: IoTaskPool ;
18
18
use core:: net:: { IpAddr , Ipv4Addr } ;
19
19
use http_body_util:: { BodyExt as _, Full } ;
20
+ pub use hyper:: header:: { HeaderName , HeaderValue } ;
20
21
use hyper:: {
21
22
body:: { Bytes , Incoming } ,
22
- header:: HeaderValue ,
23
23
server:: conn:: http1,
24
24
service, Request , Response ,
25
25
} ;
26
26
use serde_json:: Value ;
27
27
use smol_hyper:: rt:: { FuturesIo , SmolTimer } ;
28
+ use std:: collections:: HashMap ;
28
29
use std:: net:: TcpListener ;
29
30
use std:: net:: TcpStream ;
30
31
@@ -36,6 +37,37 @@ pub const DEFAULT_PORT: u16 = 15702;
36
37
/// The default host address that Bevy will use for its server.
37
38
pub const DEFAULT_ADDR : IpAddr = IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 ) ) ;
38
39
40
+ /// A struct that holds a collection of HTTP headers.
41
+ ///
42
+ /// This struct is used to store a set of HTTP headers as key-value pairs, where the keys are
43
+ /// of type [`HeaderName`] and the values are of type [`HeaderValue`].
44
+ ///
45
+ #[ derive( Debug , Resource , Clone ) ]
46
+ pub struct Headers {
47
+ headers : HashMap < HeaderName , HeaderValue > ,
48
+ }
49
+
50
+ impl Headers {
51
+ /// Create a new instance of `Headers`.
52
+ pub fn new ( ) -> Self {
53
+ Self {
54
+ headers : HashMap :: new ( ) ,
55
+ }
56
+ }
57
+
58
+ /// Add a key value pair to the `Headers` instance.
59
+ pub fn add ( mut self , key : HeaderName , value : HeaderValue ) -> Self {
60
+ self . headers . insert ( key, value) ;
61
+ self
62
+ }
63
+ }
64
+
65
+ impl Default for Headers {
66
+ fn default ( ) -> Self {
67
+ Self :: new ( )
68
+ }
69
+ }
70
+
39
71
/// Add this plugin to your [`App`] to allow remote connections over HTTP to inspect and modify entities.
40
72
/// It requires the [`RemotePlugin`](super::RemotePlugin).
41
73
///
@@ -44,18 +76,40 @@ pub const DEFAULT_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
44
76
/// The defaults are:
45
77
/// - [`DEFAULT_ADDR`] : 127.0.0.1.
46
78
/// - [`DEFAULT_PORT`] : 15702.
79
+ ///
80
+ /// /// # Example
81
+ ///
82
+ /// ```ignore
83
+ ///
84
+ /// // Create CORS headers
85
+ /// let cors_headers = Headers::new()
86
+ /// .add(HeaderName::from_static("Access-Control-Allow-Origin"), HeaderValue::from_static("*"))
87
+ /// .add(HeaderName::from_static("Access-Control-Allow-Headers"), HeaderValue::from_static("Content-Type, Authorization"));
88
+ ///
89
+ /// // Create the Bevy app and add the RemoteHttpPlugin with CORS headers
90
+ /// fn main() {
91
+ /// App::new()
92
+ /// .add_plugins(DefaultPlugins)
93
+ /// .add_plugins(RemoteHttpPlugin::default()
94
+ /// .with_headers(cors_headers))
95
+ /// .run();
96
+ /// }
97
+ /// ```
47
98
pub struct RemoteHttpPlugin {
48
99
/// The address that Bevy will bind to.
49
100
address : IpAddr ,
50
101
/// The port that Bevy will listen on.
51
102
port : u16 ,
103
+ /// The headers that Bevy will include in its HTTP responses
104
+ headers : Headers ,
52
105
}
53
106
54
107
impl Default for RemoteHttpPlugin {
55
108
fn default ( ) -> Self {
56
109
Self {
57
110
address : DEFAULT_ADDR ,
58
111
port : DEFAULT_PORT ,
112
+ headers : Headers :: new ( ) ,
59
113
}
60
114
}
61
115
}
@@ -64,6 +118,7 @@ impl Plugin for RemoteHttpPlugin {
64
118
fn build ( & self , app : & mut App ) {
65
119
app. insert_resource ( HostAddress ( self . address ) )
66
120
. insert_resource ( HostPort ( self . port ) )
121
+ . insert_resource ( HostHeaders ( self . headers . clone ( ) ) )
67
122
. add_systems ( Startup , start_http_server) ;
68
123
}
69
124
}
@@ -75,13 +130,34 @@ impl RemoteHttpPlugin {
75
130
self . address = address. into ( ) ;
76
131
self
77
132
}
78
-
79
133
/// Set the remote port that the server will listen on.
80
134
#[ must_use]
81
135
pub fn with_port ( mut self , port : u16 ) -> Self {
82
136
self . port = port;
83
137
self
84
138
}
139
+ /// Set the extra headers that the response will include.
140
+ #[ must_use]
141
+ pub fn with_headers ( mut self , headers : Headers ) -> Self {
142
+ self . headers = headers;
143
+ self
144
+ }
145
+ /// Add a single header to the response headers.
146
+ #[ must_use]
147
+ pub fn with_header (
148
+ mut self ,
149
+ name : impl TryInto < HeaderName > ,
150
+ value : impl TryInto < HeaderValue > ,
151
+ ) -> Self {
152
+ let Ok ( header_name) = name. try_into ( ) else {
153
+ panic ! ( "Invalid header name" )
154
+ } ;
155
+ let Ok ( header_value) = value. try_into ( ) else {
156
+ panic ! ( "Invalid header value" )
157
+ } ;
158
+ self . headers = self . headers . add ( header_name, header_value) ;
159
+ self
160
+ }
85
161
}
86
162
87
163
/// A resource containing the IP address that Bevy will host on.
@@ -98,17 +174,24 @@ pub struct HostAddress(pub IpAddr);
98
174
#[ derive( Debug , Resource ) ]
99
175
pub struct HostPort ( pub u16 ) ;
100
176
177
+ /// A resource containing the headers that Bevy will include in its HTTP responses.
178
+ ///
179
+ #[ derive( Debug , Resource ) ]
180
+ struct HostHeaders ( pub Headers ) ;
181
+
101
182
/// A system that starts up the Bevy Remote Protocol HTTP server.
102
183
fn start_http_server (
103
184
request_sender : Res < BrpSender > ,
104
185
address : Res < HostAddress > ,
105
186
remote_port : Res < HostPort > ,
187
+ headers : Res < HostHeaders > ,
106
188
) {
107
189
IoTaskPool :: get ( )
108
190
. spawn ( server_main (
109
191
address. 0 ,
110
192
remote_port. 0 ,
111
193
request_sender. clone ( ) ,
194
+ headers. 0 . clone ( ) ,
112
195
) )
113
196
. detach ( ) ;
114
197
}
@@ -118,25 +201,29 @@ async fn server_main(
118
201
address : IpAddr ,
119
202
port : u16 ,
120
203
request_sender : Sender < BrpMessage > ,
204
+ headers : Headers ,
121
205
) -> AnyhowResult < ( ) > {
122
206
listen (
123
207
Async :: < TcpListener > :: bind ( ( address, port) ) ?,
124
208
& request_sender,
209
+ & headers,
125
210
)
126
211
. await
127
212
}
128
213
129
214
async fn listen (
130
215
listener : Async < TcpListener > ,
131
216
request_sender : & Sender < BrpMessage > ,
217
+ headers : & Headers ,
132
218
) -> AnyhowResult < ( ) > {
133
219
loop {
134
220
let ( client, _) = listener. accept ( ) . await ?;
135
221
136
222
let request_sender = request_sender. clone ( ) ;
223
+ let headers = headers. clone ( ) ;
137
224
IoTaskPool :: get ( )
138
225
. spawn ( async move {
139
- let _ = handle_client ( client, request_sender) . await ;
226
+ let _ = handle_client ( client, request_sender, headers ) . await ;
140
227
} )
141
228
. detach ( ) ;
142
229
}
@@ -145,12 +232,15 @@ async fn listen(
145
232
async fn handle_client (
146
233
client : Async < TcpStream > ,
147
234
request_sender : Sender < BrpMessage > ,
235
+ headers : Headers ,
148
236
) -> AnyhowResult < ( ) > {
149
237
http1:: Builder :: new ( )
150
238
. timer ( SmolTimer :: new ( ) )
151
239
. serve_connection (
152
240
FuturesIo :: new ( client) ,
153
- service:: service_fn ( |request| process_request_batch ( request, & request_sender) ) ,
241
+ service:: service_fn ( |request| {
242
+ process_request_batch ( request, & request_sender, & headers)
243
+ } ) ,
154
244
)
155
245
. await ?;
156
246
@@ -162,6 +252,7 @@ async fn handle_client(
162
252
async fn process_request_batch (
163
253
request : Request < Incoming > ,
164
254
request_sender : & Sender < BrpMessage > ,
255
+ headers : & Headers ,
165
256
) -> AnyhowResult < Response < Full < Bytes > > > {
166
257
let batch_bytes = request. into_body ( ) . collect ( ) . await ?. to_bytes ( ) ;
167
258
let batch: Result < BrpBatch , _ > = serde_json:: from_slice ( & batch_bytes) ;
@@ -198,6 +289,9 @@ async fn process_request_batch(
198
289
hyper:: header:: CONTENT_TYPE ,
199
290
HeaderValue :: from_static ( "application/json" ) ,
200
291
) ;
292
+ for ( key, value) in & headers. headers {
293
+ response. headers_mut ( ) . insert ( key, value. clone ( ) ) ;
294
+ }
201
295
Ok ( response)
202
296
}
203
297
0 commit comments