github.com/mistwind/reviewdog@v0.0.0-20230322024206-9cfa11856d58/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/mistwind/reviewdog/doghouse"
    14  	"github.com/mistwind/reviewdog/doghouse/server"
    15  	"github.com/mistwind/reviewdog/doghouse/server/ciutil"
    16  	"github.com/mistwind/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  		http.Error(w, fmt.Sprintf("failed to decode request: %v", err), http.StatusBadRequest)
    37  		return
    38  	}
    39  
    40  	// Check authorization.
    41  	if !gc.validateCheckRequest(ctx, w, r, req.Owner, req.Repo) {
    42  		return
    43  	}
    44  
    45  	opt := &server.NewGitHubClientOption{
    46  		PrivateKey:    gc.privateKey,
    47  		IntegrationID: gc.integrationID,
    48  		RepoOwner:     req.Owner,
    49  		Client: &http.Client{
    50  			Transport: gc.tr,
    51  		},
    52  	}
    53  
    54  	gh, err := server.NewGitHubClient(ctx, opt)
    55  	if err != nil {
    56  		aelog.Errorf(ctx, "failed to create GitHub client: %v", err)
    57  		w.WriteHeader(http.StatusBadRequest)
    58  		fmt.Fprintln(w, err)
    59  		return
    60  	}
    61  
    62  	res, err := server.NewChecker(&req, gh).Check(ctx)
    63  	if err != nil {
    64  		aelog.Errorf(ctx, "failed to run checker: %v", err)
    65  		w.WriteHeader(http.StatusBadRequest)
    66  		fmt.Fprintln(w, err)
    67  		return
    68  	}
    69  	if err := json.NewEncoder(w).Encode(res); err != nil {
    70  		w.WriteHeader(http.StatusBadRequest)
    71  		fmt.Fprintln(w, err)
    72  		return
    73  	}
    74  }
    75  
    76  func (gc *githubChecker) validateCheckRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, owner, repo string) bool {
    77  	if extractBearerToken(r) == "" {
    78  		// Update Travis IP Address before checking IP to reduce the # of
    79  		// flaky errors when token is not present.
    80  		if err := ciutil.UpdateTravisCIIPAddrs(&http.Client{}); err != nil {
    81  			aelog.Errorf(ctx, "failed to update travis CI IP addresses: %v", err)
    82  		}
    83  	}
    84  	aelog.Infof(ctx, "Remote Addr: %s", ciutil.IPFromReq(r))
    85  	if ciutil.IsFromCI(r) {
    86  		// Skip token validation if it's from trusted CI providers.
    87  		return true
    88  	}
    89  	return gc.validateCheckToken(ctx, w, r, owner, repo)
    90  }
    91  
    92  func (gc *githubChecker) validateCheckToken(ctx context.Context, w http.ResponseWriter, r *http.Request, owner, repo string) bool {
    93  	token := extractBearerToken(r)
    94  	if token == "" {
    95  		w.Header().Set("The WWW-Authenticate", `error="invalid_request", error_description="The access token not provided"`)
    96  		msg := fmt.Sprintf("The access token not provided. Get token from %s", githubRepoURL(ctx, r, owner, repo))
    97  		http.Error(w, msg, http.StatusUnauthorized)
    98  		return false
    99  	}
   100  	_, wantToken, err := gc.ghRepoTokenStore.Get(ctx, owner, repo)
   101  	if err != nil {
   102  		aelog.Errorf(ctx, "failed to get repository (%s/%s) token: %v", owner, repo, err)
   103  	}
   104  	if wantToken == nil {
   105  		w.WriteHeader(http.StatusNotFound)
   106  		return false
   107  	}
   108  	if token != wantToken.Token {
   109  		w.Header().Set("The WWW-Authenticate", `error="invalid_token", error_description="The access token is invalid"`)
   110  		msg := fmt.Sprintf("The access token is invalid. Get valid token from %s", githubRepoURL(ctx, r, owner, repo))
   111  		http.Error(w, msg, http.StatusUnauthorized)
   112  		return false
   113  	}
   114  	return true
   115  }
   116  
   117  func githubRepoURL(ctx context.Context, r *http.Request, owner, repo string) string {
   118  	u := doghouseBaseURL(ctx, r)
   119  	u.Path = fmt.Sprintf("/gh/%s/%s", owner, repo)
   120  	return u.String()
   121  }
   122  
   123  func doghouseBaseURL(ctx context.Context, r *http.Request) *url.URL {
   124  	scheme := ""
   125  	if r.URL != nil && r.URL.Scheme != "" {
   126  		scheme = r.URL.Scheme
   127  	}
   128  	if scheme == "" {
   129  		scheme = "https"
   130  	}
   131  	u, err := url.Parse(scheme + "://" + r.Host)
   132  	if err != nil {
   133  		aelog.Errorf(ctx, "%v", err)
   134  	}
   135  	return u
   136  }
   137  
   138  func extractBearerToken(r *http.Request) string {
   139  	auth := r.Header.Get("Authorization")
   140  	prefix := "bearer "
   141  	if strings.HasPrefix(strings.ToLower(auth), prefix) {
   142  		return auth[len(prefix):]
   143  	}
   144  	return ""
   145  }