~sircmpwn/names.sr.ht

3d99b9ed23773aeef698f79f8e42410dd35920f2 — Drew DeVault 1 year, 10 months ago fbbb5f5
Initial OpenSRS riggings, domain availability check
M names/blueprints/web.py => names/blueprints/web.py +25 -2
@@ 1,5 1,5 @@
import re
from flask import Blueprint, render_template, request
from flask import Blueprint, render_template, request, redirect, url_for
from flask_login import current_user
from jinja2 import Markup
from names.pygments import ZoneLexer


@@ 8,6 8,10 @@ from pygments.formatters import HtmlFormatter
from srht.flask import loginrequired
from srht.validation import Validation

# TODO: Make OpenSRS integration optional
from names.opensrs import DomainStatus, lookup_domain, get_price, check_transfer
from names.opensrs import name_suggest

web = Blueprint("names.web", __name__)

# https://stackoverflow.com/a/26987741


@@ 36,9 40,28 @@ def create_POST():
            "For IDNs, use punycode.", field="domain_name")
    if not valid.ok:
        return render_template("create.html", **valid.kwargs), 400
    return redirect(url_for(".create_domain_GET", domain=domain))

@web.route("/create/<domain>")
@loginrequired
def create_domain_GET(domain):
    status = lookup_domain(domain)

    # TODO: Check if user has payment info on file
    # TODO: Check if domain is available
    if status == DomainStatus.available:
        price = get_price(domain)
        return render_template("create-domain.html",
                DomainStatus=DomainStatus, status=status, domain=domain,
                price=price)
    else:
        transferable, transfer_email = check_transfer(domain)
        # Note: this could be merged into lookup_domain and save us an API call
        suggestions = name_suggest(domain)
        return render_template("create-domain.html",
                DomainStatus=DomainStatus, status=status, domain=domain,
                transferable=transferable,
                transfer_email=transfer_email,
                suggestions=suggestions)

@web.route("/mockup")
def mockup():

A names/opensrs.py => names/opensrs.py +190 -0
@@ 0,0 1,190 @@
# Based on https://github.com/fatbox/OpenSRS-py
from collections import namedtuple
import enum
import hashlib
import requests
from srht.config import cfg
from xml.etree.ElementTree import fromstring, tostring, SubElement, Element

_OPENSRS_XML_HEADER = "<?xml version='1.0' encoding='UTF-8' standalone='no' ?><!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>"
_OPENSRS_VERSION = '0.9'

_opensrs_key = cfg("names.sr.ht", "opensrs-key")
_opensrs_username = cfg("names.sr.ht", "opensrs-username")
_opensrs_upstream = cfg("names.sr.ht", "opensrs-upstream")

def _xml_to_data(elm, is_list=False):
    """
    This converts an element that has a bunch of 'item' tags
    as children into a Python data structure.
    If is_list is true it is assumed that the child items all
    have numeric indices and should be treated as a list, else
    they are treated as a dict
    """
    if is_list:
        data = []
    else:
        data = {}

    for child in elm:
        if child.tag == 'item':
            if len(child) > 0:
                if child[0].tag == 'dt_assoc':
                    new_data = _xml_to_data(child[0])
                elif child[0].tag == 'dt_array':
                    new_data = _xml_to_data(child[0], is_list=True)
            else:
                new_data = str(child.text)

            key = child.get('key')
            if is_list:
                data.insert(int(key), new_data)
            else:
                data[key] = new_data

    return data

def _data_to_xml(elm, key, data):
    """
    data_to_xml adds a item sub element to elm and then sets the
    text if its not a list or dict, otherwise it recurses
    """
    item = SubElement(elm, 'item', { 'key': key })
    if isinstance(data, dict):
        _data_to_dt_assoc(item, data)
    elif isinstance(data, list):
        _data_to_dt_array(item, data)
    else:
        item.text = str(data)
    return item

def _data_to_dt_array(elm, list):
    """
    Adds an list of data in the format that opensrs requires,
    uses data_to_xml to recurse
    """
    _dt_array = SubElement(elm, 'dt_array')
    key = 0
    for ent in list:
        _data_to_xml(_dt_array, str(key), ent)
        key += 1

def _data_to_dt_assoc(elm, data):
    """
    Adds an associative array of data in the format that opensrs
    requires, uses data_to_xml to recurse
    """
    _dt_assoc = SubElement(elm, 'dt_assoc')
    for key in data.keys():
        _data_to_xml(_dt_assoc, key, data[key])

def _post(action, obj, attrs, extra_items=None):
    env = Element("OPS_envelope")

    header = SubElement(env, 'header')
    version = SubElement(header, 'version')
    version.text = _OPENSRS_VERSION

    body = SubElement(env, 'body')
    data_block = SubElement(body, 'data_block')

    params = {
        'protocol': 'XCP',
        'action': action,
        'object': obj,
        'attributes': attrs,
    }

    if isinstance(extra_items, dict):
        params.update(extra_items)

    _data_to_dt_assoc(data_block, params)
    data = f"{_OPENSRS_XML_HEADER}{tostring(env)}"

    # wut
    checksum = hashlib.md5((data + _opensrs_key).encode()).hexdigest()
    checksum = hashlib.md5((checksum + _opensrs_key).encode()).hexdigest()

    r = requests.post(_opensrs_upstream, data=data.encode(), headers={
        'Content-Type': 'text/xml',
        'X-Username': _opensrs_username,
        'X-Signature': checksum,
    })

    if r.status_code != 200:
        raise Exception(f"Unexpected response from OpenSRS\n{r.text}")

    dom = fromstring(r.text)
    data_block = dom.find('body/data_block/dt_assoc')
    if data_block is None:
        raise Exception("Unexpected response from OpenSRS\n{r.text}")

    print(r.text)
    return _xml_to_data(data_block)

