diff --git a/main/distro/all/all.go b/main/distro/all/all.go index c16a5064..c522ef5c 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -29,6 +29,7 @@ import ( _ "v2ray.com/core/proxy/mtproto" _ "v2ray.com/core/proxy/shadowsocks" _ "v2ray.com/core/proxy/socks" + _ "v2ray.com/core/proxy/speedtest" _ "v2ray.com/core/proxy/vmess/inbound" _ "v2ray.com/core/proxy/vmess/outbound" diff --git a/proxy/speedtest/config.pb.go b/proxy/speedtest/config.pb.go new file mode 100644 index 00000000..8f1c2057 --- /dev/null +++ b/proxy/speedtest/config.pb.go @@ -0,0 +1,67 @@ +package speedtest + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Config struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Config) Reset() { *m = Config{} } +func (m *Config) String() string { return proto.CompactTextString(m) } +func (*Config) ProtoMessage() {} +func (*Config) Descriptor() ([]byte, []int) { + return fileDescriptor_config_a46c59b79195c0b1, []int{0} +} +func (m *Config) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Config.Unmarshal(m, b) +} +func (m *Config) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Config.Marshal(b, m, deterministic) +} +func (dst *Config) XXX_Merge(src proto.Message) { + xxx_messageInfo_Config.Merge(dst, src) +} +func (m *Config) XXX_Size() int { + return xxx_messageInfo_Config.Size(m) +} +func (m *Config) XXX_DiscardUnknown() { + xxx_messageInfo_Config.DiscardUnknown(m) +} + +var xxx_messageInfo_Config proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Config)(nil), "v2ray.core.proxy.speedtest.Config") +} + +func init() { + proto.RegisterFile("v2ray.com/core/proxy/speedtest/config.proto", fileDescriptor_config_a46c59b79195c0b1) +} + +var fileDescriptor_config_a46c59b79195c0b1 = []byte{ + // 131 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x2e, 0x33, 0x2a, 0x4a, + 0xac, 0xd4, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0x2f, 0x4a, 0xd5, 0x2f, 0x28, 0xca, 0xaf, 0xa8, + 0xd4, 0x2f, 0x2e, 0x48, 0x4d, 0x4d, 0x29, 0x49, 0x2d, 0x2e, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, + 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, 0x82, 0x29, 0x2e, 0x4a, 0xd5, 0x03, 0x2b, + 0xd4, 0x83, 0x2b, 0x54, 0xe2, 0xe0, 0x62, 0x73, 0x06, 0xab, 0x75, 0xf2, 0xe2, 0x92, 0x4b, 0xce, + 0xcf, 0xd5, 0xc3, 0xad, 0x36, 0x80, 0x31, 0x8a, 0x13, 0xce, 0x59, 0xc5, 0x24, 0x15, 0x66, 0x14, + 0x94, 0x58, 0xa9, 0xe7, 0x0c, 0x52, 0x19, 0x00, 0x56, 0x19, 0x0c, 0x92, 0x0c, 0x49, 0x2d, 0x2e, + 0x49, 0x62, 0x03, 0x5b, 0x6c, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x07, 0x81, 0xb2, 0x36, 0xa7, + 0x00, 0x00, 0x00, +} diff --git a/proxy/speedtest/config.proto b/proxy/speedtest/config.proto new file mode 100644 index 00000000..ba2d30ab --- /dev/null +++ b/proxy/speedtest/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package v2ray.core.proxy.speedtest; +option csharp_namespace = "V2Ray.Core.Proxy.SpeedTest"; +option go_package = "speedtest"; +option java_package = "com.v2ray.core.proxy.speedtest"; +option java_multiple_files = true; + +message Config {} diff --git a/proxy/speedtest/errors.generated.go b/proxy/speedtest/errors.generated.go new file mode 100644 index 00000000..92d41dff --- /dev/null +++ b/proxy/speedtest/errors.generated.go @@ -0,0 +1,7 @@ +package speedtest + +import "v2ray.com/core/common/errors" + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).Path("Proxy", "SpeedTest") +} diff --git a/proxy/speedtest/speedtest.go b/proxy/speedtest/speedtest.go new file mode 100644 index 00000000..8093a29d --- /dev/null +++ b/proxy/speedtest/speedtest.go @@ -0,0 +1,181 @@ +package speedtest + +//go:generate go run $GOPATH/src/v2ray.com/core/common/errors/errorgen/main.go -pkg speedtest -path Proxy,SpeedTest + +import ( + "bufio" + "context" + "io" + "net/http" + "strconv" + "strings" + + "v2ray.com/core" + "v2ray.com/core/common" + "v2ray.com/core/common/buf" + "v2ray.com/core/common/serial" + "v2ray.com/core/common/session" + "v2ray.com/core/proxy" +) + +var rndBytes = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\\'()*+,-") + +type rndBytesReader struct{} + +func (rndBytesReader) Read(b []byte) (int, error) { + totalBytes := 0 + for totalBytes < len(b) { + nBytes := copy(b[totalBytes:], rndBytes[:]) + totalBytes += nBytes + } + return totalBytes, nil +} + +func (rndBytesReader) Close() error { + return nil +} + +type SpeedTestHandler struct{} + +func New(ctx context.Context, config *Config) (*SpeedTestHandler, error) { + return &SpeedTestHandler{}, nil +} + +type noOpCloser struct { + io.Reader +} + +func (c *noOpCloser) Close() error { + return nil +} + +func defaultResponse() *http.Response { + response := &http.Response{ + Status: "Not Found", + StatusCode: 404, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + response.Header.Set("Content-Type", "text/plain; charset=UTF-8") + return response +} + +func (h *SpeedTestHandler) Process(ctx context.Context, link *core.Link, dialer proxy.Dialer) error { + reader := link.Reader + writer := link.Writer + + defer func() { + common.Close(writer) + }() + + bufReader := bufio.NewReader(&buf.BufferedReader{ + Reader: reader, + Direct: true, + }) + + bufWriter := buf.NewBufferedWriter(writer) + common.Must(bufWriter.SetBuffered(false)) + + request, err := http.ReadRequest(bufReader) + if err != nil { + return newError("failed to read speedtest request").Base(err) + } + + path := strings.ToLower(request.URL.Path) + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + + switch path { + case "hello": + respBody := "hello 2.5 2017-08-15.1314.4ae12d5" + response := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: &noOpCloser{strings.NewReader(respBody)}, + ContentLength: int64(len(respBody)), + Close: true, + } + response.Header.Set("Content-Type", "text/plain; charset=UTF-8") + return response.Write(bufWriter) + case "upload": + switch strings.ToUpper(request.Method) { + case "POST": + var sc buf.SizeCounter + buf.Copy(buf.NewReader(request.Body), buf.Discard, buf.CountSize(&sc)) // nolint: errcheck + + response := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: &noOpCloser{strings.NewReader(serial.Concat("size=", sc.Size))}, + Close: true, + } + response.Header.Set("Content-Type", "text/plain; charset=UTF-8") + return response.Write(bufWriter) + case "OPTIONS": + response := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + + response.Header.Set("Content-Type", "text/plain; charset=UTF-8") + response.Header.Set("Connection", "Close") + response.Header.Set("Access-Control-Allow-Methods", "OPTIONS, POST") + response.Header.Set("Access-Control-Allow-Headers", "content-type") + response.Header.Set("Access-Control-Allow-Origin", "http://www.speedtest.net") + return response.Write(bufWriter) + default: + newError("unknown method for upload: ", request.Method).WriteToLog(session.ExportIDToError(ctx)) + return defaultResponse().Write(bufWriter) + } + case "download": + query := request.URL.Query() + sizeStr := query.Get("size") + size, err := strconv.Atoi(sizeStr) + if err != nil { + return defaultResponse().Write(bufWriter) + } + response := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: rndBytesReader{}, + ContentLength: int64(size), + Close: true, + } + response.Header.Set("Content-Type", "text/plain; charset=UTF-8") + return response.Write(bufWriter) + default: + newError("unknown path: ", path).WriteToLog(session.ExportIDToError(ctx)) + return defaultResponse().Write(bufWriter) + } +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +}