package main import ( "bytes" "crypto/hmac" "crypto/sha256" _ "embed" "encoding/base64" "encoding/hex" "errors" "flag" "fmt" "html/template" "io" "log" "mime" "net/url" "os" "path/filepath" "regexp" "strings" "time" "unicode/utf8" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpproxy" "golang.org/x/net/html" "golang.org/x/net/html/charset" "golang.org/x/text/encoding" "marisa.chaotic.ninja/yukari" "marisa.chaotic.ninja/yukari/config" "marisa.chaotic.ninja/yukari/contenttype" ) const ( STATE_DEFAULT int = 0 STATE_IN_STYLE int = 1 STATE_IN_NOSCRIPT int = 2 ) const MaxRedirectCount = 5 var Gap *fasthttp.Client = &fasthttp.Client{ MaxResponseBodySize: 10 * 1024 * 1024, // 10M ReadBufferSize: 16 * 1024, // 16K } var cssURLRegex *regexp.Regexp = regexp.MustCompile("url\\((['\"]?)[ \\t\\f]*([\u0009\u0021\u0023-\u0026\u0028\u002a-\u007E]+)(['\"]?)\\)?") type Proxy struct { Key []byte RequestTimeout time.Duration FollowRedirect bool } type RequestConfig struct { Key []byte BaseURL *url.URL BodyInjected bool } type HTMLBodyExtParam struct { BaseURL string HasYukariKey bool URLParamName string } type HTMLFormExtParam struct { BaseURL string YukariHash string URLParamName string HashParamName string } type HTMLMainPageFormParam struct { URLParamName string } var htmlFormExtension *template.Template var htmlBodyExtension *template.Template var htmlMainPageForm *template.Template //go:embed templates/yukari_content_type.html var htmlHeadContentType string //go:embed templates/yukari_start.html var htmlPageStart string //go:embed templates/yukari_stop.html var htmlPageStop string //go:embed favicon.ico var faviconBytes []byte func init() { var err error htmlFormExtension, err = template.New("html_form_extension").Parse( `{{if .YukariHash}}{{end}}`) if err != nil { panic(err) } htmlBodyExtension, err = template.New("html_body_extension").Parse(`
Yukari's Gap
`) if err != nil { panic(err) } htmlMainPageForm, err = template.New("html_main_page_form").Parse(`
Visit url:
`) if err != nil { panic(err) } } func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) { if appRequestHandler(ctx) { return } requestHash := popRequestParam(ctx, []byte(config.Config.HashParameter)) requestURI := popRequestParam(ctx, []byte(config.Config.UrlParameter)) if requestURI == nil { p.serveMainPage(ctx, 200, nil) return } if p.Key != nil { if !verifyRequestURI(requestURI, requestHash, p.Key) { // HTTP status code 403 : Forbidden error_message := fmt.Sprintf(`invalid "%s" parameter. hint: Hash URL Parameter`, config.Config.HashParameter) p.serveMainPage(ctx, 403, errors.New(error_message)) return } } requestURIQuery := ctx.QueryArgs().QueryString() if len(requestURIQuery) > 0 { if bytes.ContainsRune(requestURI, '?') { requestURI = append(requestURI, '&') } else { requestURI = append(requestURI, '?') } requestURI = append(requestURI, requestURIQuery...) } p.ProcessUri(ctx, string(requestURI), 0) } func (p *Proxy) ProcessUri(ctx *fasthttp.RequestCtx, requestURIStr string, redirectCount int) { parsedURI, err := url.Parse(requestURIStr) if err != nil { // HTTP status code 500 : Internal Server Error p.serveMainPage(ctx, 500, err) return } if parsedURI.Scheme == "" { requestURIStr = "https://" + requestURIStr parsedURI, err = url.Parse(requestURIStr) if err != nil { p.serveMainPage(ctx, 500, err) return } } // Serve an intermediate page for protocols other than HTTP(S) if (parsedURI.Scheme != "http" && parsedURI.Scheme != "https") || strings.HasSuffix(parsedURI.Host, ".onion") || strings.HasSuffix(parsedURI.Host, ".i2p") { p.serveExitYukariPage(ctx, parsedURI) return } req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) req.SetConnectionClose() if config.Config.Debug { log.Println(string(ctx.Method()), requestURIStr) } req.SetRequestURI(requestURIStr) req.Header.SetUserAgentBytes([]byte("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:112.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36")) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) req.Header.SetMethodBytes(ctx.Method()) if ctx.IsPost() || ctx.IsPut() { req.SetBody(ctx.PostBody()) } err = Gap.DoTimeout(req, resp, p.RequestTimeout) if err != nil { if err == fasthttp.ErrTimeout { // HTTP status code 504 : Gateway Time-Out p.serveMainPage(ctx, 504, err) } else { // HTTP status code 500 : Internal Server Error p.serveMainPage(ctx, 500, err) } return } if resp.StatusCode() != 200 { switch resp.StatusCode() { case 301, 302, 303, 307, 308: loc := resp.Header.Peek("Location") if loc != nil { if p.FollowRedirect && ctx.IsGet() { // GET method: Yukari follows the redirect if redirectCount < MaxRedirectCount { if config.Config.Debug { log.Println("follow redirect to", string(loc)) } p.ProcessUri(ctx, string(loc), redirectCount+1) } else { p.serveMainPage(ctx, 310, errors.New("Too many redirects")) } return } else { // Other HTTP methods: Yukari does NOT follow the redirect rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI} url, err := rc.ProxifyURI(loc) if err == nil { ctx.SetStatusCode(resp.StatusCode()) ctx.Response.Header.Add("Location", url) if config.Config.Debug { log.Println("redirect to", string(loc)) } return } } } } error_message := fmt.Sprintf("invalid response: %d (%s)", resp.StatusCode(), requestURIStr) p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message)) return } contentTypeBytes := resp.Header.Peek("Content-Type") if contentTypeBytes == nil { // HTTP status code 503 : Service Unavailable p.serveMainPage(ctx, 503, errors.New("invalid content type")) return } contentTypeString := string(contentTypeBytes) // decode Content-Type header contentType, error := contenttype.ParseContentType(contentTypeString) if error != nil { // HTTP status code 503 : Service Unavailable p.serveMainPage(ctx, 503, errors.New("invalid content type")) return } // content-disposition contentDispositionBytes := ctx.Request.Header.Peek("Content-Disposition") // check content type if !ALLOWED_CONTENTTYPE_FILTER(contentType) { // it is not a usual content type if ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER(contentType) { // force attachment for allowed content type contentDispositionBytes = contentDispositionForceAttachment(contentDispositionBytes, parsedURI) } else { // deny access to forbidden content type // HTTP status code 403 : Forbidden p.serveMainPage(ctx, 403, errors.New("forbidden content type "+parsedURI.String())) return } } // HACK : replace */xhtml by text/html if contentType.SubType == "xhtml" { contentType.TopLevelType = "text" contentType.SubType = "html" contentType.Suffix = "" } // conversion to UTF-8 var responseBody []byte if contentType.TopLevelType == "text" { e, ename, _ := charset.DetermineEncoding(resp.Body(), contentTypeString) if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) { responseBody, err = e.NewDecoder().Bytes(resp.Body()) if err != nil { // HTTP status code 503 : Service Unavailable p.serveMainPage(ctx, 503, err) return } } else { responseBody = resp.Body() } // update the charset or specify it contentType.Parameters["charset"] = "UTF-8" } else { responseBody = resp.Body() } // contentType.FilterParameters(ALLOWED_CONTENTTYPE_PARAMETERS) // set the content type ctx.SetContentType(contentType.String()) // output according to MIME type switch { case contentType.SubType == "css" && contentType.Suffix == "": sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody) case contentType.SubType == "html" && contentType.Suffix == "": rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI} sanitizeHTML(rc, ctx, responseBody) if !rc.BodyInjected { p := HTMLBodyExtParam{rc.BaseURL.String(), false, config.Config.UrlParameter} if len(rc.Key) > 0 { p.HasYukariKey = true } err := htmlBodyExtension.Execute(ctx, p) if err != nil { if config.Config.Debug { fmt.Println("failed to inject body extension", err) } } } default: if contentDispositionBytes != nil { ctx.Response.Header.AddBytesV("Content-Disposition", contentDispositionBytes) } ctx.Write(responseBody) } } // force content-disposition to attachment func contentDispositionForceAttachment(contentDispositionBytes []byte, url *url.URL) []byte { var contentDispositionParams map[string]string if contentDispositionBytes != nil { var err error _, contentDispositionParams, err = mime.ParseMediaType(string(contentDispositionBytes)) if err != nil { contentDispositionParams = make(map[string]string) } } else { contentDispositionParams = make(map[string]string) } _, fileNameDefined := contentDispositionParams["filename"] if !fileNameDefined { // TODO : sanitize filename contentDispositionParams["fileName"] = filepath.Base(url.Path) } return []byte(mime.FormatMediaType("attachment", contentDispositionParams)) } func appRequestHandler(ctx *fasthttp.RequestCtx) bool { // serve robots.txt if bytes.Equal(ctx.Path(), []byte("/robots.txt")) { ctx.SetContentType("text/plain") ctx.Write([]byte("User-Agent: *\nDisallow: /\n")) return true } // server favicon.ico if bytes.Equal(ctx.Path(), []byte("/favicon.ico")) { ctx.SetContentType("image/vnd.microsoft.icon") ctx.Write(faviconBytes) return true } return false } func popRequestParam(ctx *fasthttp.RequestCtx, paramName []byte) []byte { param := ctx.QueryArgs().PeekBytes(paramName) if param == nil { param = ctx.PostArgs().PeekBytes(paramName) ctx.PostArgs().DelBytes(paramName) } ctx.QueryArgs().DelBytes(paramName) return param } func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) { // TODO urlSlices := cssURLRegex.FindAllSubmatchIndex(css, -1) if urlSlices == nil { out.Write(css) return } startIndex := 0 for _, s := range urlSlices { urlStart := s[4] urlEnd := s[5] if uri, err := rc.ProxifyURI(css[urlStart:urlEnd]); err == nil { out.Write(css[startIndex:urlStart]) out.Write([]byte(uri)) startIndex = urlEnd } else if config.Config.Debug { log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd])) } } if startIndex < len(css) { out.Write(css[startIndex:len(css)]) } } func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) { r := bytes.NewReader(htmlDoc) decoder := html.NewTokenizer(r) decoder.AllowCDATA(true) unsafeElements := make([][]byte, 0, 8) state := STATE_DEFAULT for { token := decoder.Next() if token == html.ErrorToken { err := decoder.Err() if err != io.EOF { log.Println("failed to parse HTML") } break } if len(unsafeElements) == 0 { switch token { case html.StartTagToken, html.SelfClosingTagToken: tag, hasAttrs := decoder.TagName() safe := !inArray(tag, UNSAFE_ELEMENTS) if !safe { if token != html.SelfClosingTagToken { var unsafeTag []byte = make([]byte, len(tag)) copy(unsafeTag, tag) unsafeElements = append(unsafeElements, unsafeTag) } break } if bytes.Equal(tag, []byte("base")) { for { attrName, attrValue, moreAttr := decoder.TagAttr() if bytes.Equal(attrName, []byte("href")) { parsedURI, err := url.Parse(string(attrValue)) if err == nil { rc.BaseURL = parsedURI } } if !moreAttr { break } } break } if bytes.Equal(tag, []byte("noscript")) { state = STATE_IN_NOSCRIPT break } var attrs [][][]byte if hasAttrs { for { attrName, attrValue, moreAttr := decoder.TagAttr() attrs = append(attrs, [][]byte{ attrName, attrValue, []byte(html.EscapeString(string(attrValue))), }) if !moreAttr { break } } } if bytes.Equal(tag, []byte("link")) { sanitizeLinkTag(rc, out, attrs) break } if bytes.Equal(tag, []byte("meta")) { sanitizeMetaTag(rc, out, attrs) break } fmt.Fprintf(out, "<%s", tag) if hasAttrs { sanitizeAttrs(rc, out, attrs) } if token == html.SelfClosingTagToken { fmt.Fprintf(out, " />") } else { fmt.Fprintf(out, ">") if bytes.Equal(tag, []byte("style")) { state = STATE_IN_STYLE } } if bytes.Equal(tag, []byte("head")) { fmt.Fprintf(out, htmlHeadContentType) } if bytes.Equal(tag, []byte("form")) { var formURL *url.URL for _, attr := range attrs { if bytes.Equal(attr[0], []byte("action")) { formURL, _ = url.Parse(string(attr[1])) formURL = mergeURIs(rc.BaseURL, formURL) break } } if formURL == nil { formURL = rc.BaseURL } urlStr := formURL.String() var key string if rc.Key != nil { key = hash(urlStr, rc.Key) } err := htmlFormExtension.Execute(out, HTMLFormExtParam{urlStr, key, config.Config.UrlParameter, config.Config.HashParameter}) if err != nil { if config.Config.Debug { fmt.Println("failed to inject body extension", err) } } } case html.EndTagToken: tag, _ := decoder.TagName() writeEndTag := true switch string(tag) { case "body": p := HTMLBodyExtParam{rc.BaseURL.String(), false, config.Config.UrlParameter} if len(rc.Key) > 0 { p.HasYukariKey = true } err := htmlBodyExtension.Execute(out, p) if err != nil { if config.Config.Debug { fmt.Println("failed to inject body extension", err) } } rc.BodyInjected = true case "style": state = STATE_DEFAULT case "noscript": state = STATE_DEFAULT writeEndTag = false } // skip noscript tags - only the tag, not the content, because javascript is sanitized if writeEndTag { fmt.Fprintf(out, "", tag) } case html.TextToken: switch state { case STATE_DEFAULT: fmt.Fprintf(out, "%s", decoder.Raw()) case STATE_IN_STYLE: sanitizeCSS(rc, out, decoder.Raw()) case STATE_IN_NOSCRIPT: sanitizeHTML(rc, out, decoder.Raw()) } case html.CommentToken: // ignore comment. TODO : parse IE conditional comment case html.DoctypeToken: out.Write(decoder.Raw()) } } else { switch token { case html.StartTagToken, html.SelfClosingTagToken: tag, _ := decoder.TagName() if inArray(tag, UNSAFE_ELEMENTS) { unsafeElements = append(unsafeElements, tag) } case html.EndTagToken: tag, _ := decoder.TagName() if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) { unsafeElements = unsafeElements[:len(unsafeElements)-1] } } } } } func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) { exclude := false for _, attr := range attrs { attrName := attr[0] attrValue := attr[1] if bytes.Equal(attrName, []byte("rel")) { if !inArray(attrValue, LINK_REL_SAFE_VALUES) { exclude = true break } } if bytes.Equal(attrName, []byte("as")) { if bytes.Equal(attrValue, []byte("script")) { exclude = true break } } } if !exclude { out.Write([]byte("")) } } func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) { var http_equiv []byte var content []byte for _, attr := range attrs { attrName := attr[0] attrValue := attr[1] if bytes.Equal(attrName, []byte("http-equiv")) { http_equiv = bytes.ToLower(attrValue) // exclude some if !inArray(http_equiv, LINK_HTTP_EQUIV_SAFE_VALUES) { return } } if bytes.Equal(attrName, []byte("content")) { content = attrValue } if bytes.Equal(attrName, []byte("charset")) { // exclude return } } out.Write([]byte(" if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) { if contentUrl[0] == contentUrl[len(contentUrl)-1] { contentUrl = contentUrl[1 : len(contentUrl)-1] } } // output proxify result if uri, err := rc.ProxifyURI(contentUrl); err == nil { fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri) } } else { if len(http_equiv) > 0 { fmt.Fprintf(out, ` http-equiv="%s"`, http_equiv) } sanitizeAttrs(rc, out, attrs) } out.Write([]byte(">")) } func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) { for _, attr := range attrs { sanitizeAttr(rc, out, attr[0], attr[1], attr[2]) } } func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) { if inArray(attrName, SAFE_ATTRIBUTES) { fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue) return } switch string(attrName) { case "src", "href", "action": if uri, err := rc.ProxifyURI(attrValue); err == nil { fmt.Fprintf(out, " %s=\"%s\"", attrName, uri) } else if config.Config.Debug { log.Println("cannot proxify uri:", string(attrValue)) } case "style": cssAttr := bytes.NewBuffer(nil) sanitizeCSS(rc, cssAttr, attrValue) fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes()))) } } func mergeURIs(u1, u2 *url.URL) *url.URL { if u2 == nil { return u1 } return u1.ResolveReference(u2) } // Sanitized URI : removes all runes bellow 32 (included) as the begining and end of URI, and lower case the scheme. // avoid memory allocation (except for the scheme) func sanitizeURI(uri []byte) ([]byte, string) { first_rune_index := 0 first_rune_seen := false scheme_last_index := -1 buffer := bytes.NewBuffer(make([]byte, 0, 10)) // remove trailing space and special characters uri = bytes.TrimRight(uri, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x20") // loop over byte by byte for i, c := range uri { // ignore special characters and space (c <= 32) if c > 32 { // append to the lower case of the rune to buffer if c < utf8.RuneSelf && 'A' <= c && c <= 'Z' { c = c + 'a' - 'A' } buffer.WriteByte(c) // update the first rune index that is not a special rune if !first_rune_seen { first_rune_index = i first_rune_seen = true } if c == ':' { // colon rune found, we have found the scheme scheme_last_index = i break } else if c == '/' || c == '?' || c == '\\' || c == '#' { // special case : most probably a relative URI break } } } if scheme_last_index != -1 { // scheme found // copy the "lower case without special runes scheme" before the ":" rune scheme_start_index := scheme_last_index - buffer.Len() + 1 copy(uri[scheme_start_index:], buffer.Bytes()) // and return the result return uri[scheme_start_index:], buffer.String() } else { // scheme NOT found return uri[first_rune_index:], "" } } func (rc *RequestConfig) ProxifyURI(uri []byte) (string, error) { // sanitize URI uri, scheme := sanitizeURI(uri) // remove javascript protocol if scheme == "javascript:" { return "", nil } // TODO check malicious data: - e.g. data:script if scheme == "data:" { if bytes.HasPrefix(uri, []byte("data:image/png")) || bytes.HasPrefix(uri, []byte("data:image/jpeg")) || bytes.HasPrefix(uri, []byte("data:image/pjpeg")) || bytes.HasPrefix(uri, []byte("data:image/gif")) || bytes.HasPrefix(uri, []byte("data:image/webp")) { // should be safe return string(uri), nil } else { // unsafe data return "", nil } } // parse the uri u, err := url.Parse(string(uri)) if err != nil { return "", err } // get the fragment (with the prefix "#") fragment := "" if len(u.Fragment) > 0 { fragment = "#" + u.Fragment } // reset the fragment: it is not included in the yukariurl u.Fragment = "" // merge the URI with the document URI u = mergeURIs(rc.BaseURL, u) // simple internal link ? // some web pages describe the whole link https://same:auth@same.host/same.path?same.query#new.fragment if u.Scheme == rc.BaseURL.Scheme && (rc.BaseURL.User == nil || (u.User != nil && u.User.String() == rc.BaseURL.User.String())) && u.Host == rc.BaseURL.Host && u.Path == rc.BaseURL.Path && u.RawQuery == rc.BaseURL.RawQuery { // the fragment is the only difference between the document URI and the uri parameter return fragment, nil } // return full URI and fragment (if not empty) yukari_uri := u.String() if rc.Key == nil { return fmt.Sprintf("./?%s=%s%s", config.Config.UrlParameter, url.QueryEscape(yukari_uri), fragment), nil } return fmt.Sprintf("./?%s=%s&%s=%s%s", config.Config.HashParameter, hash(yukari_uri, rc.Key), config.Config.UrlParameter, url.QueryEscape(yukari_uri), fragment), nil } func inArray(b []byte, a [][]byte) bool { for _, b2 := range a { if bytes.Equal(b, b2) { return true } } return false } func hash(msg string, key []byte) string { mac := hmac.New(sha256.New, key) mac.Write([]byte(msg)) return hex.EncodeToString(mac.Sum(nil)) } func verifyRequestURI(uri, hashMsg, key []byte) bool { h := make([]byte, hex.DecodedLen(len(hashMsg))) _, err := hex.Decode(h, hashMsg) if err != nil { if config.Config.Debug { log.Println("hmac error:", err) } return false } mac := hmac.New(sha256.New, key) mac.Write(uri) return hmac.Equal(h, mac.Sum(nil)) } func (p *Proxy) serveExitYukariPage(ctx *fasthttp.RequestCtx, uri *url.URL) { ctx.SetContentType("text/html") ctx.SetStatusCode(403) ctx.Write([]byte(htmlPageStart)) ctx.Write([]byte("

You are about to exit Yukari no Sukima

")) ctx.Write([]byte("

Following

")) ctx.Write([]byte(html.EscapeString(uri.String()))) ctx.Write([]byte("

the content of this URL will be NOT sanitized.

")) ctx.Write([]byte(htmlPageStop)) } func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) { ctx.SetContentType("text/html; charset=UTF-8") ctx.SetStatusCode(statusCode) ctx.Write([]byte(htmlPageStart)) if err != nil { if config.Config.Debug { log.Println("error:", err) } ctx.Write([]byte("

Error: ")) ctx.Write([]byte(html.EscapeString(err.Error()))) ctx.Write([]byte("

")) } if p.Key == nil { p := HTMLMainPageFormParam{config.Config.UrlParameter} err := htmlMainPageForm.Execute(ctx, p) if err != nil { if config.Config.Debug { fmt.Println("failed to inject main page form", err) } } } else { ctx.Write([]byte(`

Warning! This instance does not support direct URL opening.

`)) } ctx.Write([]byte(htmlPageStop)) } func main() { config.Config.ListenAddress = "127.0.0.1:3000" config.Config.Key = "" config.Config.IPV6 = true config.Config.Debug = false config.Config.RequestTimeout = 5 config.Config.FollowRedirect = false config.Config.UrlParameter = "yukariurl" config.Config.HashParameter = "yukarihash" config.Config.MaxConnsPerHost = 5 config.Config.ProxyEnv = false var configFile string var proxy string var socks5 string var version bool flag.StringVar(&configFile, "f", "", "Configuration file") flag.StringVar(&proxy, "proxy", "", "Use the specified HTTP proxy (ie: '[user:pass@]hostname:port'). Overrides: -socks5, IPv6") flag.StringVar(&socks5, "socks5", "", "Use a SOCKS5 proxy (ie: 'hostname:port'). Overrides: IPv6.") flag.BoolVar(&version, "version", false, "Show version") flag.Parse() if configFile != "" { config.ReadConfig(configFile) } if version { yukari.FullVersion() return } if config.Config.ProxyEnv && os.Getenv("HTTP_PROXY") == "" && os.Getenv("HTTPS_PROXY") == "" { log.Fatal("Error -proxyenv is used but no environment variables named 'HTTP_PROXY' and/or 'HTTPS_PROXY' could be found.") os.Exit(1) } if config.Config.ProxyEnv { config.Config.IPV6 = false Gap.Dial = fasthttpproxy.FasthttpProxyHTTPDialer() log.Println("Using environment defined proxy(ies).") } else if proxy != "" { config.Config.IPV6 = false Gap.Dial = fasthttpproxy.FasthttpHTTPDialer(proxy) log.Println("Using custom HTTP proxy.") } else if socks5 != "" { config.Config.IPV6 = false Gap.Dial = fasthttpproxy.FasthttpSocksDialer(socks5) log.Println("Using Socks5 proxy.") } else if config.Config.IPV6 { Gap.Dial = fasthttp.DialDualStack log.Println("Using dual stack (IPv4/IPv6) direct connections.") } else { Gap.Dial = fasthttp.Dial log.Println("Using IPv4 only direct connections.") } p := &Proxy{RequestTimeout: time.Duration(config.Config.RequestTimeout) * time.Second, FollowRedirect: config.Config.FollowRedirect} if config.Config.Key != "" { var err error p.Key, err = base64.StdEncoding.DecodeString(config.Config.Key) if err != nil { log.Fatal("Error parsing -key", err.Error()) os.Exit(1) } } log.Println("ゆかり様、お願いします…!") log.Println("Listening on", config.Config.ListenAddress) if err := fasthttp.ListenAndServe(config.Config.ListenAddress, p.RequestHandler); err != nil { log.Fatal("Error in ListenAndServe:", err) } }