All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bertramlabs.plugins.karman.azure.AzureNetworkProvider.groovy Maven / Gradle / Ivy

The newest version!
package com.bertramlabs.plugins.karman.azure

import com.bertramlabs.plugins.karman.network.NetworkProvider
import com.bertramlabs.plugins.karman.network.SecurityGroupInterface
import groovy.util.logging.Commons
import groovy.json.JsonOutput
import org.apache.http.ssl.SSLContexts
import org.apache.http.Header
import org.apache.http.HttpEntity
import org.apache.http.HttpHost
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import org.apache.http.NameValuePair
import org.apache.http.ParseException
import org.apache.http.auth.AuthScope
import org.apache.http.auth.NTCredentials
import org.apache.http.client.CredentialsProvider
import org.apache.http.client.HttpClient
import org.apache.commons.beanutils.PropertyUtils
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpHead
import org.apache.http.client.methods.HttpPatch
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.client.methods.HttpRequestBase
import org.apache.http.client.utils.URIBuilder
import org.apache.http.config.MessageConstraints
import org.apache.http.config.Registry
import org.apache.http.config.RegistryBuilder
import org.apache.http.conn.ConnectTimeoutException
import org.apache.http.conn.HttpConnectionFactory
import org.apache.http.conn.ManagedHttpClientConnection
import org.apache.http.conn.routing.HttpRoute
import org.apache.http.conn.socket.ConnectionSocketFactory
import org.apache.http.conn.socket.PlainConnectionSocketFactory
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.conn.ssl.SSLContextBuilder
import org.apache.http.conn.ssl.TrustStrategy
import org.apache.http.conn.ssl.X509HostnameVerifier
import org.apache.http.entity.StringEntity
import org.apache.http.impl.DefaultHttpResponseFactory
import org.apache.http.impl.client.BasicCredentialsProvider
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.client.HttpClients
import org.apache.http.impl.client.ProxyAuthenticationStrategy
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
import org.apache.http.impl.conn.DefaultHttpResponseParser
import org.apache.http.impl.conn.DefaultHttpResponseParserFactory
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory
import org.apache.http.io.HttpMessageParser
import org.apache.http.io.HttpMessageParserFactory
import org.apache.http.io.HttpMessageWriterFactory
import org.apache.http.io.SessionInputBuffer
import org.apache.http.message.BasicHeader
import org.apache.http.message.BasicLineParser
import org.apache.http.message.BasicNameValuePair
import org.apache.http.message.LineParser
import org.apache.http.protocol.HttpContext
import org.apache.http.util.CharArrayBuffer
import org.apache.http.util.EntityUtils

import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
import javax.net.ssl.SSLSocket
import java.lang.reflect.InvocationTargetException
import java.security.cert.X509Certificate

/**
 * Created by bwhiton on 05/13/2019.
 */
@Commons
class AzureNetworkProvider extends NetworkProvider {
	static String providerName = "azure"

	static Integer WEB_CONNECTION_TIMEOUT = 60 * 1000

	String subscriptionId
	String resourceGroup
	String identityUrl = 'https://login.microsoftonline.com'
	String identityPath
	String identityResourceUrl = 'https://management.core.windows.net'
	String managementUrl = 'https://management.azure.com'
	String location
	Boolean ignoreSSL = false
	String tenantId
	String clientId
	String clientKey

	String proxyHost
	Integer proxyPort
	String proxyUser
	String proxyPassword
	String proxyWorkstation
	String proxyDomain
	String protocol = 'https'

	String getProviderName() {
		return this.providerName
	}

