]> git.bitcoin.ninja Git - satsto.me/commitdiff
Initial checkin
authorMatt Corallo <git@bluematt.me>
Fri, 12 Jul 2024 12:58:58 +0000 (12:58 +0000)
committerMatt Corallo <git@bluematt.me>
Fri, 12 Jul 2024 12:58:58 +0000 (12:58 +0000)
dnssec_prover_wasm.js [new file with mode: 0644]
dnssec_prover_wasm_bg.wasm [new file with mode: 0644]
doh_lookup.js [new file with mode: 0644]
index.html [new file with mode: 0644]

diff --git a/dnssec_prover_wasm.js b/dnssec_prover_wasm.js
new file mode 100644 (file)
index 0000000..6470494
--- /dev/null
@@ -0,0 +1,364 @@
+let wasm;
+
+const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
+
+if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
+
+let cachedUint8Memory0 = null;
+
+function getUint8Memory0() {
+    if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
+        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+    }
+    return cachedUint8Memory0;
+}
+
+function getStringFromWasm0(ptr, len) {
+    ptr = ptr >>> 0;
+    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+
+const heap = new Array(128).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+let heap_next = heap.length;
+
+function addHeapObject(obj) {
+    if (heap_next === heap.length) heap.push(heap.length + 1);
+    const idx = heap_next;
+    heap_next = heap[idx];
+
+    heap[idx] = obj;
+    return idx;
+}
+
+let WASM_VECTOR_LEN = 0;
+
+const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+    ? function (arg, view) {
+    return cachedTextEncoder.encodeInto(arg, view);
+}
+    : function (arg, view) {
+    const buf = cachedTextEncoder.encode(arg);
+    view.set(buf);
+    return {
+        read: arg.length,
+        written: buf.length
+    };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+    if (realloc === undefined) {
+        const buf = cachedTextEncoder.encode(arg);
+        const ptr = malloc(buf.length, 1) >>> 0;
+        getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+        WASM_VECTOR_LEN = buf.length;
+        return ptr;
+    }
+
+    let len = arg.length;
+    let ptr = malloc(len, 1) >>> 0;
+
+    const mem = getUint8Memory0();
+
+    let offset = 0;
+
+    for (; offset < len; offset++) {
+        const code = arg.charCodeAt(offset);
+        if (code > 0x7F) break;
+        mem[ptr + offset] = code;
+    }
+
+    if (offset !== len) {
+        if (offset !== 0) {
+            arg = arg.slice(offset);
+        }
+        ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
+        const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+        const ret = encodeString(arg, view);
+
+        offset += ret.written;
+        ptr = realloc(ptr, len, offset, 1) >>> 0;
+    }
+
+    WASM_VECTOR_LEN = offset;
+    return ptr;
+}
+/**
+* Builds a proof builder which can generate a proof for records of the given `ty`pe at the given
+* `name`.
+*
+* After calling this [`get_next_query`] should be called to fetch the initial query.
+* @param {string} name
+* @param {number} ty
+* @returns {WASMProofBuilder | undefined}
+*/
+export function init_proof_builder(name, ty) {
+    const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+    const len0 = WASM_VECTOR_LEN;
+    const ret = wasm.init_proof_builder(ptr0, len0, ty);
+    return ret === 0 ? undefined : WASMProofBuilder.__wrap(ret);
+}
+
+function _assertClass(instance, klass) {
+    if (!(instance instanceof klass)) {
+        throw new Error(`expected instance of ${klass.name}`);
+    }
+    return instance.ptr;
+}
+
+function passArray8ToWasm0(arg, malloc) {
+    const ptr = malloc(arg.length * 1, 1) >>> 0;
+    getUint8Memory0().set(arg, ptr / 1);
+    WASM_VECTOR_LEN = arg.length;
+    return ptr;
+}
+/**
+* Processes a response to a query previously fetched from [`get_next_query`].
+*
+* After calling this, [`get_next_query`] should be called until pending queries are exhausted and
+* no more pending queries exist, at which point [`get_unverified_proof`] should be called.
+* @param {WASMProofBuilder} proof_builder
+* @param {Uint8Array} response
+*/
+export function process_query_response(proof_builder, response) {
+    _assertClass(proof_builder, WASMProofBuilder);
+    const ptr0 = passArray8ToWasm0(response, wasm.__wbindgen_malloc);
+    const len0 = WASM_VECTOR_LEN;
+    wasm.process_query_response(proof_builder.__wbg_ptr, ptr0, len0);
+}
+
+let cachedInt32Memory0 = null;
+
+function getInt32Memory0() {
+    if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
+        cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+    }
+    return cachedInt32Memory0;
+}
+
+function getArrayU8FromWasm0(ptr, len) {
+    ptr = ptr >>> 0;
+    return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
+}
+/**
+* Gets the next query (if any) that should be sent to the resolver for the given proof builder.
+*
+* Once the resolver responds [`process_query_response`] should be called with the response.
+* @param {WASMProofBuilder} proof_builder
+* @returns {Uint8Array | undefined}
+*/
+export function get_next_query(proof_builder) {
+    try {
+        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+        _assertClass(proof_builder, WASMProofBuilder);
+        wasm.get_next_query(retptr, proof_builder.__wbg_ptr);
+        var r0 = getInt32Memory0()[retptr / 4 + 0];
+        var r1 = getInt32Memory0()[retptr / 4 + 1];
+        let v1;
+        if (r0 !== 0) {
+            v1 = getArrayU8FromWasm0(r0, r1).slice();
+            wasm.__wbindgen_free(r0, r1 * 1, 1);
+        }
+        return v1;
+    } finally {
+        wasm.__wbindgen_add_to_stack_pointer(16);
+    }
+}
+
+function getObject(idx) { return heap[idx]; }
+
+function dropObject(idx) {
+    if (idx < 132) return;
+    heap[idx] = heap_next;
+    heap_next = idx;
+}
+
+function takeObject(idx) {
+    const ret = getObject(idx);
+    dropObject(idx);
+    return ret;
+}
+/**
+* Gets the final, unverified, proof once all queries fetched via [`get_next_query`] have
+* completed and their responses passed to [`process_query_response`].
+* @param {WASMProofBuilder} proof_builder
+* @returns {Uint8Array}
+*/
+export function get_unverified_proof(proof_builder) {
+    try {
+        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+        _assertClass(proof_builder, WASMProofBuilder);
+        var ptr0 = proof_builder.__destroy_into_raw();
+        wasm.get_unverified_proof(retptr, ptr0);
+        var r0 = getInt32Memory0()[retptr / 4 + 0];
+        var r1 = getInt32Memory0()[retptr / 4 + 1];
+        var r2 = getInt32Memory0()[retptr / 4 + 2];
+        var r3 = getInt32Memory0()[retptr / 4 + 3];
+        if (r3) {
+            throw takeObject(r2);
+        }
+        var v2 = getArrayU8FromWasm0(r0, r1).slice();
+        wasm.__wbindgen_free(r0, r1 * 1, 1);
+        return v2;
+    } finally {
+        wasm.__wbindgen_add_to_stack_pointer(16);
+    }
+}
+
+/**
+* Verifies an RFC 9102-formatted proof and returns verified records matching the given name
+* (resolving any C/DNAMEs as required).
+* @param {Uint8Array} stream
+* @param {string} name_to_resolve
+* @returns {string}
+*/
+export function verify_byte_stream(stream, name_to_resolve) {
+    let deferred3_0;
+    let deferred3_1;
+    try {
+        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+        const ptr0 = passArray8ToWasm0(stream, wasm.__wbindgen_malloc);
+        const len0 = WASM_VECTOR_LEN;
+        const ptr1 = passStringToWasm0(name_to_resolve, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+        const len1 = WASM_VECTOR_LEN;
+        wasm.verify_byte_stream(retptr, ptr0, len0, ptr1, len1);
+        var r0 = getInt32Memory0()[retptr / 4 + 0];
+        var r1 = getInt32Memory0()[retptr / 4 + 1];
+        deferred3_0 = r0;
+        deferred3_1 = r1;
+        return getStringFromWasm0(r0, r1);
+    } finally {
+        wasm.__wbindgen_add_to_stack_pointer(16);
+        wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
+    }
+}
+
+const WASMProofBuilderFinalization = (typeof FinalizationRegistry === 'undefined')
+    ? { register: () => {}, unregister: () => {} }
+    : new FinalizationRegistry(ptr => wasm.__wbg_wasmproofbuilder_free(ptr >>> 0));
+/**
+*/
+export class WASMProofBuilder {
+
+    static __wrap(ptr) {
+        ptr = ptr >>> 0;
+        const obj = Object.create(WASMProofBuilder.prototype);
+        obj.__wbg_ptr = ptr;
+        WASMProofBuilderFinalization.register(obj, obj.__wbg_ptr, obj);
+        return obj;
+    }
+
+    __destroy_into_raw() {
+        const ptr = this.__wbg_ptr;
+        this.__wbg_ptr = 0;
+        WASMProofBuilderFinalization.unregister(this);
+        return ptr;
+    }
+
+    free() {
+        const ptr = this.__destroy_into_raw();
+        wasm.__wbg_wasmproofbuilder_free(ptr);
+    }
+}
+
+async function __wbg_load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+function __wbg_get_imports() {
+    const imports = {};
+    imports.wbg = {};
+    imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
+        const ret = getStringFromWasm0(arg0, arg1);
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbindgen_throw = function(arg0, arg1) {
+        throw new Error(getStringFromWasm0(arg0, arg1));
+    };
+
+    return imports;
+}
+
+function __wbg_init_memory(imports, maybe_memory) {
+
+}
+
+function __wbg_finalize_init(instance, module) {
+    wasm = instance.exports;
+    __wbg_init.__wbindgen_wasm_module = module;
+    cachedInt32Memory0 = null;
+    cachedUint8Memory0 = null;
+
+
+    return wasm;
+}
+
+function initSync(module) {
+    if (wasm !== undefined) return wasm;
+
+    const imports = __wbg_get_imports();
+
+    __wbg_init_memory(imports);
+
+    if (!(module instanceof WebAssembly.Module)) {
+        module = new WebAssembly.Module(module);
+    }
+
+    const instance = new WebAssembly.Instance(module, imports);
+
+    return __wbg_finalize_init(instance, module);
+}
+
+async function __wbg_init(input) {
+    if (wasm !== undefined) return wasm;
+
+    if (typeof input === 'undefined') {
+        input = new URL('dnssec_prover_wasm_bg.wasm', import.meta.url);
+    }
+    const imports = __wbg_get_imports();
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+    __wbg_init_memory(imports);
+
+    const { instance, module } = await __wbg_load(await input, imports);
+
+    return __wbg_finalize_init(instance, module);
+}
+
+export { initSync }
+export default __wbg_init;
diff --git a/dnssec_prover_wasm_bg.wasm b/dnssec_prover_wasm_bg.wasm
new file mode 100644 (file)
index 0000000..9cd4df3
Binary files /dev/null and b/dnssec_prover_wasm_bg.wasm differ
diff --git a/doh_lookup.js b/doh_lookup.js
new file mode 100644 (file)
index 0000000..f35a059
--- /dev/null
@@ -0,0 +1,61 @@
+import init from './dnssec_prover_wasm.js';
+import * as wasm from './dnssec_prover_wasm.js';
+
+/**
+* Asynchronously resolves a given domain and type using the provided DoH endpoint, then verifies
+* the returned DNSSEC data and ultimately returns a JSON-encoded list of validated records.
+*/
+export async function lookup_doh(domain, ty, doh_endpoint) {
+       await init();
+
+       if (!domain.endsWith(".")) domain += ".";
+       if (ty.toLowerCase() == "txt") {
+               ty = 16;
+       } else if (ty.toLowerCase() == "tlsa") {
+               ty = 52;
+       } else if (ty.toLowerCase() == "a") {
+               ty = 1;
+       } else if (ty.toLowerCase() == "aaaa") {
+               ty = 28;
+       }
+       if (typeof(ty) == "number") {
+               var builder = wasm.init_proof_builder(domain, ty);
+               if (builder == null) {
+                       return "{\"error\":\"Bad domain\"}";
+               } else {
+                       var queries_pending = 0;
+                       var send_next_query;
+                       send_next_query = async function() {
+                               var query = wasm.get_next_query(builder);
+                               if (query != null) {
+                                       queries_pending += 1;
+                                       var b64 = btoa(String.fromCodePoint(...query));
+                                       var b64url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
+                                       try {
+                                               var resp = await fetch(doh_endpoint + "?dns=" + b64url,
+                                                       {headers: {"accept": "application/dns-message"}});
+                                               if (!resp.ok) { throw "Query returned HTTP " + resp.status; }
+                                               var array = await resp.arrayBuffer();
+                                               var buf = new Uint8Array(array);
+                                               wasm.process_query_response(builder, buf);
+                                               queries_pending -= 1;
+                                       } catch (e) {
+                                               return "{\"error\":\"DoH Query failed: " + e + "\"}";
+                                       }
+                                       return await send_next_query();
+                               } else if (queries_pending == 0) {
+                                       var proof = wasm.get_unverified_proof(builder);
+                                       if (proof != null) {
+                                               var result = wasm.verify_byte_stream(proof, domain);
+                                               return JSON.parse(result);
+                                       } else {
+                                               return "{\"error\":\"Failed to build proof\"}";
+                                       }
+                               }
+                       }
+                       return await send_next_query();
+               }
+       } else {
+               return "{\"error\":\"Unsupported Type\"}";
+       }
+}
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..9674a71
--- /dev/null
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html lang="en">
+       <head>
+               <meta charset="utf-8">
+               <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
+               <style type="text/css">
+                       body {
+                               margin:40px auto;
+                               max-width:660px;
+                               line-height:1.6;
+                               font-size:14pt;
+                               color:#444;
+                               padding:0 10px;
+                       }
+                       h1, h2, h3 {
+                               line-height: 1.2;
+                       }
+                       .fill {
+                               display: flex;
+                       }
+                       .fill-use {
+                               flex: 1;
+                       }
+                       .small-print, .errors {
+                               font-size:11pt;
+                       }
+                       .button, .errors {
+                               width: fit-content;
+                               margin-left: auto;
+                               margin-right: auto;
+                               display: block;
+                       }
+               </style>
+               <title>Provable DNS Querying</title>
+       </head>
+       <body>
+               <h2>
+                       BIP 353 Human Readable Names Resolver
+               </h2>
+               <p>
+                       <a href="https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki">BIP 353</a> defines the way to encode simple human-readable names and map them to Bitcoin payment intructions.
+               <p>
+                       If your wallet doesn't yet resolve BIP 353 names natively, this site will resolve them for you, letting you pay human readable names seamlessly.
+               </p>
+               <p>
+                       <form onsubmit="return false;" method="post">
+                       <div class="fill">
+                               ₿
+                               
+                               <input class="fill-use" type="text" id="address" value="matt@satsto.me"/><br>
+                       </div>
+                       <div>
+                               Resolve name using&nbsp;<select id="server">
+                                       <option value="https://dns.google/dns-query">Google's 8.8.8.8 Public DNS server</option>
+                                       <option value="http">satsto.me's native DNS proof server</option>
+                                       <option value="https://1.1.1.1/dns-query">Cloudflare's 1.1.1.1 Public DNS server</option>
+                               </select>
+                       </div>
+                       <div class="errors" id="result"></div>
+                       <input type="submit" class="button" onclick="lookup_domain()" id="paybutton" value="Pay" />
+               </p>
+               <p></p>
+               <p class="small-print">Note that most BIP 353 addresses rely on at least <a href="https://bolt12.org">BOLT 12</a> or <a href="https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki">Silent Payments</a> and as both are relatively new, wallet support isn't yet universal. Check that your wallet supports at least one of the two if you are unable to pay.</p>
+               <p class="small-print">While you're absolutely trusting this site to not provide you with bad code, the code we promise we served you fully validates the name using DNSSEC. Thus, no matter what server you use to resolve the name, the worst they can do is log who you're paying or tell you they're not payable. They can never lie and give you the wrong address!</p>
+
+               <!-- dnssec_prover_wasm.js comes from running wasm-pack build --target web` in the `wasmpack` folder in dnssec-prover -->
+               <script type="module" src="dnssec_prover_wasm.js"></script>
+               <!-- doh_lookup.js comes from the `wasmpack` folder in the above git repo -->
+               <script type="module" src="doh_lookup.js"></script>
+               <script type="module">
+                       import init, {verify_byte_stream} from './dnssec_prover_wasm.js';
+                       import * as doh from './doh_lookup.js';
+                       init().then(() => {
+                               const address_box = document.getElementById("address");
+                               const check_text = function() {
+                                       if (address_box.value.startsWith("₿")) {
+                                               address_box.value = address_box.value.substring(1);
+                                       }
+                                       const addr_parts = address_box.value.split("@");
+                                       if (addr_parts.length != 2) {
+                                               document.getElementById("paybutton").disabled = true;
+                                               document.getElementById("result").innerHTML = "Address should have exactly one @";
+                                               return true;
+                                       }
+                                       if (addr_parts[0].length == 0) {
+                                               document.getElementById("paybutton").disabled = true;
+                                               document.getElementById("result").innerHTML = "Missing user part";
+                                               return true;
+                                       }
+                                       if (addr_parts[1].length == 0) {
+                                               document.getElementById("paybutton").disabled = true;
+                                               document.getElementById("result").innerHTML = "Missing domain";
+                                               return true;
+                                       }
+                                       if (!/^[\p{ASCII}]*$/u.test(addr_parts[0])) {
+                                               document.getElementById("paybutton").disabled = true;
+                                               document.getElementById("result").innerHTML = "User part of addres must be ASCII";
+                                               return true;
+                                       }
+                                       if (!/^[\p{ASCII}]*$/u.test(addr_parts[1])) {
+                                               document.getElementById("paybutton").disabled = true;
+                                               document.getElementById("result").innerHTML = "Domain part of address must be ASCII";
+                                               return true;
+                                       }
+                                       document.getElementById("paybutton").disabled = false;
+                                       document.getElementById("result").innerHTML = "";
+                                       return true;
+                               }
+                               document.getElementById("address").onchange = check_text;
+                               document.getElementById("address").onkeypress = check_text;
+                               document.getElementById("address").oninput = check_text;
+                               window.lookup_domain = function() {
+                                       const addr_parts = address_box.value.split("@");
+                                       var dom = addr_parts[0] + ".user._bitcoin-payment." + addr_parts[1];
+                                       if (!dom.endsWith(".")) dom += ".";
+                                       var source = document.getElementById("server").value;
+                                       if (source == "http") {
+                                               lookup_http(address_box.value, dom);
+                                       } else {
+                                               lookup_doh(address_box.value, dom, source);
+                                       }
+                               }
+                               window.lookup_http = function(addr, dom) {
+                                       var request = "https://http-dns-prover.as397444.net/dnssecproof?d=" + dom + "&t=txt";
+                                       fetch(request).then((resp) => {
+                                               resp.arrayBuffer().then((array) => {
+                                                       var buf = new Uint8Array(array);
+                                                       var result = verify_byte_stream(buf, dom);
+                                                       handle_result(addr, JSON.parse(result));
+                                               }, () => {
+                                                       document.getElementById("result").innerHTML = "Failed to read proof from server";
+                                               })
+                                       }, (e) => {
+                                               document.getElementById("result").innerHTML = "Failed to fetch proof: " + e;
+                                       });
+                               }
+                               window.lookup_doh = function(addr, dom, doh_endpoint) {
+                                       doh.lookup_doh(dom, "txt", doh_endpoint).then((res) => {
+                                               handle_result(addr, res);
+                                       }, (e) => {
+                                               document.getElementById("result").innerHTML = "Failed to fetch proof: " + e;
+                                       });
+                               }
+                               window.handle_result = function(name, result) {
+                                       if (!result.hasOwnProperty("valid_from") || !result.hasOwnProperty("expires") || !result.hasOwnProperty("verified_rrs")) {
+                                               document.getElementById("result").innerHTML = "Failed to fetch valid proof";
+                                               return;
+                                       }
+                                       if (Date.now() / 1000 < result.valid_from) {
+                                               document.getElementById("result").innerHTML = "Proof is not yet valid (check your system clock)";
+                                               return;
+                                       }
+                                       if (Date.now() / 1000 > result.expires) {
+                                               document.getElementById("result").innerHTML = "Proof has expired (check your system clock?)";
+                                               return;
+                                       }
+                                       var bip353 = null;
+                                       for (const rr of result.verified_rrs) {
+                                               if (rr.type != "txt") {
+                                                       document.getElementById("result").innerHTML = "Proof was invalid";
+                                                       return;
+                                               }
+                                               if (typeof rr.contents === "string" && rr.contents.toLowerCase().startsWith("bitcoin:")) {
+                                                       if (bip353 !== null) {
+                                                               document.getElementById("result").innerHTML = "Address is BIP 353-invalid - it contains multiple results";
+                                                               return;
+                                                       }
+                                                       bip353 = rr.contents;
+                                               }
+                                       }
+                                       if (bip353 === null) {
+                                               document.getElementById("result").innerHTML = "Address is BIP 353-invalid - it contains no bitcoin: URI";
+                                               return;
+                                       }
+                                       document.getElementById("result").innerHTML = "<a href=\"" + bip353 + "\">Opening your bitcoin wallet to pay " + name + "! If it doesn't work, click here.</a>";
+                                       window.location = bip353;
+                               }
+                       });
+               </script>
+               </script>
+       </body>
+</html>