Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91324e8de7 | ||
|
|
d63afc2c6d | ||
|
|
315e39d61e | ||
|
|
b1077531bf | ||
|
|
9ecc937dc4 | ||
|
|
7202b68733 |
@@ -20,6 +20,9 @@ builds:
|
||||
- 386
|
||||
goarm:
|
||||
- 7
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
ldflags:
|
||||
- -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }}
|
||||
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
FROM golang:1.24.3-alpine3.21 AS builder
|
||||
FROM golang:1.26.3-alpine3.22 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN CGO_ENABLED=0 go build .
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.22
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/glance .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.22
|
||||
|
||||
WORKDIR /app
|
||||
COPY glance .
|
||||
|
||||
@@ -267,8 +267,9 @@ Download and extract the executable from the [latest release](https://github.com
|
||||
<br>
|
||||
|
||||
Glance can also be installed through the following 3rd party channels:
|
||||
* [Proxmox VE Helper Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=glance)
|
||||
* [Proxmox VE Helper Script](https://community-scripts.org/scripts/glance?id=glance)
|
||||
* [NixOS package](https://search.nixos.org/packages?channel=unstable&show=glance)
|
||||
* [Hostinger](https://www.hostinger.com/vps/docker/glance)
|
||||
* [Coolify.io](https://coolify.io/docs/services/glance/)
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
module github.com/glanceapp/glance
|
||||
|
||||
go 1.24.3
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mmcdole/gofeed v1.3.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.4
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/text v0.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -28,6 +32,5 @@ require (
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -18,6 +20,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||
@@ -33,8 +37,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package glance
|
||||
|
||||
import "sync"
|
||||
|
||||
type SingleflightCall[T any] struct {
|
||||
done chan struct{}
|
||||
val T
|
||||
err error
|
||||
}
|
||||
|
||||
type Singleflight[T any] struct {
|
||||
fn func() (T, error)
|
||||
mu sync.Mutex
|
||||
current *SingleflightCall[T]
|
||||
}
|
||||
|
||||
func (s *Singleflight[T]) Do() (T, error) {
|
||||
s.mu.Lock()
|
||||
if s.current != nil {
|
||||
// Wait for the current call to finish and return its result
|
||||
c := s.current
|
||||
s.mu.Unlock()
|
||||
<-c.done
|
||||
return c.val, c.err
|
||||
}
|
||||
|
||||
// Initiate a new call
|
||||
c := &SingleflightCall[T]{done: make(chan struct{})}
|
||||
s.current = c
|
||||
s.mu.Unlock()
|
||||
|
||||
c.val, c.err = s.fn()
|
||||
|
||||
s.mu.Lock()
|
||||
s.current = nil
|
||||
s.mu.Unlock()
|
||||
close(c.done)
|
||||
return c.val, c.err
|
||||
}
|
||||
|
||||
func NewSingleflight[T any](fn func() (T, error)) func() (T, error) {
|
||||
sf := &Singleflight[T]{fn: fn}
|
||||
return sf.Do
|
||||
}
|
||||
@@ -2,15 +2,22 @@ package glance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -158,7 +165,7 @@ func (widget *redditWidget) parseCustomCommentsURL(subreddit, postId, postPath s
|
||||
}
|
||||
|
||||
func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
|
||||
var client requestDoer = defaultHTTPClient
|
||||
var client requestDoer = redditHTTPClient
|
||||
var baseURL string
|
||||
var requestURL string
|
||||
var headers http.Header
|
||||
@@ -212,6 +219,13 @@ func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
|
||||
}
|
||||
request.Header = headers
|
||||
|
||||
loid, err := getRedditLoidCookie()
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching reddit loid cookie: %v\n", err)
|
||||
return nil, errors.New("could not solve reddit challenge")
|
||||
}
|
||||
request.AddCookie(&http.Cookie{Name: "loid", Value: loid})
|
||||
|
||||
responseJson, err := decodeJsonFromRequest[subredditResponseJson](client, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -308,3 +322,135 @@ func (widget *redditWidget) fetchNewAppAccessToken() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// On Windows the default HTTP client works fine, but on Linux it seems to
|
||||
// get detected and blocked by reddit (or cloudflare) presumably because of TLS fingerprinting,
|
||||
// so we use uTLS to mimic a real browser's TLS fingerprint, which seems to work around the issue
|
||||
var redditHTTPClient = &http.Client{Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcpConn, err := (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uconn := utls.UClient(tcpConn, &utls.Config{
|
||||
ServerName: host,
|
||||
}, utls.HelloFirefox_Auto)
|
||||
|
||||
if err := uconn.HandshakeContext(ctx); err != nil {
|
||||
tcpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return uconn, nil
|
||||
},
|
||||
}}
|
||||
|
||||
var (
|
||||
redditChallengePattern = regexp.MustCompile(`await\(async \w+\s*=>\s*\w+\s*\+\s*\w+\)\("([^"]+)"\)`)
|
||||
redditTokenPattern = regexp.MustCompile(`name="token"\s+value="([^"]+)"`)
|
||||
)
|
||||
|
||||
// Allows all widget instances to share a single loid cookie, since we don't want to draw
|
||||
// too much attention by making a lot of requests to the flow that allows us to obtain
|
||||
// the cookie required to access the .json endpoints
|
||||
var getRedditLoidCookie = func() func() (string, error) {
|
||||
var lastUpdate time.Time
|
||||
var cachedLoid string
|
||||
|
||||
return NewSingleflight(func() (string, error) {
|
||||
// Caching for 6 hours is a bit arbitrary, presumably the cookie is valid for 24 hours,
|
||||
// but we want to keep the cache time short in the event that a cookie becomes invalid
|
||||
// for whatever reason, since we don't have a way to force refresh it
|
||||
if time.Since(lastUpdate) < 6*time.Hour && cachedLoid != "" {
|
||||
return cachedLoid, nil
|
||||
}
|
||||
|
||||
loid, err := fetchRedditLoidCookie()
|
||||
if err != nil {
|
||||
if cachedLoid != "" {
|
||||
fmt.Printf("Error fetching new reddit loid cookie, using cached value: %v\n", err)
|
||||
return cachedLoid, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
lastUpdate = time.Now()
|
||||
cachedLoid = loid
|
||||
return loid, nil
|
||||
})
|
||||
}()
|
||||
|
||||
func fetchRedditLoidCookie() (string, error) {
|
||||
request, err := http.NewRequest("GET", "https://www.reddit.com/", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
setBrowserUserAgentHeader(request)
|
||||
|
||||
response, err := redditHTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code %d when requesting challenge page", response.StatusCode)
|
||||
}
|
||||
|
||||
challengeBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
challengeMatches := redditChallengePattern.FindSubmatch(challengeBody)
|
||||
tokenMatches := redditTokenPattern.FindSubmatch(challengeBody)
|
||||
|
||||
if challengeMatches == nil {
|
||||
return "", fmt.Errorf("no JS challenge found")
|
||||
}
|
||||
|
||||
if tokenMatches == nil {
|
||||
return "", fmt.Errorf("no token found in challenge page")
|
||||
}
|
||||
|
||||
challengeStr := string(challengeMatches[1])
|
||||
token := string(tokenMatches[1])
|
||||
solution := challengeStr + challengeStr // the JS does: e + e
|
||||
|
||||
params := url.Values{
|
||||
"solution": {solution},
|
||||
"js_challenge": {"1"},
|
||||
"token": {token},
|
||||
}
|
||||
request, err = http.NewRequest("GET", "https://www.reddit.com/?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
setBrowserUserAgentHeader(request)
|
||||
|
||||
response, err = redditHTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code %d when submitting challenge solution", response.StatusCode)
|
||||
}
|
||||
|
||||
for _, cookie := range response.Cookies() {
|
||||
if cookie.Name == "loid" {
|
||||
return cookie.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no loid cookie found")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func getBrowserUserAgentHeader() string {
|
||||
userAgentPersistentVersion.Store(rand.Int32N(5))
|
||||
}
|
||||
|
||||
version := strconv.Itoa(130 + int(userAgentPersistentVersion.Load()))
|
||||
version := strconv.Itoa(148 + int(userAgentPersistentVersion.Load()))
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + version + ".0) Gecko/20100101 Firefox/" + version + ".0"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user