Initial commit
This commit is contained in:
commit
2a8f56610e
16 changed files with 1047 additions and 0 deletions
221
auth/auth.go
Normal file
221
auth/auth.go
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO; Handle scopes properly
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
HaveAuth bool
|
||||||
|
client *helix.Client
|
||||||
|
context *context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
var authTimeout time.Duration = time.Second * 60
|
||||||
|
|
||||||
|
func New(ctx *context.Context, c *helix.Client, s []string) (*Auth, error) {
|
||||||
|
var auth *Auth = &Auth{context: ctx, client: c}
|
||||||
|
|
||||||
|
// See about getting a token
|
||||||
|
c.SetRedirectURI("http://localhost:3000")
|
||||||
|
|
||||||
|
// See if there are any stored credentials.
|
||||||
|
creds, err := getUserToken()
|
||||||
|
if err != nil {
|
||||||
|
// Caller needs to get followup to get credentials via StartAuth()
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token in case what we've got on file is still good.
|
||||||
|
validate := func() error {
|
||||||
|
valid, resp, err := c.ValidateToken(creds.AccessToken)
|
||||||
|
if valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Validation error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = resp
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to refresh the token if possible.
|
||||||
|
refresh := func() (*helix.AccessCredentials, error) {
|
||||||
|
resp, err := c.RefreshUserAccessToken(creds.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error refreshing token: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This should return an error.
|
||||||
|
if err := validate(); err != nil {
|
||||||
|
return auth, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshedToken, err := refresh()
|
||||||
|
if err != nil {
|
||||||
|
return auth, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Maybe wrap this in a function.
|
||||||
|
c.SetUserAccessToken(refreshedToken.AccessToken)
|
||||||
|
c.SetRefreshToken(refreshedToken.RefreshToken)
|
||||||
|
storeUserToken(refreshedToken)
|
||||||
|
auth.HaveAuth = true
|
||||||
|
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns path to a token file. Creates it if it does nt exist.
|
||||||
|
func tokenFilePath() (string, error) {
|
||||||
|
storePath, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
storePath = filepath.Join(storePath, "tremble")
|
||||||
|
err = os.Mkdir(storePath, 0755)
|
||||||
|
if err != nil && !os.IsExist(err) {
|
||||||
|
log.Printf("Error creating %s: %v\n", storePath, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(storePath, "token.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reporting here for now since it's probably not worth trying to recover
|
||||||
|
// from an error here.
|
||||||
|
func storeUserToken(token *helix.AccessCredentials) {
|
||||||
|
tf, err := tokenFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't marshal %+v: %v\n", token, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(tf, b, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error writing token: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserToken() (creds helix.AccessCredentials, err error) {
|
||||||
|
tf, err := tokenFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(tf)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
if err != os.ErrNotExist {
|
||||||
|
log.Printf("Couldn't read token file: %v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(b, &creds); err != nil {
|
||||||
|
log.Printf("Error unmarshalling token: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) updateCode(code string) error {
|
||||||
|
resp, err := a.client.RequestUserAccessToken(code)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error requesting user access token: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%+v\n", resp.Data)
|
||||||
|
|
||||||
|
a.client.SetUserAccessToken(resp.Data.AccessToken)
|
||||||
|
storeUserToken(&resp.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) StartAuth(userId string, s []string) (chan int, error) {
|
||||||
|
// Need to get authorization
|
||||||
|
authUrl := a.client.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
||||||
|
ResponseType: "code",
|
||||||
|
Scopes: s,
|
||||||
|
State: userId,
|
||||||
|
ForceVerify: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Check for MacOS here.
|
||||||
|
// Open browser and do the auth. Let the auth listener take care of the
|
||||||
|
// rest.
|
||||||
|
cmd := exec.Command("xdg-open", authUrl)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := make(chan int)
|
||||||
|
|
||||||
|
a.waitForAuth(notify)
|
||||||
|
|
||||||
|
return notify, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) waitForAuth(notify chan int) {
|
||||||
|
authCtx, cancel := context.WithTimeout(*a.context, authTimeout)
|
||||||
|
srv := &http.Server{Addr: ":3000"}
|
||||||
|
|
||||||
|
var code string = ""
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
select {
|
||||||
|
case <-authCtx.Done():
|
||||||
|
// Authenticated or timed out
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Just continue
|
||||||
|
}
|
||||||
|
code = r.URL.Query().Get("code")
|
||||||
|
|
||||||
|
if code != "" {
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := srv.Shutdown(authCtx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error shutting down auth server: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.updateCode(code)
|
||||||
|
notify <- 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
err := http.ListenAndServe(":3000", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error running auth server: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
7
auth/go.mod
Normal file
7
auth/go.mod
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
module auth
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
|
|
||||||
|
require github.com/nicklaw5/helix/v2 v2.31.0
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||||
4
auth/go.sum
Normal file
4
auth/go.sum
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/nicklaw5/helix/v2 v2.31.0 h1:/8E5H20D/f3PGmSWT5NWtjwt+M8/GeCjnK/AkoLIFQA=
|
||||||
|
github.com/nicklaw5/helix/v2 v2.31.0/go.mod h1:e1GsZq4NDk9sQlPJ0Nr3+14R9cizqg09VAk7/IonpOU=
|
||||||
53
cli/categories.go
Normal file
53
cli/categories.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func categoryHelp() {
|
||||||
|
fmt.Print("\nTwitch category commands. Valid commands are:\n\n")
|
||||||
|
fmt.Println(" search <search-term>: search twitch for categories matching search term.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func categories(client *helix.Client, args []string) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
log.Error("Insufficient arguments.")
|
||||||
|
categoryHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "search":
|
||||||
|
{
|
||||||
|
data, err := searchCategories(client, args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error searching categories: %v", err)
|
||||||
|
}
|
||||||
|
_ = data
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
categoryHelp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchCategories prints a list of categories matching searchTerm. On success it also
|
||||||
|
// returns a list of categories.
|
||||||
|
func searchCategories(client *helix.Client, searchTerm string) ([]helix.Category, error) {
|
||||||
|
resp, err := client.SearchCategories(&helix.SearchCategoriesParams{
|
||||||
|
Query: searchTerm,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error while searching for category '%v'\n", searchTerm)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pagination and such
|
||||||
|
for _, category := range resp.Data.Categories {
|
||||||
|
fmt.Printf("[%v] %v\n", category.ID, category.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Data.Categories, nil
|
||||||
|
}
|
||||||
104
cli/config.go
Normal file
104
cli/config.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
UserName string
|
||||||
|
UserID string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds a valid configuration file if it exists. If nil is returned one
|
||||||
|
// should be created.
|
||||||
|
func find_config() (string, error) {
|
||||||
|
// Paths where a tremble.conf file might reside
|
||||||
|
// TODO: Check XDG_CONFIG_DIRS last in case XDG_CONFIG_HOME isn't set.
|
||||||
|
var paths []string = []string{
|
||||||
|
".",
|
||||||
|
"$XDG_CONFIG_HOME/tremble",
|
||||||
|
"$HOME/.tremble",
|
||||||
|
"$HOME/.config/tremble",
|
||||||
|
}
|
||||||
|
|
||||||
|
if configDirs, isSet := os.LookupEnv("XDG_CONFIG_DIRS"); isSet {
|
||||||
|
paths = slices.Concat(paths, strings.Split(configDirs, ":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
dir := os.ExpandEnv(p)
|
||||||
|
pstr := path.Join(dir, "tremble.conf")
|
||||||
|
|
||||||
|
_, err := os.Lstat(pstr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return pstr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("No config file found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds a configuration file and processes it if one exists.
|
||||||
|
func read_config() (*Config, error) {
|
||||||
|
pstr, err := find_config()
|
||||||
|
if err != nil {
|
||||||
|
log.Info(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cdata, err := os.ReadFile(pstr)
|
||||||
|
if err != nil {
|
||||||
|
log.Info(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the file and return.
|
||||||
|
var confOut Config
|
||||||
|
err = json.Unmarshal(cdata, &confOut)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &confOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes configuration to the tremble config file. Currently this overwrites
|
||||||
|
// the entire thing if found, or creates a new one if not.
|
||||||
|
func write_config(config *Config) error {
|
||||||
|
var pstr string
|
||||||
|
|
||||||
|
// XXX: Maybe check to make sure this isn't some substantial error.
|
||||||
|
// XXX: Some duplication here.
|
||||||
|
pstr, err := find_config()
|
||||||
|
if err != nil {
|
||||||
|
dir := os.ExpandEnv("$XDG_CONFIG_HOME/tremble")
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
log.Fatalf("Couldnt create directory '%v': %v\n", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pstr = path.Join(dir, "tremble.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this fatal in the caller, or... not. Just not here.
|
||||||
|
cdata, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't write config %v: %v\n", config, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(pstr, cdata, 0755)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't write config %v: %v\n", config, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
36
cli/games.go
Normal file
36
cli/games.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func searchGames(client *helix.Client, searchTerms []string) ([]helix.Game, error) {
|
||||||
|
resp, err := client.GetGames(&helix.GamesParams{
|
||||||
|
Names: searchTerms,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pagination and such
|
||||||
|
|
||||||
|
return resp.Data.Games, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printGames(client *helix.Client, searchTerms []string) error {
|
||||||
|
games, err := searchGames(client, searchTerms)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error while seardching for games %v\n", searchTerms)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pagination and such ?
|
||||||
|
for _, game := range games {
|
||||||
|
fmt.Printf("[%v] %v\n", game.ID, game.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
23
cli/go.mod
Normal file
23
cli/go.mod
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
module cli
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/log v0.4.0
|
||||||
|
github.com/nicklaw5/helix/v2 v2.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
)
|
||||||
40
cli/go.sum
Normal file
40
cli/go.sum
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||||
|
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||||
|
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||||
|
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
|
github.com/nicklaw5/helix/v2 v2.31.0 h1:/8E5H20D/f3PGmSWT5NWtjwt+M8/GeCjnK/AkoLIFQA=
|
||||||
|
github.com/nicklaw5/helix/v2 v2.31.0/go.mod h1:e1GsZq4NDk9sQlPJ0Nr3+14R9cizqg09VAk7/IonpOU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
43
cli/init.go
Normal file
43
cli/init.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup authentication credentials for the user.
|
||||||
|
func setupAuth() (*Config, error) {
|
||||||
|
// Yikes here we go
|
||||||
|
input := bufio.NewReaderSize(os.Stdin, 256)
|
||||||
|
|
||||||
|
// Reads user input. Fatal if there's an error.
|
||||||
|
readInput := func(line string) string {
|
||||||
|
fmt.Println(line)
|
||||||
|
in, err := input.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't read user input; %v\n", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(in, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := readInput("Please input client id.")
|
||||||
|
clientSecret := readInput("Please input client secret.")
|
||||||
|
userName := readInput("Input username you use to login to twitch.")
|
||||||
|
|
||||||
|
// TODO: Probably do some input validation. Maybe.
|
||||||
|
if len(clientSecret) == 0 || len(clientId) == 0 || len(userName) == 0 {
|
||||||
|
panic("An input was empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID needs to be set by auth because twitch needs some auth to access it.
|
||||||
|
conf := &Config{
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
UserName: userName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
32
cli/live_followed.go
Normal file
32
cli/live_followed.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getFollowedLive prints details about channels the user is following that
|
||||||
|
// are live. Currently auth is expected to be handled by the caller. The
|
||||||
|
// passed client should be ready to go. uid is just the user id string.
|
||||||
|
// XXX: Should maybe do at least scope handling here in the future.
|
||||||
|
func getFollowedLive(client *helix.Client, uid string) error {
|
||||||
|
resp, err := client.GetFollowedStream(&helix.FollowedStreamsParams{
|
||||||
|
UserID: uid,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting followed streams: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pagination and such
|
||||||
|
for _, stream := range resp.Data.Streams {
|
||||||
|
fmt.Printf("%v streaming %v for %v viewers since %v\n%v\n\n",
|
||||||
|
stream.UserName, stream.GameName, stream.ViewerCount,
|
||||||
|
stream.StartedAt, stream.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
151
cli/main.go
Normal file
151
cli/main.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
// Just a program to display information about twitch.
|
||||||
|
// Token can be found at https://dev.twitch.tv/console
|
||||||
|
// Also can be obtained at the console I beleive, but twitch-cli token also
|
||||||
|
// gives a valid token.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"auth"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authCtx, authCancel = context.WithCancel(context.Background())
|
||||||
|
var helixOptions helix.Options
|
||||||
|
var defaultUserID string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config, err := read_config()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error with config: %v\n", err)
|
||||||
|
fmt.Println("Use the init subcommand to initialize a config file.")
|
||||||
|
} else {
|
||||||
|
helixOptions.ClientID = config.ClientID
|
||||||
|
helixOptions.ClientSecret = config.ClientSecret
|
||||||
|
defaultUserID = config.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeSet := func(out *string, env string) {
|
||||||
|
val, isSet := os.LookupEnv(env)
|
||||||
|
if isSet {
|
||||||
|
*out = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override any argument if a corresponding environment variable is set.
|
||||||
|
maybeSet(&helixOptions.ClientID, "TWITCH_CLIENT_ID")
|
||||||
|
// maybeSet(&helixOptions.AppAccessToken, "TWITCH_APP_TOKEN")
|
||||||
|
maybeSet(&helixOptions.ClientSecret, "TWITCH_CLIENT_SECRET")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Println("Print help about commands here.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note:
|
||||||
|
// Currently everything is intended to just have all the scopes possible for
|
||||||
|
// us to need to be requested..
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
runAuth := func() *helix.Client {
|
||||||
|
client, err := helix.NewClient(&helixOptions)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for all scopes up front. Not handling scope renegotiation at the
|
||||||
|
// moment.
|
||||||
|
a, err := auth.New(&ctx, client, []string{"user:read:follows"})
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Need to reauth")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.HaveAuth {
|
||||||
|
ch, err := a.StartAuth("", []string{"user:read:follows"})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
printHelp()
|
||||||
|
panic("Insufficient arguments.")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "category":
|
||||||
|
{
|
||||||
|
client := runAuth()
|
||||||
|
categories(client, os.Args[2:])
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
{
|
||||||
|
client := runAuth()
|
||||||
|
search(client, os.Args[2:])
|
||||||
|
}
|
||||||
|
case "live":
|
||||||
|
{
|
||||||
|
// TODO: FlagSet
|
||||||
|
client := runAuth()
|
||||||
|
// Always assume config has ID for now.
|
||||||
|
err := getFollowedLive(client, defaultUserID)
|
||||||
|
if err != nil {
|
||||||
|
panic("Couldn't get followed streams.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "games":
|
||||||
|
{
|
||||||
|
// TODO: FlagSet
|
||||||
|
// XXX: Handle a list of game search terms maybe.
|
||||||
|
client := runAuth()
|
||||||
|
if err := printGames(client, []string{os.Args[2]}); err != nil {
|
||||||
|
panic("Couldn't list games")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "init":
|
||||||
|
{
|
||||||
|
newConfig, err := setupAuth()
|
||||||
|
if err != nil {
|
||||||
|
panic("Something went wrong with init.")
|
||||||
|
}
|
||||||
|
helixOptions.ClientID = newConfig.ClientID
|
||||||
|
helixOptions.ClientSecret = newConfig.ClientSecret
|
||||||
|
|
||||||
|
client := runAuth()
|
||||||
|
users, err := getUsersByName(client, []string{newConfig.UserName})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't look up user '%v': %v\n", newConfig.UserName, err)
|
||||||
|
}
|
||||||
|
newConfig.UserID = users[0].ID
|
||||||
|
|
||||||
|
// Hopefully the config is safe to write.
|
||||||
|
write_config(newConfig)
|
||||||
|
}
|
||||||
|
case "user":
|
||||||
|
{
|
||||||
|
// TODO: FlagSet
|
||||||
|
client := runAuth()
|
||||||
|
users, err := getUsersByName(client, []string{os.Args[2]})
|
||||||
|
if err != nil {
|
||||||
|
panic("Something went wrong with user fetch.")
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
fmt.Printf("%+v\n", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
printHelp()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
74
cli/search.go
Normal file
74
cli/search.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Comma separated strings
|
||||||
|
type CSStrings []string
|
||||||
|
|
||||||
|
func searchHelp() {
|
||||||
|
fmt.Print("\nTwitch category commands. Valid commands are:\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(client *helix.Client, args []string) {
|
||||||
|
var params = helix.StreamsParams{}
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
||||||
|
|
||||||
|
csvValue := func(in string) (s []string) {
|
||||||
|
s = strings.Split(in, ",")
|
||||||
|
for i := range s {
|
||||||
|
s[i] = strings.TrimSpace(s[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
langs := fs.String("lang", "en", "Comma separated list of languages (e.g., en,de,etc).")
|
||||||
|
games := fs.String("games", "", "List of games to search in. (IDs only for now)")
|
||||||
|
streamType := fs.String("type", "all", "Type of videos to return (e.g., all, live, or vodcast)")
|
||||||
|
|
||||||
|
_ = client
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
log.Fatalf("Couldn't parse arguments to search: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Language = csvValue(*langs)
|
||||||
|
params.GameIDs = csvValue(*games)
|
||||||
|
params.Type = *streamType
|
||||||
|
log.Infof("Params: %+v", params)
|
||||||
|
|
||||||
|
searchWithParams(client, ¶ms)
|
||||||
|
|
||||||
|
// if len(args) < 1 {
|
||||||
|
// log.Error("Insufficient arguments.")
|
||||||
|
// searchHelp()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchWithParams(client *helix.Client, params *helix.StreamsParams) {
|
||||||
|
resp, err := client.GetStreams(params)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Search error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print what we've got
|
||||||
|
for _, stream := range resp.Data.Streams {
|
||||||
|
fmt.Printf(
|
||||||
|
"%v is streaming %v for %v viewers\n",
|
||||||
|
stream.UserName,
|
||||||
|
stream.GameName,
|
||||||
|
stream.ViewerCount,
|
||||||
|
)
|
||||||
|
fmt.Printf("%v\n\n", stream.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
cli/users.go
Normal file
27
cli/users.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getUsersByName(client *helix.Client, names []string) ([]helix.User, error) {
|
||||||
|
resp, err := client.GetUsers(&helix.UsersParams{
|
||||||
|
Logins: names,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Data.Users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsersByID(client *helix.Client, IDs []string) ([]helix.User, error) {
|
||||||
|
resp, err := client.GetUsers(&helix.UsersParams{
|
||||||
|
IDs: IDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Data.Users, nil
|
||||||
|
}
|
||||||
33
flake.nix
Normal file
33
flake.nix
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
description = "A Nix-flake-based Go 1.22 development environment";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
goVersion = 23; # Change this to update the whole stack
|
||||||
|
|
||||||
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
|
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ self.overlays.default ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
go = final."go_1_${toString goVersion}";
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells = forEachSupportedSystem ({ pkgs }: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
# go (version is specified by overlay)
|
||||||
|
go
|
||||||
|
golines
|
||||||
|
gotools
|
||||||
|
gopls
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
193
generate_cert.go
Normal file
193
generate_cert.go
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
// Generate a self-signed X.509 certificate for a TLS server. Outputs to
|
||||||
|
// 'cert.pem' and 'key.pem' and will overwrite existing files.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
host = flag.String(
|
||||||
|
"host",
|
||||||
|
"",
|
||||||
|
"Comma-separated hostnames and IPs to generate a certificate for",
|
||||||
|
)
|
||||||
|
validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
|
||||||
|
validFor = flag.Duration(
|
||||||
|
"duration",
|
||||||
|
365*24*time.Hour,
|
||||||
|
"Duration that certificate is valid for",
|
||||||
|
)
|
||||||
|
isCA = flag.Bool("ca", false, "whether this cert should be its own Certificate Authority")
|
||||||
|
rsaBits = flag.Int(
|
||||||
|
"rsa-bits",
|
||||||
|
2048,
|
||||||
|
"Size of RSA key to generate. Ignored if --ecdsa-curve is set",
|
||||||
|
)
|
||||||
|
ecdsaCurve = flag.String(
|
||||||
|
"ecdsa-curve",
|
||||||
|
"",
|
||||||
|
"ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521",
|
||||||
|
)
|
||||||
|
ed25519Key = flag.Bool("ed25519", false, "Generate an Ed25519 key")
|
||||||
|
)
|
||||||
|
|
||||||
|
func publicKey(priv any) any {
|
||||||
|
switch k := priv.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return &k.PublicKey
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return &k.PublicKey
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
return k.Public().(ed25519.PublicKey)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(*host) == 0 {
|
||||||
|
log.Fatalf("Missing required --host parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
var priv any
|
||||||
|
var err error
|
||||||
|
switch *ecdsaCurve {
|
||||||
|
case "":
|
||||||
|
if *ed25519Key {
|
||||||
|
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
} else {
|
||||||
|
priv, err = rsa.GenerateKey(rand.Reader, *rsaBits)
|
||||||
|
}
|
||||||
|
case "P224":
|
||||||
|
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||||||
|
case "P256":
|
||||||
|
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
case "P384":
|
||||||
|
priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||||
|
case "P521":
|
||||||
|
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unrecognized elliptic curve: %q", *ecdsaCurve)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
|
||||||
|
// KeyUsage bits set in the x509.Certificate template
|
||||||
|
keyUsage := x509.KeyUsageDigitalSignature
|
||||||
|
// Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In
|
||||||
|
// the context of TLS this KeyUsage is particular to RSA key exchange and
|
||||||
|
// authentication.
|
||||||
|
if _, isRSA := priv.(*rsa.PrivateKey); isRSA {
|
||||||
|
keyUsage |= x509.KeyUsageKeyEncipherment
|
||||||
|
}
|
||||||
|
|
||||||
|
var notBefore time.Time
|
||||||
|
if len(*validFrom) == 0 {
|
||||||
|
notBefore = time.Now()
|
||||||
|
} else {
|
||||||
|
notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse creation date: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notAfter := notBefore.Add(*validFor)
|
||||||
|
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Acme Co"},
|
||||||
|
},
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
|
||||||
|
KeyUsage: keyUsage,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := strings.Split(*host, ",")
|
||||||
|
for _, h := range hosts {
|
||||||
|
if ip := net.ParseIP(h); ip != nil {
|
||||||
|
template.IPAddresses = append(template.IPAddresses, ip)
|
||||||
|
} else {
|
||||||
|
template.DNSNames = append(template.DNSNames, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *isCA {
|
||||||
|
template.IsCA = true
|
||||||
|
template.KeyUsage |= x509.KeyUsageCertSign
|
||||||
|
}
|
||||||
|
|
||||||
|
derBytes, err := x509.CreateCertificate(
|
||||||
|
rand.Reader,
|
||||||
|
&template,
|
||||||
|
&template,
|
||||||
|
publicKey(priv),
|
||||||
|
priv,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certOut, err := os.Create("cert.pem")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open cert.pem for writing: %v", err)
|
||||||
|
}
|
||||||
|
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||||
|
log.Fatalf("Failed to write data to cert.pem: %v", err)
|
||||||
|
}
|
||||||
|
if err := certOut.Close(); err != nil {
|
||||||
|
log.Fatalf("Error closing cert.pem: %v", err)
|
||||||
|
}
|
||||||
|
log.Print("wrote cert.pem\n")
|
||||||
|
|
||||||
|
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open key.pem for writing: %v", err)
|
||||||
|
}
|
||||||
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to marshal private key: %v", err)
|
||||||
|
}
|
||||||
|
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||||
|
log.Fatalf("Failed to write data to key.pem: %v", err)
|
||||||
|
}
|
||||||
|
if err := keyOut.Close(); err != nil {
|
||||||
|
log.Fatalf("Error closing key.pem: %v", err)
|
||||||
|
}
|
||||||
|
log.Print("wrote key.pem\n")
|
||||||
|
}
|
||||||
6
go.work
Normal file
6
go.work
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
go 1.23.3
|
||||||
|
|
||||||
|
use (
|
||||||
|
./auth
|
||||||
|
./cli
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue