├── .gitignore ├── README.md ├── history-server.png └── history-server.sh /.gitignore: -------------------------------------------------------------------------------- 1 | history.dat 2 | secret 3 | writer.sh 4 | run-one 5 | history-server.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # history-service 2 | 3 | ## Overview 4 | 5 | Stores your history on a central server. 6 | 7 | All you need is a port and a host to serve from, and your bash commands will be 8 | sent to and retrieved from there. 9 | 10 | ![overviewimage](https://raw.githubusercontent.com/ianmiell/history-service/master/history-server.png) 11 | 12 | Basic security is provided by a simple shared secret. 13 | 14 | ## Setup 15 | 16 | 17 | Key for below: 18 | 19 | ``` 20 | PORTNUMBER - port you want to run on 21 | HOSTNAME - hostname you run the service on 22 | YOURSECRET - your secret word for entry to service 23 | ``` 24 | 25 | - Put password for the service in `secret` file (on one line) 26 | 27 | - Run: `chmod 400 secret` to make the file (relatively) secure 28 | 29 | - Test it on the `localhost`: 30 | 31 | Run, replacing `YOURSECRET` with your secret above. 32 | 33 | ``` 34 | $ ./history-server.sh PORTNUMBER & 35 | printf 'YOURSECRET\ntest\n' | nc localhost PORTNUMBER 36 | printf 'YOURSECRET\n\n' | nc localhost PORTNUMBER 37 | kill %1 38 | ``` 39 | 40 | - Add `/path/to/history-service/server.sh` to cronjob to run as a service - 41 | `run-one` takes care of duplicates 42 | 43 | ``` 44 | $ crontab -e 45 | ``` 46 | 47 | Then input the line (replacing the path): 48 | 49 | ``` 50 | * * * * * /path/to/history-service/history-server.sh 51 | ``` 52 | 53 | - Append the following to your ~/.bashrc file, replacing `YOURSECRET` with the 54 | secret in the `secret` file and `HOSTNAME` with the host the service is running 55 | on. 56 | 57 | ``` 58 | # history service 59 | function history_service_send_last_command() { LAST=$(HISTTIMEFORMAT='' builtin history 1 | cut -c 8-); printf '%q' 'YOURSECRET\n'"${LAST}"'\n' | nc HOSTNAME PORTNUMBER; } 60 | if [[ ${PROMPT_COMMAND} = '' ]] 61 | then 62 | PROMPT_COMMAND="history_service_send_last_command" 63 | else 64 | PROMPT_COMMAND="${PROMPT_COMMAND};history_service_send_last_command" 65 | fi 66 | alias historyservice="printf '%q' 'YOURSECRET\n\n' | nc HOSTNAME PORTNUMBER' 67 | ``` 68 | 69 | The security level of this is sufficent to stop casual users from abusing your 70 | file system or getting access (assuming your secret is strong enough and kept 71 | safe), but is not enough to stop a determined attacker from doing damage. 72 | Use at your own risk. 73 | 74 | ## Requirements 75 | 76 | Requires: 77 | 78 | - `bash` v4+ 79 | 80 | Check your version with: 81 | 82 | ``` 83 | echo ${BASH_VERSION[0]} 84 | ``` 85 | 86 | If you are on a Mac, you may want to `brew install bash` to get a later version. 87 | The default one that ships is a 3.x version (!?). 88 | 89 | - `socat` 90 | 91 | http://www.dest-unreach.org/socat/ 92 | 93 | Available on most package managers. 94 | 95 | ## Bugs / TODOs 96 | 97 | - Multi-line commands not well-handled 98 | - No 'offline' store and forward 99 | - Unique the commands? 100 | - Record the date and source of the command? 101 | - Create a history db? 102 | -------------------------------------------------------------------------------- /history-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianmiell/history-service/7f4e77297030834ef0e374bb244e03678c86aa81/history-server.png -------------------------------------------------------------------------------- /history-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uex 4 | 5 | # make sure we are in the right folder 6 | cd "$(dirname ${BASH_SOURCE[0]})" 7 | 8 | if ! [ -a secret ] 9 | then 10 | echo 'Create secret file with your password, and run "chmod 400 secret"' 11 | exit 1 12 | fi 13 | if [[ $(expr $(stat -c %a secret 2> /dev/null || stat -f%A secret 2> /dev/null || echo stat failed) % 100) != '0' ]] 14 | then 15 | echo 'Run "chmod 400 secret" - file should only be readable by you' 16 | exit 1 17 | fi 18 | 19 | LISTEN_PORT=${1} 20 | LOGFILE=${2:-history-server.log} 21 | 22 | # Check socat is there 23 | which socat > /dev/null 2>&1 || (echo socat should be installed && exit 1) 24 | 25 | cat > writer.sh <<- 'END' 26 | #!/bin/bash 27 | set -eu 28 | read password 29 | # Remove whitespace from end of password (eg when using telnet) 30 | password="${password%"${password##*[![:space:]]}"}" 31 | SECRET="$(cat secret)" 32 | if [[ ${password} != ${SECRET} ]] 33 | then 34 | echo 'Password failure' 35 | echo "Password failure: ${password} ${SECRET}" >> history-server.log 36 | exit 1 37 | fi 38 | touch history.dat 39 | while read input 40 | do 41 | echo $input >> history-server.log 42 | if [[ $input = '' ]] 43 | then 44 | cat history.dat 45 | fi 46 | echo $input >> history.dat 47 | done 48 | END 49 | chmod +x writer.sh 50 | 51 | RUN_ONE='run-one' 52 | if [[ $(uname -s) = 'Darwin' ]] 53 | then 54 | RUN_ONE='./run-one' 55 | cat > run-one <<- 'END' 56 | #!/bin/sh -e 57 | PROG="run-one" 58 | if [ $# -eq 0 ]; then 59 | echo "ERROR: no arguments specified" 1>&2 60 | exit 1 61 | fi 62 | USER=$(id -un) 63 | for i in "${HOME}" "/dev/shm/${PROG}_${USER}"*; do 64 | if [ -w "$i" ] && [ -O "$i" ]; then 65 | DIR="$i" 66 | break 67 | fi 68 | done 69 | if [ -w "${DIR}" ] && [ -O "${DIR}" ]; then 70 | DIR="${DIR}/.cache/${PROG}" 71 | else 72 | DIR=$(mktemp -d "/dev/shm/${PROG}_${USER}_XXXXXXXX") 73 | DIR="${DIR}/.cache/${PROG}" 74 | fi 75 | mkdir -p "$DIR" 76 | CMD="$@" 77 | CMDHASH=$(echo "$CMD" | md5sum | awk '{print $1}') 78 | FLAG="$DIR/$CMDHASH" 79 | base="$(basename $0)" 80 | case "$base" in 81 | run-one) 82 | if [[ $(uname) == 'Darwin' ]] 83 | then 84 | flock -n "$FLAG" "$@" 85 | else 86 | flock -xn "$FLAG" "$@" 87 | fi 88 | ;; 89 | run-this-one) 90 | ps="$CMD" 91 | for p in $(pgrep -u "$USER" -f "^$ps$" || true); do 92 | kill $p 93 | while ps $p >/dev/null 2>&1; do 94 | kill $p 95 | sleep 1 96 | done 97 | done 98 | pid=$(lsof "$FLAG" 2>/dev/null | awk '{print $2}' | grep "^[0-9]") || true 99 | [ -z "$pid" ] || kill $pid 100 | sleep 0.5 101 | if [[ $(uname) == 'Darwin' ]] 102 | then 103 | flock -n "$FLAG" "$@" 104 | else 105 | flock -xn "$FLAG" "$@" 106 | fi 107 | ;; 108 | keep-one-running|run-one-constantly|run-one-until-success|run-one-until-failure) 109 | backoff=0 110 | retries=0 111 | while true; do 112 | set +e 113 | if [[ $(uname) == 'Darwin' ]] 114 | then 115 | flock -n "$FLAG" "$@" 116 | else 117 | flock -xn "$FLAG" "$@" 118 | fi 119 | if [ "$?" = 0 ]; then 120 | [ "$base" = "run-one-until-success" ] && exit $? 121 | backoff=0 122 | backoff=0 123 | else 124 | [ "$base" = "run-one-until-failure" ] && exit $? 125 | retries=$((retries + 1)) 126 | backoff=$((retries / 10)) 127 | logger -t "${base}[$$]" "last run failed; sleeping [$backoff] seconds before next run" 128 | fi 129 | [ $backoff -gt 60 ] && backoff=60 130 | sleep $backoff 131 | done 132 | ;; 133 | esac 134 | END 135 | chmod +x run-one 136 | fi 137 | 138 | which ${RUN_ONE} || (echo run-one should be installed && exit 1) 139 | 140 | ${RUN_ONE} socat -vvv TCP-LISTEN:${LISTEN_PORT},reuseaddr,fork SYSTEM:"$(pwd)/writer.sh" 141 | 142 | SOCATPID="$!" 143 | 144 | function cleanup() { 145 | kill ${SOCATPID} >/dev/null 2>&1 || return > /dev/null 2>&1 146 | echo Pausing before final kill >> ${LOGFILE} 147 | sleep 10 148 | if ps -p ${SOCATPID} > /dev/null 2>&1 149 | then 150 | kill -9 ${SOCATPID} 151 | fi 152 | } 153 | 154 | trap cleanup EXIT INT TERM 155 | 156 | wait 157 | --------------------------------------------------------------------------------