Rainbond/pkg/webcli/app/app.go
2017-11-20 18:12:53 +08:00

234 lines
5.9 KiB
Go

// RAINBOND, Application Management Platform
// Copyright (C) 2014-2017 Goodrain Co., Ltd.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package app
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"log"
"net"
"net/http"
"os/exec"
"sync"
"text/template"
"github.com/gorilla/websocket"
"github.com/kr/pty"
"github.com/yudai/umutex"
)
type App struct {
command []string
options *Options
upgrader *websocket.Upgrader
titleTemplate *template.Template
onceMutex *umutex.UnblockingMutex
}
type Options struct {
Address string `hcl:"address"`
Port string `hcl:"port"`
PermitWrite bool `hcl:"permit_write"`
IndexFile string `hcl:"index_file"`
TitleFormat string `hcl:"title_format"`
EnableReconnect bool `hcl:"enable_reconnect"`
ReconnectTime int `hcl:"reconnect_time"`
PermitArguments bool `hcl:"permit_arguments"`
CloseSignal int `hcl:"close_signal"`
Preferences HtermPrefernces `hcl:"preferences"`
RawPreferences map[string]interface{} `hcl:"preferences"`
SessionKey string `hcl:"session_key"`
}
var Version = "0.0.2"
var DefaultOptions = Options{
Address: "",
Port: "8080",
PermitWrite: true,
IndexFile: "",
TitleFormat: "GRTTY Command",
EnableReconnect: true,
ReconnectTime: 10,
CloseSignal: 1, // syscall.SIGHUP
Preferences: HtermPrefernces{},
SessionKey: "_auth_user_id",
}
type InitMessage struct {
TenantID string `json:"T_id"`
ServiceID string `json:"S_id"`
PodName string `json:"C_id"`
Md5 string `json:"Md5"`
}
func checkSameOrigin(r *http.Request) bool {
return true
}
func New(command []string, options *Options) (*App, error) {
titleTemplate, _ := template.New("title").Parse(options.TitleFormat)
return &App{
command: command,
options: options,
upgrader: &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: checkSameOrigin,
},
titleTemplate: titleTemplate,
onceMutex: umutex.New(),
}, nil
}
func (app *App) Run() error {
endpoint := net.JoinHostPort(app.options.Address, app.options.Port)
wsHandler := http.HandlerFunc(app.handleWS)
var siteMux = http.NewServeMux()
siteHandler := http.Handler(siteMux)
siteHandler = wrapHeaders(siteHandler)
wsMux := http.NewServeMux()
wsMux.Handle("/", siteHandler)
wsMux.Handle("/docker_console", wsHandler)
siteHandler = (http.Handler(wsMux))
siteHandler = wrapLogger(siteHandler)
server, err := app.makeServer(endpoint, &siteHandler)
if err != nil {
return errors.New("Failed to build server: " + err.Error())
}
log.Printf("webcli listen %s", endpoint)
log.Fatal(server.ListenAndServe())
log.Printf("Exiting...")
return nil
}
func (app *App) makeServer(addr string, handler *http.Handler) (*http.Server, error) {
server := &http.Server{
Addr: addr,
Handler: *handler,
}
return server, nil
}
func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
log.Printf("New client connected: %s", r.RemoteAddr)
if r.Method != "GET" {
http.Error(w, "Method not allowed", 405)
return
}
conn, err := app.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Failed to upgrade connection: " + err.Error())
return
}
_, stream, err := conn.ReadMessage()
if err != nil {
log.Print("Failed to authenticate websocket connection " + err.Error())
conn.Close()
return
}
message := string(stream)
log.Print("message=", message)
var init InitMessage
err = json.Unmarshal(stream, &init)
//todo auth
if init.PodName == "" {
log.Print("Parameter is error, pod name is empty")
conn.WriteMessage(websocket.TextMessage, []byte("pod name can not be empty"))
conn.Close()
return
}
key := init.TenantID + "_" + init.ServiceID + "_" + init.PodName
log.Print(key)
md5 := md5Func(key)
if md5 != init.Md5 {
log.Print("Auth is not allowed !")
conn.WriteMessage(websocket.TextMessage, []byte("Auth is not allowed!"))
conn.Close()
return
}
cmd := exec.Command("kubectl", "--namespace", init.TenantID, "exec", "-ti", init.PodName, "/bin/bash")
ptyIo, err := pty.Start(cmd)
if err != nil {
log.Print("Failed to execute command")
return
}
log.Printf("Command is running for client %s with PID %d ", r.RemoteAddr, cmd.Process.Pid)
context := &clientContext{
app: app,
request: r,
connection: conn,
command: cmd,
pty: ptyIo,
writeMutex: &sync.Mutex{},
}
context.goHandleClient()
}
func (app *App) Exit() (firstCall bool) {
return true
}
func wrapLogger(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &responseWrapper{w, 200}
handler.ServeHTTP(rw, r)
log.Printf("%s %d %s %s", r.RemoteAddr, rw.status, r.Method, r.URL.Path)
})
}
func wrapHeaders(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "GoTTY/"+Version)
handler.ServeHTTP(w, r)
})
}
func md5Func(str string) string {
h := md5.New()
h.Write([]byte(str))
cipherStr := h.Sum(nil)
return hex.EncodeToString(cipherStr)
}