]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- |
2 | // vim: ts=8 sw=2 smarttab | |
3 | /* | |
4 | * Ceph - scalable distributed file system | |
5 | * | |
6 | * Copyright (C) 2004-2009 Sage Weil <sage@newdream.net> | |
7 | * | |
8 | * This is free software; you can redistribute it and/or | |
9 | * modify it under the terms of the GNU Lesser General Public | |
10 | * License version 2.1, as published by the Free Software | |
11 | * Foundation. See file COPYING. | |
12 | * | |
13 | */ | |
14 | ||
15 | #ifndef CEPH_CEPHXPROTOCOL_H | |
16 | #define CEPH_CEPHXPROTOCOL_H | |
17 | ||
18 | /* | |
19 | Ceph X protocol | |
20 | ||
11fdf7f2 | 21 | See doc/dev/cephx.rst |
7c673cae | 22 | |
7c673cae FG |
23 | */ |
24 | ||
25 | /* authenticate requests */ | |
26 | #define CEPHX_GET_AUTH_SESSION_KEY 0x0100 | |
27 | #define CEPHX_GET_PRINCIPAL_SESSION_KEY 0x0200 | |
28 | #define CEPHX_GET_ROTATING_KEY 0x0400 | |
29 | ||
30 | #define CEPHX_REQUEST_TYPE_MASK 0x0F00 | |
31 | #define CEPHX_CRYPT_ERR 1 | |
32 | ||
33 | #include "auth/Auth.h" | |
34 | #include <errno.h> | |
35 | #include <sstream> | |
36 | ||
9f95a23c | 37 | #include "include/common_fwd.h" |
7c673cae FG |
38 | /* |
39 | * Authentication | |
40 | */ | |
41 | ||
42 | // initial server -> client challenge | |
43 | struct CephXServerChallenge { | |
44 | uint64_t server_challenge; | |
45 | ||
f67539c2 | 46 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 47 | using ceph::encode; |
7c673cae | 48 | __u8 struct_v = 1; |
11fdf7f2 TL |
49 | encode(struct_v, bl); |
50 | encode(server_challenge, bl); | |
7c673cae | 51 | } |
f67539c2 | 52 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 53 | using ceph::decode; |
7c673cae | 54 | __u8 struct_v; |
11fdf7f2 TL |
55 | decode(struct_v, bl); |
56 | decode(server_challenge, bl); | |
7c673cae FG |
57 | } |
58 | }; | |
59 | WRITE_CLASS_ENCODER(CephXServerChallenge) | |
60 | ||
61 | ||
62 | // request/reply headers, for subsequent exchanges. | |
63 | ||
64 | struct CephXRequestHeader { | |
65 | __u16 request_type; | |
66 | ||
f67539c2 | 67 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 TL |
68 | using ceph::encode; |
69 | encode(request_type, bl); | |
7c673cae | 70 | } |
f67539c2 | 71 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 TL |
72 | using ceph::decode; |
73 | decode(request_type, bl); | |
7c673cae FG |
74 | } |
75 | }; | |
76 | WRITE_CLASS_ENCODER(CephXRequestHeader) | |
77 | ||
78 | struct CephXResponseHeader { | |
79 | uint16_t request_type; | |
80 | int32_t status; | |
81 | ||
f67539c2 | 82 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 TL |
83 | using ceph::encode; |
84 | encode(request_type, bl); | |
85 | encode(status, bl); | |
7c673cae | 86 | } |
f67539c2 | 87 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 TL |
88 | using ceph::decode; |
89 | decode(request_type, bl); | |
90 | decode(status, bl); | |
7c673cae FG |
91 | } |
92 | }; | |
93 | WRITE_CLASS_ENCODER(CephXResponseHeader) | |
94 | ||
95 | struct CephXTicketBlob { | |
96 | uint64_t secret_id; | |
f67539c2 | 97 | ceph::buffer::list blob; |
7c673cae FG |
98 | |
99 | CephXTicketBlob() : secret_id(0) {} | |
100 | ||
f67539c2 | 101 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 102 | using ceph::encode; |
7c673cae | 103 | __u8 struct_v = 1; |
11fdf7f2 TL |
104 | encode(struct_v, bl); |
105 | encode(secret_id, bl); | |
106 | encode(blob, bl); | |
7c673cae FG |
107 | } |
108 | ||
f67539c2 | 109 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 TL |
110 | using ceph::decode; |
111 | __u8 struct_v; | |
112 | decode(struct_v, bl); | |
113 | decode(secret_id, bl); | |
114 | decode(blob, bl); | |
7c673cae FG |
115 | } |
116 | }; | |
117 | WRITE_CLASS_ENCODER(CephXTicketBlob) | |
118 | ||
119 | // client -> server response to challenge | |
120 | struct CephXAuthenticate { | |
121 | uint64_t client_challenge; | |
122 | uint64_t key; | |
123 | CephXTicketBlob old_ticket; | |
11fdf7f2 | 124 | uint32_t other_keys = 0; // replaces CephXServiceTicketRequest |
7c673cae | 125 | |
c5c27e9a TL |
126 | bool old_ticket_may_be_omitted; |
127 | ||
f67539c2 | 128 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 129 | using ceph::encode; |
c5c27e9a | 130 | __u8 struct_v = 3; |
11fdf7f2 TL |
131 | encode(struct_v, bl); |
132 | encode(client_challenge, bl); | |
133 | encode(key, bl); | |
134 | encode(old_ticket, bl); | |
135 | encode(other_keys, bl); | |
7c673cae | 136 | } |
f67539c2 | 137 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 138 | using ceph::decode; |
7c673cae | 139 | __u8 struct_v; |
11fdf7f2 TL |
140 | decode(struct_v, bl); |
141 | decode(client_challenge, bl); | |
142 | decode(key, bl); | |
143 | decode(old_ticket, bl); | |
144 | if (struct_v >= 2) { | |
145 | decode(other_keys, bl); | |
146 | } | |
c5c27e9a TL |
147 | |
148 | // v2 and v3 encodings are the same, but: | |
149 | // - some clients that send v1 or v2 don't populate old_ticket | |
150 | // on reconnects (but do on renewals) | |
151 | // - any client that sends v3 or later is expected to populate | |
152 | // old_ticket both on reconnects and renewals | |
153 | old_ticket_may_be_omitted = struct_v < 3; | |
11fdf7f2 | 154 | } |
7c673cae FG |
155 | }; |
156 | WRITE_CLASS_ENCODER(CephXAuthenticate) | |
157 | ||
158 | struct CephXChallengeBlob { | |
159 | uint64_t server_challenge, client_challenge; | |
160 | ||
f67539c2 | 161 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 TL |
162 | using ceph::encode; |
163 | encode(server_challenge, bl); | |
164 | encode(client_challenge, bl); | |
7c673cae | 165 | } |
f67539c2 | 166 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 TL |
167 | using ceph::decode; |
168 | decode(server_challenge, bl); | |
169 | decode(client_challenge, bl); | |
7c673cae FG |
170 | } |
171 | }; | |
172 | WRITE_CLASS_ENCODER(CephXChallengeBlob) | |
173 | ||
174 | void cephx_calc_client_server_challenge(CephContext *cct, | |
175 | CryptoKey& secret, uint64_t server_challenge, uint64_t client_challenge, | |
176 | uint64_t *key, std::string &error); | |
177 | ||
178 | ||
179 | /* | |
180 | * getting service tickets | |
181 | */ | |
182 | struct CephXSessionAuthInfo { | |
183 | uint32_t service_id; | |
184 | uint64_t secret_id; | |
185 | AuthTicket ticket; | |
186 | CryptoKey session_key; | |
187 | CryptoKey service_secret; | |
188 | utime_t validity; | |
189 | }; | |
190 | ||
191 | ||
192 | extern bool cephx_build_service_ticket_blob(CephContext *cct, | |
193 | CephXSessionAuthInfo& ticket_info, CephXTicketBlob& blob); | |
194 | ||
195 | extern void cephx_build_service_ticket_request(CephContext *cct, | |
196 | uint32_t keys, | |
f67539c2 | 197 | ceph::buffer::list& request); |
7c673cae FG |
198 | |
199 | extern bool cephx_build_service_ticket_reply(CephContext *cct, | |
200 | CryptoKey& principal_secret, | |
f67539c2 | 201 | std::vector<CephXSessionAuthInfo> ticket_info, |
7c673cae FG |
202 | bool should_encrypt_ticket, |
203 | CryptoKey& ticket_enc_key, | |
f67539c2 | 204 | ceph::buffer::list& reply); |
7c673cae FG |
205 | |
206 | struct CephXServiceTicketRequest { | |
207 | uint32_t keys; | |
208 | ||
f67539c2 | 209 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 210 | using ceph::encode; |
7c673cae | 211 | __u8 struct_v = 1; |
11fdf7f2 TL |
212 | encode(struct_v, bl); |
213 | encode(keys, bl); | |
7c673cae | 214 | } |
f67539c2 | 215 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 216 | using ceph::decode; |
7c673cae | 217 | __u8 struct_v; |
11fdf7f2 TL |
218 | decode(struct_v, bl); |
219 | decode(keys, bl); | |
7c673cae FG |
220 | } |
221 | }; | |
222 | WRITE_CLASS_ENCODER(CephXServiceTicketRequest) | |
223 | ||
224 | ||
225 | /* | |
226 | * Authorize | |
227 | */ | |
228 | ||
229 | struct CephXAuthorizeReply { | |
230 | uint64_t nonce_plus_one; | |
11fdf7f2 | 231 | std::string connection_secret; |
f67539c2 | 232 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 233 | using ceph::encode; |
7c673cae | 234 | __u8 struct_v = 1; |
11fdf7f2 TL |
235 | if (connection_secret.size()) { |
236 | struct_v = 2; | |
237 | } | |
238 | encode(struct_v, bl); | |
239 | encode(nonce_plus_one, bl); | |
240 | if (struct_v >= 2) { | |
241 | struct_v = 2; | |
242 | encode(connection_secret, bl); | |
243 | } | |
7c673cae | 244 | } |
f67539c2 | 245 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 246 | using ceph::decode; |
7c673cae | 247 | __u8 struct_v; |
11fdf7f2 TL |
248 | decode(struct_v, bl); |
249 | decode(nonce_plus_one, bl); | |
250 | if (struct_v >= 2) { | |
251 | decode(connection_secret, bl); | |
252 | } | |
7c673cae FG |
253 | } |
254 | }; | |
255 | WRITE_CLASS_ENCODER(CephXAuthorizeReply) | |
256 | ||
257 | ||
258 | struct CephXAuthorizer : public AuthAuthorizer { | |
259 | private: | |
260 | CephContext *cct; | |
261 | public: | |
262 | uint64_t nonce; | |
f67539c2 | 263 | ceph::buffer::list base_bl; |
7c673cae FG |
264 | |
265 | explicit CephXAuthorizer(CephContext *cct_) | |
266 | : AuthAuthorizer(CEPH_AUTH_CEPHX), cct(cct_), nonce(0) {} | |
267 | ||
268 | bool build_authorizer(); | |
f67539c2 | 269 | bool verify_reply(ceph::buffer::list::const_iterator& reply, |
11fdf7f2 | 270 | std::string *connection_secret) override; |
f67539c2 | 271 | bool add_challenge(CephContext *cct, const ceph::buffer::list& challenge) override; |
7c673cae FG |
272 | }; |
273 | ||
274 | ||
275 | ||
276 | /* | |
277 | * TicketHandler | |
278 | */ | |
279 | struct CephXTicketHandler { | |
280 | uint32_t service_id; | |
281 | CryptoKey session_key; | |
282 | CephXTicketBlob ticket; // opaque to us | |
283 | utime_t renew_after, expires; | |
284 | bool have_key_flag; | |
285 | ||
286 | CephXTicketHandler(CephContext *cct_, uint32_t service_id_) | |
287 | : service_id(service_id_), have_key_flag(false), cct(cct_) { } | |
288 | ||
289 | // to build our ServiceTicket | |
290 | bool verify_service_ticket_reply(CryptoKey& principal_secret, | |
f67539c2 | 291 | ceph::buffer::list::const_iterator& indata); |
7c673cae FG |
292 | // to access the service |
293 | CephXAuthorizer *build_authorizer(uint64_t global_id) const; | |
294 | ||
295 | bool have_key(); | |
296 | bool need_key() const; | |
297 | ||
298 | void invalidate_ticket() { | |
1e59de90 | 299 | have_key_flag = false; |
7c673cae FG |
300 | } |
301 | private: | |
302 | CephContext *cct; | |
303 | }; | |
304 | ||
305 | struct CephXTicketManager { | |
f67539c2 | 306 | typedef std::map<uint32_t, CephXTicketHandler> tickets_map_t; |
7c673cae FG |
307 | tickets_map_t tickets_map; |
308 | uint64_t global_id; | |
309 | ||
310 | explicit CephXTicketManager(CephContext *cct_) : global_id(0), cct(cct_) {} | |
311 | ||
312 | bool verify_service_ticket_reply(CryptoKey& principal_secret, | |
f67539c2 | 313 | ceph::buffer::list::const_iterator& indata); |
7c673cae FG |
314 | |
315 | CephXTicketHandler& get_handler(uint32_t type) { | |
316 | tickets_map_t::iterator i = tickets_map.find(type); | |
317 | if (i != tickets_map.end()) | |
318 | return i->second; | |
319 | CephXTicketHandler newTicketHandler(cct, type); | |
320 | std::pair < tickets_map_t::iterator, bool > res = | |
321 | tickets_map.insert(std::make_pair(type, newTicketHandler)); | |
11fdf7f2 | 322 | ceph_assert(res.second); |
7c673cae FG |
323 | return res.first->second; |
324 | } | |
325 | CephXAuthorizer *build_authorizer(uint32_t service_id) const; | |
326 | bool have_key(uint32_t service_id); | |
327 | bool need_key(uint32_t service_id) const; | |
328 | void set_have_need_key(uint32_t service_id, uint32_t& have, uint32_t& need); | |
329 | void validate_tickets(uint32_t mask, uint32_t& have, uint32_t& need); | |
330 | void invalidate_ticket(uint32_t service_id); | |
331 | ||
332 | private: | |
333 | CephContext *cct; | |
334 | }; | |
335 | ||
336 | ||
337 | /* A */ | |
338 | struct CephXServiceTicket { | |
339 | CryptoKey session_key; | |
340 | utime_t validity; | |
341 | ||
f67539c2 | 342 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 343 | using ceph::encode; |
7c673cae | 344 | __u8 struct_v = 1; |
11fdf7f2 TL |
345 | encode(struct_v, bl); |
346 | encode(session_key, bl); | |
347 | encode(validity, bl); | |
7c673cae | 348 | } |
f67539c2 | 349 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 350 | using ceph::decode; |
7c673cae | 351 | __u8 struct_v; |
11fdf7f2 TL |
352 | decode(struct_v, bl); |
353 | decode(session_key, bl); | |
354 | decode(validity, bl); | |
7c673cae FG |
355 | } |
356 | }; | |
357 | WRITE_CLASS_ENCODER(CephXServiceTicket) | |
358 | ||
359 | /* B */ | |
360 | struct CephXServiceTicketInfo { | |
361 | AuthTicket ticket; | |
362 | CryptoKey session_key; | |
363 | ||
f67539c2 | 364 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 365 | using ceph::encode; |
7c673cae | 366 | __u8 struct_v = 1; |
11fdf7f2 TL |
367 | encode(struct_v, bl); |
368 | encode(ticket, bl); | |
369 | encode(session_key, bl); | |
7c673cae | 370 | } |
f67539c2 | 371 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 372 | using ceph::decode; |
7c673cae | 373 | __u8 struct_v; |
11fdf7f2 TL |
374 | decode(struct_v, bl); |
375 | decode(ticket, bl); | |
376 | decode(session_key, bl); | |
7c673cae FG |
377 | } |
378 | }; | |
379 | WRITE_CLASS_ENCODER(CephXServiceTicketInfo) | |
380 | ||
28e407b8 AA |
381 | struct CephXAuthorizeChallenge : public AuthAuthorizerChallenge { |
382 | uint64_t server_challenge; | |
f67539c2 | 383 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 384 | using ceph::encode; |
28e407b8 | 385 | __u8 struct_v = 1; |
11fdf7f2 TL |
386 | encode(struct_v, bl); |
387 | encode(server_challenge, bl); | |
28e407b8 | 388 | } |
f67539c2 | 389 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 390 | using ceph::decode; |
28e407b8 | 391 | __u8 struct_v; |
11fdf7f2 TL |
392 | decode(struct_v, bl); |
393 | decode(server_challenge, bl); | |
28e407b8 AA |
394 | } |
395 | }; | |
396 | WRITE_CLASS_ENCODER(CephXAuthorizeChallenge) | |
397 | ||
7c673cae FG |
398 | struct CephXAuthorize { |
399 | uint64_t nonce; | |
28e407b8 AA |
400 | bool have_challenge = false; |
401 | uint64_t server_challenge_plus_one = 0; | |
f67539c2 | 402 | void encode(ceph::buffer::list& bl) const { |
11fdf7f2 | 403 | using ceph::encode; |
28e407b8 | 404 | __u8 struct_v = 2; |
11fdf7f2 TL |
405 | encode(struct_v, bl); |
406 | encode(nonce, bl); | |
407 | encode(have_challenge, bl); | |
408 | encode(server_challenge_plus_one, bl); | |
7c673cae | 409 | } |
f67539c2 | 410 | void decode(ceph::buffer::list::const_iterator& bl) { |
11fdf7f2 | 411 | using ceph::decode; |
7c673cae | 412 | __u8 struct_v; |
11fdf7f2 TL |
413 | decode(struct_v, bl); |
414 | decode(nonce, bl); | |
28e407b8 | 415 | if (struct_v >= 2) { |
11fdf7f2 TL |
416 | decode(have_challenge, bl); |
417 | decode(server_challenge_plus_one, bl); | |
28e407b8 | 418 | } |
7c673cae FG |
419 | } |
420 | }; | |
421 | WRITE_CLASS_ENCODER(CephXAuthorize) | |
422 | ||
423 | /* | |
424 | * Decode an extract ticket | |
425 | */ | |
426 | bool cephx_decode_ticket(CephContext *cct, KeyStore *keys, | |
c5c27e9a TL |
427 | uint32_t service_id, |
428 | const CephXTicketBlob& ticket_blob, | |
7c673cae FG |
429 | CephXServiceTicketInfo& ticket_info); |
430 | ||
431 | /* | |
432 | * Verify authorizer and generate reply authorizer | |
433 | */ | |
28e407b8 | 434 | extern bool cephx_verify_authorizer( |
11fdf7f2 | 435 | CephContext *cct, |
9f95a23c | 436 | const KeyStore& keys, |
f67539c2 | 437 | ceph::buffer::list::const_iterator& indata, |
11fdf7f2 | 438 | size_t connection_secret_required_len, |
28e407b8 AA |
439 | CephXServiceTicketInfo& ticket_info, |
440 | std::unique_ptr<AuthAuthorizerChallenge> *challenge, | |
11fdf7f2 | 441 | std::string *connection_secret, |
f67539c2 | 442 | ceph::buffer::list *reply_bl); |
7c673cae FG |
443 | |
444 | ||
445 | ||
446 | ||
447 | ||
448 | ||
449 | /* | |
450 | * encode+encrypt macros | |
451 | */ | |
452 | static constexpr uint64_t AUTH_ENC_MAGIC = 0xff009cad8826aa55ull; | |
453 | ||
454 | template <typename T> | |
11fdf7f2 | 455 | void decode_decrypt_enc_bl(CephContext *cct, T& t, CryptoKey key, |
f67539c2 | 456 | const ceph::buffer::list& bl_enc, |
7c673cae FG |
457 | std::string &error) |
458 | { | |
459 | uint64_t magic; | |
f67539c2 | 460 | ceph::buffer::list bl; |
7c673cae FG |
461 | |
462 | if (key.decrypt(cct, bl_enc, bl, &error) < 0) | |
463 | return; | |
464 | ||
11fdf7f2 | 465 | auto iter2 = bl.cbegin(); |
7c673cae | 466 | __u8 struct_v; |
f67539c2 | 467 | using ceph::decode; |
11fdf7f2 TL |
468 | decode(struct_v, iter2); |
469 | decode(magic, iter2); | |
7c673cae | 470 | if (magic != AUTH_ENC_MAGIC) { |
f67539c2 | 471 | std::ostringstream oss; |
7c673cae FG |
472 | oss << "bad magic in decode_decrypt, " << magic << " != " << AUTH_ENC_MAGIC; |
473 | error = oss.str(); | |
474 | return; | |
475 | } | |
476 | ||
11fdf7f2 | 477 | decode(t, iter2); |
7c673cae FG |
478 | } |
479 | ||
480 | template <typename T> | |
481 | void encode_encrypt_enc_bl(CephContext *cct, const T& t, const CryptoKey& key, | |
f67539c2 | 482 | ceph::buffer::list& out, std::string &error) |
7c673cae | 483 | { |
f67539c2 | 484 | ceph::buffer::list bl; |
7c673cae | 485 | __u8 struct_v = 1; |
f67539c2 | 486 | using ceph::encode; |
11fdf7f2 | 487 | encode(struct_v, bl); |
7c673cae | 488 | uint64_t magic = AUTH_ENC_MAGIC; |
11fdf7f2 TL |
489 | encode(magic, bl); |
490 | encode(t, bl); | |
7c673cae FG |
491 | |
492 | key.encrypt(cct, bl, out, &error); | |
493 | } | |
494 | ||
495 | template <typename T> | |
496 | int decode_decrypt(CephContext *cct, T& t, const CryptoKey& key, | |
f67539c2 | 497 | ceph::buffer::list::const_iterator& iter, std::string &error) |
7c673cae | 498 | { |
f67539c2 TL |
499 | ceph::buffer::list bl_enc; |
500 | using ceph::decode; | |
7c673cae | 501 | try { |
11fdf7f2 | 502 | decode(bl_enc, iter); |
7c673cae FG |
503 | decode_decrypt_enc_bl(cct, t, key, bl_enc, error); |
504 | } | |
f67539c2 | 505 | catch (ceph::buffer::error &e) { |
7c673cae FG |
506 | error = "error decoding block for decryption"; |
507 | } | |
508 | if (!error.empty()) | |
509 | return CEPHX_CRYPT_ERR; | |
510 | return 0; | |
511 | } | |
512 | ||
513 | template <typename T> | |
514 | int encode_encrypt(CephContext *cct, const T& t, const CryptoKey& key, | |
f67539c2 | 515 | ceph::buffer::list& out, std::string &error) |
7c673cae | 516 | { |
f67539c2 TL |
517 | using ceph::encode; |
518 | ceph::buffer::list bl_enc; | |
7c673cae FG |
519 | encode_encrypt_enc_bl(cct, t, key, bl_enc, error); |
520 | if (!error.empty()){ | |
521 | return CEPHX_CRYPT_ERR; | |
522 | } | |
11fdf7f2 | 523 | encode(bl_enc, out); |
7c673cae FG |
524 | return 0; |
525 | } | |
526 | ||
7c673cae | 527 | #endif |