/****************************************************************************
*																			*
*					Network Stream URL Processing Functions					*
*						Copyright Peter Gutmann 1993-2025					*
*																			*
****************************************************************************/

#if defined( INC_ALL )
  #include "stream_int.h"
#else
  #include "io/stream_int.h"
#endif /* Compiler-specific includes */

#ifdef USE_TCP

/****************************************************************************
*																			*
*								Utility Functions							*
*																			*
****************************************************************************/

#ifndef CONFIG_CONSERVE_MEMORY_EXTRA

/* Sanity-check the URL parse state */

#define checkUrlValue( data, dataLength, minLength, maxLength ) \
		( ( ( data ) == NULL && ( dataLength ) == 0 ) || \
		  ( ( data ) != NULL && \
			( dataLength ) >= ( minLength ) && \
			( dataLength ) <= ( maxLength ) ) )

CHECK_RETVAL_BOOL STDC_NONNULL_ARG( ( 1 ) ) \
static BOOLEAN sanityCheckURL( const URL_INFO *urlInfo )
	{
	assert( isReadPtr( urlInfo, sizeof( URL_INFO ) ) );

	/* Make sure that the URL type is valid.  URL_TYPE_NONE is valid since 
	   it denotes an unrecognised URL type */
	if( !isEnumRangeOpt( urlInfo->type, URL_TYPE ) )
		{
		DEBUG_PUTS(( "sanityCheckURL: Type" ));
		return( FALSE );
		}

	/* Make sure the schema, user info, and location are either absent or 
	   have valid values */
	if( !checkUrlValue( urlInfo->schema, urlInfo->schemaLen, 
						MIN_SCHEMA_SIZE + 3, MAX_SCHEMA_SIZE + 3 ) )
						/* [...] + "://" */
		{
		DEBUG_PUTS(( "sanityCheckURL: Schema" ));
		return( FALSE );
		}
	if( !checkUrlValue( urlInfo->userInfo, urlInfo->userInfoLen, 
						1, CRYPT_MAX_TEXTSIZE ) )
		{
		DEBUG_PUTS(( "sanityCheckURL: User info" ));
		return( FALSE );
		}
	if( !checkUrlValue( urlInfo->location, urlInfo->locationLen, 
						MIN_LOCATION_SIZE, MAX_LOCATION_SIZE ) )
		{
		DEBUG_PUTS(( "sanityCheckURL: Location" ));
		return( FALSE );
		}

	/* The host always has to be present.  We allow a single special case in
	   which the host name is less than MIN_HOST_SIZE bytes for the IPv6 
	   loopback address "::1" */
	if( urlInfo->host == NULL )
		{
		DEBUG_PUTS(( "sanityCheckURL: Host" ));
		return( FALSE );
		}
	if( ( urlInfo->hostLen < MIN_HOST_SIZE || \
		  urlInfo->hostLen > MAX_HOST_SIZE ) && \
		!( urlInfo->hostLen == 3 && !memcmp( urlInfo->host, "::1", 3 ) ) )
		{
		DEBUG_PUTS(( "sanityCheckURL: Host" ));
		return( FALSE );
		}

	return( TRUE );
	}
#endif /* !CONFIG_CONSERVE_MEMORY_EXTRA */

/****************************************************************************
*																			*
*							URL Processing Functions						*
*																			*
****************************************************************************/

/* Check a schema */

