OLD | NEW |
---|---|
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #import "ios/web/net/crw_cert_verification_controller.h" | 5 #import "ios/web/net/crw_cert_verification_controller.h" |
6 | 6 |
7 #include "base/logging.h" | 7 #include "base/logging.h" |
8 #include "base/mac/bind_objc_block.h" | 8 #include "base/mac/bind_objc_block.h" |
9 #import "base/memory/ref_counted.h" | 9 #import "base/memory/ref_counted.h" |
10 #import "base/memory/scoped_ptr.h" | 10 #import "base/memory/scoped_ptr.h" |
11 #include "base/strings/sys_string_conversions.h" | 11 #include "base/strings/sys_string_conversions.h" |
12 #include "base/threading/worker_pool.h" | 12 #include "base/threading/worker_pool.h" |
13 #include "ios/web/net/cert_verifier_block_adapter.h" | 13 #include "ios/web/net/cert_verifier_block_adapter.h" |
14 #include "ios/web/public/browser_state.h" | 14 #include "ios/web/public/browser_state.h" |
15 #include "ios/web/public/certificate_policy_cache.h" | |
15 #include "ios/web/public/web_thread.h" | 16 #include "ios/web/public/web_thread.h" |
16 #import "ios/web/web_state/wk_web_view_security_util.h" | 17 #import "ios/web/web_state/wk_web_view_security_util.h" |
17 #include "net/cert/cert_verify_result.h" | 18 #include "net/cert/cert_verify_result.h" |
18 #include "net/ssl/ssl_config_service.h" | 19 #include "net/ssl/ssl_config_service.h" |
19 #include "net/url_request/url_request_context.h" | 20 #include "net/url_request/url_request_context.h" |
20 #include "net/url_request/url_request_context_getter.h" | 21 #include "net/url_request/url_request_context_getter.h" |
21 | 22 |
22 namespace { | 23 namespace { |
23 | 24 |
24 // This class takes ownership of block and releases it on UI thread, even if | 25 // This class takes ownership of block and releases it on UI thread, even if |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
60 | 61 |
61 } // namespace | 62 } // namespace |
62 | 63 |
63 @interface CRWCertVerificationController () { | 64 @interface CRWCertVerificationController () { |
64 // Cert verification object which wraps |net::CertVerifier|. Must be created, | 65 // Cert verification object which wraps |net::CertVerifier|. Must be created, |
65 // used and destroyed on IO Thread. | 66 // used and destroyed on IO Thread. |
66 scoped_ptr<web::CertVerifierBlockAdapter> _certVerifier; | 67 scoped_ptr<web::CertVerifierBlockAdapter> _certVerifier; |
67 | 68 |
68 // URLRequestContextGetter for obtaining net layer objects. | 69 // URLRequestContextGetter for obtaining net layer objects. |
69 net::URLRequestContextGetter* _contextGetter; | 70 net::URLRequestContextGetter* _contextGetter; |
71 | |
72 // Used to remember user exceptions to invalid certs. | |
73 scoped_refptr<web::CertificatePolicyCache> _certPolicyCache; | |
70 } | 74 } |
71 | 75 |
72 // Cert verification flags. Must be used on IO Thread. | 76 // Cert verification flags. Must be used on IO Thread. |
73 @property(nonatomic, readonly) int certVerifyFlags; | 77 @property(nonatomic, readonly) int certVerifyFlags; |
74 | 78 |
75 // Creates _certVerifier object on IO thread. | 79 // Creates _certVerifier object on IO thread. |
76 - (void)createCertVerifier; | 80 - (void)createCertVerifier; |
77 | 81 |
78 // Verifies the given |cert| for the given |host| using |net::CertVerifier| and | 82 // Verifies the given |cert| for the given |host| using |net::CertVerifier| and |
79 // calls |completionHandler| on completion. This method can be called on any | 83 // calls |completionHandler| on completion. This method can be called on any |
80 // thread. |completionHandler| cannot be null and will be called asynchronously | 84 // thread. |completionHandler| cannot be null and will be called asynchronously |
81 // on IO thread. | 85 // on IO thread or synchronously on current thread if IO task can't start (in |
86 // this case |dispatched| argument will be NO). | |
Ryan Sleevi
2015/10/19 23:56:22
This is really surprising; should this be in a sep
Eugene But (OOO till 7-30)
2015/10/21 04:01:02
WKWebView throws an exception (which leads to a cr
| |
82 - (void)verifyCert:(const scoped_refptr<net::X509Certificate>&)cert | 87 - (void)verifyCert:(const scoped_refptr<net::X509Certificate>&)cert |
83 forHost:(NSString*)host | 88 forHost:(NSString*)host |
84 completionHandler:(void (^)(net::CertVerifyResult, int))completionHandler; | 89 completionHandler:(void (^)(net::CertVerifyResult, BOOL dispatched))handler; |
85 | 90 |
86 // Verifies the given |trust| using SecTrustRef API. |completionHandler| cannot | 91 // Verifies the given |trust| using SecTrustRef API. |completionHandler| cannot |
87 // be null and will be either called asynchronously on Worker thread or | 92 // be null and will be either called asynchronously on Worker thread or |
88 // synchronously on current thread if the worker task can't start (in this | 93 // synchronously on current thread if the worker task can't start (in this |
89 // case |dispatched| argument will be NO). | 94 // case |dispatched| argument will be NO). |
90 - (void)verifyTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust | 95 - (void)verifyTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust |
91 completionHandler:(void (^)(SecTrustResultType, BOOL dispatched))handler; | 96 completionHandler:(void (^)(SecTrustResultType, BOOL dispatched))handler; |
92 | 97 |
98 // Returns cert accept policy for the given SecTrust result. |trustResult| must | |
99 // not be for a valid cert. Must be called on IO thread. | |
100 - (web::CertAcceptPolicy) | |
101 loadPolicyForBadTrustResult:(SecTrustResultType)trustResult | |
102 certVerifierResult:(net::CertVerifyResult)certVerifierResult | |
103 serverTrust:(SecTrustRef)trust | |
104 host:(NSString*)host; | |
105 | |
93 @end | 106 @end |
94 | 107 |
95 @implementation CRWCertVerificationController | 108 @implementation CRWCertVerificationController |
96 | 109 |
97 #pragma mark - Superclass | 110 #pragma mark - Superclass |
98 | 111 |
99 - (void)dealloc { | 112 - (void)dealloc { |
100 DCHECK(!_certVerifier); | 113 DCHECK(!_certVerifier); |
101 [super dealloc]; | 114 [super dealloc]; |
102 } | 115 } |
103 | 116 |
104 #pragma mark - Public | 117 #pragma mark - Public |
105 | 118 |
106 - (instancetype)init { | 119 - (instancetype)init { |
107 NOTREACHED(); | 120 NOTREACHED(); |
108 return nil; | 121 return nil; |
109 } | 122 } |
110 | 123 |
111 - (instancetype)initWithBrowserState:(web::BrowserState*)browserState { | 124 - (instancetype)initWithBrowserState:(web::BrowserState*)browserState { |
112 DCHECK(browserState); | 125 DCHECK(browserState); |
113 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | 126 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
114 self = [super init]; | 127 self = [super init]; |
115 if (self) { | 128 if (self) { |
116 _contextGetter = browserState->GetRequestContext(); | 129 _contextGetter = browserState->GetRequestContext(); |
117 DCHECK(_contextGetter); | 130 DCHECK(_contextGetter); |
131 _certPolicyCache = | |
132 web::BrowserState::GetCertificatePolicyCache(browserState); | |
133 | |
118 [self createCertVerifier]; | 134 [self createCertVerifier]; |
119 } | 135 } |
120 return self; | 136 return self; |
121 } | 137 } |
122 | 138 |
123 - (void)decidePolicyForCert:(const scoped_refptr<net::X509Certificate>&)cert | 139 - (void)decideLoadPolicyForTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust |
124 host:(NSString*)host | 140 host:(NSString*)host |
125 completionHandler:(web::PolicyDecisionHandler)completionHandler { | 141 completionHandler:(web::PolicyDecisionHandler)completionHandler { |
126 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | 142 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
127 // completionHandler of |verifyCert:forHost:completionHandler:| is called on | 143 // completionHandler of |verifyCert:forHost:completionHandler:| is called on |
128 // IO thread and then bounces back to UI thread. As a result all objects | 144 // IO thread and then bounces back to UI thread. As a result all objects |
129 // captured by completionHandler may be released on either UI or IO thread. | 145 // captured by completionHandler may be released on either UI or IO thread. |
130 // Since |completionHandler| can potentially capture multiple thread unsafe | 146 // Since |completionHandler| can potentially capture multiple thread unsafe |
131 // objects (like Web Controller) |completionHandler| itself should never be | 147 // objects (like Web Controller) |completionHandler| itself should never be |
132 // released on background thread and |BlockHolder| ensures that. | 148 // released on background thread and |BlockHolder| ensures that. |
133 __block scoped_refptr<BlockHolder<web::PolicyDecisionHandler>> handlerHolder( | 149 __block scoped_refptr<BlockHolder<web::PolicyDecisionHandler>> handlerHolder( |
134 new BlockHolder<web::PolicyDecisionHandler>(completionHandler)); | 150 new BlockHolder<web::PolicyDecisionHandler>(completionHandler)); |
135 [self verifyCert:cert | 151 [self verifyTrust:trust |
136 forHost:host | 152 completionHandler:^(SecTrustResultType trustResult, BOOL dispatched) { |
137 completionHandler:^(net::CertVerifyResult result, int error) { | 153 if (!dispatched) { |
138 web::CertAcceptPolicy policy = | 154 // Cert verification task did not start. |
139 web::CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR; | 155 dispatch_async(dispatch_get_main_queue(), ^{ |
140 if (error == net::OK) { | 156 handlerHolder->call(web::CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR, |
141 policy = web::CERT_ACCEPT_POLICY_ALLOW; | 157 net::CertStatus()); |
142 } else if (net::IsCertStatusError(result.cert_status)) { | 158 }); |
143 policy = net::IsCertStatusMinorError(result.cert_status) | 159 return; |
144 ? web::CERT_ACCEPT_POLICY_ALLOW | |
145 : web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR; | |
146 } | 160 } |
147 | 161 |
148 dispatch_async(dispatch_get_main_queue(), ^{ | 162 if (web::GetSecurityStyleFromTrustResult(trustResult) == |
149 handlerHolder->call(policy, result.cert_status); | 163 web::SECURITY_STYLE_AUTHENTICATED) { |
150 }); | 164 // SecTrust API considers this cert as valid. |
165 dispatch_async(dispatch_get_main_queue(), ^{ | |
166 handlerHolder->call(web::CERT_ACCEPT_POLICY_ALLOW, | |
167 net::CertStatus()); | |
168 }); | |
169 return; | |
170 } | |
171 | |
172 // SecTrust API considers this cert as invalid. Check the reason and | |
173 // whether or not user has decided to proceed with this bad cert. | |
174 scoped_refptr<net::X509Certificate> cert( | |
175 web::CreateCertFromTrust(trust)); | |
176 [self verifyCert:cert | |
177 forHost:host | |
178 completionHandler:^(net::CertVerifyResult certVerifierResult, | |
179 BOOL dispatched) { | |
180 if (!dispatched) { | |
181 // Cert verification task did not start. | |
182 dispatch_async(dispatch_get_main_queue(), ^{ | |
183 handlerHolder->call( | |
184 web::CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR, | |
185 net::CertStatus()); | |
186 }); | |
187 return; | |
188 } | |
189 | |
190 web::CertAcceptPolicy policy = | |
191 [self loadPolicyForBadTrustResult:trustResult | |
192 certVerifierResult:certVerifierResult | |
193 serverTrust:trust.get() | |
194 host:host]; | |
195 | |
196 dispatch_async(dispatch_get_main_queue(), ^{ | |
197 handlerHolder->call(policy, certVerifierResult.cert_status); | |
198 }); | |
199 }]; | |
151 }]; | 200 }]; |
152 } | 201 } |
153 | 202 |
154 - (void)querySSLStatusForTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust | 203 - (void)querySSLStatusForTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust |
155 host:(NSString*)host | 204 host:(NSString*)host |
156 completionHandler:(web::StatusQueryHandler)completionHandler { | 205 completionHandler:(web::StatusQueryHandler)completionHandler { |
157 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | 206 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
158 | 207 |
159 // The completion handlers of |verifyCert:forHost:completionHandler:| and | 208 // The completion handlers of |verifyCert:forHost:completionHandler:| and |
160 // |verifyTrust:completionHandler:| will be deallocated on background thread. | 209 // |verifyTrust:completionHandler:| will be deallocated on background thread. |
(...skipping 23 matching lines...) Expand all Loading... | |
184 | 233 |
185 // Retrieve the net::CertStatus for invalid certificates to determine | 234 // Retrieve the net::CertStatus for invalid certificates to determine |
186 // the rejection reason, it is possible that rejection reason could not | 235 // the rejection reason, it is possible that rejection reason could not |
187 // be determined and |cert_status| will be empty. | 236 // be determined and |cert_status| will be empty. |
188 // TODO(eugenebut): Add UMA for CertVerifier and SecTrust verification | 237 // TODO(eugenebut): Add UMA for CertVerifier and SecTrust verification |
189 // mismatch (crbug.com/535699). | 238 // mismatch (crbug.com/535699). |
190 scoped_refptr<net::X509Certificate> cert( | 239 scoped_refptr<net::X509Certificate> cert( |
191 web::CreateCertFromTrust(trust)); | 240 web::CreateCertFromTrust(trust)); |
192 [self verifyCert:cert | 241 [self verifyCert:cert |
193 forHost:host | 242 forHost:host |
194 completionHandler:^(net::CertVerifyResult certVerifierResult, int) { | 243 completionHandler:^(net::CertVerifyResult certVerifierResult, |
244 BOOL) { | |
195 dispatch_async(dispatch_get_main_queue(), ^{ | 245 dispatch_async(dispatch_get_main_queue(), ^{ |
196 handlerHolder->call(securityStyle, | 246 handlerHolder->call(securityStyle, |
197 certVerifierResult.cert_status); | 247 certVerifierResult.cert_status); |
198 }); | 248 }); |
199 }]; | 249 }]; |
200 }]; | 250 }]; |
201 } | 251 } |
202 | 252 |
253 - (void)allowCert:(scoped_refptr<net::X509Certificate>)cert | |
254 forHost:(NSString*)host | |
255 status:(net::CertStatus)status { | |
256 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | |
257 // Store user decision only for leaf server cert, and drop any intermediates. | |
258 // This is a workaround for WKWebView behavior, where different delegate | |
259 // methods receive different certificate chains for the same server cert. | |
Ryan Sleevi
2015/10/19 23:56:21
You should explain more of the context in the code
Eugene But (OOO till 7-30)
2015/10/21 04:01:02
Done.
| |
260 if (cert->GetIntermediateCertificates().size() > 0) { | |
261 cert = net::X509Certificate::CreateFromHandle( | |
262 cert->os_cert_handle(), net::X509Certificate::OSCertHandles()); | |
263 } | |
264 DCHECK_EQ(0U, cert->GetIntermediateCertificates().size()); | |
265 web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, base::BindBlock(^{ | |
266 _certPolicyCache->AllowCertForHost( | |
267 cert.get(), base::SysNSStringToUTF8(host), status); | |
268 })); | |
269 } | |
270 | |
203 - (void)shutDown { | 271 - (void)shutDown { |
204 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | 272 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
205 web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, base::BindBlock(^{ | 273 web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, base::BindBlock(^{ |
206 // This block captures |self| delaying its deallocation and causing dealloc | 274 // This block captures |self| delaying its deallocation and causing dealloc |
207 // to happen on either IO or UI thread (which is fine for this class). | 275 // to happen on either IO or UI thread (which is fine for this class). |
208 _certVerifier.reset(); | 276 _certVerifier.reset(); |
209 })); | 277 })); |
210 } | 278 } |
211 | 279 |
212 #pragma mark - Private | 280 #pragma mark - Private |
(...skipping 15 matching lines...) Expand all Loading... | |
228 - (void)createCertVerifier { | 296 - (void)createCertVerifier { |
229 web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, base::BindBlock(^{ | 297 web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, base::BindBlock(^{ |
230 net::URLRequestContext* context = _contextGetter->GetURLRequestContext(); | 298 net::URLRequestContext* context = _contextGetter->GetURLRequestContext(); |
231 _certVerifier.reset(new web::CertVerifierBlockAdapter( | 299 _certVerifier.reset(new web::CertVerifierBlockAdapter( |
232 context->cert_verifier(), context->net_log())); | 300 context->cert_verifier(), context->net_log())); |
233 })); | 301 })); |
234 } | 302 } |
235 | 303 |
236 - (void)verifyCert:(const scoped_refptr<net::X509Certificate>&)cert | 304 - (void)verifyCert:(const scoped_refptr<net::X509Certificate>&)cert |
237 forHost:(NSString*)host | 305 forHost:(NSString*)host |
238 completionHandler:(void (^)(net::CertVerifyResult, int))completionHandler { | 306 completionHandler:(void (^)(net::CertVerifyResult, BOOL))completionHandler { |
239 DCHECK(completionHandler); | 307 DCHECK(completionHandler); |
240 __block scoped_refptr<net::X509Certificate> blockCert = cert; | 308 __block scoped_refptr<net::X509Certificate> blockCert = cert; |
241 web::WebThread::PostTask( | 309 bool dispatched = web::WebThread::PostTask( |
242 web::WebThread::IO, FROM_HERE, base::BindBlock(^{ | 310 web::WebThread::IO, FROM_HERE, base::BindBlock(^{ |
243 // WeakNSObject does not work across different threads, hence this block | 311 // WeakNSObject does not work across different threads, hence this block |
244 // retains self. | 312 // retains self. |
245 if (!_certVerifier) { | 313 if (!_certVerifier) { |
246 completionHandler(net::CertVerifyResult(), net::ERR_FAILED); | 314 completionHandler(net::CertVerifyResult(), net::ERR_FAILED); |
247 return; | 315 return; |
248 } | 316 } |
249 | 317 |
250 web::CertVerifierBlockAdapter::Params params( | 318 web::CertVerifierBlockAdapter::Params params( |
251 blockCert.Pass(), base::SysNSStringToUTF8(host)); | 319 blockCert.Pass(), base::SysNSStringToUTF8(host)); |
252 params.flags = self.certVerifyFlags; | 320 params.flags = self.certVerifyFlags; |
253 params.crl_set = net::SSLConfigService::GetCRLSet(); | 321 params.crl_set = net::SSLConfigService::GetCRLSet(); |
254 // OCSP response is not provided by iOS API. | 322 // OCSP response is not provided by iOS API. |
255 _certVerifier->Verify(params, completionHandler); | 323 _certVerifier->Verify(params, ^(net::CertVerifyResult result, int) { |
324 completionHandler(result, YES); | |
325 }); | |
256 })); | 326 })); |
327 | |
328 if (!dispatched) { | |
329 completionHandler(net::CertVerifyResult(), NO); | |
Ryan Sleevi
2015/10/19 23:56:21
Are you sure this is safe to recursively call into
Eugene But (OOO till 7-30)
2015/10/21 04:01:02
Yes, this is safe. Maybe weird, but still safe.
| |
330 } | |
257 } | 331 } |
258 | 332 |
259 - (void)verifyTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust | 333 - (void)verifyTrust:(base::ScopedCFTypeRef<SecTrustRef>)trust |
260 completionHandler:(void (^)(SecTrustResultType, BOOL))completionHandler { | 334 completionHandler:(void (^)(SecTrustResultType, BOOL))completionHandler { |
261 DCHECK(completionHandler); | 335 DCHECK(completionHandler); |
262 // SecTrustEvaluate performs trust evaluation synchronously, possibly making | 336 // SecTrustEvaluate performs trust evaluation synchronously, possibly making |
263 // network requests. The UI thread should not be blocked by that operation. | 337 // network requests. The UI thread should not be blocked by that operation. |
264 bool dispatched = base::WorkerPool::PostTask(FROM_HERE, base::BindBlock(^{ | 338 bool dispatched = base::WorkerPool::PostTask(FROM_HERE, base::BindBlock(^{ |
265 SecTrustResultType trustResult = kSecTrustResultInvalid; | 339 SecTrustResultType trustResult = kSecTrustResultInvalid; |
266 if (SecTrustEvaluate(trust.get(), &trustResult) != errSecSuccess) { | 340 if (SecTrustEvaluate(trust.get(), &trustResult) != errSecSuccess) { |
267 trustResult = kSecTrustResultInvalid; | 341 trustResult = kSecTrustResultInvalid; |
268 } | 342 } |
269 completionHandler(trustResult, YES); | 343 completionHandler(trustResult, YES); |
270 }), false /* task_is_slow */); | 344 }), false /* task_is_slow */); |
271 | 345 |
272 if (!dispatched) { | 346 if (!dispatched) { |
273 completionHandler(kSecTrustResultInvalid, NO); | 347 completionHandler(kSecTrustResultInvalid, NO); |
274 } | 348 } |
275 } | 349 } |
276 | 350 |
351 - (web::CertAcceptPolicy) | |
352 loadPolicyForBadTrustResult:(SecTrustResultType)trustResult | |
353 certVerifierResult:(net::CertVerifyResult)certVerifierResult | |
354 serverTrust:(SecTrustRef)trust | |
355 host:(NSString*)host { | |
356 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); | |
357 DCHECK_NE(web::SECURITY_STYLE_AUTHENTICATED, | |
358 web::GetSecurityStyleFromTrustResult(trustResult)); | |
359 | |
360 if (trustResult != kSecTrustResultRecoverableTrustFailure || | |
361 SecTrustGetCertificateCount(trust) == 0) { | |
362 // Trust result is not recoverable or leaf cert is missing. | |
363 return web::CERT_ACCEPT_POLICY_NON_RECOVERABLE_ERROR; | |
364 } | |
365 | |
366 // Check if user has decided to proceed with this bad cert. | |
367 scoped_refptr<net::X509Certificate> leafCert = | |
368 net::X509Certificate::CreateFromHandle( | |
369 SecTrustGetCertificateAtIndex(trust, 0), | |
370 net::X509Certificate::OSCertHandles()); | |
371 | |
372 web::CertPolicy::Judgment judgment = _certPolicyCache->QueryPolicy( | |
373 leafCert.get(), base::SysNSStringToUTF8(host), | |
374 certVerifierResult.cert_status); | |
375 | |
376 return (judgment == web::CertPolicy::ALLOWED) | |
377 ? web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_ACCEPTED_BY_USER | |
378 : web::CERT_ACCEPT_POLICY_RECOVERABLE_ERROR_UNDECIDED_BY_USER; | |
379 } | |
380 | |
277 @end | 381 @end |
OLD | NEW |