diff --git a/README.md b/README.md
index c755bfd..c13df69 100644
--- a/README.md
+++ b/README.md
@@ -170,8 +170,7 @@ Note that you can pipe to anything that produces an output to the `stdin`.
### `gcp-stream` Command
l`oGGo natively supports GCP Logging but in order to use this feature, there are a few caveats:
-- You have [gcloud command line SDK](https://cloud.google.com/sdk/docs/install) installed locally.
-- Your account has the required permissions to access the logging resources.
+- Your personal account has the required permissions to access the logging resources.
Note: `gcp-stream` **does not** support piped commands. If you want to use piped
@@ -191,6 +190,7 @@ Usage:
Flags:
-f, --filter string Standard GCP filters
+ --force-auth Force re-authentication even if you may have a valid authentication file.
-d, --from string Start streaming from:
Relative: Use format "1s", "1m", "1h" or "1d", where:
digit followed by s, m, h, d as second, minute, hour, day.
@@ -198,7 +198,7 @@ Flags:
Now: Use "tail" to start from now (default "tail")
-h, --help help for gcp-stream
--params-list List saved gcp connection/filtering parameters for convenient reuse.
- --params-load string Load the parameters for reuse. If any additional parameters are
+ --params-load string Load the parameters for reuse. If any additional parameters are
provided, it overrides the loaded parameter with the one explicitly provided.
--params-save string Save the following parameters (if provided) for reuse:
Project: The GCP Project ID
diff --git a/cmd/debug.go b/cmd/debug.go
new file mode 100644
index 0000000..bba8896
--- /dev/null
+++ b/cmd/debug.go
@@ -0,0 +1,47 @@
+/*
+Copyright © 2022 Aurelio Calegari, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package cmd
+
+import (
+ "github.com/aurc/loggo/internal/loggo"
+ "github.com/aurc/loggo/internal/reader"
+ "github.com/spf13/cobra"
+)
+
+// streamCmd represents the stream command
+var debugCmd = &cobra.Command{
+ Use: "debug",
+ Short: "Continuously stream l'oggo log",
+ Long: `This command aims to assist troubleshoot loggos issue and would be rarely utilised by loggo's users':
+
+ loggo debug`,
+ Run: func(cmd *cobra.Command, args []string) {
+ reader := reader.MakeReader(loggo.LatestLog, nil)
+ app := loggo.NewLoggoApp(reader, "")
+ app.Run()
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(debugCmd)
+}
diff --git a/cmd/gcpstream.go b/cmd/gcpstream.go
index b4ad169..e509cdd 100644
--- a/cmd/gcpstream.go
+++ b/cmd/gcpstream.go
@@ -24,10 +24,13 @@ package cmd
import (
"context"
- "log"
"strconv"
"time"
+ "github.com/aurc/loggo/internal/util"
+
+ "github.com/aurc/loggo/internal/gcp"
+
"github.com/aurc/loggo/internal/loggo"
"github.com/aurc/loggo/internal/reader"
"github.com/spf13/cobra"
@@ -40,9 +43,10 @@ var gcpStreamCmd = &cobra.Command{
Long: `Continuously stream Google Cloud Platform log entries
from a given selected project and GCP logging filters:
- loggo gcp-stream --project myGCPProject123 --from 1m \
- --filter 'resource.labels.namespace_name="awesome-sit" AND resource.labels.container_name="some"' \
- --template
+ loggo gcp-stream \
+ --project myGCPProject123 \
+ --from 1m \
+ --filter 'resource.labels.namespace_name="awesome-sit" AND resource.labels.container_name="some"'
`,
Run: func(cmd *cobra.Command, args []string) {
projectName := cmd.Flag("project").Value.String()
@@ -53,6 +57,10 @@ from a given selected project and GCP logging filters:
listParams := cmd.Flag("params-list").Value.String()
lp, _ := strconv.ParseBool(listParams)
loadParams := cmd.Flag("params-load").Value.String()
+ auth, _ := strconv.ParseBool(cmd.Flag("force-auth").Value.String())
+ if auth {
+ gcp.Delete()
+ }
if len(saveParams) > 0 {
if err := reader.Save(saveParams,
&reader.SavedParams{
@@ -61,12 +69,12 @@ from a given selected project and GCP logging filters:
Project: projectName,
Template: templateFile,
}); err != nil {
- log.Fatal(err)
+ util.Log().Fatal(err)
}
} else if lp {
l, err := reader.List()
if err != nil {
- log.Fatal(err)
+ util.Log().Fatal(err)
}
for _, v := range l {
v.Print()
@@ -75,7 +83,7 @@ from a given selected project and GCP logging filters:
if len(loadParams) > 0 {
p, err := reader.Load(loadParams)
if err != nil {
- log.Fatal(err)
+ util.Log().Fatal(err)
}
if len(templateFile) == 0 && len(p.Template) > 0 {
templateFile = p.Template
@@ -91,11 +99,11 @@ from a given selected project and GCP logging filters:
}
}
if len(projectName) == 0 {
- log.Fatal("--project flag is required.")
+ util.Log().Fatal("--project flag is required.")
}
err := reader.CheckAuth(context.Background(), projectName)
if err != nil {
- log.Fatal("Unable to obtain GCP credentials. ", err)
+ util.Log().Fatal("Unable to obtain GCP credentials. ", err)
}
time.Sleep(time.Second)
reader := reader.MakeGCPReader(projectName, filter, reader.ParseFrom(from), nil)
@@ -138,4 +146,7 @@ provided, it overrides the loaded parameter with the one explicitly provided.`)
BoolP("params-list", "", false,
"List saved gcp connection/filtering parameters for convenient reuse.")
gcpStreamCmd.MarkFlagsMutuallyExclusive("params-save", "params-load", "params-list")
+ gcpStreamCmd.Flags().
+ BoolP("force-auth", "", false,
+ "Force re-authentication even if you may have a valid authentication file.")
}
diff --git a/cmd/root.go b/cmd/root.go
index 9dbcfca..6caea18 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -23,9 +23,9 @@ THE SOFTWARE.
package cmd
import (
- "github.com/aurc/loggo/internal/loggo"
"os"
+ "github.com/aurc/loggo/internal/loggo"
"github.com/spf13/cobra"
)
@@ -59,5 +59,5 @@ func init() {
// Cobra also supports local flags, which will only run
// when this action is called directly.
- rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
diff --git a/cmd/template.go b/cmd/template.go
index 2fabdd2..635438d 100644
--- a/cmd/template.go
+++ b/cmd/template.go
@@ -25,8 +25,8 @@ package cmd
import (
"github.com/aurc/loggo/internal/config"
"github.com/aurc/loggo/internal/loggo"
+ "github.com/aurc/loggo/internal/util"
"github.com/spf13/cobra"
- "log"
)
// templateCmd represents the template command
@@ -61,7 +61,7 @@ To start from an example template:
cfg, err = config.MakeConfig(templateFile)
}
if err != nil {
- log.Fatalln("Unable to start app: ", err)
+ util.Log().Fatal("Unable to start app: ", err)
}
app := loggo.NewAppWithConfig(cfg)
view := loggo.NewTemplateView(app, true, nil, nil)
diff --git a/internal/char/canvas.go b/internal/char/canvas.go
index 28259be..88a5d17 100644
--- a/internal/char/canvas.go
+++ b/internal/char/canvas.go
@@ -23,6 +23,9 @@ THE SOFTWARE.
package char
import (
+ "bufio"
+ "bytes"
+ "fmt"
"strings"
)
@@ -131,6 +134,45 @@ func (c *Canvas) PrintCanvas() [][]rune {
return bc
}
+func (c *Canvas) PrintCanvasAsHtml() string {
+ str := c.PrintCanvasAsString()
+ buf := bytes.NewBufferString(str)
+ reader := bufio.NewReader(buf)
+ builder := strings.Builder{}
+ convMap := map[rune]string{
+ '▓': "▓",
+ '░': "░",
+ '╬': "╬",
+ '╦': "╦",
+ '╩': "╩",
+ '╠': "╠",
+ '╣': "╣",
+ '╔': "╔",
+ '╗': "╗",
+ '╚': "╚",
+ '╝': "╝",
+ }
+ paintChar := '▓'
+ shade := '░'
+ for {
+ str, err := reader.ReadString('\n')
+ if err == nil {
+ for _, char := range str {
+ switch char {
+ case paintChar, shade:
+ builder.WriteString(fmt.Sprintf(`%s`, convMap[char]))
+ default:
+ builder.WriteString(fmt.Sprintf(`%s`, convMap[char]))
+ }
+ }
+ builder.WriteString("
\n")
+ } else {
+ break
+ }
+ }
+ return builder.String()
+}
+
func (c *Canvas) PrintCanvasAsString() string {
return c.toString(c.PrintCanvas())
}
diff --git a/internal/char/canvas_test.go b/internal/char/canvas_test.go
index 4fc70c0..dcec04a 100644
--- a/internal/char/canvas_test.go
+++ b/internal/char/canvas_test.go
@@ -24,8 +24,9 @@ package char
import (
"fmt"
- "github.com/stretchr/testify/assert"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func TestCanvas_BlankCanvas(t *testing.T) {
@@ -152,3 +153,9 @@ func TestCanvas_BlankCanvasAsString(t *testing.T) {
})
}
}
+
+func TestCanvas_PrintCanvasAsHtml(t *testing.T) {
+ c := NewCanvas().WithWord(LoggoLogo...)
+ str := c.PrintCanvasAsHtml()
+ fmt.Println(str)
+}
diff --git a/internal/gcp/auth.go b/internal/gcp/auth.go
new file mode 100644
index 0000000..9e0dcc6
--- /dev/null
+++ b/internal/gcp/auth.go
@@ -0,0 +1,69 @@
+/*
+Copyright © 2022 Aurelio Calegari, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package gcp
+
+import (
+ "encoding/json"
+ "os"
+ "path"
+)
+
+type Auth struct {
+ ClientId string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+ RefreshToken string `json:"refresh_token"`
+ Type string `json:"type"`
+}
+
+func AuthDir() string {
+ hd, _ := os.UserHomeDir()
+ dir := path.Join(hd, ".loggo", "auth")
+ return dir
+}
+
+func AuthFile() string {
+ return path.Join(AuthDir(), "gcp.json")
+}
+
+func Delete() {
+ _ = os.Remove(AuthFile())
+}
+
+func (a *Auth) Save() error {
+ if err := os.MkdirAll(AuthDir(), os.ModePerm); err != nil {
+ return err
+ }
+ b, err := json.MarshalIndent(a, "", " ")
+ if err != nil {
+ return err
+ }
+
+ file, err := os.Create(AuthFile())
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = file.Write(b)
+ return err
+}
diff --git a/internal/gcp/challenger.go b/internal/gcp/challenger.go
new file mode 100644
index 0000000..a6f5bd2
--- /dev/null
+++ b/internal/gcp/challenger.go
@@ -0,0 +1,109 @@
+/*
+Copyright © 2022 Aurelio Calegari, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package gcp
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "strings"
+)
+
+const (
+ DefaultLength = 32
+ MinLength = 32
+ MaxLength = 96
+)
+
+type CodeVerifier struct {
+ Value string
+}
+
+func CreateCodeVerifier() (*CodeVerifier, error) {
+ return CreateCodeVerifierWithLength(DefaultLength)
+}
+
+func CreateCodeVerifierWithLength(length int) (*CodeVerifier, error) {
+ if length < MinLength || length > MaxLength {
+ return nil, fmt.Errorf("invalid length: %v", length)
+ }
+ buf, err := randomBytes(length)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate random bytes: %v", err)
+ }
+ return CreateCodeVerifierFromBytes(buf)
+}
+
+func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) {
+ return &CodeVerifier{
+ Value: encode(b),
+ }, nil
+}
+
+func (v *CodeVerifier) String() string {
+ return v.Value
+}
+
+func (v *CodeVerifier) CodeChallengePlain() string {
+ return v.Value
+}
+
+func (v *CodeVerifier) CodeChallengeS256() string {
+ h := sha256.New()
+ h.Write([]byte(v.Value))
+ return encode(h.Sum(nil))
+}
+
+func encode(msg []byte) string {
+ encoded := base64.StdEncoding.EncodeToString(msg)
+ encoded = strings.Replace(encoded, "+", "-", -1)
+ encoded = strings.Replace(encoded, "/", "_", -1)
+ encoded = strings.Replace(encoded, "=", "", -1)
+ return encoded
+}
+
+// https://tools.ietf.org/html/rfc7636#section-4.1)
+func randomBytes(length int) ([]byte, error) {
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ const csLen = byte(len(charset))
+ output := make([]byte, 0, length)
+ for {
+ buf := make([]byte, length)
+ if _, err := io.ReadFull(rand.Reader, buf); err != nil {
+ return nil, fmt.Errorf("failed to read random bytes: %v", err)
+ }
+ for _, b := range buf {
+ // Avoid bias by using a value range that's a multiple of 62
+ if b < (csLen * 4) {
+ output = append(output, charset[b%csLen])
+
+ if len(output) == length {
+ return output, nil
+ }
+ }
+ }
+ }
+
+}
diff --git a/internal/gcp/login.go b/internal/gcp/login.go
new file mode 100644
index 0000000..12aee54
--- /dev/null
+++ b/internal/gcp/login.go
@@ -0,0 +1,203 @@
+/*
+Copyright © 2022 Aurelio Calegari, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package gcp
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/aurc/loggo/internal/char"
+ "github.com/aurc/loggo/internal/util"
+)
+
+//const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth/oauthchooseaccount"
+
+const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
+const codeChallengeMethod = "S256"
+
+// Client ID from project "usable-auth-library", configured for
+// general purpose API testing, extracted from gcloud sdk sourcecode.
+const (
+ DefaultCredentialsDefaultClientId = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
+ DefaultCredentialsDefaultClientSecret = "d-FL95Q19q7MQmFpd7hHD0Ty"
+)
+
+func OAuth() {
+ doOAuthAsync(DefaultCredentialsDefaultClientId, DefaultCredentialsDefaultClientSecret)
+}
+
+func doOAuthAsync(clientId, clientSecret string) {
+ // Generates state and PKCE values.
+ state, _ := CreateCodeVerifier()
+ codeVerifier, _ := CreateCodeVerifier()
+
+ // Creates a redirect URI using an available port on the loopback address.
+ listener, err := net.Listen("tcp", ":0")
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+ tcp := listener.Addr().(*net.TCPAddr)
+
+ redirectUri := fmt.Sprintf("http://%s:%d/", "127.0.0.1", tcp.Port)
+
+ scopes := []string{
+ "openid",
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/cloud-platform",
+ "https://www.googleapis.com/auth/accounts.reauth",
+ }
+
+ // Creates the OAuth 2.0 authorization request.
+ data := url.Values{}
+ data.Set("response_type", "code")
+ data.Set("scope", strings.Join(scopes, " "))
+ data.Set("redirect_uri", redirectUri)
+ data.Set("access_type", "offline")
+ data.Set("client_id", clientId)
+ data.Set("state", state.String())
+ data.Set("flowName", "GeneralOAuthFlow")
+ data.Set("code_challenge", codeVerifier.CodeChallengeS256())
+ data.Set("code_challenge_method", codeChallengeMethod)
+
+ authorizationRequest := fmt.Sprintf(`%s?%s`, AuthorizationEndpoint, data.Encode())
+ c := &callbackHandler{
+ state: state.String(),
+ code: make(chan string, 1),
+ }
+
+ go func() {
+ err = util.OpenBrowser(authorizationRequest)
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+
+ }()
+
+ go func() {
+ err = http.Serve(listener, c)
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+ }()
+
+ code := <-c.code
+ a := exchangeCodeForTokensAsync(code, codeVerifier.String(), redirectUri, clientId, clientSecret)
+ a.Save()
+}
+
+func exchangeCodeForTokensAsync(code, codeVerifier, redirectUri, clientId, clientSecret string) *Auth {
+ // builds the request
+ tokenRequestUri := "https://www.googleapis.com/oauth2/v4/token"
+
+ data := url.Values{}
+ data.Set("code", code)
+ data.Set("redirect_uri", redirectUri)
+ data.Set("client_id", clientId)
+ data.Set("code_verifier", codeVerifier)
+ data.Set("client_secret", clientSecret)
+ data.Set("scope", "")
+ data.Set("grant_type", "authorization_code")
+ encodedData := data.Encode()
+
+ req, err := http.NewRequest(http.MethodPost, tokenRequestUri, strings.NewReader(encodedData))
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(encodedData)))
+
+ client := &http.Client{}
+ response, err := client.Do(req)
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+ body := bytes.NewBufferString("")
+ _, err = body.ReadFrom(response.Body)
+ if err != nil {
+ util.Log().Fatal(err)
+ }
+ m := make(map[string]string)
+ _ = json.Unmarshal(body.Bytes(), &m)
+ return &Auth{
+ ClientId: clientId,
+ ClientSecret: clientSecret,
+ RefreshToken: m["refresh_token"],
+ Type: "authorized_user",
+ }
+}
+
+type callbackHandler struct {
+ state string
+ code chan string
+}
+
+func (c *callbackHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ time.Sleep(time.Second)
+ vals := req.URL.Query()
+
+ if strings.Contains(req.RequestURI, "favicon") {
+ resp.WriteHeader(404)
+ return
+ }
+ if vals.Has("error") {
+ _, _ = resp.Write([]byte(fmt.Sprintf(`