Compare commits

...

14 Commits

Author SHA1 Message Date
9dd951e0af chore(deps): update golang docker tag to v1.24.3 2025-05-06 21:00:55 +00:00
ngn
d133b45575
save flaresolverr solution to use it again
All checks were successful
build / build (push) Successful in 1m1s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-03 21:40:49 +03:00
ngn
c9e2cf1a44
increase the timeout for the flaresolverr
All checks were successful
build / build (push) Successful in 1m27s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-03 15:44:28 +03:00
ngn
a04cfc2fc3
update the docker workflow
All checks were successful
build / build (push) Successful in 58s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 17:47:45 +03:00
ngn
801ec1badc
make flameresolver timeout shorter
All checks were successful
Build and publish the docker image / build (push) Successful in 57s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 17:37:54 +03:00
ngn
ef63d59931
who the actual fuck wrote this dogshit http lib
All checks were successful
Build and publish the docker image / build (push) Successful in 58s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 17:26:13 +03:00
ngn
a1b89e543c
add more logging to flareresolver request
All checks were successful
Build and publish the docker image / build (push) Successful in 57s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 17:10:24 +03:00
ngn
c9d498d1f4
fix flareresolver url and use /v1 api path
All checks were successful
Build and publish the docker image / build (push) Successful in 58s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 17:04:28 +03:00
ngn
48288da414
use flareresolver url when configured
All checks were successful
Build and publish the docker image / build (push) Successful in 59s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 16:57:32 +03:00
ngn
2a7121d85c
remove unused module from ratelimit middleware
All checks were successful
Build and publish the docker image / build (push) Successful in 56s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 16:53:31 +03:00
ngn
2f160a8649
add request util to properly use flareresolver
Some checks failed
Build and publish the docker image / build (push) Failing after 1m22s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 16:49:38 +03:00
ngn
4e8606c44a
attempt to add flareresolver support
All checks were successful
Build and publish the docker image / build (push) Successful in 1m4s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 12:31:05 +03:00
ngn
ed37cc6af6
ups: update to f13ed33
All checks were successful
Build and publish the docker image / build (push) Successful in 1m8s
2025-05-02 12:01:25 +03:00
ngn
36a21ed859
add ups config
Some checks failed
Build and publish the docker image / build (push) Has been cancelled
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-02 12:00:02 +03:00
15 changed files with 240 additions and 63 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
.git
ups.json
renovate.json
README.md
LICENSE.txt
docker-compose.example.yml
Dockerfile

View File

@ -1,8 +1,15 @@
name: Build and publish the docker image
name: build
on:
push:
branches: ["custom"]
branches:
- "custom"
paths-ignore:
- "README.md"
- "LICENSE.txt"
- "*.json"
- "docker-compose.example.yml"
- ".prettierrc"
env:
REGISTRY: git.ngn.tf

View File

@ -1,4 +1,4 @@
FROM golang:1.23.6 AS build
FROM golang:1.24.3 AS build
WORKDIR /app

View File

