Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKrasimir Angelov <kangelov@gitlab.com>2019-10-18 00:27:10 +0300
committerKrasimir Angelov <kangelov@gitlab.com>2019-11-12 03:50:52 +0300
commit46873b1439eb3bed30cbb834fde44ffede1f0371 (patch)
tree7db495db4ec7c2145535fbde3d5c1a2972081427
parent7edf2444b7e4affcf370927edb1d7ee4c95d293d (diff)
Add HTTP client to consume GitLab internal API for Pages
At the moment this supports only getting virtual domain configuration for given host. Related to https://gitlab.com/gitlab-org/gitlab-pages/issues/253.
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--internal/source/gitlab/client.go137
-rw-r--r--internal/source/gitlab/client_test.go120
-rw-r--r--internal/source/gitlab/response.go21
5 files changed, 281 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index 37325f2b..f6f5d046 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.12
require (
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67 // indirect
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
github.com/getsentry/raven-go v0.1.2 // indirect
github.com/golang/mock v1.3.1
diff --git a/go.sum b/go.sum
index 01a25859..ad380534 100644
--- a/go.sum
+++ b/go.sum
@@ -17,6 +17,8 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs=
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8=
diff --git a/internal/source/gitlab/client.go b/internal/source/gitlab/client.go
new file mode 100644
index 00000000..bd422a4e
--- /dev/null
+++ b/internal/source/gitlab/client.go
@@ -0,0 +1,137 @@
+package gitlab
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/url"
+ "time"
+
+ jwt "github.com/dgrijalva/jwt-go"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
+)
+
+// Client is a HTTP client to access Pages internal API
+type Client struct {
+ secretKey []byte
+ baseURL *url.URL
+ httpClient *http.Client
+}
+
+var (
+ errUnknown = errors.New("Unknown")
+ errNoContent = errors.New("No Content")
+ errUnauthorized = errors.New("Unauthorized")
+ errNotFound = errors.New("Not Found")
+)
+
+// NewClient initializes and returns new Client
+func NewClient(baseURL string, secretKey []byte) (*Client, error) {
+ url, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{
+ secretKey: secretKey,
+ baseURL: url,
+ httpClient: &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: httptransport.Transport,
+ },
+ }, nil
+}
+
+// GetVirtualDomain returns VirtualDomain configuration for the given host
+func (gc *Client) GetVirtualDomain(host string) (*VirtualDomain, error) {
+ params := map[string]string{"host": host}
+
+ resp, err := gc.get("/api/v4/internal/pages", params)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var domain VirtualDomain
+ err = json.NewDecoder(resp.Body).Decode(&domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return &domain, nil
+}
+
+func (gc *Client) get(path string, params map[string]string) (*http.Response, error) {
+ endpoint, err := gc.endpoint(path, params)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := gc.request("GET", endpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := gc.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ switch {
+ case resp.StatusCode == http.StatusOK:
+ return resp, nil
+ case resp.StatusCode == http.StatusNoContent:
+ return resp, errNoContent
+ case resp.StatusCode == http.StatusUnauthorized:
+ return resp, errUnauthorized
+ case resp.StatusCode == http.StatusNotFound:
+ return resp, errNotFound
+ default:
+ return resp, errUnknown
+ }
+}
+
+func (gc *Client) endpoint(path string, params map[string]string) (*url.URL, error) {
+ endpoint, err := gc.baseURL.Parse(path)
+ if err != nil {
+ return nil, err
+ }
+
+ values := url.Values{}
+ for key, value := range params {
+ values.Add(key, value)
+ }
+ endpoint.RawQuery = values.Encode()
+
+ return endpoint, nil
+}
+
+func (gc *Client) request(method string, endpoint *url.URL) (*http.Request, error) {
+ req, err := http.NewRequest("GET", endpoint.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ token, err := gc.token()
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Gitlab-Pages-Api-Request", token)
+
+ return req, nil
+}
+
+func (gc *Client) token() (string, error) {
+ claims := jwt.StandardClaims{
+ Issuer: "gitlab-pages",
+ ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
+ }
+
+ token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(gc.secretKey)
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/internal/source/gitlab/client_test.go b/internal/source/gitlab/client_test.go
new file mode 100644
index 00000000..d8c86fad
--- /dev/null
+++ b/internal/source/gitlab/client_test.go
@@ -0,0 +1,120 @@
+package gitlab
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ jwt "github.com/dgrijalva/jwt-go"
+)
+
+var (
+ encodedSecret = "e41rcFh7XBA7sNABWVCe2AZvxMsy6QDtJ8S9Ql1UiN8=" // 32 bytes, base64 encoded
+)
+
+func TestNewValidBaseURL(t *testing.T) {
+ _, err := NewClient("https://gitlab.com", secretKey())
+ require.NoError(t, err)
+}
+
+func TestNewInvalidBaseURL(t *testing.T) {
+ client, err := NewClient("%", secretKey())
+ require.Error(t, err)
+ require.Nil(t, client)
+}
+
+func TestGetVirtualDomainForErrorResponses(t *testing.T) {
+ tests := map[int]string{
+ http.StatusNoContent: "No Content",
+ http.StatusUnauthorized: "Unauthorized",
+ http.StatusNotFound: "Not Found",
+ }
+
+ for statusCode, expectedError := range tests {
+ name := fmt.Sprintf("%d %s", statusCode, expectedError)
+ t.Run(name, func(t *testing.T) {
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("/api/v4/internal/pages", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(statusCode)
+ })
+
+ server := httptest.NewServer(mux)
+ defer server.Close()
+
+ client, _ := NewClient(server.URL, secretKey())
+
+ actual, err := client.GetVirtualDomain("group.gitlab.io")
+
+ require.EqualError(t, err, expectedError)
+ require.Nil(t, actual)
+ })
+ }
+}
+
+func TestGetVirtualDomainAuthenticatedRequest(t *testing.T) {
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("/api/v4/internal/pages", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "GET", r.Method)
+ assert.Equal(t, "group.gitlab.io", r.FormValue("host"))
+
+ if checkRequest(r.Header.Get("Gitlab-Pages-Api-Request")) {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, `{"certificate":"foo","key":"bar","lookup_paths":[{"project_id":123,"access_control":false,"source":{"type":"file","path":"mygroup/myproject/public/"},"https_only":true,"prefix":"/myproject/"}]}`)
+ } else {
+ w.WriteHeader(http.StatusUnauthorized)
+ }
+ })
+
+ server := httptest.NewServer(mux)
+ defer server.Close()
+
+ client, _ := NewClient(server.URL, secretKey())
+
+ actual, err := client.GetVirtualDomain("group.gitlab.io")
+ require.NoError(t, err)
+
+ require.Equal(t, "foo", actual.Certificate)
+ require.Equal(t, "bar", actual.Key)
+
+ lookupPath := actual.LookupPaths[0]
+ require.Equal(t, 123, lookupPath.ProjectID)
+ require.Equal(t, false, lookupPath.AccessControl)
+ require.Equal(t, true, lookupPath.HTTPSOnly)
+ require.Equal(t, "/myproject/", lookupPath.Prefix)
+
+ require.Equal(t, "file", lookupPath.Source.Type)
+ require.Equal(t, "mygroup/myproject/public/", lookupPath.Source.Path)
+}
+
+func checkRequest(tokenString string) bool {
+ token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
+ }
+
+ return secretKey(), nil
+ })
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok || !token.Valid {
+ return false
+ }
+
+ if _, ok := claims["exp"]; !ok {
+ return false
+ }
+
+ return claims["iss"] == "gitlab-pages"
+}
+
+func secretKey() []byte {
+ secretKey, _ := base64.StdEncoding.DecodeString(encodedSecret)
+ return secretKey
+}
diff --git a/internal/source/gitlab/response.go b/internal/source/gitlab/response.go
new file mode 100644
index 00000000..20597362
--- /dev/null
+++ b/internal/source/gitlab/response.go
@@ -0,0 +1,21 @@
+package gitlab
+
+// LookupPath represents a lookup path for a GitLab Pages virtual domain
+type LookupPath struct {
+ ProjectID int `json:"project_id,omitempty"`
+ AccessControl bool `json:"access_control,omitempty"`
+ HTTPSOnly bool `json:"https_only,omitempty"`
+ Prefix string `json:"prefix,omitempty"`
+ Source struct {
+ Type string `json:"type,omitempty"`
+ Path string `json:"path,omitempty"`
+ }
+}
+
+// VirtualDomain represents a GitLab Pages virtual domain
+type VirtualDomain struct {
+ Certificate string `json:"certificate,omitempty"`
+ Key string `json:"key,omitempty"`
+
+ LookupPaths []LookupPath `json:"lookup_paths"`
+}