	public AzureNetworkProvider(Map options) {
		subscriptionId = options.subscriptionId ?: subscriptionId
		resourceGroup = options.resourceGroup ?: resourceGroup
		identityUrl = options.identityUrl ?: identityUrl
		identityPath = options.identityPath ?: identityPath
		identityResourceUrl = options.identityResourceUrl ?: identityResourceUrl
		managementUrl = options.managementUrl ?: managementUrl
		ignoreSSL = options.ignoreSSL != null ? options.ignoreSSL : ignoreSSL
		tenantId = options.tenantId ?: tenantId
		clientId = options.clientId ?: clientId
		clientKey = options.clientKey ?: clientKey
		location = options.location ?: location

		protocol = options.protocol ?: protocol
		proxyHost = options.proxyHost ?: proxyHost
		proxyPort = options.proxyPort ?: proxyPort
		proxyUser = options.proxyUser ?: proxyUser
		proxyPassword = options.proxyPassword ?: proxyPassword
		proxyDomain = options.proxyDomain ?: proxyDomain
		proxyWorkstation = options.proxyWorkstation ?: proxyWorkstation
	}

	@Override
	Collection getSecurityGroups(Map options = [:]) {
		def securityGroups = []
		def apiPath
		if(resourceGroup) {
			apiPath = "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkSecurityGroups"
		} else {
			apiPath = "/subscriptions/${subscriptionId}/providers/Microsoft.Network/networkSecurityGroups"
		}

		def nextLink = apiPath
		while(nextLink) {
			def extraParams = [token: options.token, query: [:]]
			if(!nextLink.contains('api-version')) {
				extraParams.query['api-version'] = '2018-11-01'
			}
			def results = callApi(nextLink, extraParams, 'GET')
			if (!results.success) {
				throw new Exception('Error in calling Azure api')
			}
			def parsedResults = new groovy.json.JsonSlurper().parseText(results.content)
			if (parsedResults?.error) {
				throw new RuntimeException(parsedResults?.error?.message ?: 'Error in getting Security Groups from Azure')
			}
			parsedResults?.value?.each {
				securityGroups << new AzureSecurityGroup(this, [id: it.id, name: it.name, resourceGroup: parseResourceGroupName(it.id), location: it.location, tags: it.tags, etag: it.etag, properties: it.properties])
			}
			nextLink = parsedResults.results?.nextLink ?: parsedResults.nextLink
			if(nextLink?.startsWith('http')) {
				nextLink = nextLink.toString().substring(nextLink.toString().indexOf('/subscriptions'))
			}
		}

		return securityGroups
	}

	@Override
	SecurityGroupInterface getSecurityGroup(String name) {
		def apiPath = "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkSecurityGroups/${name}"
		def results = callApi(apiPath, [query: ['api-version': '2018-11-01']], 'GET')
		def parsedResult = new groovy.json.JsonSlurper().parseText(results.content)
		if(parsedResult?.error) {
			throw new RuntimeException(parsedResult?.error?.message ?: 'Error in getting Security Group from Azure')
		}
		def securityGroup = new AzureSecurityGroup(this, [id:parsedResult.id, name:parsedResult.name, resourceGroup: resourceGroup, location:parsedResult.location, tags:parsedResult.tags, etag:parsedResult.etag, properties:parsedResult.properties])

		return securityGroup
	}

	SecurityGroupInterface createSecurityGroup(String name) {
		return new AzureSecurityGroup(this, [name: name, resourceGroup: resourceGroup, location: location])
	}

	public String getResourceGroup(){
		this.resourceGroup
	}

	public setResourceGroup(String resourceGroup) {
		this.resourceGroup = resourceGroup
	}

	private getToken() {
		def azureUrl = identityUrl
		def apiPath = identityPath ?: "/${tenantId}/oauth2/token"

		def body = [grant_type   : 'client_credentials',
		            resource     : identityResourceUrl,
		            client_id    : clientId,
		            client_secret: clientKey
		]

		def results = callTokenApi(azureUrl, apiPath, [body: body])
		def parsedResults = new groovy.json.JsonSlurper().parseText(results)
		return parsedResults.access_token
	}

