@@ -53,6 +53,9 @@ pub enum Error {
53
53
source : ureq:: Error ,
54
54
} ,
55
55
56
+ #[ snafu( display( "Session username doesn't match configured username: {}" , username) ) ]
57
+ UsernameMismatch { username : String } ,
58
+
56
59
#[ snafu( display( "Could not complete API request: {}" , source) ) ]
57
60
Request { source : ureq:: Error } ,
58
61
@@ -99,37 +102,34 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
99
102
100
103
struct HttpWrapper {
101
104
/// Value of HTTP Authorization header.
102
- authorization : String ,
105
+ authorization : Option < String > ,
103
106
/// Persistent ureq agent to use for all HTTP requests.
104
107
agent : ureq:: Agent ,
105
108
}
106
109
107
110
impl HttpWrapper {
108
- fn new ( username : & str , password : & str , timeout : u64 ) -> Self {
109
- let safe_username = match username. find ( ':' ) {
110
- Some ( idx) => & username[ ..idx] ,
111
- None => username,
112
- } ;
113
- let authorization = format ! (
114
- "Basic {}" ,
115
- base64:: encode( format!( "{}:{}" , safe_username, password) )
116
- ) ;
111
+ fn new ( authorization : Option < String > , timeout : u64 ) -> Self {
117
112
let agent = ureq:: AgentBuilder :: new ( )
118
113
. redirect_auth_headers ( ureq:: RedirectAuthHeaders :: SameHost )
119
114
. timeout ( Duration :: from_secs ( timeout) )
120
115
. build ( ) ;
121
116
122
117
Self {
123
- agent,
124
118
authorization,
119
+ agent,
120
+ }
121
+ }
122
+
123
+ fn apply_authorization ( & self , req : ureq:: Request ) -> ureq:: Request {
124
+ match & self . authorization {
125
+ Some ( authorization) => req. set ( "Authorization" , authorization) ,
126
+ _ => req,
125
127
}
126
128
}
127
129
128
130
fn get_session ( & self , session_url : & str ) -> Result < ( String , jmap:: Session ) , ureq:: Error > {
129
131
let response = self
130
- . agent
131
- . get ( session_url)
132
- . set ( "Authorization" , & self . authorization )
132
+ . apply_authorization ( self . agent . get ( session_url) )
133
133
. call ( ) ?;
134
134
135
135
let session_url = response. get_url ( ) . to_string ( ) ;
@@ -139,9 +139,7 @@ impl HttpWrapper {
139
139
140
140
fn get_reader ( & self , url : & str ) -> Result < impl Read + Send > {
141
141
Ok ( self
142
- . agent
143
- . get ( url)
144
- . set ( "Authorization" , & self . authorization )
142
+ . apply_authorization ( self . agent . get ( url) )
145
143
. call ( )
146
144
. context ( ReadEmailBlobSnafu { } ) ?
147
145
. into_reader ( )
@@ -152,9 +150,7 @@ impl HttpWrapper {
152
150
153
151
fn post_string < D : DeserializeOwned > ( & self , url : & str , body : & str ) -> Result < D > {
154
152
let post = self
155
- . agent
156
- . post ( url)
157
- . set ( "Authorization" , & self . authorization )
153
+ . apply_authorization ( self . agent . post ( url) )
158
154
. send_string ( body)
159
155
. context ( RequestSnafu { } ) ?;
160
156
if log_enabled ! ( log:: Level :: Trace ) {
@@ -168,9 +164,7 @@ impl HttpWrapper {
168
164
169
165
fn post_json < S : Serialize , D : DeserializeOwned > ( & self , url : & str , body : S ) -> Result < D > {
170
166
let post = self
171
- . agent
172
- . post ( url)
173
- . set ( "Authorization" , & self . authorization )
167
+ . apply_authorization ( self . agent . post ( url) )
174
168
. send_json ( body)
175
169
. context ( RequestSnafu { } ) ?;
176
170
if log_enabled ! ( log:: Level :: Trace ) {
@@ -194,7 +188,8 @@ pub struct Remote {
194
188
impl Remote {
195
189
pub fn open ( config : & Config ) -> Result < Self > {
196
190
let password = config. password ( ) . context ( GetPasswordSnafu { } ) ?;
197
- match ( & config. fqdn , & config. session_url ) {
191
+
192
+ let remote = match ( & config. fqdn , & config. session_url ) {
198
193
( Some ( fqdn) , _) => {
199
194
Self :: open_host ( & fqdn, config. username . as_str ( ) , & password, config. timeout )
200
195
}
@@ -211,10 +206,19 @@ impl Remote {
211
206
. context ( NoDomainNameSnafu { } ) ?;
212
207
Self :: open_host ( domain, config. username . as_str ( ) , & password, config. timeout )
213
208
}
214
- }
209
+ } ?;
210
+
211
+ ensure ! (
212
+ remote. session. username == config. username,
213
+ UsernameMismatchSnafu {
214
+ username: remote. session. username
215
+ }
216
+ ) ;
217
+
218
+ Ok ( remote)
215
219
}
216
220
217
- pub fn open_host ( fqdn : & str , username : & str , password : & str , timeout : u64 ) -> Result < Self > {
221
+ fn open_host ( fqdn : & str , username : & str , password : & str , timeout : u64 ) -> Result < Self > {
218
222
let resolver = Resolver :: from_system_conf ( ) . context ( ParseResolvConfSnafu { } ) ?;
219
223
let mut address = format ! ( "_jmap._tcp.{}" , fqdn) ;
220
224
if !address. ends_with ( "." ) {
@@ -224,8 +228,6 @@ impl Remote {
224
228
. srv_lookup ( address. as_str ( ) )
225
229
. context ( SrvLookupSnafu { address } ) ?;
226
230
227
- let http_wrapper = HttpWrapper :: new ( username, password, timeout) ;
228
-
229
231
// Try all SRV names in order of priority.
230
232
let mut last_err = None ;
231
233
for name in resolver_response
@@ -238,38 +240,89 @@ impl Remote {
238
240
target. pop ( ) ;
239
241
240
242
let url = format ! ( "https://{}:{}/.well-known/jmap" , target, name. port( ) ) ;
241
- match http_wrapper. get_session ( url. as_str ( ) ) {
242
- Ok ( ( session_url, session) ) => {
243
- return Ok ( Remote {
244
- http_wrapper,
245
- session_url,
246
- session,
247
- } )
248
- }
249
-
250
- Err ( e) => last_err = Some ( ( url, e) ) ,
243
+ match Self :: open_url ( url. as_str ( ) , username, password, timeout) {
244
+ Ok ( s) => return Ok ( s) ,
245
+ Err ( e) => last_err = Some ( e) ,
251
246
} ;
252
247
}
253
248
// All of them failed! Return the last error.
254
- let ( session_url, error) = last_err. unwrap ( ) ;
255
- Err ( error) . context ( OpenSessionSnafu { session_url } )
249
+ Err ( last_err. unwrap ( ) )
256
250
}
257
251
258
- pub fn open_url (
259
- session_url : & str ,
260
- username : & str ,
261
- password : & str ,
262
- timeout : u64 ,
263
- ) -> Result < Self > {
264
- let http_wrapper = HttpWrapper :: new ( username, password, timeout) ;
265
- let ( session_url, session) = http_wrapper
266
- . get_session ( session_url)
267
- . context ( OpenSessionSnafu { session_url } ) ?;
268
- Ok ( Remote {
269
- http_wrapper,
270
- session_url,
271
- session,
272
- } )
252
+ fn open_url ( session_url : & str , username : & str , password : & str , timeout : u64 ) -> Result < Self > {
253
+ let agent = ureq:: AgentBuilder :: new ( )
254
+ . redirect_auth_headers ( ureq:: RedirectAuthHeaders :: SameHost )
255
+ . timeout ( Duration :: from_secs ( timeout) )
256
+ . build ( ) ;
257
+
258
+ match agent. get ( session_url) . call ( ) {
259
+ Ok ( r) => {
260
+ // Server returned success without authentication. Surprising, but valid.
261
+ let session_url = r. get_url ( ) . to_string ( ) ;
262
+ let session: jmap:: Session = r. into_json ( ) . context ( ResponseSnafu { } ) ?;
263
+ Ok ( Self {
264
+ http_wrapper : HttpWrapper :: new ( None , timeout) ,
265
+ session_url,
266
+ session,
267
+ } )
268
+ }
269
+
270
+ Err ( ureq:: Error :: Status ( code, ref r) ) if code == 401 => {
271
+ fn encode_basic ( username : & str , password : & str ) -> String {
272
+ let safe_username = match username. find ( ':' ) {
273
+ Some ( idx) => & username[ ..idx] ,
274
+ None => username,
275
+ } ;
276
+ format ! (
277
+ "Basic {}" ,
278
+ base64:: encode( format!( "{}:{}" , safe_username, password) )
279
+ )
280
+ }
281
+
282
+ let authorization = match r. header ( "WWW-Authenticate" ) {
283
+ Some ( v) if v. starts_with ( "Basic" ) => {
284
+ debug ! ( "server offered Basic auth" ) ;
285
+ Some ( encode_basic ( username, password) )
286
+ }
287
+
288
+ Some ( v) if v. starts_with ( "Bearer" ) => {
289
+ debug ! ( "server offered Bearer auth" ) ;
290
+ Some ( format ! ( "Bearer {}" , password) )
291
+ }
292
+
293
+ // Server didn't offer any auth schemes but still requires authentication.
294
+ // Probably it will accept Basic; try that.
295
+ None => {
296
+ debug ! ( "server requires auth but didn't offer a scheme, assuming Basic" ) ;
297
+ Some ( encode_basic ( username, password) )
298
+ }
299
+
300
+ // No authorization, which will make the next call fail, and then we'll just
301
+ // return an error.
302
+ Some ( v) => {
303
+ debug ! ( "server offered unsupported auth scheme: {}" , v) ;
304
+ None
305
+ }
306
+ } ;
307
+
308
+ let url = r. get_url ( ) ;
309
+
310
+ let mut req = agent. get ( url) ;
311
+ if let Some ( a) = & authorization {
312
+ req = req. set ( "Authorization" , a) ;
313
+ }
314
+
315
+ let r = req. call ( ) . context ( OpenSessionSnafu { session_url } ) ?;
316
+ let session: jmap:: Session = r. into_json ( ) . context ( ResponseSnafu { } ) ?;
317
+ Ok ( Self {
318
+ http_wrapper : HttpWrapper :: new ( authorization, timeout) ,
319
+ session_url : url. to_string ( ) ,
320
+ session,
321
+ } )
322
+ }
323
+
324
+ Err ( e) => Err ( e) . context ( OpenSessionSnafu { session_url } ) ,
325
+ }
273
326
}
274
327
275
328
/// Return a list of all `Email` IDs that exist on the server and a state `String` returned by
0 commit comments