commands.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "strconv"
  6. "sync"
  7. "syscall"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. )
  12. func scpDirectoryDoesNotExist(config *Configuration, create_directory bool) bool {
  13. check_scp_directory := exec.Command(
  14. "ssh",
  15. // specify port
  16. "-p", strconv.Itoa(config.ssh_port),
  17. // specify identity file
  18. "-i", config.ssh_identity_path,
  19. // specify user@host
  20. fmt.Sprintf("%s@%s", config.ssh_user, config.url),
  21. // specify remote command
  22. fmt.Sprintf("test -e %s", config.scp_directory),
  23. )
  24. // start command
  25. err := check_scp_directory.Start()
  26. if err != nil {
  27. fmt.Fprintf(os.Stderr, "fatal error: ssh command could not be started\n")
  28. // this error is fatal (partly also because basically everything makes use of ssh)
  29. os.Exit(EXIT_SSH_ERROR)
  30. }
  31. // use wait to synchronize with command and get exit code (as err)
  32. does_not_exist := check_scp_directory.Wait() != nil
  33. if create_directory && does_not_exist {
  34. fmt.Printf("scp target directory %s does not exist, will be created\n", config.scp_directory)
  35. create_scp_directory := exec.Command(
  36. "ssh",
  37. // specify port
  38. "-p", strconv.Itoa(config.ssh_port),
  39. // specify identity file
  40. "-i", config.ssh_identity_path,
  41. // specify user@host
  42. fmt.Sprintf("%s@%s", config.ssh_user, config.url),
  43. // specify remote command
  44. fmt.Sprintf("mkdir -p %s", config.scp_directory),
  45. )
  46. err = create_scp_directory.Run()
  47. if err != nil {
  48. fmt.Fprintf(os.Stderr, "scp target directory %s could not be created\n", config.scp_directory)
  49. os.Exit(EXIT_SCP_ERROR)
  50. }
  51. }
  52. // signal if scp target directory does not exists
  53. return does_not_exist
  54. }
  55. func list(config *Configuration, files []string, placeholder bool) {
  56. if scpDirectoryDoesNotExist(config, false) {
  57. fmt.Fprintf(os.Stderr, "fatal error: remote directory %s does not exist\n", config.scp_directory)
  58. return
  59. }
  60. // get parameters
  61. target := fmt.Sprintf(config.url_fmt, config.url)
  62. // define commands
  63. //curl -s "${url}" | grep href | sed -e 's/<[^<]*>//g'
  64. cmd_curl := exec.Command("curl", "-s", target)
  65. cmd_grep := exec.Command("grep", "href")
  66. cmd_sed := exec.Command("sed", "-e", "s/<[^<]*>//g")
  67. // pipe curl into grep
  68. pipe_curl_to_grep, err := cmd_curl.StdoutPipe()
  69. if err != nil {
  70. fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
  71. // this error is fatal
  72. return
  73. }
  74. // make sure to close pipe at the end (if it was opened)
  75. defer pipe_curl_to_grep.Close()
  76. // finalize pipe
  77. cmd_grep.Stdin = pipe_curl_to_grep
  78. // pipe grep into sed
  79. pipe_grep_to_sed, err := cmd_grep.StdoutPipe()
  80. if err != nil {
  81. fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
  82. return
  83. }
  84. // make sure to close pipe at the end (if it was opened)
  85. defer pipe_grep_to_sed.Close()
  86. // finalize pipe
  87. cmd_sed.Stdin = pipe_grep_to_sed
  88. // let both initial commands do their work
  89. cmd_curl.Start()
  90. cmd_grep.Start()
  91. // finally query stdout/stderr of last command
  92. stdoutstderr, err := cmd_sed.CombinedOutput()
  93. if err != nil {
  94. fmt.Fprintf(os.Stderr, "error: getting of output by `sed` failed\n")
  95. }
  96. // give it to our user
  97. fmt.Printf("%s", stdoutstderr)
  98. }
  99. func pull(config *Configuration, filenames []string, save bool) {
  100. // require at least one file
  101. if len(filenames) <= 0 {
  102. fmt.Printf("pull command requires at least one file\n")
  103. return
  104. }
  105. // NOTE: this function does allow duplicate filenames, which, quite magically, still works
  106. // check existence of directory on remote host
  107. if scpDirectoryDoesNotExist(config, false) {
  108. fmt.Fprintf(os.Stderr, "fatal error: remote directory %s does not exist\n", config.scp_directory)
  109. return
  110. }
  111. // do this now so it is already done once we get to the files
  112. url := fmt.Sprintf(config.url_fmt, config.url)
  113. // set up saving of files (instead of stdout output)
  114. single_file := len(filenames) <= 1
  115. save_files := save || !single_file
  116. // configure wait group for synchronization
  117. wg := sync.WaitGroup{}
  118. wg.Add(len(filenames))
  119. // iterate all filenames for pull action
  120. for _, f := range filenames {
  121. // skip problemativ filenames
  122. if strings.Contains(f, "/") {
  123. fmt.Printf("directories are not supported at the time\n")
  124. // finish task pre-emptively
  125. wg.Done()
  126. continue
  127. }
  128. // use goroutines because tasks are independent
  129. go func(wg *sync.WaitGroup, f string) {
  130. // finish task at the end
  131. defer wg.Done()
  132. // because this is a naive reimplementation I'm not going to use the actual libraries here
  133. // OUTDATED
  134. //curl -s "${url}${1}" | base64 -d | openssl enc -d -aes-256-cbc -k "${secret}" > "${file}"
  135. // NEW COMMAND
  136. //curl -s "${url}${1}" | openssl enc -d -aes-256-cbc -k "${secret}" --base64 > "${file}"
  137. cmd_curl := exec.Command("curl", "-s", fmt.Sprintf("%s%s", url, f))
  138. cmd_openssl := exec.Command("openssl", "enc", "-d", "-aes-256-cbc", "-base64", "-k", config.secret)
  139. pipe_curl_to_openssl, err := cmd_curl.StdoutPipe()
  140. // pipe curl into grep
  141. if err != nil {
  142. fmt.Fprintf(os.Stderr, "fatal error: construction of pipe failed\n")
  143. // this error is fatal
  144. return
  145. }
  146. // make sure to close pipe at the end (if it was opened)
  147. defer pipe_curl_to_openssl.Close()
  148. // finalize pipe
  149. cmd_openssl.Stdin = pipe_curl_to_openssl
  150. // set up command output
  151. cmd_openssl.Stdout = os.Stdout
  152. if save_files {
  153. // create file in preparation to execute command
  154. file, err := os.Create(f)
  155. defer file.Close()
  156. // handle error if no such file exists exists on remote host
  157. if err != nil {
  158. fmt.Fprintf(os.Stderr, "cannot create file %s: %s\n", f, err.Error())
  159. }
  160. // assign stdout to newly created file
  161. cmd_openssl.Stdout = file
  162. }
  163. // let both initial commands do their work
  164. cmd_curl.Start()
  165. // finally let the actual command do it's work
  166. cmd_openssl.Run()
  167. // finally query stdout/stderr of last command
  168. if err != nil {
  169. fmt.Fprintf(os.Stderr, "file %s could not be pulled\n", f)
  170. return
  171. } else {
  172. if save_files {
  173. fmt.Printf("successfully pulled %s\n", f)
  174. }
  175. }
  176. }(&wg, f)
  177. }
  178. // synchronize (program has to wait for goroutines otherwise output will be lost)
  179. wg.Wait()
  180. }
  181. func push(config *Configuration, filenames []string, placeholder bool) {
  182. // require at least one file
  183. if len(filenames) <= 0 {
  184. fmt.Printf("pull command requires at least one file\n")
  185. return
  186. }
  187. // check for existence of directory on remote host and create if necessary
  188. scpDirectoryDoesNotExist(config, true)
  189. // configure wait group for synchronization
  190. wg := sync.WaitGroup{}
  191. wg.Add(len(filenames))
  192. // iterate all filenames for pull action
  193. for _, f := range filenames {
  194. // skip problemativ filenames
  195. if strings.Contains(f, "/") {
  196. fmt.Printf("directories are not supported at the time\n")
  197. // finish task pre-emptively
  198. wg.Done()
  199. continue
  200. }
  201. // use goroutines because tasks are independent
  202. go func(wg *sync.WaitGroup, f string) {
  203. // finish task at the end
  204. defer wg.Done()
  205. // because this is a naive reimplementation I'm not going to use the actual libraries here
  206. // this one is more complicated as it's composed of two commands
  207. // OUTDATED
  208. //openssl enc -aes-256-cbc -k "${secret}" < "${1}" | base64 > "${tmp}"
  209. //scp "${port}" -i "${identity}" "${tmp}" "${user}"@"${webserver}":~/dropzone/"${file}"
  210. // NEW COMMAND
  211. //openssl enc -aes-256-cbc -base64 -k "${secret}" -in "${1}" -out "${tmp}"
  212. //scp "${port}" -i "${identity}" "${tmp}" "${user}"@"${webserver}":~/dropzone/"${file}"
  213. // create temporary file to put encrypted contents in and later push
  214. tmp, err := os.CreateTemp("/tmp", "strlst.*")
  215. if err != nil {
  216. fmt.Fprintf(os.Stderr, "error creating temporary file while trying to push %s\n", f)
  217. return
  218. }
  219. // make sure clean up happens afterwards
  220. defer os.Remove(tmp.Name())
  221. // define openssl command
  222. cmd_openssl := exec.Command(
  223. // openssl command with parameters
  224. "openssl", "enc", "-aes-256-cbc", "-base64",
  225. // specify encryption secret
  226. "-k", config.secret,
  227. // specify input file
  228. "-in", f,
  229. // specify output file
  230. "-out", tmp.Name(),
  231. )
  232. // start openssl command
  233. err = cmd_openssl.Start()
  234. if err != nil {
  235. fmt.Fprintf(os.Stderr, "error starting openssl command while trying to push %s\n", f)
  236. return
  237. }
  238. // wait and get exit code
  239. err = cmd_openssl.Wait()
  240. if err != nil {
  241. fmt.Fprintf(os.Stderr, "error executing openssl command while trying to push %s: %v\n", f, err)
  242. return
  243. }
  244. // define scp command
  245. cmd_scp := exec.Command(
  246. "scp",
  247. // specify port
  248. "-P", strconv.Itoa(config.ssh_port),
  249. // specify identity file
  250. "-i", config.ssh_identity_path,
  251. // temporary file that was created earlier
  252. tmp.Name(),
  253. // specify user, url and destination location
  254. fmt.Sprintf("%s@%s:%s", config.ssh_user, config.url, filepath.Join(config.scp_directory, f)),
  255. )
  256. // start scp command
  257. err = cmd_scp.Start()
  258. if err != nil {
  259. fmt.Fprintf(os.Stderr, "error starting scp command while trying to push %s\n", f)
  260. return
  261. }
  262. // wait and get exit code
  263. err = cmd_scp.Wait()
  264. if err != nil {
  265. fmt.Fprintf(os.Stderr, "error executing scp command while trying to push %s: %v\n", f, err)
  266. return
  267. } else {
  268. fmt.Printf("successfully pushed %s\n", f)
  269. }
  270. }(&wg, f)
  271. }
  272. // synchronize (program has to wait for goroutines otherwise output will be lost)
  273. wg.Wait()
  274. // at last, commit changes
  275. commit(config, filenames, placeholder)
  276. }
  277. func commit(config *Configuration, filenames []string, placeholder bool) {
  278. // try to make changes stick (in a git history)
  279. commit_changes := exec.Command(
  280. "ssh",
  281. // specify port
  282. "-p", strconv.Itoa(config.ssh_port),
  283. // specify identity file
  284. "-i", config.ssh_identity_path,
  285. // specify user@host
  286. fmt.Sprintf("%s@%s", config.ssh_user, config.url),
  287. // specify remote command
  288. "/home/strlst/rpi-server-config/commit-updates",
  289. )
  290. // start command and report error (if there is one)
  291. err := commit_changes.Start()
  292. if err != nil {
  293. fmt.Fprintf(os.Stderr, "error starting ssh command: %v\n", err)
  294. return
  295. }
  296. // then call wait to extract exit code (which encodes important information in this case)
  297. err = commit_changes.Wait()
  298. if err != nil {
  299. // big ugly nested ifs to extract an exit code, the go way (tm)
  300. if exit_err, ok := err.(*exec.ExitError); ok {
  301. if status, ok := exit_err.Sys().(syscall.WaitStatus); ok {
  302. if status.ExitStatus() == 9 {
  303. fmt.Printf("nothing to commit\n")
  304. return
  305. }
  306. }
  307. }
  308. // ugly unconditional execution path due to nested ifs
  309. fmt.Fprintf(os.Stderr, "error finalizing changes by committing updates: %v\n", err)
  310. return
  311. } else {
  312. fmt.Printf("committed changes\n")
  313. }
  314. }
  315. // technically the map could be modified at runtime, making for exciting possibilities
  316. type fn func (*Configuration, []string, bool)
  317. var COMMANDS map[string]fn = map[string]fn {
  318. "list": list,
  319. "pull": pull,
  320. "push": push,
  321. "commit": commit,
  322. }
  323. func IsImplemented(command string) bool {
  324. if _, ok := COMMANDS[command]; ok {
  325. return true;
  326. } else {
  327. return false;
  328. }
  329. }
  330. func Exec(command string, config *Configuration, filenames []string, save bool) {
  331. COMMANDS[command](config, filenames, save)
  332. }