Coder Social home page Coder Social logo

goesi's Introduction

GoESI "Go Easy" API client for esi

ko-fi

An OpenAPI for EVE Online ESI API

A module to allow access to CCP's EVE Online ESI API. This module offers:

  • Versioned Endpoints
  • OAuth2 authentication to login.eveonline.com
  • Handle many tokens, with different scopes.
  • 100% ESI API coverage.
  • context.Context passthrough (for httptrace, logging, etc).

Installation

    go get github.com/antihax/goesi

New Client

client := goesi.NewAPIClient(&http.Client, "MyApp ([email protected] dude on slack)")

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. 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.

Example:

package main

import (
	"context"
	"fmt"

	"github.com/antihax/goesi"
)

func main() {
	// create ESI client
	client := goesi.NewAPIClient(nil, "[email protected]")
	// call Status endpoint
	status, _, err := client.ESI.StatusApi.GetStatus(context.Background(), nil)
	if err != nil {
		panic(err)
	}
	// print current status
	fmt.Println("Players online: ", status.Players)
}

Etiquette

Obeying the Cache Times

Caching is not implimented by the client and thus it is required to utilize a caching http client. It is highly recommended to utilize a client capable of caching the entire cluster of API clients.

An example using gregjones/httpcache and memcache:

import (
	"github.com/bradfitz/gomemcache/memcache"
	"github.com/gregjones/httpcache"
	httpmemcache "github.com/gregjones/httpcache/memcache"
)

func main() {
	// Connect to the memcache server
	cache := memcache.New(MemcachedAddresses...)

	// Create a memcached http client for the CCP APIs.
	transport := httpcache.NewTransport(httpmemcache.NewWithClient(cache))
	transport.Transport = &http.Transport{Proxy: http.ProxyFromEnvironment}
	client = &http.Client{Transport: transport}

	// Get our API Client.
	eve := goesi.NewAPIClient(client, "My user agent, contact somewhere@nowhere")
}

ETags

You should support using ETags if you are requesting data that is frequently not changed. IF you are using httpcache, it supports etags already. If you are not using a cache middleware, you will want to create your own middleware like this.

package myetagpackage
type contextKey string

func (c contextKey) String() string {
	return "mylib " + string(c)
}

// ContextETag is the context to pass etags to the transport
var (
	ContextETag = contextKey("etag")
)

// Custom transport to chain into the HTTPClient to gather statistics.
type ETagTransport struct {
	Next *http.Transport
}

// RoundTrip wraps http.DefaultTransport.RoundTrip to provide stats and handle error rates.
func (t *ETagTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if etag, ok := req.Context().Value(ContextETag).(string); ok {
		req.Header.Set("if-none-match", etag)
	}

	// Run the request.
	return t.Next.RoundTrip(req)
}

This is then looped in the transport, and passed through a context like so:

func main() {
	// Loop in our middleware
	client := &http.Client{Transport: &ETagTransport{Next: &http.Transport{}}}

	// Make a new client with the middleware
	esiClient := goesi.NewAPIClient(client, "MyApp ([email protected] dude on slack)")

	// Make a request with the context
	ctx := context.WithValue(context.Background(), myetagpackage.ContextETag, "etag goes here")
	regions, _, err := esiClient.UniverseApi.GetUniverseRegions(ctx, nil)
	if err != nil {
		return err
	}
}

Authenticating

Register your application at https://developers.eveonline.com/ to get your secretKey, clientID, and scopes.

Obtaining tokens for client requires two HTTP handlers. One to generate and redirect to the SSO URL, and one to receive the response.

It is mandatory to create a random state and compare this state on return to prevent token injection attacks on the application.

pseudocode example:

func main() {
var err error
ctx := appContext.AppContext{}
ctx.ESI = goesi.NewAPIClient(httpClient, "My App, contact someone@nowhere")
ctx.SSOAuthenticator = goesi.NewSSOAuthenticator(httpClient, clientID, secretKey, scopes)
}

