Skip to content

Defang / Refang — Ruby source

Make URLs, IPs, and emails safe to paste by defanging indicators (https:// → hxxps[://], . → [.], @ → [@]) - and reverse refanged text back to its original form. Runs entirely in your browser, with a shareable link to your exact input and mode.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# frozen_string_literal: true

# defang-refang — make URLs, IPs, and emails safe to paste (and reverse it).
#
# Language: Ruby (3.2, standard library only)
# Source:   CosmoDev polyglot showcase port of the Defang/Refang tool, ported
#           from src/lib/defang.ts (the canonical TypeScript implementation)
#           and held in lock-step with cli/defang-refang/defang-refang.go.
# License:  display source — part of CosmoDev's polyglot tool pages.
#
# Design goals:
#   - Pure + deterministic; never raises.
#   - Functionally equivalent to the TS / Go reference: same inputs -> same outputs.
#   - Self-contained: String#gsub with a String pattern is a literal
#     (non-regex) replace-all — the direct equivalent of the TS replaceAll.
#
# Algorithm: DefangRefang.defang replaces "https://" / "http://" schemes with
# "hxxps[://]" / "hxxp[://]", then brackets EVERY dot as "[.]" and every
# at-sign as "[@]" — the standard threat-intel indicator-defanging convention.
# DefangRefang.refang is its exact inverse: scheme markers are restored first,
# then "[.]" / "[@]" are unbracketed, so refang(defang(x)) == x for typical
# URLs, IPs, emails, and plain text.
#
# Ordering note: scheme replacements run BEFORE dot/at bracketing so the dots
# and colon inside the resulting "[://]" marker are not themselves re-bracketed
# (which would corrupt the scheme marker). Note that EVERY dot is bracketed,
# not just the domain separator — so defang is NOT idempotent by design: apply
# it once to fresh text.

module DefangRefang
  module_function

  # Defang text so URLs, IPs, and emails are no longer clickable or auto-linked.
  #
  # Mirrors `defang()` in src/lib/defang.ts and `Defang()` in the Go twin, and
  # must agree with both on every shared vector. Scheme replacements run first
  # so the dots and colon inside the resulting `[://]` marker are not
  # themselves re-bracketed. EVERY dot is bracketed, not just the domain
  # separator. Empty input returns empty; text with no indicators passes
  # through unchanged.
  def defang(text)
    text
      .gsub("https://", "hxxps[://]")
      .gsub("http://", "hxxp[://]")
      .gsub(".", "[.]")
      .gsub("@", "[@]")
  end

  # Reverse defanging — restore `hxxps[://]` / `hxxp[://]` to real schemes and
  # unbracket `[.]` / `[@]`. Mirrors `refang()` in src/lib/defang.ts and
  # `Refang()` in the Go twin: the exact inverse of #defang.
  #
  # Scheme markers are restored before `[.]` / `[@]` are unbracketed, so a
  # marker like `hxxps[://]` reassembles into `https://` rather than being
  # split by an earlier dot/at substitution. Empty input returns empty.
  def refang(text)
    text
      .gsub("hxxps[://]", "https://")
      .gsub("hxxp[://]", "http://")
      .gsub("[.]", ".")
      .gsub("[@]", "@")
  end
end

# ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
if $PROGRAM_NAME == __FILE__
  failures = []

  check = lambda do |got, want, label|
    failures << label unless got == want
  end

  # defangs url, email, and ip
  check.call(DefangRefang.defang("https://example.com"), "hxxps[://]example[.]com", "defang url")
  check.call(DefangRefang.defang("user@example.com"), "user[@]example[.]com", "defang email")
  check.call(DefangRefang.defang("192.168.1.1"), "192[.]168[.]1[.]1", "defang ip")

  # defangs mixed text
  check.call(
    DefangRefang.defang("contact admin@site.co.uk or visit http://site.co.uk"),
    "contact admin[@]site[.]co[.]uk or visit hxxp[://]site[.]co[.]uk",
    "defang mixed"
  )

  # applies scheme replacement before dot bracketing
  got = DefangRefang.defang("https://a.com")
  check.call(got, "hxxps[://]a[.]com", "scheme first")
  failures << "scheme marker not split" if got.include?("https[://]") || got.include?("hxxps://")

  # refangs and round-trips
  check.call(DefangRefang.refang("hxxps[://]example[.]com"), "https://example.com", "refang https")
  check.call(DefangRefang.refang("hxxp[://]single[.]com"), "http://single.com", "refang http")
  # hxxp and hxxps are distinct markers — never confused.
  check.call(DefangRefang.refang("hxxps[://]secure[.]com"), "https://secure.com", "refang hxxps distinct")
  # Round-trip identity for typical URLs, IPs, emails, and plain text.
  [
    "https://sub.example.com/path?q=1&a=b",
    "first.last@sub.example.co.uk",
    "172.16.254.1",
    "just a sentence with no indicators"
  ].each { |x| check.call(DefangRefang.refang(DefangRefang.defang(x)), x, "round trip") }

  # empty and passthrough
  check.call(DefangRefang.defang(""), "", "defang empty")
  check.call(DefangRefang.refang(""), "", "refang empty")
  check.call(DefangRefang.defang("nothing to transform here"), "nothing to transform here", "defang passthrough")
  check.call(DefangRefang.refang("no indicators here"), "no indicators here", "refang passthrough")

  if failures.empty?
    puts "all checks passed"
  else
    warn "failed checks: #{failures.uniq.join(', ')}"
    exit 1
  end
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →