github.com/friedemannf/reviewdog@v0.14.0/doghouse/appengine/checker.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/vvakame/sdlog/aelog"
    12  
    13  	"github.com/friedemannf/reviewdog/doghouse"
    14  	"github.com/friedemannf/reviewdog/doghouse/server"
    15  	"github.com/friedemannf/reviewdog/doghouse/server/ciutil"
    16  	"github.com/friedemannf/reviewdog/doghouse/server/storage"
    17  )
    18  
    19  type githubChecker struct {
    20  	privateKey       []byte
    21  	integrationID    int
    22  	ghInstStore      storage.GitHubInstallationStore
    23  	ghRepoTokenStore storage.GitHubRepositoryTokenStore
    24  	tr               http.RoundTripper
    25  }
    26  
    27  func (gc *githubChecker) handleCheck(w http.ResponseWriter, r *http.Request) {
    28  	if r.Method != http.MethodPost {
    29  		w.WriteHeader(http.StatusMethodNotAllowed)
    30  		return
    31  	}
    32  	ctx := r.Context()
    33  
    34  	var req doghouse.CheckRequest
    35  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    36  		w.WriteHeader(http.StatusBadRequest)
    37  		fmt.Fprintf(w, "failed to decode request: %v", err)
    38  		return
    39  	}
    40  
    41  	// Check authorization.
    42  	if !gc.validateCheckRequest(ctx, w, r, req.Owner, req.Repo) {
    43  		return
    44  	}
    45  
    46  	opt := &server.NewGitHubClientOption{
    47  		PrivateKey:    gc.privateKey,
    48  		IntegrationID: gc.integrationID,
    49  		RepoOwner:     req.Owner,
    50  		Client: &http.Client{
    51  			Transport: gc.tr,
    52  		},
    53  	}
    54  
    55  	gh, err := server.NewGitHubClient(ctx, opt)
    56  	if err != nil {
    57  		aelog.Errorf(ctx, "failed to create GitHub client: %v", err)
    58  		w.WriteHeader(http.StatusBadRequest)
    59  		fmt.Fprintln(w, err)
    60  		return
    61  	}
    62  
    63  	res, err := server.NewChecker(&req, gh).Check(ctx)
    64  	if err != nil {
    65  		aelog.Errorf(ctx, "failed to run checker: %v", err)
    66  		w.WriteHeader(http.StatusBadRequest)
    67  		fmt.Fprintln(w, err)
    68  		return
    69  	}
    70  	if err := json.NewEncoder(w).Encode(res); err != nil {
    71  		w.WriteHeader(http.StatusBadRequest)
    72  		fmt.Fprintln(w, err)
    73  		return
    74  	}
    75  }
    76  
    77  func (gc *githubChecker) validateCheckRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, owner, repo string) bool {
    78  	if extractBearerToken(r) == "" {
    79  		// Update Travis IP Address before checking IP to reduce the # of
    80  		// flaky errors when token is not present.
    81  		if err := ciutil.UpdateTravisCIIPAddrs(&http.Client{}); err != nil {
    82  			aelog.Errorf(ctx, "failed to update travis CI IP addresses: %v", err)
    83  		}
    84  	}
    85  	aelog.Infof(ctx, "Remote Addr: %s", ciutil.IPFromReq(r))
    86  	if ciutil.IsFromCI(r) {
    87  		// Skip token validation if it's from trusted CI providers.
    88  		return true
    89  	}
    90  	return gc.validateCheckToken(ctx, w, r, owner, repo)
    91  }
    92  
    93  func (gc *githubChecker) validateCheckToken(ctx context.Context, w http.ResponseWriter, r *http.Request, owner, repo string) bool {
    94  	token := extractBearerToken(r)
    95  	if token == "" {
    96  		w.Header().Set("The WWW-Authenticate", `error="invalid_request", error_description="The access token not provided"`)
    97  		w.WriteHeader(http.StatusUnauthorized)
    98  		fmt.Fprintf(w, "The access token not provided. Get token from %s", githubRepoURL(ctx, r, owner, repo))
    99  		return false
   100  	}
   101  	_, wantToken, err := gc.ghRepoTokenStore.Get(ctx, owner, repo)
   102  	if err != nil {
   103  		aelog.Errorf(ctx, "failed to get repository (%s/%s) token: %v", owner, repo, err)
   104  	}
   105  	if wantToken == nil {
   106  		w.WriteHeader(http.StatusNotFound)
   107  		return false
   108  	}
   109  	if token != wantToken.Token {
   110  		w.Header().Set("The WWW-Authenticate", `error="invalid_token", error_description="The access token is invalid"`)
   111  		w.WriteHeader(http.StatusUnauthorized)
   112  		fmt.Fprintf(w, "The access token is invalid. Get valid token from %s", githubRepoURL(ctx, r, owner, repo))
   113  		return false
   114  	}
   115  	return true
   116  }
   117  
   118  func githubRepoURL(ctx context.Context, r *http.Request, owner, repo string) string {
   119  	u := doghouseBaseURL(ctx, r)
   120  	u.Path = fmt.Sprintf("/gh/%s/%s", owner, repo)
   121  	return u.String()
   122  }
   123  
   124  func doghouseBaseURL(ctx context.Context, r *http.Request) *url.URL {
   125  	scheme := ""
   126  	if r.URL != nil && r.URL.Scheme != "" {
   127  		scheme = r.URL.Scheme
   128  	}
   129  	if scheme == "" {
   130  		scheme = "https"
   131  	}
   132  	u, err := url.Parse(scheme + "://" + r.Host)
   133  	if err != nil {
   134  		aelog.Errorf(ctx, "%v", err)
   135  	}
   136  	return u
   137  }
   138  
   139  func extractBearerToken(r *http.Request) string {
   140  	auth := r.Header.Get("Authorization")
   141  	prefix := "bearer "
   142  	if strings.HasPrefix(strings.ToLower(auth), prefix) {
   143  		return auth[len(prefix):]
   144  	}
   145  	return ""
   146  }