Skip to content

Instantly share code, notes, and snippets.

@dankrause
Last active September 10, 2024 19:45
Show Gist options
  • Save dankrause/5585907 to your computer and use it in GitHub Desktop.
Save dankrause/5585907 to your computer and use it in GitHub Desktop.
Example code to use the (unofficial, unsupported, undocumented) hover.com DNS API.
import requests
class HoverException(Exception):
pass
class HoverAPI(object):
def __init__(self, username, password):
params = {"username": username, "password": password}
r = requests.post("https://www.hover.com/api/login", params=params)
if not r.ok or "hoverauth" not in r.cookies:
raise HoverException(r)
self.cookies = {"hoverauth": r.cookies["hoverauth"]}
def call(self, method, resource, data=None):
url = "https://www.hover.com/api/{0}".format(resource)
r = requests.request(method, url, data=data, cookies=self.cookies)
if not r.ok:
raise HoverException(r)
if r.content:
body = r.json()
if "succeeded" not in body or body["succeeded"] is not True:
raise HoverException(body)
return body
# connect to the API using your account
client = HoverAPI("myusername", "mypassword")
# get details of a domains without DNS records
client.call("get", "domains")
# get all domains and DNS records
client.call("get", "dns")
# notice the "id" field of domains in response to the above calls - that's needed
# to address the domains individually, like so:
# get details of a specific domain without DNS records
client.call("get", "domains/dom123456")
# get DNS records of a specific domain:
client.call("get", "domains/dom123456/dns")
# create a new A record:
record = {"name": "mysubdomain", "type": "A", "content": "127.0.0.1"}
client.call("post", "domains/dom123456/dns", record)
# create a new SRV record
# note that content is "{priority} {weight} {port} {target}"
record = {"name": "mysubdomain", "type": "SRV", "content": "10 10 123 __service"}
client.call("post", "domains/dom123456/dns", record)
# create a new MX record
# note that content is "{priority} {host}"
record = {"name": "mysubdomain", "type": "MX", "content": "10 mail"}
client.call("post", "domains/dom123456/dns", record)
# notice the "id" field of DNS records in the above calls - that's
# needed to address the DNS records individually, like so:
# update an existing DNS record
client.call("put", "dns/dns1234567", {"content": "127.0.0.1"})
# delete a DNS record:
client.call("delete", "dns/dns1234567")
#!/usr/bin/python
"""
bulkhover.py 1.1
This is a command-line script to import and export DNS records for a single
domain into or out of a hover account.
Usage:
bulkhover.py [options] (import|export) <domain> <dnsfile>
bulkhover.py (-h | --help)
bulkhover.py --version
Options:
-h --help Show this screen
--version Show version
-c --conf=<conf> Path to conf
-u --username=<user> Your hover username
-p --password=<pass> Your hover password
-f --flush Delete all existing records before importing
Examples:
The DNS file should have one record per line, in the format:
{name} {type} {content}
For example:
www A 127.0.0.1
@ MX 10 example.com
Since the script output is in the same format as its input, you can use shell
pipelines to do complex operations.
Copy all DNS records from one domain to another:
bulkhover.py -c my.conf export example.com - | ./bulkhover.py -c my.conf -f import other.com -
Copy only MX records from one domain to another:
./bulkhover.py -c my.conf export foo.com - | awk '$2 == "MX" {print $0}' | ./bulkhover.py -c my.conf import bar.com -
To avoid passing your username and password in the command-line, you can use
a conf file that contains them instead:
[hover]
username=YOUR_USERNAME
password=YOUR_PASSWORD
"""
import ConfigParser
import docopt
import requests
import sys
class HoverException(Exception):
pass
class HoverAPI(object):
def __init__(self, username, password):
params = {"username": username, "password": password}
r = requests.post("https://www.hover.com/api/login", params=params)
if not r.ok or "hoverauth" not in r.cookies:
raise HoverException(r)
self.cookies = {"hoverauth": r.cookies["hoverauth"]}
def call(self, method, resource, data=None):
url = "https://www.hover.com/api/{0}".format(resource)
r = requests.request(method, url, data=data, cookies=self.cookies)
if not r.ok:
raise HoverException(r)
if r.content:
body = r.json()
if "succeeded" not in body or body["succeeded"] is not True:
raise HoverException(body)
return body
def import_dns(username, password, domain, filename, flush=False):
try:
client = HoverAPI(username, password)
except HoverException as e:
raise HoverException("Authentication failed")
if flush:
records = client.call("get", "domains/{0}/dns".format(domain))["domains"][0]["entries"]
for record in records:
client.call("delete", "dns/{0}".format(record["id"]))
print "Deleted {name} {type} {content}".format(**record)
domain_id = client.call("get", "domains/{0}".format(domain))["domain"]["id"]
if filename == "-": filename = "/dev/stdin"
with open(filename, "r") as f:
for line in f:
parts = line.strip().split(" ", 2)
record = {"name": parts[0], "type": parts[1], "content": parts[2]}
client.call("post", "domains/{0}/dns".format(domain), record)
print "Created {name} {type} {content}".format(**record)
def export_dns(username, password, domain, filename):
try:
client = HoverAPI(username, password)
except HoverException as e:
raise HoverException("Authentication failed")
records = client.call("get", "domains/{0}/dns".format(domain))["domains"][0]["entries"]
if filename == "-": filename = "/dev/stdout"
with open(filename, "w") as f:
for record in records:
f.write("{name} {type} {content}\n".format(**record))
def main(args):
def get_conf(filename):
config = ConfigParser.ConfigParser()
config.read(filename)
items = dict(config.items("hover"))
return items["username"], items["password"]
if args["--conf"] is None:
if not all((args["--username"], args["--password"])):
print("You must specifiy either a conf file, or a username and password")
return 1
else:
username, password = args["--username"], args["--password"]
else:
username, password = get_conf(args["--conf"])
try:
if args["import"]:
import_dns(username, password, args["<domain>"], args["<dnsfile>"], args["--flush"])
elif args["export"]:
export_dns(username, password, args["<domain>"], args["<dnsfile>"])
except HoverException as e:
print "Unable to update DNS: {0}".format(e)
return 1
if __name__ == "__main__":
version = __doc__.strip().split("\n")[0]
args = docopt.docopt(__doc__, version=version)
status = main(args)
sys.exit(status)
#!/usr/bin/env python
"""
dynhover.py 1.2
This tool will update an A record for given (sub)domain in your hover.com
with your IP, or an IP that you specify
Usage:
dynhover.py (-c <conf> | -u <user> -p <password>) <domain>
dynhover.py (-h | --help)
dynhover.py --version
Options:
-h --help Show this screen
--version Show version
-c --conf=<conf> Path to conf
-u --username=<user> Your hover username
-p --password=<pass> Your hover password
-i --ip=<ip> An IP to set (auto-detected by default)
"""
import ConfigParser
import docopt
import requests
import sys
class HoverException(Exception):
pass
class HoverAPI(object):
def __init__(self, username, password):
params = {"username": username, "password": password}
r = requests.post("https://www.hover.com/api/login", params=params)
if not r.ok or "hoverauth" not in r.cookies:
raise HoverException(r)
self.cookies = {"hoverauth": r.cookies["hoverauth"]}
def call(self, method, resource, data=None):
url = "https://www.hover.com/api/{0}".format(resource)
r = requests.request(method, url, data=data, cookies=self.cookies)
if not r.ok:
raise HoverException(r)
if r.content:
body = r.json()
if "succeeded" not in body or body["succeeded"] is not True:
raise HoverException(body)
return body
def get_public_ip():
return requests.get("http://ifconfig.me/ip").content
def update_dns(username, password, fqdn, ip):
try:
client = HoverAPI(username, password)
except HoverException as e:
raise HoverException("Authentication failed")
dns = client.call("get", "dns")
dns_id = None
for domain in dns["domains"]:
if fqdn == domain["domain_name"]:
fqdn = "@.{domain_name}".format(**domain)
for entry in domain["entries"]:
if entry["type"] != "A": continue
if "{0}.{1}".format(entry["name"], domain["domain_name"]) == fqdn:
dns_id = entry["id"]
break
if dns_id is None:
raise HoverException("No DNS record found for {0}".format(fqdn))
response = client.call("put", "dns/{0}".format(dns_id), {"content": my_ip})
if "succeeded" not in response or response["succeeded"] is not True:
raise HoverException(response)
def main(args):
if args["--username"]:
username, password = args["--username"], args["--password"]
else:
config = ConfigParser.ConfigParser()
config.read(args["--conf"])
items = dict(config.items("hover"))
username, password = items["username"], items["password"]
domain = args["<domain>"]
ip = args["--ip"] or get_public_ip()
try:
update_dns(username, password, domain, ip)
except HoverException as e:
print "Unable to update DNS: {0}".format(e)
return 1
return 0
if __name__ == "__main__":
version = __doc__.strip().split("\n")[0]
args = docopt.docopt(__doc__, version=version)
status = main(args)
sys.exit(status)
#!/bin/bash
[[ $# -lt 3 ]] && echo "Usage: $0 USERNAME PASSWORD DNS_ID"
USERNAME=${1}
PASSWORD=${2}
DNS_ID=${3}
# find your DNS ID here: https://www.hover.com/api/domains/yourdomain.com/dns/
# (replace "yourdomain.com" with your actual domain, and look for the record
# you want to change. The ID looks like: dns1234567)
IP=$(curl "http://ifconfig.me/ip" -s)
curl "https://www.hover.com/api/dns/${DNS_ID}" \
-X PUT \
-d "content=${IP}" \
-s \
-b <(curl "https://www.hover.com/api/login" \
-X POST \
-G \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
-s \
-o /dev/null \
-c -)
echo
@barcoboy
Copy link

My logins to hover.com are not 2FA enforced. I enabled 2FA a while ago, and as soon as I saw it broke my dynamic DNS updates, I turned it off. Just tried logging into the Hover website now and there was no prompt for 2FA, nor did it ask me to re-enable it or set it back up again. I am no longer using this code, but rather some calls to the Hover API with curl, and everything has continued to work fine ever since I turned 2FA off.

@wayneconnolly
Copy link

@barcoboy is this still working? Can you show the login curl please? I have no 2FA and keep getting locked out.

@barcoboy
Copy link

@wayneconnolly, here are the two curl calls I use to add an entry to my Hover DNS:

#Login
curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null

#Add to DNS
curl -k "https://www.hover.com/api/domains/mydomain.com/dns" -X POST -d "name=${NAME}" -d "type=TXT" -d "content=${DATA}" -s -S -b $COOKIEJAR

@wayneconnolly
Copy link

wayneconnolly commented Jun 29, 2023

@wayneconnolly, here are the two curl calls I use to add an entry to my Hover DNS:

#Login curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null

#Add to DNS curl -k "https://www.hover.com/api/domains/mydomain.com/dns" -X POST -d "name=${NAME}" -d "type=TXT" -d "content=${DATA}" -s -S -b $COOKIEJAR

Thank you very much. I'll try this later today. I do note that the login is different so that's a positive thing. https://www.hover.com/signin/auth.json. I emailed hover yesterday asking how to do it also. No idea if they will reply in a helpful manner.

UPDATE: I just tried with username password as you described and it gets a 404. A direct curl to that endpoint shows a 404 also. Could you please confirm on your end?

$ curl -k "https://www.hover.com/api/login"
<!DOCTYPE html>
<html>
<head>
<script src="https://browser.sentry-cdn.com/6.2.3/bundle.min.js" crossorigin="anonymous"></script>
<script src="https://browser.sentry-cdn.com/6.2.3/bundle.tracing.min.js" crossorigin="anonymous"></script>
<script>
  if (window.Sentry) {
    var release = "";
    var username = "";
    var admin = "";
    Sentry.init({
      dsn: "https://[email protected]/164872",
      release,
      ignoreErrors: [
        "gCrWeb",
        "hotjar",
        "bat.bing.com",

        // The rest of this list is taken from
        //   https://gist.github.com/impressiver/5092952
        // which is linked from:
        //   https://docs.sentry.io/clients/javascript/tips/
        "top.GLOBALS",
        "originalCreateNotification",
        "canvas.contentDocument",
        "MyApp_RemoveAllHighlights",
        "http://tt.epicplay.com",
        "Can't find variable: ZiteReader",
        "jigsaw is not defined",
        "ComboSearch is not defined",
        "http://loading.retry.widdit.com/",
        "atomicFindClose",
        "fb_xd_fragment",
        "bmi_SafeAddOnload",
        "EBCallBackMessageReceived",
        "conduitPage",
      ],
      whitelistUrls: [/www\.hover\.com/i],
      integrations: [new Sentry.Integrations.BrowserTracing()],
    });

    if (username) {
      Sentry.setUser({ username });
    }
    if (admin) {
      Sentry.setContext("admin", {
        username: admin,
      });
    }
  }
</script>

<title>Domain Names | Buy Domains &amp; Email At Hover.com</title>
<meta content='text/html; charset=UTF-8' http-equiv='content-type'>
<meta content='IE=Edge' http-equiv='X-UA-Compatible'>
<meta content='3CbaVvw-I7MlrmmmHz0bfbko7oMCW1mn2u65uWsWWB8' name='google-site-verification'>
<meta content='b6zle0gvjb63epbzegrrmbp4wwltnn' name='facebook-domain-verification'>
<meta content='width=device-width, initial-scale=1.0' name='viewport'>
<meta content='telephone=no' name='format-detection'>
<meta content='Find the perfect domain name for your idea at Hover. All domains come with industry-leading customer support and free WHOIS privacy. Name your passion today!' name='description'>
<meta content='Find the perfect domain name for your idea at Hover. All domains come with industry-leading customer support and free WHOIS privacy. Name your passion today!' property='og:description'>
<meta content='Domain Names | Buy Domains &amp; Email At Hover.com' property='og:title'>
<meta content='/packs/src/application/images/home/og_hover-ff5e561a072494f142806a1ee8541fca.png' property='og:image'>
<link href='https://www.hover.com/api/login' rel='canonical'>
<link href='/site.webmanifest' rel='manifest'>
<link color='#229e87' href='/safari-pinned-tab.svg' rel='mask-icon'>
<link href='/apple-touch-icon-57x57.png' rel='apple-touch-icon' sizes='57x57'>
<link href='/apple-touch-icon-114x114.png' rel='apple-touch-icon' sizes='114x114'>
<link href='/apple-touch-icon-72x72.png' rel='apple-touch-icon' sizes='72x72'>
<link href='/apple-touch-icon-144x144.png' rel='apple-touch-icon' sizes='144x144'>
<link href='/apple-touch-icon-60x60.png' rel='apple-touch-icon' sizes='60x60'>
<link href='/apple-touch-icon-120x120.png' rel='apple-touch-icon' sizes='120x120'>
<link href='/apple-touch-icon-76x76.png' rel='apple-touch-icon' sizes='76x76'>
<link href='/apple-touch-icon-152x152.png' rel='apple-touch-icon' sizes='152x152'>
<link href='/apple-touch-icon-180x180.png' rel='apple-touch-icon' sizes='180x180'>
<link href='/favicon-196x196.png' rel='icon' sizes='196x196' type='image/png'>
<link href='/favicon-160x160.png' rel='icon' sizes='160x160' type='image/png'>
<link href='/favicon-96x96.png' rel='icon' sizes='96x96' type='image/png'>
<link href='/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png'>
<link href='/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png'>
<meta content='#229e87' name='msapplication-TileColor'>
<meta content='/mstile-144x144.png' name='msapplication-TileImage'>
<meta content='#229e87' name='theme-color'>

<link rel="stylesheet" media="all" href="/packs/fonts-fba7b87f80dc4ec5b49cfc53e3d92ff7.css" />
<link rel="stylesheet" media="all" href="/packs/application-3825aff153368bada4a3ba66c31869af.css" />
<link rel="stylesheet" media="all" href="/packs/hover_refresh-c7bcda836c7a2770625a7ecff139ed21.css" />
<link rel="stylesheet" media="all" href="https://fonts.googleapis.com/css?family=Open+Sans:400,300,600" />
<script src="/packs/manifest-35aff3e5d85edee906e3.js"></script>

</head>
<body class='has_new_styles'>
<script>dataLayer = [{}];</script>
<noscript><iframe src="//www.googletagmanager.com/ns.html?id=GTM-TDSBDF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TDSBDF');</script>


<header class='main__header'>
<div class='main__logo'>
<a href='/' title='Return to home page'>
<img alt="Hover Logo" src="/packs/src/application/images/common/hv-logo-2020-e535dce1f67e9a648643e23cac750002.svg" />
</a>
</div>
<nav class='main__nav'>
<div class='nav__primary'>
<a class='nav__link' href='/domains' title='Domains'>Domains</a>
<a class='nav__link' href='/email' title='Email'>Email</a>
<a class='nav__link' href='/about' title='About Us'>About Us</a>
<a class='nav__link' href='https://hover.blog' title='Blog'>Blog</a>
</div>
<div class='nav__secondary'>
<a class='nav__link' href='https://help.hover.com'>Help</a>
<a class='nav__link has-more' data-trigger='signin' href='/signin'>
Sign In
<i class='fas fa-chevron-down'></i>
</a>
<div class='signin__menu'>
<a class='signin__item' href='/signin'>
<div class='signin__heading'>Control Panel</div>
<p>View and manage domains, email, and features for your account.</p>
<i class='far fa-arrow-right'></i>
</a>
<a class='signin__item' href='https://mail.hover.com/'>
<div class='signin__heading'>Webmail</div>
<p>Access your email inbox from your web browser.</p>
<i class='far fa-arrow-right'></i>
</a>
<a class='signin__item' href='/signup'>Need an account? Sign up</a>
</div>
</div>
<div class='nav__cart' style='display: none'>
<a class='cart__button' href='/cart'>
<i class='fas fa-shopping-cart'></i>
<span class='cart__count'></span>
</a>
</div>
</nav>
<a class='mobile__menu-trigger' data-trigger='menu'>
<i class='far fa-bars'></i>
</a>
</header>
<div aria-hidden='true' class='mobile__menu'>
<a class='mobile__menu-trigger' data-trigger='menu'>
<i class='fal fa-times'></i>
</a>
<nav class='mobile__nav'>
<a class='nav__link' href='/domains'>Domains</a>
<a class='nav__link' href='/email'>Email</a>
<a class='nav__link' href='/about'>About Us</a>
<a class='nav__link' href='https://hover.blog'>Blog</a>
<div class='nav__separator'></div>
<a class='nav__link' href='https://help.hover.com'>Help</a>
<a class='nav__link' href='https://mail.hover.com/'>Webmail</a>
<a class='nav__link' href='/signin'>Sign In</a>
</nav>
</div>

<div id='your_account_menu'>
<ul>
<li class='title'>
Your Account
</li>
</ul>
</div>


<main>



<div class='grid_container' id='content'>

<div class='home error not_found'>
<section class='hero' style='padding-top: 40px;'>
<img src='/packs/src/application/images/404-456216b46583c9e46ecec75ca1c626cc.svg' width='580'>
<h1>is a great number...but not a page at Hover.com.</h1>
</section>
</div>

</div>
</main>
<footer class='main__footer'>
<div class='footer__top'>
<div class='footer__brand'>
<div class='main__logo'>
<a href='/'>
<img src='https://hover-assets.s3.ca-central-1.amazonaws.com/images/logo.svg'>
</a>
</div>
<div class='footer__social'>
<a aria-label='Twitter' href='https://twitter.com/hover'>
<i class='fab fa-twitter'></i>
</a>
<a aria-label='Facebook' href='https://www.facebook.com/hover'>
<i class='fab fa-facebook-f'></i>
</a>
<a aria-label='LinkedIn' href='https://www.linkedin.com/showcase/hover-domains-and-email/'>
<i class='fab fa-linkedin-in'></i>
</a>
<a aria-label='TikTok' href='https://www.tiktok.com/@hoverdomainnames'>
<i class='fab fa-tiktok'></i>
</a>
</div>
</div>
<div class='footer__column footer__column--products'>
<div class='footer__heading'>Products</div>
<a class='footer__link' href='/domains'>Domains</a>
<a class='footer__link indented' href='/transfer-in'>Transfer</a>
<a class='footer__link indented' href='/renew'>Renew</a>
<a class='footer__link indented' href='/domain-pricing'>Pricing</a>
<a class='footer__link' href='/email'>Email</a>
</div>
<div class='footer__column footer__column--company'>
<div class='footer__heading'>Company</div>
<a class='footer__link' href='/about'>About Us</a>
<a class='footer__link' href='https://hover.blog'>Blog</a>
<a class='footer__link' href='https://www.tucows.com/careers/'>Jobs</a>
<a class='footer__link' href='/affiliates'>Affiliates</a>
</div>
<div class='footer__column footer__column--support'>
<div class='footer__heading'>Support</div>
<a class='footer__link' href='https://help.hover.com'>Help Center</a>
<a class='footer__link' href='https://hoverstatus.com/'>Service Status</a>
</div>
<div class='footer__column'>
<div class='footer__heading'>Account</div>
<a class='footer__link' href='/signin'>Sign In</a>
<a class='footer__link' href='https://mail.hover.com'>Webmail</a>
</div>
</div>
<div class='footer__bottom'>
<div class='footer__legal'>
<a href='/tos'>Terms of Service</a>
<a href='/privacy'>Privacy Policy</a>
</div>
<div class='footer__copyright'>
Copyright © 2023 Hover
</div>
</div>
</footer>


<script src="/packs/jquery-eb76d5d3fc0228c47ac1.js"></script>
<script src="/packs/application-c3f1f2b2a53c1db41d28.js"></script>
<script src="/packs/hover_refresh-fd258324985771c547ed.js"></script>

<script>
 (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
 (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
 })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

 ga('create', 'UA-4171338-2', 'auto');
 ga('require', 'linkid', 'linkid.js');
 ga('set', 'anonymizeIp', true);
 ga('send', 'pageview');
</script>


</body>
</html>

@barcoboy
Copy link

Sorry for the delay in getting back to you... slipped through the cracks!

a 404 error is normal to that URL when doing a GET request... you have use the POST method. Make sure you are doing that by using the "-X POST" command line parameter with curl.

@wayneconnolly
Copy link

Sorry for the delay in getting back to you... slipped through the cracks!

a 404 error is normal to that URL when doing a GET request... you have use the POST method. Make sure you are doing that by using the "-X POST" command line parameter with curl.

Thank you - curl "https://www.hover.com/api/login" -H "Content-type: application/json" -X POST -d "{"password": "$PASSWORD", "username": "$USERNAME"}" -s -S -c $COOKIEJAR -o /dev/null

I was using -X POST.

I contacted hover.com and it's definately been disabled even with 2FA

Lei (Hover Help Center)
Jul 6, 2023, 12:05 EDT

Hello!
 
Thanks for your reply - API is no longer usable to log in to Hover. 
If using an API is critical to your setup, I may suggest our sister company, [OpenSRS.com](http://opensrs.com/).
 
Best,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment