1package restapi
2
3import (
4	"encoding/json"
5	"fmt"
6	"io/ioutil"
7	"net/http"
8	"net/http/cookiejar"
9	"net/url"
10	"strings"
11)
12
13type HttpClientFactory func() *http.Client
14
15// BaseAPIResponse represents the most basic standard Centrify API response,
16//	where Result itself is left as raw json
17type BaseAPIResponse struct {
18	Success bool `json:"success"`
19	Result  json.RawMessage
20	Message string
21}
22
23type StringResponse struct {
24	BaseAPIResponse
25	Result string
26}
27
28type BoolResponse struct {
29	BaseAPIResponse
30	Result bool
31}
32
33// GenericMapResponse represents Centrify API responses where results are map[string]interface{},
34//	this type allows direct access to these without further decoding.
35type GenericMapResponse struct {
36	BaseAPIResponse
37	Result map[string]interface{}
38}
39
40type HttpError struct {
41    error               // error type
42    StatusCode  int     // HTTP status
43}
44
45// BackendType is the type of backend that is being implemented
46type RestClientMode uint32
47
48// RestClient represents a stateful API client (cookies maintained between calls, single service etc)
49type RestClient struct {
50	Service      string
51	Client       *http.Client
52	Headers      map[string]string
53	SourceHeader string
54}
55
56// GetNewRestClient creates a new RestClient for the specified endpoint.  If a factory for creating
57//	http.Client's is not provided, you'll get a new: &http.Client{}.
58func GetNewRestClient(service string, httpFactory HttpClientFactory) (*RestClient, error) {
59	jar, err := cookiejar.New(nil)
60
61	if err != nil {
62		return nil, err
63	}
64
65	// Munge on the service a little bit, force it to have no trailing / and always start with https://
66	url, err := url.Parse(service)
67	if err != nil {
68		return nil, err
69	}
70	url.Scheme = "https"
71	url.Path = ""
72
73	client := &RestClient{}
74	client.Service = url.String()
75	if httpFactory != nil {
76		client.Client = httpFactory()
77	} else {
78		client.Client = &http.Client{}
79	}
80	client.Client.Jar = jar
81	client.Headers = make(map[string]string)
82	client.SourceHeader = "cloud-golang-sdk"
83	return client, nil
84}
85
86func (r *RestClient) CallRawAPI(method string, args map[string]interface{}) ([]byte, error) {
87	return r.postAndGetBody(method, args)
88}
89
90func (r *RestClient) CallBaseAPI(method string, args map[string]interface{}) (*BaseAPIResponse, error) {
91	body, err := r.postAndGetBody(method, args)
92	if err != nil {
93		return nil, err
94	}
95	return bodyToBaseAPIResponse(body)
96}
97
98func (r *RestClient) CallGenericMapAPI(method string, args map[string]interface{}) (*GenericMapResponse, error) {
99	body, err := r.postAndGetBody(method, args)
100	if err != nil {
101		return nil, err
102	}
103	return bodyToGenericMapResponse(body)
104}
105
106func (r *RestClient) CallStringAPI(method string, args map[string]interface{}) (*StringResponse, error) {
107	body, err := r.postAndGetBody(method, args)
108	if err != nil {
109		return nil, err
110	}
111	return bodyToStringResponse(body)
112}
113
114func (r *RestClient) CallBoolAPI(method string, args map[string]interface{}) (*BoolResponse, error) {
115	body, err := r.postAndGetBody(method, args)
116	if err != nil {
117		return nil, err
118	}
119	return bodyToBoolResponse(body)
120}
121
122func (r *RestClient) postAndGetBody(method string, args map[string]interface{}) ([]byte, error) {
123	service := strings.TrimSuffix(r.Service, "/")
124	method = strings.TrimPrefix(method, "/")
125	postdata := strings.NewReader(payloadFromMap(args))
126	postreq, err := http.NewRequest("POST", service+"/"+method, postdata)
127
128	if err != nil {
129		return nil, err
130	}
131
132	postreq.Header.Add("Content-Type", "application/json")
133	postreq.Header.Add("X-CENTRIFY-NATIVE-CLIENT", "Yes")
134	postreq.Header.Add("X-CFY-SRC", r.SourceHeader)
135
136	for k, v := range r.Headers {
137		postreq.Header.Add(k, v)
138	}
139
140	httpresp, err := r.Client.Do(postreq)
141	if err != nil {
142		return nil, err
143	}
144
145	defer httpresp.Body.Close()
146
147	if httpresp.StatusCode == 200 {
148		return ioutil.ReadAll(httpresp.Body)
149	}
150
151	body, _ := ioutil.ReadAll(httpresp.Body)
152	return nil, &HttpError{error: fmt.Errorf("POST to %s failed with code %d, body: %s", method, httpresp.StatusCode, body), StatusCode: httpresp.StatusCode}
153}
154
155// This function converts a map[string]interface{} into json string
156func payloadFromMap(input map[string]interface{}) string {
157	if input != nil {
158		p, _ := json.Marshal(input)
159		return string(p)
160	}
161
162	return ""
163}
164
165func bodyToBaseAPIResponse(body []byte) (*BaseAPIResponse, error) {
166	reply := &BaseAPIResponse{}
167	err := json.Unmarshal(body, &reply)
168	if err != nil {
169		return nil, fmt.Errorf("Failed to unmarshal BaseApiResponse from HTTP response: %v", err)
170	}
171	return reply, nil
172}
173
174func bodyToGenericMapResponse(body []byte) (*GenericMapResponse, error) {
175	reply := &GenericMapResponse{}
176	err := json.Unmarshal(body, &reply)
177	if err != nil {
178		return nil, fmt.Errorf("Failed to unmarshal GenericMapResponse from HTTP response: %v", err)
179	}
180	return reply, nil
181}
182
183func bodyToStringResponse(body []byte) (*StringResponse, error) {
184	reply := &StringResponse{}
185	err := json.Unmarshal(body, &reply)
186	if err != nil {
187		return nil, fmt.Errorf("Failed to unmarshal StringResponse from HTTP response: %v", err)
188	}
189	return reply, nil
190}
191
192func bodyToBoolResponse(body []byte) (*BoolResponse, error) {
193	reply := &BoolResponse{}
194	err := json.Unmarshal(body, &reply)
195	if err != nil {
196		return nil, fmt.Errorf("Failed to unmarshal BoolResponse from HTTP response: %v", err)
197	}
198	return reply, nil
199}
200