├── cmd1.txt.example ├── cmd2.txt.example ├── host.txt.example ├── .gitignore ├── ip.txt.example ├── go.mod ├── g ├── const.go └── cfg.go ├── ssh.json.example ├── go.sum ├── funcs ├── ssh_test.go └── sshconnect.go ├── main.go ├── README.MD └── LICENSE /cmd1.txt.example: -------------------------------------------------------------------------------- 1 | show clock -------------------------------------------------------------------------------- /cmd2.txt.example: -------------------------------------------------------------------------------- 1 | cd /opt 2 | sleep 2 3 | ls -------------------------------------------------------------------------------- /host.txt.example: -------------------------------------------------------------------------------- 1 | 192.168.31.21 2 | 192.168.15.102 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.json 3 | *.exe 4 | *.key 5 | *.zip -------------------------------------------------------------------------------- /ip.txt.example: -------------------------------------------------------------------------------- 1 | 192.168.15.101-192.168.15.110 2 | 192.168.31.21-192.168.31.22 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shanghai-edu/multissh 2 | 3 | go 1.17 4 | 5 | require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 6 | 7 | require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 8 | -------------------------------------------------------------------------------- /g/const.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | // changelog: 4 | // 0.1 fisrt version 5 | // 0.1.2 fix ssh error on h3c switch 6 | // 0.2 7 | // 0.2.1 8 | // add write locate file 9 | // json Unmarshal with error 10 | // 0.2.3 11 | const ( 12 | VERSION = "0.4.0" 13 | ) 14 | -------------------------------------------------------------------------------- /ssh.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "SshHosts": [{ 3 | "Host": "192.168.31.51", 4 | "Port": 22, 5 | "Username": "admin", 6 | "Password": "admin", 7 | "cmds": "show clock;show clock" 8 | }, 9 | { 10 | "Host": "192.168.80.131", 11 | "Port": 22, 12 | "Username": "root", 13 | "Password": "", 14 | "key": "./server.key", 15 | "linuxMode": true, 16 | "CmdFile": "cmd2.txt.example" 17 | } 18 | ], 19 | "Global": { 20 | "Ciphers": "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc", 21 | "KeyExchanges": "diffie-hellman-group1-sha1,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1" 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 2 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 3 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 4 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 6 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 8 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 9 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 10 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 11 | -------------------------------------------------------------------------------- /funcs/ssh_test.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "bytes" 5 | // "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | username = "root" 12 | password = "" 13 | ip = "192.168.80.131" 14 | port = 22 15 | cmd = "cd /opt;pwd;exit" 16 | key = "../server.key" 17 | ) 18 | 19 | // Tests the SSH functionality of the package. 20 | // 21 | // It requires manual input of the local SSH private key path into the key 22 | // variable, and the remote address into the ip variable. 23 | func Test_SSH(t *testing.T) { 24 | var cipherList []string 25 | session, err := connect(username, password, ip, key, port, cipherList, nil) 26 | if err != nil { 27 | t.Error(err) 28 | return 29 | } 30 | defer session.Close() 31 | 32 | cmdlist := strings.Split(cmd, ";") 33 | stdinBuf, err := session.StdinPipe() 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | 39 | var outbt, errbt bytes.Buffer 40 | session.Stdout = &outbt 41 | 42 | session.Stderr = &errbt 43 | err = session.Shell() 44 | if err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | for _, c := range cmdlist { 49 | c = c + "\n" 50 | stdinBuf.Write([]byte(c)) 51 | 52 | } 53 | session.Wait() 54 | t.Log((outbt.String() + errbt.String())) 55 | return 56 | } 57 | 58 | /* 59 | func Test_SSH_run(t *testing.T) { 60 | var cipherList []string 61 | session, err := connect(username, password, ip, key, port, cipherList) 62 | if err != nil { 63 | t.Error(err) 64 | return 65 | } 66 | defer session.Close() 67 | 68 | //cmdlist := strings.Split(cmd, ";") 69 | //newcmd := strings.Join(cmdlist, "&&") 70 | var outbt, errbt bytes.Buffer 71 | session.Stdout = &outbt 72 | 73 | session.Stderr = &errbt 74 | err = session.Run(cmd) 75 | if err != nil { 76 | t.Error(err) 77 | return 78 | } 79 | t.Log((outbt.String() + errbt.String())) 80 | 81 | return 82 | } 83 | */ 84 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "time" 11 | 12 | "github.com/shanghai-edu/multissh/funcs" 13 | "github.com/shanghai-edu/multissh/g" 14 | ) 15 | 16 | func main() { 17 | version := flag.Bool("v", false, "show version") 18 | hosts := flag.String("hosts", "", "host address list") 19 | ips := flag.String("ips", "", "ip address list") 20 | cmds := flag.String("cmds", "", "cmds") 21 | username := flag.String("u", "", "username") 22 | password := flag.String("p", "", "password") 23 | key := flag.String("k", "", "ssh private key") 24 | port := flag.Int("port", 22, "ssh port") 25 | ciphers := flag.String("ciphers", "", "ciphers") 26 | keyExchanges := flag.String("keyexchanges", "", "keyexchanges") 27 | cmdFile := flag.String("cmdfile", "", "cmdfile path") 28 | hostFile := flag.String("hostfile", "", "hostfile path") 29 | ipFile := flag.String("ipfile", "", "ipfile path") 30 | cfgFile := flag.String("c", "", "cfg File Path") 31 | jsonMode := flag.Bool("j", false, "print output in json format") 32 | outTxt := flag.Bool("outTxt", false, "write result into txt") 33 | fileLocate := flag.String("f", "", "write file locate") 34 | linuxMode := flag.Bool("l", false, "In linux mode,multi command combine with && ,such as date&&cd /opt&&ls") 35 | timeLimit := flag.Int("t", 30, "max timeout") 36 | numLimit := flag.Int("n", 20, "max execute number") 37 | 38 | flag.Parse() 39 | 40 | var cmdList, hostList, cipherList, keyExchangeList []string 41 | var err error 42 | 43 | sshHosts := []g.SSHHost{} 44 | var host_Struct g.SSHHost 45 | 46 | if *version { 47 | fmt.Println(g.VERSION) 48 | os.Exit(0) 49 | } 50 | 51 | if *ipFile != "" { 52 | hostList, err = g.GetIpListFromFile(*ipFile) 53 | if err != nil { 54 | log.Println("load iplist error: ", err) 55 | return 56 | } 57 | } 58 | 59 | if *hostFile != "" { 60 | hostList, err = g.Getfile(*hostFile) 61 | if err != nil { 62 | log.Println("load hostfile error: ", err) 63 | return 64 | } 65 | } 66 | if *ips != "" { 67 | hostList, err = g.GetIpList(*ips) 68 | if err != nil { 69 | log.Println("load iplist error: ", err) 70 | return 71 | } 72 | } 73 | 74 | if *hosts != "" { 75 | hostList = g.SplitString(*hosts) 76 | } 77 | 78 | if *cmdFile != "" { 79 | cmdList, err = g.Getfile(*cmdFile) 80 | if err != nil { 81 | log.Println("load cmdfile error: ", err) 82 | return 83 | } 84 | } 85 | if *cmds != "" { 86 | cmdList = g.SplitString(*cmds) 87 | 88 | } 89 | if *ciphers != "" { 90 | cipherList = g.SplitString(*ciphers) 91 | } 92 | if *keyExchanges != "" { 93 | keyExchangeList = g.SplitString(*keyExchanges) 94 | } 95 | if *cfgFile == "" { 96 | for _, host := range hostList { 97 | host_Struct.Host = host 98 | host_Struct.Username = *username 99 | host_Struct.Password = *password 100 | host_Struct.Port = *port 101 | host_Struct.CmdList = cmdList 102 | host_Struct.Key = *key 103 | host_Struct.LinuxMode = *linuxMode 104 | sshHosts = append(sshHosts, host_Struct) 105 | } 106 | } else { 107 | sshHostConfig, err := g.GetJsonFile(*cfgFile) 108 | if err != nil { 109 | log.Println("load cfgFile error: ", err) 110 | return 111 | } 112 | cipherList = g.SplitString(sshHostConfig.Global.Ciphers) 113 | keyExchangeList = g.SplitString(sshHostConfig.Global.KeyExchanges) 114 | sshHosts = sshHostConfig.SshHosts 115 | for i := 0; i < len(sshHosts); i++ { 116 | if sshHosts[i].Cmds != "" { 117 | sshHosts[i].CmdList = g.SplitString(sshHosts[i].Cmds) 118 | } else { 119 | cmdList, err = g.Getfile(sshHosts[i].CmdFile) 120 | if err != nil { 121 | log.Println("load cmdFile error: ", err) 122 | return 123 | } 124 | sshHosts[i].CmdList = cmdList 125 | } 126 | } 127 | } 128 | 129 | chLimit := make(chan bool, *numLimit) //控制并发访问量 130 | chs := make([]chan g.SSHResult, len(sshHosts)) 131 | startTime := time.Now() 132 | log.Println("Multissh start") 133 | limitFunc := func(chLimit chan bool, ch chan g.SSHResult, host g.SSHHost) { 134 | funcs.Dossh(host.Username, host.Password, host.Host, host.Key, host.CmdList, host.Port, *timeLimit, cipherList, keyExchangeList, host.LinuxMode, ch) 135 | <-chLimit 136 | } 137 | for i, host := range sshHosts { 138 | chs[i] = make(chan g.SSHResult, 1) 139 | chLimit <- true 140 | go limitFunc(chLimit, chs[i], host) 141 | } 142 | sshResults := []g.SSHResult{} 143 | for _, ch := range chs { 144 | res := <-ch 145 | if res.Result != "" { 146 | sshResults = append(sshResults, res) 147 | } 148 | } 149 | endTime := time.Now() 150 | log.Printf("Multissh finished. Process time %s. Number of active ip is %d", endTime.Sub(startTime), len(sshHosts)) 151 | //gu 152 | if *outTxt { 153 | for _, sshResult := range sshResults { 154 | err = g.WriteIntoTxt(sshResult, *fileLocate) 155 | if err != nil { 156 | log.Println("write into txt error: ", err) 157 | return 158 | } 159 | } 160 | return 161 | } 162 | if *jsonMode { 163 | jsonResult, err := json.Marshal(sshResults) 164 | if err != nil { 165 | log.Println("json Marshal error: ", err) 166 | } 167 | fmt.Println(string(jsonResult)) 168 | return 169 | } 170 | for _, sshResult := range sshResults { 171 | fmt.Println("host: ", sshResult.Host) 172 | fmt.Println("========= Result =========") 173 | fmt.Println(sshResult.Result) 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /funcs/sshconnect.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "net" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/shanghai-edu/multissh/g" 14 | "golang.org/x/crypto/ssh" 15 | ) 16 | 17 | func connect(user, password, host, key string, port int, cipherList, keyExchangeList []string) (*ssh.Session, error) { 18 | var ( 19 | auth []ssh.AuthMethod 20 | addr string 21 | clientConfig *ssh.ClientConfig 22 | client *ssh.Client 23 | config ssh.Config 24 | session *ssh.Session 25 | err error 26 | ) 27 | // get auth method 28 | auth = make([]ssh.AuthMethod, 0) 29 | if key == "" { 30 | auth = append(auth, ssh.Password(password)) 31 | } else { 32 | pemBytes, err := ioutil.ReadFile(key) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var signer ssh.Signer 38 | if password == "" { 39 | signer, err = ssh.ParsePrivateKey(pemBytes) 40 | } else { 41 | signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password)) 42 | } 43 | if err != nil { 44 | return nil, err 45 | } 46 | auth = append(auth, ssh.PublicKeys(signer)) 47 | } 48 | if len(cipherList) == 0 { 49 | config.Ciphers = []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"} 50 | } else { 51 | config.Ciphers = cipherList 52 | } 53 | 54 | if len(keyExchangeList) == 0 { 55 | config.KeyExchanges = []string{"diffie-hellman-group-exchange-sha1", "diffie-hellman-group1-sha1", "diffie-hellman-group-exchange-sha256"} 56 | } else { 57 | config.KeyExchanges = keyExchangeList 58 | } 59 | 60 | clientConfig = &ssh.ClientConfig{ 61 | User: user, 62 | Auth: auth, 63 | Timeout: 30 * time.Second, 64 | Config: config, 65 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 66 | return nil 67 | }, 68 | } 69 | 70 | // connet to ssh 71 | addr = fmt.Sprintf("%s:%d", host, port) 72 | 73 | if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil { 74 | return nil, err 75 | } 76 | 77 | // create session 78 | if session, err = client.NewSession(); err != nil { 79 | return nil, err 80 | } 81 | 82 | modes := ssh.TerminalModes{ 83 | ssh.ECHO: 0, // disable echoing 84 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 85 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 86 | } 87 | 88 | if err := session.RequestPty("xterm", 80, 40, modes); err != nil { 89 | return nil, err 90 | } 91 | 92 | return session, nil 93 | } 94 | 95 | func Dossh(username, password, host, key string, cmdlist []string, port, timeout int, cipherList, keyExchangeList []string, linuxMode bool, ch chan g.SSHResult) { 96 | chSSH := make(chan g.SSHResult) 97 | if linuxMode { 98 | go dossh_run(username, password, host, key, cmdlist, port, cipherList, keyExchangeList, chSSH) 99 | } else { 100 | go dossh_session(username, password, host, key, cmdlist, port, cipherList, keyExchangeList, chSSH) 101 | } 102 | var res g.SSHResult 103 | 104 | select { 105 | case <-time.After(time.Duration(timeout) * time.Second): 106 | res.Host = host 107 | res.Success = false 108 | res.Result = ("SSH run timeout:" + strconv.Itoa(timeout) + " second.") 109 | ch <- res 110 | case res = <-chSSH: 111 | ch <- res 112 | } 113 | return 114 | } 115 | 116 | func dossh_session(username, password, host, key string, cmdlist []string, port int, cipherList, keyExchangeList []string, ch chan g.SSHResult) { 117 | session, err := connect(username, password, host, key, port, cipherList, keyExchangeList) 118 | var sshResult g.SSHResult 119 | sshResult.Host = host 120 | 121 | if err != nil { 122 | sshResult.Success = false 123 | sshResult.Result = fmt.Sprintf("<%s>", err.Error()) 124 | ch <- sshResult 125 | return 126 | } 127 | defer session.Close() 128 | 129 | cmdlist = append(cmdlist, "exit") 130 | 131 | stdinBuf, _ := session.StdinPipe() 132 | 133 | var outbt, errbt bytes.Buffer 134 | session.Stdout = &outbt 135 | 136 | session.Stderr = &errbt 137 | err = session.Shell() 138 | if err != nil { 139 | sshResult.Success = false 140 | sshResult.Result = fmt.Sprintf("<%s>", err.Error()) 141 | ch <- sshResult 142 | return 143 | } 144 | for _, c := range cmdlist { 145 | c = c + "\n" 146 | stdinBuf.Write([]byte(c)) 147 | } 148 | session.Wait() 149 | if errbt.String() != "" { 150 | sshResult.Success = false 151 | sshResult.Result = errbt.String() 152 | ch <- sshResult 153 | } else { 154 | sshResult.Success = true 155 | sshResult.Result = outbt.String() 156 | ch <- sshResult 157 | } 158 | 159 | return 160 | } 161 | 162 | func dossh_run(username, password, host, key string, cmdlist []string, port int, cipherList, keyExchangeList []string, ch chan g.SSHResult) { 163 | session, err := connect(username, password, host, key, port, cipherList, keyExchangeList) 164 | var sshResult g.SSHResult 165 | sshResult.Host = host 166 | 167 | if err != nil { 168 | sshResult.Success = false 169 | sshResult.Result = fmt.Sprintf("<%s>", err.Error()) 170 | ch <- sshResult 171 | return 172 | } 173 | defer session.Close() 174 | 175 | cmdlist = append(cmdlist, "exit") 176 | newcmd := strings.Join(cmdlist, "&&") 177 | 178 | var outbt, errbt bytes.Buffer 179 | session.Stdout = &outbt 180 | 181 | session.Stderr = &errbt 182 | err = session.Run(newcmd) 183 | if err != nil { 184 | sshResult.Success = false 185 | sshResult.Result = fmt.Sprintf("<%s>", err.Error()) 186 | ch <- sshResult 187 | return 188 | } 189 | 190 | if errbt.String() != "" { 191 | sshResult.Success = false 192 | sshResult.Result = errbt.String() 193 | ch <- sshResult 194 | } else { 195 | sshResult.Success = true 196 | sshResult.Result = outbt.String() 197 | ch <- sshResult 198 | } 199 | 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /g/cfg.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "encoding/json" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type SSHHost struct { 16 | Host string 17 | Port int 18 | Username string 19 | Password string 20 | CmdFile string 21 | Cmds string 22 | CmdList []string 23 | Key string 24 | LinuxMode bool 25 | Result SSHResult 26 | } 27 | 28 | type HostJson struct { 29 | SshHosts []SSHHost 30 | Global GlobalConfig 31 | } 32 | 33 | type GlobalConfig struct { 34 | Ciphers string 35 | KeyExchanges string 36 | } 37 | 38 | type SSHResult struct { 39 | Host string 40 | Success bool 41 | Result string 42 | } 43 | 44 | func SplitString(str string) (strList []string) { 45 | if str == "" { 46 | return 47 | } 48 | if strings.Contains(str, ",") { 49 | strList = strings.Split(str, ",") 50 | } else { 51 | strList = strings.Split(str, ";") 52 | } 53 | return 54 | } 55 | 56 | func GetfileAll(filePath string) ([]byte, error) { 57 | result, err := ioutil.ReadFile(filePath) 58 | if err != nil { 59 | log.Println("read file ", filePath, err) 60 | return result, err 61 | } 62 | return result, nil 63 | } 64 | 65 | func Getfile(filePath string) ([]string, error) { 66 | result := []string{} 67 | b, err := ioutil.ReadFile(filePath) 68 | if err != nil { 69 | log.Println("read file ", filePath, err) 70 | return result, err 71 | } 72 | s := string(b) 73 | for _, lineStr := range strings.Split(s, "\n") { 74 | lineStr = strings.TrimSpace(lineStr) 75 | if lineStr == "" { 76 | continue 77 | } 78 | result = append(result, lineStr) 79 | } 80 | return result, nil 81 | } 82 | 83 | //gu 84 | func GetJsonFile(filePath string) (HostJson, error) { 85 | var result HostJson 86 | b, err := ioutil.ReadFile(filePath) 87 | if err != nil { 88 | log.Println("read file ", filePath, err) 89 | return result, err 90 | } 91 | err = json.Unmarshal(b, &result) 92 | if err != nil { 93 | log.Println("read file ", filePath, err) 94 | return result, err 95 | } 96 | return result, nil 97 | } 98 | func WriteIntoTxt(sshResult SSHResult, locate string) error { 99 | outputFile, outputError := os.OpenFile(locate+sshResult.Host+".txt", os.O_WRONLY|os.O_CREATE, 0666) 100 | if outputError != nil { 101 | return outputError 102 | } 103 | defer outputFile.Close() 104 | 105 | outputWriter := bufio.NewWriter(outputFile) 106 | //var outputString string 107 | 108 | outputString := sshResult.Result 109 | outputWriter.WriteString(outputString) 110 | outputWriter.Flush() 111 | return nil 112 | } 113 | 114 | func GetIpList(ipString string) ([]string, error) { 115 | res := SplitString(ipString) 116 | var allIp []string 117 | if len(res) > 0 { 118 | for _, sip := range res { 119 | aip := ParseIp(sip) 120 | for _, ip := range aip { 121 | allIp = append(allIp, ip) 122 | } 123 | } 124 | } 125 | return allIp, nil 126 | } 127 | 128 | func GetIpListFromFile(filePath string) ([]string, error) { 129 | res, err := Getfile(filePath) 130 | if err != nil { 131 | return nil, nil 132 | } 133 | var allIp []string 134 | if len(res) > 0 { 135 | for _, sip := range res { 136 | aip := ParseIp(sip) 137 | for _, ip := range aip { 138 | allIp = append(allIp, ip) 139 | } 140 | } 141 | } 142 | return allIp, nil 143 | } 144 | 145 | func ParseIp(ip string) []string { 146 | var availableIPs []string 147 | // if ip is "1.1.1.1/",trim / 148 | ip = strings.TrimRight(ip, "/") 149 | if strings.Contains(ip, "/") == true { 150 | if strings.Contains(ip, "/32") == true { 151 | aip := strings.Replace(ip, "/32", "", -1) 152 | availableIPs = append(availableIPs, aip) 153 | } else { 154 | availableIPs = GetAvailableIP(ip) 155 | } 156 | } else if strings.Contains(ip, "-") == true { 157 | ipRange := strings.SplitN(ip, "-", 2) 158 | availableIPs = GetAvailableIPRange(ipRange[0], ipRange[1]) 159 | } else { 160 | availableIPs = append(availableIPs, ip) 161 | } 162 | return availableIPs 163 | } 164 | 165 | func GetAvailableIPRange(ipStart, ipEnd string) []string { 166 | var availableIPs []string 167 | 168 | firstIP := net.ParseIP(ipStart) 169 | endIP := net.ParseIP(ipEnd) 170 | if firstIP.To4() == nil || endIP.To4() == nil { 171 | return availableIPs 172 | } 173 | firstIPNum := ipToInt(firstIP.To4()) 174 | EndIPNum := ipToInt(endIP.To4()) 175 | pos := int32(1) 176 | 177 | newNum := firstIPNum 178 | 179 | for newNum <= EndIPNum { 180 | availableIPs = append(availableIPs, intToIP(newNum).String()) 181 | newNum = newNum + pos 182 | } 183 | return availableIPs 184 | } 185 | 186 | func GetAvailableIP(ipAndMask string) []string { 187 | var availableIPs []string 188 | 189 | ipAndMask = strings.TrimSpace(ipAndMask) 190 | ipAndMask = IPAddressToCIDR(ipAndMask) 191 | _, ipnet, _ := net.ParseCIDR(ipAndMask) 192 | 193 | firstIP, _ := networkRange(ipnet) 194 | ipNum := ipToInt(firstIP) 195 | size := networkSize(ipnet.Mask) 196 | pos := int32(1) 197 | max := size - 2 // -1 for the broadcast address, -1 for the gateway address 198 | 199 | var newNum int32 200 | for attempt := int32(0); attempt < max; attempt++ { 201 | newNum = ipNum + pos 202 | pos = pos%max + 1 203 | availableIPs = append(availableIPs, intToIP(newNum).String()) 204 | } 205 | return availableIPs 206 | } 207 | 208 | func IPAddressToCIDR(ipAdress string) string { 209 | if strings.Contains(ipAdress, "/") == true { 210 | ipAndMask := strings.Split(ipAdress, "/") 211 | ip := ipAndMask[0] 212 | mask := ipAndMask[1] 213 | if strings.Contains(mask, ".") == true { 214 | mask = IPMaskStringToCIDR(mask) 215 | } 216 | return ip + "/" + mask 217 | } else { 218 | return ipAdress 219 | } 220 | } 221 | 222 | func IPMaskStringToCIDR(netmask string) string { 223 | netmaskList := strings.Split(netmask, ".") 224 | var mint []int 225 | for _, v := range netmaskList { 226 | strv, _ := strconv.Atoi(v) 227 | mint = append(mint, strv) 228 | } 229 | myIPMask := net.IPv4Mask(byte(mint[0]), byte(mint[1]), byte(mint[2]), byte(mint[3])) 230 | ones, _ := myIPMask.Size() 231 | return strconv.Itoa(ones) 232 | } 233 | 234 | func IPMaskCIDRToString(one string) string { 235 | oneInt, _ := strconv.Atoi(one) 236 | mIPmask := net.CIDRMask(oneInt, 32) 237 | var maskstring []string 238 | for _, v := range mIPmask { 239 | maskstring = append(maskstring, strconv.Itoa(int(v))) 240 | } 241 | return strings.Join(maskstring, ".") 242 | } 243 | 244 | // Calculates the first and last IP addresses in an IPNet 245 | func networkRange(network *net.IPNet) (net.IP, net.IP) { 246 | netIP := network.IP.To4() 247 | firstIP := netIP.Mask(network.Mask) 248 | lastIP := net.IPv4(0, 0, 0, 0).To4() 249 | for i := 0; i < len(lastIP); i++ { 250 | lastIP[i] = netIP[i] | ^network.Mask[i] 251 | } 252 | return firstIP, lastIP 253 | } 254 | 255 | // Given a netmask, calculates the number of available hosts 256 | func networkSize(mask net.IPMask) int32 { 257 | m := net.IPv4Mask(0, 0, 0, 0) 258 | for i := 0; i < net.IPv4len; i++ { 259 | m[i] = ^mask[i] 260 | } 261 | return int32(binary.BigEndian.Uint32(m)) + 1 262 | } 263 | 264 | // Converts a 4 bytes IP into a 32 bit integer 265 | func ipToInt(ip net.IP) int32 { 266 | return int32(binary.BigEndian.Uint32(ip.To4())) 267 | } 268 | 269 | // Converts 32 bit integer into a 4 bytes IP address 270 | func intToIP(n int32) net.IP { 271 | b := make([]byte, 4) 272 | binary.BigEndian.PutUint32(b, uint32(n)) 273 | return net.IP(b) 274 | } 275 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## multissh 2 | 3 | 一个简单的并行 SSH 工具,可以批量的对主机通过 SSH 执行命令组合。 4 | 5 | 支持: 6 | - 并发执行 7 | - 单次执行多条命令 8 | - ip 地址段自动匹配主机(192.168.0.1-192.168.0.100) 9 | - ssh 用户名/密码认证 10 | - ssh key 认证 11 | - json 格式输出 12 | - 输出到文本,文件名为 host.txt 13 | 14 | #### 编译 15 | ``` 16 | go get ./... 17 | go build 18 | ``` 19 | 20 | #### release 21 | 可以直接下载编译好的 release 版本 22 | 23 | 提供 win64 和 linux64 两个平台的可执行文件 24 | 25 | https://github.com/shanghai-edu/multissh/releases/ 26 | 27 | #### 命令体系 28 | ``` 29 | # ./multissh -h 30 | Usage of ./multissh: 31 | -c string 32 | cfg File Path 33 | -ciphers string 34 | ciphers 35 | -cmdfile string 36 | cmdfile path 37 | -cmds string 38 | cmds 39 | -f string 40 | write file locate 41 | -hostfile string 42 | hostfile path 43 | -hosts string 44 | host address list 45 | -ipfile string 46 | ipfile path 47 | -ips string 48 | ip address list 49 | -j print output in json format 50 | -k string 51 | ssh private key 52 | -keyexchanges string 53 | keyexchanges 54 | -l In linux mode,multi command combine with && ,such as date&&cd /opt&&ls 55 | -n int 56 | max execute number (default 20) 57 | -outTxt 58 | write result into txt 59 | -p string 60 | password 61 | -port int 62 | ssh port (default 22) 63 | -t int 64 | max timeout (default 30) 65 | -u string 66 | username 67 | -v show version 68 | ``` 69 | **cmdfile 示例** 70 | ``` 71 | show clock 72 | ``` 73 | **hostfile 示例** 74 | ``` 75 | 192.168.31.21 76 | 192.168.15.102 77 | ``` 78 | **ipfile 示例** 79 | ``` 80 | 192.168.15.101-192.168.15.103 81 | 192.168.31.21-192.168.31.22 82 | ``` 83 | 84 | **ssh.json 示例** 85 | ``` 86 | { 87 | "SshHosts": [{ 88 | "Host": "192.168.31.51", 89 | "Port": 22, 90 | "Username": "admin", 91 | "Password": "admin", 92 | "cmds": "show clock;show clock" 93 | }, 94 | { 95 | "Host": "192.168.80.131", 96 | "Port": 22, 97 | "Username": "root", 98 | "Password": "", 99 | "key": "./server.key", 100 | "linuxMode": true, 101 | "CmdFile": "cmd2.txt.example" 102 | } 103 | ], 104 | "Global": { 105 | "Ciphers": "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc", 106 | "KeyExchanges": "diffie-hellman-group1-sha1,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1" 107 | } 108 | 109 | } 110 | ``` 111 | 112 | ## 用法 113 | #### cmd string & host string 114 | ``` 115 | # ./multissh -cmds "show clock" -hosts "192.168.31.21;192.168.15.102" -u admin -p password 116 | 2018/01/17 14:01:28 Multissh start 117 | 2018/01/17 14:01:31 Multissh finished. Process time 2.867808673s. Number of active ip is 2 118 | host: 192.168.31.21 119 | ========= Result ========= 120 | 121 | ****************************************************************************** 122 | * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved. * 123 | * Without the owner's prior written consent, * 124 | * no decompiling or reverse-engineering shall be allowed. * 125 | ****************************************************************************** 126 | 127 | show clock 128 | 14:01:31 CN Wed 01/17/2018 129 | Time Zone : CN add 08:00:00 130 | exit 131 | 132 | host: 192.168.15.102 133 | ========= Result ========= 134 | 135 | sw-cisco#show clock 136 | 05:50:24.935 UTC Wed Jan 17 2018 137 | sw-cisco#exit 138 | 139 | ``` 140 | 141 | #### cmdfile & hostfile 142 | ``` 143 | # ./multissh -cmdfile cmd1.txt.example -hostfile host.txt.example -u admin -p password 144 | 2018/01/17 14:01:28 Multissh start 145 | 2018/01/17 14:01:31 Multissh finished. Process time 2.867808673s. Number of active ip is 2 146 | host: 192.168.31.21 147 | ========= Result ========= 148 | 149 | ****************************************************************************** 150 | * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved. * 151 | * Without the owner's prior written consent, * 152 | * no decompiling or reverse-engineering shall be allowed. * 153 | ****************************************************************************** 154 | 155 | show clock 156 | 14:01:31 CN Wed 01/17/2018 157 | Time Zone : CN add 08:00:00 158 | exit 159 | 160 | host: 192.168.15.102 161 | ========= Result ========= 162 | 163 | sw-cisco#show clock 164 | 05:50:24.935 UTC Wed Jan 17 2018 165 | sw-cisco#exit 166 | 167 | ``` 168 | 169 | #### ipfile 170 | ``` 171 | # ./multissh -cmdfile cmd1.txt.example -ipfile ip.txt.example -u admin -p password 172 | 2018/01/17 14:25:26 Multissh start 173 | 2018/01/17 14:25:29 Multissh finished. Process time 2.847347642s. Number of active ip is 5 174 | host: 192.168.15.101 175 | ========= Result ========= 176 | 177 | sw-cisco-1#show clock 178 | 06:17:49.422 UTC Wed Jan 17 2018 179 | sw-cisco-1#exit 180 | 181 | host: 192.168.15.102 182 | ========= Result ========= 183 | sw-cisco-2#show clock 184 | 06:14:22.445 UTC Wed Jan 17 2018 185 | sw-cisco-2#exit 186 | 187 | host: 192.168.15.103 188 | ========= Result ========= 189 | sw-cisco-3#show clock 190 | 06:19:14.487 UTC Wed Jan 17 2018 191 | sw-cisco-3#exit 192 | 193 | host: 192.168.31.21 194 | ========= Result ========= 195 | 196 | ****************************************************************************** 197 | * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved. * 198 | * Without the owner's prior written consent, * 199 | * no decompiling or reverse-engineering shall be allowed. * 200 | ****************************************************************************** 201 | 202 | show clock 203 | 14:25:29 CN Wed 01/17/2018 204 | Time Zone : CN add 08:00:00 205 | exit 206 | 207 | host: 192.168.31.22 208 | ========= Result ========= 209 | 210 | sw-cisco-4#show clock 211 | 14:25:27.639 beijing Wed Jan 17 2018 212 | sw-cisco-4#exit 213 | ``` 214 | #### ssh key-based Auth and linuxMode 215 | ``` 216 | # ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key" 217 | 2018/01/17 14:33:55 Multissh start 218 | 2018/01/17 14:33:56 Multissh finished. Process time 960.367764ms. Number of active ip is 1 219 | host: 192.168.80.131 220 | ========= Result ========= 221 | Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-98-generic x86_64) 222 | 223 | * Documentation: https://help.ubuntu.com 224 | * Management: https://landscape.canonical.com 225 | * Support: https://ubuntu.com/advantage 226 | 227 | System information as of Wed Jan 17 14:33:55 CST 2018 228 | 229 | System load: 0.0 Processes: 335 230 | Usage of /: 10.0% of 90.18GB Users logged in: 0 231 | Memory usage: 2% IP address for eth0: 192.168.80.131 232 | Swap usage: 0% IP address for docker0: 172.17.0.1 233 | 234 | Graph this data and manage this system at: 235 | https://landscape.canonical.com/ 236 | 237 | 0 个可升级软件包。 238 | 0 个安全更新。 239 | 240 | New release '17.10' available. 241 | Run 'do-release-upgrade' to upgrade to it. 242 | 243 | You have new mail. 244 | Last login: Wed Jan 17 14:29:39 2018 from 202.120.80.201 245 | root@ubuntu-docker-node3:~# 201817:33:56 CST 246 | root@ubuntu-docker-node3:~# root@ubuntu-docker-node3:/opt# cisco 247 | composer.json 248 | composer.phar 249 | example-oauth2-server 250 | getting-started-with-mmdb 251 | gitlab 252 | gitlab-ce_8.0.4-ce.1_amd64.deb 253 | oauth2-demo-php 254 | oauth2-server-php 255 | python_test 256 | rsyslog-maxminddb 257 | root@ubuntu-docker-node3:/opt# 注销 258 | 259 | # ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key" -l 260 | 2018/01/17 14:34:02 Multissh start 261 | 2018/01/17 14:34:02 Multissh finished. Process time 842.465643ms. Number of active ip is 1 262 | host: 192.168.80.131 263 | ========= Result ========= 264 | 201817:34:02 CST 265 | cisco 266 | composer.json 267 | composer.phar 268 | example-oauth2-server 269 | getting-started-with-mmdb 270 | gitlab 271 | gitlab-ce_8.0.4-ce.1_amd64.deb 272 | oauth2-demo-php 273 | oauth2-server-php 274 | python_test 275 | rsyslog-maxminddb 276 | 277 | ``` 278 | 279 | #### ssh.json 280 | ``` 281 | ./multissh -c ssh.json.example 282 | 2018/01/17 14:29:38 Multissh start 283 | 2018/01/17 14:29:41 Multissh finished. Process time 2.922928532s. Number of active ip is 2 284 | host: 192.168.31.51 285 | ========= Result ========= 286 | 287 | ****************************************************************************** 288 | * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved. * 289 | * Without the owner's prior written consent, * 290 | * no decompiling or reverse-engineering shall be allowed. * 291 | ****************************************************************************** 292 | 293 | show clock 294 | 14:29:41 CN Wed 01/17/2018 295 | Time Zone : CN add 08:00:00 296 | show clock 297 | 14:29:41 CN Wed 01/17/2018 298 | Time Zone : CN add 08:00:00 299 | exit 300 | 301 | host: 192.168.80.131 302 | ========= Result ========= 303 | cisco 304 | composer.json 305 | composer.phar 306 | example-oauth2-server 307 | getting-started-with-mmdb 308 | gitlab 309 | gitlab-ce_8.0.4-ce.1_amd64.deb 310 | oauth2-demo-php 311 | oauth2-server-php 312 | python_test 313 | rsyslog-maxminddb 314 | ``` 315 | 316 | #### LICENSE 317 | Apache License 2.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [2017] [shanghai-edu & ECNU] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------