#!/usr/bin/env bash # stdout styles bold='\033[0;1m' italic='\033[0;3m' underl='\033[0;4m' red='\033[0;31m' green='\033[0;32m' yellow='\033[0;33m' normal='\033[0m' ################# ### Functions ### ################# # delete lines with comments from jsonC jsonc2json () { if [ ! -v $1 ] then filename=$1 cat $filename | grep -v \/\/ else echo "${red}jsonc2json: no argument is given${red}" exit 1 fi } # convert string with number of bytes to pretty form bytes2MB () { if [ -v $1 ] then echo "" else bytes=$1 length=${#bytes} if [ $length -gt 9 ] then head=${bytes::-9} tail=${bytes: -9} echo "${head}.${tail::2} GB" elif [ $length -gt 6 ] then head=${bytes::-6} tail=${bytes: -6} echo "${head}.${tail::2} MB" elif [ $length -gt 3 ] then head=${bytes::-3} tail=${bytes: -3} echo "${head}.${tail::2} kB" else echo "$bytes bytes" fi fi } # drop quotes (") at the start and at the end of a string strip_quotes () { if [ -v $1 ] || [ ${#1} -lt 2 ] then echo "" else s=$1 s=${s: 1} # from 1 to the end s=${s:: -1} # from 0 to that is before the last one echo $s fi } # convert json string with statistics to pretty form; # use with pipe | to deal with multiline strings correctly! pretty_stats () { read stats if [ -v "$stats" ] then echo "" else bytes=$(echo $stats | jq ".stat.value") echo "$(bytes2MB $(strip_quotes $bytes))" fi } # check if the mandatory command exists check_command () { cmd=$1 cmd_aim=$2 comment=$3 if command -v $cmd > /dev/null then echo -e "${green}${cmd} found${normal}" else echo -e "${red}${cmd} not found; ${cmd_aim}${normal}" echo -e "${comment}" exit 1 fi } # make directory `dir`; if it already exists, first move it to dir.backup; # if dir.backup already exists, first move it to dir.backup.backup; if it exists, # first delete it unsafe_mkdir () { dir=$1 if [ -d "$dir" ] then if [ -d "${dir}.backup" ] then if [ -d "${dir}.backup.backup" ] then rm -r "${dir}.backup.backup" fi mv "${dir}.backup" "${dir}.backup.backup" [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}.backup.backup" fi mv "$dir" "${dir}.backup" [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}.backup" fi mkdir "$dir" [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}" } # copy file to file.backup with the same logic as in `unsafe_mkdir` cp_to_backup () { file=$1 if [ -f "${file}.backup" ] then cp "${file}.backup" "${file}.backup.backup" [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${file}.backup.backup" fi cp "$file" "${file}.backup" [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${file}.backup" } # the main part of `./ex.sh conf` command, generates config files for server and clients conf () { export PATH=$PATH:/usr/local/bin/ # brings xray to the path for sudo user check_command xray "needed for config generation" "to install xray, try: sudo ./ex.sh install" check_command jq "needed for operations with configs" check_command openssl "needed for strong random numbers excluding some types of attacks" # echo -e "Enter IPv4 or IPv6 address of your xray server, or its domain name:" read address if [ -v $address ] then echo -e "${red}no address given${normal}" exit 1 fi id=$(xray uuid) # random uuid for VLESS keys=$(xray x25519) # string "Private key: Abc... Public key: Xyz..." private_key=$(echo $keys | cut -d " " -f 3) # get 3rd field of fields delimited by spaces public_key=$(echo $keys | cut -d " " -f 6) # get 6th field short_id=$(openssl rand -hex 8) # random short_id for REALITY # echo -e "Choose a fake site to mimic. Better if it is quite popular and not blocked in your country: (1) www.youtube.com (default) (2) www.microsoft.com (3) www.google.com (4) www.bing.com (5) www.yahoo.com (6) www.adobe.com (7) aws.amazon.com (8) discord.com (9) your variant" read number default_fake_site="www.youtube.com" if [ -v $number ] then fake_site=$default_fake_site else if [ $number -eq 2 ] then fake_site="www.microsoft.com" elif [ $number -eq 3 ] then fake_site="www.google.com" elif [ $number -eq 4 ] then fake_site="www.bing.com" elif [ $number -eq 5 ] then fake_site="www.yahoo.com" elif [ $number -eq 6 ] then fake_site="www.adobe.com" elif [ $number -eq 7 ] then fake_site="aws.amazon.com" elif [ $number -eq 8 ] then fake_site="discord.com" elif [ $number -eq 9 ] then echo -e "type your variant:" read fake_site if [ -v $fake_site ] then fake_site=$default_fake_site fi else fake_site=$default_fake_site fi fi echo -e "${green}mimic ${fake_site}${normal}" server_names="[ \"$fake_site\" ]" email="love@xray.com" # unsafe_mkdir conf ## Make server config ## jsonc2json template_config_server.jsonc \ | jq ".inbounds[1].settings.clients[0].id=\"${id}\" | .inbounds[2].settings.clients[0].id=\"${id}\" | .inbounds[1].settings.clients[0].email=\"${email}\" | .inbounds[2].settings.clients[0].email=\"${email}\" | .inbounds[1].streamSettings.realitySettings.dest=\"${fake_site}:443\" | .inbounds[2].streamSettings.realitySettings.dest=\"${fake_site}:80\" | .inbounds[1].streamSettings.realitySettings.serverNames=${server_names} | .inbounds[2].streamSettings.realitySettings.serverNames=${server_names} | .inbounds[1].streamSettings.realitySettings.privateKey=\"${private_key}\" | .inbounds[2].streamSettings.realitySettings.privateKey=\"${private_key}\" | .inbounds[1].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ] | .inbounds[2].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]" \ > ./conf/config_server.json # make the user (not root) the owner of the file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json vnext=" [ { \"address\": \"${address}\", \"port\": 443, \"users\": [ { \"id\": \"${id}\", \"email\": \"${email}\", \"encryption\": \"none\", \"flow\": \"xtls-rprx-vision\" } ] } ]" clientRealitySettings=" { \"fingerprint\": \"chrome\", \"serverName\": \"${fake_site}\", \"show\": false, \"publicKey\": \"${public_key}\", \"shortId\": \"${short_id}\", }" ## Make main client config ## jsonc2json template_config_client.jsonc \ | jq ".outbounds |= map(if .settings.vnext then .settings.vnext=${vnext} else . end) | .outbounds |= map(if .streamSettings.realitySettings then .streamSettings.realitySettings=${clientRealitySettings} else . end)" \ > ./conf/config_client.json # make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client.json if [ -f "./conf/config_client.json" ] && [ -f "./conf/config_server.json" ] then echo -e "${green}config files are generated${normal}" else echo -e "${red}config files are not generated${normal}" exit 1 fi } # the main part of `./ex.sh add` command, adds config for given users and updates server config add () { export PATH=$PATH:/usr/local/bin/ # brings xray to the path for sudo user check_command xray "needed for config generation" "to install xray, try: sudo ./ex.sh install" check_command jq "needed for operations with configs" check_command openssl "needed for strong random numbers excluding some types of attacks" if [ ! -f "./conf/config_client.json" ] || [ ! -f "./conf/config_server.json" ] then echo -e "${red}server config and config for default user are needed but not present; to generate them, try ./ex.sh conf${normal}" exit 1 fi if [ -v $1 ] then echo -e "${red}usernames not set${normal} For default user, use config_client.json generated by ${underl}install${normal} command. Otherwise use non-void usernames, preferably of letters and digits only." exit 1 fi # backup server config cp_to_backup ./conf/config_server.json # loop over usernames for username in "$@" do username_exists=false client_emails=$(jq ".inbounds[1].settings.clients[].email" ./conf/config_server.json) for email in ${client_emails[@]} do # convert "name@example.com" to name name=$(echo $email | cut -d "@" -f 1 | cut -c 2-) if [ $username = $name ] then username_exists=true fi done if $username_exists then echo -e "${yellow}username ${username} already exists, no new config created fot it${normal}" else id=$(xray uuid) # generate random uuid for vless # generate random short_id for grpc-reality short_id=$(openssl rand -hex 8) # make new user config from default user config ok1=$(cat ./conf/config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${username}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > ./conf/config_client_${username}.json) # then make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client_${username}.json # update server config client=" { \"id\": \"${id}\", \"email\": \"${username}@example.com\", \"flow\": \"xtls-rprx-vision\" } " # update server config cp ./conf/config_server.json ./conf/tmpconfig.json ok2=$(cat ./conf/tmpconfig.json | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > ./conf/config_server.json) rm ./conf/tmpconfig.json # then make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json if $ok1 && $ok2 then echo -e "${green}config_client_${username}.json is written, config_server.json is updated${normal}" else echo -e "${yellow}something went wrong with username ${username}${normal}" fi fi done } # `./ex.sh push` command, copies config to xray's dir and restarts xray push () { if [ $(id -u) -ne 0 ] # not root then echo -e "${red}you should have root privileges for that, try sudo ./ex.sh push${normal}" exit 1 fi echo -e "Which config to use, server/client/other? (S/c/o)" read answer if [ ! -v $answer ] && [ ${answer::1} = "c" ] then # use main client config config="config_client.json" elif [ ! -v $answer ] && [ ${answer::1} = "o" ] then # use config of some other user echo -e "Which config from ./conf/ to use? (write the filename)" read answer config="$answer" else # use server config config="config_server.json" fi if $(cp ./conf/${config} /usr/local/etc/xray/config.json && systemctl restart xray) then sleep 1s # gives time to xray restart journalctl -u xray | tail -n 5 # message about xray start else echo -e "${red}can't copy config or start xray, try sudo xray run -c ./conf/${config}${normal}" fi } ############# ### MAIN #### ############# command="help" # default if [ ! -v $1 ] then command=$1 fi if [ $command = "install" ] then if [ $(id -u) -ne 0 ] # not root then echo -e "${red}you should have root privileges to install xray, try sudo ./ex.sh install${normal}" exit 1 fi # if command -v xray > /dev/null # xray already installed then echo -e "${yellow}xray ${version} detected, install anyway?${normal} (y/N)" read answer # default answer, answer not set or it's first letter is not `y` or `Y` if [ -v $answer ] || ([ ${answer::1} != "y" ] && [ ${answer::1} != "Y" ]) then exit 1 fi fi # if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install then echo -e "${green}xray installed${normal}" dat_dir="/usr/local/share/xray/" mkdir -p $dat_dir if cp customgeo.dat ${dat_dir} then echo -e "${green}customgeo.dat copied to ${dat_dir}${normal}" else echo -e "${red}customgeo.dat not copied to ${dat_dir}${normal}" exit 1 fi else echo -e "${red}xray not installed, something goes wrong${normal}" exit 1 fi # echo -e "Generate configs? (Y/n)" read answer if [ ! -v $answer ] && [ ${answer::1} = "n" ] then # config generation is not requested echo -e "If you have a config file for xray, you can manually start xray with the following commands: sudo cp yourconfig.json /usr/local/etc/xray/config.json sudo systemctl start xray or sudo xray run -c yourconfig.json" exit 0 else # config generation is requested conf fi # echo -e "Add other users? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then echo -e "Enter usernames separated by spaces" read usernames add $usernames fi # echo -e "Copy config to xray's dir and restart xray? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then push fi elif [ $command = "conf" ] then conf # echo -e "Add other users? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then echo -e "Enter usernames separated by spaces" read usernames add $usernames fi # echo -e "Copy config to xray's dir and restart xray? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then push fi elif [ $command = "add" ] then add "${@:2}" # echo -e "Copy config to xray's dir and restart xray? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then push fi elif [ $command = "del" ] then if [ -v $2 ] then echo -e "${red}usernames not set${normal}" exit 1 fi check_command jq "needed for operations with configs" if [ ! -f "./conf/config_server.json" ] then echo -e "${red}server config not found" exit 1 fi # backup server config cp_to_backup ./conf/config_server.json # loop over usernames for username in "${@:2}" do config="./conf/config_client_${username}.json" if [ ! -f $config ] then echo -e "${yellow}no config for user ${username}${normal}" else short_id=$(jq ".outbounds[0].streamSettings.realitySettings.shortId" $config) cp ./conf/config_server.json ./conf/tmpconfig.json # update server config ok1=$(cat ./conf/tmpconfig.json | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > ./conf/config_server.json) rm ./conf/tmpconfig.json # then make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json ok2=$(rm ./conf/config_client_${username}.json) if $ok1 && $ok2 then echo -e "${green}config_client_${username}.json is deleted, config_server.json is updated${normal}" else echo -e "${red}something went wrong with username ${username}${normal}" fi fi done echo -e "Copy config to xray's dir and restart xray? (Y/n)" read answer if [ -v $answer ] || [ ${answer::1} != "n" ] then push fi elif [ $command = "push" ] then push elif [ $command = "link" ] then conf_file=$2 if [ -v $conf_file ] then echo -e "${red}no config is given${normal}" exit 1 fi id=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].id" $conf_file)) address=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].address" $conf_file)) port=$(jq ".outbounds[0].settings.vnext[0].port" $conf_file) public_key=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.publicKey" $conf_file)) server_name=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.serverName" $conf_file)) short_id=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.shortId" $conf_file)) link="vless://${id}@${address}:${port}?fragment=&security=reality&encryption=none&pbk=${public_key}&fp=chrome&type=tcp&flow=xtls-rprx-vision&sni=${server_name}&sid=${short_id}#easy-xray+%F0%9F%97%BD" echo -e "${yellow}don't forget to share misc/customgeo4hiddify.txt as well ${green}here is your link:${normal}" echo $link elif [ $command = "stats" ] then client_stats_proxy_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>proxy>>>traffic>>>downlink" 2> /dev/null) server_stats_direct_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>downlink" 2> /dev/null) if [ ! -z "$client_stats_proxy_down" ] # output is not a zero string, hence script is running on a client then ## Client statistics ## echo "Downloaded via server: $(echo $client_stats_proxy_down | pretty_stats)" # client_stats_proxy_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>proxy>>>traffic>>>uplink" 2> /dev/null) echo "Uploaded via server: $(echo $client_stats_proxy_up | pretty_stats)" # client_stats_direct_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>downlink" 2> /dev/null) echo "Downloaded via client directly: $(echo $client_stats_direct_down | pretty_stats)" # client_stats_direct_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>uplink" 2> /dev/null) echo "Uploaded via client directly: $(echo $client_stats_direct_up | pretty_stats)" elif [ ! -z "$server_stats_direct_down" ] # output is not a zero string, hence script is running on a server then ## Server statistics ## echo "Downloaded in total: $(echo $server_stats_direct_down | pretty_stats)" # server_stats_direct_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>uplink" 2> /dev/null) echo "Uploaded in total: $(echo $server_stats_direct_up | pretty_stats)" # # Per user statistics conf_file="./conf/config_server.json" # assuming xray is running with this config qemails=$(cat $conf_file | jq ".inbounds[1].settings.clients[].email") for qemail in ${qemails[@]} do echo "" email=$(strip_quotes $qemail) user_stats_down=$(xray api stats -server=127.0.0.1:8080 -name "user>>>${email}>>>traffic>>>downlink" 2> /dev/null) echo "Downloaded by ${email}: $(echo $user_stats_down | pretty_stats)" user_stats_up=$(xray api stats -server=127.0.0.1:8080 -name "user>>>${email}>>>traffic>>>uplink" 2> /dev/null) echo "Uploaded by ${email}: $(echo $user_stats_up | pretty_stats)" done else echo -e "${red}xray should be running to aquire or reset statistics${normal}" exit 1 fi # if [ ! -v $2 ] && [ $2 = "reset" ] then echo "" xray api statsquery -server=127.0.0.1:8080 -reset > /dev/null \ && echo -e "${green}statistics reset successfully${normal}" \ || echo -e "${red}statistics reset failed${normal}" fi elif [ $command = "import" ] then if [ -v $2 ] || [ -v $3 ] then echo -e "${red}both directories (from and to) should be set${normal}" exit 1 fi from=$2 to=$3 # backup the server and the main client configs cp_to_backup ${to}/config_server.json # configs="${from}/config_client_*.json" for c in $configs do if [ -f $c ] then uname_with_json=$(echo $c | cut -d "_" -f 3-) # remove "config_client_" uname_from_filename=${uname_with_json:: -5} # remove ".json" email=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].email" $c)) uname_from_email=${email%@*} # remove "@example.com" if [ $uname_from_filename != $uname_from_email ] then echo -e "${yellow}username ${uname_from_filename} (from filename) inconsistent with username ${uname_from_email} (from email), continue with name from email${normal}" fi if [ -f ${to}/config_client_${uname_from_email}.json ] # username already exists then echo -e "${yellow}username ${uname_from_email} already exists in ${to}, no new config created fot it${normal}" else id=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].id" $c)) short_id=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.shortId" $c)) # make new user config ok1=$(cat ${to}/config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${uname_from_email}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > ${to}/config_client_${uname_from_email}.json) # then make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ${to}/config_client_${uname_from_email}.json # update server config client=" { \"id\": \"${id}\", \"email\": \"${uname_from_email}@example.com\", \"flow\": \"xtls-rprx-vision\" } " cp ${to}/config_server.json ${to}/tmpconfig.json ok2=$(cat ${to}/tmpconfig.json | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > ${to}/config_server.json) rm ${to}/tmpconfig.json # then make the user (not root) an owner of a file [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ${to}/config_server.json if $ok1 && $ok2 then echo -e "${green}${to}/config_client_${uname_from_email}.json is written, ${to}/config_server.json is updated${normal}" else echo -e "${yellow}something went wrong with username ${uname_from_email}${normal}" fi fi fi done elif [ $command = "upgrade" ] then if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install then echo -e "${green}xray upgraded${normal}" else echo -e "${red}xray not upgraded${normal}" fi elif [ $command = "remove" ] then echo -e "Remove xray? (y/N)" read answer if [ ! -v $answer ] && [ ${answer::1} = "y" ] then if [ $(id -u) -ne 0 ] # not root then echo -e "${red}you should have root privileges for that, try sudo ./ex.sh push${normal}" exit 1 fi if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ remove --purge then echo -e "${green}xray removed${normal}" else echo -e "${red}xray not removed${normal}" fi fi else # "help", default echo -e " ${green}**** Hi, there! ****${normal} The aim of ${italic}easy-xray${normal} is to help to administrate an xray server. It's all about ${italic}conf${normal} directory that contains the server config ${italic}config_server.json${normal}, the main client config ${italic}config_client.json${normal} and configs for other users ${italic}config_client_*.json${normal}. How to use it: ${bold}./ex.sh ${underl}command${normal} Here is a list of all the commands available: ${bold}help${normal} show this message (default) ${bold}install${normal} run interactive prompt, that asks to download and install XRay, and to generate configs ${bold}conf${normal} generate config files for server and clients ${bold}add ${underl}usernames${normal} add users with given usernames to configs, usernames should by separated by spaces ${bold}del ${underl}usernames${normal} delete users with given usernames from configs ${bold}push${normal} copy config to xray's dir and restart xray ${bold}link ${underl}config${normal} convert user config to a link acceptable by client applications such as Hiddify or V2ray ${bold}stats${normal} print some traffic statistics ${bold}stats reset${normal} print statistics then set them to zero ${bold}import ${underl}from${normal} ${underl}to${normal} import users from one directory that contains user configs to another directory that contains server config and the main client config ${bold}upgrade${normal} upgrade xray, do not touch configs ${bold}remove${normal} remove xray" fi