class DomainStatus(enum.Enum):
    available = 210
    taken = 211

def lookup_domain(domain):
    """
    Returns True if this domain is available.
    """
    # TODO: Deal with premium domains
    r = _post("lookup", "domain", attrs={
        "domain": domain,
    })
    print(r)
    return DomainStatus(int(r["response_code"]))

PricingInfo = namedtuple('PricingInfo', ['cents'])

class RegistrationType(enum.Enum):
    new = "new"
    renewal = "renewal"
    transfer = "trade"

def get_price(domain, reg_type=RegistrationType.new):
    """
    Returns pricing info for this domain.
    """
    # TODO: Should we support the period parameter?
    r = _post("get_price", "domain", attrs={
        "domain": domain,
        "reg_type": reg_type.value,
    })
    print(r)
    return PricingInfo(int(float(r["attributes"]["price"]) * 100))

def check_transfer(domain):
    """
    Checks if this domain can be transferred into OpenSRS. Returns a tuple
    of the transferability (bool) and the email address the transfer request
    must be acknowledged at.
    """
    r = _post("check_transfer", "domain attributes", attrs={
        "domain": domain,
        "get_request_address": "1",
    })
    print(r)
    return (r["attributes"]["transferrable"] == '1',
            r["attributes"].get("request_address"))

Suggestion = namedtuple("Suggestion", ["domain", "status"])

def name_suggest(search_string, maximum=20):
    """
    """
    # TODO: Ask OpenSRS to add suggestions for more gTLDs
    r = _post("name_suggest", "domain", attrs={
        "lookup": search_string,
        "maximum": str(maximum),
        "searchstring": search_string,
        "services": ["lookup", "suggestion"],
        "tlds": [".COM", ".NET", ".ORG", ".INFO"],
    })
    print(r)
    to_suggestion = lambda i: Suggestion(i["domain"],
            DomainStatus.available if i["status"] == "available"
            else DomainStatus.taken)
    return [to_suggestion(i) for i in r["attributes"]["lookup"]["items"]]

A names/templates/create-domain.html => names/templates/create-domain.html +76 -0
@@ 0,0 1,76 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
  <form class="col-md-12" method="POST">
    <h3>Register {{domain}}</h3>
    {{csrf_token()}}
    {% if status == DomainStatus.available %}
    <p>
      <strong>{{domain}}</strong> is available. You can register it with us for
      ${{"{:.02f}".format(price.cents / 100.0)}} per year, or use our
      nameservers for free.
    </p>
    <button class="btn btn-primary">
      Purchase this domain {{icon("caret-right")}}
    </button>
    <button class="btn btn-default">
      Configure nameservers {{icon("caret-right")}}
    </button>
    {% else %}
    <p>
      <strong>{{domain}}</strong> is
      <span class='text-danger'>not available</span>,
    {% if transferable %}
      but you may transfer it to us
      {%- if transfer_email %} if you can receive the confirmation email at
      <a href="mailto:{{transfer_email}}">{{transfer_email}}</a>.
      {%- else %}.
      {% endif %}
    </p>

    <button class="btn btn-primary">
      Transfer this domain {{icon("caret-right")}}
    </button>
    <button class="btn btn-default">
      I just want DNS {{icon("caret-right")}}
    </button>
    {% else %} {# if transferable #}
      and cannot be transfered. However, you may use our nameservers if you
      wish.
    </p>

    <button class="btn btn-primary">
      Configure nameservers {{icon("caret-right")}}
    </button>
    <a class="btn btn-default" href="{{url_for('names.web.index')}}">
      Cancel {{icon("caret-right")}}
    </a>
  </form>
  {% endif %} {# if transferable #}

  </form>
  <div class="col-md-6">
    <p>Alternatively, consider these suggestions:</p>
    <div class="event-list">
      {% for suggestion in suggestions %}
      <div class="event">
        {% if suggestion.status == DomainStatus.available %}
        <a href="{{url_for('.create_domain_GET', domain=suggestion.domain)}}">
          <strong>{{suggestion.domain}}</strong>
        </a>
        <span class="text-success pull-right">
          {{icon('check')}} {{suggestion.status.name}}
        </span>
        {% else %}
        <strong>{{suggestion.domain}}</strong>
        <span class="text-danger pull-right">
          {{icon('times')}} {{suggestion.status.name}}
        </span>
        {% endif %}
      </div>
      {% endfor %}
    </div>
  </div>
  {% endif %} {# if status == DomainStatus.available #}
</div>
{% endblock %}

A names/templates/register-domain.html => names/templates/register-domain.html +19 -0
@@ 0,0 1,19 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
  <form class="col-md-12" method="POST">
    <h3>Register {{domain}}</h3>
    <p>
      <strong>{{domain}}</strong> is available for
      ${{"{:.02f}".format(price.cents / 100.0)}} per year!
    </p>
    {{csrf_token()}}
    <button class="btn btn-primary">
      Purchase this domain {{icon("caret-right")}}
    </button>
    <button class="btn btn-default">
      I just want DNS {{icon("caret-right")}}
    </button>
  </form>
</div>
{% endblock %}