	def callApi(path, opts = [:], method = 'POST') {
		def token = opts.token ?: getToken()
		def rtn = [success: false, headers: [:]]
		try {
			def apiUrl = managementUrl
			if(opts.body)
				log.debug("calling apiUrl: ${apiUrl}/${path} with body: ${JsonOutput.prettyPrint(opts.body.encodeAsJson().toString())}")
			else
				log.debug("calling apiUrl: ${apiUrl}/${path} no body")
			URIBuilder uriBuilder = new URIBuilder("${apiUrl}/${path}")
			if(opts.query) {
				opts.query?.each { k, v ->
					uriBuilder.addParameter(k, v)
				}
			}
			def statusCode = 429
			while(statusCode == 429 || statusCode == 302) {
				if(opts.redirectUrl) {
					uriBuilder = new URIBuilder("${opts.redirectUrl}")
				}
				HttpRequestBase request
				switch(method) {
					case 'HEAD':
						request = new HttpHead(uriBuilder.build())
						break
					case 'PUT':
						request = new HttpPut(uriBuilder.build())
						break
					case 'POST':
						request = new HttpPost(uriBuilder.build())
						break
					case 'GET':
						request = new HttpGet(uriBuilder.build())
						break
					case 'DELETE':
						request = new HttpDelete(uriBuilder.build())
						break
					case 'PATCH':
						request = new HttpPatch(uriBuilder.build())
						break
					default:
						throw new Exception('method was not specified')
				}
				if(!opts.redirectUrl) {
					request.addHeader('Authorization', (opts.authType ?: 'Bearer') + ' ' + token)
				}
				// Headers
				if(!opts.headers || !opts.headers['Content-Type']) {
					request.addHeader('Content-Type', 'application/json')
				}
				opts.headers?.each { k, v ->
					request.addHeader(k, v)
				}
				if(opts.body) {
					HttpEntityEnclosingRequestBase postRequest = (HttpEntityEnclosingRequestBase)request
					postRequest.setEntity(new StringEntity(opts.body.encodeAsJson().toString()))
				}
				withClient(opts) { HttpClient client ->
					request.getAllHeaders()?.each { h ->

					}
					CloseableHttpResponse response = client.execute(request)
					try {
						statusCode = response.getStatusLine().getStatusCode()
						if(response.getStatusLine().getStatusCode() == 302) {
							response.getAllHeaders().each { h ->
								rtn.headers["${h.name}"] = h.value
							}
							if(rtn.headers['Location']) {
								opts.redirectUrl = rtn.headers['Location']
								log.debug("Setting redirectUrl: ${opts.redirectUrl}")
							}
							log.debug("Redirect Headers ${rtn.headers}")
						} else if(response.getStatusLine().getStatusCode() <= 300) {
							rtn.success = true
							response.getAllHeaders().each { h ->
								rtn.headers["${h.name}"] = h.value
							}
							HttpEntity entity = response.getEntity()
							if(entity) {
								rtn.content = EntityUtils.toString(entity)
								if(!opts.suppressLog) {
									log.debug "results of SUCCESSFUL call to ${apiUrl}/${path}, results: ${JsonOutput.prettyPrint(rtn.content ?: '')}"
								}
							} else {
								rtn.content = null
							}
							rtn.success = true
						} else if(response.getStatusLine().getStatusCode() == 429) {
							def retryAfter = response.getFirstHeader('Retry-After')?.getValue()?.toLong() ?: 60l
							log.warn("Azure Rate Limit Reached... received Retry After Recommendation of ${retryAfter} seconds. Sleeping...")
							sleep(retryAfter*1000l)
						} else {
							if(response.getEntity()) {
								rtn.content = EntityUtils.toString(response.getEntity())
								if(!opts.suppressLog) {
									log.debug "results of FAILURE call to ${apiUrl}/${path}, results: ${JsonOutput.prettyPrint(rtn.content ?: '')}"
								}
							}
							rtn.success = false
							rtn.errorCode = response.getStatusLine().getStatusCode()?.toString()
							log.warn("path: ${path} error: ${rtn.errorCode} - ${rtn.content}")
						}
					} catch(ex) {
						rtn.success = false
						log.error "Error occurred processing the response for ${apiUrl}", ex
					} finally {
						if(response) {
							response.close()
						}
					}
				}

			}


		} catch (e) {
			rtn.success = false
			log.error("Error Occurred calling Azure management API: ${e.message}",e)
			rtn.error = e.message
		}
		return rtn
	}

