# Torex
[](https://dl.circleci.com/status-badge/redirect/gh/alexfilatov/torex/tree/master)
[](https://hex.pm/packages/torex)
[](https://hexdocs.pm/torex)
[](https://hex.pm/packages/torex)
[](https://opensource.org/licenses/MIT)
Elixir HTTP client for making requests through the Tor network. Wraps HTTPoison with SOCKS5 proxy support for routing traffic through a local Tor node.
## Requirements
- Elixir 1.14+
- A running Tor node
## Installation
Add `torex` to your dependencies in `mix.exs`:
```elixir
def deps do
[{:torex, "~> 0.2.0"}]
end
```
## Tor Setup
### macOS
```bash
brew install tor
brew services start tor
```
### Linux (Debian/Ubuntu)
```bash
sudo apt install tor
sudo systemctl start tor
```
### Docker
```bash
docker run -d -p 9050:9050 dperson/torproxy
```
Tor runs on port 9050 by default.
## Configuration
Add to your `config/config.exs`:
```elixir
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.
## Usage
### GET requests
```elixir
{: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}")
end
```
### POST requests
POST requests automatically encode the body as JSON:
```elixir
{:ok, response} = Torex.post("http://example.onion/api", %{
username: "user",
password: "secret"
})
```
### Error Handling
Torex returns tagged tuples for all responses:
```elixir
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)}")
end
```
## Verifying Tor Connection
Test that your traffic is routing through Tor:
```elixir
{:ok, body} = Torex.get("https://check.torproject.org/api/ip")
IO.inspect(Jason.decode!(body))
# => %{"IsTor" => true, "IP" => "..."}
```
## Circuit Renewal (IP Rotation)
For scraping or when you need a fresh exit node IP, use `renew_circuit/0`:
```elixir
# 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}")
```
### Tor Control Port Setup
Circuit renewal requires the Tor control port. Add to your `torrc`:
```text
ControlPort 9051
HashedControlPassword <your_hashed_password>
```
Generate a hashed password:
```bash
tor --hash-password "your_password"
```
### Scraping Example with IP Rotation
```elixir
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
end
```
**Note:** Tor rate-limits circuit renewal to once per 10 seconds. Calling more frequently succeeds but won't change the circuit.
## License
MIT