From fec832402625d856aba0cc2fbdec9e773469ad87 Mon Sep 17 00:00:00 2001
From: a1012112796 <1012112796@qq.com>
Date: Sat, 22 May 2021 05:37:16 +0800
Subject: [PATCH] add a new internal hook to save ssh log (#15787)

* add a new internal hook to save ssh log

as title, when a ssh error ocure like #15785.
only when switch ``RUN_MODE`` to dev can we
found which error is ocure. But this way is
not a good idea for production envirment.

this changes try save ssh error mesage to the
log file like other log by a new internal hook.
I think it's usefull for find error message
in production envirment. Thanks.

Signed-off-by: a1012112796 <1012112796@qq.com>

* rename and fix nit

* Update modules/private/hook.go

Co-authored-by: silverwind <me@silverwind.io>

Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
---
 cmd/serv.go                                   |  4 +++
 custom/conf/app.example.ini                   |  5 +++
 .../doc/advanced/config-cheat-sheet.en-us.md  |  1 +
 modules/private/hook.go                       | 31 +++++++++++++++++
 modules/setting/log.go                        |  1 +
 modules/setting/setting.go                    |  1 +
 routers/private/internal.go                   |  1 +
 routers/private/ssh_log.go                    | 34 +++++++++++++++++++
 8 files changed, 78 insertions(+)
 create mode 100644 routers/private/ssh_log.go

diff --git a/cmd/serv.go b/cmd/serv.go
index 56167f63a8..1c9f5dc44e 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -81,6 +81,10 @@ func fail(userMessage, logMessage string, args ...interface{}) {
 		}
 	}
 
+	if len(logMessage) > 0 {
+		_ = private.SSHLog(true, fmt.Sprintf(logMessage+": ", args...))
+	}
+
 	os.Exit(1)
 }
 
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b1fe95e6e2..f37157c3b1 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -444,6 +444,11 @@ ROUTER = console
 ;ACCESS_LOG_TEMPLATE = {{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; SSH log (Creates log from ssh git request)
+;;
+;ENABLE_SSH_LOG = false
+;;
 ;; Other Settings
 ;;
 ;; Print Stacktraces with logs. (Rarely helpful.) Either "Trace", "Debug", "Info", "Warn", "Error", "Critical", default is "None"
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 9bb790f4c9..ee701b6572 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -657,6 +657,7 @@ Default templates for project boards:
 - `ROUTER`: **console**: The mode or name of the log the router should log to. (If you set this to `,` it will log to default gitea logger.)
 NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take effect. Configure each mode in per mode log subsections `\[log.modename.router\]`.
 - `ENABLE_ACCESS_LOG`: **false**: Creates an access.log in NCSA common log format, or as per the following template
+- `ENABLE_SSH_LOG`: **false**: save ssh log to log file
 - `ACCESS`: **file**: Logging mode for the access logger, use a comma to separate values. Configure each mode in per mode log subsections `\[log.modename.access\]`. By default the file mode will log to `$ROOT_PATH/access.log`. (If you set this to `,` it will log to the default gitea logger.)
 - `ACCESS_LOG_TEMPLATE`: **`{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`**: Sets the template used to create the access log.
   - The following variables are available:
diff --git a/modules/private/hook.go b/modules/private/hook.go
index 178500f736..cb8fe25708 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -5,6 +5,7 @@
 package private
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -57,6 +58,12 @@ type HookOptions struct {
 	IsDeployKey                     bool
 }
 
+// SSHLogOption ssh log options
+type SSHLogOption struct {
+	IsError bool
+	Message string
+}
+
 // HookPostReceiveResult represents an individual result from PostReceive
 type HookPostReceiveResult struct {
 	Results      []HookPostReceiveBranchResult
@@ -146,3 +153,27 @@ func SetDefaultBranch(ownerName, repoName, branch string) error {
 	}
 	return nil
 }
+
+// SSHLog sends ssh error log response
+func SSHLog(isErr bool, msg string) error {
+	reqURL := setting.LocalURL + "api/internal/ssh/log"
+	req := newInternalRequest(reqURL, "POST")
+	req = req.Header("Content-Type", "application/json")
+
+	jsonBytes, _ := json.Marshal(&SSHLogOption{
+		IsError: isErr,
+		Message: msg,
+	})
+	req.Body(jsonBytes)
+
+	req.SetTimeout(60*time.Second, 60*time.Second)
+	resp, err := req.Response()
+	if err != nil {
+		return fmt.Errorf("unable to contact gitea: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err)
+	}
+	return nil
+}
diff --git a/modules/setting/log.go b/modules/setting/log.go
index 44017b1138..0fb108c93d 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -287,6 +287,7 @@ func newLogService() {
 
 	options := newDefaultLogOptions()
 	options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
+	EnableSSHLog = Cfg.Section("log").Key("ENABLE_SSH_LOG").MustBool(false)
 
 	description := LogDescription{
 		Name: log.DEFAULT,
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 4244b55939..ec8e64578b 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -319,6 +319,7 @@ var (
 	DisableRouterLog   bool
 	RouterLogLevel     log.Level
 	EnableAccessLog    bool
+	EnableSSHLog       bool
 	AccessLogTemplate  string
 	EnableXORMLog      bool
 
diff --git a/routers/private/internal.go b/routers/private/internal.go
index c6cc61fc29..9202e67218 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -55,6 +55,7 @@ func Routes() *web.Route {
 
 	r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
 	r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
+	r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
 	r.Post("/hook/pre-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPreReceive)
 	r.Post("/hook/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive)
 	r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch)
diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go
new file mode 100644
index 0000000000..2f1793a0e0
--- /dev/null
+++ b/routers/private/ssh_log.go
@@ -0,0 +1,34 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package private
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/web"
+)
+
+// SSHLog hook to response ssh log
+func SSHLog(ctx *context.PrivateContext) {
+	if !setting.EnableSSHLog {
+		ctx.Status(http.StatusOK)
+		return
+	}
+
+	opts := web.GetForm(ctx).(*private.SSHLogOption)
+
+	if opts.IsError {
+		log.Error("ssh: %v", opts.Message)
+		ctx.Status(http.StatusOK)
+		return
+	}
+
+	log.Debug("ssh: %v", opts.Message)
+	ctx.Status(http.StatusOK)
+}