	private callTokenApi(apiUrl, path, opts = [:]) {
		log.debug "callTokenApi"

		def content
		if(opts.body && opts.body instanceof String) {
			log.debug("calling callTokenApi: ${apiUrl}/${path} with body: ${JsonOutput.prettyPrint(opts.body)}")
		} else {
			log.debug("calling callTokenApi: ${apiUrl}/${path} no body")
		}

		//build request
		URI uri = new URI("${apiUrl}/${path}")
		HttpRequestBase request = new HttpPost(uri)
		request.addHeader('Content-Type', 'application/x-www-form-urlencoded')
		// Set the request body
		HttpEntityEnclosingRequestBase postRequest = (HttpEntityEnclosingRequestBase)request
		List urlParameters = new ArrayList()
		opts.body?.each { k, v ->
			urlParameters.add(new BasicNameValuePair(k, v))
		}
		postRequest.setEntity(new UrlEncodedFormEntity(urlParameters))
		withClient([:]) { HttpClient client ->
			CloseableHttpResponse response = client.execute(request)
			try {
				if(response.getStatusLine().getStatusCode() <= 399) {
					HttpEntity entity = response.getEntity()
					content = EntityUtils.toString(entity)
					log.debug "results of SUCCESSFUL call to ${apiUrl}/${path}, results: ${JsonOutput.prettyPrint(content ?: '')}"
				} else {
					content = EntityUtils.toString(response.getEntity())
					log.debug "results of FAILURE call to ${apiUrl}/${path}, results: ${JsonOutput.prettyPrint(content ?: '')}"
					def errorCode = response.getStatusLine().getStatusCode()?.toString()
					log.warn("path: ${path} error: ${errorCode} - ${content}")
					throw new RuntimeException("Error in obtaining Azure token: ErrorCode=${errorCode}, Content=${content}")
				}
			} catch(ex) {
				log.error "Error occurred processing the response for ${apiUrl}", ex
			} finally {
				if(response) {
					response.close()
				}
			}
		}
		content
	}

