Fix console access and logs

This commit is contained in:
Antonio Mika
2021-11-08 02:48:21 +00:00
committed by Antonio Mika
parent aefb5b0822
commit b71acec24d
9 changed files with 153 additions and 103 deletions

View File

@@ -64,12 +64,14 @@ func Start(state *utils.State) {
param.Latency = param.Latency - param.Latency%time.Second
}
if viper.GetString("admin-console-token") != "" && strings.Contains(param.Path, viper.GetString("admin-console-token")) {
param.Path = strings.Replace(param.Path, viper.GetString("admin-console-token"), "[REDACTED]", 1)
originalURI := param.Keys["originalURI"].(string)
if viper.GetString("admin-console-token") != "" && strings.Contains(originalURI, viper.GetString("admin-console-token")) {
originalURI = strings.Replace(originalURI, viper.GetString("admin-console-token"), "[REDACTED]", 1)
}
if viper.GetString("service-console-token") != "" && strings.Contains(param.Path, viper.GetString("service-console-token")) {
param.Path = strings.Replace(param.Path, viper.GetString("service-console-token"), "[REDACTED]", 1)
if viper.GetString("service-console-token") != "" && strings.Contains(originalURI, viper.GetString("service-console-token")) {
originalURI = strings.Replace(originalURI, viper.GetString("service-console-token"), "[REDACTED]", 1)
}
logLine := fmt.Sprintf("%v | %s |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
@@ -79,35 +81,12 @@ func Start(state *utils.State) {
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
originalURI,
param.ErrorMessage,
)
if viper.GetBool("log-to-client") {
var currentListener *utils.HTTPHolder
var secondOption *utils.HTTPHolder
hostname := strings.Split(param.Request.Host, ":")[0]
state.HTTPListeners.Range(func(key, value interface{}) bool {
locationListener := value.(*utils.HTTPHolder)
requestUsername, requestPassword, _ := param.Request.BasicAuth()
parsedPassword, _ := locationListener.HTTPUrl.User.Password()
if hostname == locationListener.HTTPUrl.Host && strings.HasPrefix(param.Request.URL.Path, locationListener.HTTPUrl.Path) {
secondOption = locationListener
if requestUsername == locationListener.HTTPUrl.User.Username() && requestPassword == parsedPassword {
currentListener = locationListener
return false
}
}
return true
})
if currentListener == nil && secondOption != nil {
currentListener = secondOption
}
if viper.GetBool("log-to-client") && param.Keys["httpHolder"] != nil {
currentListener := param.Keys["httpHolder"].(*utils.HTTPHolder)
if currentListener != nil {
sshConnTmp, ok := currentListener.SSHConnections.Load(param.Keys["proxySocket"])
@@ -126,34 +105,36 @@ func Start(state *utils.State) {
return logLine
}), gin.Recovery(), func(c *gin.Context) {
c.Set("originalURI", c.Request.RequestURI)
c.Set("originalPath", c.Request.URL.Path)
c.Set("originalRawPath", c.Request.URL.RawPath)
hostSplit := strings.Split(c.Request.Host, ":")
hostname := hostSplit[0]
hostIsRoot := hostname == viper.GetString("domain")
if (viper.GetBool("admin-console") || viper.GetBool("service-console")) && strings.HasPrefix(c.Request.URL.Path, "/_sish/") {
state.Console.HandleRequest(hostname, hostIsRoot, c)
if viper.GetBool("admin-console") && hostIsRoot && strings.HasPrefix(c.Request.URL.Path, "/_sish/") {
state.Console.HandleRequest("", hostIsRoot, c)
return
}
var currentListener *utils.HTTPHolder
var secondOption *utils.HTTPHolder
requestUsername, requestPassword, _ := c.Request.BasicAuth()
exactMatch := false
authNeeded := true
state.HTTPListeners.Range(func(key, value interface{}) bool {
locationListener := value.(*utils.HTTPHolder)
requestUsername, requestPassword, _ := c.Request.BasicAuth()
parsedPassword, _ := locationListener.HTTPUrl.User.Password()
if hostname == locationListener.HTTPUrl.Host && strings.HasPrefix(c.Request.URL.Path, locationListener.HTTPUrl.Path) {
secondOption = locationListener
if requestUsername == locationListener.HTTPUrl.User.Username() && requestPassword == parsedPassword {
currentListener = locationListener
return false
}
currentListener = locationListener
if (locationListener.HTTPUrl.User.Username() != "" && requestUsername == "") || (parsedPassword != "" && requestPassword == "") {
c.Header("WWW-Authenticate", "Basic realm=\"sish\"")
c.AbortWithStatus(http.StatusUnauthorized)
if requestUsername == locationListener.HTTPUrl.User.Username() && requestPassword == parsedPassword {
exactMatch = true
authNeeded = false
return false
}
}
@@ -161,14 +142,6 @@ func Start(state *utils.State) {
return true
})
if c.IsAborted() {
return
}
if currentListener == nil && secondOption != nil {
currentListener = secondOption
}
if currentListener == nil && hostIsRoot {
if viper.GetBool("redirect-root") && !strings.HasPrefix(c.Request.URL.Path, "/favicon.ico") {
c.Redirect(http.StatusFound, viper.GetString("redirect-root-location"))
@@ -187,7 +160,37 @@ func Start(state *utils.State) {
return
}
if viper.GetBool("strip-http-path") {
c.Set("httpHolder", currentListener)
if !exactMatch || authNeeded {
c.Header("WWW-Authenticate", "Basic realm=\"sish\"")
c.AbortWithStatus(http.StatusUnauthorized)
return
}
stripPath := viper.GetBool("strip-http-path")
currentListener.SSHConnections.Range(func(key, val interface{}) bool {
sshConn := val.(*utils.SSHConnection)
newHost := sshConn.HostHeader
if sshConn.StripPath != viper.GetBool("strip-http-path") {
stripPath = sshConn.StripPath
}
if newHost == "" {
return true
}
if len(hostSplit) > 1 {
newHost = fmt.Sprintf("%s:%s", newHost, hostSplit[1])
}
c.Request.Host = newHost
return false
})
if viper.GetBool("strip-http-path") && stripPath {
c.Request.RequestURI = strings.TrimPrefix(c.Request.RequestURI, currentListener.HTTPUrl.Path)
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, currentListener.HTTPUrl.Path)
c.Request.URL.RawPath = strings.TrimPrefix(c.Request.URL.RawPath, currentListener.HTTPUrl.Path)
@@ -211,6 +214,11 @@ func Start(state *utils.State) {
})
}
if exactMatch && (viper.GetBool("admin-console") || viper.GetBool("service-console")) && strings.HasPrefix(c.Request.URL.Path, "/_sish/") {
state.Console.HandleRequest(currentListener.HTTPUrl.String(), hostIsRoot, c)
return
}
reqBody, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
log.Println("Error reading request body:", err)
@@ -219,7 +227,7 @@ func Start(state *utils.State) {
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody))
err = forward.ResponseModifier(ResponseModifier(state, hostname, reqBody, c))(currentListener.Forward)
err = forward.ResponseModifier(ResponseModifier(state, hostname, reqBody, c, currentListener))(currentListener.Forward)
if err != nil {
log.Println("Unable to set response modifier:", err)
}

View File

@@ -43,7 +43,7 @@ func RoundTripper() *http.Transport {
// ResponseModifier implements a response modifier for the specified request.
// We don't actually modify any requests, but we do want to record the request
// so we can send it to the web console.
func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gin.Context) func(*http.Response) error {
func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gin.Context, currentListener *utils.HTTPHolder) func(*http.Response) error {
return func(response *http.Response) error {
if viper.GetBool("admin-console") || viper.GetBool("service-console") {
resBody, err := ioutil.ReadAll(response.Body)
@@ -79,19 +79,20 @@ func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gi
requestHeaders.Add("Host", hostname)
data, err := json.Marshal(map[string]interface{}{
"startTime": startTime,
"startTimePretty": startTime.Format(viper.GetString("time-format")),
"currentTime": currentTime,
"requestIP": c.ClientIP(),
"requestTime": diffTime.Round(roundTime).String(),
"requestMethod": c.Request.Method,
"requestUrl": c.Request.URL,
"requestHeaders": requestHeaders,
"requestBody": base64.StdEncoding.EncodeToString(reqBody),
"responseHeaders": response.Header,
"responseCode": response.StatusCode,
"responseStatus": response.Status,
"responseBody": base64.StdEncoding.EncodeToString(resBody),
"startTime": startTime,
"startTimePretty": startTime.Format(viper.GetString("time-format")),
"currentTime": currentTime,
"requestIP": c.ClientIP(),
"requestTime": diffTime.Round(roundTime).String(),
"requestMethod": c.Request.Method,
"requestUrl": c.Request.URL,
"originalRequestURI": c.GetString("originalURI"),
"requestHeaders": requestHeaders,
"requestBody": base64.StdEncoding.EncodeToString(reqBody),
"responseHeaders": response.Header,
"responseCode": response.StatusCode,
"responseStatus": response.Status,
"responseBody": base64.StdEncoding.EncodeToString(resBody),
})
if err != nil {
@@ -107,7 +108,7 @@ func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gi
c.Set("proxySocket", string(hostLocation))
}
state.Console.BroadcastRoute(hostname, data)
state.Console.BroadcastRoute(currentListener.HTTPUrl.String(), data)
}
return nil

View File

@@ -6,6 +6,7 @@ import (
"io"
"log"
"net"
"strconv"
"strings"
"github.com/antoniomika/sish/utils"
@@ -15,14 +16,17 @@ import (
)
// commandSplitter is the character that terminates a prefix.
var commandSplitter = "="
const commandSplitter = "="
// proxyProtoPrefix is used when deciding what proxy protocol
// version to use.
var proxyProtoPrefix = "proxyproto"
const proxyProtoPrefix = "proxyproto"
// hostHeaderPrefix is the host-header for a specific session.
var hostHeaderPrefix = "host-header"
const hostHeaderPrefix = "host-header"
// stripPathPrefix defines whether or not to strip the path (if enabled globally).
const stripPathPrefix = "strip-path"
// handleSession handles the channel when a user requests a session.
// This is how we send console messages.
@@ -75,6 +79,8 @@ func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, stat
}()
go func() {
sshConn.StripPath = viper.GetBool("strip-http-path")
for req := range requests {
switch req.Type {
case "shell":
@@ -89,6 +95,10 @@ func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, stat
for _, commandFlag := range commandFlags {
commandFlagParts := strings.Split(commandFlag, commandSplitter)
if len(commandFlagParts) < 2 {
continue
}
command, param := commandFlagParts[0], commandFlagParts[1]
switch command {
@@ -104,6 +114,18 @@ func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, stat
sshConn.HostHeader = param
sshConn.SendMessage(fmt.Sprintf("Using host header %s for HTTP handlers", sshConn.HostHeader), true)
}
case stripPathPrefix:
if sshConn.StripPath {
stripPath, err := strconv.ParseBool(param)
if err != nil {
log.Printf("Unable to detect strip path. Using configuration: %s", err)
} else {
sshConn.StripPath = stripPath
}
sshConn.SendMessage(fmt.Sprintf("Strip path for HTTP handlers set to: %t", sshConn.StripPath), true)
}
}
}
default:

View File

@@ -75,6 +75,12 @@ func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMess
log.Println("Unable to add server to balancer")
}
var userPass string
password, _ := pH.HTTPUrl.User.Password()
if pH.HTTPUrl.User.Username() != "" || password != "" {
userPass = fmt.Sprintf("%s:%s@", pH.HTTPUrl.User.Username(), password)
}
if viper.GetBool("admin-console") || viper.GetBool("service-console") {
routeToken := viper.GetString("service-console-token")
sendToken := false
@@ -108,7 +114,12 @@ func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMess
}
}
consoleURL := fmt.Sprintf("%s://%s%s", scheme, pH.HTTPUrl.Host, portString)
pathParam := ""
if pH.HTTPUrl.Path != "/" {
pathParam = pH.HTTPUrl.Path
}
consoleURL := fmt.Sprintf("%s://%s%s%s%s", scheme, userPass, pH.HTTPUrl.Host, portString, pathParam)
requestMessages += fmt.Sprintf("Service console can be accessed here: %s/_sish/console?x-authorization=%s\r\n", consoleURL, routeToken)
}
@@ -119,12 +130,6 @@ func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMess
httpPortString = fmt.Sprintf(":%d", httpPort)
}
var userPass string
password, _ := pH.HTTPUrl.User.Password()
if pH.HTTPUrl.User.Username() != "" || password != "" {
userPass = fmt.Sprintf("%s:%s@", pH.HTTPUrl.User.Username(), password)
}
requestMessages += fmt.Sprintf("%s: http://%s%s%s%s\r\n", aurora.BgBlue("HTTP"), userPass, pH.HTTPUrl.Host, httpPortString, pH.HTTPUrl.Path)
log.Printf("%s forwarding started: http://%s%s%s%s -> %s for client: %s\n", aurora.BgBlue("HTTP"), userPass, pH.HTTPUrl.Host, httpPortString, pH.HTTPUrl.Path, listenerHolder.Addr().String(), sshConn.SSHConn.RemoteAddr().String())

View File

@@ -18,7 +18,7 @@
<th scope="row" data-bind="text: startTimePretty"></th>
<th scope="row" data-bind="text: requestIP"></th>
<th scope="row" data-bind="text: requestMethod"></th>
<td data-bind="text: requestUrl.Path"></td>
<td data-bind="text: originalRequestURI"></td>
<td data-bind="text: responseStatus"></td>
<td data-bind="text: requestTime"></td>
</tr>
@@ -74,6 +74,7 @@
this.startTimePretty = requestData.startTimePretty;
this.requestMethod = requestData.requestMethod;
this.requestUrl = requestData.requestUrl;
this.originalRequestURI = requestData.originalRequestURI;
this.responseStatus = requestData.responseStatus;
this.requestTime = requestData.requestTime;
this.requestIP = requestData.requestIP;

View File

@@ -40,7 +40,7 @@
<body>
<nav class="navbar navbar-expand-md fixed-top navbar-dark bg-primary" role="navigation">
<a class="navbar-brand" href="/_sish/console">sish</a>
<a class="navbar-brand" href="">sish</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

View File

@@ -69,6 +69,13 @@
this.data = routeData;
this.type = routeType;
this.name = routeName;
if (this.name.indexOf("/") !== -1) {
var routeParts = this.name.split("/");
this.name = routeParts[0];
this.pathname = `/${routeParts[1]}`;
}
this.disconnectType = disconnectType;
this.disconnectRoute = function() {
@@ -78,7 +85,7 @@
}.bind(this);
this.forwardToConsole = function() {
window.location.hostname = this.name;
window.location.href = window.location.href.replace(window.location.hostname, this.name).replace(window.location.pathname, `${this.pathname.length > 1 ? this.pathname : ''}${window.location.pathname}`);
}.bind(this);
};

View File

@@ -22,6 +22,7 @@ type SSHConnection struct {
Messages chan string
ProxyProto byte
HostHeader string
StripPath bool
Session chan bool
CleanupHandler bool
SetupLock *sync.Mutex

View File

@@ -46,7 +46,7 @@ func NewWebConsole() *WebConsole {
}
// HandleRequest handles an incoming web request, handles auth, and then routes it.
func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Context) {
func (c *WebConsole) HandleRequest(proxyUrl string, hostIsRoot bool, g *gin.Context) {
userAuthed := false
userIsAdmin := false
if (viper.GetBool("admin-console") && viper.GetString("admin-console-token") != "") && (g.Request.URL.Query().Get("x-authorization") == viper.GetString("admin-console-token") || g.Request.Header.Get("x-authorization") == viper.GetString("admin-console-token")) {
@@ -54,7 +54,7 @@ func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Cont
userAuthed = true
}
tokenInterface, ok := c.RouteTokens.Load(hostname)
tokenInterface, ok := c.RouteTokens.Load(proxyUrl)
if ok {
routeToken, ok := tokenInterface.(string)
if viper.GetBool("service-console") && ok && (g.Request.URL.Query().Get("x-authorization") == routeToken || g.Request.Header.Get("x-authorization") == routeToken) {
@@ -63,43 +63,43 @@ func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Cont
}
if strings.HasPrefix(g.Request.URL.Path, "/_sish/console/ws") && userAuthed {
c.HandleWebSocket(hostname, g)
c.HandleWebSocket(proxyUrl, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/console") && userAuthed {
c.HandleTemplate(hostname, hostIsRoot, userIsAdmin, g)
c.HandleTemplate(proxyUrl, hostIsRoot, userIsAdmin, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/disconnectclient/") && userIsAdmin {
c.HandleDisconnectClient(hostname, g)
c.HandleDisconnectClient(proxyUrl, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/") && userIsAdmin {
c.HandleDisconnectRoute(hostname, g)
c.HandleDisconnectRoute(proxyUrl, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/clients") && hostIsRoot && userIsAdmin {
c.HandleClients(hostname, g)
c.HandleClients(proxyUrl, g)
return
}
}
// HandleTemplate handles rendering the console templates.
func (c *WebConsole) HandleTemplate(hostname string, hostIsRoot bool, userIsAdmin bool, g *gin.Context) {
func (c *WebConsole) HandleTemplate(proxyUrl string, hostIsRoot bool, userIsAdmin bool, g *gin.Context) {
if hostIsRoot && userIsAdmin {
g.HTML(http.StatusOK, "routes", nil)
return
}
if c.RouteExists(hostname) {
if c.RouteExists(proxyUrl) {
g.HTML(http.StatusOK, "console", nil)
return
}
err := g.AbortWithError(http.StatusNotFound, fmt.Errorf("cannot find connection for host: %s", hostname))
err := g.AbortWithError(http.StatusNotFound, fmt.Errorf("cannot find connection for host: %s", proxyUrl))
if err != nil {
log.Println("Aborting with error", err)
}
}
// HandleWebSocket handles the websocket route.
func (c *WebConsole) HandleWebSocket(hostname string, g *gin.Context) {
func (c *WebConsole) HandleWebSocket(proxyUrl string, g *gin.Context) {
conn, err := upgrader.Upgrade(g.Writer, g.Request, nil)
if err != nil {
log.Println(err)
@@ -110,16 +110,16 @@ func (c *WebConsole) HandleWebSocket(hostname string, g *gin.Context) {
Conn: conn,
Console: c,
Send: make(chan []byte),
Route: hostname,
Route: proxyUrl,
}
c.AddClient(hostname, client)
c.AddClient(proxyUrl, client)
go client.Handle()
}
// HandleDisconnectClient handles the disconnection request for a SSH client.
func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) {
func (c *WebConsole) HandleDisconnectClient(proxyUrl string, g *gin.Context) {
client := strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectclient/")
c.State.SSHConnections.Range(func(key interface{}, val interface{}) bool {
@@ -143,7 +143,7 @@ func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) {
}
// HandleDisconnectRoute handles the disconnection request for a forwarded route.
func (c *WebConsole) HandleDisconnectRoute(hostname string, g *gin.Context) {
func (c *WebConsole) HandleDisconnectRoute(proxyUrl string, g *gin.Context) {
route := strings.Split(strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/"), "/")
encRouteName := route[1]
@@ -179,7 +179,7 @@ func (c *WebConsole) HandleDisconnectRoute(hostname string, g *gin.Context) {
// HandleClients handles returning all connected SSH clients. This will
// also go through all of the forwarded connections for the SSH client and
// return them.
func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
func (c *WebConsole) HandleClients(proxyUrl string, g *gin.Context) {
data := map[string]interface{}{
"status": true,
}
@@ -252,23 +252,28 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
httpListeners := map[string]interface{}{}
c.State.HTTPListeners.Range(func(key interface{}, val interface{}) bool {
httpListener := key.(string)
aliasAddress := val.(*HTTPHolder)
httpHolder := val.(*HTTPHolder)
listenerHandlers := []string{}
aliasAddress.SSHConnections.Range(func(key interface{}, val interface{}) bool {
aliasAddr := key.(string)
httpHolder.SSHConnections.Range(func(key interface{}, val interface{}) bool {
httpAddr := key.(string)
for _, v := range listeners {
if v == aliasAddr {
listenerHandlers = append(listenerHandlers, aliasAddr)
if v == httpAddr {
listenerHandlers = append(listenerHandlers, httpAddr)
}
}
return true
})
if len(listenerHandlers) > 0 {
httpListeners[httpListener] = listenerHandlers
var userPass string
password, _ := httpHolder.HTTPUrl.User.Password()
if httpHolder.HTTPUrl.User.Username() != "" || password != "" {
userPass = fmt.Sprintf("%s:%s@", httpHolder.HTTPUrl.User.Username(), password)
}
httpListeners[fmt.Sprintf("%s%s%s", userPass, httpHolder.HTTPUrl.Hostname(), httpHolder.HTTPUrl.Path)] = listenerHandlers
}
return true