func eveSSO(c *appContext.AppContext, w http.ResponseWriter, r *http.Request,
	s *sessions.Session) (int, error) {

	// Generate a random state string
	b := make([]byte, 16)
	rand.Read(b)
	state := base64.URLEncoding.EncodeToString(b)

	// Save the state on the session
	s.Values["state"] = state
	err := s.Save(r, w)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Generate the SSO URL with the state string
	url := c.SSOAuthenticator.AuthorizeURL(state, true)

	// Send the user to the URL
	http.Redirect(w, r, url, 302)
	return http.StatusMovedPermanently, nil
}

func eveSSOAnswer(c *appContext.AppContext, w http.ResponseWriter, r *http.Request,
	s *sessions.Session) (int, error) {

	// get our code and state
	code := r.FormValue("code")
	state := r.FormValue("state")

	// Verify the state matches our randomly generated string from earlier.
	if s.Values["state"] != state {
		return http.StatusInternalServerError, errors.New("Invalid State.")
	}

	// Exchange the code for an Access and Refresh token.
	token, err := c.SSOAuthenticator.TokenExchange(code)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Obtain a token source (automaticlly pulls refresh as needed)
	tokSrc, err := c.SSOAuthenticator.TokenSource(tok)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Assign an auth context to the calls
	auth := context.WithValue(context.TODO(), goesi.ContextOAuth2, tokSrc.Token)

	// Verify the client (returns clientID)
	v, err := c.SSOAuthenticator.Verify(auth)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Save the verification structure on the session for quick access.
	s.Values["character"] = v
	err = s.Save(r, w)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Redirect to the account page.
	http.Redirect(w, r, "/account", 302)
	return http.StatusMovedPermanently, nil
}

Passing Tokens

OAuth2 tokens are passed to endpoints via contexts. Example:

	ctx := context.WithValue(context.Background(), goesi.ContextOAuth2, ESIPublicToken)
	struc, response, err := client.V1.UniverseApi.GetUniverseStructuresStructureId(ctx, structureID, nil)

This is done here rather than at the client so you can use one client for many tokens, saving connections.

Testing

If you would rather not rely on public ESI for testing, a mock ESI server is available for local and CI use. Information here: https://github.com/antihax/mock-esi

What about the other stuff?

If you need bleeding edge access, add the endpoint to the generator and rebuild this module. Generator is here: https://github.com/antihax/swagger-esi-goclient

Documentation for API Endpoints

ESI Endpoints

Author

antihax on #devfleet slack

Credits

https://github.com/go-resty/resty (MIT license) Copyright © 2015-2016 Jeevanandam M ([email protected])

  • Uses modified setBody and detectContentType

https://github.com/gregjones/httpcache (MIT license) Copyright © 2012 Greg Jones ([email protected])

  • Uses parseCacheControl and CacheExpires as a helper function

goesi's People

Contributors

a-tal avatar antihax avatar dedo1911 avatar hakshak avatar samsamm777 avatar tyler-sommer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

goesi's Issues

New notification types

Here's the one missing piece. Have no time to modify go file, sorry.

  {
    "is_read": true,
    "notification_id": 1077367335,
    "sender_id": 1000125,
    "sender_type": "corporation",
    "text": "againstID: 99003557\ncost: 100000000\ndeclaredByID: 99009348\ndelayHours: 24\nhostileState: false\ntimeStarted: 132090519000000000\nwarHQ: <b>Perimeter - quan fu wu cha bie xuan zhan</b>\nwarHQ_IdType:\n- 1031068245078\n- 35833\n",
    "timestamp": "2019-07-30T13:06:00Z",
    "type": "WarDeclared"
  },

Distinguishing unprovided values from provided zero values

I'm having an issue with loading public contracts using the ContractsApi.GetContractsPublicRegionId function.

For optional arguments (e.g. the "reward" field from the corresponding ESI endpoint), is there any way to distinguish between a field that wasn't provided (e.g. a contract that doesn't have a "reward" field, such as an item exchange), vs. a contract where that field was provided, but has a zero value (e.g. a courier contract where the reward is 0)?

I know I could do a conditional check based on the Type_ field of the resulting GetContractsPublicRegionId200Ok struct, and only check the Reward field if it's a contract of type courier, but that's a lot of switch statements over a lot of endpoints.

The conventional Go way to do this would be for these structs to use pointer type for the optional fields, which would be nil if they weren't provided in the ESI response.
@antihax

Lost of decimal precision on the Price in market

