forked from Silverfish/proton-bridge
184 lines
5.1 KiB
Go
184 lines
5.1 KiB
Go
// Copyright (c) 2024 Proton AG
|
|
//
|
|
// This file is part of Proton Mail Bridge.Bridge.
|
|
//
|
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package tokenservice
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/gmail/v1"
|
|
)
|
|
|
|
const (
|
|
NexusTestingPwd = "NEXUS_TESTING_PWD"
|
|
NexusTestingUser = "NEXUS_TESTING_USER"
|
|
)
|
|
|
|
var nexusAccessVerificationURL = os.Getenv("NEXUS_ACCESS_VERIFICATION_URL")
|
|
var nexusCredentialsURL = os.Getenv("NEXUS_GMAIL_CREDENTIALS_URL")
|
|
var nexusTokenURL = os.Getenv("NEXUS_GMAIL_TOKEN_URL")
|
|
|
|
var gmailScopes = []string{gmail.GmailComposeScope, gmail.GmailInsertScope, gmail.GmailLabelsScope, gmail.MailGoogleComScope, gmail.GmailMetadataScope, gmail.GmailModifyScope, gmail.GmailSendScope}
|
|
|
|
func fetchBytes(url string) ([]byte, error) {
|
|
res, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to fetch data from url %v: %v", url, err)
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read response body for url %v: %v", url, err)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func fetchConfig() (*oauth2.Config, error) {
|
|
data, err := fetchBytes(nexusCredentialsURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to fetch credentials: %v", err)
|
|
}
|
|
|
|
config, err := google.ConfigFromJSON(data, gmailScopes...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse credentials to gmail config: %v", err)
|
|
}
|
|
|
|
return config, err
|
|
}
|
|
|
|
func fetchToken() (*oauth2.Token, error) {
|
|
data, err := fetchBytes(nexusTokenURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to fetch token: %v", err)
|
|
}
|
|
|
|
token := &oauth2.Token{}
|
|
if err = json.Unmarshal(data, token); err != nil {
|
|
return nil, fmt.Errorf("error when unmarshaling token: %v", err)
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func refreshToken(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
|
|
ctx := context.Background()
|
|
tokenSource := config.TokenSource(ctx, token)
|
|
newToken, err := tokenSource.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error when refreshing access token: %v", err)
|
|
}
|
|
return newToken, nil
|
|
}
|
|
|
|
func getNexusCreds() string {
|
|
credentials := os.Getenv(NexusTestingUser) + ":" + os.Getenv(NexusTestingPwd)
|
|
return base64.StdEncoding.EncodeToString([]byte(credentials))
|
|
}
|
|
|
|
func pushToNexus(url string, data []byte) error {
|
|
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return fmt.Errorf("error creating put request to nexus: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
encodedCredentials := getNexusCreds()
|
|
req.Header.Set("Authorization", "Basic "+encodedCredentials)
|
|
|
|
client := http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("error making put request to nexus: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("unexpected status code when making put request to nexus: %v", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func uploadTokenToNexus(token *oauth2.Token) error {
|
|
jsonData, err := json.Marshal(token)
|
|
if err != nil {
|
|
return fmt.Errorf("error when encoding access token to json: %v", err)
|
|
}
|
|
|
|
return pushToNexus(nexusTokenURL, jsonData)
|
|
}
|
|
|
|
func verifyNexusAccess() error {
|
|
return pushToNexus(nexusAccessVerificationURL, nil)
|
|
}
|
|
|
|
func checkTokenValidityAndRefresh(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
|
|
// Validate token (check if it has expired, or is 5 minutes from expiring) and refresh
|
|
timeTillExpiry := time.Until(token.Expiry)
|
|
if !token.Valid() || timeTillExpiry < 5*time.Minute {
|
|
token, err := refreshToken(config, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = uploadTokenToNexus(token); err != nil {
|
|
return nil, fmt.Errorf("unable to upload token to nexus: %v", err)
|
|
}
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func LoadGmailClient(ctx context.Context) (*http.Client, error) {
|
|
err := verifyNexusAccess()
|
|
if err != nil {
|
|
log.Fatalf("error occurred when verifying nexus access, check your credentials: %v", err)
|
|
}
|
|
|
|
config, err := fetchConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("issue obtaining oauth config: %v", err)
|
|
}
|
|
|
|
token, err := fetchToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("issue obtaining oauth token: %v", err)
|
|
}
|
|
|
|
token, err = checkTokenValidityAndRefresh(config, token)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error checking token validity: %v", err)
|
|
}
|
|
|
|
client := config.Client(ctx, token)
|
|
return client, nil
|
|
}
|