Compare commits

..

1 Commits

Author SHA1 Message Date
de880da152 Update golang Docker tag to v1.24.2 2025-04-01 21:00:33 +00:00
15 changed files with 63 additions and 240 deletions

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# AnonymousOverflow | frontend for stackoverflow.com
# [ngn.tf] | AnonymousOverflow
![](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("Listening on %s:%s\n", host, port)
fmt.Printf("Running in production mode. Listening on %s:%s.", host, port)
}
r := gin.Default()

View File

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

View File

@ -1,6 +1,7 @@
package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"fmt"
"regexp"
@ -10,7 +11,11 @@ import (
)
func GetHome(c *gin.Context) {
utils.Render(c, 200, "home", nil)
theme := utils.GetThemeFromEnv()
c.HTML(200, "home.html", gin.H{
"version": config.Version,
"theme": theme,
})
}
type urlConversionRequest struct {
@ -57,7 +62,7 @@ func PostHome(c *gin.Context) {
body := urlConversionRequest{}
if err := c.ShouldBind(&body); err != nil {
utils.Render(c, 400, "home", gin.H{
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid request body",
})
return
@ -66,8 +71,10 @@ func PostHome(c *gin.Context) {
translated := translateUrl(body.URL)
if translated == "" {
utils.Render(c, 400, "home", gin.H{
theme := utils.GetThemeFromEnv()
c.HTML(400, "home.html", 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,15 +51,16 @@ func GetImage(c *gin.Context) {
}
// download the image
res, err := utils.GET(claims.ImageURL)
client := resty.New()
resp, err := client.R().Get(claims.ImageURL)
if err != nil {
c.AbortWithStatus(500)
return
}
// set the content type
c.Header("Content-Type", res.Header().Get("Content-Type"))
c.Header("Content-Type", resp.Header().Get("Content-Type"))
// write the image to the response
c.Writer.Write(res.Body())
c.Writer.Write(resp.Body())
}

View File

@ -1,6 +1,7 @@
package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"fmt"
@ -13,16 +14,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)
utils.Render(c, 200, "home", gin.H{
theme := utils.GetThemeFromEnv()
c.HTML(200, "home.html", gin.H{
"successMessage": "Images are now " + text,
"version": config.Version,
"theme": theme,
})
default:
c.String(400, "400 Bad Request")
}

View File

@ -3,7 +3,6 @@ package routes
import (
"anonymousoverflow/config"
"anonymousoverflow/src/utils"
"bytes"
"fmt"
"html"
"html/template"
@ -16,6 +15,7 @@ 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 {
utils.Render(c, 400, "home", gin.H{
c.HTML(400, "home.html", 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)
if err != nil {
fmt.Printf("failed to get %s: %s", soLink, err.Error())
resp, err := fetchQuestionData(soLink)
utils.Render(c, 500, "home", gin.H{
"errorMessage": fmt.Sprintf("Request to server failed"),
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,
})
return
}
if res.StatusCode() != 200 {
utils.Render(c, 500, "home", gin.H{
"errorMessage": fmt.Sprintf("Received a non-OK status code: %d", res.StatusCode()),
})
return
}
respBody := resp.String()
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(res.Body()))
respBodyReader := strings.NewReader(respBody)
doc, err := goquery.NewDocumentFromReader(respBodyReader)
if err != nil {
utils.Render(c, 500, "home", gin.H{
c.HTML(500, "home.html", gin.H{
"errorMessage": "Unable to parse question data",
"version": config.Version,
})
return
}
newFilteredQuestion, err := extractQuestionData(doc, domain)
if err != nil {
utils.Render(c, 500, "home", gin.H{
c.HTML(500, "home.html", gin.H{
"errorMessage": "Failed to extract question data",
"version": config.Version,
})
return
}
answers, err := extractAnswersData(doc, domain)
if err != nil {
utils.Render(c, 500, "home", gin.H{
c.HTML(500, "home.html", gin.H{
"errorMessage": "Failed to extract answer data",
"version": config.Version,
})
return
}
@ -101,14 +101,18 @@ func ViewQuestion(c *gin.Context) {
imagePolicy = "'self'"
}
utils.Render(c, 200, "question", gin.H{
theme := utils.GetThemeFromEnv()
c.HTML(200, "question.html", 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 {
@ -123,8 +127,9 @@ func parseAndValidateParameters(c *gin.Context) (inputs viewQuestionInputs, err
questionId := c.Param("id")
if _, err = strconv.Atoi(questionId); err != nil {
utils.Render(c, 400, "home", gin.H{
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid question ID",
"version": config.Version,
})
return
}
@ -150,6 +155,13 @@ 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,7 +1,6 @@
package routes
import (
"anonymousoverflow/src/utils"
"fmt"
"net/http"
"os"
@ -32,14 +31,14 @@ func RedirectShortenedOverflowURL(c *gin.Context) {
}
resp, err := client.R().Get(fmt.Sprintf("https://%s/a/%s/%s", domain, id, answerId))
if err != nil {
utils.Render(c, 400, "home", gin.H{
c.HTML(400, "home.html", gin.H{
"errorMessage": "Unable to fetch stack overflow URL",
})
return
}
if resp.StatusCode() != 302 {
utils.Render(c, 400, "home", gin.H{
c.HTML(400, "home.html", 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)
})
}
}

View File

@ -1,24 +0,0 @@
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)
}

View File

@ -1,150 +0,0 @@
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")
}

View File

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