forked from ForceCLI/force
/
force.go
400 lines (367 loc) · 11.5 KB
/
force.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"runtime"
"strings"
)
const (
ProductionClientId = "3MVG9A2kN3Bn17huXZp1OQhPe8y4_ozAQZZCKxsWbef9GjSnHGOunHSwhnY1BWz_5vHkTL9BeLMriIX5EUKaw"
PrereleaseClientId = "3MVG9lKcPoNINVBIRgC7lsz5tIhlg0mtoEqkA9ZjDAwEMbBy43gsnfkzzdTdhFLeNnWS8M4bnRnVv1Qj0k9MD"
RedirectUri = "https://force-cli.herokuapp.com/auth/callback"
)
const (
EndpointProduction = iota
EndpointTest = iota
EndpointPrerelease = iota
)
const (
apiVersion = "v29.0" //winter 14
)
var RootCertificates = `
-----BEGIN CERTIFICATE-----
MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG
A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do
lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc
AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
+OkuE6N36B9K
-----END CERTIFICATE-----`
type Force struct {
Credentials ForceCredentials
Metadata *ForceMetadata
Partner *ForcePartner
}
type ForceCredentials struct {
AccessToken string
Id string
InstanceUrl string
IssuedAt string
Scope string
}
type ForceError struct {
Message string
ErrorCode string
}
type ForceEndpoint int
type ForceRecord map[string]interface{}
type ForceSobject map[string]interface{}
type ForceCreateRecordResult struct {
Errors []string
Id string
Success bool
}
type ForceQueryResult struct {
Done bool
Records []ForceRecord
TotalSize int
}
type ForceSobjectsResult struct {
Encoding string
MaxBatchSize int
Sobjects []ForceSobject
}
func NewForce(creds ForceCredentials) (force *Force) {
force = new(Force)
force.Credentials = creds
force.Metadata = NewForceMetadata(force)
force.Partner = NewForcePartner(force)
return
}
func ForceLogin(endpoint ForceEndpoint) (creds ForceCredentials, err error) {
ch := make(chan ForceCredentials)
port, err := startLocalHttpServer(ch)
var url string
switch endpoint {
case EndpointProduction:
url = fmt.Sprintf("https://login.salesforce.com/services/oauth2/authorize?response_type=token&client_id=%s&redirect_uri=%s&state=%d&prompt=login", ProductionClientId, RedirectUri, port)
case EndpointTest:
url = fmt.Sprintf("https://test.salesforce.com/services/oauth2/authorize?response_type=token&client_id=%s&redirect_uri=%s&state=%d&prompt=login", ProductionClientId, RedirectUri, port)
case EndpointPrerelease:
url = fmt.Sprintf("https://prerellogin.pre.salesforce.com/services/oauth2/authorize?response_type=token&client_id=%s&redirect_uri=%s&state=%d&prompt=login", PrereleaseClientId, RedirectUri, port)
default:
ErrorAndExit("no such endpoint type")
}
err = Open(url)
creds = <-ch
return
}
func (f *Force) ListSobjects() (sobjects []ForceSobject, err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects", f.Credentials.InstanceUrl, apiVersion)
body, err := f.httpGet(url)
if err != nil {
return
}
var result ForceSobjectsResult
json.Unmarshal(body, &result)
sobjects = result.Sobjects
return
}
func (f *Force) GetSobject(name string) (sobject ForceSobject, err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects/%s/describe", f.Credentials.InstanceUrl, apiVersion, name)
body, err := f.httpGet(url)
if err != nil {
return
}
json.Unmarshal(body, &sobject)
return
}
func (f *Force) Query(query string) (result ForceQueryResult, err error) {
url := fmt.Sprintf("%s/services/data/%s/query?q=%s", f.Credentials.InstanceUrl, apiVersion, url.QueryEscape(query))
body, err := f.httpGet(url)
if err != nil {
return
}
json.Unmarshal(body, &result)
return
}
func (f *Force) Get(url string) (object ForceRecord, err error) {
body, err := f.httpGet(url)
if err != nil {
return
}
err = json.Unmarshal(body, &object)
return
}
func (f *Force) GetRecord(sobject, id string) (object ForceRecord, err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects/%s/%s", f.Credentials.InstanceUrl, apiVersion, sobject, id)
body, err := f.httpGet(url)
if err != nil {
return
}
err = json.Unmarshal(body, &object)
return
}
func (f *Force) CreateRecord(sobject string, attrs map[string]string) (id string, err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects/%s", f.Credentials.InstanceUrl, apiVersion, sobject)
body, err := f.httpPost(url, attrs)
var result ForceCreateRecordResult
json.Unmarshal(body, &result)
id = result.Id
return
}
func (f *Force) UpdateRecord(sobject string, id string, attrs map[string]string) (err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects/%s/%s", f.Credentials.InstanceUrl, apiVersion, sobject, id)
_, err = f.httpPatch(url, attrs)
return
}
func (f *Force) DeleteRecord(sobject string, id string) (err error) {
url := fmt.Sprintf("%s/services/data/%s/sobjects/%s/%s", f.Credentials.InstanceUrl, apiVersion, sobject, id)
_, err = f.httpDelete(url)
return
}
func (f *Force) Whoami() (me ForceRecord, err error) {
parts := strings.Split(f.Credentials.Id, "/")
me, err = f.GetRecord("User", parts[len(parts)-1])
return
}
func (f *Force) httpGet(url string) (body []byte, err error) {
req, err := httpRequest("GET", url, nil)
if err != nil {
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", f.Credentials.AccessToken))
res, err := httpClient().Do(req)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode == 401 {
err = errors.New("authorization expired, please run `force login`")
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
if len(messages) > 0 {
err = errors.New(messages[0].Message)
} else {
err = errors.New(string(body))
}
return
}
return
}
func (f *Force) httpPost(url string, attrs map[string]string) (body []byte, err error) {
rbody, _ := json.Marshal(attrs)
req, err := httpRequest("POST", url, bytes.NewReader(rbody))
if err != nil {
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", f.Credentials.AccessToken))
req.Header.Add("Content-Type", "application/json")
res, err := httpClient().Do(req)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode == 401 {
err = errors.New("authorization expired, please run `force login`")
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
return
}
return
}
func (f *Force) httpPatch(url string, attrs map[string]string) (body []byte, err error) {
rbody, _ := json.Marshal(attrs)
req, err := httpRequest("PATCH", url, bytes.NewReader(rbody))
if err != nil {
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", f.Credentials.AccessToken))
req.Header.Add("Content-Type", "application/json")
res, err := httpClient().Do(req)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode == 401 {
err = errors.New("authorization expired, please run `force login`")
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
return
}
return
}
func (f *Force) httpDelete(url string) (body []byte, err error) {
req, err := httpRequest("DELETE", url, nil)
if err != nil {
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", f.Credentials.AccessToken))
res, err := httpClient().Do(req)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode == 401 {
err = errors.New("authorization expired, please run `force login`")
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
return
}
return
}
func httpClient() (client *http.Client) {
chain := rootCertificate()
config := tls.Config{}
config.RootCAs = x509.NewCertPool()
for _, cert := range chain.Certificate {
x509Cert, err := x509.ParseCertificate(cert)
if err != nil {
panic(err)
}
config.RootCAs.AddCert(x509Cert)
}
config.BuildNameToCertificate()
tr := http.Transport{TLSClientConfig: &config}
client = &http.Client{Transport: &tr}
return
}
func httpRequest(method, url string, body io.Reader) (request *http.Request, err error) {
request, err = http.NewRequest(method, url, body)
if err != nil {
return
}
request.Header.Add("User-Agent", fmt.Sprintf("force/%s (%s-%s)", Version, runtime.GOOS, runtime.GOARCH))
return
}
func rootCertificate() (cert tls.Certificate) {
certPEMBlock := []byte(RootCertificates)
var certDERBlock *pem.Block
for {
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
}
}
return
}
func startLocalHttpServer(ch chan ForceCredentials) (port int, err error) {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return
}
port = listener.Addr().(*net.TCPAddr).Port
h := http.NewServeMux()
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://force-cli.herokuapp.com")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With")
} else {
query := r.URL.Query()
var creds ForceCredentials
creds.AccessToken = query.Get("access_token")
creds.Id = query.Get("id")
creds.InstanceUrl = query.Get("instance_url")
creds.IssuedAt = query.Get("issued_at")
creds.Scope = query.Get("scope")
ch <- creds
if _, ok := r.Header["X-Requested-With"]; ok == false {
http.Redirect(w, r, "https://force-cli.herokuapp.com/auth/complete", http.StatusSeeOther)
}
listener.Close()
}
})
go http.Serve(listener, h)
return
}