CHECK_RETVAL STDC_NONNULL_ARG( ( 1, 3 ) ) \
static int checkSchema( IN_BUFFER( schemaLen ) const void *schema, 
						IN_RANGE( 2, 16 ) const int schemaLen,
						OUT_ENUM_OPT( URL_TYPE ) URL_TYPE *urlType,
						IN_ENUM_OPT( URL_TYPE ) URL_TYPE urlTypeHint )
	{
	typedef struct {
		BUFFER_FIXED( schemaLength ) \
		const char *schema;
		const int schemaLength;
		const URL_TYPE type;
		} URL_SCHEMA_INFO;
	static const URL_SCHEMA_INFO urlSchemaInfo[] = {
		{ "http://", 7, URL_TYPE_HTTP },
		{ "https://", 8, URL_TYPE_HTTPS },
		{ "wss://", 6, URL_TYPE_WEBSOCKET },
		{ "ssh://", 6, URL_TYPE_SSH },
		{ "scp://", 6, URL_TYPE_SSH },
		{ "sftp://", 7, URL_TYPE_SSH },
		{ "cmp://", 6, URL_TYPE_CMP },
		{ "tsp://", 6, URL_TYPE_TSP },
		{ "ldap://", 7, URL_TYPE_LDAP },
		{ NULL, 0, URL_TYPE_NONE }, { NULL, 0, URL_TYPE_NONE }
		};
	URL_TYPE type;
	LOOP_INDEX i;

	assert( isReadPtrDynamic( schema, schemaLen ) );
	assert( isWritePtr( urlType, sizeof( URL_TYPE ) ) );

	REQUIRES( schemaLen >= 2 && schemaLen <= 16 );
	REQUIRES( isEnumRangeOpt( urlTypeHint, URL_TYPE ) );

	/* Clear return value */
	*urlType = URL_TYPE_NONE;

	/* Check whether the schema is one that we recognise */
	LOOP_MED( i = 0, 
			  i < FAILSAFE_ARRAYSIZE( urlSchemaInfo, URL_SCHEMA_INFO ) && \
					urlSchemaInfo[ i ].type != URL_TYPE_NONE,
			  i++ )
		{
		ENSURES( LOOP_INVARIANT_MED( i, 0, 
									 FAILSAFE_ARRAYSIZE( urlSchemaInfo, \
														 URL_SCHEMA_INFO ) - 1 ) );

		if( urlSchemaInfo[ i ].schemaLength == schemaLen && \
			!strCompare( urlSchemaInfo[ i ].schema, schema, schemaLen ) )
			break;
		}
	ENSURES( LOOP_BOUND_OK );
	ENSURES( i < FAILSAFE_ARRAYSIZE( urlSchemaInfo, URL_SCHEMA_INFO ) );
	type = urlSchemaInfo[ i ].type;

	/* If there's no URL hint given, we're done */
	if( urlTypeHint == URL_TYPE_NONE )
		{
		*urlType = type;

		return( CRYPT_OK );
		}

	/* Make sure that the URL type matches the hint.  A requirement for an 
	   HTTPS URL can also match an HTTP URL, this type is used for SSL in 
	   which the use of HTTPS is implied by the fact that an SSL session is 
	   being used even if it's a straight HTTP URL.  In addition it can also
	   match a WSS, secure WebSockets URL, which is an additional transport
	   layer over the top of SSL.
	   
	   On the other hand an explicit HTTP URL must really be HTTP and not 
	   just a generic HTTP/HTTPS mix */
	if( urlTypeHint == URL_TYPE_HTTPS )
		{
		if( type != URL_TYPE_HTTP && type != URL_TYPE_HTTPS && \
			type != URL_TYPE_WEBSOCKET )
			return( CRYPT_ERROR_BADDATA );
		}
	else
		{
		if( type != urlTypeHint )
			return( CRYPT_ERROR_BADDATA );
		}
	*urlType = type;

	return( CRYPT_OK );
	}

/* Parse an RFC 1738 URI into:

	<schema>://[<user>@]<host>[:<port>]/<path> components

   The user:password portion is returned as a single 'userInfo' rather than 
   an explicit user:password because RFC 3986 redefined it as a more generic 
   "user:<other-stuff>" although in practice <other-stuff> is almost 
   universally a password even though you're supposed to pretend that it 
   isn't.

   This function is intended for use from the internal interface (i.e. to
   parse URLs supplied by the caller to the cryptlib API) and not so much
   for the external interface (i.e. URLs supplied by remote systems for
   processing by cryptlib).  Because of this it's rather more liberal with
   what it'll accept than a generic URL parser would be.
   
   Note though that it does occasionally have to deal with externally-
   supplied URLs, for example when breaking down a DNS name in a certificate 
   for comparison against the FQDN that the client is connecting to, so it 
   treats input as suspicious (an unqualified 'BYTE *' rather than a clean
   'char *') until proven otherwise.
   
   However even in this case it assumes that some level of common sense has
   been applied to the URI, in other words that the CA hasn't signd a
   certificate that uses URI-encoding tricks that would require a full-
   fledged URI processor to handle.  The only place where this would ever
   be an issue is with certificate name constraints, and the documentation
   warns against relying on these because of the ease with which they're
   bypassed.  For example the PKIX spec never addresses most URI 
   complications like use of percent encodings, so either the encoded or 
   decoded form could be regarded as valid, leading to a a Schroedinger's 
   constraint where comparing two forms with or without percent-encoding has 
   them both valid and invalid at the same time until you submit it to a PKI 
   app.  This would make a good, if slow, random number generator, one bit 
   per PKI code base */