@ -1,5 +1,7 @@
# [ngn.tf] | AnonymousOverflow
# AnonymousOverflow | frontend for stackoverflow.com
![](https://git.ngn.tf/ngn/anonymous_overflow/actions/workflows/build.yml/badge.svg)
A fork of the [AnonymousOverflow](https://github.com/httpjamesm/AnonymousOverflow) project, with my personal changes.
A fork of the
[AnonymousOverflow](https://github.com/httpjamesm/AnonymousOverflow) project,
with my personal changes.

View File

@ -29,7 +29,7 @@ func main() {
if os.Getenv("DEV") != "true" {
gin.SetMode(gin.ReleaseMode)
fmt.Printf("Running in production mode. Listening on %s:%s.", host, port)
fmt.Printf("Listening on %s:%s\n", host, port)
}
r := gin.Default()

View File

@ -1,7 +1,7 @@
package middleware
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"os"
"strings"
"sync"
@ -45,9 +45,8 @@ func Ratelimit() gin.HandlerFunc {
// if they exceed 30 requests in 1 minute, return a 429
if val.(int) > 30 {
c.HTML(429, "home.html", gin.H{
utils.Render(c, 429, "home", gin.H{
"errorMessage": "You have exceeded the request limit. Please try again in a minute.",
"version": config.Version,
})
c.Abort()
return

View File

@ -1,7 +1,6 @@
package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"fmt"
"regexp"
@ -11,11 +10,7 @@ import (
)
func GetHome(c *gin.Context) {
theme := utils.GetThemeFromEnv()
c.HTML(200, "home.html", gin.H{
"version": config.Version,
"theme": theme,
})
utils.Render(c, 200, "home", nil)
}
type urlConversionRequest struct {
@ -62,7 +57,7 @@ func PostHome(c *gin.Context) {
body := urlConversionRequest{}
if err := c.ShouldBind(&body); err != nil {
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": "Invalid request body",
})
return
@ -71,10 +66,8 @@ func PostHome(c *gin.Context) {
translated := translateUrl(body.URL)
if translated == "" {
theme := utils.GetThemeFromEnv()
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": "Invalid stack overflow/exchange URL",
"theme": theme,
})
return
}

View File

@ -2,12 +2,12 @@ package routes
import (
"anonymousoverflow/src/types"
"anonymousoverflow/src/utils"
"fmt"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
"github.com/golang-jwt/jwt/v4"
)
@ -51,16 +51,15 @@ func GetImage(c *gin.Context) {
}
// download the image
client := resty.New()
resp, err := client.R().Get(claims.ImageURL)
res, err := utils.GET(claims.ImageURL)
if err != nil {
c.AbortWithStatus(500)
return
}
// set the content type
c.Header("Content-Type", resp.Header().Get("Content-Type"))
c.Header("Content-Type", res.Header().Get("Content-Type"))
// write the image to the response
c.Writer.Write(resp.Body())
c.Writer.Write(res.Body())
}

View File

@ -1,7 +1,6 @@
package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"fmt"
@ -14,16 +13,16 @@ func ChangeOptions(c *gin.Context) {
switch name {
case "images":
text := "disabled"
if c.MustGet("disable_images").(bool) {
text = "enabled"
}
c.SetCookie("disable_images", fmt.Sprintf("%t", !c.MustGet("disable_images").(bool)), 60*60*24*365*10, "/", "", false, true)
theme := utils.GetThemeFromEnv()
c.HTML(200, "home.html", gin.H{
utils.Render(c, 200, "home", gin.H{
"successMessage": "Images are now " + text,
"version": config.Version,
"theme": theme,
})
default:
c.String(400, "400 Bad Request")
}

View File

@ -3,6 +3,7 @@ package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"bytes"
"fmt"
"html"
"html/template"
@ -15,7 +16,6 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.+?)<\/code><\/pre>`)
@ -32,7 +32,7 @@ func ViewQuestion(c *gin.Context) {
questionId := c.Param("id")
if _, err := strconv.Atoi(questionId); err != nil {
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": "Invalid question ID",
"version": config.Version,
})
@ -53,44 +53,44 @@ func ViewQuestion(c *gin.Context) {
}
soLink := fmt.Sprintf("https://%s/questions/%s/%s?answertab=%s", domain, questionId, params.QuestionTitle, params.SoSortValue)
res, err := utils.GET(soLink)
resp, err := fetchQuestionData(soLink)
if err != nil {
fmt.Printf("failed to get %s: %s", soLink, err.Error())
if resp.StatusCode() != 200 {
c.HTML(500, "home.html", gin.H{
"errorMessage": fmt.Sprintf("Received a non-OK status code %d", resp.StatusCode()),
"version": config.Version,
utils.Render(c, 500, "home", gin.H{
"errorMessage": fmt.Sprintf("Request to server failed"),
})
return
}
respBody := resp.String()
if res.StatusCode() != 200 {
utils.Render(c, 500, "home", gin.H{
"errorMessage": fmt.Sprintf("Received a non-OK status code: %d", res.StatusCode()),
})
return
}
respBodyReader := strings.NewReader(respBody)
doc, err := goquery.NewDocumentFromReader(respBodyReader)
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(res.Body()))
if err != nil {
c.HTML(500, "home.html", gin.H{
utils.Render(c, 500, "home", gin.H{
"errorMessage": "Unable to parse question data",
"version": config.Version,
})
return
}
newFilteredQuestion, err := extractQuestionData(doc, domain)
if err != nil {
c.HTML(500, "home.html", gin.H{
utils.Render(c, 500, "home", gin.H{
"errorMessage": "Failed to extract question data",
"version": config.Version,
})
return
}
answers, err := extractAnswersData(doc, domain)
if err != nil {
c.HTML(500, "home.html", gin.H{
utils.Render(c, 500, "home", gin.H{
"errorMessage": "Failed to extract answer data",
"version": config.Version,
})
return
}
@ -101,18 +101,14 @@ func ViewQuestion(c *gin.Context) {
imagePolicy = "'self'"
}
theme := utils.GetThemeFromEnv()
c.HTML(200, "question.html", gin.H{
utils.Render(c, 200, "question", gin.H{
"question": newFilteredQuestion,
"answers": answers,
"imagePolicy": imagePolicy,
"currentUrl": fmt.Sprintf("%s%s", os.Getenv("APP_URL"), c.Request.URL.Path),
"sortValue": params.SoSortValue,
"domain": domain,
"theme": theme,
})
}
type viewQuestionInputs struct {
@ -127,9 +123,8 @@ func parseAndValidateParameters(c *gin.Context) (inputs viewQuestionInputs, err
questionId := c.Param("id")
if _, err = strconv.Atoi(questionId); err != nil {
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": "Invalid question ID",
"version": config.Version,
})
return
}
@ -155,13 +150,6 @@ func parseAndValidateParameters(c *gin.Context) (inputs viewQuestionInputs, err
return
}
// fetchQuestionData sends the request to StackOverflow.
func fetchQuestionData(soLink string) (resp *resty.Response, err error) {
client := resty.New()
resp, err = client.R().Get(soLink)
return
}
// extractQuestionData parses the HTML document and extracts question data.
func extractQuestionData(doc *goquery.Document, domain string) (question types.FilteredQuestion, err error) {
// Extract the question title.

View File

@ -1,6 +1,7 @@
package routes
import (
"anonymousoverflow/src/utils"
"fmt"
"net/http"
"os"
@ -31,14 +32,14 @@ func RedirectShortenedOverflowURL(c *gin.Context) {
}
resp, err := client.R().Get(fmt.Sprintf("https://%s/a/%s/%s", domain, id, answerId))
if err != nil {
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": "Unable to fetch stack overflow URL",
})
return
}
if resp.StatusCode() != 302 {
c.HTML(400, "home.html", gin.H{
utils.Render(c, 400, "home", gin.H{
"errorMessage": fmt.Sprintf("Unexpected HTTP status from origin: %d", resp.StatusCode()),
})
return

View File

@ -42,4 +42,4 @@ func ReplaceStackOverflowLinks(html string) string {
// Replace the href attribute value in the anchor tag
return strings.Replace(match, hrefMatch[1], newUrl, 1)
})
}
}

24
src/utils/render.go Normal file
View File

@ -0,0 +1,24 @@
package utils
import (
"anonymousoverflow/config"
"github.com/gin-gonic/gin"
)
func Render(c *gin.Context, code int, temp string, _data ...gin.H) {
var data gin.H = nil
if len(_data) > 0 {
data = _data[0]
}
if data == nil {
data = gin.H{}
}
data["version"] = config.Version
data["theme"] = GetThemeFromEnv()
c.HTML(code, temp+".html", data)
}

150
src/utils/request.go Normal file
View File

@ -0,0 +1,150 @@
package utils
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
"github.com/go-resty/resty/v2"
)
// https://github.com/FlareSolverr/FlareSolverr#-requestget
type Request struct {
Cmd string `json:"cmd"`
Url string `json:"url"`
MaxTimeout int `json:"maxTimeout"`
OnlyCookies bool `json:"returnOnlyCookies"`
}
type Cookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
Path string `json:"path"`
Expires time.Time `json:"expires"`
Size uint64 `json:"size"`
HttpOnly bool `json:"httpOnly"`
Secure bool `json:"secure"`
Session bool `json:"session"`
SameSite string `json:"sameSite"`
}
type Solution struct {
Status int `json:"status"`
Cookies []Cookie `json:"cookies"`
Agent string `json:"userAgent"`
}
type Response struct {
Solution Solution `json:"solution"`
}
func (c *Cookie) ToCookie() *http.Cookie {
ss := http.SameSiteNoneMode
switch c.SameSite {
case "Lax":
ss = http.SameSiteLaxMode
case "Strict":
ss = http.SameSiteStrictMode
case "Default":
ss = http.SameSiteDefaultMode
}
return &http.Cookie{
Name: c.Name,
Value: c.Value,
Domain: c.Domain,
Path: c.Path,
Expires: c.Expires,
HttpOnly: c.HttpOnly,
Secure: c.Secure,
SameSite: ss,
}
}
func (s *Solution) HttpCookies() []*http.Cookie {
cookies := []*http.Cookie{}
for i := range s.Cookies {
cookies = append(cookies, s.Cookies[i].ToCookie())
}
return cookies
}
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3"
var solution *Solution = nil
func Solve(target string) error {
fsurl := os.Getenv("FLARESOLVER")
if fsurl == "" {
return fmt.Errorf("flaresolver is not configured")
}
fsurl, _ = url.JoinPath(fsurl, "/v1")
response := Response{}
client := resty.New()
res, err := client.R().
SetBody(Request{
Cmd: "request.get",
Url: target,
MaxTimeout: 40_000,
OnlyCookies: true,
}).
Post(fsurl)
if err != nil {
return fmt.Errorf("request failed: %s", err.Error())
}
if res.StatusCode() != 200 {
return fmt.Errorf("bad status code: %d", res.StatusCode())
}
if err := json.Unmarshal(res.Body(), &response); err != nil {
return fmt.Errorf("failed to parse body: %s", err.Error())
}
solution = &response.Solution
return nil
}
func GET(target string, no_retry ...bool) (*resty.Response, error) {
client := resty.New()
req := client.R()
if solution != nil {
req.SetCookies(solution.HttpCookies())
req.SetHeader("User-Agent", solution.Agent)
} else {
req.SetHeader("User-Agent", USER_AGENT)
}
res, err := req.Get(target)
if err != nil {
return nil, err
}
if res.StatusCode() != http.StatusForbidden {
return res, err
}
if err = Solve(target); err != nil {
return nil, err
}
if len(no_retry) == 0 {
return GET(target)
}
return nil, fmt.Errorf("solution did not work")
}

5
ups.json Normal file
View File

@ -0,0 +1,5 @@
{
"upstream": "https://github.com/httpjamesm/AnonymousOverflow",
"provider": "github",
"commit": "f13ed3387321931cea434fa3c4b7f83420a29ece"
}