Compare commits

...
6 Commits
Author SHA1 Message Date
Svilen Markov 91324e8de7 Remove windows/arm build
Create release / release (push) Has been cancelled
2026-05-30 21:35:16 +01:00
Svilen Markov d63afc2c6d Work around Reddit's TLS handshake detection 2026-05-30 21:07:07 +01:00
Svilen Markov 315e39d61e Bump go and alpine versions 2026-05-30 19:42:44 +01:00
Svilen Markov b1077531bf Update readme 2026-05-30 19:42:32 +01:00
Svilen Markov 9ecc937dc4 Add a way to bypass Reddit API restrictions 2026-05-30 19:42:17 +01:00
Svilen Markov 7202b68733 Bump spoofed user agent versions 2026-05-30 19:40:37 +01:00
9 changed files with 214 additions and 11 deletions
+3
View File
@@ -20,6 +20,9 @@ builds:
- 386 - 386
goarm: goarm:
- 7 - 7
ignore:
- goos: windows
goarch: arm
ldflags: ldflags:
- -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }} - -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }}
+2 -2
View File
@@ -1,10 +1,10 @@
FROM golang:1.24.3-alpine3.21 AS builder FROM golang:1.26.3-alpine3.22 AS builder
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN CGO_ENABLED=0 go build . RUN CGO_ENABLED=0 go build .
FROM alpine:3.21 FROM alpine:3.22
WORKDIR /app WORKDIR /app
COPY --from=builder /app/glance . COPY --from=builder /app/glance .
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.22
WORKDIR /app WORKDIR /app
COPY glance . COPY glance .
+2 -1
View File
@@ -267,8 +267,9 @@ Download and extract the executable from the [latest release](https://github.com
<br> <br>
Glance can also be installed through the following 3rd party channels: 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) * [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/) * [Coolify.io](https://coolify.io/docs/services/glance/)
<hr> <hr>
+6 -3
View File
@@ -1,23 +1,27 @@
module github.com/glanceapp/glance module github.com/glanceapp/glance
go 1.24.3 go 1.26.3
require ( require (
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/mmcdole/gofeed v1.3.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 github.com/tidwall/gjson v1.18.0
golang.org/x/crypto v0.38.0 golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/text v0.25.0 golang.org/x/text v0.25.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/PuerkitoBio/goquery v1.10.3 // indirect 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/andybalholm/cascadia v1.3.3 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // 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/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mmcdole/goxpp v1.1.1 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // 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 golang.org/x/sys v0.33.0 // indirect
) )
+8 -2
View File
@@ -1,5 +1,7 @@
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= 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 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= 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= 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 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= 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/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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+44
View File
@@ -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
}
+147 -1
View File
@@ -2,15 +2,22 @@ package glance
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"html" "html"
"html/template" "html/template"
"io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
) )
var ( var (
@@ -158,7 +165,7 @@ func (widget *redditWidget) parseCustomCommentsURL(subreddit, postId, postPath s
} }
func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) { func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
var client requestDoer = defaultHTTPClient var client requestDoer = redditHTTPClient
var baseURL string var baseURL string
var requestURL string var requestURL string
var headers http.Header var headers http.Header
@@ -212,6 +219,13 @@ func (widget *redditWidget) fetchSubredditPosts() (forumPostList, error) {
} }
request.Header = headers 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) responseJson, err := decodeJsonFromRequest[subredditResponseJson](client, request)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -308,3 +322,135 @@ func (widget *redditWidget) fetchNewAppAccessToken() error {
return nil 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")
}
+1 -1
View File
@@ -51,7 +51,7 @@ func getBrowserUserAgentHeader() string {
userAgentPersistentVersion.Store(rand.Int32N(5)) 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" return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + version + ".0) Gecko/20100101 Firefox/" + version + ".0"
} }