Browse Source

Add internal routes for ssh hook comands (#1471)

* add internal routes for ssh hook comands

* fix lint

* add comment on why package named private not internal but the route name is internal

* add comment above package private why package named private not internal but the route name is internal

* remove exp time on internal access

* move routes from /internal to /api/internal

* add comment and defer on UpdatePublicKeyUpdated
Lunny Xiao 3 years ago
parent
commit
2eeae84cbd
7 changed files with 161 additions and 12 deletions
  1. 2 1
      cmd/serv.go
  2. 6 0
      cmd/web.go
  3. 4 2
      models/ssh_key.go
  4. 5 0
      modules/httplib/httplib.go
  5. 53 0
      modules/private/internal.go
  6. 47 9
      modules/setting/setting.go
  7. 44 0
      routers/private/internal.go

+ 2 - 1
cmd/serv.go

@@ -16,6 +16,7 @@ import (
16 16
 
17 17
 	"code.gitea.io/gitea/models"
18 18
 	"code.gitea.io/gitea/modules/log"
19
+	"code.gitea.io/gitea/modules/private"
19 20
 	"code.gitea.io/gitea/modules/setting"
20 21
 
21 22
 	"github.com/Unknwon/com"
@@ -318,7 +319,7 @@ func runServ(c *cli.Context) error {
318 319
 
319 320
 	// Update user key activity.
320 321
 	if keyID > 0 {
321
-		if err = models.UpdatePublicKeyUpdated(keyID); err != nil {
322
+		if err = private.UpdatePublicKeyUpdated(keyID); err != nil {
322 323
 			fail("Internal error", "UpdatePublicKey: %v", err)
323 324
 		}
324 325
 	}

+ 6 - 0
cmd/web.go

@@ -29,6 +29,7 @@ import (
29 29
 	apiv1 "code.gitea.io/gitea/routers/api/v1"
30 30
 	"code.gitea.io/gitea/routers/dev"
31 31
 	"code.gitea.io/gitea/routers/org"
32
+	"code.gitea.io/gitea/routers/private"
32 33
 	"code.gitea.io/gitea/routers/repo"
33 34
 	"code.gitea.io/gitea/routers/user"
34 35
 
@@ -661,6 +662,11 @@ func runWeb(ctx *cli.Context) error {
661 662
 		apiv1.RegisterRoutes(m)
662 663
 	}, ignSignIn)
663 664
 
665
+	m.Group("/api/internal", func() {
666
+		// package name internal is ideal but Golang is not allowed, so we use private as package name.
667
+		private.RegisterRoutes(m)
668
+	})
669
+
664 670
 	// robots.txt
665 671
 	m.Get("/robots.txt", func(ctx *context.Context) {
666 672
 		if setting.HasRobotsTxt {

+ 4 - 2
models/ssh_key.go

@@ -502,8 +502,10 @@ func UpdatePublicKey(key *PublicKey) error {
502 502
 
503 503
 // UpdatePublicKeyUpdated updates public key use time.
504 504
 func UpdatePublicKeyUpdated(id int64) error {
505
-	cnt, err := x.ID(id).Cols("updated").Update(&PublicKey{
506
-		Updated: time.Now(),
505
+	now := time.Now()
506
+	cnt, err := x.ID(id).Cols("updated_unix").Update(&PublicKey{
507
+		Updated:     now,
508
+		UpdatedUnix: now.Unix(),
507 509
 	})
508 510
 	if err != nil {
509 511
 		return err

+ 5 - 0
modules/httplib/httplib.go

@@ -62,6 +62,11 @@ func newRequest(url, method string) *Request {
62 62
 	return &Request{url, &req, map[string]string{}, map[string]string{}, defaultSetting, &resp, nil}
63 63
 }
64 64
 
65
+// NewRequest returns *Request with specific method
66
+func NewRequest(url, method string) *Request {
67
+	return newRequest(url, method)
68
+}
69
+
65 70
 // Get returns *Request with GET method.
66 71
 func Get(url string) *Request {
67 72
 	return newRequest(url, "GET")

+ 53 - 0
modules/private/internal.go

@@ -0,0 +1,53 @@
1
+package private
2
+
3
+import (
4
+	"crypto/tls"
5
+	"encoding/json"
6
+	"fmt"
7
+	"net/http"
8
+
9
+	"code.gitea.io/gitea/modules/httplib"
10
+	"code.gitea.io/gitea/modules/log"
11
+	"code.gitea.io/gitea/modules/setting"
12
+)
13
+
14
+func newRequest(url, method string) *httplib.Request {
15
+	return httplib.NewRequest(url, method).Header("Authorization",
16
+		fmt.Sprintf("Bearer %s", setting.InternalToken))
17
+}
18
+
19
+// Response internal request response
20
+type Response struct {
21
+	Err string `json:"err"`
22
+}
23
+
24
+func decodeJSONError(resp *http.Response) *Response {
25
+	var res Response
26
+	err := json.NewDecoder(resp.Body).Decode(&res)
27
+	if err != nil {
28
+		res.Err = err.Error()
29
+	}
30
+	return &res
31
+}
32
+
33
+// UpdatePublicKeyUpdated update publick key updates
34
+func UpdatePublicKeyUpdated(keyID int64) error {
35
+	// Ask for running deliver hook and test pull request tasks.
36
+	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update", keyID)
37
+	log.GitLogger.Trace("UpdatePublicKeyUpdated: %s", reqURL)
38
+
39
+	resp, err := newRequest(reqURL, "POST").SetTLSClientConfig(&tls.Config{
40
+		InsecureSkipVerify: true,
41
+	}).Response()
42
+	if err != nil {
43
+		return err
44
+	}
45
+
46
+	defer resp.Body.Close()
47
+
48
+	// All 2XX status codes are accepted and others will return an error
49
+	if resp.StatusCode/100 != 2 {
50
+		return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err)
51
+	}
52
+	return nil
53
+}

+ 47 - 9
modules/setting/setting.go

@@ -27,6 +27,7 @@ import (
27 27
 	"code.gitea.io/gitea/modules/user"
28 28
 
29 29
 	"github.com/Unknwon/com"
30
+	"github.com/dgrijalva/jwt-go"
30 31
 	_ "github.com/go-macaron/cache/memcache" // memcache plugin for cache
31 32
 	_ "github.com/go-macaron/cache/redis"
32 33
 	"github.com/go-macaron/session"
@@ -442,14 +443,15 @@ var (
442 443
 	ShowFooterTemplateLoadTime bool
443 444
 
444 445
 	// Global setting objects
445
-	Cfg          *ini.File
446
-	CustomPath   string // Custom directory path
447
-	CustomConf   string
448
-	CustomPID    string
449
-	ProdMode     bool
450
-	RunUser      string
451
-	IsWindows    bool
452
-	HasRobotsTxt bool
446
+	Cfg           *ini.File
447
+	CustomPath    string // Custom directory path
448
+	CustomConf    string
449
+	CustomPID     string
450
+	ProdMode      bool
451
+	RunUser       string
452
+	IsWindows     bool
453
+	HasRobotsTxt  bool
454
+	InternalToken string // internal access token
453 455
 )
454 456
 
455 457
 // DateLang transforms standard language locale name to corresponding value in datetime plugin.
@@ -764,6 +766,43 @@ please consider changing to GITEA_CUSTOM`)
764 766
 	ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
765 767
 	MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(6)
766 768
 	ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
769
+	InternalToken = sec.Key("INTERNAL_TOKEN").String()
770
+	if len(InternalToken) == 0 {
771
+		secretBytes := make([]byte, 32)
772
+		_, err := io.ReadFull(rand.Reader, secretBytes)
773
+		if err != nil {
774
+			log.Fatal(4, "Error reading random bytes: %v", err)
775
+		}
776
+
777
+		secretKey := base64.RawURLEncoding.EncodeToString(secretBytes)
778
+
779
+		now := time.Now()
780
+		InternalToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
781
+			"nbf": now.Unix(),
782
+		}).SignedString([]byte(secretKey))
783
+
784
+		if err != nil {
785
+			log.Fatal(4, "Error generate internal token: %v", err)
786
+		}
787
+
788
+		// Save secret
789
+		cfgSave := ini.Empty()
790
+		if com.IsFile(CustomConf) {
791
+			// Keeps custom settings if there is already something.
792
+			if err := cfgSave.Append(CustomConf); err != nil {
793
+				log.Error(4, "Failed to load custom conf '%s': %v", CustomConf, err)
794
+			}
795
+		}
796
+
797
+		cfgSave.Section("security").Key("INTERNAL_TOKEN").SetValue(InternalToken)
798
+
799
+		if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
800
+			log.Fatal(4, "Failed to create '%s': %v", CustomConf, err)
801
+		}
802
+		if err := cfgSave.SaveTo(CustomConf); err != nil {
803
+			log.Fatal(4, "Error saving generated JWT Secret to custom config: %v", err)
804
+		}
805
+	}
767 806
 
768 807
 	sec = Cfg.Section("attachment")
769 808
 	AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))
@@ -940,7 +979,6 @@ var Service struct {
940 979
 	EnableOpenIDSignUp bool
941 980
 	OpenIDWhitelist    []*regexp.Regexp
942 981
 	OpenIDBlacklist    []*regexp.Regexp
943
-
944 982
 }
945 983
 
946 984
 func newService() {

+ 44 - 0
routers/private/internal.go

@@ -0,0 +1,44 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
6
+package private
7
+
8
+import (
9
+	"strings"
10
+
11
+	"code.gitea.io/gitea/models"
12
+	"code.gitea.io/gitea/modules/setting"
13
+	macaron "gopkg.in/macaron.v1"
14
+)
15
+
16
+// CheckInternalToken check internal token is set
17
+func CheckInternalToken(ctx *macaron.Context) {
18
+	tokens := ctx.Req.Header.Get("Authorization")
19
+	fields := strings.Fields(tokens)
20
+	if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
21
+		ctx.Error(403)
22
+	}
23
+}
24
+
25
+// UpdatePublicKey update publick key updates
26
+func UpdatePublicKey(ctx *macaron.Context) {
27
+	keyID := ctx.ParamsInt64(":id")
28
+	if err := models.UpdatePublicKeyUpdated(keyID); err != nil {
29
+		ctx.JSON(500, map[string]interface{}{
30
+			"err": err.Error(),
31
+		})
32
+		return
33
+	}
34
+
35
+	ctx.PlainText(200, []byte("success"))
36
+}
37
+
38
+// RegisterRoutes registers all internal APIs routes to web application.
39
+// These APIs will be invoked by internal commands for example `gitea serv` and etc.
40
+func RegisterRoutes(m *macaron.Macaron) {
41
+	m.Group("/", func() {
42
+		m.Post("/ssh/:id/update", UpdatePublicKey)
43
+	}, CheckInternalToken)
44
+}