github.com/antihax/goesi@v0.0.0-20240126031043-6c54d0cb7f95/README.md (about) 1 # GoESI "Go Easy" API client for esi 2 [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/O5O33VK5S) 3 4 5 An OpenAPI for EVE Online ESI API 6 7 A module to allow access to CCP's EVE Online ESI API. 8 This module offers: 9 10 * Versioned Endpoints 11 * OAuth2 authentication to login.eveonline.com 12 * Handle many tokens, with different scopes. 13 * 100% ESI API coverage. 14 * context.Context passthrough (for httptrace, logging, etc). 15 16 ## Installation 17 18 ```go 19 go get github.com/antihax/goesi 20 ``` 21 22 ## New Client 23 24 ```go 25 client := goesi.NewAPIClient(&http.Client, "MyApp (someone@somewhere.com dude on slack)") 26 ``` 27 28 One client should be created that will serve as an agent for all requests. This allows http2 multiplexing and keep-alive be used to optimize connections. 29 It is also good manners to provide a user-agent describing the point of use of the API, allowing CCP to contact you in case of emergencies. 30 31 Example: 32 33 ```go 34 package main 35 36 import ( 37 "context" 38 "fmt" 39 40 "github.com/antihax/goesi" 41 ) 42 43 func main() { 44 // create ESI client 45 client := goesi.NewAPIClient(nil, "name@example.com") 46 // call Status endpoint 47 status, _, err := client.ESI.StatusApi.GetStatus(context.Background(), nil) 48 if err != nil { 49 panic(err) 50 } 51 // print current status 52 fmt.Println("Players online: ", status.Players) 53 } 54 ``` 55 56 ## Etiquette 57 58 * Create a descriptive user agent so CCP can contact you (preferably on devfleet slack). 59 * Obey Cache Timers. 60 * Obey error rate limits: https://developers.eveonline.com/blog/article/error-limiting-imminent 61 62 ## Obeying the Cache Times 63 64 Caching is not implimented by the client and thus it is required to utilize 65 a caching http client. It is highly recommended to utilize a client capable 66 of caching the entire cluster of API clients. 67 68 An example using gregjones/httpcache and memcache: 69 70 ```go 71 import ( 72 "github.com/bradfitz/gomemcache/memcache" 73 "github.com/gregjones/httpcache" 74 httpmemcache "github.com/gregjones/httpcache/memcache" 75 ) 76 77 func main() { 78 // Connect to the memcache server 79 cache := memcache.New(MemcachedAddresses...) 80 81 // Create a memcached http client for the CCP APIs. 82 transport := httpcache.NewTransport(httpmemcache.NewWithClient(cache)) 83 transport.Transport = &http.Transport{Proxy: http.ProxyFromEnvironment} 84 client = &http.Client{Transport: transport} 85 86 // Get our API Client. 87 eve := goesi.NewAPIClient(client, "My user agent, contact somewhere@nowhere") 88 } 89 ``` 90 91 ## ETags 92 93 You should support using ETags if you are requesting data that is frequently not changed. 94 IF you are using httpcache, it supports etags already. If you are not using a cache 95 middleware, you will want to create your own middleware like this. 96 97 ```go 98 package myetagpackage 99 type contextKey string 100 101 func (c contextKey) String() string { 102 return "mylib " + string(c) 103 } 104 105 // ContextETag is the context to pass etags to the transport 106 var ( 107 ContextETag = contextKey("etag") 108 ) 109 110 // Custom transport to chain into the HTTPClient to gather statistics. 111 type ETagTransport struct { 112 Next *http.Transport 113 } 114 115 // RoundTrip wraps http.DefaultTransport.RoundTrip to provide stats and handle error rates. 116 func (t *ETagTransport) RoundTrip(req *http.Request) (*http.Response, error) { 117 if etag, ok := req.Context().Value(ContextETag).(string); ok { 118 req.Header.Set("if-none-match", etag) 119 } 120 121 // Run the request. 122 return t.Next.RoundTrip(req) 123 } 124 ``` 125 126 This is then looped in the transport, and passed through a context like so: 127 128 ```go 129 func main() { 130 // Loop in our middleware 131 client := &http.Client{Transport: &ETagTransport{Next: &http.Transport{}}} 132 133 // Make a new client with the middleware 134 esiClient := goesi.NewAPIClient(client, "MyApp (someone@somewhere.com dude on slack)") 135 136 // Make a request with the context 137 ctx := context.WithValue(context.Background(), myetagpackage.ContextETag, "etag goes here") 138 regions, _, err := esiClient.UniverseApi.GetUniverseRegions(ctx, nil) 139 if err != nil { 140 return err 141 } 142 } 143 ``` 144 145 ## Authenticating 146 147 Register your application at https://developers.eveonline.com/ to get your secretKey, clientID, and scopes. 148 149 Obtaining tokens for client requires two HTTP handlers. One to generate and redirect 150 to the SSO URL, and one to receive the response. 151 152 It is mandatory to create a random state and compare this state on return to prevent token injection attacks on the application. 153 154 pseudocode example: 155 156 ```go 157 158 func main() { 159 var err error 160 ctx := appContext.AppContext{} 161 ctx.ESI = goesi.NewAPIClient(httpClient, "My App, contact someone@nowhere") 162 ctx.SSOAuthenticator = goesi.NewSSOAuthenticator(httpClient, clientID, secretKey, scopes) 163 } 164 165 func eveSSO(c *appContext.AppContext, w http.ResponseWriter, r *http.Request, 166 s *sessions.Session) (int, error) { 167 168 // Generate a random state string 169 b := make([]byte, 16) 170 rand.Read(b) 171 state := base64.URLEncoding.EncodeToString(b) 172 173 // Save the state on the session 174 s.Values["state"] = state 175 err := s.Save(r, w) 176 if err != nil { 177 return http.StatusInternalServerError, err 178 } 179 180 // Generate the SSO URL with the state string 181 url := c.SSOAuthenticator.AuthorizeURL(state, true) 182 183 // Send the user to the URL 184 http.Redirect(w, r, url, 302) 185 return http.StatusMovedPermanently, nil 186 } 187 188 func eveSSOAnswer(c *appContext.AppContext, w http.ResponseWriter, r *http.Request, 189 s *sessions.Session) (int, error) { 190 191 // get our code and state 192 code := r.FormValue("code") 193 state := r.FormValue("state") 194 195 // Verify the state matches our randomly generated string from earlier. 196 if s.Values["state"] != state { 197 return http.StatusInternalServerError, errors.New("Invalid State.") 198 } 199 200 // Exchange the code for an Access and Refresh token. 201 token, err := c.SSOAuthenticator.TokenExchange(code) 202 if err != nil { 203 return http.StatusInternalServerError, err 204 } 205 206 // Obtain a token source (automaticlly pulls refresh as needed) 207 tokSrc, err := c.SSOAuthenticator.TokenSource(tok) 208 if err != nil { 209 return http.StatusInternalServerError, err 210 } 211 212 // Assign an auth context to the calls 213 auth := context.WithValue(context.TODO(), goesi.ContextOAuth2, tokSrc.Token) 214 215 // Verify the client (returns clientID) 216 v, err := c.SSOAuthenticator.Verify(auth) 217 if err != nil { 218 return http.StatusInternalServerError, err 219 } 220 221 if err != nil { 222 return http.StatusInternalServerError, err 223 } 224 225 // Save the verification structure on the session for quick access. 226 s.Values["character"] = v 227 err = s.Save(r, w) 228 if err != nil { 229 return http.StatusInternalServerError, err 230 } 231 232 // Redirect to the account page. 233 http.Redirect(w, r, "/account", 302) 234 return http.StatusMovedPermanently, nil 235 } 236 ``` 237 238 ## Passing Tokens 239 240 OAuth2 tokens are passed to endpoints via contexts. Example: 241 242 ```go 243 ctx := context.WithValue(context.Background(), goesi.ContextOAuth2, ESIPublicToken) 244 struc, response, err := client.V1.UniverseApi.GetUniverseStructuresStructureId(ctx, structureID, nil) 245 ``` 246 247 This is done here rather than at the client so you can use one client for many tokens, saving connections. 248 249 ## Testing 250 251 If you would rather not rely on public ESI for testing, a mock ESI server is available for local and CI use. 252 Information here: https://github.com/antihax/mock-esi 253 254 ## What about the other stuff? 255 256 If you need bleeding edge access, add the endpoint to the generator and rebuild this module. 257 Generator is here: https://github.com/antihax/swagger-esi-goclient 258 259 ## Documentation for API Endpoints 260 261 [ESI Endpoints](./esi/README.md) 262 263 ## Author 264 265 antihax on #devfleet slack 266 267 ## Credits 268 269 https://github.com/go-resty/resty (MIT license) Copyright © 2015-2016 Jeevanandam M (jeeva@myjeeva.com) 270 - Uses modified setBody and detectContentType 271 272 https://github.com/gregjones/httpcache (MIT license) Copyright © 2012 Greg Jones (greg.jones@gmail.com) 273 - Uses parseCacheControl and CacheExpires as a helper function 274 275