config.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package config
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "os"
  8. "path/filepath"
  9. "strconv"
  10. "github.com/adrg/xdg"
  11. "github.com/asaskevich/govalidator"
  12. "github.com/claudiodangelis/qrcp/util"
  13. "github.com/manifoldco/promptui"
  14. )
  15. // Config of qrcp
  16. type Config struct {
  17. FQDN string `json:"fqdn"`
  18. Interface string `json:"interface"`
  19. Port int `json:"port"`
  20. KeepAlive bool `json:"keepAlive"`
  21. Path string `json:"path"`
  22. Secure bool `json:"secure"`
  23. TLSKey string `json:"tls-key"`
  24. TLSCert string `json:"tls-cert"`
  25. Output string `json:"output"`
  26. }
  27. var configFile string
  28. // Options of the qrcp configuration
  29. type Options struct {
  30. Interface string
  31. Port int
  32. Path string
  33. FQDN string
  34. KeepAlive bool
  35. Interactive bool
  36. ListAllInterfaces bool
  37. Secure bool
  38. TLSCert string
  39. TLSKey string
  40. Output string
  41. }
  42. func chooseInterface(opts Options) (string, error) {
  43. interfaces, err := util.Interfaces(opts.ListAllInterfaces)
  44. if err != nil {
  45. return "", err
  46. }
  47. if len(interfaces) == 0 {
  48. return "", errors.New("no interfaces found")
  49. }
  50. if len(interfaces) == 1 && !opts.Interactive {
  51. for name := range interfaces {
  52. fmt.Printf("only one interface found: %s, using this one\n", name)
  53. return name, nil
  54. }
  55. }
  56. // Map for pretty printing
  57. m := make(map[string]string)
  58. items := []string{}
  59. for name, ip := range interfaces {
  60. label := fmt.Sprintf("%s (%s)", name, ip)
  61. m[label] = name
  62. items = append(items, label)
  63. }
  64. // Add the "any" interface
  65. anyIP := "0.0.0.0"
  66. anyName := "any"
  67. anyLabel := fmt.Sprintf("%s (%s)", anyName, anyIP)
  68. m[anyLabel] = anyName
  69. items = append(items, anyLabel)
  70. prompt := promptui.Select{
  71. Items: items,
  72. Label: "Choose interface",
  73. }
  74. _, result, err := prompt.Run()
  75. if err != nil {
  76. return "", err
  77. }
  78. return m[result], nil
  79. }
  80. // Load a new configuration
  81. func Load(opts Options) (Config, error) {
  82. var cfg Config
  83. // Read the configuration file, if it exists
  84. if file, err := ioutil.ReadFile(configFile); err == nil {
  85. // Read the config
  86. if err := json.Unmarshal(file, &cfg); err != nil {
  87. return cfg, err
  88. }
  89. }
  90. // Prompt if needed
  91. if cfg.Interface == "" {
  92. iface, err := chooseInterface(opts)
  93. if err != nil {
  94. return cfg, err
  95. }
  96. cfg.Interface = iface
  97. // Write config
  98. if err := write(cfg); err != nil {
  99. return cfg, err
  100. }
  101. }
  102. return cfg, nil
  103. }
  104. // Wizard starts an interactive configuration managements
  105. func Wizard(path string, listAllInterfaces bool) error {
  106. if err := setConfigFile(path); err != nil {
  107. return err
  108. }
  109. var cfg Config
  110. if file, err := ioutil.ReadFile(configFile); err == nil {
  111. // Read the config
  112. if err := json.Unmarshal(file, &cfg); err != nil {
  113. return err
  114. }
  115. }
  116. // Ask for interface
  117. opts := Options{
  118. Interactive: true,
  119. ListAllInterfaces: listAllInterfaces,
  120. }
  121. iface, err := chooseInterface(opts)
  122. if err != nil {
  123. return err
  124. }
  125. cfg.Interface = iface
  126. // Ask for fully qualified domain name
  127. validateFqdn := func(input string) error {
  128. if input != "" && !govalidator.IsDNSName(input) {
  129. return errors.New("invalid domain")
  130. }
  131. return nil
  132. }
  133. promptFqdn := promptui.Prompt{
  134. Validate: validateFqdn,
  135. Label: "Choose fully-qualified domain name",
  136. Default: "",
  137. }
  138. if promptFqdnString, err := promptFqdn.Run(); err == nil {
  139. cfg.FQDN = promptFqdnString
  140. }
  141. // Ask for port
  142. validatePort := func(input string) error {
  143. _, err := strconv.ParseUint(input, 10, 16)
  144. if err != nil {
  145. return errors.New("invalid number")
  146. }
  147. return nil
  148. }
  149. promptPort := promptui.Prompt{
  150. Validate: validatePort,
  151. Label: "Choose port, 0 means random port",
  152. Default: fmt.Sprintf("%d", cfg.Port),
  153. }
  154. if promptPortResultString, err := promptPort.Run(); err == nil {
  155. if port, err := strconv.ParseUint(promptPortResultString, 10, 16); err == nil {
  156. cfg.Port = int(port)
  157. }
  158. }
  159. validateIsDir := func(input string) error {
  160. if input == "" {
  161. return nil
  162. }
  163. path, err := filepath.Abs(input)
  164. if err != nil {
  165. return err
  166. }
  167. f, err := os.Stat(path)
  168. if err != nil {
  169. return err
  170. }
  171. if !f.IsDir() {
  172. return errors.New("path is not a directory")
  173. }
  174. return nil
  175. }
  176. promptOutput := promptui.Prompt{
  177. Label: "Choose default output directory for received files, empty does not set a default",
  178. Default: cfg.Output,
  179. Validate: validateIsDir,
  180. }
  181. if promptOutputResultString, err := promptOutput.Run(); err == nil {
  182. if promptOutputResultString != "" {
  183. p, _ := filepath.Abs(promptOutputResultString)
  184. cfg.Output = p
  185. }
  186. }
  187. // Ask for path
  188. promptPath := promptui.Prompt{
  189. Label: "Choose path, empty means random",
  190. Default: cfg.Path,
  191. }
  192. if promptPathResultString, err := promptPath.Run(); err == nil {
  193. if promptPathResultString != "" {
  194. cfg.Path = promptPathResultString
  195. }
  196. }
  197. // Ask for keep alive
  198. promptKeepAlive := promptui.Select{
  199. Items: []string{"No", "Yes"},
  200. Label: "Should the server keep alive after transferring?",
  201. }
  202. if _, promptKeepAliveResultString, err := promptKeepAlive.Run(); err == nil {
  203. if promptKeepAliveResultString == "Yes" {
  204. cfg.KeepAlive = true
  205. } else {
  206. cfg.KeepAlive = false
  207. }
  208. }
  209. // TLS
  210. promptSecure := promptui.Select{
  211. Items: []string{"No", "Yes"},
  212. Label: "Should files be securely transferred with HTTPS?",
  213. }
  214. if _, promptSecureResultString, err := promptSecure.Run(); err == nil {
  215. if promptSecureResultString == "Yes" {
  216. cfg.Secure = true
  217. } else {
  218. cfg.Secure = false
  219. }
  220. }
  221. pathIsReadable := func(input string) error {
  222. if input == "" {
  223. return nil
  224. }
  225. path, err := filepath.Abs(util.Expand(input))
  226. if err != nil {
  227. return err
  228. }
  229. fmt.Println(path)
  230. fileinfo, err := os.Stat(path)
  231. if err != nil {
  232. return err
  233. }
  234. if fileinfo.Mode().IsDir() {
  235. return fmt.Errorf(fmt.Sprintf("%s is a directory", input))
  236. }
  237. return nil
  238. }
  239. // TLS Cert
  240. promptTLSCert := promptui.Prompt{
  241. Label: "Choose TLS certificate path. Empty if not using HTTPS.",
  242. Default: cfg.TLSCert,
  243. Validate: pathIsReadable,
  244. }
  245. if promptTLSCertString, err := promptTLSCert.Run(); err == nil {
  246. cfg.TLSCert = util.Expand(promptTLSCertString)
  247. }
  248. // TLS key
  249. promptTLSKey := promptui.Prompt{
  250. Label: "Choose TLS certificate key. Empty if not using HTTPS.",
  251. Default: cfg.TLSKey,
  252. Validate: pathIsReadable,
  253. }
  254. if promptTLSKeyString, err := promptTLSKey.Run(); err == nil {
  255. cfg.TLSKey = util.Expand(promptTLSKeyString)
  256. }
  257. // Write it down
  258. if err := write(cfg); err != nil {
  259. return err
  260. }
  261. b, err := json.MarshalIndent(cfg, "", " ")
  262. if err != nil {
  263. return err
  264. }
  265. fmt.Printf("Configuration updated:\n%s\n", string(b))
  266. return nil
  267. }
  268. // write the configuration file to disk
  269. func write(cfg Config) error {
  270. j, err := json.MarshalIndent(cfg, "", " ")
  271. if err != nil {
  272. return err
  273. }
  274. if err := ioutil.WriteFile(configFile, j, 0644); err != nil {
  275. return err
  276. }
  277. return nil
  278. }
  279. func pathExists(path string) bool {
  280. _, err := os.Stat(path)
  281. return !os.IsNotExist(err)
  282. }
  283. func setConfigFile(path string) error {
  284. // If not explicitly set then use the default
  285. if path == "" {
  286. // First try legacy location
  287. var legacyConfigFile = filepath.Join(xdg.Home, ".qrcp.json")
  288. if pathExists(legacyConfigFile) {
  289. configFile = legacyConfigFile
  290. return nil
  291. }
  292. // Else use modern location, first ensuring that the directory
  293. // exists
  294. var configDir = filepath.Join(xdg.ConfigHome, "qrcp")
  295. if !pathExists(configDir) {
  296. if err := os.Mkdir(configDir, 0744); err != nil {
  297. panic(err)
  298. }
  299. }
  300. configFile = filepath.Join(configDir, "config.json")
  301. return nil
  302. }
  303. absolutepath, err := filepath.Abs(path)
  304. if err != nil {
  305. return err
  306. }
  307. fileinfo, err := os.Stat(absolutepath)
  308. if err != nil && !os.IsNotExist(err) {
  309. return err
  310. }
  311. if fileinfo != nil && fileinfo.IsDir() {
  312. return fmt.Errorf("%s is not a file", absolutepath)
  313. }
  314. configFile = absolutepath
  315. return nil
  316. }
  317. // New returns a new configuration struct. It loads defaults, then overrides
  318. // values if any.
  319. func New(path string, opts Options) (Config, error) {
  320. var cfg Config
  321. // Set configFile
  322. if err := setConfigFile(path); err != nil {
  323. return cfg, err
  324. }
  325. // Load saved file / defaults
  326. cfg, err := Load(opts)
  327. if err != nil {
  328. return cfg, err
  329. }
  330. if opts.Interface != "" {
  331. cfg.Interface = opts.Interface
  332. }
  333. if opts.FQDN != "" {
  334. if !govalidator.IsDNSName(opts.FQDN) {
  335. return cfg, errors.New("invalid value for fully-qualified domain name")
  336. }
  337. cfg.FQDN = opts.FQDN
  338. }
  339. if opts.Port != 0 {
  340. cfg.Port = opts.Port
  341. } else if portVal, ok := os.LookupEnv("QRCP_PORT"); ok {
  342. port, err := strconv.Atoi(portVal)
  343. if err != nil {
  344. return cfg, errors.New("could not parse port from environment variable QRCP_PORT")
  345. }
  346. cfg.Port = port
  347. }
  348. if cfg.Port != 0 && !govalidator.IsPort(fmt.Sprintf("%d", cfg.Port)) {
  349. return cfg, fmt.Errorf("%d is not a valid port", cfg.Port)
  350. }
  351. if opts.KeepAlive {
  352. cfg.KeepAlive = true
  353. }
  354. if opts.Path != "" {
  355. cfg.Path = opts.Path
  356. }
  357. if opts.Secure {
  358. cfg.Secure = true
  359. }
  360. if opts.TLSCert != "" {
  361. cfg.TLSCert = opts.TLSCert
  362. }
  363. if opts.TLSKey != "" {
  364. cfg.TLSKey = opts.TLSKey
  365. }
  366. if opts.Output != "" {
  367. cfg.Output = opts.Output
  368. }
  369. return cfg, nil
  370. }