/* * ======================== * ===== =============== * ====== ================ * ====== ================ * ====== ==== ==== == * ====== === == = = * ====== === = == = * = === === = == ==== * = === === = == = = * == ===== ==== == * ======================== * * SPDX-License-Identifier: BSD-3-Clause * * Copyright (c) 2023-2024, Joe * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the organization nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ''AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * hardflip: src/i_insert.go * Thu Apr 11 17:52:13 2024 * Joe * * insert a new host */ package main import ( "errors" "os" "path/filepath" "regexp" "strconv" "strings" "github.com/gdamore/tcell/v2" "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/util/uuid" ) func i_insert_format_filename(name, path string) string { str := name if len(name) == 0 { return "" } re := regexp.MustCompile("[^a-zA-Z0-9]+") replace := "_" str = re.ReplaceAllString(str, replace) _, err := os.Stat(path + str + ".yml") base := str i := 0 for err == nil && i < 10000 { uid := uuid.NewUUID() str = base + "_" + string(uid[0:4]) _, err = os.Stat(path + str + ".yml") i++ } str = strings.ToLower(str) + ".yml" return str } func i_insert_abs_files(insert *HostNode) { files := []*string{ &insert.Priv, &insert.Jump.Priv, &insert.RDPFile, } for _, v := range files { if len(*v) > 0 { if (*v)[0] == '~' { home_dir, err := os.UserHomeDir() if err != nil { return } *v = home_dir + (*v)[1:] } *v, _ = filepath.Abs(*v) } } for k, v := range insert.Drive { if len(v) > 0 { if (v)[0] == '~' { home_dir, err := os.UserHomeDir() if err != nil { return } v = home_dir + (v)[1:] } v, _ = filepath.Abs(v) insert.Drive[k] = v } } } func i_insert_default_users(insert *HostNode) { switch insert.Protocol { case PROTOCOL_SSH: if len(insert.User) == 0 { insert.User = "root" } case PROTOCOL_RDP: if len(insert.User) == 0 { insert.User = "Administrator" } default: return } } func i_insert_host(data *HardData, insert *HostNode) { i_insert_abs_files(insert) i_insert_default_users(insert) if len(insert.Drive) == 0 { insert.Drive = nil } filename := i_insert_format_filename(insert.Name, data.data_dir + insert.parent.path()) insert.filename = filename fmt, err := yaml.Marshal(insert) if err != nil { c_error_mode("yaml", err, &data.ui) data.insert = nil return } err = os.WriteFile(data.data_dir + insert.parent.path() + filename, fmt, 0644) if err != nil { c_error_mode("can't write file", err, &data.ui) data.insert = nil return } // HACK: not sure if this is necessary // if data.litems.curr.is_dir() == true { // data.litems.curr.Dirs.lhost.add_back(insert) // } else { // tmp_next := data.litems.curr.Host.next // data.litems.curr.Host.next = insert // data.litems.curr.Host.next.next = tmp_next // } var next *ItemsNode = nil if data.litems.curr != nil { next = data.litems.curr.next } item := &ItemsNode{ 0, nil, insert, data.litems.curr, next, } curr := data.litems.curr if curr != nil { curr.next = item if curr.next.next != nil { data.litems.curr.next.next.prev = item } data.litems.curr = data.litems.curr.next } else { data.litems.add_back(item) data.litems.curr = data.litems.head } data.litems.reset_id() data.ui.mode = NORMAL_MODE data.insert = nil } func i_insert_check_ok(data *HardData, in *HostNode) { if len(in.Name) == 0 { data.insert_err = append(data.insert_err, errors.New("no name")) } if len(in.Host) == 0 { if (in.Protocol == PROTOCOL_RDP && len(in.RDPFile) > 0) == false { data.insert_err = append(data.insert_err, errors.New("no host")) } } if in.Port == 0 { data.insert_err = append(data.insert_err, errors.New("port can't be 0")) } if len(in.Jump.Host) > 0 && in.Jump.Port == 0 { data.insert_err = append(data.insert_err, errors.New("jump port can't be 0")) } var file [2]string switch in.Protocol { case PROTOCOL_SSH: file[0], file[1] = in.Priv, in.Jump.Priv case PROTOCOL_RDP: file[0] = in.RDPFile default: return } for _, v := range file { if len(v) > 0 { if v[0] == '~' { home_dir, err := os.UserHomeDir() if err != nil { return } v = home_dir + v[1:] } if stat, err := os.Stat(v); err != nil { data.insert_err = append(data.insert_err, errors.New(v + ": file does not exist")) } else if stat.IsDir() == true { data.insert_err = append(data.insert_err, errors.New(v + ": file is a directory")) } } } for _, v := range in.Drive { if v[0] == '~' { home_dir, err := os.UserHomeDir() if err != nil { return } v = home_dir + v[1:] } if stat, err := os.Stat(v); err != nil { data.insert_err = append(data.insert_err, errors.New(v + ": path does not exist")) } else if stat.IsDir() == false { data.insert_err = append(data.insert_err, errors.New(v + ": path is not a directory")) } } } func i_draw_tick_box(ui HardUI, line int, dim Quad, label string, content bool, id, selected int) { tbox_style := ui.style[DEF_STYLE].Background(tcell.ColorBlack).Dim(true) if id == selected { tbox_style = tbox_style.Reverse(true).Dim(false) } l := ui.dim[W] / 2 - len(label) - 2 if l <= dim.L { l = dim.L + 1 } i_draw_text(ui.s, l, line, ui.dim[W] / 2, line, ui.style[DEF_STYLE], label) x := " " if content == true { x = "x" } i_draw_text(ui.s, ui.dim[W] / 2, line, dim.R, line, tbox_style, "[" + x + "]") } func i_draw_text_box(ui HardUI, line int, dim Quad, label, content string, id, selected int, red bool) { const tbox_size int = 14 tbox_style := ui.style[DEF_STYLE].Background(tcell.ColorBlack).Dim(true) if id == selected { tbox_style = tbox_style.Reverse(true).Dim(false) } l := ui.dim[W] / 2 - len(label) - 2 if l <= dim.L { l = dim.L + 1 } i_draw_text(ui.s, l, line, ui.dim[W] / 2, line, ui.style[DEF_STYLE], label) if (id == INS_SSH_PASS || id == INS_SSH_JUMP_PASS || id == INS_RDP_PASS) && len(content) > 0 { content = "***" } if red == true { tbox_style = tbox_style.Foreground(tcell.ColorRed) } spaces := "" for i := 0; i < tbox_size; i++ { spaces += " " } i_draw_text(ui.s, ui.dim[W] / 2, line, dim.R, line, tbox_style, "[" + spaces + "]") i_draw_text(ui.s, ui.dim[W] / 2 + 1, line, ui.dim[W] / 2 + 1 + tbox_size, line, tbox_style, content) } func i_draw_ok_butt(ui HardUI, line int, id, selected int) { const butt_size int = 10 const txt string = "ok" style := ui.style[DEF_STYLE].Background(tcell.ColorBlack).Dim(true) if id == selected { style = style.Reverse(true).Dim(false) } buff := "[" for i := 0; i < butt_size / 2 - len(txt); i++ { buff += " " } buff += txt for i := 0; i < butt_size / 2 - len(txt); i++ { buff += " " } buff += "]" i_draw_text(ui.s, (ui.dim[W] / 2) - (butt_size / 2), line, (ui.dim[W] / 2) + (butt_size / 2), line, style, buff) } func i_draw_insert_panel(ui HardUI, in *HostNode) { type draw_insert_func func(ui HardUI, line int, win Quad, in *HostNode) int if len(in.Name) == 0 { return } win := Quad{ ui.dim[W] / 8, ui.dim[H] / 8, ui.dim[W] - ui.dim[W] / 8 - 1, ui.dim[H] - ui.dim[H] / 8 - 1, } i_draw_box(ui.s, win.L, win.T, win.R, win.B, ui.style[BOX_STYLE], ui.style[HEAD_STYLE], " Insert - " + in.Name + " ", true) line := 2 if win.T + line >= win.B { return } i_draw_text_box(ui, win.T + line, win, "Connection type", PROTOCOL_STR[in.Protocol], 0, ui.insert_sel, false) line += 2 var end_line int fp := [PROTOCOL_MAX + 1]draw_insert_func{ i_draw_insert_ssh, i_draw_insert_rdp, i_draw_insert_cmd, i_draw_insert_os, } end_line = fp[in.Protocol](ui, line, win, in) if win.T + end_line >= win.B { ui.s.SetContent(ui.dim[W] / 2, win.B, '▼', nil, ui.style[BOX_STYLE]) // TODO: scroll or something } } func i_draw_insert_ssh(ui HardUI, line int, win Quad, in *HostNode) int { red := false if win.T + line >= win.B { return line } text := "---- Host settings ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Host/IP", in.Host, INS_SSH_HOST, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Port", strconv.Itoa(int(in.Port)), INS_SSH_PORT, ui.insert_sel, false); if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "User", in.User, INS_SSH_USER, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Pass", in.Pass, INS_SSH_PASS, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } if file := in.Priv; len(file) > 0 { if file[0] == '~' { home, _ := os.UserHomeDir() file = home + file[1:] } if stat, err := os.Stat(file); err != nil || stat.IsDir() == true { red = true } } i_draw_text_box(ui, win.T + line, win, "SSH private key", in.Priv, INS_SSH_PRIV, ui.insert_sel, red) if red == true { if line += 1; win.T + line >= win.B { return line } text := "file does not exist" i_draw_text(ui.s, ui.dim[W] / 2, win.T + line, win.R - 1, win.T + line, ui.style[ERR_STYLE], text) } red = false if line += 2; win.T + line >= win.B { return line } text = "---- Jump settings ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Host/IP", in.Jump.Host, INS_SSH_JUMP_HOST, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Port", strconv.Itoa(int(in.Jump.Port)), INS_SSH_JUMP_PORT, ui.insert_sel, false) if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "User", in.Jump.User, INS_SSH_JUMP_USER, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Pass", in.Jump.Pass, INS_SSH_JUMP_PASS, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line} if len(in.Jump.Priv) > 0 { file := in.Jump.Priv if file[0] == '~' { home, _ := os.UserHomeDir() file = home + file[1:] } if stat, err := os.Stat(file); err != nil || stat.IsDir() == true { red = true } } i_draw_text_box(ui, win.T + line, win, "SSH private key", in.Jump.Priv, INS_SSH_JUMP_PRIV, ui.insert_sel, red) if red == true { if line += 1; win.T + line >= win.B { return line } text := "file does not exist" i_draw_text(ui.s, ui.dim[W] / 2, win.T + line, win.R - 1, win.T + line, ui.style[ERR_STYLE], text) } if line += 2; win.T + line >= win.B { return line } i_draw_ok_butt(ui, win.T + line, INS_SSH_OK, ui.insert_sel) return line } func i_draw_insert_rdp(ui HardUI, line int, win Quad, in *HostNode) int { red := false if win.T + line >= win.B { return line } text := "---- Host settings ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Host/IP", in.Host, INS_RDP_HOST, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Port", strconv.Itoa(int(in.Port)), INS_RDP_PORT, ui.insert_sel, false); if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Domain", in.Domain, INS_RDP_DOMAIN, ui.insert_sel, false); if line += 2; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "User", in.User, INS_RDP_USER, ui.insert_sel, false) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Pass", in.Pass, INS_RDP_PASS, ui.insert_sel, false) if line += 2; win.T + line >= win.B { return line } text = "---- RDP File ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } if file := in.RDPFile; len(file) > 0 { if file[0] == '~' { home, _ := os.UserHomeDir() file = home + file[1:] } if stat, err := os.Stat(file); err != nil || stat.IsDir() == true { red = true } } i_draw_text_box(ui, win.T + line, win, "RDP file", in.RDPFile, INS_RDP_FILE, ui.insert_sel, red) if red == true { if line += 1; win.T + line >= win.B { return line } text := "file does not exist" i_draw_text(ui.s, ui.dim[W] / 2, win.T + line, win.R - 1, win.T + line, ui.style[ERR_STYLE], text) } red = false if line += 2; win.T + line >= win.B { return line } text = "---- Window settings ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } screensize := strconv.Itoa(int(in.Width)) + "x" + strconv.Itoa(int(in.Height)) i_draw_text_box(ui, win.T + line, win, "Window size", screensize, INS_RDP_SCREENSIZE, ui.insert_sel, red) if line += 1; win.T + line >= win.B { return line } i_draw_tick_box(ui, win.T + line, win, "Dynamic window", in.Dynamic, INS_RDP_DYNAMIC, ui.insert_sel) if line += 1; win.T + line >= win.B { return line } i_draw_text_box(ui, win.T + line, win, "Quality", RDP_QUALITY[in.Quality], INS_RDP_QUALITY, ui.insert_sel, red) if line += 2; win.T + line >= win.B { return line } text = "---- Share mounts ----" i_draw_text(ui.s, ui.dim[W] / 2 - len(text) / 2, win.T + line, win.R - 1, win.T + line, ui.style[DEF_STYLE], text) if line += 2; win.T + line >= win.B { return line } for k, v := range in.drive_keys { if dir := in.Drive[v]; len(dir) > 0 { if dir[0] == '~' { home, _ := os.UserHomeDir() dir = home + dir[1:] } if stat, err := os.Stat(dir); err != nil || stat.IsDir() == false { red = true } } i_draw_text_box(ui, win.T + line, win, "Share " + strconv.Itoa(k + 1), "(" + v + "): " + in.Drive[v], INS_RDP_DRIVE + k, ui.insert_sel, red) if red == true { if line += 1; win.T + line >= win.B { return line } text := "path is not a directory" i_draw_text(ui.s, ui.dim[W] / 2, win.T + line, win.R - 1, win.T + line, ui.style[ERR_STYLE], text) } if line += 1; win.T + line >= win.B { return line } } i_draw_text_box(ui, win.T + line, win, "Add share", "", INS_RDP_DRIVE + len(in.Drive), ui.insert_sel, false) if line += 2; win.T + line >= win.B { return line } i_draw_ok_butt(ui, win.T + line, INS_RDP_OK + len(in.Drive), ui.insert_sel) return line } func i_draw_insert_os(ui HardUI, line int, win Quad, in *HostNode) int { return 0 } func i_draw_insert_cmd(ui HardUI, line int, win Quad, in *HostNode) int { return 0 }