/* This source code was modified by Martin Hedenfalk <mhe@stacken.kth.se> for
 * use in Curl. His latest changes were done 2000-09-18.
 *
 * It has since been patched and modified a lot by Daniel Stenberg
 * <daniel@haxx.se> to make it better applied to curl conditions, and to make
 * it not use globals, pollute name space and more. This source code awaits a
 * rewrite to work around the paragraph 2 in the BSD licenses as explained
 * below.
 *
 * Copyright (c) 1998, 1999 Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.  */

#include "setup.h"

#ifndef CURL_DISABLE_FTP
#ifdef HAVE_KRB4

#define _MPRINTF_REPLACE /* we want curl-functions instead of native ones */
#include <curl/mprintf.h>

#include "security.h"
#include <stdlib.h>
#include <string.h>
#include <netdb.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "base64.h"
#include "sendf.h"
#include "ftp.h"
#include "memory.h"

/* The last #include file should be: */
#include "memdebug.h"

#define min( a, b )   ( ( a ) < ( b ) ? ( a ) : ( b ) )

static struct {
	enum protection_level level;
	const char *name;
} level_names[] = {
	{ prot_clear, "clear" },
	{ prot_safe, "safe" },
	{ prot_confidential, "confidential" },
	{ prot_private, "private" }
};

static enum protection_level
name_to_level( const char *name ) {
	int i;
	for ( i = 0; i < (int)sizeof( level_names ) / (int)sizeof( level_names[0] ); i++ )
		if ( curl_strnequal( level_names[i].name, name, strlen( name ) ) ) {
			return level_names[i].level;
		}
	return (enum protection_level)-1;
}

static struct Curl_sec_client_mech *mechs[] = {
#ifdef KRB5
	/* not supported */
#endif
#ifdef HAVE_KRB4
	&Curl_krb4_client_mech,
#endif
	NULL
};

int
Curl_sec_getc( struct connectdata *conn, FILE *F ) {
	if ( conn->sec_complete && conn->data_prot ) {
		char c;
		if ( Curl_sec_read( conn, fileno( F ), &c, 1 ) <= 0 ) {
			return EOF;
		}
		return c;
	} else {
		return getc( F );
	}
}

static int
block_read( int fd, void *buf, size_t len ) {
	unsigned char *p = buf;
	int b;
	while ( len ) {
		b = read( fd, p, len );
		if ( b == 0 ) {
			return 0;
		} else if ( b < 0 ) {
			return -1;
		}
		len -= b;
		p += b;
	}
	return p - (unsigned char*)buf;
}

static int
block_write( int fd, void *buf, size_t len ) {
	unsigned char *p = buf;
	int b;
	while ( len ) {
		b = write( fd, p, len );
		if ( b < 0 ) {
			return -1;
		}
		len -= b;
		p += b;
	}
	return p - (unsigned char*)buf;
}

static int
sec_get_data( struct connectdata *conn,
			  int fd, struct krb4buffer *buf ) {
	int len;
	int b;

	b = block_read( fd, &len, sizeof( len ) );
	if ( b == 0 ) {
		return 0;
	} else if ( b < 0 )  {
		return -1;
	}
	len = ntohl( len );
	buf->data = realloc( buf->data, len );
	b = block_read( fd, buf->data, len );
	if ( b == 0 ) {
		return 0;
	} else if ( b < 0 )  {
		return -1;
	}
	buf->size = ( conn->mech->decode )( conn->app_data, buf->data, len,
										conn->data_prot, conn );
	buf->index = 0;
	return 0;
}

static size_t
buffer_read( struct krb4buffer *buf, void *data, size_t len ) {
	len = min( len, buf->size - buf->index );
	memcpy( data, (char*)buf->data + buf->index, len );
	buf->index += len;
	return len;
}

static size_t
buffer_write( struct krb4buffer *buf, void *data, size_t len ) {
	if ( buf->index + len > buf->size ) {
		void *tmp;
		if ( buf->data == NULL ) {
			tmp = malloc( 1024 );
		} else {
			tmp = realloc( buf->data, buf->index + len );
		}
		if ( tmp == NULL ) {
			return -1;
		}
		buf->data = tmp;
		buf->size = buf->index + len;
	}
	memcpy( (char*)buf->data + buf->index, data, len );
	buf->index += len;
	return len;
}

