routerconsole/jsp/js/netdbLookup.js

/**
 * @module netdbLookup
 * @description Compact search form for the /netdb page with
 * dropdown selectors for encryption, signature, transport, and country fields.
 * Strips empty params and nonce from URL on every page load.
 * @author dr|z3d
 * @license AGPLv3 or later
 */

document.addEventListener("DOMContentLoaded", () => {
  // Clean URL — strip empty params and nonce on every page load
  const url = new URL(window.location.href);
  let dirty = false;
  for (const key of Array.from(url.searchParams.keys())) {
    if (url.searchParams.get(key) === "" || key === "nonce") {
      url.searchParams.delete(key);
      dirty = true;
    }
  }
  if (dirty) {
    const cleanUrl = url.origin + url.pathname + (url.searchParams.toString() ? "?" + url.searchParams.toString() : "");
    window.history.replaceState(null, "", cleanUrl);
  }

  const form = document.getElementById("netdbSearchCompact");
  if (!form) return;

  form.querySelector('input[name="nonce"]')?.remove();

  // Dropdown options per field
  const dropdownFields = {
    etype: [
      "ELGAMAL_2048",
      "EC_P256",
      "EC_P384",
      "EC_P521",
      "ECIES_X25519",
      "MLKEM512_X25519",
      "MLKEM768_X25519",
      "MLKEM1024_X25519",
      "MLKEM512_X25519_INT",
      "MLKEM768_X25519_INT",
      "MLKEM1024_X25519_INT",
      "MLKEM512_X25519_CT",
      "MLKEM768_X25519_CT",
      "MLKEM1024_X25519_CT"
    ],
    type: [
      "DSA_SHA1",
      "ECDSA_SHA256_P256",
      "ECDSA_SHA384_P384",
      "ECDSA_SHA512_P521",
      "RSA_SHA256_2048",
      "RSA_SHA384_3072",
      "RSA_SHA512_4096",
      "EdDSA_SHA512_Ed25519",
      "EdDSA_SHA512_Ed25519ph",
      "RedDSA_SHA512_Ed25519"
    ],
    tr: [
      "NTCP2",
      "SSU",
      "SSU2"
    ],
    c: [
      ["af", "Afghanistan"],
      ["ax", "Åland Islands"],
      ["al", "Albania"],
      ["dz", "Algeria"],
      ["as", "American Samoa"],
      ["ad", "Andorra"],
      ["ao", "Angola"],
      ["ai", "Anguilla"],
      ["a1", "Anonymous Proxy"],
      ["aq", "Antarctica"],
      ["ag", "Antigua and Barbuda"],
      ["ar", "Argentina"],
      ["am", "Armenia"],
      ["aw", "Aruba"],
      ["ap", "Asia/Pacific Region"],
      ["au", "Australia"],
      ["at", "Austria"],
      ["az", "Azerbaijan"],
      ["bs", "Bahamas"],
      ["bh", "Bahrain"],
      ["bd", "Bangladesh"],
      ["bb", "Barbados"],
      ["by", "Belarus"],
      ["be", "Belgium"],
      ["bz", "Belize"],
      ["bj", "Benin"],
      ["bm", "Bermuda"],
      ["bt", "Bhutan"],
      ["bo", "Bolivia"],
      ["bq", "Bonaire"],
      ["ba", "Bosnia and Herzegovina"],
      ["bw", "Botswana"],
      ["bv", "Bouvet Island"],
      ["br", "Brazil"],
      ["io", "British Indian Ocean Territory"],
      ["bn", "Brunei Darussalam"],
      ["bg", "Bulgaria"],
      ["bf", "Burkina Faso"],
      ["bi", "Burundi"],
      ["kh", "Cambodia"],
      ["cm", "Cameroon"],
      ["ca", "Canada"],
      ["cv", "Cape Verde"],
      ["ky", "Cayman Islands"],
      ["cf", "Central African Republic"],
      ["td", "Chad"],
      ["cl", "Chile"],
      ["cn", "China"],
      ["cx", "Christmas Island"],
      ["cc", "Cocos (Keeling) Islands"],
      ["co", "Colombia"],
      ["km", "Comoros"],
      ["cg", "Congo"],
      ["ck", "Cook Islands"],
      ["cr", "Costa Rica"],
      ["ci", "Cote D'Ivoire"],
      ["hr", "Croatia"],
      ["cu", "Cuba"],
      ["cw", "Curaçao"],
      ["cy", "Cyprus"],
      ["cz", "Czech Republic"],
      ["dk", "Denmark"],
      ["dj", "Djibouti"],
      ["dm", "Dominica"],
      ["do", "Dominican Republic"],
      ["ec", "Ecuador"],
      ["eg", "Egypt"],
      ["sv", "El Salvador"],
      ["gq", "Equatorial Guinea"],
      ["er", "Eritrea"],
      ["ee", "Estonia"],
      ["et", "Ethiopia"],
      ["eu", "European Union"],
      ["fk", "Falkland Islands (Malvinas)"],
      ["fo", "Faroe Islands"],
      ["fj", "Fiji"],
      ["fi", "Finland"],
      ["fr", "France"],
      ["gf", "French Guiana"],
      ["pf", "French Polynesia"],
      ["tf", "French Southern Territories"],
      ["ga", "Gabon"],
      ["gm", "Gambia"],
      ["ge", "Georgia"],
      ["de", "Germany"],
      ["gh", "Ghana"],
      ["gi", "Gibraltar"],
      ["gr", "Greece"],
      ["gl", "Greenland"],
      ["gd", "Grenada"],
      ["gp", "Guadeloupe"],
      ["gu", "Guam"],
      ["gt", "Guatemala"],
      ["gg", "Guernsey"],
      ["gn", "Guinea"],
      ["gw", "Guinea-Bissau"],
      ["gy", "Guyana"],
      ["ht", "Haiti"],
      ["hn", "Honduras"],
      ["hk", "Hong Kong"],
      ["hu", "Hungary"],
      ["is", "Iceland"],
      ["in", "India"],
      ["id", "Indonesia"],
      ["ir", "Iran"],
      ["iq", "Iraq"],
      ["ie", "Ireland"],
      ["im", "Isle of Man"],
      ["il", "Israel"],
      ["it", "Italy"],
      ["jm", "Jamaica"],
      ["jp", "Japan"],
      ["je", "Jersey"],
      ["jo", "Jordan"],
      ["kz", "Kazakhstan"],
      ["ke", "Kenya"],
      ["ki", "Kiribati"],
      ["xk", "Kosovo"],
      ["kw", "Kuwait"],
      ["kg", "Kyrgyzstan"],
      ["la", "Lao People's Democratic Republic"],
      ["lv", "Latvia"],
      ["lb", "Lebanon"],
      ["ls", "Lesotho"],
      ["lr", "Liberia"],
      ["ly", "Libya"],
      ["li", "Liechtenstein"],
      ["lt", "Lithuania"],
      ["lu", "Luxembourg"],
      ["mo", "Macau"],
      ["mk", "Macedonia"],
      ["mg", "Madagascar"],
      ["mw", "Malawi"],
      ["my", "Malaysia"],
      ["mv", "Maldives"],
      ["ml", "Mali"],
      ["mt", "Malta"],
      ["mh", "Marshall Islands"],
      ["mq", "Martinique"],
      ["mr", "Mauritania"],
      ["mu", "Mauritius"],
      ["yt", "Mayotte"],
      ["mx", "Mexico"],
      ["fm", "Micronesia"],
      ["md", "Moldova"],
      ["mc", "Monaco"],
      ["mn", "Mongolia"],
      ["me", "Montenegro"],
      ["ms", "Montserrat"],
      ["ma", "Morocco"],
      ["mz", "Mozambique"],
      ["mm", "Myanmar"],
      ["na", "Namibia"],
      ["nr", "Nauru"],
      ["np", "Nepal"],
      ["nl", "Netherlands"],
      ["an", "Netherlands Antilles"],
      ["nc", "New Caledonia"],
      ["nz", "New Zealand"],
      ["ni", "Nicaragua"],
      ["ne", "Niger"],
      ["ng", "Nigeria"],
      ["nu", "Niue"],
      ["nf", "Norfolk Island"],
      ["mp", "Northern Mariana Islands"],
      ["kp", "North Korea"],
      ["no", "Norway"],
      ["om", "Oman"],
      ["pk", "Pakistan"],
      ["pw", "Palau"],
      ["ps", "Palestinian Territory"],
      ["pa", "Panama"],
      ["pg", "Papua New Guinea"],
      ["py", "Paraguay"],
      ["pe", "Peru"],
      ["ph", "Philippines"],
      ["pn", "Pitcairn Islands"],
      ["pl", "Poland"],
      ["pt", "Portugal"],
      ["pr", "Puerto Rico"],
      ["qa", "Qatar"],
      ["kr", "Republic of Korea"],
      ["re", "Réunion"],
      ["ro", "Romania"],
      ["ru", "Russian Federation"],
      ["rw", "Rwanda"],
      ["bl", "Saint Barthélemy"],
      ["sh", "Saint Helena"],
      ["kn", "Saint Kitts and Nevis"],
      ["lc", "Saint Lucia"],
      ["mf", "Saint Martin"],
      ["pm", "Saint Pierre and Miquelon"],
      ["vc", "Saint Vincent and the Grenadines"],
      ["ws", "Samoa"],
      ["sm", "San Marino"],
      ["st", "Sao Tome and Principe"],
      ["a2", "Satellite Provider"],
      ["sa", "Saudi Arabia"],
      ["sn", "Senegal"],
      ["rs", "Serbia"],
      ["cs", "Serbia and Montenegro"],
      ["sc", "Seychelles"],
      ["sl", "Sierra Leone"],
      ["sg", "Singapore"],
      ["sx", "Sint Maarten"],
      ["sk", "Slovakia"],
      ["si", "Slovenia"],
      ["sb", "Solomon Islands"],
      ["so", "Somalia"],
      ["za", "South Africa"],
      ["gs", "South Georgia and the South Sandwich Islands"],
      ["ss", "South Sudan"],
      ["es", "Spain"],
      ["lk", "Sri Lanka"],
      ["sd", "Sudan"],
      ["sr", "Suriname"],
      ["sj", "Svalbard and Jan Mayen"],
      ["sz", "Swaziland"],
      ["se", "Sweden"],
      ["ch", "Switzerland"],
      ["sy", "Syria"],
      ["tw", "Taiwan"],
      ["tj", "Tajikistan"],
      ["tz", "Tanzania"],
      ["th", "Thailand"],
      ["tl", "Timor-Leste"],
      ["tg", "Togo"],
      ["tk", "Tokelau"],
      ["to", "Tonga"],
      ["tt", "Trinidad and Tobago"],
      ["tn", "Tunisia"],
      ["tr", "Turkey"],
      ["tm", "Turkmenistan"],
      ["tc", "Turks and Caicos Islands"],
      ["tv", "Tuvalu"],
      ["ug", "Uganda"],
      ["ua", "Ukraine"],
      ["ae", "United Arab Emirates"],
      ["gb", "United Kingdom"],
      ["us", "United States"],
      ["um", "United States Minor Outlying Islands"],
      ["xx", "Unknown"],
      ["zz", "Unknown"],
      ["uy", "Uruguay"],
      ["uz", "Uzbekistan"],
      ["vu", "Vanuatu"],
      ["va", "Vatican"],
      ["ve", "Venezuela"],
      ["vn", "Vietnam"],
      ["vi", "Virgin Islands"],
      ["wf", "Wallis and Futuna"],
      ["eh", "Western Sahara"],
      ["ye", "Yemen"],
      ["zm", "Zambia"],
      ["zw", "Zimbabwe"]
    ]
  };

  const fieldSelect = form.querySelector('select[name="field"]');
  const queryCell = form.querySelector('input[name="query"]')?.closest('td');
  if (!fieldSelect || !queryCell) return;

  if (fieldSelect) {
    const options = Array.from(fieldSelect.options);
    options.sort((a, b) => {
      return a.text.localeCompare(b.text);
    });
    fieldSelect.innerHTML = "";
    options.forEach(opt => fieldSelect.appendChild(opt));
  }

  let currentQueryElement = form.querySelector('input[name="query"]');

  /**
   * Populated <select> for the given dropdown field.
   * @param   {string}  field - Key into dropdownFields (etype, type, tr, c)
   * @param   {string}  [value=""] - Pre-selected value
   */
  function createSelectForField(field, value = "") {
    const select = document.createElement("select");
    select.name = "query";
    const labels = {etype: "encryption", type: "signature", tr: "transport", c: "country"};
    select.title = `Select ${labels[field] || field}`;

    const options = dropdownFields[field] || [];

    options.forEach(opt => {
      const option = document.createElement("option");
      if (Array.isArray(opt)) {
        option.value = opt[0];
        option.textContent = `${opt[1]} (${opt[0].toUpperCase()})`;
      } else {
        option.value = opt;
        option.textContent = opt;
      }
      if (option.value === value) { option.selected = true; }
      select.appendChild(option);
    });

    return select;
  }

  /**
   * Text input for free-form query entry.
   * @param   {string} [value=""] - Initial input value
   */
  function createTextInput(value = "") {
    const input = document.createElement("input");
    input.type = "text";
    input.name = "query";
    input.title = "Enter search value here";
    input.value = value;
    return input;
  }

  let lastField = fieldSelect.value;
  /**
   * Swap query input between text and <select> depending on the field type.
   */
  function switchQueryField() {
    const newField = fieldSelect.value;
    // Only clear if field actually changed
    const shouldClear = newField !== lastField;
    lastField = newField;

    if (currentQueryElement) {
      currentQueryElement.remove();
    }

    if (newField in dropdownFields) {
      // For dropdowns, only preserve value if it's valid AND field didn't change
      const preservedValue = shouldClear ? "" : currentQueryElement?.value || "";
      currentQueryElement = createSelectForField(newField, preservedValue);
    } else {
      // Always clear text input on field change
      currentQueryElement = createTextInput(shouldClear ? "" : currentQueryElement?.value || "");
    }

    queryCell.appendChild(currentQueryElement);
  }

  fieldSelect.addEventListener("change", switchQueryField);

  // Form submission
  form.addEventListener("submit", event => {
    event.preventDefault();
    const field = fieldSelect.value;
    const query = currentQueryElement ? currentQueryElement.value.trim() : "";

    const url = new URL(form.action, window.location.href);
    url.search = "";
    if (field && query) {
      url.searchParams.set(field, query);
    }
    url.searchParams.delete("nonce");
    window.location.href = url.toString();
  });

  // Populate from URL — must run AFTER initial DOM setup
  const urlParams = new URLSearchParams(window.location.search);
  const possibleFields = [
    "caps", "cost", "c", "cc", "r", "ip", "ls", "fam",
    "ipv6", "mtu", "port", "ssucaps", "v",
    "type", "etype", "tr",
    "sybil2", "sybil"
  ];

  if (fieldSelect && !urlParams.toString()) {
      fieldSelect.value = "caps";
      switchQueryField();
  }

  let found = false;
  for (const key of possibleFields) {
    if (urlParams.has(key)) {
      const val = urlParams.get(key);
      if (val) {
        fieldSelect.value = key;
        lastField = key; // sync lastField to avoid premature clear
        setTimeout(() => {
          const isDropdown = key in dropdownFields;
          if (isDropdown) {
            switchQueryField(); // this will use preservedValue = "" but we override below
            if (currentQueryElement?.tagName === "SELECT") {
              const valid = Array.from(currentQueryElement.options).some(opt => opt.value === val);
              if (valid) {
                currentQueryElement.value = val;
              }
              // If invalid, leave as first option (or blank if empty)
            }
          } else {
            switchQueryField(); // clears if field changed, but we just loaded
            if (currentQueryElement?.tagName === "INPUT") {
              currentQueryElement.value = val;
            }
          }
        }, 0);
        found = true;
        break;
      }
    }
  }

  // If no param, ensure initial state is clean
  if (!found) {
    // Keep initial input, but ensure lastField is synced
    lastField = fieldSelect.value;
  }
});