github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/azure/endpoints.go (about)

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package azure
    18  
    19  import (
    20  	"net"
    21  	"net/url"
    22  	"strings"
    23  
    24  	"github.com/gravitational/trace"
    25  )
    26  
    27  // IsAzureEndpoint returns true if the input URI is an Azure endpoint.
    28  //
    29  // The code implements approximate solution based on:
    30  // - https://management.azure.com/metadata/endpoints?api-version=2019-05-01
    31  // - https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-core/azure/cli/core/cloud.py
    32  func IsAzureEndpoint(hostname string) bool {
    33  	suffixes := []string{
    34  		"management.azure.com",
    35  		"graph.windows.net",
    36  		"batch.core.windows.net",
    37  		"rest.media.azure.net",
    38  		"datalake.azure.net",
    39  		"management.core.windows.net",
    40  		"gallery.azure.com",
    41  
    42  		"azuredatalakestore.net",
    43  		"azurecr.io",
    44  		"database.windows.net",
    45  		"azuredatalakeanalytics.net",
    46  		"vault.azure.net",
    47  		"core.windows.net",
    48  		"azurefd.net",
    49  
    50  		"login.microsoftonline.com", // required for "az logout"
    51  		"graph.microsoft.com",       // Azure AD
    52  	}
    53  
    54  	for _, suffix := range suffixes {
    55  		// exact match
    56  		if hostname == suffix {
    57  			return true
    58  		}
    59  		// .suffix match
    60  		if strings.HasSuffix(hostname, "."+suffix) {
    61  			return true
    62  		}
    63  	}
    64  
    65  	return false
    66  }
    67  
    68  // IsDatabaseEndpoint returns true if provided endpoint is a valid database
    69  // endpoint.
    70  func IsDatabaseEndpoint(endpoint string) bool {
    71  	return strings.Contains(endpoint, DatabaseEndpointSuffix)
    72  }
    73  
    74  // IsCacheForRedisEndpoint returns true if provided endpoint is a valid Azure
    75  // Cache for Redis endpoint.
    76  func IsCacheForRedisEndpoint(endpoint string) bool {
    77  	return IsRedisEndpoint(endpoint) || IsRedisEnterpriseEndpoint(endpoint)
    78  }
    79  
    80  // IsRedisEndpoint returns true if provided endpoint is a valid Redis
    81  // (non-Enterprise tier) endpoint.
    82  func IsRedisEndpoint(endpoint string) bool {
    83  	return strings.Contains(endpoint, RedisEndpointSuffix)
    84  }
    85  
    86  // IsRedisEnterpriseEndpoint returns true if provided endpoint is a valid Redis
    87  // Enterprise endpoint.
    88  func IsRedisEnterpriseEndpoint(endpoint string) bool {
    89  	return strings.Contains(endpoint, RedisEnterpriseEndpointSuffix)
    90  }
    91  
    92  // IsMSSQLServerEndpoint returns true if provided endpoint is a valid SQL server
    93  // database endpoint.
    94  func IsMSSQLServerEndpoint(endpoint string) bool {
    95  	return strings.Contains(endpoint, MSSQLEndpointSuffix)
    96  }
    97  
    98  // ParseDatabaseEndpoint extracts database server name from Azure endpoint.
    99  func ParseDatabaseEndpoint(endpoint string) (name string, err error) {
   100  	host, _, err := net.SplitHostPort(endpoint)
   101  	if err != nil {
   102  		return "", trace.Wrap(err)
   103  	}
   104  	// Azure endpoint looks like this:
   105  	// name.mysql.database.azure.com
   106  	parts := strings.Split(host, ".")
   107  	if !strings.HasSuffix(host, DatabaseEndpointSuffix) || len(parts) != 5 {
   108  		return "", trace.BadParameter("failed to parse %v as Azure endpoint", endpoint)
   109  	}
   110  	return parts[0], nil
   111  }
   112  
   113  // ParseCacheForRedisEndpoint extracts database server name from Azure Cache
   114  // for Redis endpoint.
   115  func ParseCacheForRedisEndpoint(endpoint string) (name string, err error) {
   116  	// Note that the Redis URI may contain schema and parameters.
   117  	host, err := GetHostFromRedisURI(endpoint)
   118  	if err != nil {
   119  		return "", trace.Wrap(err)
   120  	}
   121  
   122  	switch {
   123  	// Redis (non-Enterprise) endpoint looks like this:
   124  	// name.redis.cache.windows.net
   125  	case strings.HasSuffix(host, RedisEndpointSuffix):
   126  		return strings.TrimSuffix(host, RedisEndpointSuffix), nil
   127  
   128  	// Redis Enterprise endpoint looks like this:
   129  	// name.region.redisenterprise.cache.azure.net
   130  	case strings.HasSuffix(host, RedisEnterpriseEndpointSuffix):
   131  		name, _, ok := strings.Cut(strings.TrimSuffix(host, RedisEnterpriseEndpointSuffix), ".")
   132  		if !ok {
   133  			return "", trace.BadParameter("failed to parse %v as Azure Cache endpoint", endpoint)
   134  		}
   135  		return name, nil
   136  
   137  	default:
   138  		return "", trace.BadParameter("failed to parse %v as Azure Cache endpoint", endpoint)
   139  	}
   140  }
   141  
   142  // GetHostFromRedisURI extracts host name from a Redis URI. The URI may start
   143  // with "redis://", "rediss://", or without. The URI may also have parameters
   144  // like "?mode=cluster".
   145  func GetHostFromRedisURI(uri string) (string, error) {
   146  	// Add a temporary schema to make a valid URL for url.Parse if schema is
   147  	// not found.
   148  	if !strings.Contains(uri, "://") {
   149  		uri = "schema://" + uri
   150  	}
   151  
   152  	parsed, err := url.Parse(uri)
   153  	if err != nil {
   154  		return "", trace.Wrap(err)
   155  	}
   156  	return parsed.Hostname(), nil
   157  }
   158  
   159  // ParseMSSQLEndpoint extracts database server name from Azure endpoint.
   160  func ParseMSSQLEndpoint(endpoint string) (name string, err error) {
   161  	host, _, err := net.SplitHostPort(endpoint)
   162  	if err != nil {
   163  		return "", trace.Wrap(err)
   164  	}
   165  	// Azure endpoint looks like this:
   166  	// name.database.windows.net
   167  	parts := strings.Split(host, ".")
   168  	if !strings.HasSuffix(host, MSSQLEndpointSuffix) || len(parts) != 4 {
   169  		return "", trace.BadParameter("failed to parse %v as Azure MSSQL endpoint", endpoint)
   170  	}
   171  
   172  	if parts[0] == "" {
   173  		return "", trace.BadParameter("endpoint %v must contain database name", endpoint)
   174  	}
   175  
   176  	return parts[0], nil
   177  }
   178  
   179  const (
   180  	// DatabaseEndpointSuffix is the Azure database endpoint suffix. Used for
   181  	// MySQL, PostgresSQL, etc.
   182  	DatabaseEndpointSuffix = ".database.azure.com"
   183  
   184  	// RedisEndpointSuffix is the endpoint suffix for Redis.
   185  	RedisEndpointSuffix = ".redis.cache.windows.net"
   186  
   187  	// RedisEnterpriseEndpointSuffix is the endpoint suffix for Redis Enterprise.
   188  	RedisEnterpriseEndpointSuffix = ".redisenterprise.cache.azure.net"
   189  
   190  	// MSSQLEndpointSuffix is the Azure SQL Server endpoint suffix.
   191  	MSSQLEndpointSuffix = ".database.windows.net"
   192  )