Skip to content

Commit

Permalink
fix(credentials): use YAML and allow token-based auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Darkness4 committed Aug 20, 2024
1 parent fc933bd commit 48db007
Show file tree
Hide file tree
Showing 17 changed files with 220 additions and 195 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ target/
out/
*.local
cookies.txt
credentials*.txt
credentials*.yaml

.vscode/

Expand Down
47 changes: 30 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,21 @@ Available format options:

Streaming:

--credentials-file value Path to a credentials file. Format: 'usernameb64:passwordb64' with usernameb64 and passwordb64 encoded in base64 (like basic authentication).
--credentials.email value Email for withny login
--credentials.password value Password for withny login
--credentials.username value Username for withny login
--quality.audio-only Only download audio streams. (default: false)
--quality.max-bandwidth value Maximum inclusive bandwidth of the stream. (default: 0)
--quality.max-framerate value Maximum inclusive framerate of the stream. (default: 0)
--quality.max-height value Maximum inclusive height of the stream. (default: 0)
--quality.max-width value Maximum inclusive width of the stream. (default: 0)
--quality.min-bandwidth value Minimum inclusive bandwidth of the stream. (default: 0)
--quality.min-framerate value Minimum inclusive framerate of the stream. (default: 0)
--quality.min-height value Minimum inclusive height of the stream. (default: 0)
--quality.min-width value Minimum inclusive width of the stream. (default: 0)
--credentials-file value Path to a credentials file. Format is YAML and must contain 'username' and 'password' or 'access-token' and 'refresh-token'.
--credentials.access-token value Access token for withny login. You should also provide a refresh token.
--credentials.password value Password for withny login
--credentials.refresh-token value Refresh token for withny login.
--credentials.username value, --credentials.email value Username/email for withny login
--quality.audio-only Only download audio streams. (default: false)
--quality.max-bandwidth value Maximum inclusive bandwidth of the stream. (default: 0)
--quality.max-framerate value Maximum inclusive framerate of the stream. (default: 0)
--quality.max-height value Maximum inclusive height of the stream. (default: 0)
--quality.max-width value Maximum inclusive width of the stream. (default: 0)
--quality.min-bandwidth value Minimum inclusive bandwidth of the stream. (default: 0)
--quality.min-framerate value Minimum inclusive framerate of the stream. (default: 0)
--quality.min-height value Minimum inclusive height of the stream. (default: 0)
--quality.min-width value Minimum inclusive width of the stream. (default: 0)

```

### Download multiple live withny streams
Expand Down Expand Up @@ -211,11 +213,22 @@ To configure the watcher, you must provide a configuration file. The configurati

```yaml
---
## [REQUIRED] Path to the file containing a "usernameB64:passwordB64" pair.
---
## [REQUIRED] Path to the file containing the credentials. (default: '')
##
## Example of content:
##
## ```yaml
## # User/Password-based
## username: admin
## password: password
##
## # Token-based
## token: "ey..."
## refreshToken: "abc..."
## ```
##
## usernameB64 is the base64 encoded username.
## passwordB64 is the base64 encoded password.
credentialsFile: ''
credentialsFile: 'credentials.yaml'

defaultParams:
## Quality constraint to select the stream to download.
Expand Down
31 changes: 19 additions & 12 deletions cmd/download/download_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var (
loop bool

credentialFile string
credentialsStatic secret.UserPasswordStatic
credentialsStatic secret.Static
)

// Command is the command for downloading a live withny stream.
Expand Down Expand Up @@ -181,28 +181,35 @@ Available format options:
},
&cli.PathFlag{
Name: "credentials-file",
Usage: "Path to a credentials file. Format: 'usernameb64:passwordb64' with usernameb64 and passwordb64 encoded in base64 (like basic authentication).",
Usage: "Path to a credentials file. Format is YAML and must contain 'username' and 'password' or 'access-token' and 'refresh-token'.",
Category: "Streaming:",
Destination: &credentialFile,
},
&cli.StringFlag{
Name: "credentials.username",
Usage: "Username for withny login",
Usage: "Username/email for withny login",
Category: "Streaming:",
Destination: &credentialsStatic.Email,
},
&cli.StringFlag{
Name: "credentials.email",
Usage: "Email for withny login",
Category: "Streaming:",
Destination: &credentialsStatic.Email,
Aliases: []string{"credentials.email"},
Destination: &credentialsStatic.Username,
},
&cli.StringFlag{
Name: "credentials.password",
Usage: "Password for withny login",
Category: "Streaming:",
Destination: &credentialsStatic.Password,
},
&cli.StringFlag{
Name: "credentials.access-token",
Usage: "Access token for withny login. You should also provide a refresh token.",
Category: "Streaming:",
Destination: &credentialsStatic.Token,
},
&cli.StringFlag{
Name: "credentials.refresh-token",
Usage: "Refresh token for withny login.",
Category: "Streaming:",
Destination: &credentialsStatic.RefreshToken,
},
&cli.BoolFlag{
Name: "no-wait",
Value: false,
Expand Down Expand Up @@ -259,8 +266,8 @@ Available format options:
}
hclient := &http.Client{Jar: jar, Timeout: time.Minute}

