Compare commits
14 commits
Author | SHA1 | Date | |
---|---|---|---|
e1ddb50589 | |||
60b260c86f | |||
efe4533e72 | |||
59716d25b2 | |||
4107c86082 | |||
fcbac544d8 | |||
33059e1971 | |||
18bb8ee8c6 | |||
d8928a2037 | |||
06bdc39a75 | |||
84100ee981 | |||
369b7a94af | |||
6c0e596a09 | |||
a1d67cedb8 |
6 changed files with 117 additions and 19 deletions
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Alex Kelly
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
56
README.md
Normal file
56
README.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
Chainlink
|
||||||
|
=========
|
||||||
|
|
||||||
|
Chainlink is a utility that will run on a webserver to handle crypto-backed
|
||||||
|
domain names Specifically this works with
|
||||||
|
[Unstoppable Domains](https://unstoppabledomains.com)
|
||||||
|
This utilizes public IPFS gateways to display IPFS content, or will redirect to
|
||||||
|
whatever the URL is set to in the Unstoppable Domain management.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
To install this on your own server:
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. Install the python dependancies via `pip install -r requirements.txt`
|
||||||
|
3. Run the server with `./chainlink.py`
|
||||||
|
4. You can now hit the app at http://localhost:5000
|
||||||
|
|
||||||
|
Testing on a running instance
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Assuming that it is running (I could be messing with it, or it might just be
|
||||||
|
broken), you may try this without installing by going to
|
||||||
|
https://chainlink.arachnitech.com/
|
||||||
|
|
||||||
|
URL Patterns
|
||||||
|
============
|
||||||
|
|
||||||
|
The general format is http://localhost:5000/{domain}/{action}
|
||||||
|
where {domain} is a .crypto or .zil name registered with Unstoppable
|
||||||
|
Domains and {action} can be
|
||||||
|
* html - uses the IPFS hash set in Unstoppable Domains management
|
||||||
|
* redir - uses the redirect_url set in Unstoppable Domains management
|
||||||
|
* raw - displays an HTML table view of the full JSON returned from Unstoppable
|
||||||
|
Domains API
|
||||||
|
|
||||||
|
The default action is "html", so if you don't specify anything it will attempt
|
||||||
|
to use that field.
|
||||||
|
|
||||||
|
Browser Search Engine
|
||||||
|
=====================
|
||||||
|
|
||||||
|
I added a browser search engine for each of the UD domains (.zil and .crypto) so
|
||||||
|
that, when I type `.crypto domain` The browser will automatically redirect based
|
||||||
|
on the output from my chainlink script.
|
||||||
|
|
||||||
|
The settings to make that work for .crypto is: <p>
|
||||||
|
![.crypto search registration][crypto]
|
||||||
|
|
||||||
|
It's basically the same for .zil: <p>
|
||||||
|
![.crypto search registration][zil]
|
||||||
|
|
||||||
|
|
||||||
|
[crypto]: images/cryptosearch.png
|
||||||
|
[zil]: images/zilsearch.png
|
59
chainlink.py
59
chainlink.py
|
@ -1,25 +1,34 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Utilize crypto domains DNS and either redirect, or display information."""
|
"""Utilize crypto domains DNS and either redirect, or display information."""
|
||||||
from bottle import Bottle, run, response, request
|
from bottle import Bottle, run, response, request, static_file, redirect
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
import json2html
|
import json2html
|
||||||
|
|
||||||
VERSION = '0.0.1'
|
VERSION = '0.1.0'
|
||||||
|
|
||||||
app = Bottle()
|
app = Bottle()
|
||||||
|
|
||||||
|
|
||||||
def domainLookup(domain):
|
def domainLookup(domain):
|
||||||
apibase = 'https://unstoppabledomains.com/api/v1/'
|
"""Return a dictionary from JSON results of API call
|
||||||
dnslookup = requests.get(apibase + domain)
|
or return False if there is an error
|
||||||
domainJSON = json.loads(dnslookup.content)
|
"""
|
||||||
print(domainJSON)
|
try:
|
||||||
return domainJSON
|
apibase = 'https://unstoppabledomains.com/api/v1/'
|
||||||
|
dnslookup = requests.get(apibase + domain)
|
||||||
|
if dnslookup.status_code == 200:
|
||||||
|
domainInfo = json.loads(dnslookup.content)
|
||||||
|
return domainInfo
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def root():
|
def root():
|
||||||
|
"""Index page, just displays information about URL formatting"""
|
||||||
host = request.get_header('host')
|
host = request.get_header('host')
|
||||||
helptext = f"""
|
helptext = f"""
|
||||||
<p>General format is {host}/<domain>/<action>
|
<p>General format is {host}/<domain>/<action>
|
||||||
|
@ -33,7 +42,7 @@ def root():
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>html</td>
|
<td>html</td>
|
||||||
<td>Hit the IPFS hash via cloudflare-ipfs.com</td>
|
<td>Hit the IPFS hash via gateway.ipfs.io</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>redir</td>
|
<td>redir</td>
|
||||||
|
@ -49,10 +58,24 @@ def root():
|
||||||
@app.route("/<domain>/")
|
@app.route("/<domain>/")
|
||||||
@app.route("/<domain>/<action>")
|
@app.route("/<domain>/<action>")
|
||||||
def redirectDomain(domain, action=None):
|
def redirectDomain(domain, action=None):
|
||||||
|
"""Handle the actual redirect for the domain queried"""
|
||||||
lookupResult = domainLookup(domain)
|
lookupResult = domainLookup(domain)
|
||||||
redirect_url = lookupResult['ipfs']['redirect_domain']
|
if domain == 'yourdomain.crypto' or domain == 'yourdomain.zil':
|
||||||
html = lookupResult['ipfs']['html']
|
redirect('/')
|
||||||
if action == 'redir':
|
if lookupResult:
|
||||||
|
redirect_url = lookupResult['ipfs']['redirect_domain']
|
||||||
|
ipfs_hash = lookupResult['ipfs']['html']
|
||||||
|
else:
|
||||||
|
return f"Unable to get info for {domain}"
|
||||||
|
if action is None or action == 'html':
|
||||||
|
response.status = 302
|
||||||
|
if not ipfs_hash.startswith('/ip'):
|
||||||
|
ipfshash = "ipfs/" + ipfs_hash
|
||||||
|
else:
|
||||||
|
ipfshash = ipfs_hash
|
||||||
|
response.set_header('Location',
|
||||||
|
f"https://gateway.ipfs.io/{ipfshash}")
|
||||||
|
elif action == 'redir':
|
||||||
try:
|
try:
|
||||||
if not redirect_url.startswith('http'):
|
if not redirect_url.startswith('http'):
|
||||||
redirect_url = "http://" + redirect_url
|
redirect_url = "http://" + redirect_url
|
||||||
|
@ -60,17 +83,15 @@ def redirectDomain(domain, action=None):
|
||||||
response.set_header('Location', redirect_url)
|
response.set_header('Location', redirect_url)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return f'Did not find a redirect for {domain}'
|
return f'Did not find a redirect for {domain}'
|
||||||
elif action is None or action == 'html':
|
|
||||||
response.status = 302
|
|
||||||
if not html.startswith('/ip'):
|
|
||||||
ipfshash = "ipfs/" + html
|
|
||||||
else:
|
|
||||||
ipfshash = html
|
|
||||||
response.set_header('Location',
|
|
||||||
f"https://cloudflare-ipfs.com/{ipfshash}")
|
|
||||||
elif action == 'raw':
|
elif action == 'raw':
|
||||||
return json2html.json2html.convert(json=lookupResult)
|
return json2html.json2html.convert(json=lookupResult)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/favicon.ico")
|
||||||
|
def favicon():
|
||||||
|
"""Display a favicon to make the errors go away :)"""
|
||||||
|
return static_file('favicon.ico', root='images')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run(app, host='0.0.0.0', port='5000', reloader=True)
|
run(app, host='0.0.0.0', port='5000', reloader=True)
|
||||||
|
|
BIN
images/cryptosearch.png
Normal file
BIN
images/cryptosearch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
images/favicon.ico
Normal file
BIN
images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
images/zilsearch.png
Normal file
BIN
images/zilsearch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in a new issue