/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.brooklyn.entity.dns.geoscaling; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.text.MessageFormat; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.brooklyn.util.http.HttpTool; import org.apache.brooklyn.util.text.Strings; import org.apache.commons.io.FilenameUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.tidy.Tidy; /** * For interacting with the www.geoscaling.com DNS service. * * If you get the SSL error "peer not authenticated", then it means the required certificate is * not in your trust store. For example, see: * {@linkplain http://stackoverflow.com/questions/373295/digital-certificate-how-to-import-cer-file-in-to-truststore-file-using}. * The chain of certificates (as of October 2014, found by viewing in Chrome) is: * <ol> * <li> AddTrust External CA root * <li> COMODO RSA Certification Authority * <li> COMODO RSA Domain Validation Secure Server CA * <li> www.geoscaling.com * </ol> */ public class GeoscalingWebClient { public static final Logger log = LoggerFactory.getLogger(GeoscalingWebClient.class); public static final long PROVIDE_NETWORK_INFO = 1 << 0; public static final long PROVIDE_CITY_INFO = 1 << 1; public static final long PROVIDE_COUNTRY_INFO = 1 << 2; public static final long PROVIDE_EXTRA_INFO = 1 << 3; public static final long PROVIDE_UPTIME_INFO = 1 << 4; private static final String HOST ="www.geoscaling.com"; private static final String PATH ="dns2/index.php"; private HttpClient httpClient; private Tidy tidy; private List<Domain> primaryDomains = null; public class Domain { public final int id; public final String name; private List<SmartSubdomain> smartSubdomains = null; public Domain(int id, String name) { this.id = id; this.name = name.toLowerCase(); } public List<SmartSubdomain> getSmartSubdomains() { if (smartSubdomains == null) smartSubdomains = GeoscalingWebClient.this.fetchSmartSubdomains(this); return smartSubdomains; } public SmartSubdomain getSmartSubdomain(String name) { name = name.toLowerCase(); for (SmartSubdomain s : getSmartSubdomains()) { if (s.name.equals(name)) return s; } return null; } /** e.g. editRecord("foo", "A", "1.2.3.4"), which assuming this domain is "bar.com", will create A record for foo.bar.com. * <p> * or editRecord("*.foo", "CNAME", "foo.bar.com") to map everything at *.foo.bar.com to foo.bar.com */ public void editRecord(String subdomainPart, String type, String content) { subdomainPart = Strings.removeFromEnd(subdomainPart, "."+name); editSubdomainRecord(id, subdomainPart, type, content); } public SmartSubdomain getSmartSubdomain(int id) { for (SmartSubdomain s : getSmartSubdomains()) { if (s.id == id) return s; } return null; } public void createSmartSubdomain(String name) { GeoscalingWebClient.this.createSmartSubdomain(id, name); smartSubdomains = fetchSmartSubdomains(this); } public void delete() { deletePrimaryDomain(id); primaryDomains = fetchPrimaryDomains(); } @Override public String toString() { return "Domain["+name+" ("+id+")]"; } @Override public int hashCode() { return id; } } public class SmartSubdomain { public final Domain parent; public final int id; public String name; public SmartSubdomain(Domain parent, int id, String name) { this.parent = parent; this.id = id; this.name = name.toLowerCase(); } public void configure(long flags, String phpScript) { configureSmartSubdomain(parent.id, id, name, flags, phpScript); } public void delete() { deleteSmartSubdomain(parent.id, id); parent.smartSubdomains = fetchSmartSubdomains(parent); } @Override public String toString() { return "SmartSubdomain["+name+" ("+id+")]"; } @Override public int hashCode() { return id; } } public GeoscalingWebClient() { this(HttpTool.httpClientBuilder().build()); } public GeoscalingWebClient(HttpClient httpClient) { this.httpClient = httpClient; this.tidy = new Tidy(); // Silently swallow all HTML errors/warnings. tidy.setErrout(new PrintWriter(new OutputStream() { @Override public void write(int b) throws IOException { } })); } public GeoscalingWebClient(String username, String password) { this(); login(username, password); } public void login(String username, String password) { try { String url = MessageFormat.format("https://{0}/{1}?module=auth", HOST, PATH); HttpPost request = new HttpPost(url); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("username", username)); nameValuePairs.add(new BasicNameValuePair("password", password)); request.setEntity(new UrlEncodedFormEntity(nameValuePairs)); sendRequest(request, true); } catch (Exception e) { throw new RuntimeException("Failed to log-in to GeoScaling service: "+e, e); } } public void logout() { try { String url = MessageFormat.format("https://{0}/{1}?module=auth&logout", HOST, PATH); sendRequest(new HttpGet(url), true); } catch (Exception e) { throw new RuntimeException("Failed to log-out of GeoScaling service: "+e, e); } } public List<Domain> getPrimaryDomains() { if (primaryDomains == null) primaryDomains = fetchPrimaryDomains(); return primaryDomains; } public Domain getPrimaryDomain(String name) { name = name.toLowerCase(); for (Domain d : getPrimaryDomains()) { if (d.name.equals(name)) return d; } return null; } public Domain getPrimaryDomain(int id) { for (Domain d : getPrimaryDomains()) { if (d.id == id) return d; } return null; } public void createPrimaryDomain(String name) { try { name = name.toLowerCase(); String url = MessageFormat.format("https://{0}/{1}?module=domains", HOST, PATH); HttpPost request = new HttpPost(url); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536")); nameValuePairs.add(new BasicNameValuePair("domain", FilenameUtils.removeExtension(name))); nameValuePairs.add(new BasicNameValuePair("tld", FilenameUtils.getExtension(name))); request.setEntity(new UrlEncodedFormEntity(nameValuePairs)); sendRequest(request, true); } catch (Exception e) { throw new RuntimeException("Failed to create GeoScaling smart subdomain: "+e, e); } primaryDomains = fetchPrimaryDomains(); } private List<Domain> fetchPrimaryDomains() { try { List<Domain> domains = new LinkedList<Domain>(); String url = MessageFormat.format("https://{0}/{1}?module=domains", HOST, PATH); HttpResponse response = sendRequest(new HttpGet(url), false); HttpEntity entity = response.getEntity(); if (entity != null) { Document document = tidy.parseDOM(entity.getContent(), null); NodeList links = document.getElementsByTagName("a"); for (int i = 0; i < links.getLength(); ++i) { Element link = (Element) links.item(i); String href = link.getAttribute("href"); Pattern p = Pattern.compile("module=domain.*&id=(\\d+)"); Matcher m = p.matcher(href); if (!m.find(0)) continue; int id = Integer.parseInt(m.group(1)); String name = getTextContent(link).trim(); if (name.length() == 0) continue; domains.add(new Domain(id, name)); } EntityUtils.consume(entity); } return domains; } catch (Exception e) { throw new RuntimeException("Failed to retrieve GeoScaling subdomains: "+e, e); } } private void deletePrimaryDomain(int primaryDomainId) { try { String url = MessageFormat.format( "https://{0}/{1}?module=domain&id={2,number,#}&delete=1", HOST, PATH, primaryDomainId); sendRequest(new HttpGet(url), true); } catch (Exception e) { throw new RuntimeException("Failed to delete GeoScaling primary domain: "+e, e); } } private List<SmartSubdomain> fetchSmartSubdomains(Domain parent) { try { List<SmartSubdomain> subdomains = new LinkedList<SmartSubdomain>(); String url = MessageFormat.format( "https://{0}/{1}?module=smart_subdomains&id={2,number,#}", HOST, PATH, parent.id); HttpResponse response = sendRequest(new HttpGet(url), false); HttpEntity entity = response.getEntity(); if (entity != null) { Document document = tidy.parseDOM(entity.getContent(), null); NodeList links = document.getElementsByTagName("a"); for (int i = 0; i < links.getLength(); ++i) { Element link = (Element) links.item(i); String href = link.getAttribute("href"); Pattern p = Pattern.compile("module=smart_subdomain.*&subdomain_id=(\\d+)"); Matcher m = p.matcher(href); if (!m.find(0)) continue; int subdomainId = Integer.parseInt(m.group(1)); String name = getTextContent(link); if (name.trim().length() == 0) continue; name = name.substring(0, name.length() - parent.name.length() - 1); subdomains.add(new SmartSubdomain(parent, subdomainId, name)); } EntityUtils.consume(entity); } return subdomains; } catch (Exception e) { throw new RuntimeException("Failed to retrieve GeoScaling smart subdomains: "+e, e); } } private void createSmartSubdomain(int primaryDomainId, String smartSubdomainName) { try { smartSubdomainName = smartSubdomainName.toLowerCase(); String url = MessageFormat.format( "https://{0}/{1}?module=smart_subdomains&id={2,number,#}", HOST, PATH, primaryDomainId); HttpPost request = new HttpPost(url); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536")); nameValuePairs.add(new BasicNameValuePair("smart_subdomain_name", smartSubdomainName)); request.setEntity(new UrlEncodedFormEntity(nameValuePairs)); sendRequest(request, true); } catch (Exception e) { throw new RuntimeException("Failed to create GeoScaling smart subdomain: "+e, e); } } private void deleteSmartSubdomain(int primaryDomainId, int smartSubdomainId) { try { String url = MessageFormat.format( "https://{0}/{1}?module=smart_subdomains&id={2,number,#}&delete={3,number,#}", HOST, PATH, primaryDomainId, smartSubdomainId); sendRequest(new HttpGet(url), true); } catch (Exception e) { throw new RuntimeException("Failed to delete GeoScaling smart subdomain: "+e, e); } } private void configureSmartSubdomain(int primaryDomainId, int smartSubdomainId, String smartSubdomainName, long flags, String phpScript) { try { smartSubdomainName = smartSubdomainName.toLowerCase(); String url = MessageFormat.format( "https://{0}/{1}?module=smart_subdomain&id={2,number,#}&subdomain_id={3,number,#}", HOST, PATH, primaryDomainId, smartSubdomainId); HttpPost request = new HttpPost(url); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("MAX_FILE_SIZE", "65536")); nameValuePairs.add(new BasicNameValuePair("name", smartSubdomainName)); if ((flags & PROVIDE_NETWORK_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_as_info", "on")); if ((flags & PROVIDE_CITY_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_city_info", "on")); if ((flags & PROVIDE_COUNTRY_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_country_info", "on")); if ((flags & PROVIDE_EXTRA_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_extra_info", "on")); if ((flags & PROVIDE_UPTIME_INFO) != 0) nameValuePairs.add(new BasicNameValuePair("share_uptime_info", "on")); nameValuePairs.add(new BasicNameValuePair("code", phpScript)); request.setEntity(new UrlEncodedFormEntity(nameValuePairs)); sendRequest(request, true); } catch (Exception e) { throw new RuntimeException("Failed to update GeoScaling smart subdomain: "+e, e); } } private void editSubdomainRecord(int primaryDomainId, String record, String type, String content) { try { String url = MessageFormat.format( "https://{0}/{1}?", HOST, "dns2/ajax/add_record.php"); HttpPost request = new HttpPost(url); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("id", ""+primaryDomainId)); nameValuePairs.add(new BasicNameValuePair("name", record)); nameValuePairs.add(new BasicNameValuePair("type", type)); nameValuePairs.add(new BasicNameValuePair("content", content)); nameValuePairs.add(new BasicNameValuePair("ttl", "300")); nameValuePairs.add(new BasicNameValuePair("prio", "0")); request.setEntity(new UrlEncodedFormEntity(nameValuePairs)); sendRequest(request, true); } catch (Exception e) { throw new RuntimeException("Failed to update GeoScaling smart subdomain: "+e, e); } } protected HttpResponse sendRequest(HttpUriRequest request, boolean consumeResponse) throws ClientProtocolException, IOException { if (log.isDebugEnabled()) log.debug("Geoscaling request: "+ request.getURI()+ (request instanceof HttpPost ? " "+((HttpPost)request).getEntity() : "")); HttpResponse response = httpClient.execute(request); if (log.isDebugEnabled()) log.debug("Geoscaling response: "+response); if (consumeResponse) EntityUtils.consume(response.getEntity()); return response; } private static String getTextContent(Node n) { StringBuffer sb = new StringBuffer(); NodeList childNodes = n.getChildNodes(); for (int i = 0; i < childNodes.getLength(); ++i) { Node child = childNodes.item(i); if (child.getNodeType() == Node.TEXT_NODE) sb.append(child.getNodeValue()); else sb.append(getTextContent(child)); } return sb.toString(); } }