var reader api.UserPasswordReader
if credentialsStatic.Email != "" && credentialsStatic.Password != "" {
var reader api.CredentialsReader
if credentialsStatic.Username != "" || credentialsStatic.Token != "" {
reader = &credentialsStatic
}
if credentialFile != "" {
Expand Down
62 changes: 6 additions & 56 deletions cmd/logintest/logintest_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package logintest

import (
"context"
"encoding/base64"
"net/http"
"net/http/cookiejar"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"

Expand All @@ -23,7 +20,7 @@ import (
// Command is the command for logging in and testing the login.
var Command = &cli.Command{
Name: "login-test",
Usage: "Test the login and encode to base64.",
Usage: "Test the login.",
Action: func(cCtx *cli.Context) error {
ctx, cancel := context.WithCancel(cCtx.Context)

Expand Down Expand Up @@ -54,9 +51,11 @@ var Command = &cli.Command{
return err
}

client := api.NewClient(hclient, &secret.UserPasswordStatic{
Email: username,
Password: password,
client := api.NewClient(hclient, &secret.Static{
SavedCredentials: api.SavedCredentials{
Username: username,
Password: password,
},
})
if err := client.Login(ctx); err != nil {
log.Err(err).
Expand All @@ -65,55 +64,6 @@ var Command = &cli.Command{
}

log.Info().Msg("Login successful")

usernameB64 := base64.StdEncoding.EncodeToString([]byte(username))
passwordB64 := base64.StdEncoding.EncodeToString([]byte(password))

f, err := createFileWithRename("credentials.txt")
if err != nil {
log.Err(err).Msg("failed to create credentials.txt")
return err
}
defer f.Close()
if err := f.Chmod(0600); err != nil {
log.Err(err).Msg("failed to chmod credentials.txt")
return err
}
if _, err := f.WriteString(usernameB64 + ":" + passwordB64); err != nil {
log.Err(err).Msg("failed to write credentials.txt")
return err
}

log.Info().Msg("Credentials written to credentials.txt")
return nil
},
}

// Create a file, renaming it if there's a conflict
func createFileWithRename(path string) (*os.File, error) {
base := filepath.Base(path)
ext := filepath.Ext(path)
dir := filepath.Dir(path)
name := base[:len(base)-len(ext)]

// Attempt to create the file, renaming it if necessary
for i := 0; ; i++ {
newPath := filepath.Join(dir, name+ext)
if i > 0 {
newPath = filepath.Join(dir, name+"_"+strconv.Itoa(i)+ext)
}

// Attempt to open the file for creation
file, err := os.OpenFile(newPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err == nil {
// Successfully created the file
return file, nil
} else if os.IsExist(err) {
// File already exists, continue renaming
continue
} else {
// Some other error occurred
return nil, err
}
}
}
18 changes: 14 additions & 4 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
---
## [REQUIRED] Path to the file containing a "usernameB64:passwordB64" pair.
## [REQUIRED] Path to the file containing the credentials. (default: '')
##
## usernameB64 is the base64 encoded username.
## passwordB64 is the base64 encoded password.
credentialsFile: ''
## Example of content:
##
## ```yaml
## # User/Password-based
## username: admin
## password: password
##
## # Token-based
## token: "ey..."
## refreshToken: "abc..."
## ```
##
credentialsFile: 'credentials.yaml'

defaultParams:
## Quality constraint to select the stream to download.
Expand Down
2 changes: 1 addition & 1 deletion deployments/docker/config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
credentialsFile: '/secrets/credentials.txt'
credentialsFile: '/secrets/credentials.yaml'

notifier:
gotify:
Expand Down
1 change: 0 additions & 1 deletion deployments/docker/credentials.txt

This file was deleted.

5 changes: 5 additions & 0 deletions deployments/docker/credentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
username: admin
password: password
# Or, use token-based authentication:
# token: "ey..."
# refreshToken: "abc..."
3 changes: 1 addition & 2 deletions deployments/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ services:
- 3000:3000
user: '${UID}:${GID}'
volumes:
# credentials.txt contains "example" as username and "password" as password, in base64.
- ./credentials.txt:/secrets/credentials.txt:ro
- ./credentials.yaml:/secrets/credentials.yaml:ro
- ./config.yaml:/config/config.yaml:ro
- ./output:/output
mem_reservation: 256m
Expand Down
2 changes: 1 addition & 1 deletion deployments/kubernetes/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
data:
config.yaml: |
credentialsFile: '/secrets/credentials.txt'
credentialsFile: '/secrets/credentials.yaml'
notifier:
gotify:
Expand Down
6 changes: 5 additions & 1 deletion deployments/kubernetes/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ metadata:
name: credentials-secret
type: Opaque
stringData:
credentials.txt: ZXhhbXBsZQo=:cGFzc3dvcmQK
credentials.yaml: |
username: admin
password: admin
# token: "ey..."
# refreshToken: "abc..."
24 changes: 12 additions & 12 deletions hls/hls_downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ func NewDownloader(
}

// GetFragmentURLs fetches the fragment URLs from the HLS manifest.
func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error) {
func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]Fragment, error) {
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
req, err := hls.NewAuthRequestWithContext(ctx, "GET", hls.url, nil)
if err != nil {
return []fragment{}, err
return []Fragment{}, err
}
req.Header.Set(
"Accept",
Expand All @@ -71,7 +71,7 @@ func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error)

resp, err := hls.Client.Do(req)
if err != nil {
return []fragment{}, err
return []Fragment{}, err
}
defer resp.Body.Close()

Expand All @@ -88,15 +88,15 @@ func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error)
Str("method", "GET").
Msg("http error")
metrics.Downloads.Errors.Add(ctx, 1)
return []fragment{}, ErrHLSForbidden
return []Fragment{}, ErrHLSForbidden
case 404:
hls.log.Warn().
Str("url", url.String()).
Int("response.status", resp.StatusCode).
Str("response.body", string(body)).
Str("method", "GET").
Msg("stream not ready")
return []fragment{}, nil
return []Fragment{}, nil
default:
hls.log.Error().
Str("url", url.String()).
Expand All @@ -105,16 +105,16 @@ func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error)
Str("method", "GET").
Msg("http error")
metrics.Downloads.Errors.Add(ctx, 1)
return []fragment{}, errors.New("http error")
return []Fragment{}, errors.New("http error")
}
}

scanner := bufio.NewScanner(resp.Body)
fragments := make([]fragment, 0, 10)
fragments := make([]Fragment, 0, 10)
exists := make(map[string]bool) // Avoid duplicates

// URLs are supposedly sorted.
var currentFragment fragment
var currentFragment Fragment
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

Expand All @@ -139,7 +139,7 @@ func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error)
continue
}
currentFragment.URL = line
fragments = append(fragments, fragment{
fragments = append(fragments, Fragment{
URL: currentFragment.URL,
Time: currentFragment.Time,
})
Expand All @@ -157,7 +157,7 @@ func (hls *Downloader) GetFragmentURLs(ctx context.Context) ([]fragment, error)
// fillQueue continuously fetches fragments url until stream end
func (hls *Downloader) fillQueue(
ctx context.Context,
fragChan chan<- fragment,
fragChan chan<- Fragment,
) (err error) {
hls.log.Debug().Msg("started to fill queue")
ctx, span := otel.Tracer(tracerName).Start(ctx, "hls.fillQueue")
Expand Down Expand Up @@ -300,7 +300,7 @@ func (hls *Downloader) download(
return err
}

type fragment struct {
type Fragment struct {
URL string
Time time.Time
}
Expand All @@ -326,7 +326,7 @@ func (hls *Downloader) Read(
errChan := make(chan error) // Blocking channel is used to wait for fillQueue to finish.
defer close(errChan)

fragChan := make(chan fragment, 10)
fragChan := make(chan Fragment, 10)
defer close(fragChan)

go func() {
Expand Down
Loading

0 comments on commit 48db007

Please sign in to comment.