View Javadoc
1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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  package org.kuali.common.dns.dnsme;
17  
18  import static com.google.common.base.Optional.fromNullable;
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static com.google.common.collect.Lists.newArrayList;
21  import static org.kuali.common.dns.model.DnsRecordType.CNAME;
22  import static org.kuali.common.dns.util.DNS.checkIpv4Fqdn;
23  import static org.kuali.common.util.base.Precondition.checkMin;
24  import static org.kuali.common.util.base.Precondition.checkNotBlank;
25  import static org.kuali.common.util.base.Precondition.checkNotNull;
26  import static org.kuali.common.util.nullify.NullUtils.trimToNone;
27  
28  import java.io.UnsupportedEncodingException;
29  import java.lang.reflect.Type;
30  import java.net.URLEncoder;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.List;
34  
35  import org.apache.commons.httpclient.HttpMethod;
36  import org.apache.commons.httpclient.NameValuePair;
37  import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
38  import org.apache.commons.httpclient.methods.PostMethod;
39  import org.apache.commons.httpclient.methods.PutMethod;
40  import org.kuali.common.dns.api.DnsService;
41  import org.kuali.common.dns.dnsme.model.DNSMadeEasyCredentials;
42  import org.kuali.common.dns.dnsme.model.DNSMadeEasyServiceContext;
43  import org.kuali.common.dns.dnsme.model.DnsMadeEasyDnsRecord;
44  import org.kuali.common.dns.dnsme.model.DnsMadeEasyDnsRecordComparator;
45  import org.kuali.common.dns.dnsme.model.DnsMadeEasyDomain;
46  import org.kuali.common.dns.dnsme.model.DnsMadeEasyDomainNames;
47  import org.kuali.common.dns.dnsme.model.DnsMadeEasySearchCriteria;
48  import org.kuali.common.dns.http.HttpRequestResult;
49  import org.kuali.common.dns.http.HttpUtil;
50  import org.kuali.common.dns.model.DnsRecordSearchCriteria;
51  import org.kuali.common.dns.model.DnsRecordType;
52  import org.kuali.common.dns.model.SimpleDnsRecord;
53  import org.kuali.common.util.Assert;
54  
55  import com.google.common.base.Optional;
56  import com.google.common.collect.Ordering;
57  import com.google.gson.Gson;
58  import com.google.gson.reflect.TypeToken;
59  
60  public final class DNSMadeEasyDnsService implements DnsService {
61  
62  	public static final int HTTP_OK = 200;
63  	public static final int HTTP_CREATED = 201;
64  
65  	// These are immutable and thus ok to expose via getters
66  	private final DNSMadeEasyServiceContext context;
67  	private final String restApiUrl;
68  	private final String domainName;
69  	private final DNSMadeEasyCredentials credentials;
70  	private final DnsMadeEasyDomain domain;
71  
72  	//
73  	private final Gson gson = new Gson();
74  	private final HttpUtil http = new HttpUtil();
75  	private final DNSMEUtil dnsme = new DNSMEUtil();
76  
77  	public DNSMadeEasyDnsService(DNSMadeEasyServiceContext context) {
78  		this.context = checkNotNull(context, "context");
79  		this.restApiUrl = context.getRestApiUrl();
80  		this.credentials = context.getCredentials();
81  		this.domainName = context.getDomainName();
82  
83  		// Now that the rest of the instance variables have been initialized it is safe to invoke the getDomain() method
84  		// This establishes an http connection to DNSME and creates a DnsMadeEasyDomain object from the domain name string
85  		this.domain = getDomain(domainName);
86  	}
87  
88  	protected List<DnsMadeEasyDomain> getDomains() {
89  		String url = this.restApiUrl + "/domains";
90  		String json = getJson(url, HTTP_OK);
91  		DnsMadeEasyDomainNames domainNames = gson.fromJson(json, DnsMadeEasyDomainNames.class);
92  		return getDomains(domainNames);
93  	}
94  
95  	protected DnsMadeEasyDomain getDomain(String name) {
96  		List<DnsMadeEasyDomain> domains = getDomains();
97  		for (DnsMadeEasyDomain domain : domains) {
98  			if (domain.getName().equalsIgnoreCase(name)) {
99  				return domain;
100 			}
101 		}
102 		return null;
103 	}
104 
105 	protected DnsMadeEasyDomain addDomain(DnsMadeEasyDomain domain) {
106 		String url = this.restApiUrl + "/domains/" + domain.getName();
107 		PutMethod method = new PutMethod(url);
108 		return addOrUpdateObject(url, HTTP_CREATED, domain, method);
109 	}
110 
111 	protected void deleteDomain(DnsMadeEasyDomain domain) {
112 		String url = this.restApiUrl + "/domains/" + domain.getName();
113 		deleteObject(url);
114 	}
115 
116 	protected String getQueryString(DnsMadeEasySearchCriteria search) {
117 		List<NameValuePair> pairs = getPairs(search);
118 		StringBuilder sb = new StringBuilder();
119 		for (int i = 0; i < pairs.size(); i++) {
120 			NameValuePair pair = pairs.get(i);
121 			if (i == 0) {
122 				sb.append("?");
123 			} else {
124 				sb.append("&");
125 			}
126 			sb.append(pair.getName() + "=" + encode(pair.getValue()));
127 		}
128 		return sb.toString();
129 	}
130 
131 	protected List<NameValuePair> getPairs(DnsMadeEasySearchCriteria search) {
132 		List<NameValuePair> pairs = new ArrayList<NameValuePair>();
133 		addIfNotNull(pairs, getPair("name", search.getName()));
134 		addIfNotNull(pairs, getPair("nameContains", search.getNameContains()));
135 		addIfNotNull(pairs, getPair("value", search.getValue()));
136 		addIfNotNull(pairs, getPair("valueContains", search.getValueContains()));
137 		addIfNotNull(pairs, getPair("gtdLocation", search.getGtdLocation()));
138 		addIfNotNull(pairs, getPair("type", search.getType()));
139 		return pairs;
140 	}
141 
142 	protected void addIfNotNull(List<NameValuePair> pairs, NameValuePair pair) {
143 		if (pair == null) {
144 			return;
145 		} else {
146 			pairs.add(pair);
147 		}
148 	}
149 
150 	protected NameValuePair getPair(String name, Object value) {
151 		if (value == null) {
152 			return null;
153 		} else {
154 			return new NameValuePair(name, value.toString());
155 		}
156 	}
157 
158 	protected String encode(String value) {
159 		try {
160 			return URLEncoder.encode(value, "UTF-8");
161 		} catch (UnsupportedEncodingException e) {
162 			throw new IllegalStateException(e);
163 		}
164 	}
165 
166 	protected DnsMadeEasyDnsRecord getRecord(DnsMadeEasyDomain domain, DnsMadeEasySearchCriteria search) {
167 		List<DnsMadeEasyDnsRecord> records = getRecords(domain, search);
168 		if (records.size() != 1) {
169 			throw new IllegalStateException("Search criteria must match exactly 1 record but it matched " + records.size() + " records");
170 		} else {
171 			return records.get(0);
172 		}
173 	}
174 
175 	protected String getAllRecordsApiUrl() {
176 		return getRecordsApiUrl(Optional.<DnsMadeEasySearchCriteria> absent());
177 
178 	}
179 
180 	protected String getRecordsApiUrl(Optional<DnsMadeEasySearchCriteria> criteria) {
181 		checkNotNull(criteria, "criteria");
182 		String url = this.restApiUrl + "/domains/" + this.domainName + "/records";
183 		if (criteria.isPresent()) {
184 			url += getQueryString(criteria.get());
185 		}
186 		return url;
187 	}
188 
189 	protected List<DnsMadeEasyDnsRecord> getRecords(DnsMadeEasyDomain domain, DnsMadeEasySearchCriteria search) {
190 		String url = getRecordsApiUrl(fromNullable(search));
191 		String json = getJson(url, HTTP_OK);
192 		List<DnsMadeEasyDnsRecord> records = getRecords(json);
193 		for (DnsMadeEasyDnsRecord record : records) {
194 			record.setDomain(domain);
195 		}
196 		return records;
197 	}
198 
199 	protected DnsMadeEasySearchCriteria getSearch(String name, DnsRecordType type) {
200 		DnsMadeEasySearchCriteria search = new DnsMadeEasySearchCriteria();
201 		search.setName(name);
202 		search.setType(type);
203 		return search;
204 	}
205 
206 	protected DnsMadeEasySearchCriteria getSearch(String name) {
207 		DnsMadeEasySearchCriteria search = new DnsMadeEasySearchCriteria();
208 		search.setName(name);
209 		return search;
210 	}
211 
212 	protected DnsMadeEasyDnsRecord getRecord(DnsMadeEasyDomain domain, String name) {
213 		return getRecord(domain, getSearch(name));
214 	}
215 
216 	protected DnsMadeEasyDnsRecord getRecord(DnsMadeEasyDomain domain, int recordId) {
217 		String url = this.restApiUrl + "/domains/" + domain.getName() + "/records/" + recordId;
218 		String resultJson = getJson(url, HTTP_OK);
219 		DnsMadeEasyDnsRecord resultRecord = gson.fromJson(resultJson, DnsMadeEasyDnsRecord.class);
220 		return resultRecord;
221 	}
222 
223 	protected void validateForUpdate(DnsMadeEasyDnsRecord record) {
224 		if (record.getId() == null && record.getName() == null) {
225 			throw new IllegalStateException("Either id or name must have a value when updating");
226 		}
227 	}
228 
229 	protected void updateRecord(DnsMadeEasyDomain domain, DnsMadeEasyDnsRecord record) {
230 		validateForUpdate(record);
231 		if (record.getId() == null) {
232 			DnsMadeEasyDnsRecord existingRecord = getRecord(domain, record.getName());
233 			record.setId(existingRecord.getId());
234 		}
235 		validateRecord(record);
236 		String url = this.restApiUrl + "/domains/" + domain.getName() + "/records/" + record.getId();
237 		PutMethod method = new PutMethod(url);
238 		addOrUpdateObject(url, HTTP_OK, record, method);
239 	}
240 
241 	protected DnsMadeEasyDnsRecord addRecord(DnsMadeEasyDomain domain, DnsMadeEasyDnsRecord record) {
242 		String url = this.restApiUrl + "/domains/" + domain.getName() + "/records";
243 		if (record.getId() != null) {
244 			throw new IllegalStateException("id must be null when adding");
245 		}
246 		validateRecord(record);
247 		PostMethod method = new PostMethod(url);
248 		return addOrUpdateObject(url, HTTP_CREATED, record, method);
249 	}
250 
251 	protected void deleteRecord(DnsMadeEasyDomain domain, int recordId) {
252 		String url = this.restApiUrl + "/domains/" + domain.getName() + "/records/" + recordId;
253 		deleteObject(url);
254 	}
255 
256 	protected void deleteRecord(DnsMadeEasyDomain domain, String name, DnsRecordType type) {
257 		DnsMadeEasySearchCriteria search = getSearch(name, type);
258 		DnsMadeEasyDnsRecord record = getRecord(domain, search);
259 		Assert.isTrue(record.getId() != null, "id is required");
260 		deleteRecord(domain, record.getId());
261 	}
262 
263 	protected void deleteRecord(DnsMadeEasyDomain domain, String name) {
264 		DnsMadeEasyDnsRecord record = getRecord(domain, name);
265 		Assert.isTrue(record.getId() != null, "id is required");
266 		deleteRecord(domain, record.getId());
267 	}
268 
269 	protected void deleteCNAMERecord(DnsMadeEasyDomain domain, String name) {
270 		DnsMadeEasyDnsRecord record = getRecord(domain, name);
271 		Assert.isTrue(record.getId() != null, "id is required");
272 		deleteRecord(domain, record.getId());
273 	}
274 
275 	protected void deleteObject(String url) {
276 		HttpMethod method = dnsme.getDeleteMethod(credentials, url);
277 		HttpRequestResult result = http.executeMethod(method);
278 		validateResult(result, HTTP_OK);
279 	}
280 
281 	protected List<DnsMadeEasyDnsRecord> getCNAMERecords(DnsMadeEasyDomain domain) {
282 		DnsMadeEasySearchCriteria search = new DnsMadeEasySearchCriteria();
283 		search.setType(DnsRecordType.CNAME);
284 		List<DnsMadeEasyDnsRecord> records = getRecords(domain, search);
285 		Collections.sort(records, new DnsMadeEasyDnsRecordComparator());
286 		return records;
287 	}
288 
289 	protected void validateRecord(DnsMadeEasyDnsRecord record) {
290 		StringBuilder sb = new StringBuilder();
291 		if (record.getName() == null) {
292 			sb.append("Name must not be null\n");
293 		}
294 		if (record.getData() == null) {
295 			sb.append("Data must not be null\n");
296 		}
297 		if (record.getTtl() == null) {
298 			sb.append("TTL must not be null\n");
299 		}
300 		if (record.getType() == null) {
301 			sb.append("Type must not be null\n");
302 		}
303 		if (sb.length() > 0) {
304 			throw new IllegalStateException(sb.toString());
305 		}
306 	}
307 
308 	protected <T> T addOrUpdateObject(String url, int statusCode, T object, EntityEnclosingMethod method) {
309 		String json = gson.toJson(object);
310 		dnsme.updateMethod(credentials, json, method);
311 		String resultJson = getJson(url, method, statusCode);
312 		@SuppressWarnings("unchecked")
313 		T resultObject = (T) gson.fromJson(resultJson, object.getClass());
314 		return resultObject;
315 	}
316 
317 	protected List<DnsMadeEasyDnsRecord> getRecords(DnsMadeEasyDomain domain) {
318 		return getRecords(domain, null);
319 	}
320 
321 	protected String getJson(String url, HttpMethod method, int successCode) {
322 		HttpRequestResult result = http.executeMethod(method);
323 		validateResult(result, successCode);
324 		return result.getResponseBody();
325 	}
326 
327 	protected String getJson(String url, int successCode) {
328 		HttpMethod method = dnsme.getGetMethod(credentials, url);
329 		return getJson(url, method, successCode);
330 	}
331 
332 	protected List<DnsMadeEasyDnsRecord> getRecords(String json) {
333 		Type recordsListType = new TypeToken<List<DnsMadeEasyDnsRecord>>() {
334 		}.getType();
335 
336 		@SuppressWarnings("unchecked")
337 		List<DnsMadeEasyDnsRecord> records = (List<DnsMadeEasyDnsRecord>) gson.fromJson(json, recordsListType);
338 		if (records == null) {
339 			return new ArrayList<DnsMadeEasyDnsRecord>();
340 		} else {
341 			return records;
342 		}
343 	}
344 
345 	protected void validateResult(HttpRequestResult result, int statusCode) {
346 		switch (result.getType()) {
347 		case EXCEPTION:
348 			throw new IllegalStateException(result.getException());
349 		case TIMEOUT:
350 			throw new IllegalStateException("Operation timed out");
351 		case COMPLETED:
352 			int code = result.getStatusCode();
353 			if (statusCode == result.getStatusCode()) {
354 				return;
355 			} else {
356 				String errorText = result.getResponseBody();
357 				throw new IllegalStateException("Invalid http status '" + code + ":" + result.getStatusText() + "' Expected: '" + statusCode + "'  Error:" + errorText);
358 			}
359 		default:
360 			throw new IllegalStateException("Unknown result type: " + result.getType());
361 		}
362 	}
363 
364 	protected List<DnsMadeEasyDomain> getDomains(DnsMadeEasyDomainNames domainNames) {
365 		if (domainNames.getList() == null) {
366 			return new ArrayList<DnsMadeEasyDomain>();
367 		}
368 		List<String> names = domainNames.getList();
369 		List<DnsMadeEasyDomain> domains = new ArrayList<DnsMadeEasyDomain>();
370 		for (String name : names) {
371 			DnsMadeEasyDomain domain = new DnsMadeEasyDomain(credentials, name);
372 			domains.add(domain);
373 		}
374 		return domains;
375 	}
376 
377 	@Override
378 	public String getDomainName() {
379 		return domainName;
380 	}
381 
382 	/**
383 	 * Validate that <code>fqdn</code> ends with <code>domain</code>
384 	 */
385 	protected String checkDomain(String fqdn, String domain) {
386 		checkNotBlank(fqdn, "fqdn");
387 		checkNotBlank(domain, "domain");
388 		checkArgument(fqdn.endsWith(domain), "[%s] doesn't end with [%s]", fqdn, domain);
389 		return fqdn;
390 	}
391 
392 	protected String getRecordNameFromFQDN(String fqdn, String domain) {
393 		checkNotBlank(fqdn, "fqdn");
394 		checkNotBlank(domain, "domain");
395 
396 		// Make sure fqdn is in the domain
397 		checkDomain(fqdn, domain);
398 
399 		// Trim the domain off the end of the fqdn
400 		int start = 0;
401 		int end = fqdn.length() - domain.length();
402 		String fragment = fqdn.substring(start, end);
403 
404 		// Account for the root record by only trimming off a dot at the end if it is present
405 		if (fragment.endsWith(".")) {
406 			return fragment.substring(0, fragment.length() - 1);
407 		} else {
408 			return fragment;
409 		}
410 	}
411 
412 	@Override
413 	public String getCNAMERecordValueFromFQDN(String fqdn) {
414 		// Make sure it's a valid fully qualified domain name
415 		checkIpv4Fqdn(fqdn);
416 		return fqdn + ".";
417 	}
418 
419 	@Override
420 	public SimpleDnsRecord createCNAMERecord(String aliasFQDN, String fqdn, int timeToLiveInSeconds) {
421 
422 		// Make sure both are syntactically valid fully qualified domain names
423 		checkIpv4Fqdn(aliasFQDN);
424 		checkIpv4Fqdn(fqdn);
425 
426 		// The alias must be in our domain
427 		checkDomain(aliasFQDN, getDomainName());
428 
429 		// TTL can't be negative
430 		checkMin(timeToLiveInSeconds, 0, "timeToLiveInSeconds");
431 
432 		// The dot at the end is a magic value telling DNSME that this is a fully qualified domain name
433 		// Without it, DNSME auto-appends our domain name
434 		String recordValue = getCNAMERecordValueFromFQDN(fqdn);
435 
436 		// Trim the domain name off the end of the aliasFQDN
437 		String recordName = getRecordNameFromFQDN(aliasFQDN, getDomainName());
438 
439 		// Create a Record object
440 		DnsMadeEasyDnsRecord record = new DnsMadeEasyDnsRecord();
441 		record.setName(recordName);
442 		record.setType(DnsRecordType.CNAME);
443 		record.setData(recordValue);
444 		record.setTtl(timeToLiveInSeconds);
445 
446 		// Actually add the record
447 		DnsMadeEasyDnsRecord added = addRecord(domain, record);
448 
449 		// Convert to a DnsRecord and return
450 		SimpleDnsRecord.Builder builder = SimpleDnsRecord.builder();
451 		builder.withName(added.getName());
452 		builder.withType(added.getType());
453 		builder.withValue(record.getData());
454 		builder.withTtl(added.getTtl());
455 		return builder.build();
456 	}
457 
458 	@Override
459 	public boolean isExistingCNAMERecord(String fqdn) {
460 		return getCNAMERecord(fqdn).isPresent();
461 	}
462 
463 	@Override
464 	public void deleteCNAMERecord(String fqdn) {
465 
466 		// Make sure it's a valid fully qualified domain name
467 		checkIpv4Fqdn(fqdn);
468 
469 		// Can only delete fqdn's in our domain
470 		checkDomain(fqdn, getDomainName());
471 
472 		// Pull out the existing record (if there is one)
473 		Optional<SimpleDnsRecord> optional = getCNAMERecord(fqdn);
474 
475 		// If there is a record we need to delete it
476 		if (optional.isPresent()) {
477 
478 			// Extract the DnsRecord from the optional
479 			SimpleDnsRecord record = optional.get();
480 
481 			// Delete the DNS record
482 			deleteRecord(domain, record.getName(), record.getType());
483 		}
484 	}
485 
486 	@Override
487 	public Optional<SimpleDnsRecord> getCNAMERecord(String fqdn) {
488 
489 		// Make sure it's a valid fully qualified domain name
490 		checkIpv4Fqdn(fqdn);
491 
492 		// Can only check for the existence of fqdn's in our domain
493 		checkDomain(fqdn, getDomainName());
494 
495 		// Extract the DNS record name from the fqdn
496 		String recordName = getRecordNameFromFQDN(fqdn, getDomainName());
497 
498 		// Setup a search object based on the fqdn
499 		DnsMadeEasySearchCriteria search = getSearch(recordName, CNAME);
500 
501 		// Get a list of matching records from DNSME
502 		List<DnsMadeEasyDnsRecord> records = getRecords(domain, search);
503 
504 		// If there is more than 1 record, something has gone wrong
505 		Assert.isFalse(records.size() > 1, "Found " + records.size() + " records when expecting a max of 1");
506 
507 		if (records.size() == 0) {
508 			// No matching CNAME record
509 			return Optional.<SimpleDnsRecord> absent();
510 		} else {
511 			// Extract the first (and only) item in the list
512 			DnsMadeEasyDnsRecord dnsme = records.get(0);
513 
514 			// Create a new DnsRecord from the DNSME record
515 			SimpleDnsRecord.Builder builder = SimpleDnsRecord.builder();
516 			builder.withName(dnsme.getName());
517 			builder.withType(dnsme.getType());
518 			builder.withValue(dnsme.getData());
519 			builder.withTtl(dnsme.getTtl());
520 			SimpleDnsRecord record = builder.build();
521 
522 			// Return an Optional containing the DnsRecord
523 			return Optional.of(record);
524 		}
525 	}
526 
527 	@Override
528 	public List<SimpleDnsRecord> getRecords() {
529 		List<DnsMadeEasyDnsRecord> records = getRecords(domain);
530 		return getRecords(records);
531 	}
532 
533 	@Override
534 	public List<SimpleDnsRecord> getRecords(DnsRecordSearchCriteria searchCriteria) {
535 		Assert.noNulls(searchCriteria);
536 		DnsMadeEasySearchCriteria search = new DnsMadeEasySearchCriteria();
537 		if (searchCriteria.getNameContains().isPresent()) {
538 			search.setNameContains(searchCriteria.getNameContains().get());
539 		}
540 		if (searchCriteria.getValueContains().isPresent()) {
541 			search.setValueContains(searchCriteria.getValueContains().get());
542 		}
543 		if (searchCriteria.getType().isPresent()) {
544 			search.setType(searchCriteria.getType().get());
545 		}
546 		List<DnsMadeEasyDnsRecord> records = getRecords(domain, search);
547 		return getRecords(records);
548 	}
549 
550 	protected List<SimpleDnsRecord> getRecords(List<DnsMadeEasyDnsRecord> records) {
551 		List<SimpleDnsRecord> list = newArrayList();
552 		for (DnsMadeEasyDnsRecord record : records) {
553 			String name = trimToNone(record.getName());
554 			String value = trimToNone(record.getData());
555 
556 			SimpleDnsRecord.Builder builder = SimpleDnsRecord.builder();
557 			builder.withName(name);
558 			builder.withType(record.getType());
559 			builder.withValue(value);
560 			builder.withTtl(record.getTtl());
561 			SimpleDnsRecord element = builder.build();
562 			list.add(element);
563 		}
564 		return Ordering.natural().immutableSortedCopy(list);
565 	}
566 
567 	public DNSMadeEasyServiceContext getContext() {
568 		return context;
569 	}
570 
571 	public String getRestApiUrl() {
572 		return restApiUrl;
573 	}
574 
575 	public DNSMadeEasyCredentials getCredentials() {
576 		return credentials;
577 	}
578 
579 	public DnsMadeEasyDomain getDomain() {
580 		return domain;
581 	}
582 
583 }