summaryrefslogtreecommitdiff
path: root/internal/api/api.go
diff options
context:
space:
mode:
authorJeroen Wijenbergh <jeroen.wijenbergh@geant.org>2026-02-12 12:34:08 +0100
committerJeroen Wijenbergh <jeroen.wijenbergh@geant.org>2026-02-12 12:59:03 +0100
commita30ef6b27e578a4cf0a674b24f5b52b4c1516c63 (patch)
tree27c7321cbceac2a487c1ba17151711de3d438a53 /internal/api/api.go
parentb00ce8214479c50e137db73c77b0cc1393c5e7d4 (diff)
All: Rename packages that sound useless or clash with std
Diffstat (limited to 'internal/api/api.go')
-rw-r--r--internal/api/api.go395
1 files changed, 0 insertions, 395 deletions
diff --git a/internal/api/api.go b/internal/api/api.go
deleted file mode 100644
index 0d8e03c..0000000
--- a/internal/api/api.go
+++ /dev/null
@@ -1,395 +0,0 @@
-// Package api implements version 3 of the eduVPN api: https://docs.eduvpn.org/server/v3/api.html
-package api
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log/slog"
- "net/http"
- "net/url"
- "time"
-
- "codeberg.org/jwijenbergh/eduoauth-go/v2"
- "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
-
- "codeberg.org/eduVPN/eduvpn-common/internal/api/endpoints"
- "codeberg.org/eduVPN/eduvpn-common/internal/api/profiles"
- httpw "codeberg.org/eduVPN/eduvpn-common/internal/http"
- "codeberg.org/eduVPN/eduvpn-common/internal/wireguard"
- "codeberg.org/eduVPN/eduvpn-common/types/protocol"
- "codeberg.org/eduVPN/eduvpn-common/types/server"
-)
-
-// Callbacks is the API callback interface
-// It is used to trigger authorization and forward token updates
-type Callbacks interface {
- // TriggerAuth is called when authorization should be triggered
- TriggerAuth(context.Context, string, bool) (string, error)
- // AuthDone is called when authorization has just completed
- AuthDone(string, server.Type)
- // TokensUpdates is called when tokens are updated
- TokensUpdated(string, server.Type, eduoauth.Token)
-}
-
-// ServerData is the data for a server that is passed to the API struct
-type ServerData struct {
- // ID is the identifier for the server
- ID string
- // Type is the type of server
- Type server.Type
- // BaseWK is the base well-known endpoint
- BaseWK string
- // BaseAuthWK is the base well-known endpoint for authorization. This is only different in case of secure internet
- BaseAuthWK string
- // ProcessAuth processes the OAuth authorization
- ProcessAuth func(context.Context, string) (string, error)
- // DisableAuthorize indicates whether or not new authorization requests should be disabled
- DisableAuthorize bool
- // transport is the HTTP transport, only used for testing currently
- transport http.RoundTripper
-}
-
-// Transport returns the transport to be used for the server
-// By default it uses the transport from internal/http DefaultTransport
-func (s *ServerData) Transport() http.RoundTripper {
- if s.transport == nil {
- return httpw.DefaultTransport
- }
- return s.transport
-}
-
-// API is the top-level struct that each method is defined on
-type API struct {
- cb Callbacks
- // oauth is the oauth object
- oauth *eduoauth.OAuth
- // Data is the server data
- Data ServerData
-}
-
-// NewAPI creates a new API object by creating an OAuth object
-func NewAPI(ctx context.Context, clientID string, sd ServerData, cb Callbacks, tokens *eduoauth.Token) (*API, error) {
- cr := customRedirect(clientID)
- // Construct OAuth
-
- transp := sd.Transport()
- post := true
- // we do not support non-loopback clients with response_mode form_post
- if cr != "" {
- post = false
- }
- o := eduoauth.OAuth{
- ClientID: clientID,
- EndpointFunc: func(ctx context.Context) (*eduoauth.EndpointResponse, error) {
- ep, err := GetEndpointCache().Get(ctx, sd.BaseAuthWK, transp)
- if err != nil {
- return nil, err
- }
- return &eduoauth.EndpointResponse{
- AuthorizationURL: ep.API.V3.Authorization,
- TokenURL: ep.API.V3.Token,
- }, nil
- },
- CustomRedirect: cr,
- FormPost: post,
- RedirectPath: "/callback",
- TokensUpdated: func(tok eduoauth.Token) {
- cb.TokensUpdated(sd.ID, sd.Type, tok)
- },
- Transport: transp,
- UserAgent: httpw.UserAgent,
- }
-
- if tokens != nil {
- o.UpdateTokens(*tokens)
- }
-
- api := &API{
- cb: cb,
- oauth: &o,
- Data: sd,
- }
- err := api.authorize(ctx)
- if err != nil {
- return nil, err
- }
- return api, nil
-}
-
-// ErrAuthorizeDisabled is returned when authorization is disabled but is needed to complete
-var ErrAuthorizeDisabled = errors.New("cannot authorize as re-authorization is disabled")
-
-func (a *API) authorize(ctx context.Context) (err error) {
- _, err = a.oauth.AccessToken(ctx)
- // already authorized
- if err == nil {
- return nil
- }
-
- // otherwise check if invalid tokens,
- // if not then something else is wrong with the API
- // return an error
- tErr := &eduoauth.TokensInvalidError{}
- if !errors.As(err, &tErr) {
- return err
- }
-
- if a.Data.DisableAuthorize {
- return ErrAuthorizeDisabled
- }
-
- defer func() {
- if err == nil {
- a.cb.AuthDone(a.Data.ID, a.Data.Type)
- }
- }()
-
- scope := "config"
- url, err := a.oauth.AuthURL(ctx, scope)
- if err != nil {
- return err
- }
- if a.Data.ProcessAuth != nil {
- url, err = a.Data.ProcessAuth(ctx, url)
- if err != nil {
- return err
- }
- }
- // We expect an uri if custom redirect is non empty
- uri, err := a.cb.TriggerAuth(ctx, url, a.oauth.CustomRedirect != "")
- if err != nil {
- return err
- }
- // The uri is only given here if a custom redirect is done
- err = a.oauth.Exchange(ctx, uri)
- if err != nil {
- return err
- }
- return nil
-}
-
-func (a *API) authorized(ctx context.Context, method string, endpoint string, opts *httpw.OptionalParams) (http.Header, []byte, error) {
- ep, err := GetEndpointCache().Get(ctx, a.Data.BaseWK, a.Data.Transport())
- if err != nil {
- return nil, nil, err
- }
- u := ep.API.V3.API + endpoint
-
- // TODO: Cache HTTP client?
- httpC := httpw.NewClient(a.oauth.NewHTTPClient())
- return httpC.Do(ctx, method, u, opts)
-}
-
-func (a *API) authorizedRetry(ctx context.Context, method string, endpoint string, opts *httpw.OptionalParams) (http.Header, []byte, error) {
- h, body, err := a.authorized(ctx, method, endpoint, opts)
- if err == nil {
- return h, body, nil
- }
-
- statErr := &httpw.StatusError{}
- // Only retry authorized if we get an HTTP 401
- // TODO: Can the OAuth client handle this instead?
- if errors.As(err, &statErr) && statErr.Status == 401 {
- slog.Debug("Got a HTTP 401. Marking tokens as expired...", "HTTP method", method, "endpoint", endpoint)
- // Mark the token as expired and retry, so we trigger the refresh flow
- a.oauth.SetTokenExpired()
- h, body, err = a.authorized(ctx, method, endpoint, opts)
- }
- // Tokens is invalid we need to renew and authorize again
- tErr := &eduoauth.TokensInvalidError{}
- if err != nil && errors.As(err, &tErr) {
- // Mark the token as invalid and retry, so we trigger the authorization flow
- a.oauth.SetTokenRenew()
- slog.Debug("The tokens were invalid, trying again...")
- if autherr := a.authorize(ctx); autherr != nil {
- return nil, nil, autherr
- }
- return a.authorized(ctx, method, endpoint, opts)
- }
- return h, body, err
-}
-
-// Disconnect disconnects a client from the server by sending a /disconnect API call
-// This cleans up resources such as WireGuard IP allocation
-func (a *API) Disconnect(ctx context.Context) error {
- _, _, err := a.authorized(ctx, http.MethodPost, "/disconnect", &httpw.OptionalParams{Timeout: 5 * time.Second})
- return err
-}
-
-// Info does the /info API call
-func (a *API) Info(ctx context.Context) (*profiles.Info, error) {
- _, body, err := a.authorizedRetry(ctx, http.MethodGet, "/info", nil)
- if err != nil {
- return nil, fmt.Errorf("failed API /info: %w", err)
- }
- p := profiles.Info{}
- if err = json.Unmarshal(body, &p); err != nil {
- return nil, fmt.Errorf("failed API /info: %w", err)
- }
- return &p, nil
-}
-
-// ConnectData is the data that is returned when the /connect call completes without error
-type ConnectData struct {
- // Configuration is the VPN configuration
- Configuration string
- // Protocol tells us what protocol it is, OpenVPN or WireGuard (proxied or not)
- Protocol protocol.Protocol
- // Expires tells us when this configuration expires
- Expires time.Time
-}
-
-// see https://github.com/eduvpn/documentation/blob/v3/API.md#request-1
-func boolToYesNo(preferTCP bool) string {
- if preferTCP {
- return "yes"
- }
- return "no"
-}
-
-func protocolFromCT(ct string) (protocol.Protocol, error) {
- switch ct {
- case "application/x-wireguard-profile":
- return protocol.WireGuard, nil
- case "application/x-wireguard+tcp-profile":
- return protocol.WireGuardProxy, nil
- case "application/x-openvpn-profile":
- return protocol.OpenVPN, nil
- }
- return protocol.Unknown, fmt.Errorf("invalid content type: %s", ct)
-}
-
-// ErrNoProtocols is returned when a connect call is given with an empty protocol slice
-var ErrNoProtocols = errors.New("no protocols supplied")
-
-// ErrUnknownProtocol is returned when the client in a connect gives an unknown protocol
-var ErrUnknownProtocol = errors.New("unknown protocol supplied")
-
-// Connect sends a /connect to an eduVPN server
-// `ctx` is the context used for cancellation
-// protos is the list of protocols supported and wanted by the client
-func (a *API) Connect(ctx context.Context, prof profiles.Profile, protos []protocol.Protocol, pTCP bool) (*ConnectData, error) {
- hdrs := http.Header{
- "content-type": {"application/x-www-form-urlencoded"},
- }
- uv := url.Values{
- "profile_id": {prof.ID},
- }
-
- if len(protos) == 0 {
- return nil, ErrNoProtocols
- }
-
- var wgKey *wgtypes.Key
-
- // Loop over the protocols and set the correct headers and values
- for _, p := range protos {
- switch p {
- case protocol.WireGuard:
- gk, err := wgtypes.GeneratePrivateKey()
- if err != nil {
- return nil, err
- }
- wgKey = &gk
- // Set the public key
- pubkey := wgKey.PublicKey()
- uv.Set("public_key", pubkey.String())
- hdrs.Add("accept", "application/x-wireguard-profile")
- hdrs.Add("accept", "application/x-wireguard+tcp-profile")
- case protocol.OpenVPN:
- hdrs.Add("accept", "application/x-openvpn-profile")
- default:
- return nil, ErrUnknownProtocol
- }
- }
- // set prefer TCP
- uv.Set("prefer_tcp", boolToYesNo(pTCP))
-
- // Construct the parameters
- params := &httpw.OptionalParams{Headers: hdrs, Body: uv}
- h, body, err := a.authorizedRetry(ctx, http.MethodPost, "/connect", params)
- if err != nil {
- return nil, fmt.Errorf("failed API /connect call: %w", err)
- }
-
- // Parse expiry
- expH := h.Get("expires")
- if expH == "" {
- return nil, errors.New("the server did not give an expires header")
- }
- expT, err := http.ParseTime(expH)
- if err != nil {
- return nil, fmt.Errorf("failed parsing expiry time: %w", err)
- }
-
- vpnCfg := string(body)
- // Parse content type
- contentH := h.Get("content-type")
- proto, err := protocolFromCT(contentH)
- if err != nil {
- return nil, err
- }
-
- if proto == protocol.OpenVPN {
- // ensure scripts are not ran by default by append script-security 0 to the config
- vpnCfg += "\nscript-security 0"
- return &ConnectData{
- Configuration: vpnCfg,
- Protocol: proto,
- Expires: expT,
- }, nil
- }
-
- vpnCfg, err = wireguard.Config(vpnCfg, wgKey)
- if err != nil {
- return nil, err
- }
- return &ConnectData{
- Configuration: vpnCfg,
- Protocol: proto,
- Expires: expT,
- }, nil
-}
-
-func getEndpoints(ctx context.Context, url string, tp http.RoundTripper) (*endpoints.Endpoints, error) {
- uStr, err := httpw.JoinURLPath(url, "/.well-known/vpn-user-portal")
- if err != nil {
- return nil, err
- }
- httpC := httpw.NewClient(nil)
- httpC.Client.Transport = tp
- _, body, err := httpC.Get(ctx, uStr)
- if err != nil {
- return nil, fmt.Errorf("failed getting server endpoints with error: %w", err)
- }
-
- ep := endpoints.Endpoints{}
- if err = json.Unmarshal(body, &ep); err != nil {
- return nil, fmt.Errorf("failed getting server endpoints with error: %w", err)
- }
- err = ep.Validate()
- if err != nil {
- return nil, err
- }
- return &ep, nil
-}
-
-// OAuthLogger is defined here to update the internal logger
-// for the eduoauth library
-type OAuthLogger struct{}
-
-// Logf logs a message with parameters
-func (ol *OAuthLogger) Logf(msg string, params ...any) {
- slog.Debug("OAuth log", "log", fmt.Sprintf(msg, params...))
-}
-
-// Log logs a message
-func (ol *OAuthLogger) Log(msg string) {
- slog.Debug("OAuth log", "log", msg)
-}
-
-func init() {
- eduoauth.UpdateLogger(&OAuthLogger{})
-}