-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathclient.go
257 lines (226 loc) · 6.85 KB
/
client.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
package soratun
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/soracom/soratun/internal"
)
// A SoracomClient represents an API client for SORACOM API. See
// https://developers.soracom.io/en/docs/tools/api-reference/ or
// https://dev.soracom.io/jp/docs/api_guide/
//
//go:generate mockgen -source client.go -destination internal/mock/client.go
type SoracomClient interface {
CreateVirtualSim() (*VirtualSim, error)
CreateArcSession(simId, publicKey string) (*ArcSession, error)
SetVerbose(v bool)
Verbose() bool
}
// DefaultSoracomClient is an implementation of the SoracomClient for the general use case.
type DefaultSoracomClient struct {
apiKey string // SORACOM API key.
token string // SORACOM API token.
endpoint string // SORACOM API endpoint.
client *http.Client // HTTP client.
verbose bool
}
// A Profile holds SORACOM API client related information.
type Profile struct {
// AuthKey is SORACOM API auth key secret.
AuthKey string `json:"authKey,omitempty"`
// AuthKeyID is SORACOM API auth key ID.
AuthKeyID string `json:"authKeyId,omitempty"`
// Endpoint is SORACOM API endpoint.
Endpoint string `json:"endpoint,omitempty"`
}
type apiParams struct {
body string
method string
path string
}
// VirtualSim represents virtual subscriber.
type VirtualSim struct {
// OperatorId is operator ID of the subscriber.
OperatorId string `json:"operatorId"`
// Status is virtual SIM status, active or terminated as of 2021 first release.
Status string `json:"status"`
// SimId is SIM ID of the subscriber.
SimId string `json:"simId"`
// ArcSession holds Arc connection information.
ArcSession ArcSession `json:"arcSessionStatus"`
// Profiles holds series of SimProfile, (not SORACOM API Profile).
Profiles map[string]SimProfile `json:"profiles"`
}
// SimProfile is a SIM profile which holds one of profiles in the subscription container.
type SimProfile struct {
// Iccid is ICCID of the subscriber.
Iccid string `json:"iccid"`
// ArcClientPeerPrivateKey is WireGuard private key of the subscriber.
ArcClientPeerPrivateKey string `json:"arcClientPeerPrivateKey"`
// ArcClientPeerPublicKey is WireGuard public key of the subscriber.
ArcClientPeerPublicKey string `json:"arcClientPeerPublicKey"`
// PrimaryImsi is Imsi of this virtual SIM.
PrimaryImsi string `json:"primaryImsi"`
}
// NewDefaultSoracomClient returns new SoracomClient for caller.
func NewDefaultSoracomClient(p Profile) (SoracomClient, error) {
authKeyId := p.AuthKeyID
if authKeyId == "" || !strings.HasPrefix(authKeyId, "keyId-") {
return nil, fmt.Errorf("invalid AuthKeyId is provided. It must starts with \"keyId-\"")
}
authKey := p.AuthKey
if authKey == "" || !strings.HasPrefix(authKey, "secret-") {
return nil, fmt.Errorf("invalid AuthKey is provided. It must starts with \"secret-\"")
}
endpoint := p.Endpoint
if endpoint == "" {
endpoint = "https://api.soracom.io"
}
c := DefaultSoracomClient{
apiKey: "",
token: "",
endpoint: endpoint,
client: http.DefaultClient,
verbose: false,
}
body, err := json.Marshal(struct {
AuthKeyID string `json:"authKeyId"`
AuthKey string `json:"authKey"`
TokenTimeoutSeconds int `json:"tokenTimeoutSeconds"`
}{
AuthKeyID: authKeyId,
AuthKey: authKey,
TokenTimeoutSeconds: 5 * 60,
})
if err != nil {
return nil, err
}
res, err := c.callAPI(&apiParams{
method: "POST",
path: "/auth",
body: string(body),
})
if err != nil {
return nil, err
}
ar := struct {
APIKey string `json:"apiKey"`
Token string `json:"token"`
}{}
if err := json.NewDecoder(res.Body).Decode(&ar); err != nil {
return nil, fmt.Errorf("failed to decode auth response: %w", err)
}
c.apiKey = ar.APIKey
c.token = ar.Token
return &c, nil
}
// SetVerbose sets if verbose output is enabled or not.
func (c *DefaultSoracomClient) SetVerbose(v bool) {
c.verbose = v
}
// Verbose returns if verbose output is enabled or not.
func (c *DefaultSoracomClient) Verbose() bool {
return c.verbose
}
// CreateVirtualSim creates new virtual SIM.
func (c *DefaultSoracomClient) CreateVirtualSim() (*VirtualSim, error) {
body, err := json.Marshal(struct {
Type string `json:"type"`
Subscription string `json:"subscription"`
}{
Type: "virtual",
Subscription: "planArc01",
})
if err != nil {
return nil, err
}
res, err := c.callAPI(&apiParams{
method: "POST",
path: "/sims",
body: string(body),
})
if err != nil {
return nil, err
}
var subscriber VirtualSim
err = json.NewDecoder(res.Body).Decode(&subscriber)
return &subscriber, err
}
// CreateArcSession creates new Arc session.
func (c *DefaultSoracomClient) CreateArcSession(simId, publicKey string) (*ArcSession, error) {
// bootstrapped SIM will have attached credential. So we can just sent empty object.
res, err := c.callAPI(&apiParams{
method: "POST",
path: "/sims/" + simId + "/sessions/arc",
body: "{}",
})
if err != nil {
return nil, err
}
var session ArcSession
err = json.NewDecoder(res.Body).Decode(&session)
return &session, err
}
func (c *DefaultSoracomClient) callAPI(params *apiParams) (*http.Response, error) {
req, err := c.makeRequest(params)
if err != nil {
return nil, err
}
if c.Verbose() {
fmt.Fprintln(os.Stderr, "--- Request dump ---------------------------------")
r, _ := httputil.DumpRequest(req, true)
fmt.Fprintln(os.Stderr, string(r))
fmt.Fprintln(os.Stderr, "--- End of request dump --------------------------")
}
res, err := c.doRequest(req)
return res, err
}
func (c *DefaultSoracomClient) makeRequest(params *apiParams) (*http.Request, error) {
var body io.Reader
if params.body != "" {
body = strings.NewReader(params.body)
}
req, err := http.NewRequest(params.method,
fmt.Sprintf("%s/v1%s", c.endpoint, params.path),
body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Soracom-Lang", "en")
req.Header.Set("User-Agent", internal.UserAgent)
if c.apiKey != "" {
req.Header.Set("X-Soracom-Api-Key", c.apiKey)
}
if c.token != "" {
req.Header.Set("X-Soracom-Token", c.token)
}
return req, nil
}
func (c *DefaultSoracomClient) doRequest(req *http.Request) (*http.Response, error) {
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
if c.Verbose() && res != nil {
fmt.Fprintln(os.Stderr, "--- Response dump --------------------------------")
r, _ := httputil.DumpResponse(res, true)
fmt.Fprintln(os.Stderr, string(r))
fmt.Fprintln(os.Stderr, "--- End of response dump -------------------------")
}
if res.StatusCode >= http.StatusBadRequest {
defer func() {
err := res.Body.Close()
if err != nil {
fmt.Println("failed to close response", err)
}
}()
r, _ := io.ReadAll(res.Body)
return res, fmt.Errorf("%s: %s %s: %s", res.Status, req.Method, req.URL, r)
}
return res, nil
}