1- // jamfpro_api_handler.go
21package jamfpro
32
4- import (
5- "bytes"
6- "encoding/json"
7- "encoding/xml"
8- "fmt"
9- "io"
10- "mime/multipart"
11- "net/http"
12- "os"
13- "strings"
14-
15- "github.com/deploymenttheory/go-api-http-client/logger"
16- "go.uber.org/zap"
17- )
3+ import "github.com/deploymenttheory/go-api-http-client/logger"
184
195// EndpointConfig is a struct that holds configuration details for a specific API endpoint.
206// It includes what type of content it can accept and what content type it should send.
@@ -29,217 +15,3 @@ type JamfAPIHandler struct {
2915 InstanceName string // InstanceName is the name of the Jamf instance.
3016 Logger logger.Logger // Logger is the structured logger used for logging.
3117}
32-
33- // Functions
34-
35- // MarshalRequest encodes the request body according to the endpoint for the API.
36- func (j * JamfAPIHandler ) MarshalRequest (body interface {}, method string , endpoint string , log logger.Logger ) ([]byte , error ) {
37- var (
38- data []byte
39- err error
40- )
41-
42- // Determine the format based on the endpoint
43- format := "json"
44- if strings .Contains (endpoint , "/JSSResource" ) {
45- format = "xml"
46- } else if strings .Contains (endpoint , "/api" ) {
47- format = "json"
48- }
49-
50- switch format {
51- case "xml" :
52- data , err = xml .Marshal (body )
53- if err != nil {
54- return nil , err
55- }
56-
57- if method == "POST" || method == "PUT" {
58- j .Logger .Debug ("XML Request Body" , zap .String ("Body" , string (data )))
59- }
60-
61- case "json" :
62- data , err = json .Marshal (body )
63- if err != nil {
64- j .Logger .Error ("Failed marshaling JSON request" , zap .Error (err ))
65- return nil , err
66- }
67-
68- if method == "POST" || method == "PUT" || method == "PATCH" {
69- j .Logger .Debug ("JSON Request Body" , zap .String ("Body" , string (data )))
70- }
71- }
72-
73- return data , nil
74- }
75-
76- // UnmarshalResponse decodes the response body from XML or JSON format depending on the Content-Type header.
77- func (j * JamfAPIHandler ) UnmarshalResponse (resp * http.Response , out interface {}, log logger.Logger ) error {
78- // Handle DELETE method
79- if resp .Request .Method == "DELETE" {
80- if resp .StatusCode >= 200 && resp .StatusCode < 300 {
81- return nil
82- } else {
83- return j .Logger .Error ("DELETE request failed" , zap .Int ("Status Code" , resp .StatusCode ))
84- }
85- }
86-
87- bodyBytes , err := io .ReadAll (resp .Body )
88- if err != nil {
89- j .Logger .Error ("Failed reading response body" , zap .Error (err ))
90- return err
91- }
92-
93- // Log the raw response body and headers
94- j .Logger .Debug ("Raw HTTP Response" , zap .String ("Body" , string (bodyBytes )))
95- j .Logger .Debug ("Unmarshaling response" , zap .String ("status" , resp .Status ))
96-
97- // Log headers when in debug mode
98- j .Logger .Debug ("HTTP Response Headers" , zap .Any ("Headers" , resp .Header ))
99-
100- // Check the Content-Type and Content-Disposition headers
101- contentType := resp .Header .Get ("Content-Type" )
102- contentDisposition := resp .Header .Get ("Content-Disposition" )
103-
104- // Handle binary data if necessary
105- if err := j .handleBinaryData (contentType , contentDisposition , bodyBytes , out ); err != nil {
106- return err
107- }
108-
109- // Check for non-success status codes
110- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
111- // If the content type is HTML, extract and log the error message
112- if strings .Contains (contentType , "text/html" ) {
113- htmlErrorMessage := ExtractErrorMessageFromHTML (string (bodyBytes ))
114-
115- // Log the HTML error message using Zap
116- j .Logger .Error ("Received HTML error content" ,
117- zap .String ("error_message" , htmlErrorMessage ),
118- zap .Int ("status_code" , resp .StatusCode ),
119- )
120- } else {
121- // Log a generic error message if the response is not HTML
122- j .Logger .Error ("Received non-success status code without detailed error response" ,
123- zap .Int ("status_code" , resp .StatusCode ),
124- )
125- }
126- }
127-
128- // Check for non-success status codes before attempting to unmarshal
129- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
130- // Parse the error details from the response body for JSON content type
131- if strings .Contains (contentType , "application/json" ) {
132- description , err := ParseJSONErrorResponse (bodyBytes )
133- if err != nil {
134- // Log the error using the structured logger and return the error
135- j .Logger .Error ("Failed to parse JSON error response" ,
136- zap .Error (err ),
137- zap .Int ("status_code" , resp .StatusCode ),
138- )
139- return err
140- }
141- // Log the error with description using the structured logger and return the error
142- j .Logger .Error ("Received non-success status code with JSON response" ,
143- zap .Int ("status_code" , resp .StatusCode ),
144- zap .String ("error_description" , description ),
145- )
146- return fmt .Errorf ("received non-success status code with JSON response: %s" , description )
147- }
148-
149- // If the response is not JSON or another error occurs, log a generic error message and return an error
150- j .Logger .Error ("Received non-success status code without JSON response" ,
151- zap .Int ("status_code" , resp .StatusCode ),
152- )
153- return fmt .Errorf ("received non-success status code without JSON response: %d" , resp .StatusCode )
154- }
155-
156- // Determine whether the content type is JSON or XML and unmarshal accordingly
157- switch {
158- case strings .Contains (contentType , "application/json" ):
159- err = json .Unmarshal (bodyBytes , out )
160- case strings .Contains (contentType , "application/xml" ), strings .Contains (contentType , "text/xml;charset=UTF-8" ):
161- err = xml .Unmarshal (bodyBytes , out )
162- default :
163- // If the content type is neither JSON nor XML nor HTML
164- return fmt .Errorf ("unexpected content type: %s" , contentType )
165- }
166-
167- // Handle any errors that occurred during unmarshaling
168- if err != nil {
169- // If unmarshalling fails, check if the content might be HTML
170- if strings .Contains (string (bodyBytes ), "<html>" ) {
171- htmlErrorMessage := ExtractErrorMessageFromHTML (string (bodyBytes ))
172-
173- // Log the HTML error message
174- j .Logger .Warn ("Received HTML content instead of expected format" ,
175- zap .String ("error_message" , htmlErrorMessage ),
176- zap .Int ("status_code" , resp .StatusCode ),
177- )
178-
179- // Use the HTML error message for logging the error
180- j .Logger .Error ("Unmarshal error with HTML content" ,
181- zap .String ("error_message" , htmlErrorMessage ),
182- zap .Int ("status_code" , resp .StatusCode ),
183- )
184- } else {
185- // If the error is not due to HTML content, log the original error
186- j .Logger .Error ("Unmarshal error" ,
187- zap .Error (err ),
188- zap .Int ("status_code" , resp .StatusCode ),
189- )
190- }
191- }
192-
193- return err
194- }
195-
196- // MarshalMultipartFormData takes a map with form fields and file paths and returns the encoded body and content type.
197- func (j * JamfAPIHandler ) MarshalMultipartRequest (fields map [string ]string , files map [string ]string , log logger.Logger ) ([]byte , string , error ) {
198- body := & bytes.Buffer {}
199- writer := multipart .NewWriter (body )
200-
201- // Add the simple fields to the form data
202- for field , value := range fields {
203- if err := writer .WriteField (field , value ); err != nil {
204- return nil , "" , err
205- }
206- }
207-
208- // Add the files to the form data
209- for formField , filepath := range files {
210- file , err := os .Open (filepath )
211- if err != nil {
212- return nil , "" , err
213- }
214- defer file .Close ()
215-
216- part , err := writer .CreateFormFile (formField , filepath )
217- if err != nil {
218- return nil , "" , err
219- }
220- if _ , err := io .Copy (part , file ); err != nil {
221- return nil , "" , err
222- }
223- }
224-
225- // Close the writer before returning
226- contentType := writer .FormDataContentType ()
227- if err := writer .Close (); err != nil {
228- return nil , "" , err
229- }
230-
231- return body .Bytes (), contentType , nil
232- }
233-
234- // handleBinaryData checks if the response should be treated as binary data and assigns to out if so.
235- func (j * JamfAPIHandler ) handleBinaryData (contentType , contentDisposition string , bodyBytes []byte , out interface {}) error {
236- if strings .Contains (contentType , "application/octet-stream" ) || strings .HasPrefix (contentDisposition , "attachment" ) {
237- if outPointer , ok := out .(* []byte ); ok {
238- * outPointer = bodyBytes
239- return nil
240- } else {
241- return fmt .Errorf ("output parameter is not a *[]byte for binary data" )
242- }
243- }
244- return nil // If not binary data, no action needed
245- }
0 commit comments