Skip to content

Commit 62e2c68

Browse files
authored
Merge pull request #59 from deploymenttheory/dev
Dev
2 parents 9edadb1 + fb2cf1d commit 62e2c68

18 files changed

+739
-548
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// github_api_handler.go
2+
package github
3+
4+
import "github.com/deploymenttheory/go-api-http-client/logger"
5+
6+
// GitHubHandler implements the APIHandler interface for the GitHub API.
7+
type GitHubAPIHandler struct {
8+
OverrideBaseDomain string // OverrideBaseDomain is used to override the base domain for URL construction.
9+
InstanceName string // InstanceName is the name of the GitHub instance.
10+
Logger logger.Logger // Logger is the structured logger used for logging.
11+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// github_handler_constants.go
2+
package github
3+
4+
// Endpoint constants represent the URL suffixes used for GitHub token interactions.
5+
const (
6+
APIName = "github" // APIName: represents the name of the API.
7+
DefaultBaseDomain = "api.github.com" // DefaultBaseDomain: represents the base domain for the github instance.
8+
OAuthTokenEndpoint = "github.com/login/oauth/access_token" // OAuthTokenEndpoint: The endpoint to obtain an OAuth token.
9+
BearerTokenEndpoint = "" // BearerTokenEndpoint: The endpoint to obtain a bearer token.
10+
TokenRefreshEndpoint = "github.com/login/oauth/access_token" // TokenRefreshEndpoint: The endpoint to refresh an existing token.
11+
TokenInvalidateEndpoint = "api.github.com/applications/:client_id/token" // TokenInvalidateEndpoint: The endpoint to invalidate an active token.
12+
BearerTokenAuthenticationSupport = false // BearerTokenAuthSuppport: A boolean to indicate if the API supports bearer token authentication.
13+
OAuthAuthenticationSupport = true // OAuthAuthSuppport: A boolean to indicate if the API supports OAuth authentication.
14+
OAuthWithCertAuthenticationSupport = true // OAuthWithCertAuthSuppport: A boolean to indicate if the API supports OAuth with client certificate authentication.
15+
)
16+
17+
// GetDefaultBaseDomain returns the default base domain used for constructing API URLs to the http client.
18+
func (g *GitHubAPIHandler) GetDefaultBaseDomain() string {
19+
return DefaultBaseDomain
20+
}
21+
22+
// GetOAuthTokenEndpoint returns the endpoint for obtaining an OAuth token. Used for constructing API URLs for the http client.
23+
func (g *GitHubAPIHandler) GetOAuthTokenEndpoint() string {
24+
return OAuthTokenEndpoint
25+
}
26+
27+
// GetBearerTokenEndpoint returns the endpoint for obtaining a bearer token. Used for constructing API URLs for the http client.
28+
func (g *GitHubAPIHandler) GetBearerTokenEndpoint() string {
29+
return BearerTokenEndpoint
30+
}
31+
32+
// GetTokenRefreshEndpoint returns the endpoint for refreshing an existing token. Used for constructing API URLs for the http client.
33+
func (g *GitHubAPIHandler) GetTokenRefreshEndpoint() string {
34+
return TokenRefreshEndpoint
35+
}
36+
37+
// GetTokenInvalidateEndpoint returns the endpoint for invalidating an active token. Used for constructing API URLs for the http client.
38+
func (g *GitHubAPIHandler) GetTokenInvalidateEndpoint() string {
39+
return TokenInvalidateEndpoint
40+
}
41+
42+
// GetAPIBearerTokenAuthenticationSupportStatus returns a boolean indicating if bearer token authentication is supported in the api handler.
43+
func (g *GitHubAPIHandler) GetAPIBearerTokenAuthenticationSupportStatus() bool {
44+
return BearerTokenAuthenticationSupport
45+
}
46+
47+
// GetAPIOAuthAuthenticationSupportStatus returns a boolean indicating if OAuth authentication is supported in the api handler.
48+
func (g *GitHubAPIHandler) GetAPIOAuthAuthenticationSupportStatus() bool {
49+
return OAuthAuthenticationSupport
50+
}
51+
52+
// GetAPIOAuthWithCertAuthenticationSupportStatus returns a boolean indicating if OAuth with client certificate authentication is supported in the api handler.
53+
func (g *GitHubAPIHandler) GetAPIOAuthWithCertAuthenticationSupportStatus() bool {
54+
return OAuthWithCertAuthenticationSupport
55+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// graph_api_error_messages.go
2+
package graph
3+
4+
import (
5+
"bytes"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"strings"
10+
11+
"github.com/PuerkitoBio/goquery"
12+
)
13+
14+
// APIHandlerError represents an error response from the Jamf Pro API.
15+
type APIHandlerError struct {
16+
HTTPStatusCode int `json:"httpStatusCode"`
17+
ErrorType string `json:"errorType"`
18+
ErrorMessage string `json:"errorMessage"`
19+
ExtraDetails map[string]interface{} `json:"extraDetails"`
20+
}
21+
22+
// ReturnAPIErrorResponse parses an HTTP error response from the Jamf Pro API.
23+
func (g *GraphAPIHandler) ReturnAPIErrorResponse(resp *http.Response) *APIHandlerError {
24+
var errorMessage, errorType string
25+
var extraDetails map[string]interface{}
26+
27+
// Safely read the response body
28+
bodyBytes, readErr := io.ReadAll(resp.Body)
29+
if readErr != nil {
30+
return &APIHandlerError{
31+
HTTPStatusCode: resp.StatusCode,
32+
ErrorType: "ReadError",
33+
ErrorMessage: "Failed to read response body",
34+
}
35+
}
36+
37+
// Ensure the body can be re-read for subsequent operations
38+
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
39+
40+
contentType := resp.Header.Get("Content-Type")
41+
42+
// Handle JSON content type
43+
if strings.Contains(contentType, "application/json") {
44+
description, parseErr := ParseJSONErrorResponse(bodyBytes)
45+
if parseErr == nil {
46+
errorMessage = description
47+
errorType = "JSONError"
48+
} else {
49+
errorMessage = "Failed to parse JSON error response: " + parseErr.Error()
50+
}
51+
} else if strings.Contains(contentType, "text/html") {
52+
// Handle HTML content type
53+
bodyBytes, err := io.ReadAll(resp.Body)
54+
if err == nil {
55+
errorMessage = ExtractErrorMessageFromHTML(string(bodyBytes))
56+
errorType = "HTMLError"
57+
} else {
58+
errorMessage = "Failed to read response body for HTML error parsing"
59+
}
60+
} else {
61+
// Fallback for unhandled content types
62+
errorMessage = "An unknown error occurred"
63+
}
64+
65+
return &APIHandlerError{
66+
HTTPStatusCode: resp.StatusCode,
67+
ErrorType: errorType,
68+
ErrorMessage: errorMessage,
69+
ExtraDetails: extraDetails,
70+
}
71+
}
72+
73+
// ExtractErrorMessageFromHTML attempts to parse an HTML error page and extract a combined human-readable error message.
74+
func ExtractErrorMessageFromHTML(htmlContent string) string {
75+
r := bytes.NewReader([]byte(htmlContent))
76+
doc, err := goquery.NewDocumentFromReader(r)
77+
if err != nil {
78+
return "Unable to parse HTML content"
79+
}
80+
81+
var messages []string
82+
doc.Find("p").Each(func(i int, s *goquery.Selection) {
83+
text := strings.TrimSpace(s.Text())
84+
if text != "" {
85+
messages = append(messages, text)
86+
}
87+
})
88+
89+
combinedMessage := strings.Join(messages, " - ")
90+
return combinedMessage
91+
}
92+
93+
// ParseJSONErrorResponse parses the JSON error message from the response body.
94+
func ParseJSONErrorResponse(body []byte) (string, error) {
95+
var errorResponse struct {
96+
HTTPStatus int `json:"httpStatus"`
97+
Errors []struct {
98+
Code string `json:"code"`
99+
Description string `json:"description"`
100+
ID string `json:"id"`
101+
Field string `json:"field"`
102+
} `json:"errors"`
103+
}
104+
105+
err := json.Unmarshal(body, &errorResponse)
106+
if err != nil {
107+
return "", err
108+
}
109+
110+
if len(errorResponse.Errors) > 0 {
111+
return errorResponse.Errors[0].Description, nil
112+
}
113+
114+
return "No error description available", nil
115+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// graph_api_exceptions.go
2+
package graph
3+
4+
import (
5+
_ "embed"
6+
7+
"encoding/json"
8+
"log"
9+
)
10+
11+
// EndpointConfig is a struct that holds configuration details for a specific API endpoint.
12+
// It includes what type of content it can accept and what content type it should send.
13+
type EndpointConfig struct {
14+
Accept string `json:"accept"` // Accept specifies the MIME type the endpoint can handle in responses.
15+
ContentType *string `json:"content_type"` // ContentType, if not nil, specifies the MIME type to set for requests sent to the endpoint. A pointer is used to distinguish between a missing field and an empty string.
16+
}
17+
18+
// ConfigMap is a map that associates endpoint URL patterns with their corresponding configurations.
19+
// The map's keys are strings that identify the endpoint, and the values are EndpointConfig structs
20+
// that hold the configuration for that endpoint.
21+
type ConfigMap map[string]EndpointConfig
22+
23+
// Variables
24+
var configMap ConfigMap
25+
26+
// Embedded Resources
27+
//
28+
//go:embed graph_api_exceptions_configuration.json
29+
var graph_api_exceptions_configuration []byte
30+
31+
// init is invoked automatically on package initialization and is responsible for
32+
// setting up the default state of the package by loading the api exceptions configuration.
33+
func init() {
34+
// Load the default configuration from an embedded resource.
35+
err := loadAPIExceptionsConfiguration()
36+
if err != nil {
37+
log.Fatalf("Error loading Microsoft Graph API exceptions configuration: %s", err)
38+
}
39+
}
40+
41+
// loadAPIExceptionsConfiguration reads and unmarshals the graph_api_exceptions_configuration JSON data from an embedded file
42+
// into the configMap variable, which holds the exceptions configuration for endpoint-specific headers.
43+
func loadAPIExceptionsConfiguration() error {
44+
// Unmarshal the embedded default configuration into the global configMap.
45+
return json.Unmarshal(graph_api_exceptions_configuration, &configMap)
46+
}
Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
11
{
2-
"/api/v1/icon/download/": {
3-
"accept": "image/*",
4-
"content_type": null
5-
},
6-
"/api/v1/branding-images/download/": {
7-
"accept": "image/*",
8-
"content_type": null
9-
},
10-
"/api/v2/inventory-preload/csv-template": {
11-
"accept": "text/csv",
12-
"content_type": null
13-
},
14-
"/api/v1/pki/certificate-authority/active/der": {
15-
"accept": "application/pkix-cert",
16-
"content_type": null
17-
}
2+
183
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// graph_api_handler.go
2+
package graph
3+
4+
import "github.com/deploymenttheory/go-api-http-client/logger"
5+
6+
// GraphAPIHandler implements the APIHandler interface for the graph Pro API.
7+
type GraphAPIHandler struct {
8+
OverrideBaseDomain string // OverrideBaseDomain is used to override the base domain for URL construction.
9+
InstanceName string // InstanceName is the name of the graph instance.
10+
Logger logger.Logger // Logger is the structured logger used for logging.
11+
}

0 commit comments

Comments
 (0)