Creating a Hidden Service with Phoenix and Elixir on FreeBSD
This post is a synthesis of a couple of prior posts.
It covers configuration and deployment of simple Phoenix application as a tor hidden service. Important topics like Phoenix auth, and properly securing a hidden service are not covered.
security/tor needs to be installed.
portmaster security/tor
Phoenix, Elixir and PostgreSQL also need to be installed.
Software Versions
$ date -u "+%Y-%m-%d %H:%M:%S +0000"
2016-03-22 06:45:48 +0000
$ uname -vm
FreeBSD 11.0-CURRENT #0 r287598: Thu Sep 10 14:45:48 JST 2015 root@:/usr/obj/usr/src/sys/MIRAGE_KERNEL amd64
$ tor --version
Tor version 0.2.7.6.
$ curl --version
curl 7.47.0 (amd64-portbld-freebsd11.0) libcurl/7.47.0 OpenSSL/1.0.2e zlib/1.2.8
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP UnixSockets
$ mix hex.info
Hex: 0.11.3
Elixir: 1.2.3
OTP: 18.2.4
* snip *
$ mix phoenix.new -v
Phoenix v1.1.4
Creating a Sample Project
Create a new Phoenix project.
mix phoenix.new phoenix_service --no-brunch
cd phoenix_service
mix ecto.create
The sample project will be a simple memo JSON API service. A real service will almost certainly need some sort of authentication, but that is not covered in this post.
mix phoenix.gen.json Memo memos title:string body:string
Revise the web/router.ex file. The “/api” scope needs to be uncommented and the “/memos” route needs to be added.
web/router.ex
defmodule PhoenixService.Router do
use PhoenixService.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", PhoenixService do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
scope "/api", PhoenixService do
pipe_through :api
resources "/memos", MemoController, except: [:new, :edit]
end
end
Run the migration.
mix ecto.migrate
Configure the app to only serve localhost (127.0.0.1) in config/prod.exs. Also make sure server is set to true and a dynamic port configuration is used.
config/prod.exs partial listing
config :phoenix_service, PhoenixService.Endpoint,
http: [port: {:system, "PORT"}, ip: {127,0,0,1}], # serve localhost with dynamic port
url: [host: "example.com", port: 80],
cache_static_manifest: "priv/static/manifest.json", # added comma
server: true # this line is new
Optionally, configure the IP address and add a dynamic port configuration with a default value to config/dev.exs. The above dynamic port solution is suitable for releases. The interpreted solution below is suitable for development.
config/dev.exs partial listing
# http: [port: 4000], # old line 10
http: [port: System.get_env("PORT") || Application.get_env(:phoenix_service, :port) || 4000, ip: {127,0,0,1}],
Tests should pass
mix test
Start the server.
mix phoenix.server
POST and GET a memo to make sure the server works. A prior post covers a shell script for conveniently interacting with this sample app.
# POST new
curl -H 'Content-Type: application/json' -X POST -d '{"memo": {"title": "New Title", "body": "This is the new memo body."}}' http://localhost:4000/api/memos
# GET id 1
curl -H 'Content-Type: application/json' http://localhost:4000/api/memos/1
Generating a Release
Now that the Phoenix app is working, it is time to build a release. Add the elixir release manager (exrm) to mix.exs as a project dependency.
mix.exs partial listing
defp deps do
[{:phoenix, "~> 1.1.4"},
{:postgrex, ">= 0.0.0"},
{:phoenix_ecto, "~> 2.0"},
{:phoenix_html, "~> 2.4"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.9"},
{:exrm, "~> 1.0.2"}, # this line is new
{:cowboy, "~> 1.0"}]
end
Install exrm and build a release. This will create the rel/ directory.
mix deps.get
mix deps.compile
MIX_ENV=prod mix ecto.create
MIX_ENV=prod mix ecto.migrate
MIX_ENV=prod mix compile
# brunch build --production # if using brunch
MIX_ENV=prod mix phoenix.digest
MIX_ENV=prod mix release
The rc script will use environment variable knobs to configure the app. Note that the RELX_REPLACE_OS_VARS=true environment variable needs to be defined to use environment variables for dynamic configuration.
The vm.args file is primarily used to configure the erlang VM. It can also be used to define application configure parameters. Application configuration parameters defined in this file can be passed into the program as atoms or integers. Note that the location of this file can be configured with the RELEASE_CONFIG_DIR environment variable. Add the following to rel/vm.args.
rel/vm.args
## Name of the node
-name ${NODE_NAME}
## Cookie for distributed erlang
-setcookie ${COOKIE}
## App Settings
-phoenix_service port ${PORT}
Alternatively, sys.config can be used to pass in application configuration parameters. In this file, application configuration parameters defined with environment variables must be strings. Pass the port setting in as above or add the following to rel/sys.config. The app module should work with either solution. Adding both files will not break anything. Note that rel/sys.config is written in Erlang.
rel/sys.config
[
{phoenix_service, [
{port, "${PORT}"}
]}
].
Rebuild the release with the configuration files.
MIX_ENV=prod mix release
Start the release in the console.
RELX_REPLACE_OS_VARS=true PORT=7777 rel/phoenix_service/bin/phoenix_service console
Make sure the server responds.
curl http://localhost:7777/api/memos
Exit the console with ^C.
Consider configuring the Phoenix app as a service to get it to start automatically when the machine boots.
Serving a Hidden Service
Content needs to be served to a port on localhost. This post will use port 8080.
RELX_REPLACE_OS_VARS=true PORT=8080 rel/phoenix_service/bin/phoenix_service start
Read the tor configuration instructions. Open /usr/local/etc/tor/torrc (see torrc instructions). Add the following lines to the section titled “This section is just for location-hidden services”.
/usr/local/etc/tor/torrc partial listing
HiddenServiceDir /usr/home/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:8080
Enable tor in /etc/rc.conf
/etc/rc.conf partial listing
tor_enable="YES"
Start tor.
service tor start
Get the hostname for your hidden service with the following command. Do not share the private_key, found in the same directory.
cat /usr/home/tor/hidden_service/hostname
Test your hidden service with curl by supplying the tor proxy with the -x option. The -v flag gives verbose output.
# POST new
curl -v -x socks5h://127.0.0.1:9050 -H 'Content-Type: application/json' -X POST -d '{"memo": {"title": "New Title", "body": "This is the new memo body."}}' http://$(cat /usr/home/tor/hidden_service/hostname)/api/memos
# GET id 1
curl -v -x socks5h://127.0.0.1:9050 -H 'Content-Type: application/json' http://$(cat /usr/home/tor/hidden_service/hostname)/api/memos/1
You can also test your hidden service with Tor2web. For example, if your hidden service has a hostname of ABCDEFGHIJKLMNOP.onion, go to https://ABCDEFGHIJKLMNOP.onion.to to view it in a web browser.
The author of this post could not get Tor2web curl commands to work with a Phoenix app. Something like the following commands should theoretically work. Note that Tor2web blocks the curl user agent, so the user agent is set to test instead.
# POST new
curl -v -A test -x socks5h://127.0.0.1:9050 -H 'Content-Type: application/json' -X POST -d '{"memo": {"title": "New Title", "body": "This is the new memo body."}}' http://$(cat /usr/home/tor/hidden_service/hostname).to/api/memos
# GET id 1
curl -v -A test -x socks5h://127.0.0.1:9050 -H 'Content-Type: application/json' http://$(cat /usr/home/tor/hidden_service/hostname).to/api/memos/1
To disable tor when you no longer need to use it, stop it with the service command.
service tor stop
Then disable it in /etc/rc.conf.
tor_enable="NO"
A Sample Shell Script for Working With Hidden Phoenix JSON APIs
This is a modified version of the script covered in Phoenix, A Shell Script for Working with Phoenix JSON APIs. See that post for a description of the script.
Support for flags has been added. The flags default to using the tor proxy. The default host has been changed to automatically pull in the hidden service hostname.
tor_memo_api.sh
#!/bin/sh
reset() {
HOST=http://$(cat /usr/home/tor/hidden_service/hostname)
SCOPE=api
ROUTE=memos
METHOD="GET"
FLAGS="-x socks5h://127.0.0.1:9050"
HEADERS="Content-Type: application/json"
ID=""
TITLE=""
BODY=""
}
usage() {
reset
echo "Usage: ${0} [options]"
echo "Options:"
echo " -o HOST : set URL host, defaults to \"${HOST}\""
echo " -s SCOPE : set URL scope, defaults to \"${SCOPE}\""
echo " -r ROUTE : set URL route, defaults to \"${ROUTE}\""
echo " -X METHOD : set HTTP method, defaults to \"${METHOD}\""
echo " -f FLAGS : set flags passed to curl, defaults to \"${FLAGS}\""
echo " -H HEADERS : set HTTP headers, defaults to \"${HEADERS}\""
echo " -i ID : set memo id, defaults to \"${ID}\""
echo " -t TITLE : set memo title, defaults to \"${TITLE}\""
echo " -b BODY : set memo body, defaults to \"${BODY}\""
echo " -h : display this help"
echo "Examples:"
echo " ${0} -X GET"
echo " ${0} -X GET -i 7"
echo " ${0} -X POST -t \"Memo Title\" -b \"Memo body here.\""
echo " ${0} -X PATCH -t \"Patched title.\" -i 7"
echo " ${0} -X PATCH -b \"Patched body.\" -i 7"
echo " ${0} -X PUT -t \"New Title\" -b \"New body.\" -i 7"
echo " ${0} -X DELETE -i 7"
exit ${1}
}
reset
while getopts "o:s:r:X:f:H:i:t:b:h" opt
do
case "${opt}" in
o) HOST="${OPTARG}" ;;
s) SCOPE="${OPTARG}" ;;
r) ROUTE="${OPTARG}" ;;
X) METHOD="${OPTARG}" ;;
f) FLAGS="${OPTARG}" ;;
H) HEADERS="${OPTARG}" ;;
i) ID="${OPTARG}" ;;
t) TITLE="${OPTARG}" ;;
b) BODY="${OPTARG}" ;;
h) usage 1 ;;
\?) usage 2 ;;
esac
done
shift $(expr ${OPTIND} - 1)
case "${METHOD}" in
GET)
curl ${FLAGS} -H "${HEADERS}" -X ${METHOD} "${HOST}/${SCOPE}/${ROUTE}${ID:+"/${ID}"}"
;;
POST)
PAYLOAD='{"memo": {"title": "'"${TITLE:-(no title)}"'", "body": "'"${BODY:-(no body)}"'"}}'
curl ${FLAGS} -H "${HEADERS}" -X ${METHOD} -d "${PAYLOAD}" "${HOST}/${SCOPE}/${ROUTE}"
;;
PUT)
PAYLOAD='{"memo": {"title": "'"${TITLE:-(no title)}"'", "body": "'"${BODY:-(no body)}"'"}}'
curl ${FLAGS} -H "${HEADERS}" -X ${METHOD} -d "${PAYLOAD}" "${HOST}/${SCOPE}/${ROUTE}/${ID:?'No ID specified.'}"
;;
PATCH)
# if defined replace individual fields with
# JSON fragments followed by a comma and space
TITLE=${TITLE:+"\"title\": \"${TITLE}\", "}
BODY=${BODY:+"\"body\": \"${BODY}\", "}
# strip trailing comma and space
PAYLOAD="$(echo "${TITLE}${BODY}" | sed 's/, $//g')"
# complete JSON payload
PAYLOAD="{\"memo\": {${PAYLOAD}}}"
curl ${FLAGS} -H "${HEADERS}" -X ${METHOD} -d "${PAYLOAD}" "${HOST}/${SCOPE}/${ROUTE}/${ID:?'No ID specified.'}"
;;
DELETE)
curl ${FLAGS} -H "${HEADERS}" -X ${METHOD} "${HOST}/${SCOPE}/${ROUTE}/${ID:?'No ID specified.'}"
;;
*)
usage 2
;;
esac
echo ""
As written, the above script can only be run as root or the tor user because /usr/home/tor does not have global read permissions. Changing the permissions is a bad idea. Instead, hard code the default host if you want to be able to use the script with unprivileged users.
tor_memo_api.sh partial listing
HOST="http://abcdefghijklmnop.onion"
The script can be used as follows.
chmod +x tor_memo_api.sh
./tor_memo_api.sh -X POST -t "Memo Title" -b "Memo body here."
./tor_memo_api.sh -X GET -i 1
./tor_memo_api.sh -X POST -t "Another Memo" -b "This memo's body."
./tor_memo_api.sh -X PATCH -t "Patched title." -i 2
./tor_memo_api.sh -X PATCH -b "Patched body." -i 1
./tor_memo_api.sh -X PUT -t "New Title" -b "New body." -i 2
./tor_memo_api.sh -X GET
./tor_memo_api.sh -X DELETE -i 1
References:
- Elixir Release Manager
- Elixir, exrm, Release Configuration
- Phoenix, A Shell Script for Working with Phoenix JSON APIs
- Tor Project
- Tor, Tor2web
- Tor, Configuring Hidden Services for Tor
- Tor, Edit torrc
- Tor, HTTP connection closed on: Excess found in a non pipelined read
- Tor, StackOverflow, curl an .onion url over an http proxy does not return expected source
- FreeBSD, Man curl
- FreeBSD, Phoenix as a Service on FreeBSD
- FreeBSD, Getting Started With tor Hidden Services on FreeBSD
- FreeBSD, Installing Phoenix, Elixir and PostgreSQL on FreeBSD