2015-09-03 12:25:21 +02:00
package jira
import (
"bytes"
2020-05-03 15:38:32 +02:00
"context"
2019-09-03 23:53:37 +02:00
"crypto/sha256"
"encoding/hex"
2015-09-03 12:25:21 +02:00
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
2016-06-15 11:20:37 +02:00
"reflect"
2019-09-03 23:53:37 +02:00
"sort"
2018-05-10 16:12:40 +02:00
"strings"
2018-02-25 02:29:17 +02:00
"time"
2016-06-15 11:20:37 +02:00
2019-09-03 23:53:37 +02:00
"github.com/dgrijalva/jwt-go"
2016-06-15 11:20:37 +02:00
"github.com/google/go-querystring/query"
2018-02-25 02:29:17 +02:00
"github.com/pkg/errors"
2015-09-03 12:25:21 +02:00
)
2019-06-21 02:58:33 +02:00
// httpClient defines an interface for an http.Client implementation so that alternative
// http Clients can be passed in for making requests
type httpClient interface {
Do ( request * http . Request ) ( response * http . Response , err error )
}
2020-05-14 17:18:31 +02:00
// A Client manages communication with the Jira API.
2015-09-03 12:25:21 +02:00
type Client struct {
// HTTP client used to communicate with the API.
2019-06-21 02:58:33 +02:00
client httpClient
2015-09-03 12:25:21 +02:00
// Base URL for API requests.
baseURL * url . URL
2019-06-21 02:58:33 +02:00
// Session storage if the user authenticates with a Session cookie
2015-09-03 12:25:21 +02:00
session * Session
2020-05-14 17:18:31 +02:00
// Services used for talking to different parts of the Jira API.
2018-11-20 13:54:02 +02:00
Authentication * AuthenticationService
Issue * IssueService
Project * ProjectService
Board * BoardService
Sprint * SprintService
User * UserService
Group * GroupService
Version * VersionService
Priority * PriorityService
Field * FieldService
Component * ComponentService
Resolution * ResolutionService
StatusCategory * StatusCategoryService
Filter * FilterService
Role * RoleService
PermissionScheme * PermissionSchemeService
2019-08-19 04:59:05 +02:00
Status * StatusService
2019-10-13 18:56:07 +02:00
IssueLinkType * IssueLinkTypeService
2015-09-03 12:25:21 +02:00
}
2020-05-14 17:18:31 +02:00
// NewClient returns a new Jira API client.
2015-09-03 12:25:21 +02:00
// If a nil httpClient is provided, http.DefaultClient will be used.
2016-03-26 22:24:23 +02:00
// To use API methods which require authentication you can follow the preferred solution and
2015-09-03 12:25:21 +02:00
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
2020-05-14 17:18:31 +02:00
// baseURL is the HTTP endpoint of your Jira instance and should always be specified with a trailing slash.
2019-06-21 02:58:33 +02:00
func NewClient ( httpClient httpClient , baseURL string ) ( * Client , error ) {
2015-09-03 12:25:21 +02:00
if httpClient == nil {
httpClient = http . DefaultClient
}
2018-05-09 02:43:24 +02:00
// ensure the baseURL contains a trailing slash so that all paths are preserved in later calls
if ! strings . HasSuffix ( baseURL , "/" ) {
baseURL += "/"
}
2015-09-03 12:25:21 +02:00
parsedBaseURL , err := url . Parse ( baseURL )
if err != nil {
return nil , err
}
c := & Client {
client : httpClient ,
baseURL : parsedBaseURL ,
}
c . Authentication = & AuthenticationService { client : c }
c . Issue = & IssueService { client : c }
2016-05-29 18:42:38 +02:00
c . Project = & ProjectService { client : c }
2016-06-15 19:08:15 +02:00
c . Board = & BoardService { client : c }
2016-06-24 10:43:32 +02:00
c . Sprint = & SprintService { client : c }
2016-10-31 14:24:30 +02:00
c . User = & UserService { client : c }
2017-01-29 18:28:04 +02:00
c . Group = & GroupService { client : c }
2018-01-22 12:34:41 +02:00
c . Version = & VersionService { client : c }
2018-06-12 10:22:48 +02:00
c . Priority = & PriorityService { client : c }
2018-06-13 03:27:42 +02:00
c . Field = & FieldService { client : c }
2018-06-25 16:47:53 +02:00
c . Component = & ComponentService { client : c }
2018-06-25 17:48:53 +02:00
c . Resolution = & ResolutionService { client : c }
2018-06-26 09:41:46 +02:00
c . StatusCategory = & StatusCategoryService { client : c }
2018-10-09 00:18:43 +02:00
c . Filter = & FilterService { client : c }
2018-11-20 13:59:23 +02:00
c . Role = & RoleService { client : c }
2018-11-20 13:54:02 +02:00
c . PermissionScheme = & PermissionSchemeService { client : c }
2019-08-19 04:59:05 +02:00
c . Status = & StatusService { client : c }
2019-10-13 18:56:07 +02:00
c . IssueLinkType = & IssueLinkTypeService { client : c }
2015-09-03 12:25:21 +02:00
return c , nil
}
2020-05-03 15:38:32 +02:00
// NewRawRequestWithContext creates an API request.
2016-10-10 09:46:21 +02:00
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Allows using an optional native io.Reader for sourcing the request body.
2020-05-03 15:38:32 +02:00
func ( c * Client ) NewRawRequestWithContext ( ctx context . Context , method , urlStr string , body io . Reader ) ( * http . Request , error ) {
2016-10-10 09:46:21 +02:00
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2016-10-10 09:46:21 +02:00
u := c . baseURL . ResolveReference ( rel )
2020-05-03 15:38:32 +02:00
req , err := newRequestWithContext ( ctx , method , u . String ( ) , body )
2016-10-10 09:46:21 +02:00
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-10-10 09:46:21 +02:00
}
}
return req , nil
}
2020-05-03 15:38:32 +02:00
// NewRawRequest wraps NewRawRequestWithContext using the background context.
func ( c * Client ) NewRawRequest ( method , urlStr string , body io . Reader ) ( * http . Request , error ) {
return c . NewRawRequestWithContext ( context . Background ( ) , method , urlStr , body )
}
// NewRequestWithContext creates an API request.
2015-09-03 12:25:21 +02:00
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// If specified, the value pointed to by body is JSON encoded and included as the request body.
2020-05-03 15:38:32 +02:00
func ( c * Client ) NewRequestWithContext ( ctx context . Context , method , urlStr string , body interface { } ) ( * http . Request , error ) {
2015-09-03 12:25:21 +02:00
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2015-09-03 12:25:21 +02:00
u := c . baseURL . ResolveReference ( rel )
var buf io . ReadWriter
if body != nil {
buf = new ( bytes . Buffer )
2016-10-23 14:51:29 +02:00
err = json . NewEncoder ( buf ) . Encode ( body )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
}
2020-05-03 15:38:32 +02:00
req , err := newRequestWithContext ( ctx , method , u . String ( ) , buf )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-05-27 12:01:54 +02:00
}
2015-09-03 12:25:21 +02:00
}
return req , nil
}
2020-05-03 15:38:32 +02:00
// NewRequest wraps NewRequestWithContext using the background context.
func ( c * Client ) NewRequest ( method , urlStr string , body interface { } ) ( * http . Request , error ) {
return c . NewRequestWithContext ( context . Background ( ) , method , urlStr , body )
}
2016-06-15 11:20:37 +02:00
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions ( s string , opt interface { } ) ( string , error ) {
v := reflect . ValueOf ( opt )
if v . Kind ( ) == reflect . Ptr && v . IsNil ( ) {
return s , nil
}
u , err := url . Parse ( s )
if err != nil {
return s , err
}
qs , err := query . Values ( opt )
if err != nil {
return s , err
}
u . RawQuery = qs . Encode ( )
return u . String ( ) , nil
}
2020-05-03 15:38:32 +02:00
// NewMultiPartRequestWithContext creates an API request including a multi-part file.
2016-05-19 23:11:21 +02:00
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
2016-05-27 14:14:09 +02:00
// If specified, the value pointed to by buf is a multipart form.
2020-05-03 15:38:32 +02:00
func ( c * Client ) NewMultiPartRequestWithContext ( ctx context . Context , method , urlStr string , buf * bytes . Buffer ) ( * http . Request , error ) {
2016-05-19 23:11:21 +02:00
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2016-05-19 23:11:21 +02:00
u := c . baseURL . ResolveReference ( rel )
2020-05-03 15:38:32 +02:00
req , err := newRequestWithContext ( ctx , method , u . String ( ) , buf )
2016-05-19 23:11:21 +02:00
if err != nil {
return nil , err
}
// Set required headers
req . Header . Set ( "X-Atlassian-Token" , "nocheck" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-07-27 12:21:09 +02:00
}
2016-05-19 23:11:21 +02:00
}
return req , nil
}
2020-05-03 15:38:32 +02:00
// NewMultiPartRequest wraps NewMultiPartRequestWithContext using the background context.
func ( c * Client ) NewMultiPartRequest ( method , urlStr string , buf * bytes . Buffer ) ( * http . Request , error ) {
return c . NewMultiPartRequestWithContext ( context . Background ( ) , method , urlStr , buf )
}
2015-09-03 12:25:21 +02:00
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
2016-06-15 17:09:13 +02:00
func ( c * Client ) Do ( req * http . Request , v interface { } ) ( * Response , error ) {
httpResp , err := c . client . Do ( req )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
2016-06-15 17:09:13 +02:00
err = CheckResponse ( httpResp )
2015-09-03 12:25:21 +02:00
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further
2016-06-15 17:09:13 +02:00
return newResponse ( httpResp , nil ) , err
2015-09-03 12:25:21 +02:00
}
if v != nil {
2016-05-24 07:48:04 +02:00
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
2016-06-15 17:09:13 +02:00
defer httpResp . Body . Close ( )
err = json . NewDecoder ( httpResp . Body ) . Decode ( v )
2015-09-03 12:25:21 +02:00
}
2016-06-15 17:09:13 +02:00
resp := newResponse ( httpResp , v )
2015-09-03 12:25:21 +02:00
return resp , err
}
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
2016-05-28 20:15:17 +02:00
// The caller is responsible to analyze the response body.
2020-05-14 17:18:31 +02:00
// The body can contain JSON (if the error is intended) or xml (sometimes Jira just failes).
2015-09-03 12:25:21 +02:00
func CheckResponse ( r * http . Response ) error {
if c := r . StatusCode ; 200 <= c && c <= 299 {
return nil
}
style: Fix staticcheck (static analysis) errors for this library (#283)
* style: Fix staticcheck errors for "error strings should not be capitalized (ST1005)"
staticcheck is a static analysis tool for go.
It reports several "error strings should not be capitalized (ST1005)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "printf-style function with dynamic format ... (SA1006)"
staticcheck is a static analysis tool for go.
It reports several "printf-style function with dynamic format string and no further arguments should use print-style function instead (SA1006)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "type X is unused (U1000)"
staticcheck is a static analysis tool for go.
It reports several "type X is unused (U1000)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "should use X instead (S1003 & SA6005)"
staticcheck is a static analysis tool for go.
It reports several
- should use !bytes.Contains(b, []byte(`"password":"bar"`)) instead (S1003)
- should use strings.EqualFold instead (SA6005)
messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "unnecessary use of fmt.Sprintf (S1039)"
staticcheck is a static analysis tool for go.
It report several "unnecessary use of fmt.Sprintf (S1039)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "this value of X is never used (SA4006)"
staticcheck is a static analysis tool for go.
It report several "this value of X is never used (SA4006)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "redundant return statement (S1023)"
staticcheck is a static analysis tool for go.
It report several "redundant return statement (S1023)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "possible nil pointer dereference (SA5011)"
staticcheck is a static analysis tool for go.
It report several
file.go:Line:character: possible nil pointer dereference (SA5011)
file.go:Line:character: this check suggests that the pointer can be nil
messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
* style: Fix staticcheck errors for "this value of X is never used (SA4006)"
staticcheck is a static analysis tool for go.
It report several "this value of X is never used (SA4006)" messages.
Here, we fix it to be more compliant with the go coding styleguide.
Related: #280
2020-05-02 23:08:01 +02:00
err := fmt . Errorf ( "request failed. Please analyze the request body for more details. Status code: %d" , r . StatusCode )
2016-05-28 20:15:17 +02:00
return err
2015-09-03 12:25:21 +02:00
}
2016-06-03 18:18:01 +02:00
// GetBaseURL will return you the Base URL.
// This is the same URL as in the NewClient constructor
func ( c * Client ) GetBaseURL ( ) url . URL {
return * c . baseURL
}
2016-06-15 17:09:13 +02:00
2020-05-14 17:18:31 +02:00
// Response represents Jira API response. It wraps http.Response returned from
2016-06-15 17:09:13 +02:00
// API and provides information about paging.
type Response struct {
* http . Response
StartAt int
MaxResults int
Total int
}
func newResponse ( r * http . Response , v interface { } ) * Response {
resp := & Response { Response : r }
resp . populatePageValues ( v )
return resp
}
// Sets paging values if response json was parsed to searchResult type
// (can be extended with other types if they also need paging info)
func ( r * Response ) populatePageValues ( v interface { } ) {
switch value := v . ( type ) {
case * searchResult :
r . StartAt = value . StartAt
r . MaxResults = value . MaxResults
r . Total = value . Total
2018-03-16 12:01:13 +02:00
case * groupMembersResult :
r . StartAt = value . StartAt
r . MaxResults = value . MaxResults
r . Total = value . Total
2016-06-15 17:09:13 +02:00
}
}
2018-02-24 21:27:46 +02:00
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
// using HTTP Basic Authentication with the provided username and password.
type BasicAuthTransport struct {
Username string
Password string
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http . RoundTripper
}
// RoundTrip implements the RoundTripper interface. We just add the
// basic auth and return the RoundTripper for this transport type.
func ( t * BasicAuthTransport ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
2018-02-25 02:29:17 +02:00
req2 := cloneRequest ( req ) // per RoundTripper contract
req2 . SetBasicAuth ( t . Username , t . Password )
return t . transport ( ) . RoundTrip ( req2 )
2018-02-24 21:27:46 +02:00
}
// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func ( t * BasicAuthTransport ) Client ( ) * http . Client {
return & http . Client { Transport : t }
}
func ( t * BasicAuthTransport ) transport ( ) http . RoundTripper {
if t . Transport != nil {
return t . Transport
}
return http . DefaultTransport
}
2018-02-25 02:29:17 +02:00
// CookieAuthTransport is an http.RoundTripper that authenticates all requests
// using Jira's cookie-based authentication.
//
2019-09-03 23:53:37 +02:00
// Note that it is generally preferable to use HTTP BASIC authentication with the REST API.
2020-05-14 17:18:31 +02:00
// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user).
2018-02-25 02:29:17 +02:00
//
2020-05-14 17:18:31 +02:00
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
2018-02-25 02:29:17 +02:00
type CookieAuthTransport struct {
Username string
Password string
2018-02-28 08:50:02 +02:00
AuthURL string
2018-02-25 02:29:17 +02:00
// SessionObject is the authenticated cookie string.s
// It's passed in each call to prove the client is authenticated.
2018-02-28 08:50:02 +02:00
SessionObject [ ] * http . Cookie
2018-02-25 02:29:17 +02:00
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http . RoundTripper
}
// RoundTrip adds the session object to the request.
func ( t * CookieAuthTransport ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
2018-02-28 08:50:02 +02:00
if t . SessionObject == nil {
2018-02-25 02:29:17 +02:00
err := t . setSessionObject ( )
if err != nil {
return nil , errors . Wrap ( err , "cookieauth: no session object has been set" )
}
}
req2 := cloneRequest ( req ) // per RoundTripper contract
2018-02-28 08:50:02 +02:00
for _ , cookie := range t . SessionObject {
2018-09-05 23:23:34 +02:00
// Don't add an empty value cookie to the request
if cookie . Value != "" {
req2 . AddCookie ( cookie )
}
2018-02-25 02:29:17 +02:00
}
return t . transport ( ) . RoundTrip ( req2 )
}
// Client returns an *http.Client that makes requests that are authenticated
// using cookie authentication
func ( t * CookieAuthTransport ) Client ( ) * http . Client {
return & http . Client { Transport : t }
}
// setSessionObject attempts to authenticate the user and set
// the session object (e.g. cookie)
func ( t * CookieAuthTransport ) setSessionObject ( ) error {
2018-02-28 08:50:02 +02:00
req , err := t . buildAuthRequest ( )
2018-02-25 02:29:17 +02:00
if err != nil {
return err
}
var authClient = & http . Client {
Timeout : time . Second * 60 ,
}
resp , err := authClient . Do ( req )
if err != nil {
return err
}
2018-02-28 08:50:02 +02:00
t . SessionObject = resp . Cookies ( )
2018-02-25 02:29:17 +02:00
return nil
}
// getAuthRequest assembles the request to get the authenticated cookie
2018-02-28 08:50:02 +02:00
func ( t * CookieAuthTransport ) buildAuthRequest ( ) ( * http . Request , error ) {
2018-02-25 02:29:17 +02:00
body := struct {
Username string ` json:"username" `
Password string ` json:"password" `
} {
t . Username ,
t . Password ,
}
b := new ( bytes . Buffer )
json . NewEncoder ( b ) . Encode ( body )
2018-02-28 08:50:02 +02:00
req , err := http . NewRequest ( "POST" , t . AuthURL , b )
2018-02-25 02:29:17 +02:00
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
return req , nil
}
func ( t * CookieAuthTransport ) transport ( ) http . RoundTripper {
if t . Transport != nil {
return t . Transport
}
return http . DefaultTransport
}
2019-09-03 23:53:37 +02:00
// JWTAuthTransport is an http.RoundTripper that authenticates all requests
// using Jira's JWT based authentication.
//
// NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace.
//
2020-05-14 17:18:31 +02:00
// Jira docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
2019-09-03 23:53:37 +02:00
// Examples in other languages:
// https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb
// https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py
type JWTAuthTransport struct {
Secret [ ] byte
Issuer string
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http . RoundTripper
}
func ( t * JWTAuthTransport ) Client ( ) * http . Client {
return & http . Client { Transport : t }
}
func ( t * JWTAuthTransport ) transport ( ) http . RoundTripper {
if t . Transport != nil {
return t . Transport
}
return http . DefaultTransport
}
// RoundTrip adds the session object to the request.
func ( t * JWTAuthTransport ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
req2 := cloneRequest ( req ) // per RoundTripper contract
exp := time . Duration ( 59 ) * time . Second
qsh := t . createQueryStringHash ( req . Method , req2 . URL )
token := jwt . NewWithClaims ( jwt . SigningMethodHS256 , jwt . MapClaims {
"iss" : t . Issuer ,
"iat" : time . Now ( ) . Unix ( ) ,
"exp" : time . Now ( ) . Add ( exp ) . Unix ( ) ,
"qsh" : qsh ,
} )
jwtStr , err := token . SignedString ( t . Secret )
if err != nil {
return nil , errors . Wrap ( err , "jwtAuth: error signing JWT" )
}
req2 . Header . Set ( "Authorization" , fmt . Sprintf ( "JWT %s" , jwtStr ) )
return t . transport ( ) . RoundTrip ( req2 )
}
func ( t * JWTAuthTransport ) createQueryStringHash ( httpMethod string , jiraURL * url . URL ) string {
canonicalRequest := t . canonicalizeRequest ( httpMethod , jiraURL )
h := sha256 . Sum256 ( [ ] byte ( canonicalRequest ) )
return hex . EncodeToString ( h [ : ] )
}
func ( t * JWTAuthTransport ) canonicalizeRequest ( httpMethod string , jiraURL * url . URL ) string {
path := "/" + strings . Replace ( strings . Trim ( jiraURL . Path , "/" ) , "&" , "%26" , - 1 )
var canonicalQueryString [ ] string
for k , v := range jiraURL . Query ( ) {
if k == "jwt" {
continue
}
param := url . QueryEscape ( k )
value := url . QueryEscape ( strings . Join ( v , "" ) )
canonicalQueryString = append ( canonicalQueryString , strings . Replace ( strings . Join ( [ ] string { param , value } , "=" ) , "+" , "%20" , - 1 ) )
}
sort . Strings ( canonicalQueryString )
return fmt . Sprintf ( "%s&%s&%s" , strings . ToUpper ( httpMethod ) , path , strings . Join ( canonicalQueryString , "&" ) )
}
2018-02-25 02:29:17 +02:00
// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest ( r * http . Request ) * http . Request {
// shallow copy of the struct
r2 := new ( http . Request )
* r2 = * r
// deep copy of the Header
r2 . Header = make ( http . Header , len ( r . Header ) )
for k , s := range r . Header {
r2 . Header [ k ] = append ( [ ] string ( nil ) , s ... )
}
return r2
}