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