Elixir HTTP client for making requests through the Tor network. Wraps HTTPoison with SOCKS5 proxy support for routing traffic through a local Tor node.
- Elixir 1.14+
- A running Tor node
Add torex to your dependencies in mix.exs:
def deps do
[{:torex, "~> 0.2.0"}]
endbrew install tor
brew services start torsudo apt install tor
sudo systemctl start tordocker run -d -p 9050:9050 dperson/torproxyTor runs on port 9050 by default.
Add to your config/config.exs:
config :torex,
tor_host: ~c"127.0.0.1",
tor_port: 9050,
# Optional: for circuit renewal (new exit node IP)
control_port: 9051,
control_password: "your_password"Note: tor_host uses a charlist (~c"...") as required by the underlying :hackney library.
{:ok, body} = Torex.get("http://example.onion")
case Torex.get("http://check.torproject.org") do
{:ok, body} ->
IO.puts("Response: #{body}")
{:error, {:http_error, status, body}} ->
IO.puts("HTTP #{status}: #{body}")
{:error, %{reason: reason}} ->
IO.puts("Request failed: #{reason}")
endPOST requests automatically encode the body as JSON:
{:ok, response} = Torex.post("http://example.onion/api", %{
username: "user",
password: "secret"
})Torex returns tagged tuples for all responses:
case Torex.get(url) do
{:ok, body} ->
# Success - HTTP 200
process(body)
{:error, {:http_error, status_code, body}} ->
# Non-200 HTTP response
Logger.warning("HTTP #{status_code}: #{body}")
{:error, %{reason: :econnrefused}} ->
# Tor not running or unreachable
Logger.error("Cannot connect to Tor")
{:error, %{reason: :timeout}} ->
# Request timed out
Logger.error("Request timed out")
{:error, error} ->
# Other errors
Logger.error("Request failed: #{inspect(error)}")
endTest that your traffic is routing through Tor:
{:ok, body} = Torex.get("https://check.torproject.org/api/ip")
IO.inspect(Jason.decode!(body))
# => %{"IsTor" => true, "IP" => "..."}For scraping or when you need a fresh exit node IP, use renew_circuit/0:
# Get current IP
{:ok, body} = Torex.get("https://api.ipify.org")
IO.puts("Current IP: #{body}")
# Request new circuit (new exit node)
:ok = Torex.renew_circuit()
# Wait a moment for the new circuit
Process.sleep(1000)
# Verify new IP
{:ok, body} = Torex.get("https://api.ipify.org")
IO.puts("New IP: #{body}")Circuit renewal requires the Tor control port. Add to your torrc:
ControlPort 9051
HashedControlPassword <your_hashed_password>
Generate a hashed password:
tor --hash-password "your_password"defmodule Scraper do
@max_requests_per_ip 10
def scrape(urls) do
urls
|> Enum.chunk_every(@max_requests_per_ip)
|> Enum.flat_map(fn chunk ->
results = Enum.map(chunk, &fetch/1)
# Rotate IP after each batch
Torex.renew_circuit()
Process.sleep(1000)
results
end)
end
defp fetch(url) do
case Torex.get(url) do
{:ok, body} -> {:ok, url, body}
{:error, reason} -> {:error, url, reason}
end
end
endNote: Tor rate-limits circuit renewal to once per 10 seconds. Calling more frequently succeeds but won't change the circuit.
MIT