int
Curl_sec_read( struct connectdata *conn, int fd, void *buffer, int length ) {
	size_t len;
	int rx = 0;

	if ( conn->sec_complete == 0 || conn->data_prot == 0 ) {
		return read( fd, buffer, length );
	}

	if ( conn->in_buffer.eof_flag ) {
		conn->in_buffer.eof_flag = 0;
		return 0;
	}

	len = buffer_read( &conn->in_buffer, buffer, length );
	length -= len;
	rx += len;
	buffer = (char*)buffer + len;

	while ( length ) {
		if ( sec_get_data( conn, fd, &conn->in_buffer ) < 0 ) {
			return -1;
		}
		if ( conn->in_buffer.size == 0 ) {
			if ( rx ) {
				conn->in_buffer.eof_flag = 1;
			}
			return rx;
		}
		len = buffer_read( &conn->in_buffer, buffer, length );
		length -= len;
		rx += len;
		buffer = (char*)buffer + len;
	}
	return rx;
}

static int
sec_send( struct connectdata *conn, int fd, char *from, int length ) {
	int bytes;
	void *buf;
	bytes = ( conn->mech->encode )( conn->app_data, from, length, conn->data_prot,
									&buf, conn );
	bytes = htonl( bytes );
	block_write( fd, &bytes, sizeof( bytes ) );
	block_write( fd, buf, ntohl( bytes ) );
	free( buf );
	return length;
}

int
Curl_sec_fflush_fd( struct connectdata *conn, int fd ) {
	if ( conn->data_prot != prot_clear ) {
		if ( conn->out_buffer.index > 0 ) {
			Curl_sec_write( conn, fd,
							conn->out_buffer.data, conn->out_buffer.index );
			conn->out_buffer.index = 0;
		}
		sec_send( conn, fd, NULL, 0 );
	}
	return 0;
}

int
Curl_sec_write( struct connectdata *conn, int fd, char *buffer, int length ) {
	int len = conn->buffer_size;
	int tx = 0;

	if ( conn->data_prot == prot_clear ) {
		return write( fd, buffer, length );
	}

	len -= ( conn->mech->overhead )( conn->app_data, conn->data_prot, len );
	while ( length ) {
		if ( length < len ) {
			len = length;
		}
		sec_send( conn, fd, buffer, len );
		length -= len;
		buffer += len;
		tx += len;
	}
	return tx;
}

int
Curl_sec_putc( struct connectdata *conn, int c, FILE *F ) {
	char ch = c;
	if ( conn->data_prot == prot_clear ) {
		return putc( c, F );
	}

	buffer_write( &conn->out_buffer, &ch, 1 );
	if ( c == '\n' || conn->out_buffer.index >= 1024 /* XXX */ ) {
		Curl_sec_write( conn, fileno( F ), conn->out_buffer.data,
						conn->out_buffer.index );
		conn->out_buffer.index = 0;
	}
	return c;
}

int
Curl_sec_read_msg( struct connectdata *conn, char *s, int level ) {
	int len;
	char *buf;
	int code;

	buf = malloc( strlen( s ) );
	len = Curl_base64_decode( s + 4, buf ); /* XXX */

	len = ( conn->mech->decode )( conn->app_data, buf, len, level, conn );
	if ( len < 0 ) {
		free( buf );
		return -1;
	}

	buf[len] = '\0';

	if ( buf[3] == '-' ) {
		code = 0;
	} else {
		sscanf( buf, "%d", &code );
	}
	if ( buf[len - 1] == '\n' ) {
		buf[len - 1] = '\0';
	}
	strcpy( s, buf );
	free( buf );
	return code;
}

enum protection_level
Curl_set_command_prot( struct connectdata *conn, enum protection_level level ) {
	enum protection_level old = conn->command_prot;
	conn->command_prot = level;
	return old;
}

static int
sec_prot_internal( struct connectdata *conn, int level ) {
	char *p;
	unsigned int s = 1048576;
	ssize_t nread;

	if ( !conn->sec_complete ) {
		infof( conn->data, "No security data exchange has taken place.\n" );
		return -1;
	}

	if ( level ) {
		int code;
		if ( Curl_ftpsendf( conn, "PBSZ %u", s ) ) {
			return -1;
		}

		if ( Curl_GetFTPResponse( &nread, conn, &code ) ) {
			return -1;
		}

		if ( code / 100 != '2' ) {
			failf( conn->data, "Failed to set protection buffer size." );
			return -1;
		}
		conn->buffer_size = s;

		p = strstr( conn->data->state.buffer, "PBSZ=" );
		if ( p ) {
			sscanf( p, "PBSZ=%u", &s );
		}
		if ( s < conn->buffer_size ) {
			conn->buffer_size = s;
		}
	}

	if ( Curl_ftpsendf( conn, "PROT %c", level["CSEP"] ) ) {
		return -1;
	}

	if ( Curl_GetFTPResponse( &nread, conn, NULL ) ) {
		return -1;
	}

	if ( conn->data->state.buffer[0] != '2' ) {
		failf( conn->data, "Failed to set protection level." );
		return -1;
	}

	conn->data_prot = (enum protection_level)level;
	return 0;
}

void
Curl_sec_set_protection_level( struct connectdata *conn ) {
	if ( conn->sec_complete && conn->data_prot != conn->request_data_prot ) {
		sec_prot_internal( conn, conn->request_data_prot );
	}
}


int
Curl_sec_request_prot( struct connectdata *conn, const char *level ) {
	int l = name_to_level( level );
	if ( l == -1 ) {
		return -1;
	}
	conn->request_data_prot = (enum protection_level)l;
	return 0;
}

int
Curl_sec_login( struct connectdata *conn ) {
	int ret;
	struct Curl_sec_client_mech **m;
	ssize_t nread;
	struct SessionHandle *data = conn->data;
	int ftpcode;

	for ( m = mechs; *m && ( *m )->name; m++ ) {
		void *tmp;

		tmp = realloc( conn->app_data, ( *m )->size );
		if ( tmp == NULL ) {
			failf( data, "realloc %u failed", ( *m )->size );
			return -1;
		}
		conn->app_data = tmp;

		if ( ( *m )->init && ( *( *m )->init )( conn->app_data ) != 0 ) {
			infof( data, "Skipping %s...\n", ( *m )->name );
			continue;
		}
		infof( data, "Trying %s...\n", ( *m )->name );

		if ( Curl_ftpsendf( conn, "AUTH %s", ( *m )->name ) ) {
			return -1;
		}

		if ( Curl_GetFTPResponse( &nread, conn, &ftpcode ) ) {
			return -1;
		}

		if ( conn->data->state.buffer[0] != '3' ) {
			switch ( ftpcode ) {
			case 504:
				infof( data,
					   "%s is not supported by the server.\n", ( *m )->name );
				break;
			case 534:
				infof( data, "%s rejected as security mechanism.\n", ( *m )->name );
				break;
			default:
				if ( conn->data->state.buffer[0] == '5' ) {
					infof( data, "The server doesn't support the FTP "
								 "security extensions.\n" );
					return -1;
				}
				break;
			}
			continue;
		}

		ret = ( *( *m )->auth )( conn->app_data, conn );

		if ( ret == AUTH_CONTINUE ) {
			continue;
		} else if ( ret != AUTH_OK ) {
			/* mechanism is supposed to output error string */
			return -1;
		}
		conn->mech = *m;
		conn->sec_complete = 1;
		conn->command_prot = prot_safe;
		break;
	}

	return *m == NULL;
}

void
Curl_sec_end( struct connectdata *conn ) {
	if ( conn->mech != NULL ) {
		if ( conn->mech->end ) {
			( conn->mech->end )( conn->app_data );
		}
		memset( conn->app_data, 0, conn->mech->size );
		free( conn->app_data );
		conn->app_data = NULL;
	}
	conn->sec_complete = 0;
	conn->data_prot = (enum protection_level)0;
	conn->mech = NULL;
}

#endif /* HAVE_KRB4 */
#endif /* CURL_DISABLE_FTP */
