Files
cloudpods/pkg/webconsole/service/handlers.go
T

513 lines
15 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/pkg/util/regutils"
agapi "yunion.io/x/onecloud/pkg/apis/apigateway"
compute_api "yunion.io/x/onecloud/pkg/apis/compute"
webconsole_api "yunion.io/x/onecloud/pkg/apis/webconsole"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
app_common "yunion.io/x/onecloud/pkg/cloudcommon/app"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/modules/k8s"
"yunion.io/x/onecloud/pkg/util/netutils2"
"yunion.io/x/onecloud/pkg/util/seclib2"
"yunion.io/x/onecloud/pkg/webconsole/command"
"yunion.io/x/onecloud/pkg/webconsole/models"
o "yunion.io/x/onecloud/pkg/webconsole/options"
"yunion.io/x/onecloud/pkg/webconsole/server"
"yunion.io/x/onecloud/pkg/webconsole/session"
)
const (
ApiPathPrefix = "/webconsole/"
ConnectPathPrefix = "/connect/"
WebsockifyPathPrefix = "/websockify/"
WebsocketProxyPathPrefix = "/wsproxy/"
)
func initHandlers(app *appsrv.Application, isSlave bool) {
app_common.ExportOptionsHandler(app, &o.Options)
app.AddHandler("GET", ApiPathPrefix+"sftp/<session-id>/list", server.HandleSftpList)
app.AddHandler("GET", ApiPathPrefix+"sftp/<session-id>/download", server.HandleSftpDownload)
if !isSlave {
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/shell", auth.Authenticate(handleK8sShell))
app.AddHandler("POST", ApiPathPrefix+"climc/shell", auth.Authenticate(handleClimcShell))
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/log", auth.Authenticate(handleK8sLog))
app.AddHandler("POST", ApiPathPrefix+"baremetal/<id>", auth.Authenticate(handleBaremetalShell))
app.AddHandler("POST", ApiPathPrefix+"ssh/<ip>", auth.Authenticate(handleSshShell))
app.AddHandler("POST", ApiPathPrefix+"server/<id>", auth.Authenticate(handleServerRemoteConsole))
app.AddHandler("POST", ApiPathPrefix+"adb/<id>/shell", auth.Authenticate(handleAdbShell))
app.AddHandler("POST", ApiPathPrefix+"server-rdp/<id>", auth.Authenticate(handleServerRemoteRDPConsole))
app.AddHandler("POST", ApiPathPrefix+"sftp/<session-id>/upload", server.HandleSftpUpload)
}
for _, man := range []db.IModelManager{
models.GetCommandLogManager(),
} {
db.RegisterModelManager(man)
handler := db.NewModelHandler(man)
dispatcher.AddModelDispatcher(ApiPathPrefix, app, handler, isSlave)
}
}
func fetchK8sEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*command.K8sEnv, error) {
params, _, body := appsrv.FetchEnv(ctx, w, r)
if !gotypes.IsNil(body) && body.Contains("webconsole") {
body, _ = body.Get("webconsole")
}
k8sReq := webconsole_api.SK8sRequest{}
err := body.Unmarshal(&k8sReq)
if err != nil {
return nil, errors.Wrap(err, "body.Unmarshal SK8sRequest")
}
if k8sReq.Cluster == "" {
k8sReq.Cluster = "default"
}
if k8sReq.Namespace == "" {
k8sReq.Namespace = "default"
}
podName := params["<podName>"]
adminSession := auth.GetAdminSession(ctx, o.Options.Region)
data := jsonutils.NewDict()
ret, err := k8s.KubeClusters.GetSpecific(adminSession, k8sReq.Cluster, "kubeconfig", data)
if err != nil {
return nil, err
}
conf, err := ret.GetString("kubeconfig")
if err != nil {
return nil, httperrors.NewNotFoundError("Not found cluster %q kubeconfig", k8sReq.Cluster)
}
f, err := os.CreateTemp("", "kubeconfig-")
if err != nil {
return nil, fmt.Errorf("Save kubeconfig error: %v", err)
}
defer f.Close()
f.WriteString(conf)
return &command.K8sEnv{
Session: adminSession,
Cluster: k8sReq.Cluster,
Namespace: k8sReq.Namespace,
Pod: podName,
Container: k8sReq.Container,
Kubeconfig: f.Name(),
Data: body,
}, nil
}
type CloudEnv struct {
ClientSessin *mcclient.ClientSession
Params map[string]string
Query jsonutils.JSONObject
Body jsonutils.JSONObject
Ctx context.Context
}
func fetchCloudEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*CloudEnv, error) {
params, query, body := appsrv.FetchEnv(ctx, w, r)
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
if userCred == nil {
return nil, httperrors.NewUnauthorizedError("No token founded")
}
if !gotypes.IsNil(body) && body.Contains("webconsole") {
body, _ = body.Get("webconsole")
}
s := auth.Client().NewSession(ctx, o.Options.Region, "", "internal", userCred)
return &CloudEnv{
ClientSessin: s,
Params: params,
Query: query,
Body: body,
Ctx: ctx,
}, nil
}
func handleK8sCommand(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
cmdFactory func(*command.K8sEnv) command.ICommand,
) {
env, err := fetchK8sEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
cmd := cmdFactory(env)
handleCommandSession(ctx, cmd, w)
}
func handleK8sShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
handleK8sCommand(ctx, w, r, command.NewPodBashCommand)
}
func handleK8sLog(ctx context.Context, w http.ResponseWriter, r *http.Request) {
handleK8sCommand(ctx, w, r, command.NewPodLogCommand)
}
func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
sshConnInfo := session.SSshConnectionInfo{}
if !gotypes.IsNil(env.Body) {
err = env.Body.Unmarshal(&sshConnInfo)
if err != nil {
httperrors.InputParameterError(ctx, w, "unmarshal error: %s", err.Error())
return
}
}
idStr := env.Params["<ip>"]
if !regutils.MatchIPAddr(idStr) {
var tryServer = func() error {
ip, port, guestDetails, err := session.ResolveServerSSHIPPortById(ctx, env.ClientSessin, idStr, sshConnInfo.IP, sshConnInfo.Port)
if err != nil {
return err
}
sshConnInfo.IP = ip
sshConnInfo.Port = port
sshConnInfo.GuestDetails = guestDetails
return nil
}
var tryHost = func() error {
ip, port, hostDetails, err := session.ResolveHostSSHIPPortById(ctx, env.ClientSessin, idStr, sshConnInfo.IP, sshConnInfo.Port)
if err != nil {
return err
}
sshConnInfo.IP = ip
sshConnInfo.Port = port
sshConnInfo.HostDetails = hostDetails
return nil
}
switch sshConnInfo.ResourceType {
case "server":
err = tryServer()
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
case "host":
err = tryHost()
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
default:
for _, try := range []func() error{
tryServer,
tryHost,
} {
err = try()
if err == nil {
s := session.NewSshSession(ctx, env.ClientSessin, sshConnInfo)
handleSshSession(ctx, s, w)
return
}
}
httperrors.NewResourceNotFoundError("%s not found", idStr)
return
}
} else {
// directly ssh IP should be deprecated gradually
sshConnInfo.IP = idStr
}
s := session.NewSshSession(ctx, env.ClientSessin, sshConnInfo)
handleSshSession(ctx, s, w)
}
func handleSshSession(ctx context.Context, session *session.SSshSession, w http.ResponseWriter) {
log.Infof("handleSshSession %s", jsonutils.Marshal(session).String())
handleDataSession(ctx, session, w, "ws", nil, false)
}
func handleBaremetalShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
hostId := env.Params["<id>"]
ret, err := modules.Hosts.GetSpecific(env.ClientSessin, hostId, "ipmi", nil)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
info := command.IpmiInfo{}
err = ret.Unmarshal(&info)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
cmd, err := command.NewIpmitoolSolCommand(&info, env.ClientSessin)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
handleCommandSession(ctx, cmd, w)
}
func handleClimcShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
info := webconsole_api.ClimcSshInfo{}
err = env.Body.Unmarshal(&info)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
cmd, err := command.NewClimcSshCommand(&info, env.ClientSessin)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
handleCommandSession(ctx, cmd, w)
}
func handleServerRemoteConsole(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
query := env.Body
srvId := env.Params["<id>"]
info, err := session.NewRemoteConsoleInfoByCloud(env.ClientSessin, srvId, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
switch info.Protocol {
case session.ALIYUN, session.QCLOUD, session.OPENSTACK,
session.VMRC, session.ZSTACK, session.CTYUN,
session.HUAWEI, session.HCS, session.APSARA,
session.JDCLOUD, session.CLOUDPODS, session.PROXMOX,
session.VOLCENGINE, session.BAIDU, session.CNWARE, session.KSYUN, session.ECLOUD:
responsePublicCloudConsole(ctx, info, w)
case session.VNC:
handleDataSession(ctx, info, w, "no-vnc", url.Values{"password": {info.GetPassword()}}, true)
case session.SPICE:
handleDataSession(ctx, info, w, "spice", url.Values{"password": {info.GetPassword()}}, true)
case session.WMKS:
handleDataSession(ctx, info, w, "wmks", url.Values{"password": {info.GetPassword()}}, true)
default:
httperrors.NotAcceptableError(ctx, w, "Unspported remote console protocol: %s", info.Protocol)
}
}
func handleServerRemoteRDPConsole(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
query := env.Body
srvId := env.Params["<id>"]
info, err := session.NewRemoteRDPConsoleInfoByCloud(ctx, env.ClientSessin, srvId, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
handleDataSession(ctx, info, w, "rdp", url.Values{"password": {info.GetPassword()}}, false)
}
func responsePublicCloudConsole(ctx context.Context, info *session.RemoteConsoleInfo, w http.ResponseWriter) {
params, err := info.GetConnectParams()
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
resp := webconsole_api.ServerRemoteConsoleResponse{
ConnectParams: params,
}
sendJSON(w, resp.JSON(resp))
}
func handleDataSession(ctx context.Context, sData session.ISessionData, w http.ResponseWriter, base string, connParams url.Values, b64Encode bool) {
s, err := session.Manager.Save(sData)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
dispInfo, err := sData.GetDisplayInfo(ctx)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
params, err := s.GetConnectParams(connParams, dispInfo)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
var accessUrl string
{
connParams, err := seclib2.AES_256.CbcEncodeBase64([]byte(params), []byte(agapi.DefaultEncryptKey))
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
dataVal := url.Values{}
dataVal.Add("data", connParams)
accessUrl = httputils.JoinPath(o.Options.ApiServer, fmt.Sprintf("web-console/%s?%s", base, dataVal.Encode()))
}
if b64Encode {
params = base64.StdEncoding.EncodeToString([]byte(params))
}
resp := webconsole_api.ServerRemoteConsoleResponse{
AccessUrl: accessUrl,
ConnectParams: params,
Session: s.Id,
}
sendJSON(w, resp.JSON(resp))
}
func handleCommandSession(ctx context.Context, cmd command.ICommand, w http.ResponseWriter) {
handleDataSession(ctx, session.WrapCommandSession(cmd), w, "tty", nil, false)
}
func sendJSON(w http.ResponseWriter, body jsonutils.JSONObject) {
ret := jsonutils.NewDict()
if body != nil {
ret.Add(body, "webconsole")
}
appsrv.SendJSON(w, ret)
}
func handleAdbShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
serverId := env.Params["<id>"]
serverDetails := compute_api.ServerDetails{}
resp, err := modules.Servers.GetById(env.ClientSessin, serverId, nil)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
err = resp.Unmarshal(&serverDetails)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
phoneIp := ""
adbPort := -1
connStr, _ := env.Body.GetString("conn")
if len(connStr) > 0 {
parts := strings.Split(connStr, ":")
if len(parts) > 0 {
phoneIp = parts[0]
if len(parts) > 1 {
adbPort, _ = strconv.Atoi(parts[1])
}
if adbPort == 0 {
adbPort = 5555
}
}
}
if len(phoneIp) == 0 {
// fallback
if len(serverDetails.Nics) > 0 {
phoneIp = serverDetails.Nics[0].IpAddr
portMaps := serverDetails.Nics[0].PortMappings
for i := range portMaps {
portMap := portMaps[i]
if portMap.Port == 5555 && portMap.Protocol == "tcp" {
adbPort = *portMap.HostPort
break
}
}
var errMsgs []string
if !regutils.MatchIP4Addr(phoneIp) {
errMsgs = append(errMsgs, "invalid phone IP")
}
if adbPort < 0 {
errMsgs = append(errMsgs, "adb port not found")
}
if len(errMsgs) > 0 {
httperrors.GeneralServerError(ctx, w, httperrors.NewNotSupportedError("%s", strings.Join(errMsgs, ";")))
return
}
} else {
httperrors.GeneralServerError(ctx, w, httperrors.NewNotSupportedError("porting_mapping not supported"))
return
}
{
// test phoneIP is accessible
err := netutils2.TestTcpPort(phoneIp, 5555, 3, 3)
if err != nil {
log.Errorf("TestTcpPort %s:%d fail %s", phoneIp, 5555, err)
phoneIp = serverDetails.HostEIP
if len(phoneIp) == 0 {
phoneIp = serverDetails.HostAccessIp
}
} else {
adbPort = 5555
}
}
}
info := command.SAdbShellInfo{
HostIp: phoneIp,
HostPort: adbPort,
}
cmd, err := command.NewAdbShellCommand(&info, env.ClientSessin)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
handleCommandSession(ctx, cmd, w)
}