1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6from azure.cli.core.azclierror import ValidationError
7from azure.cli.core.commands import DeploymentOutputLongRunningOperation
8from azure.cli.core.commands.client_factory import get_mgmt_service_client
9from azure.cli.core.profiles import ResourceType
10from azure.cli.core.util import sdk_no_wait, random_string
11from azure.mgmt.web.models import NameIdentifier
12from knack.util import CLIError
13from knack.log import get_logger
14
15from ._client_factory import web_client_factory
16
17logger = get_logger(__name__)
18
19
20def create_domain(cmd, resource_group_name, hostname, contact_info, privacy=True, auto_renew=True,  # pylint: disable=too-many-locals
21                  accept_terms=False, tags=None, dryrun=False, no_wait=False):
22    from azure.cli.core.commands.arm import ArmTemplateBuilder
23    from azure.cli.command_modules.appservice._template_builder import (build_dns_zone, build_domain)
24    from datetime import datetime
25    import socket
26    import json
27
28    tags = tags or {}
29
30    if not accept_terms and not dryrun:
31        raise CLIError("To purchase and create your custom domain '{}', you must view the terms and conditions "
32                       "using the command `az appservice domain show-terms`, and accept these terms and "
33                       "conditions using the --accept-terms flag".format(hostname))
34
35    try:
36        contact_info = json.loads(contact_info)
37    except Exception:
38        raise CLIError('Unable to load contact info. Please verify the path to your contact info file, '
39                       'and that the format matches the sample found at the following link: '
40                       'https://github.com/AzureAppServiceCLI/appservice_domains_templates'
41                       '/blob/master/contact_info.json')
42    contact_info = verify_contact_info_and_format(contact_info)
43
44    current_time = str(datetime.utcnow()).replace('+00:00', 'Z')
45    local_ip_address = ''
46    try:
47        local_ip_address = socket.gethostbyname(socket.gethostname())
48    except:
49        raise CLIError("Unable to get IP address")
50
51    web_client = web_client_factory(cmd.cli_ctx)
52    hostname_availability = web_client.domains.check_availability(NameIdentifier(name=hostname))
53
54    if not hostname_availability.available:
55        raise ValidationError("Custom domain name '{}' is not available. Please try again "
56                              "with a new hostname.".format(hostname))
57
58    tld = '.'.join(hostname.split('.')[1:])
59    TopLevelDomainAgreementOption = cmd.get_models('TopLevelDomainAgreementOption')
60    domain_agreement_option = TopLevelDomainAgreementOption(include_privacy=bool(privacy), for_transfer=False)
61    agreements = web_client.top_level_domains.list_agreements(name=tld, agreement_option=domain_agreement_option)
62    agreement_keys = [agreement.agreement_key for agreement in agreements]
63
64    if dryrun:
65        logger.warning("Custom domain will be purchased with the below configuration. Re-run command "
66                       "without the --dryrun flag to purchase & create the custom domain")
67        dry_run_params = contact_info.copy()
68        dry_run_params.update({
69            "hostname": hostname,
70            "resource_group_name": resource_group_name,
71            "privacy": bool(privacy),
72            "auto_renew": bool(auto_renew),
73            "agreement_keys": agreement_keys,
74            "accept_terms": bool(accept_terms),
75            "hostname_available": bool(hostname_availability.available),
76            "price": "$11.99 USD" if hostname_availability.available else "N/A"
77        })
78        dry_run_str = r""" {
79                    "hostname" : "%(hostname)s",
80                    "resource_group" : "%(resource_group_name)s",
81                    "contact_info": {
82                            "address1": "%(address1)s",
83                            "address2": "%(address2)s",
84                            "city": "%(city)s",
85                            "country": "%(country)s",
86                            "postal_code": "%(postal_code)s",
87                            "state": "%(state)s",
88                            "email": "%(email)s",
89                            "fax": "%(fax)s",
90                            "job_title": "%(job_title)s",
91                            "name_first": "%(name_first)s",
92                            "name_last": "%(name_last)s",
93                            "name_middle": "%(name_middle)s",
94                            "organization": "%(organization)s",
95                            "phone": "%(phone)s"
96                        },
97                    "privacy": "%(privacy)s",
98                    "auto_renew": "%(auto_renew)s",
99                    "accepted_hostname_purchase_terms": "%(accept_terms)s",
100                    "agreement_keys": "%(agreement_keys)s",
101                    "hostname_available": "%(hostname_available)s",
102                    "price": "%(price)s"
103                    }
104                    """ % dry_run_params
105        return json.loads(dry_run_str)
106
107    dns_zone_id = "[resourceId('Microsoft.Network/dnszones', '{}')]".format(hostname)
108
109    master_template = ArmTemplateBuilder()
110    dns_zone_resource = build_dns_zone(hostname)
111    domain_resource = build_domain(domain_name=hostname,
112                                   local_ip_address=local_ip_address,
113                                   current_time=current_time,
114                                   address1=contact_info['address1'],
115                                   address2=contact_info['address2'],
116                                   city=contact_info['city'],
117                                   country=contact_info['country'],
118                                   postal_code=contact_info['postal_code'],
119                                   state=contact_info['state'],
120                                   email=contact_info['email'],
121                                   fax=contact_info['fax'],
122                                   job_title=contact_info['job_title'],
123                                   name_first=contact_info['name_first'],
124                                   name_last=contact_info['name_last'],
125                                   name_middle=contact_info['name_middle'],
126                                   organization=contact_info['organization'],
127                                   phone=contact_info['phone'],
128                                   dns_zone_id=dns_zone_id,
129                                   privacy=privacy,
130                                   auto_renew=auto_renew,
131                                   agreement_keys=agreement_keys,
132                                   tags=tags,
133                                   dependencies=[dns_zone_id])
134
135    master_template.add_resource(dns_zone_resource)
136    master_template.add_resource(domain_resource)
137
138    template = master_template.build()
139
140    # deploy ARM template
141    deployment_name = 'domain_deploy_' + random_string(32)
142    client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES).deployments
143    DeploymentProperties = cmd.get_models('DeploymentProperties', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES)
144    properties = DeploymentProperties(template=template, parameters={}, mode='incremental')
145    Deployment = cmd.get_models('Deployment', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES)
146    deployment = Deployment(properties=properties)
147    deployment_result = DeploymentOutputLongRunningOperation(cmd.cli_ctx)(
148        sdk_no_wait(no_wait, client.begin_create_or_update, resource_group_name, deployment_name, deployment))
149
150    return deployment_result
151
152
153def show_domain_purchase_terms(cmd, hostname):
154    from azure.mgmt.web.models import TopLevelDomainAgreementOption
155    domain_identifier = NameIdentifier(name=hostname)
156    web_client = web_client_factory(cmd.cli_ctx)
157    hostname_availability = web_client.domains.check_availability(domain_identifier)
158    if not hostname_availability.available:  # api returns false
159        raise CLIError(" hostname: '{}' in not available. Please enter a valid hostname.".format(hostname))
160
161    tld = '.'.join(hostname.split('.')[1:])
162    domain_agreement_option = TopLevelDomainAgreementOption(include_privacy=True, for_transfer=True)
163    agreements = web_client.top_level_domains.list_agreements(name=tld, agreement_option=domain_agreement_option)
164
165    terms = {
166        "hostname": hostname,
167        "hostname_available": hostname_availability.available,
168        "hostname_purchase_price": "$11.99 USD" if hostname_availability.available else None,
169        "legal_terms":
170            "https://storedomainslegalterms.blob.core.windows.net/domain-purchase-legal-terms/legal_terms.txt",
171        "GoDaddy_domain_registration_and_customer_service_agreement":
172            "https://www.godaddy.com/legal/agreements/domain-name-registration-agreement",
173        "ICANN_rights_and_responsibilities_policy":
174            "https://www.icann.org/resources/pages/responsibilities-2014-03-14-en"
175    }
176
177    for agreement in agreements:
178        terms['_'.join(agreement.title.lower().split(' '))] = agreement.url
179
180    return terms
181
182
183def verify_contact_info_and_format(contact_info):
184    # pylint: disable=too-many-statements, too-many-branches
185    return_contact_info = {}
186    required_keys = ['name_first', 'name_last', 'email', 'phone', 'address1', 'country', 'state', 'city', 'postal_code']
187    for required_key in required_keys:
188        if not (required_key in contact_info and 'value' in contact_info[required_key] and contact_info[required_key]['value']):  # pylint: disable=line-too-long
189            raise CLIError("Missing value in contact info: {}".format(required_key))
190
191    import re
192
193    # GoDaddy regex
194    _phone_regex = r"^\+([0-9]){1,3}\.([0-9]\ ?){5,14}$"
195    _person_name_regex = r"^[a-zA-Z0-9\-.,\(\)\\\@&' ]*$"
196    _email_regex = (
197        r"^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:"
198        r"[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]"
199        r"?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$"
200    )
201    _city_regex = r"^[a-zA-Z0-9\-.,' ]+$"
202    _address_regex = r"^[a-zA-Z0-9\-.,'#*@/& ]+$"
203    _postal_code_regex = r"^[a-zA-Z0-9 .\\-]+$"
204
205    # Validate required values
206    if not re.match(_phone_regex, contact_info['phone']['value']):
207        raise CLIError('Invalid value: phone number must match pattern +areacode.phonenumber, '
208                       'for example "+1.0000000000"')
209    return_contact_info['phone'] = contact_info['phone']['value']
210
211    if not re.match(_person_name_regex, contact_info['name_first']['value']):
212        raise CLIError('Invalid value: first name')
213    if len(contact_info['name_first']['value']) > 30:
214        raise CLIError('Invalid value: first name must have a length of at most 30')
215    return_contact_info['name_first'] = contact_info['name_first']['value']
216
217    if not re.match(_person_name_regex, contact_info['name_last']['value']):
218        raise CLIError('Invalid value: last name')
219    if len(contact_info['name_last']['value']) > 30:
220        raise CLIError('Invalid value: last name must have a length of at most 30')
221    return_contact_info['name_last'] = contact_info['name_last']['value']
222
223    if not re.match(_email_regex, contact_info['email']['value']):
224        raise CLIError('Invalid value: email')
225    return_contact_info['email'] = contact_info['email']['value']
226
227    if not re.match(_address_regex, contact_info['address1']['value']):
228        raise CLIError('Invalid value: address1')
229    if len(contact_info['address1']['value']) > 41:
230        raise CLIError('Invalid value: address1 must have a length of at most 41')
231    return_contact_info['address1'] = contact_info['address1']['value']
232
233    allowed_countries = [
234        "AC", "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
235        "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV",
236        "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CV",
237        "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET",
238        "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN",
239        "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL",
240        "IM", "IN", "IO", "IQ", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR",
241        "KV", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC",
242        "MD", "ME", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW",
243        "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA",
244        "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS",
245        "RU", "RW", "SA", "SB", "SC", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "ST",
246        "SV", "SX", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TP", "TR", "TT",
247        "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF",
248        "WS", "YE", "YT", "ZA", "ZM", "ZW"
249    ]
250    if contact_info['country']['value'] not in allowed_countries:
251        raise CLIError('Invalid value: country is not one of the following values: {}'.format(allowed_countries))
252    return_contact_info['country'] = contact_info['country']['value']
253
254    if not 2 <= len(contact_info['state']['value']) <= 30:
255        raise CLIError('Invalid value: state must have a length between 2 and 30')
256    return_contact_info['state'] = contact_info['state']['value']
257
258    if not re.match(_city_regex, contact_info['city']['value']):
259        raise CLIError('Invalid value: city')
260    if len(contact_info['city']['value']) > 30:
261        raise CLIError('Invalid value: city must have a length of at most 30')
262    return_contact_info['city'] = contact_info['city']['value']
263
264    if not re.match(_postal_code_regex, contact_info['postal_code']['value']):
265        raise CLIError('Invalid value: postal code')
266    if not 2 <= len(contact_info['postal_code']['value']) <= 10:
267        raise CLIError('Invalid value: postal code must have a length between 2 and 10')
268    return_contact_info['postal_code'] = contact_info['postal_code']['value']
269
270    # Validate optional params
271    if 'fax' in contact_info and 'value' in contact_info['fax'] and contact_info['fax']['value']:
272        if not re.match(_phone_regex, contact_info['fax']['value']):
273            raise CLIError('Invalid value: fax number must match pattern +areacode.phonenumber, '
274                           'for example "+1.0000000000"')
275        return_contact_info['fax'] = contact_info['fax']['value']
276    else:
277        return_contact_info['fax'] = ''
278
279    if 'job_title' in contact_info and 'value' in contact_info['job_title'] and contact_info['job_title']['value']:
280        if len(contact_info['job_title']['value']) > 41:
281            raise CLIError('Invalid value: job title must have a length of at most 41')
282        return_contact_info['job_title'] = contact_info['job_title']['value']
283    else:
284        return_contact_info['job_title'] = ''
285
286    if ('name_middle' in contact_info and 'value' in contact_info['name_middle'] and contact_info['name_middle']['value']):  # pylint: disable=line-too-long
287        if not re.match(_person_name_regex, contact_info['name_middle']['value']):
288            raise CLIError('Invalid value: middle name')
289        if len(contact_info['name_middle']['value']) > 30:
290            raise CLIError('Invalid value: middle name must have a length of at most 30')
291        return_contact_info['name_middle'] = contact_info['name_middle']['value']
292    else:
293        return_contact_info['name_middle'] = ''
294
295    if ('organization' in contact_info and 'value' in contact_info['organization'] and contact_info['organization']['value']):  # pylint: disable=line-too-long
296        if len(contact_info['organization']['value']) > 41:
297            raise CLIError('Invalid value: organization must have a length of at most 41')
298        return_contact_info['organization'] = contact_info['organization']['value']
299    else:
300        return_contact_info['organization'] = ''
301
302    if 'address2' in contact_info and 'value' in contact_info['address2'] and contact_info['address2']['value']:
303        if not re.match(_address_regex, contact_info['address2']['value']):
304            raise CLIError('Invalid value: address2')
305        if len(contact_info['address2']['value']) > 41:
306            raise CLIError('Invalid value: address2 must have a length of at most 41')
307        return_contact_info['address2'] = contact_info['address2']['value']
308    else:
309        return_contact_info['address2'] = ''
310
311    return return_contact_info
312