	private withClient(opts, Closure cl) {
		HttpClientBuilder clientBuilder = HttpClients.custom()
		clientBuilder.setHostnameVerifier(new X509HostnameVerifier() {
			boolean verify(String host, SSLSession sess) {
				return true
			}

			void verify(String host, SSLSocket ssl) {}

			void verify(String host, String[] cns, String[] subjectAlts) {}

			void verify(String host, X509Certificate cert) {}

		})
		if(opts.disableRedirect) {
			clientBuilder.disableRedirectHandling()
		}
		SSLConnectionSocketFactory sslConnectionFactory
		SSLContext sslcontext
		if(ignoreSSL) {
			sslcontext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
				@Override
				boolean isTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException {
					return true
				}
			}).build()
			sslConnectionFactory = new SSLConnectionSocketFactory(sslcontext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) {
				@Override
				Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException, ConnectTimeoutException {
					if(socket instanceof SSLSocket) {
						try {
							socket.setEnabledProtocols(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2'] as String[])
							log.debug "hostname: ${host?.getHostName()}"
							PropertyUtils.setProperty(socket, "host", host.getHostName())
						} catch (NoSuchMethodException ex) {}
						catch (IllegalAccessException ex) {}
						catch (InvocationTargetException ex) {}
						catch (Exception ex) {
							log.error "We have an unhandled exception when attempting to connect to ${host} ignoring SSL errors", ex
						}
					}
					return super.connectSocket(WEB_CONNECTION_TIMEOUT, socket, host, remoteAddress, localAddress, context)
				}
			}
		} else {
			sslcontext = SSLContexts.createSystemDefault()
			sslConnectionFactory = new SSLConnectionSocketFactory(sslcontext) {
				@Override
				Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException, ConnectTimeoutException {
					if(socket instanceof SSLSocket) {
						try {
							socket.setEnabledProtocols(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2'] as String[])
							PropertyUtils.setProperty(socket, "host", host.getHostName())
						} catch(NoSuchMethodException ex) { }
						catch(IllegalAccessException ex) { }
						catch(InvocationTargetException ex) { }
					}
					return super.connectSocket(opts.timeout ?: 90000, socket, host, remoteAddress, localAddress, context)
				}
			}
		}

		HttpMessageParserFactory responseParserFactory = new DefaultHttpResponseParserFactory() {

			@Override
			HttpMessageParser create(SessionInputBuffer ibuffer, MessageConstraints constraints) {
				LineParser lineParser = new BasicLineParser() {
					@Override
					Header parseHeader(final CharArrayBuffer buffer) {
						try {
							return super.parseHeader(buffer)
						} catch (ParseException ex) {
							return new BasicHeader(buffer.toString(), null)
						}
					}
				}

				return new DefaultHttpResponseParser(ibuffer, lineParser, DefaultHttpResponseFactory.INSTANCE, constraints ?: MessageConstraints.DEFAULT) {
					@Override
					protected boolean reject(final CharArrayBuffer line, int count) {
						//We need to break out of forever head reads
						if(count > 100) {
							return true
						}
						return false
					}
				}
			}
		}
		clientBuilder.setSSLSocketFactory(sslConnectionFactory)
		Registry registry = RegistryBuilder. create()
				.register("https", sslConnectionFactory)
				.register("http", PlainConnectionSocketFactory.INSTANCE)
				.build()
		HttpMessageWriterFactory requestWriterFactory = new DefaultHttpRequestWriterFactory()
		HttpConnectionFactory connFactory = new ManagedHttpClientConnectionFactory(
				requestWriterFactory, responseParserFactory)
		BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(registry, connFactory)
		clientBuilder.setConnectionManager(connectionManager)

		String proxyHost = proxyHost
		Integer proxyPort = proxyPort
		String proxyUser = proxyUser
		String proxyPassword = proxyPassword
		String proxyDomain = proxyDomain
		String proxyWorkstation = proxyWorkstation
		String noProxy
		if(proxyHost && proxyPort) {
			log.debug "proxy detected at ${proxyHost}:${proxyPort}"
			clientBuilder.setProxy(new HttpHost(proxyHost, proxyPort))
			if(proxyUser) {
				CredentialsProvider credsProvider = new BasicCredentialsProvider()
				NTCredentials ntCreds = new NTCredentials(proxyUser, proxyPassword, proxyWorkstation, proxyDomain)
				credsProvider.setCredentials(new AuthScope(proxyHost, proxyPort), ntCreds)

				clientBuilder.setDefaultCredentialsProvider(credsProvider)
				clientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy())
			}
		}
		HttpClient client = clientBuilder.build()
		try {
			return cl.call(client)
		} finally {
			connectionManager.shutdown()
		}
	}

	private parseResourceGroupName(String val) {
		def parsedValue = ''
		def startIndex = val.toLowerCase().indexOf('resourcegroups')
		if(startIndex > -1) {
			startIndex = startIndex + 'resourcegroups/'.size()
			def endIndex = val.indexOf('/', startIndex)
			if(endIndex == -1) {
				endIndex = val.size()
			}
			parsedValue = val.substring(startIndex, endIndex)
		}
		parsedValue
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy