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(`
`)
if err != nil {
panic(err)
}
htmlMainPageForm, err = template.New("html_main_page_form").Parse(`
`)
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, "%s>", 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)
}
}