Line data Source code
1 : /* FreeTDS - Library of routines accessing Sybase and Microsoft databases
2 : * Copyright (C) 2007-2011 Frediano Ziglio
3 : *
4 : * This library is free software; you can redistribute it and/or
5 : * modify it under the terms of the GNU Library General Public
6 : * License as published by the Free Software Foundation; either
7 : * version 2 of the License, or (at your option) any later version.
8 : *
9 : * This library is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 : * Library General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU Library General Public
15 : * License along with this library; if not, write to the
16 : * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17 : * Boston, MA 02111-1307, USA.
18 : */
19 :
20 : #include <config.h>
21 :
22 : #if HAVE_STDLIB_H
23 : #include <stdlib.h>
24 : #endif /* HAVE_STDLIB_H */
25 :
26 : #include <ctype.h>
27 :
28 : #if HAVE_STRING_H
29 : #include <string.h>
30 : #endif /* HAVE_STRING_H */
31 :
32 : #if HAVE_UNISTD_H
33 : #include <unistd.h>
34 : #endif /* HAVE_UNISTD_H */
35 :
36 : #if HAVE_NETDB_H
37 : #include <netdb.h>
38 : #endif /* HAVE_NETDB_H */
39 :
40 : #if HAVE_SYS_SOCKET_H
41 : #include <sys/socket.h>
42 : #endif /* HAVE_SYS_SOCKET_H */
43 :
44 : #if HAVE_SYS_TYPES_H
45 : #include <sys/types.h>
46 : #endif /* HAVE_SYS_TYPES_H */
47 :
48 : #if HAVE_NETINET_IN_H
49 : #include <netinet/in.h>
50 : #endif /* HAVE_NETINET_IN_H */
51 :
52 : #if HAVE_ARPA_INET_H
53 : #include <arpa/inet.h>
54 : #endif /* HAVE_ARPA_INET_H */
55 :
56 : #if HAVE_COM_ERR_H
57 : #include <com_err.h>
58 : #endif /* HAVE_COM_ERR_H */
59 :
60 : #ifdef ENABLE_KRB5
61 :
62 : #ifdef __APPLE__
63 : #define KERBEROS_APPLE_DEPRECATED(x)
64 : #define GSSKRB_APPLE_DEPRECATED(x)
65 : #endif
66 : #include <gssapi/gssapi_krb5.h>
67 :
68 : #include <freetds/tds.h>
69 : #include <freetds/utils/string.h>
70 : #include <freetds/replacements.h>
71 :
72 : /**
73 : * \ingroup libtds
74 : * \defgroup auth Authentication
75 : * Functions for handling authentication.
76 : */
77 :
78 : /**
79 : * \addtogroup auth
80 : * @{
81 : */
82 :
83 : typedef struct tds_gss_auth
84 : {
85 : TDSAUTHENTICATION tds_auth;
86 : gss_ctx_id_t gss_context;
87 : gss_name_t target_name;
88 : char *sname;
89 : OM_uint32 last_stat;
90 : } TDSGSSAUTH;
91 :
92 : static TDSRET
93 0 : tds_gss_free(TDSCONNECTION * conn, struct tds_authentication * tds_auth)
94 : {
95 0 : TDSGSSAUTH *auth = (TDSGSSAUTH *) tds_auth;
96 : OM_uint32 min_stat;
97 :
98 0 : if (auth->tds_auth.packet) {
99 : gss_buffer_desc send_tok;
100 :
101 0 : send_tok.value = (void *) auth->tds_auth.packet;
102 0 : send_tok.length = auth->tds_auth.packet_len;
103 0 : gss_release_buffer(&min_stat, &send_tok);
104 : }
105 :
106 0 : gss_release_name(&min_stat, &auth->target_name);
107 0 : free(auth->sname);
108 0 : if (auth->gss_context != GSS_C_NO_CONTEXT)
109 0 : gss_delete_sec_context(&min_stat, &auth->gss_context, GSS_C_NO_BUFFER);
110 0 : free(auth);
111 :
112 0 : return TDS_SUCCESS;
113 : }
114 :
115 : static TDSRET tds_gss_continue(TDSSOCKET * tds, struct tds_gss_auth *auth, gss_buffer_desc *token_ptr);
116 :
117 : static TDSRET
118 0 : tds7_gss_handle_next(TDSSOCKET * tds, struct tds_authentication * auth, size_t len)
119 : {
120 : TDSRET res;
121 : gss_buffer_desc recv_tok;
122 :
123 0 : if (((struct tds_gss_auth *) auth)->last_stat != GSS_S_CONTINUE_NEEDED)
124 : return TDS_FAIL;
125 :
126 0 : if (auth->packet) {
127 : OM_uint32 min_stat;
128 : gss_buffer_desc send_tok;
129 :
130 0 : send_tok.value = (void *) auth->packet;
131 0 : send_tok.length = auth->packet_len;
132 0 : gss_release_buffer(&min_stat, &send_tok);
133 0 : auth->packet = NULL;
134 : }
135 :
136 0 : recv_tok.length = len;
137 0 : recv_tok.value = tds_new(char, len);
138 0 : if (!recv_tok.value)
139 : return TDS_FAIL;
140 0 : tds_get_n(tds, recv_tok.value, len);
141 :
142 0 : res = tds_gss_continue(tds, (struct tds_gss_auth *) auth, &recv_tok);
143 0 : free(recv_tok.value);
144 0 : if (TDS_FAILED(res))
145 : return res;
146 :
147 0 : if (auth->packet_len) {
148 0 : tds->out_flag = TDS7_AUTH;
149 0 : tds_put_n(tds, auth->packet, auth->packet_len);
150 0 : return tds_flush_packet(tds);
151 : }
152 : return TDS_SUCCESS;
153 : }
154 :
155 : static TDSRET
156 0 : tds5_gss_handle_next(TDSSOCKET * tds, struct tds_authentication * auth, size_t len)
157 : {
158 : TDSRET res;
159 : gss_buffer_desc recv_tok;
160 : TDSPARAMINFO *info;
161 : TDSCOLUMN *col;
162 :
163 0 : if (((struct tds_gss_auth *) auth)->last_stat != GSS_S_CONTINUE_NEEDED)
164 : return TDS_FAIL;
165 :
166 0 : if (auth->packet) {
167 : OM_uint32 min_stat;
168 : gss_buffer_desc send_tok;
169 :
170 0 : send_tok.value = (void *) auth->packet;
171 0 : send_tok.length = auth->packet_len;
172 0 : gss_release_buffer(&min_stat, &send_tok);
173 0 : auth->packet = NULL;
174 : }
175 :
176 : /* parse from saved message */
177 0 : if (auth->msg_type != TDS5_MSG_SEC_OPAQUE)
178 : goto error;
179 0 : auth->msg_type = 0;
180 :
181 0 : info = tds->param_info;
182 0 : if (!info || info->num_cols < 5)
183 : goto error;
184 :
185 : /* check first column is int and TDS5_SEC_VERSION */
186 0 : col = info->columns[0];
187 0 : if (tds_get_conversion_type(col->on_server.column_type, col->on_server.column_size) != SYBINT4)
188 : goto error;
189 0 : if (*((TDS_INT *) col->column_data) != TDS5_SEC_VERSION)
190 : goto error;
191 :
192 : /* check second column is int and TDS5_SEC_SECSESS */
193 0 : col = info->columns[1];
194 0 : if (tds_get_conversion_type(col->on_server.column_type, col->on_server.column_size) != SYBINT4)
195 : goto error;
196 0 : if (*((TDS_INT *) col->column_data) != TDS5_SEC_SECSESS)
197 : goto error;
198 :
199 0 : col = info->columns[3];
200 0 : if (col->column_type != SYBLONGBINARY)
201 : goto error;
202 0 : recv_tok.value = ((TDSBLOB*) col->column_data)->textvalue;
203 0 : recv_tok.length = col->column_size;
204 :
205 0 : res = tds_gss_continue(tds, (struct tds_gss_auth *) auth, &recv_tok);
206 0 : if (TDS_FAILED(res))
207 : return res;
208 :
209 0 : tds->out_flag = TDS_NORMAL;
210 0 : res = tds5_gss_send(tds);
211 0 : if (TDS_FAILED(res))
212 : return res;
213 :
214 0 : return tds_flush_packet(tds);
215 :
216 0 : error:
217 : return TDS_FAIL;
218 : }
219 :
220 : /**
221 : * Build a GSSAPI packet to send to server
222 : * @param tds A pointer to the TDSSOCKET structure managing a client/server operation.
223 : * @return size of packet
224 : */
225 : TDSAUTHENTICATION *
226 0 : tds_gss_get_auth(TDSSOCKET * tds)
227 : {
228 : /*
229 : * TODO
230 : * There are some differences between this implementation and MS on
231 : * - MS use SPNEGO with 3 mechnisms (MS KRB5, KRB5, NTLMSSP)
232 : * - MS seems to use MUTUAL flag
233 : * - name type is "Service and Instance (2)" and not "Principal (1)"
234 : * check for memory leaks
235 : * check for errors in many functions
236 : * a bit more verbose
237 : * dinamically load library ??
238 : */
239 : gss_buffer_desc send_tok;
240 : OM_uint32 maj_stat, min_stat;
241 : /* same as GSS_KRB5_NT_PRINCIPAL_NAME but do not require .so library */
242 : static gss_OID_desc nt_principal = { 10, (void*) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02\x01" };
243 : const char *server_name;
244 : /* Storage for getaddrinfo calls */
245 0 : struct addrinfo *addrs = NULL;
246 0 : int len = 0;
247 :
248 : struct tds_gss_auth *auth;
249 :
250 0 : if (!tds->login)
251 : return NULL;
252 :
253 0 : auth = tds_new0(struct tds_gss_auth, 1);
254 0 : if (!auth)
255 : return NULL;
256 :
257 0 : auth->tds_auth.free = tds_gss_free;
258 0 : auth->tds_auth.handle_next = IS_TDS50(tds->conn) ? tds5_gss_handle_next : tds7_gss_handle_next;
259 0 : auth->gss_context = GSS_C_NO_CONTEXT;
260 0 : auth->last_stat = GSS_S_COMPLETE;
261 :
262 0 : server_name = tds_dstr_cstr(&tds->login->server_host_name);
263 0 : if (IS_TDS7_PLUS(tds->conn) && strchr(server_name, '.') == NULL) {
264 : struct addrinfo hints;
265 0 : memset(&hints, 0, sizeof(hints));
266 : hints.ai_family = AF_UNSPEC;
267 0 : hints.ai_socktype = SOCK_STREAM;
268 0 : hints.ai_flags = AI_V4MAPPED|AI_ADDRCONFIG|AI_CANONNAME|AI_FQDN;
269 0 : if (!getaddrinfo(server_name, NULL, &hints, &addrs) && addrs->ai_canonname
270 0 : && strchr(addrs->ai_canonname, '.') != NULL)
271 0 : server_name = addrs->ai_canonname;
272 : }
273 :
274 0 : if (!tds_dstr_isempty(&tds->login->server_spn)) {
275 0 : auth->sname = strdup(tds_dstr_cstr(&tds->login->server_spn));
276 0 : } else if (IS_TDS7_PLUS(tds->conn)) {
277 0 : if (tds_dstr_isempty(&tds->login->server_realm_name)) {
278 0 : len = asprintf(&auth->sname, "MSSQLSvc/%s:%d", server_name, tds->login->port);
279 : } else {
280 0 : len = asprintf(&auth->sname, "MSSQLSvc/%s:%d@%s", server_name, tds->login->port,
281 0 : tds_dstr_cstr(&tds->login->server_realm_name));
282 : }
283 : } else {
284 : /* TDS 5.0, Sybase */
285 0 : server_name = tds_dstr_cstr(&tds->login->server_name);
286 0 : if (tds_dstr_isempty(&tds->login->server_realm_name)) {
287 0 : len = asprintf(&auth->sname, "%s", server_name);
288 : } else {
289 0 : len = asprintf(&auth->sname, "%s@%s", server_name,
290 0 : tds_dstr_cstr(&tds->login->server_realm_name));
291 : }
292 : }
293 0 : if (addrs)
294 0 : freeaddrinfo(addrs);
295 0 : if (len < 0 || auth->sname == NULL) {
296 0 : tds_gss_free(tds->conn, (TDSAUTHENTICATION *) auth);
297 0 : return NULL;
298 : }
299 0 : tdsdump_log(TDS_DBG_NETWORK, "using kerberos name %s\n", auth->sname);
300 :
301 : /*
302 : * Import the name into target_name. Use send_tok to save
303 : * local variable space.
304 : */
305 0 : send_tok.value = auth->sname;
306 0 : send_tok.length = strlen(auth->sname);
307 0 : maj_stat = gss_import_name(&min_stat, &send_tok, &nt_principal, &auth->target_name);
308 :
309 0 : switch (maj_stat) {
310 0 : case GSS_S_COMPLETE:
311 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_import_name: GSS_S_COMPLETE: gss_import_name completed successfully.\n");
312 0 : if (TDS_FAILED(tds_gss_continue(tds, auth, GSS_C_NO_BUFFER))) {
313 0 : tds_gss_free(tds->conn, (TDSAUTHENTICATION *) auth);
314 0 : return NULL;
315 : }
316 : break;
317 0 : case GSS_S_BAD_NAMETYPE:
318 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_import_name: GSS_S_BAD_NAMETYPE: The input_name_type was unrecognized.\n");
319 : break;
320 0 : case GSS_S_BAD_NAME:
321 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_import_name: GSS_S_BAD_NAME: The input_name parameter could not be interpreted as a name of the specified type.\n");
322 : break;
323 0 : case GSS_S_BAD_MECH:
324 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_import_name: GSS_S_BAD_MECH: The input name-type was GSS_C_NT_EXPORT_NAME, but the mechanism contained within the input-name is not supported.\n");
325 : break;
326 0 : default:
327 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_import_name: unexpected error %d.\n", maj_stat);
328 : break;
329 : }
330 :
331 0 : if (GSS_ERROR(maj_stat)) {
332 0 : tds_gss_free(tds->conn, (TDSAUTHENTICATION *) auth);
333 0 : return NULL;
334 : }
335 :
336 : return (TDSAUTHENTICATION *) auth;
337 : }
338 :
339 : #ifndef HAVE_ERROR_MESSAGE
340 : static const char *
341 : tds_error_message(OM_uint32 e)
342 : {
343 0 : const char *m = strerror(e);
344 0 : if (m == NULL)
345 : return "";
346 : return m;
347 : }
348 : #define error_message tds_error_message
349 : #endif
350 :
351 : static TDSRET
352 0 : tds_gss_continue(TDSSOCKET * tds, struct tds_gss_auth *auth, gss_buffer_desc *token_ptr)
353 : {
354 : gss_buffer_desc send_tok;
355 0 : OM_uint32 maj_stat, min_stat = 0;
356 : OM_uint32 ret_flags;
357 : int gssapi_flags;
358 0 : const char *msg = "???";
359 0 : gss_OID pmech = GSS_C_NULL_OID;
360 :
361 0 : auth->last_stat = GSS_S_COMPLETE;
362 :
363 0 : send_tok.value = NULL;
364 0 : send_tok.length = 0;
365 :
366 : /*
367 : * Perform the context-establishement loop.
368 : *
369 : * On each pass through the loop, token_ptr points to the token
370 : * to send to the server (or GSS_C_NO_BUFFER on the first pass).
371 : * Every generated token is stored in send_tok which is then
372 : * transmitted to the server; every received token is stored in
373 : * recv_tok, which token_ptr is then set to, to be processed by
374 : * the next call to gss_init_sec_context.
375 : *
376 : * GSS-API guarantees that send_tok's length will be non-zero
377 : * if and only if the server is expecting another token from us,
378 : * and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if
379 : * and only if the server has another token to send us.
380 : */
381 :
382 : /*
383 : * We always want to ask for the replay, and integ flags.
384 : * We may ask for delegation based on config in the tds.conf and other conf files.
385 : */
386 0 : gssapi_flags = GSS_C_REPLAY_FLAG | GSS_C_INTEG_FLAG;
387 :
388 0 : if (tds->login->gssapi_use_delegation)
389 0 : gssapi_flags |= GSS_C_DELEG_FLAG;
390 0 : if (tds->login->mutual_authentication || IS_TDS7_PLUS(tds->conn))
391 0 : gssapi_flags |= GSS_C_MUTUAL_FLAG;
392 :
393 0 : maj_stat = gss_init_sec_context(&min_stat, GSS_C_NO_CREDENTIAL, &auth->gss_context, auth->target_name,
394 : GSS_C_NULL_OID,
395 : gssapi_flags,
396 : 0, NULL, /* no channel bindings */
397 : token_ptr,
398 : &pmech,
399 : &send_tok, &ret_flags, NULL); /* ignore time_rec */
400 :
401 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_init_sec_context: actual mechanism at %p\n", pmech);
402 0 : if (pmech && pmech->elements) {
403 0 : tdsdump_dump_buf(TDS_DBG_NETWORK, "actual mechanism", pmech->elements, pmech->length);
404 : }
405 :
406 0 : auth->last_stat = maj_stat;
407 :
408 0 : switch (maj_stat) {
409 0 : case GSS_S_COMPLETE:
410 0 : msg = "GSS_S_COMPLETE: gss_init_sec_context completed successfully.";
411 0 : break;
412 0 : case GSS_S_CONTINUE_NEEDED:
413 0 : msg = "GSS_S_CONTINUE_NEEDED: gss_init_sec_context() routine must be called again.";
414 0 : break;
415 0 : case GSS_S_FAILURE:
416 0 : msg = "GSS_S_FAILURE: The routine failed for reasons that are not defined at the GSS level.";
417 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_init_sec_context: min_stat %ld \"%s\"\n",
418 : (long) min_stat, error_message(min_stat));
419 : break;
420 0 : case GSS_S_BAD_BINDINGS:
421 0 : msg = "GSS_S_BAD_BINDINGS: The channel bindings are not valid.";
422 0 : break;
423 0 : case GSS_S_BAD_MECH:
424 0 : msg = "GSS_S_BAD_MECH: The request security mechanism is not supported.";
425 0 : break;
426 0 : case GSS_S_BAD_NAME:
427 0 : msg = "GSS_S_BAD_NAME: The target_name parameter is not valid.";
428 0 : break;
429 0 : case GSS_S_BAD_SIG:
430 0 : msg = "GSS_S_BAD_SIG: The input token contains an incorrect integrity check value.";
431 0 : break;
432 0 : case GSS_S_CREDENTIALS_EXPIRED:
433 0 : msg = "GSS_S_CREDENTIALS_EXPIRED: The supplied credentials are no longer valid.";
434 0 : break;
435 0 : case GSS_S_DEFECTIVE_CREDENTIAL:
436 0 : msg = "GSS_S_DEFECTIVE_CREDENTIAL: Consistency checks performed on the credential failed.";
437 0 : break;
438 0 : case GSS_S_DEFECTIVE_TOKEN:
439 0 : msg = "GSS_S_DEFECTIVE_TOKEN: Consistency checks performed on the input token failed.";
440 0 : break;
441 0 : case GSS_S_DUPLICATE_TOKEN:
442 0 : msg = "GSS_S_DUPLICATE_TOKEN: The token is a duplicate of a token that has already been processed.";
443 0 : break;
444 0 : case GSS_S_NO_CONTEXT:
445 0 : msg = "GSS_S_NO_CONTEXT: The context handle provided by the caller does not refer to a valid security context.";
446 0 : break;
447 0 : case GSS_S_NO_CRED:
448 0 : msg = "GSS_S_NO_CRED: The supplied credential handle does not refer to a valid credential, the supplied credential is not";
449 0 : break;
450 0 : case GSS_S_OLD_TOKEN:
451 0 : msg = "GSS_S_OLD_TOKEN: The token is too old to be checked for duplication against previous tokens which have already been processed.";
452 0 : break;
453 : }
454 :
455 0 : if (GSS_ERROR(maj_stat)) {
456 0 : gss_release_buffer(&min_stat, &send_tok);
457 0 : tdsdump_log(TDS_DBG_NETWORK, "gss_init_sec_context: %s\n", msg);
458 : return TDS_FAIL;
459 : }
460 :
461 0 : auth->tds_auth.packet = (uint8_t *) send_tok.value;
462 0 : auth->tds_auth.packet_len = send_tok.length;
463 :
464 0 : return TDS_SUCCESS;
465 : }
466 :
467 : static void
468 0 : tds5_send_msg(TDSSOCKET *tds, uint16_t msg_type)
469 : {
470 0 : tds_put_tinyint(tds, TDS_MSG_TOKEN);
471 0 : tds_put_tinyint(tds, 3); /* length */
472 0 : tds_put_tinyint(tds, 1); /* status, 1=has params */
473 0 : tds_put_smallint(tds, msg_type);
474 0 : }
475 :
476 : TDSRET
477 0 : tds5_gss_send(TDSSOCKET *tds)
478 : {
479 0 : uint32_t flags = TDS5_SEC_NETWORK_AUTHENTICATION;
480 :
481 0 : if (!tds->conn->authentication)
482 : return TDS_FAIL;
483 :
484 0 : if (tds->login) {
485 0 : if (tds->login->gssapi_use_delegation)
486 0 : flags |= TDS5_SEC_DELEGATION;
487 0 : if (tds->login->mutual_authentication)
488 0 : flags |= TDS5_SEC_MUTUAL_AUTHENTICATION;
489 : }
490 :
491 0 : tds5_send_msg(tds, TDS5_MSG_SEC_OPAQUE);
492 :
493 0 : tds_put_byte(tds, TDS5_PARAMFMT_TOKEN);
494 0 : TDS_START_LEN_USMALLINT(tds) {
495 0 : tds_put_smallint(tds, 5); /* # parameters */
496 :
497 0 : tds_put_n(tds, NULL, 6); /* name len + output + usertype */
498 0 : tds_put_tinyint(tds, SYBINTN);
499 0 : tds_put_tinyint(tds, 4);
500 0 : tds_put_tinyint(tds, 0); /* locale len */
501 :
502 0 : tds_put_n(tds, NULL, 6); /* name len + output + usertype */
503 0 : tds_put_tinyint(tds, SYBINTN);
504 0 : tds_put_tinyint(tds, 4);
505 0 : tds_put_tinyint(tds, 0); /* locale len */
506 :
507 0 : tds_put_n(tds, NULL, 6); /* name len + output + usertype */
508 0 : tds_put_tinyint(tds, SYBVARBINARY);
509 0 : tds_put_tinyint(tds, 255);
510 0 : tds_put_tinyint(tds, 0); /* locale len */
511 :
512 0 : tds_put_n(tds, NULL, 6); /* name len + output + usertype */
513 0 : tds_put_tinyint(tds, SYBLONGBINARY);
514 0 : tds_put_int(tds, 0x7fffffff);
515 0 : tds_put_tinyint(tds, 0); /* locale len */
516 :
517 0 : tds_put_n(tds, NULL, 6); /* name len + output + usertype */
518 0 : tds_put_tinyint(tds, SYBINTN);
519 0 : tds_put_tinyint(tds, 4);
520 0 : tds_put_tinyint(tds, 0); /* locale len */
521 0 : } TDS_END_LEN
522 :
523 0 : tds_put_byte(tds, TDS5_PARAMS_TOKEN);
524 :
525 0 : tds_put_tinyint(tds, 4);
526 0 : tds_put_int(tds, TDS5_SEC_VERSION);
527 :
528 0 : tds_put_tinyint(tds, 4);
529 0 : tds_put_int(tds, TDS5_SEC_SECSESS);
530 :
531 0 : tds_put_tinyint(tds, 12);
532 0 : tds_put_n(tds, "\x06\x0a\x2b\x06\x01\x04\x01\x87\x01\x04\x06\x06", 12); /* KRB5 Sybase OID */
533 :
534 0 : tds_put_int(tds, tds->conn->authentication->packet_len);
535 0 : tds_put_n(tds, tds->conn->authentication->packet, tds->conn->authentication->packet_len);
536 :
537 0 : tds_put_tinyint(tds, 4);
538 0 : tds_put_int(tds, flags);
539 :
540 0 : return TDS_SUCCESS;
541 : }
542 :
543 : /** @} */
544 :
545 : #endif
|