Compare commits
14 Commits
de880da152
...
9dd951e0af
Author | SHA1 | Date | |
---|---|---|---|
9dd951e0af | |||
d133b45575 | |||
c9e2cf1a44 | |||
a04cfc2fc3 | |||
801ec1badc | |||
ef63d59931 | |||
a1b89e543c | |||
c9d498d1f4 | |||
48288da414 | |||
2a7121d85c | |||
2f160a8649 | |||
4e8606c44a | |||
ed37cc6af6 | |||
36a21ed859 |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
.git
|
||||
|
||||
ups.json
|
||||
renovate.json
|
||||
|
||||
README.md
|
||||
LICENSE.txt
|
||||
|
||||
docker-compose.example.yml
|
||||
Dockerfile
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.2 AS build
|
||||
FROM golang:1.24.3 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
# [ngn.tf] | AnonymousOverflow
|
||||
# AnonymousOverflow | frontend for stackoverflow.com
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
2
main.go
2
main.go
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
24
src/utils/render.go
Normal file
24
src/utils/render.go
Normal 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
150
src/utils/request.go
Normal 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")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user