CHECK_RETVAL STDC_NONNULL_ARG( ( 1, 2 ) ) \
int parseURL( OUT_PTR URL_INFO *urlInfo, 
			  IN_BUFFER( urlLen ) const BYTE *url, 
			  IN_LENGTH_SHORT const int urlLen,
			  IN_PORT_OPT const int defaultPort, 
			  IN_ENUM_OPT( URL_TYPE ) const URL_TYPE urlTypeHint,
			  IN_BOOL const BOOLEAN preParseOnly )
	{
	const char *strPtr, *hostName, *location;
	LOOP_INDEX urlIndex;
	int offset, strLen, hostNameLen, locationLen, minLen = MIN_DNS_SIZE;
	int status;

	assert( isWritePtr( urlInfo, sizeof( URL_INFO ) ) );
	assert( isReadPtrDynamic( url, urlLen ) );

	REQUIRES( isShortIntegerRangeNZ( urlLen ) );
	REQUIRES( defaultPort == CRYPT_UNUSED || \
			  ( rangeCheck( defaultPort, MIN_PORT_NUMBER, \
							MAX_DEST_PORT_NUMBER ) ) );
	REQUIRES( isEnumRangeOpt( urlTypeHint, URL_TYPE ) );
	REQUIRES( isBooleanValue( preParseOnly ) );

	/* Clear return values */
	memset( urlInfo, 0, sizeof( URL_INFO ) );
	if( defaultPort != CRYPT_UNUSED )
		urlInfo->port = defaultPort;

	/* Make sure that the input contains valid characters.  Note that this
	   doesn't check for valid URL forms, merely that the individual 
	   characters are valid.  Beyond this point we know that the 'BYTE *' 
	   is legitimately a 'char *' */
	LOOP_MAX( urlIndex = 0, urlIndex < urlLen, urlIndex++ )
		{
		int ch;

		ENSURES( LOOP_INVARIANT_MAX( urlIndex, 0, urlLen - 1 ) );

		ch = byteToInt( url[ urlIndex ] );
		if( !isValidTextChar( ch ) )
			return( CRYPT_ERROR_BADDATA );
		}
	ENSURES( LOOP_BOUND_OK );

	/* Skip leading and trailing whitespace */
	strLen = strStripWhitespace( &strPtr, url, urlLen );
	if( strLen < MIN_DNS_SIZE || strLen > urlLen || strLen > MAX_URL_SIZE )
		return( CRYPT_ERROR_BADDATA );
	ANALYSER_HINT( strPtr != NULL );

	/* Strip syntactic sugar */
	if( ( offset = strFindStr( strPtr, strLen, "://", 3 ) ) >= 0 )
		{
		/* Extract the URI schema */
		if( offset < MIN_SCHEMA_SIZE || offset > strLen || \
			offset > MAX_SCHEMA_SIZE )
			return( CRYPT_ERROR_BADDATA );
		REQUIRES( !checkOverflowAdd( offset, 3 ) );
		offset += 3;	/* Adjust for "://" */
		urlInfo->schema = strPtr;
		urlInfo->schemaLen = offset;
		strLen = strExtract( &strPtr, strPtr, offset, strLen );
		if( strLen < MIN_HOST_SIZE || strLen > MAX_URL_SIZE )
			return( CRYPT_ERROR_BADDATA );

		/* Check whether the schema is one that we recognise */
		status = checkSchema( urlInfo->schema, urlInfo->schemaLen, 
							  &urlInfo->type, urlTypeHint );
		if( cryptStatusError( status ) )
			return( status );
		}

	/* Check for user info before an '@' sign */
	if( ( offset = strFindCh( strPtr, strLen, '@' ) ) >= 0 )
		{
		const char *userInfo;
		int userInfoLen;

		/* Extract the user info, specifically as an RFC 3986 section 3.2.1 
		   opaque string rather than an RFC 1738 user:password.  However 
		   since in practice the user info is either a username or a 
		   username + password no matter what RFC 3986 wants to think it 
		   is, we treat it as a username of size CRYPT_MAX_TEXTSIZE.  We do
		   this even for LDAP which explicitly has a username + password but 
		   is essentially never used and even if it would be is unlikely to
		   overflow CRYPT_MAX_TEXTSIZE so we cap it at that to avoid having 
		   to special-case things for the remote chance that it's actually 
		   used */
		if( offset < 1 || offset > strLen || offset > MAX_URL_SIZE )
			return( CRYPT_ERROR_BADDATA );
		userInfoLen = strExtract( &userInfo, strPtr, 0, offset );
		if( userInfoLen < 1 || userInfoLen > CRYPT_MAX_TEXTSIZE )
			return( CRYPT_ERROR_BADDATA );

		/* Perform a secondary check in case someone is playing silly 
		   buggers with the URI.  For the full rules see RFC 3986 but in
		   brief our greedy parse looking for the '@', which is what we
		   care for in a caller-supplied URI, skips some delimiters that
		   can be present in a maliciously-crafted URI coming from a
		   certificate.  Specifically, adding a '/' in user info ending in
		   a '?' changes the parsing priority from "userinfo? @ fqdn" to 
		   "fqdn / path ? query".  To deal with this we check for disallowed
		   delimiters in what we assume is the user info, ']' is also
		   disallowed but since this is only used for IPv6 addresses it
		   only makes sense when used with a matching '[' */
		if( strFindCh( userInfo, userInfoLen, '/' ) >= 0 || \
			strFindCh( userInfo, userInfoLen, '#' ) >= 0 || \
			strFindCh( userInfo, userInfoLen, '?' ) >= 0 || \
			strFindCh( userInfo, userInfoLen, '[' ) >= 0 )
			return( CRYPT_ERROR_BADDATA );

		urlInfo->userInfo = userInfo;
		urlInfo->userInfoLen = userInfoLen;

		/* Skip the user info */
		REQUIRES( !checkOverflowAdd( offset, 1 ) );
		strLen = strExtract( &strPtr, strPtr, offset + 1, strLen );
		if( strLen < MIN_HOST_SIZE || strLen > MAX_URL_SIZE )
			return( CRYPT_ERROR_BADDATA );
		}

	/* RFC 2732 requires that IPv6 addresses in URLs be delimited by square 
	   brackets (at least one reason being that they use colons in their 
	   string representation, which would conflict with the way that ports
	   are denoted in URLs) so if we find one at the start of the URI we 
	   treat it as an IPv6 address */
	if( *strPtr == '[' && \
		( strLen != 12 || strCompare( strPtr, "[Autodetect]", 12 ) ) )
		{
		/* Locate the end of the RFC 2732 IPv6 address.  The returned offset 
		   can't be greater than the length - 1 but we make the check 
		   explicit here to be sure */
		if( ( offset = strFindCh( strPtr, strLen, ']' ) ) <= 0 )
			return( CRYPT_ERROR_BADDATA );
		if( offset < 2 || checkOverflowSub( strLen, 1 ) || \
			offset > strLen - 1 || offset > CRYPT_MAX_TEXTSIZE )
			return( CRYPT_ERROR_BADDATA );

		/* We know that it has to be an IPv6 address, make sure that the 
		   format is approximately valid.  This is an extra level of 
		   checking that IPv4 addresses and DNS names don't have, but since
		   we know that it's an IPv6 address we may as well catch any 
		   problems here.
		   
		   We start at offset 1 to skip the '[' at the start and stop before
		   the ']' at the end */
		LOOP_MAX( urlIndex = 1, urlIndex < offset, urlIndex++ )
			{
			int ch;

			ENSURES( LOOP_INVARIANT_MAX( urlIndex, 1, offset - 1 ) );

			ch = byteToInt( strPtr[ urlIndex ] );
			if( !isXDigit( ch ) && ch != ':' )
				return( CRYPT_ERROR_BADDATA );
			}
		ENSURES( LOOP_BOUND_OK );

		/* If we're only pre-parsing the IPv6 address for future use rather 
		   than actually parsing it to pass to the network address-
		   resolution functions then we have to leave the square-bracket 
		   delimiters in place for when we perform the actual parse later 
		   on */
		if( preParseOnly )
			{
			hostName = strPtr;
			REQUIRES( !checkOverflowAdd( offset, 1 ) );
			hostNameLen = offset + 1;	/* Include ']' */
			}
		else
			{
			/* Extract the IPv6 address starting at position 1 (past the 
			   '[') and ending at position 'offset' (before the ']') with 
			   minimum length 2.  We have to special-case the IPv6 loopback
			   address "::1" which is less than the standard MIN_DNS_SIZE */
			hostNameLen = strExtract( &hostName, strPtr, 1, offset );
			if( hostNameLen <= 0 )
				return( CRYPT_ERROR_BADDATA );
			if( hostNameLen == 3 && !memcmp( hostName, "::1", 3 ) )
				minLen = 3;
			}
		offset++;	/* Skip ']' */
		}
	else
		{
		int offset2;

		/* It's a non-IPv6 host name, check whether there's anything
		   following the name */
		offset = strFindCh( strPtr, strLen, ':' );
		offset2 = strFindCh( strPtr, strLen, '/' );
		if( offset < 0 )
			offset = offset2;
		else
			{
			ENSURES( offset >= 0 );
			if( offset2 >= 0 )
				offset = min( offset, offset2 );
			}
		if( offset <= 0 )
			{
			/* The remaining string is the server name, we're done (the 
			   string has already been trimmed in earlier code) */
			urlInfo->host = strPtr;
			urlInfo->hostLen = strLen;

			ENSURES( sanityCheckURL( urlInfo ) );

			return( CRYPT_OK );
			}

		/* There's port/location info following the server name.  Trailing
		   whitespace will be stripped later */
		hostNameLen = strExtract( &hostName, strPtr, 0, offset );
		}
	if( hostNameLen < minLen || hostNameLen > MAX_HOST_SIZE )
		return( CRYPT_ERROR_BADDATA );
	urlInfo->host = hostName;
	urlInfo->hostLen = hostNameLen;

	/* If there's nothing beyond the host name, we're done */
	if( offset >= strLen )
		{
		ENSURES( sanityCheckURL( urlInfo ) );

		return( CRYPT_OK );
		}
	strLen = strExtract( &strPtr, strPtr, offset, strLen );
	if( strLen == 1 && *strPtr == '/' )
		{
		/* URLs may end in an optional no-op trailing '/' */
		ENSURES( sanityCheckURL( urlInfo ) );

		return( CRYPT_OK );
		}
	if( strLen < 3 || strLen > MAX_URL_SIZE )
		return( CRYPT_ERROR_BADDATA );

	/* Check for a port after a ':' */
	if( *strPtr == ':' )
		{
		int port, portStrLen;

		static_assert( MIN_PORT_NUMBER >= 10,
					   "Minimum port number must have two digits" );
					   
		/* Skip the colon */
		strLen = strExtract( &strPtr, strPtr, 1, strLen );
		if( strLen < 2 || strLen > MAX_URL_SIZE )
			return( CRYPT_ERROR_BADDATA );

		/* Extract the port number */
		portStrLen = strParseNumeric( strPtr, strLen, &port, MIN_PORT_NUMBER, 
									  MAX_DEST_PORT_NUMBER );
		if( portStrLen < 2 || portStrLen > 6 )
			return( CRYPT_ERROR_BADDATA );
		urlInfo->port = port;

		/* If there's nothing beyond the port, we're done */
		if( portStrLen >= strLen )
			{
			ENSURES( sanityCheckURL( urlInfo ) );

			return( CRYPT_OK );
			}
		strLen = strExtract( &strPtr, strPtr, portStrLen, strLen );
		if( strLen == 1 && *strPtr == '/' )
			{
			/* URLs may end in an optional no-op trailing '/' */
			ENSURES( sanityCheckURL( urlInfo ) );

			return( CRYPT_OK );
			}
		if( strLen < 3 || strLen > MAX_URL_SIZE )
			return( CRYPT_ERROR_BADDATA );
		}

	/* What's left has to be a location */
	if( *strPtr != '/' )
		return( CRYPT_ERROR_BADDATA );

	/* The location string includes the leading '/' so we set the start 
	   offset to 0 and not 1 */
	locationLen = strExtract( &location, strPtr, 0, strLen );
	if( locationLen < MIN_LOCATION_SIZE || locationLen > MAX_LOCATION_SIZE )
		return( CRYPT_ERROR_BADDATA );
	urlInfo->location = location;
	urlInfo->locationLen = locationLen;

	ENSURES( sanityCheckURL( urlInfo ) );

	return( CRYPT_OK );
	}
#endif /* USE_TCP */