Hi,
in the type GetMarketsRegionIdOrders200Ok (in v1, get_markets_region_id_orders_200_ok.go file), Price are float32 type, and loss some precision in decimal, where price are greater than one million ISK.
Example : 2 938 863.98 ISK becomes 2 938 864.00 ISK
Could you correct some types to use float64 instead ?
Other files may be affected by the price problem.
Thx.

antihax\goesi\meta\model_get_status_item_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)

The latest commit causes the following errors

`$ go build .
# github.com/antihax/goesi/meta
vendor\github.com\antihax\goesi\meta\model_get_status_item_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)     
vendor\github.com\antihax\goesi\meta\model_get_status_not_found_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\meta\model_get_verify_error_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)    
vendor\github.com\antihax\goesi\meta\model_get_verify_not_found_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\meta\model_get_verify_ok_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\meta\model_get_verify_unauthorized_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
# github.com/antihax/goesi/esi
vendor\github.com\antihax\goesi\esi\model_bad_request_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_delete_characters_character_id_mail_labels_label_id_unprocessable_entity_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer 
has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_delete_fleets_fleet_id_members_member_id_not_found_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_delete_fleets_fleet_id_squads_squad_id_not_found_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_delete_fleets_fleet_id_wings_wing_id_not_found_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_error_limited_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_forbidden_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_gateway_timeout_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_get_alliances_alliance_id_contacts_200_ok_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_get_alliances_alliance_id_contacts_labels_200_ok_easyjson.go:98:12: in.UnsafeFieldName undefined (type *jlexer.Lexer has no field or method UnsafeFieldName)
vendor\github.com\antihax\goesi\esi\model_get_alliances_alliance_id_contacts_labels_200_ok_easyjson.go:98:12: too many errors`

Suggestion: Add working example to README

As a go beginner I was struggling a bit to get a very simple example to work. One reason might be that there no working example in the README and all the one has to fill in a lot of gaps with the current examples.

To help people get acquainted more easily with this nice library, I would suggest to add this simple example to the README:

package main

import (
	"context"
	"fmt"

	"github.com/antihax/goesi"
)

func main() {
	// create ESI client
	client := goesi.NewAPIClient(nil, "[email protected]")
	// call Status endpoint
	status, _, _ := client.ESI.StatusApi.GetStatus(context.Background(), nil)
	// print current status
	fmt.Println("Players online: ", status.Players)
}

Handling refresh token rotations with persistent refresh tokens

Based on this (under "PREPARE FOR REFRESH TOKEN ROTATIONS") and this (bottom of the page) the refresh tokens can rotate. While this is not necessarily an issue for "login-use-forget" type of applications, more persistent applications that save the refresh tokens to a database or disk to survive restarts and continuously run in the background, a mechanism to "catch" refresh token changes in order to update them in db/disk/whatever becomes necessary.

I have resolved this by setting a custom Transport to the http.Client passed to goesi.NewSSOAuthenticatorV2 which will catch these changes and update the tokens in the DB when they change, but since this refresh token rotation applies to everyone using the eve sso & esi, it would be nice to see goesi supporting this "more natively".

I have not dug too deep into the sources of goesi so I don't know what would be the most convenient way to make this more convenient, but at least one solution would be offering a "eve sso aware middleware" http.RoundTripper implementation that wraps another http.RoundTripper, and calls a provided function when it detects a refresh token rotation (which is more or less what I've implemented)

Sample of the Elusive AllAnchoringMsg Notification

This is the notification sov-holding corporations get whenever a POS is anchored within their space.

{
    "notification_id": 1073061512,
    "sender_id": 1000137,
    "sender_type": "corporation",
    "text": "allianceID: 99009054\ncorpID: 98598954\ncorpsPresent: []\nmoonID: 40078944\nsolarSystemID: 30001243\ntypeID: 12235\n",
    "timestamp": "2019-07-22T20:25:00Z",
    "type": "AllAnchoringMsg"
  }

How should I refresh tokens?

I've successfully set up a small application that fetches EVE mails, the easy to read methods are useful, so great job.

When I authenticate with SSO, I get an access token and a refresh token, both which I then store. However, when I try to do an ESI call after more than 1200 seconds, I get an {"error": "expired", "sso_status": 400} response.

Wasn't the library supposed to automatically refresh this token? If not, how should I proceed?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.